MERN 스택으로 To-Do 앱을 만드는 단계

20988 단어

필요한 도구



Node와 NPM이 컴퓨터에 설치되어 있는지 확인하십시오. nodejs.org에서 둘 다 다운로드할 수 있습니다(NPM은 Node 설치에 포함됨).

기술 스택



Node.js
Express.js
MongoDB
React.js

Node.js의 종속성



body-parser
mongoose
mongoose-auto-increment

노드(Express) 백엔드 생성



먼저 to-do-node(예:)라는 프로젝트용 폴더를 만듭니다.
그런 다음 코드 편집기에서 해당 폴더를 엽니다.
노드 프로젝트를 만들려면 터미널에서 다음 명령을 실행합니다.
npm init -y
이렇게 하면 모든 앱 스크립트를 추적하고 노드 앱에 필요한 모든 종속성을 관리할 수 있는 package.json 파일이 생성됩니다.

우리는 Express를 사용하여 포트 3000에서 실행되는 간단한 웹 서버를 만들 것입니다.

이제 index.js라는 이름으로 앱이 실행되기 시작하는 색인 ​​파일을 만들어 보겠습니다.

const express = require('express')
const app = express()
const bodyParser = require('body-parser');

//import router
const router = require('./app/index.js');

// body parser
app.use(bodyParser.urlencoded({ limit: '100mb', extended: true }))
app.use(bodyParser.json({ limit: '100mb', extended: true }))

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");

  if ('OPTIONS' === req.method) {
    //respond with 200
    res.send(200);
  }
  else {
    //move on
    next();
  }
});

app.get('/', (req, res) => {
  res.send("incorrect route");
})

//add routes
const base = '/api/v1/';
app.use(base, router);

app.listen(process.env.PORT || 3000, () => console.log('Running on port 3000!'));


그런 다음 터미널에서 필요한 종속성을 설치합니다.
npm I express mongoose body-parser nodemon mongoose-auto-increment
프로젝트 폴더 안에 app라는 폴더를 만들고 index.js 파일을 추가하여 경로를 정의합니다.

const express = require('express');

// Routes Import
const toDo = require("./toDo/index.js");

const router = express.Router();

// Adding Routes
router.use('/to-do', toDo);

module.exports = router;



config라는 폴더 이름을 만들고 거기에 db.js 파일을 추가합니다.
db.js 파일의 구성은 다음과 같습니다.

const mongoose = require('mongoose');
mongoose.connect('mongodb+srv://<mongodb_username>:<cluster_password>@cluster0.nxcni.mongodb.net/toDo?retryWrites=true&w=majority', { useNewUrlParser: true });
module.exports = mongoose.connection;


이제 API를 작성해 봅시다.



toDo라는 폴더를 만들고 그 안에 model.js , index.jsrouter.js 파일을 만듭니다.

index.js




module.exports = require("./router");


model.js




const mongoose = require('mongoose'); 
const autoIncrement = require('mongoose-auto-increment');
const db = require('../config/db.js');

autoIncrement.initialize(db);

const schema = new config.mongoose.Schema({
  toDos: [
    {
      toDo: String,
      tag: String,
      tagColor: String,
      done: Boolean
    }
  ],
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  },
  status: {
    type: Boolean,
    default: true
  }
}, {
  strict: true
});

var toDo = mongoose.model('toDos', schema);
module.exports = toDo;


라우터.js




const config = require('../config/routes.js');
const router = config.express.Router();
const collection = require('./model.js');

// @route GET api/v1/to-do/list
// @desc get users list with pagination
// @access Public
router.get('/list', function (req, res) {
    if(!req.query.id) {
          res.status(200).send({data: []});
        return false;
    }
    getToDosList(req.query.tag ? req.query.tag : '', req.query.id).then(resp => {
          res.status(200).send(resp[0]);
    }, err => {
          res.status(500).send({message: "Something went wrong, please try after sometime"});
    })
});

