1. 들어가기 전에..
여기서는 본격적으로 로그인, 로그아웃 기능을 위해 user라는 DB를 만들고 게시글과 댓글 기능을 위한 post와 cmt라는 DB를 만들어보도록 하겠다.
( 환경세팅 방법은 아래 1탄에 있으니 아래 링크를 타고 넘어가면 되고, Sequelize를 사용해서 기능을 구현할 생각입니다.)
https://jh-healing-place.tistory.com/105
[14] 회원가입, 로그인, 게시판CRUD 구현해보자! (Node.js x mySQL x sequelize) 1탄
1. 들어가기 전에 개발자라고 하면 모두들 CRUD는 기본이라고한다. 하지만 이 CRUD를 구현한다는게 가장 어렵고 이를 자유자재로 구현하게 되면 그때서야 비로소 초급 개발자의 문턱에 들어갔다고
jh-healing-place.tistory.com
1-2. 구현해야할 API
2탄 : 회원가입 / 로그인 / 로그아웃 / 게시글 생성, 조회, 수정, 삭제
3탄 : 댓글기능 / 좋아요 / 게시글 좋아요 개수 및 정렬
- 회원가입 (POST)
userId
password
passwordConfirm
=> npx sequelize model:generate --name Users --attributes userId:integer,password:string,passwordConfirm:string
- 로그인 (POST)
userId
password
- 게시글 생성 (POST)
postId
userId
title
content
=> npx sequelize model:generate --name Posts --attributes userId:integer,title:string,content:string
- 게시글 조회 (GET)
- 게시글 수정 (PUT)
postId
userId
title
content
- 게시글 삭제 (DELETE)
2. ERD 작성 및 API 명세
Jboard ERD ▼
3. 회원가입, 로그인, 게시판 CRUD 기능 구현
3-1. migrations, models 파일 생성
# 1. npx sequelize init ( 자동으로 파일과 폴더들이 생성된다 )
# 2. migration 생성 및 수정
- npx sequelize model:generate --name Users --attributes userId:Integer,nickname:String,password:String
- npx sequelize model:generate --name Posts --attributes userId:Integer,postId:Integer,title:String,content:String
# 3. DB 생성 및 Migrations에 정의된 테이블 MySQL에 생성
npx sequelize db:create
npx sequelize db:migrate
// user migration
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
userid: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
email: {
type: Sequelize.STRING
},
password: {
type: Sequelize.STRING
},
passwordConfirm: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Users');
}
};
// post migration
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Posts', {
postid: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
allowNull: false,
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'userId',
},
onDelete: 'CASCADE',
},
title: {
type: Sequelize.STRING
},
content: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Posts');
}
};
# 4. migration 파일 수정 및 model 폴더 내부 파일 세팅
1:1관계 (hasone) / 1:1 (belongsTo) / 1:N 관계 (hasmany)
// user model
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Users extends Model {
static associate(models) {
// Users와 Posts는 일대다 관계
this.hasMany(models.Posts, {
sourceKey: 'userId',
foreignKey: 'userId',
});
}
}
Users.init(
{
userId: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER,
},
email: {
allowNull: false,
type: DataTypes.STRING,
},
password: {
allowNull: false,
type: DataTypes.STRING,
},
passwordConfirm: {
type: DataTypes.STRING,
},
createdAt: {
allowNull: false, // NOT NULL
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
updatedAt: {
allowNull: false, // NOT NULL
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
sequelize,
modelName: 'Users',
},
);
return Users;
};
// post model
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Posts extends Model {
static associate(models) {
// Users와 Posts는 일대다 관계
this.belongsTo(models.Users, {
targetKey: 'userId',
foreignKey: 'userId',
});
}
}
Posts.init(
{
postId: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER,
},
userId: {
allowNull: false,
type: DataTypes.INTEGER,
},
title: {
type: DataTypes.STRING,
},
content: {
allowNull: false,
type: DataTypes.STRING,
},
createdAt: {
allowNull: false, // NOT NULL
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
updatedAt: {
allowNull: false, // NOT NULL
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
sequelize,
modelName: 'Posts',
},
);
return Posts;
};
# 참고. migration 테이블 삭제
npx sequelize db:migrate:undo
3-2. Route 설정
// usersRoute
const express = require("express");
const jwt = require("jsonwebtoken");
const { Users } = require("../models");
const router = express.Router();
// 1. 유저 회원 가입 API [POST]
router.post("/signup", async (req, res) => {
console.log(req.body);
const { email, password, passwordConfirm } = req.body;
const validEmailCheck = (string) => {
const pattern = /^[a-zA-Z0-9]+@[a-zA-Z]+\.[A-Za-z]+$/;
return pattern.test(string);
};
if (!validEmailCheck(email) || email.length < 3) {
return res
.status(400)
.json({ errorMessage: "이메일의 형식이 올바르지 않습니다." });
}
if (!password || password < 4) {
return res
.status(412)
.json({ errorMessage: "패스워드는 4자이상이어야 합니다." });
}
if (password !== passwordConfirm) {
return res.status(412).json({
errorMessage:
"패스워드가 일치하지 않습니다. 패스워드 재입력은 passwordConfirm 입니다.",
});
}
const isExistUser = await Users.findOne({ where: { email: email } });
if (isExistUser) {
return res
.status(412)
.json({ errorMessage: "이미 존재하는 이메일입니다." });
}
try {
await Users.create({ email, password });
return res.status(201).json({ message: "유저가 등록되었습니다." });
} catch (error) {
console.log(error);
return res
.status(400)
.json({ errorMessage: "유저 등록 과정에서 오류가 발생하였습니다." });
}
});
// 2. 사장님 로그인 API [POST]
router.post("/login", async (req, res) => {
const { email, password } = req.body;
console.log(email, password);
const userCheck = await Users.findOne({
where: { email: email },
});
if (!userCheck) {
return res
.status(401)
.json({ errorMessage: "해당하는 사용자가 존재하지 않습니다." });
} else if (userCheck.password !== password) {
return res
.status(401)
.json({ errorMessage: "비밀번호가 일치하지 않습니다." });
}
try {
// JWT 생성
const token = jwt.sign(
{
userId: userCheck.userId,
},
"customized_secret_key"
// 필요 시 수정
);
// 2. 쿠키 발급
res.cookie("authorization", `Bearer ${token}`);
// 3. response
return res.status(200).json({ message: "사장님 환영합니다." });
} catch (error) {
console.error(error);
return res
.status(400)
.json({ message: "사장님 로그인 과정에 오류가 발생하였습니다." });
}
});
// 3. 회원 정보 조회 API [GET]
router.get("/userInfoList", async (req, res) => {
try {
const userList = await Users.findAll({
attributes: ["userId", "email", "password", "createdAt"],
order: [["createdAt", "DESC"]],
});
return res.status(200).json({ data: userList });
} catch (error) {
console.log(error);
return res
.status(400)
.json({ errorMessage: "유저 목록 조회 과정에 오류가 발생하였습니다." });
}
});
router.get("/userInfo/:userId", async (req, res) => {
const { userId } = req.params;
try {
const userInfo = await Users.findOne({
where: { userId: userId },
attributes: ["userId", "email", "password", "createdAt"],
});
return res.status(200).json({ data: userInfo });
} catch (error) {
console.log(error);
return res
.status(400)
.json({ errorMessage: " 유저 정보 조회 과정에 에러가 발생했습니다." });
}
});
// 4. 유저 비밀번호 수정 API [PUT]
router.put("/user/:userId", async (req, res) => {
const { userId } = req.params;
const { password } = req.body;
const userIdToUpdate = await Users.findOne({
where: { userId: userId },
});
if (!userIdToUpdate) {
return res.status(404).json({ message: "Id를 다시 확인해주세요." });
}
try {
await Users.update({ password: password }, { where: { userId: userId } });
return res.status(200).json({ message: "유저 정보가 수정되었습니다." });
} catch (error) {
console.log(error);
return res.status(400).json({
errorMessage: "유저 정보를 수정하는 과정에 오류가 발생하였습니다.",
});
}
});
// 5. 회원 탈퇴 API. [DELETE]
router.delete("/user/:userId", async (req, res) => {
const { userId } = req.params;
const userIdToDelete = await Users.findOne({
where: { userId: userId },
});
if (!userIdToDelete) {
return res.status(404).json({ message: "userId를 다시 확인해주세요." });
}
try {
await userIdToDelete.destroy();
return res.status(200).json({ message: "계정이 삭제되었습니다." });
} catch (error) {
console.log(error);
return res.status(400).json({
errorMessage: "계정을 삭제하는 과정에 오류가 발생하였습니다.",
});
}
});
module.exports = router;
// postsRoute
const express = require("express");
const { Posts } = require("../models");
const authMiddleware = require("../middlewares/authMiddleware");
const router = express.Router();
// 게시글 등록 [POST]
router.post("/user/addPost", authMiddleware, async (req, res) => {
const { userId } = res.locals.user;
const { title, content } = req.body;
if (!userId) {
res.status(403).json({ errorMessage: "로그인 후 사용 가능합니다." });
return;
}
if (!title) {
return res.status(400).json({ errorMessage: "제목을 입력해주세요." });
}
if (!content) {
return res.status(400).json({ errorMessage: "내용을 입력해주세요." });
}
try {
const addPost = await Posts.create({
title,
content,
userId: userId,
});
return res.status(201).json({ data: addPost });
} catch (error) {
console.error(error);
return res
.status(500)
.json({ message: "게시글 등록 과정에 오류가 발생하였습니다." });
}
});
// 게시글 전체 조회 [GET]
router.get("/user/getPostAll", async (req, res) => {
try {
const posts = await Posts.findAll({
attributes: ["postId", "userId", "title", "content", "createdAt"],
order: [["createdAt", "DESC"]],
});
return res.status(200).json({ data: posts });
} catch (error) {
console.error(error);
return res
.status(500)
.json({ message: "게시글 전제 조회 과정에 오류가 발생하였습니다." });
}
});
// 게시글 수정 [PUT]
router.put("/user/update/:postId", authMiddleware, async (req, res) => {
const { postId } = req.params;
const { userId } = res.locals.user;
const { title, content } = req.body;
try {
const post = await Posts.findOne({
where: { postId: postId },
});
console.log(post);
if (post.userId !== userId) {
return res
.status(403)
.json({ errorMessage: "게시글을 수정할 권한이 없습니다." });
}
if (!post) {
return res
.status(404)
.json({ errorMessage: "게시글를 찾을 수 없습니다." });
}
await Posts.update({ title, content }, { where: { postId: postId } });
return res.status(200).json({ message: "게시글 수정을 완료하였습니다." });
} catch (error) {
console.error(error);
return res
.status(500)
.json({ errorMessage: "게시글 수정 과정에 오류가 발생하였습니다." });
}
});
// 게시글 삭제_DELETE
router.delete("/user/delete/:postId", authMiddleware, async (req, res) => {
const { postId } = req.params;
const { userId } = res.locals.user;
try {
const post = await Posts.findOne({
where: { postId: postId },
});
console.log(post);
if (post.userId !== userId) {
return res
.status(403)
.json({ errorMessage: "게시글을 삭제할 권한이 없습니다." });
}
if (!post) {
return res.status(404).json({ errorMessage: "메뉴를 찾을 수 없습니다." });
}
await Posts.destroy({ where: { postId: postId } });
return res.status(200).json({ message: "게시글 삭제를 완료하였습니다." });
} catch (error) {
console.error(error);
return res
.status(500)
.json({ errorMessage: "게시글 삭제에 실패하였습니다." });
}
});
module.exports = router;
// 로그인을 위한 middleware
// middlewares/authMiddleware
// JWT
const jwt = require('jsonwebtoken');
// Model
const { Users } = require('../models');
module.exports = async (req, res, next) => {
try {
const { authorization } = req.cookies;
if (!req.cookies.authorization) {
return res.status(403).json({
message: '현재 log-in 되어있지 않습니다.',
});
}
const [tokenType, token] = (authorization ?? '').split(' ');
if (tokenType !== 'Bearer') {
// <= 형태가 일치하지 않는 경우
return res.status(403).json({
message: '전달된 Cookie에서 오류가 발생하였습니다.',
});
}
if (!token) {
return res.status(403).json({
// <= 존재하지 않는 경우
message: 'log-in이 필요한 기능입니다.',
});
}
// Decoding ==================================================
const decodedToken = jwt.verify(token, 'customized_secret_key');
const userId = decodedToken.userId;
// Decoding ==================================================
const user = await Users.findOne({
where: { userId },
});
if (!user) {
// 사용자가 존재하지 않을 경우, "authorization" Cookie를 제거하여, 인증상태를 해제합니다.
res.clearCookie('authorization');
return res.status(403).json({
message: '전달된 Cookie에서 오류가 발생하였습니다.',
});
}
res.locals.user = user; // <= 이 변수에 'userId'가 들어있습니다.
next();
// try => catch
} catch (error) {
// 비정상적인 요청일 경우, "authorization" Cookie를 제거하여, 인증상태를 해제합니다.
res.clearCookie('authorization');
return res.status(403).json({
message: '전달된 Cookie에서 오류가 발생하였습니다.',
});
}
};
3-3. app.js 수정
const cors = require('cors');
const express = require('express');
const app = express();
const port = 3000;
const path = require('path');
// cookie parser
const cookieParser = require('cookie-parser');
const usersRouter = require('./routes/usersRoute.js');
const postsRouter = require('./routes/postsRoute.js');
// Middleware ==================================================
app.use(express.json()); // req.body parser
app.use(cookieParser()); // cookie parser
app.use(cors()); // front-back connect
// localhost:3000/api/
app.use('/api', [usersRouter]);
app.use('/api', [postsRouter]);
// Middleware ==================================================
// HTML, CSS
app.use(express.static(path.join(__dirname, 'assets')));
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'assets', 'index.html'));
});
// server start!!
app.listen(port, () => {
console.log(port, '서버가 켜졌습니다.');
});
4. Thunder Client를 이용한 테스트
- 회원가입
- 로그인
- 게시글 등록
- 게시글 조회
- 게시글 수정
- 게시글 삭제
이렇게 되면 백 개발자의 기본 소양이라고 할 수 있는 CRUD는 끝난거다.
3탄에서는 댓글기능을 추가해보고 좋아요 기능을 넣어보고자 한다.
2탄 끝~~

3탄 링크 바로가기 ▼
https://jh-healing-place.tistory.com/119
[16] 댓글기능, 좋아요 기능을 구현해보자! (Node.js x mySQL x sequelize) -3탄-
들어가기 전에.. 지난 시간까지는 개발환경 세팅하는 방법과 회원가입, 로그인, 게시글CRUD 구현까지 알아보았다. 이번 3탄에서는 댓글기능(Cmt), 좋아요(like)기능을 넣어보도록 하겠다. [아직까지
jh-healing-place.tistory.com
'프로그래밍 > Node.js' 카테고리의 다른 글
[19] todoList 프로젝트 ( node.js 6조 ) (0) | 2023.08.07 |
---|---|
[16] 댓글기능, 좋아요 기능을 구현해보자! (Node.js x mySQL x sequelize) -3탄- (0) | 2023.07.28 |
[18] Node.js Qwerty 프로젝트 회고 (0) | 2023.07.24 |
[17] Node.js Qwerty 프로젝트 (node.js 4조) (0) | 2023.07.17 |
[14] 회원가입, 로그인, 게시판CRUD 구현해보자! (Node.js x mySQL x sequelize) 1탄 (0) | 2023.07.13 |