코드코코

210918 [7장] 7.6 시퀄라이즈 사용하기 본문

기록/node.js 교과서 따라하기

210918 [7장] 7.6 시퀄라이즈 사용하기

코드코코 2021. 9. 18. 23:40

*사용자등록 및 사용자조회/댓글조회 작성 수정 삭제 구현하기

작성한것들

 

app.js

models[폴더]

  1.index.js

  2.user.js

  3.comment.js

public[폴더]

  sequelize.js

views[폴더]

  error.html

  sequelize.html

routes[폴더]

  1.index.js

  2.user.js

  3.comment.js

실습 결과물

1.app.js

const express = require('express');
const path = require('path');
const morgan = require('morgan');
const nunjucks = require('nunjucks');

const { sequelize } = require('./models');
//라우터 연결
const indexRouter = require('./routes');
const userRouter = require('./routes/users');
const commentRouter = require('./routes/comments');

const app = express();

app.set('port', process.env.PORT || 3001);
app.set('view engine', 'html');
nunjucks.configure('views', {
    express: app,
    watch: true
});

//db.sequelize를 불러와서 sync메서드를 사용해 서버실핼시 mysql과 연동되도록 함.
//force: false 옵션을 true로 설정시 서버실행 시마다 테이블 재생성. 테이블을 잘못 만들 경우 true하면 됨.
sequelize.sync({ force: false })
    .then(() => {
        console.log('데이터베이스 연결 성공');
    })
    .catch((err) => {
        console.error(err);
    })

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));


//라우터연결
app.use('/', indexRouter);
app.use('/users', userRouter);
app.use('/comments', commentRouter);

app.use((req, res, next) => {
    const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
    error.status = 404;
    next(error);
});

app.use((err, req, res, next) => {
    res.locals.message = err.message;
    res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
    res.status(err.status || 500);
    res.render('error');
});

app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});

 

2.models[폴더]-1.index.js

//Sequelize : 시퀄라이즈 패키지이자 생성자.
const Sequelize = require('sequelize');
//모델과 연결하기
const User = require('./user');
const Comment = require('./comment');

const env = process.env.NODE_ENV || 'development';

//config/config.json에서 데이터 설정을 불러옴.
//    '/../config/config.json':경로에서 / 있고 없고 차이남. 없을 때 에러남.
const config = require(__dirname + '/../config/config.json')[env];
const db = {};

//new Sequelize를 통해 MySQL 연결 객체를 생성.
const sequelize = new Sequelize(config.database, config.username, config.password, config);

//연결 객체를 재사용하기 위해 db.sequelize에 넣어둠.
db.sequelize = sequelize;

//db라는 객체에 User과 Comment 모델을 담음.
//앞으로 db객체를 require하여 모델에 접근 가능.
db.User = User;
db.Comment = Comment;

//User.init은 모델의 static.init() 호출.
User.init(sequelize);
Comment.init(sequelize);

//associate():다른 테이블과의 관계를 연결.
User.associate(db);
Comment.associate(db);

module.exports = db;

2.models[폴더]-2.user.js

//MySQL에서 정의한 테이블을 시퀄라이즈에서도 정의.
//MySQL의 테이블은 시퀄라이즈의 모델과 대응.
//시퀄라이즈는 모델과 MySQL 테이블을 연결해주는 역할.

//User모델 만들기

const Sequelize = require('sequelize');