// @route CREATE api/v1/to-do/add
// @desc add to-do
// @access Public
router.post('/add', function(req, res) {
    if(!req.query.id) {
        collection.create({toDos: [{toDo: req.body.text, done: false, tag: req.body.tag, tagColor: req.body.tagColor}]}, function (err, toDo) {
            if (!err) {
                return res.status(200).json({error: false, data: toDo, message: 'success'})
            } else {
                  return res.status(500).send({error: true, message: 'Error adding to-do'})
            }
        });
    } else {
            let updateData = {
            $push: {
                "toDos": {toDo: req.body.text, done: false, tag: req.body.tag, tagColor: req.body.tagColor}
            }
        };
            updateToDo({_id: req.query.id}, updateData).then(toDo => {
              return res.status(200).json({error: false, data: toDo, message: 'success'})
            }, err => {
            return res.status(500).send({error: true, message: 'Error adding to-do'})
            })
    }
});

// @route UPDATE api/v1/to-do/done
// @desc update toDo status
// @access Public
router.put('/done/:userId/:toDoId', function(req, res) {
    let updateData = {
        $set: {
            "toDos.$.done": req.body.done
        }
    };
    updateToDo({_id: req.params.userId, "toDos._id": req.params.toDoId}, updateData).then((toDo) => {
        return res.status(200).json({error: false, message: 'Updated successfully'})
    }, err => {
        return res.status(500).send({error: true, message: err})
    })
});

// @route UPDATE api/v1/to-do/delete
// @desc delete toDo
// @access Public
router.put('/delete/:userId/:toDoId', function(req, res) {
    let updateData = { "$pull": { "toDos": { "_id": req.params.toDoId } } }
    updateToDo({_id: req.params.userId, "toDos._id": req.params.toDoId}, updateData).then((toDo) => {
        return res.status(200).json({error: false, message: 'Updated successfully'})
    }, err => {
        return res.status(500).send({error: true, message: err})
    })
});

// function to get to-dos list with tag filter
function getToDosList(tag, id) {
    return new Promise(function(resolve, reject) {
        let agg = [
            {
                "$unwind": "$toDos"
            }, {
                "$match": {
                    $or: [{"_id": id}, {"toDos.tag": {$regex: `${tag}.*`, $options: "i" }}]
                }
            }, {
                "$group": {
                    _id: null,
                    data: {$push: "$toDos"}
                }
            }
        ]
        collection.aggregate(agg, function(err, response) {
          if(err) return reject({message: "Something went wrong"})
          if(!response) return reject({message: "Error while getting remitters data"})
          return resolve(response)
        })
    })
}

//function to update to-do
function updateToDo(query, updateData) {
    return new Promise(function(resolve, reject) {
        collection.findOneAndUpdate(query, updateData, {new: true},
            function (err, resp) {
                    if (err) return reject({error: 1, message: "There was a problem while updating data"});
                    return resolve(resp);
            }
        );
    })
}

function getToDos(query) {
    return new Promise(function(resolve, reject) {
        collection.find(query,
            function (err, resp) {
                    if (err) return reject({error: 1, message: "There was a problem while updating data"});
                    return resolve(resp);
            }
        );
    })
}


module.exports = router



마지막으로 터미널에서 nodemon index.js를 실행하여 앱을 실행할 수 있으며 앱이 포트 3000에서 실행되고 있음을 확인할 수 있습니다.

React.js의 종속성



bootstrap
react-bootstrap
react-icons

React 프런트엔드 만들기



백엔드를 생성한 후 프런트엔드로 이동해 보겠습니다.

다른 터미널 탭을 열고 create-react-app을 사용하여 to-do-react라는 이름으로 새 React 프로젝트를 만듭니다(예:).
npx create-react-app to-do-react
그런 다음 모든 종속성이 설치된 React 앱을 갖게 됩니다.

