들어가며

강의와 책, 유튜브 등으로만 이론 공부를 했는데

직접 코딩할 생각을 하니 막막해서 잠시 이론 공부는 멈추고

이전에 과제로 내주었던 것을 혼자 실행해보기로 했다.

 

[1] DB설계 (ERD 작성)

우선 만들고자 하는 서비스를 ERD를 작성 한다.

https://drawsql.app/ 서비스를 이용하여 직관적으로 볼 수 있게 정리한다.

 

[2] 목표 설정

Node.js 숙련주차 개인과제 (참고 : https://teamsparta.notion.site/Node-js-b177edcc731147b38ba49594849627e9)

  1. 회원 관련 기능을 만들 수 있어요.
  2. 게시글에 댓글을 작성하도록 만들 수 있어요.
  3. swagger를 이용하여 API 스펙을 관리할 수 있어요.
  4. ERD를 이용하여 현재 Database의 관계현황을 파악할 수 있어요

 

[3] 프로젝트 만들기

비주얼 스튜디오로 진행하였습니다.

app.js 파일을 만들어 준후 진행시 필요한 라이브러리를 설치한다.

더보기

1. express - 서버 프레임 워크
2. cookie-parser - 쿠키를 쉽게 세팅
3. jsonwebtoken - jwt 이용하여 인증
4. nodemon - 서버를 자동으로 켜줌(개발용)
5. sequelize - ORM db, table 을 서버에서 쉽게 생성 및 여러 쿼리 함수사용가능
6. sequelize-cli - 시퀄라이즈를 cli를 통해서 실행(개발용)
7. mysql2 - node용 mysql client
8. dotenv - 환경변수 .env파일을 사용하게 해줌

 

nodemon을 사용하기 위해서는 

pacakage.json 파일 작성 - scripts → start(nodemon) → 실행

 

"scripts": { "start": "nodemon app.js" }   <-- 패키지.json에서 변경해야 할 부분

npm init -y
npm i express cookie-parser jsonwebtoken sequelize mysql2 dotenv
npm i nodemon sequelize-cli -D

기본 app.js는 https://expressjs.com/ko/ 에서 [시작하기] - [Hello world]에서 가져온다.

const express = require('express')
const app = express()
const port = 8080

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

 

[4] 폴더 구성하기

위와 같은 폴더 구성을 만들기 위해 우선 시퀄라이즈를 설치한다.

설치하기 전에 config파일을 통해 데이터베이스를 접근해야 하기에

이름과 비밀번호, 데이터베이스 이름, 주소를 설정해준다.

// 시퀄라이즈 사용준비
npx sequelize init

// 데이터베이스 생성
npx sequelize db:create

//User모델 생성 명령어
npx sequelize model:generate --name User --attributes nickname:string,password:string
npx sequelize model:generate --name Post --attributes user_id:integer,title:string,content:string,like_cnt:integer
npx sequelize model:generate --name Comment --attributes user_id:integer,post_id:integer,content:string
npx sequelize model:generate --name Likes --attributes user_id:integer,post_id:integer

//테이블 생성하기
npx sequelize db:migrate

//잘못 합병했다면 이전으로 돌리자!
npx sequelize db:migrate:undo

 

 

routers 폴더는 따로 만들어서 설정해주어야 한다.

 

 

[5] 회원가입 만들기

회원가입을 담당할 routers/register.js 을 생성한다.

 

routers/register.js

const express = require('express');
const router = express.Router();
const {Op} = require('sequelize');
const {User} = require('../models');

router.post('/register', async (req, res) => {
    const {nickname, password, confirmpw} = req.body;

    const nameReg = /^[a-zA-Z0-9]{3,}$/

    try {
        if (!nameReg.test(nickname)) {
            return res.status(412).send({"errorMessage": "ID의 형식이 일치하지 않습니다."})
        }

        if (password.length < 4) {
            return res.status(412).send({"errorMessage": "패스워드의 형식이 일치하지 않습니다."})
        }

        if (password === nickname) {
            return res.status(412).send({"errorMessage": "패스워드와 닉네임이 일치합니다."})
        }

        if (password !== confirmpw) {
            return res.status(412).send({"errorMessage": "패스워드가 일치하지 않습니다."})
        }


        const existUser = await User.findAll({
            where: {nickname: nickname}
        })

        if (existUser.length){
            return res.status(412).send({"errorMessage": "중복된 닉네임입니다."})
        }

        await User.create({
            nickname, password
        })


        res.status(201).send({message: "회원가입 성공!"})

    } catch(err) {
        res.status(400).send({"errorMessage": "요청한 데이터 형식이 올바르지 않습니다."})
    }


});

module.exports = router;

app.js

const express = require('express')
const app = express()
const port = 8080

const registerRouter = require("./routers/register");

app.use(express.json());

app.use("/api", registerRouter);

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

 

app.js 에서는 register.js 에서 정보를 가져와 app.use로 실행하여 서버를 가동시킨다.

서버를 실행시킨후 썬더클라이언트로 회원가입을 한다.

 

json 형식이 큰 따움표(" ")로 감싼다는 사실을 까먹고 키값을 그냥 입력했다가 에러나서 동료의 도움을 받았다.

 

[6] 미들웨어 만들기

 

jwt를 사용하고, 모델에 user 폴더에 있는 클래스 User를 가져옵니다.

토큰 값을 받아서 토큰이 유효한지, 가지고 있는지에 따라 결과값을 반환하는 코드를 작성합니다.

다른 코드에서 사용시, 라우터쪽에 authMiddleWare 를 입력하여 사용합니다.

 

middlewares/auth-middleware.js

const jwt = require("jsonwebtoken");
const { User } = require("../models");
const SECRET_KEY = "1234";

module.exports = (req, res, next) => {
  const token = req.headers.cookie.split("=")[1];
  console.log(req.headers);

  if (!token) {
    res.status(401).send({
      errorMessage: "로그인 후 이용 가능한 기능입니다.",
    });
    return;
  }

  try {
    const { userId } = jwt.verify(token, SECRET_KEY);
    User.findByPk(userId).then((user) => {
      res.locals.user = user;
      return next();
    });
  } catch (err) {
    res.status(401).send({
      errorMessage: "로그인 후 이용 가능한 기능입니다.",
    });
  }
};

[7] 로그인 기능 만들기

routers/login.js

const express = require("express");
const cookieParser = require("cookie-parser");
const router = express.Router();
const { User } = require("../models");
const jwt = require("jsonwebtoken");
const SECRET_KEY = "1234";

const app = express();
app.use(cookieParser());

router.post("/login", async (req, res) => {
  const { nickname, password } = req.body;

  try {
    const user = await User.findOne({
      where: { nickname },
    });

    if (!user || user.password !== password) {
      return res
        .status(412)
        .send({ errorMessage: "닉네임 또는 패스워드를 확인해주세요." });
    }

    const token = jwt.sign(
      { nickname: nickname, userId: user.userId },
      SECRET_KEY,
      {
        expiresIn: "1h",
      }
    );

    res.cookie("token", token);

    return res.json({ token: token });
  } catch (err) {
    return res.status(400).send({ errorMessage: "로그인에 실패하였습니다." });
  }
});

module.exports = router;

 

app.js에서는 아래 코드를 추가해줘야 작동합니다.

const registerRouter = require("./routers/register");
const loginRouter = require("./routers/login");

app.use(express.json());

app.use("/api", [registerRouter, loginRouter]);

로그인 성공하면 토큰값을 반환하도록 설정된 모습

 

[8] 게시판 만들기

 

routers/posts.js

const express = require("express");
const router = express.Router();
const cookieParser = require("cookie-parser");

const { Post, Comment, Like } = require("../models");
const { Op } = require("sequelize");
const authMiddleWare = require("../middlewares/auth-middleware");

const app = express();
app.use(cookieParser());

// 전체 게시글 조회
router.get("/posts", async (req, res) => {
  try {
    const posts = await Post.findAll({ order: [["createdAt", "desc"]] });
    // 오류 예제
    // try catch 있을때/없을때
    // const posts = await NonexistentCollection.find({});

    res.send(posts);
  } catch (error) {
    console.error(error);
    res.status(500).send({ message: error.message });
  }
});

// 특정 게시글 조회
router.get("/posts/:postId", async (req, res) => {
  try {
    const { postId } = req.params;
    // 오류테스트
    // const postId = "63a11f34dee1fb38182cdb93234234";
    const post = await Post.findByPk(postId);

    console.log(post);
    res.send(post);
  } catch (error) {
    console.error(error);

    res.status(500).send({ message: error.message });
  }
});

// 게시글 작성
router.post("/posts", authMiddleWare, async (req, res) => {
  const { title, content } = req.body;
  const user_id = res.locals.user.userId;
  try {
    const posts = await Post.create({
      title,
      content,
      user_id,
    });

    // res.json({posts});
    // res.json(posts);
    res.send(posts);
  } catch (error) {
    console.error(error);
    res.status(500).send({ message: error.message });
  }
});

// 특정 게시글 수정
// 비밀번호 비교 후 비밀번호 일치할 때만 수정
router.put("/posts/:postId", authMiddleWare, async (req, res) => {
  // postId 값 다르게 주고 try catch 빼고 실행
  try {
    const { postId } = req.params;
    const { title, content } = req.body;

    // 조회 실패
    const post = await Post.findByPk(postId);
    if (post === null) {
      return res.status(400).send({ message: "🛑 게시글이 없습니다." });
    }

    const result = await Post.update(
      { title: title, content: content },
      { where: { postId } }
    );

    console.log("result", result);

    res.send({ message: "success" });
  } catch (error) {
    console.error(error);

    res.status(500).send({ message: error.message });
  }
});

// 특정 게시글 삭제
router.delete("/posts/:postId", authMiddleWare, async (req, res) => {
  try {
    const { postId } = req.params;

    // 조기 리턴
    const _post = await Post.findByPk(postId);
    if (_post === null) {
      return res.status(400).send({ message: "🛑 게시글이 없습니다." });
    }

    // 게시글 삭제
    await Post.destroy({
      where: { postId },
    });
    // 게시글에 속한 댓글들 삭제
    await Comment.destroy({
      where: { post_id: postId },
    });

    // console.log(comments);

    res.send("삭제완료!");
  } catch (error) {
    console.error(error);

    res.status(500).send({ message: error.message });
  }
});

module.exports = router;

 

[9] 댓글 만들기

routers/comments.js

const express = require("express");
const router = express.Router();

const { Post, Comment, Like } = require("../models");
const { Op } = require("sequelize");
const authMiddleWare = require("../middlewares/auth-middleware");

// 특정 게시글에 속한 댓글 전체 조회
router.get("/posts/:postId/comments", async (req, res) => {
  try {
    const { postId } = req.params;

    // 조기 리턴
    const post = await Post.findByPk(postId);
    if (post === null) {
      return res.status(400).send({ message: "🛑 게시글이 없습니다." });
    }
    const comment = await Comment.findAll({ order: [["createdAt", "desc"]] });

    res.send(comment);
  } catch (error) {
    console.error(error);

    res.status(500).send({ message: error.message });
  }
});

// 특정 게시글에 속한 댓글 작성
router.post("/posts/:postId/comments", authMiddleWare, async (req, res) => {
  try {
    const post_id = req.params.postId;
    const { content } = req.body;
    const user_id = res.locals.user.userId;

    // 조기 리턴
    const post = await Post.findByPk(post_id);
    if (post === null) {
      return res.status(400).send({ message: "🛑 게시글이 없습니다." });
    }

    if (content === "") {
      return res.status(400).send("🛑 댓글 내용을 입력해주세요");
    }

    const comment = await Comment.create({
      content,
      post_id,
      user_id,
    });

    res.send(comment);
  } catch (error) {
    console.error(error);
    res.status(500).send(error.message);
  }
});

// 특정 게시글에 속한 특정 댓글 수정
router.put(
  "/posts/:postId/comments/:commentId",
  authMiddleWare,
  async (req, res) => {
    try {
      const { postId, commentId } = req.params;
      const { content } = req.body;

      // 조기 리턴
      const post = await Post.findByPk(postId);
      if (post === null) {
        return res.status(400).send({ message: "🛑 게시글이 없습니다." });
      }

      const _comment = await Comment.findByPk(commentId);
      if (_comment === null) {
        return res.status(400).send({ message: "🛑 댓글이 없습니다." });
      }

      if (content === "") {
        return res.status(400).send("🛑 댓글 내용을 입력해주세요");
      }

      await Comment.update(
        {
          content: content,
        },
        { where: { commentId } }
      );

      res.send({ message: "success" });
    } catch (error) {
      console.error(error);

      res.status(500).send(error.message);
    }
  }
);

// 특정 게시글에 속한 특정 댓글 삭제
router.delete(
  "/posts/:postId/comments/:commentId",
  authMiddleWare,
  async (req, res) => {
    try {
      const { postId, commentId } = req.params;

      // 조기 리턴
      const post = await Post.findByPk(postId);
      if (post === null) {
        return res.status(400).send({ message: "🛑 게시글이 없습니다." });
      }

      const _comment = await Comment.findByPk(commentId);
      if (_comment === null) {
        return res.status(400).send({ message: "🛑 댓글이 없습니다." });
      }

      await Comment.destroy({
        where: { commentId },
      });

      res.send("삭제완료!");
    } catch (error) {
      console.error(error);

      res.status(500).send(error.message);
    }
  }
);

module.exports = router;

 

 

 

[10] 좋아요 만들기 & 마무리 app.js

 

routers/likes.js

const express = require("express");
const router = express.Router();
const { Post, likes, User } = require("../models");
const authMiddleWare = require("../middlewares/auth-middleware");

router.get("/likes/posts", authMiddleWare, async (req, res) => {
  const user_id = res.locals.user.userId;
  console.log(res.locals.user);

  const data = await likes.findAll({
    where: { user_id: user_id },
    raw: true,
    attributes: ["Post.user_id", "Post.title", "Post.content", "Post.like_cnt"],
    include: [
      {
        model: Post,
        attributes: [],
      },
    ],
    order: [[Post, "like_cnt", "desc"]],
  });

  console.log("********", data);

  res.status(200).json({ data });
});

router.put("/posts/:postId/like", authMiddleWare, async (req, res) => {
  const user_id = res.locals.user.userId;
  const { postId } = req.params;

  const existlike = await likes.findOne({
    where: { user_id, post_id: postId },
  });

  try {
    if (!existlike) {
      await likes.create({
        user_id: user_id,
        post_id: postId,
      });

      await Post.increment({ like_cnt: 1 }, { where: { postId } });
      return res.status(200).send("좋아요^^");
    } else {
      likes.destroy({
        where: { post_id: postId },
      });

      await Post.decrement({ like_cnt: 1 }, { where: { postId } });
      return res.status(200).send("안 좋아요ㅠㅠ");
    }
  } catch (error) {
    res.status(400).send({ errorMessage: "게시글 좋아요에 실패하였습니다." });
  }
});

module.exports = router;

app.js

const express = require("express");
const app = express();

const loginRouter = require("./routers/login");
const registerRouter = require("./routers/register");
const postRouter = require("./routers/posts");
const commentRouter = require("./routers/comments");
const likesRouter = require("./routers/likes");

app.use(express.json());

app.use("/api", [
  registerRouter,
  loginRouter,
  postRouter,
  commentRouter,
  likesRouter,
]);

app.get("/", (req, res) => {
  res.send("Welcome to my page");
});

app.listen(8080, () => {
  console.log("서버 접속");
});

 

[참고자료]

노드js 숙련학습자료 Sequelize

https://teamsparta.notion.site/2-4-MySQL-Sequelize-167995db19ec4f5eb1d041e95f461281#8f4304ba9ec74c878dcc0832421f8b4b

 

노드js 후발대 자료

https://teamsparta.notion.site/_1-12-26-30-1cd4866ab2e34ea194a59f09da2e166e

 

728x90

+ Recent posts