본문 바로가기
프로그래밍/Node.js

[15] 회원가입, 로그인, 게시판CRUD 구현해보자! (Node.js x mySQL x sequelize) 2탄

by 제이스톨 2023. 7. 25.
728x90

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

 

728x90