이제 폴더로 이동
cd to-do-react
디렉토리를 src로 변경하고 아래 명령을 실행합니다.
cd srcrm *
이제 아래 명령을 실행하여 index.js 파일을 만듭니다.
touch index.js
이 파일은 public 폴더에 있는 HTML 파일로 앱을 렌더링합니다. 또한 파일 이름components으로 폴더 이름app.js을 만듭니다.
mkdir components && cd components && touch app.js
app.js에는 To-Do 앱이 포함됩니다.

src에서 파일 편집index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
import 'bootstrap/dist/css/bootstrap.min.css';

ReactDOM.render(<App/>, document.getElementById('root'));


이름이 api인 폴더를 만들고 이름이 to-do.js인 파일을 추가하고 해당 파일에 아래와 같이 API 호출을 작성합니다.

import axios from 'axios';

let base = 'http://localhost:3000/api/v1/';

export default function api(url, method='GET', data={}) {
    return new Promise(function(resolve, reject) {
        const requestOptions = {
            url: base + url,
            method: method,
            headers: {
                'Content-Type': 'application/json'
            },
            data
        };
        axios(requestOptions)
        .then(function (response) {
            resolve(response.data);
        })
        .catch(function (error) {
            reject(error);
        });
    });
}

export function AddToDoAPI(data) {
    return new Promise(function(resolve, reject) {
        api(`to-do/add?id=${localStorage.userId ? localStorage.userId : ''}`, 'POST', data)
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            return reject(error.response.data.message);
        })
    })
}

export function GetToDoListAPI(tag='') {
    return new Promise(function(resolve, reject) {
        api(`to-do/list?id=${localStorage.userId ? localStorage.userId : ''}&tag=${tag}`)
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            console.log(error)
            debugger
            return reject(error.response.data.message);
        })
    })
}

export function UpdateToDoAPI(data, toDoId) {
    return new Promise(function(resolve, reject) {
        api(`to-do/done/${localStorage.userId}/${toDoId}`, 'PUT', data)
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            return reject(error.response.data.message);
        })
    })
}

export function DeleteToDoAPI(toDoId) {
    return new Promise(function(resolve, reject) {
        api(`to-do/delete/${localStorage.userId}/${toDoId}`, 'PUT', {})
        .then((resp) => {
            return resolve(resp);
        }, (error) => {
            return reject(error.response.data.message);
        })
    })
}


구성 요소에서 편집app.js:

import React, {Component} from 'react';

// Bootstrap for react
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Button from 'react-bootstrap/Button';
import InputGroup from 'react-bootstrap/InputGroup';
import FormControl from 'react-bootstrap/FormControl';
import ListGroup from 'react-bootstrap/ListGroup';
import Form from 'react-bootstrap/Form'
import Dropdown from 'react-bootstrap/Dropdown'
import DropdownButton from 'react-bootstrap/DropdownButton'
import {AddToDoAPI, GetToDoListAPI, UpdateToDoAPI, DeleteToDoAPI} from '../api/to-do'
import { BsStop, BsX } from 'react-icons/bs';
import {Badge} from "react-bootstrap";

class AppComponent extends Component {
    constructor(props) {
        super(props);

        // Setting up state
        this.state = {
            userInput : "",
            list:[],
            selectedTag: "Other",
            selectedTagColor: "grey",
            tags: [
                {tagName: 'Other', color: 'grey'},
                {tagName: 'Work', color: 'red'},
                {tagName: 'Personal', color: 'green'}
            ]
        }
    }
    componentDidMount() {
        this.getItems()
    }

    // Set a user input value
    updateInput(value){
        this.setState({
            userInput: value,
        });
    }

    // Set a selected tag value
    updateTag(value){
        this.setState({
            selectedTag: value.split(" ")[0],
            selectedTagColor: value.split(" ")[1]
        });
    }

    // Add item if user input in not empty
    addItem(event){
        if(event.code === 'Enter') {
            AddToDoAPI({text: this.state.userInput, tag: this.state.selectedTag, tagColor: this.state.selectedTagColor}).then(resp => {
                if(!localStorage.userId) {
                    localStorage.setItem('userId', resp.data._id);
                }
                this.getItems()
            })
        }
    }