//모델을 만들고 모듈로 exports함.
module.exports = class User extends Sequelize.Model {
    //static init() : 테이블에대한 설정.
    static init(sequelize) {
        //super.init(첫번째인수,두번째인수)
        //첫번째 인수 : 테이블 칼럼에 대한 설정.
        //두번째 인수 : 테이블 자체에 대한 설정.
        //시퀄라이즈는 알아서 id를 기본키로 설정하므로 id컬럼은 적을 필요가 없다.
        return super.init({
            name: {
                type: Sequelize.STRING(20),
                allowNull: false,
                unique: true,
            },
            age: {
                type: Sequelize.INTEGER.UNSIGNED,
                allowNull: false,
            },
            married: {
                type: Sequelize.BOOLEAN,
                allowNull: false,
            },
            comment: {
                type: Sequelize.TEXT,
                allowNull: true,
            },
            created_at: {
                type: Sequelize.DATE,
                allowNull: false,
                defaultValue: Sequelize.NOW,
            },
        }, {
            sequelize,
            timestamps: false,
            underscored: false,
            modelName: 'User',
            tableName: 'users',
            paranoid: false,
            charset: 'utf8',
            collate: 'utf8_general_ci',

        });
    }
    static associate(db) {
        //hasMany: 1대N의 관계
        //User 테이블의 로우 하나를 불러 올 때, 연결된 comments 테이블의 로우들도 같이 불러옴.
        //외래키를 따로 지정하지 않는다면, 모델명+기본키 로 시퀄라이즈가 임의생성함.
        db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id' });
    }
};


//static init() : 테이블에대한 설정.
//static associate() : 다른 모델과의 관계.

//시퀄라이즈와 MySQL의 자료형이 다름.

/*
테이블 옵션

1. sequelize : static init 매개변수와 연결되는 옵션, db.sequelize 객체를 넣어야 함.
timestamps: ture인 경우에 시퀄라이즈는 createdAt과 updatedAt 컬럼을 추가한다. 자동으로 시간 입력.
underscored: 스네이크케이스로 바꿈.기본값 캐멀케이스.
modelName: 모델 이름 설정. 노드 프로젝트에서 사용.
tableName: 실제 데이터베이스의 테이블 이름. 소문자 및 복수형함.
paranoid: true인 경우에 deletedAt 컬럼이 생성됨. 로우를 삭제할 때 완전히 지워지지않고 삭제된 시간이 기록됨.
-로우조회 명령시 deletedAt 값이 null인 로우(삭제되지 않았다는 뜻)를 조회. 나중에 복원하기 위해서
charset: 'utf8', 한글 만약 이모티콘까지 입력할수 있게 하려면 utf8mb4
collate: 'utf8_general_ci', 한글 만약 이모티콘까지 입력할수 있게 하려면 utf8mb4_general_ci
*/

2.models[폴더]-3.comment.js

const Sequelize = require('sequelize');

module.exports = class Comment extends Sequelize.Model {
    static init(sequelize) {
        return super.init({
            comment: {
                type: Sequelize.STRING(100),
                allowNull: false,
            },
            created_at: {
                type: Sequelize.DATE,
                allowNull: true,
                defaultValue: Sequelize.NOW,
            },
        }, {
            sequelize,
            timestamps: false,
            modelName: 'Comment',
            tableName: 'comments',
            paranoid: false,
            charset: 'utf8mb4',
            collate: 'utf8mb4_general_ci',
        });
    }
    static associate(db) {
        //belongsTo : 다른 모델의 정보가 들어가는 테이블에 사용.
        db.Comment.belongsTo(db.User, { foreignKey: 'commenter', targetKey: 'id' });
    }
};

3.public[폴더]-sequelize.js

//사용자 이름을 눌렀을 때 댓글 로딩
document.querySelectorAll('#user-list tr').forEach((el) => {
    el.addEventListener('click', function () {
        //textContent : 모든 텍스트를 그대로
        const id = el.querySelector('td').textContent;
        //댓글조회
        getComment(id);
    });
});