    //Get to-do list
    getItems(tag='') {
        GetToDoListAPI(tag).then(resp => {
            // Update list
            const list = [...resp ? resp.data : []];
            // reset state
            this.setState({
                list,
                userInput: ""
            });
        })
    }

    UpdateToDo(val, id) {
        UpdateToDoAPI({done: val}, id).then(resp => {
            this.getItems()
        })
    }

    // Function to delete item from list use id to delete
    deleteItem(id) {
        DeleteToDoAPI(id).then(resp => {
            this.getItems()
        })
    }

    render(){
        return(
        <Container>

            <Row style={{
                display: "flex",
                justifyContent: "center",
                alignItems: "center",
                fontSize: '3rem',
                fontWeight: 'bolder',
                fontFamily: 'DejaVu Sans Mono, monospace',
                paddingTop: 2
                }}
                >TODO LIST
            </Row>
            <hr style={{marginTop: 0}}/>
            <Row>
                <Col md={{ span: 5, offset: 4 }}>
                    <InputGroup className="mb-3">
                        <DropdownButton
                            variant="outline-secondary"
                            id="input-group-dropdown-2"
                            title={this.state.selectedTag}
                            align="end"
                            size="lg"
                            style={{backgroundColor: 'white'}}
                            onSelect = {e => this.updateTag(e)}
                            >
                                {this.state.tags.map(tag => (
                                    <span style={{display: 'flex'}}><BsStop style={{fontSize: 30, marginTop: 1, color: tag.color}}/><Dropdown.Item key={tag.tagName} eventKey={tag.tagName + ' ' + tag.color}>{tag.tagName}</Dropdown.Item></span>
                                ))}
                        </DropdownButton>
                        <FormControl
                            placeholder="add item . . . "
                            size="lg"
                            value = {this.state.userInput}
                            onChange = {item => this.updateInput(item.target.value)}
                            onKeyPress = {e => this.addItem(e)}
                            aria-label="add something"
                            aria-describedby="basic-addon2"
                        />
                    </InputGroup>
                </Col>
            </Row>
            {this.state.list.length ? 
                <Row>
                    <Col md={{ span: 5, offset: 4 }} style={{paddingBottom: 18}}>
                        <Button variant="primary" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('')} size="sm">All</Button>{' '}
                        <Button variant="secondary" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Other')} size="sm">Other</Button>{' '}
                        <Button variant="danger" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Work')} size="sm">Work</Button>{' '}
                        <Button variant="success" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Personal')} size="sm">Personal</Button>{' '}
                    </Col>
                </Row> : null
            }
            <Row>
                <Col md={{ span: 5, offset: 4 }}>
                    <ListGroup>
                    {/* map over and print items */}
                    {this.state.list.map(item => {return(

                        <ListGroup.Item variant="white" action
                             key={item._id}>
                            <Form.Group id="formGridCheckbox" style={{display: 'flex'}}>
                            <Form.Check type="checkbox" style={{width: 10}} className="my-1 mr-sm-2" onChange={e => this.UpdateToDo(!item.done, item._id)} checked={item.done}/>
                            {item.done ? <span style={{textDecoration: 'line-through', marginTop: 5, width: 380}}>{item.toDo}</span> : <span style={{marginTop: 5, width: 380}}>{item.toDo}</span>}
                            <BsStop style={{fontSize: 25, marginTop: 1, color: item.tagColor, float: 'right', width: 30}}/>
                            <BsX
                                onClick = { () => this.deleteItem(item._id) }
                                style={{float: 'right', fontSize: 25, marginLeft: 'auto', width: 30}}
                            />
                            </Form.Group>
                        </ListGroup.Item>
                    )})}
                    </ListGroup>
                </Col>
            </Row>
        </Container>
        );
    }
}

export default AppComponent;


터미널에 다음 명령을 입력하여 서버를 시작합니다.
npm start
출력: 브라우저에서 열기http://localhost:3000:



또한 필터를 적용하고 개인화된 할 일을 확인하세요.

좋은 웹페이지 즐겨찾기