//사용자 로딩
async function getUser() {
    try {
        const res = await axios.get('/users');
        const users = res.data;
        console.log(users);
        const tbody = document.querySelector('#user-list tbody');
        tbody.innerHTML = '';
        users.map(function (user) {
            const row = document.createElement('tr');
            row.addEventListener('click', () => {
                getComment(user.id);
            });
            //로우 셀 추가
            let td = document.createElement('td');
            td.textContent = user.id;
            row.appendChild(td);
            td = document.createElement('td');
            td.textContent = user.name;
            row.appendChild(td);
            td = document.createElement('td');
            td.textContent = user.age;
            row.appendChild(td);
            td = document.createElement('td');
            td.textContent = user.married ? '기혼' : '미혼';
            row.appendChild(td);
            tbody.appendChild(row);
        });
    } catch (err) {
        console.log(err);
    }
}
//댓글 로딩
async function getComment(id) {
    try {
        const res = await axios.get(`/users/${id}/comments`);
        const comments = res.data;
        const tbody = document.querySelector('#comment-list tbody');
        tbody.innerHTML = '';
        comments.map(function (comment) {
            //로우 셀 추가
            const row = document.createElement('tr');
            let td = document.createElement('td');
            td.textContent = comment.id;
            row.appendChild(td);
            td = document.createElement('td');
            td.textContent = comment.User.name;
            row.appendChild(td);
            td = document.createElement('td');
            td.textContent = comment.comment;
            row.appendChild(td);
            const edit = document.createElement('button');
            edit.textContent = '수정';
            //수정 클릭시
            edit.addEventListener('click', async () => {
                const newComment = prompt('바꿀 내용을 입력하세요');
                if (!newComment) {
                    return alert('내용을 반드시 입력하셔야 합니다');
                }
                try {
                    await axios.patch(`/comments/${comment.id}`, { comment: newComment });
                    getComment(id);
                } catch (err) {
                    console.error(err);
                }
            });

            const remove = document.createElement('button');
            remove.textContent = '삭제';
            //삭제 클릭시
            remove.addEventListener('click', async () => {
                try {
                    await axios.delete(`/comments/${comment.id}`);
                    getComment(id);
                } catch (err) {
                    console.log(err);
                }
            });
            //버튼 추가
            td = document.createElement('td');
            td.appendChild(edit);
            row.appendChild(td);
            td = document.createElement('td');
            td.appendChild(remove);
            row.appendChild(td);
            tbody.appendChild(row);
        });
    } catch (err) {
        console.error(err);
    }
}
//사용자 등록 시
document.getElementById('user-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const name = e.target.username.value;
    const age = e.target.age.value;
    const married = e.target.married.checked;
    if (!name) {
        return alert('이름을 입력하세요');
    }
    if (!age) {
        return alert('나이를 입력하세요');
    }
    try {
        await axios.post('/users', { name, age, married });
        getUser();
    } catch (err) {
        console.error(err)
    }
    e.target.username.value = '';
    e.target.age.value = '';
    e.target.married.checked = false;
});
//댓글 등록시
document.getElementById('comment-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const id = e.target.userid.value;
    const comment = e.target.comment.value;
    if (!id) {
        return alert('아이디를 입력하세요');
    }
    if (!comment) {
        return alert('댓글을 입력하세요');
    }
    try {
        await axios.post('/comments', { id, comment });
        getComment(id);
    } catch (err) {
        console.error(err);
    }
    e.target.userid.value = '';
    e.target.comment.value = '';
})

4.views[폴더]-1.error.html

<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>

4..views[폴더]-2.sequelize.html

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>시퀄라이즈 서버</title>
    <style>
        table {
            border: 1px solid black;
            border-collapse: collapse;
        }

        table th,
        table td {
            border: 1px solid black;
        }
    </style>
</head>

<body>
    <div>
        <form id="user-form">
            <fieldset>
                <legend>사용자등록</legend>
                <div>
                    <input id="username" type="text" placeholder="이름">
                </div>
                <div>
                    <input id="age" type="number" placeholder="나이">
                </div>
                <div>
                    <input id="married" type="checkbox"><label for="married">결혼여부</label>
                </div>
                <button type="submit">등록</button>
            </fieldset>
        </form>
    </div>
    <br>
    <table id="user-list">
        <thead>
            <tr>
                <th>아이디</th>
                <th>이름</th>
                <th>나이</th>
                <th>결혼여부</th>
            </tr>
        </thead>
        <tbody>
            {% for user in users %}
            <tr>
                <td>{{user.id}}</td>
                <td>{{user.name}}</td>
                <td>{{user.age}}</td>
                <td>{{'기혼' if user.married else '미혼'}}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    <br>
    <div>
        <form id="comment-form">
            <fieldset>
                <legend>댓글 등록</legend>
                <div>
                    <input id="userid" type="text" placeholder="사용자아이디">
                </div>
                <div>
                    <input id="comment" type="text" placeholder="댓글">
                </div>
                <button type="submit">등록</button>
            </fieldset>
        </form>
    </div>
    <br>
    <table id="comment-list">
        <thead>
            <tr>
                <th>아이디</th>
                <th>작성자</th>
                <th>댓글</th>
                <th>수정</th>
                <th>삭제</th>
            </tr>
        </thead>
        <tbody></tbody>
    </table>
    <!-- 버튼들을 눌렀을 때, 서버의 라우터로 AJAX요청을 보내는 코드 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="/sequelize.js"></script>
</body>

</html>

5.routes[폴더]-1.index.js

//sequelize.js에 나오는 GET,POST,PUT,DELETE 요청에 해당하는 라우터를 만든다.
//시퀄라이즈는 프로미스를 기본적으로 지원
// async/await와 try/catch문을 사용하여 각각 조회 성공시와 실패 시의 정보를 얻을 수 있음.


const express = require('express');
const User = require('../models/user');

const router = express.Router();

//GET /로 접속했을 때의 라우터
router.get('/', async (req, res, next) => {
    try {
        //User.findAll 메서드로 모든 사용자를 찾음.
        const users = await User.findAll();
        //sequelize.html을 렌더링 할 때 결괏값인 users를 넣는다.
        res.render('sequelize', { users });
    } catch (err) {
        console.error(err);
        next(err);
    }
});

module.exports = router;

5.routes[폴더]-2.users.js

const express = require('express');
const User = require('../models/user');
const Comment = require('../models/comment');

const router = express.Router();

//router.route로 같은 라우트 경로를 하나로 묶음.
router.route('/')
    //GET /users 요청
    //사용자를 조회하는 요청
    //GET / 와의 차이점 : 데이터를 JSON형식으로 반환.
    .get(async (req, res, next) => {
        try {
            const users = await User.findAll();
            res.json(users);
        } catch (err) {
            console.error(err);
            next(err);
        }
    })
    //POST /users 요청
    //사용자를 등록하는 요청
    .post(async (req, res, next) => {
        try {
            const user = await User.create({
                name: req.body.name,
                age: req.body.age,
                married: req.body.married,
            });
            console.log(user);
            res.status(201).json(user);
        } catch (err) {
            console.error(err);
            next(err);
        }
    });

// :id 는 라우터 매개변수로 req.params.id로 값을 가져올 수 있음.
router.get('/:id/comments', async (req, res, next) => {
    try {
        const comments = await Comment.findAll({
            //옵션 : 사용자 정보도 함께 조회
            include: {
                model: User,
                where: { id: req.params.id },
            },
        });
        console.log(comments);
        res.json(comments);
    } catch (err) {
        console.error(err);
        next(err);
    }
});

module.exports = router;

5.routes[폴더]-3.comments.js

const express = require('express');
const { User, Comment } = require('../models');

const router = express.Router();

//댓글 생성
router.post('/', async (req, res, next) => {
    try {
        const comment = await Comment.create({
            //사용자 id를 넣어 사용자와 댓글을 연결
            commenter: req.body.id,
            comment: req.body.comment,
        });
        console.log(comment);
        res.status(201).json(comment);
    } catch (err) {
        console.error(err);
        next(err);
    }
});

//댓글 수정과 삭제
router.route('/:id')
    .patch(async (req, res, next) => {
        try {
            const result = await Comment.update({
                comment: req.body.comment,
            }, {
                where: { id: req.params.id },
            });
            res.json(result);
        } catch (err) {
            console.error(err);
            next(err);
        }
    })
    .delete(async (req, res, next) => {
        try {
            const result = await Comment.destroy({ where: { id: req.params.id } });
            res.json(result);
        } catch (err) {
            console.error(err);
            next(err);
        }
    });

module.exports = router;