[nodeJS] Cafe Stamp 프로젝트 - 백엔드 기능 및 MVC 패턴

RedPanda·2022년 10월 27일
0

이후부터는 API 구현 전략과 그에 맞는 기능을 설명할 것이며, MVC패턴으로 프로젝트를 어떻게 구조화했는지 설명하고자 한다.

API 기능 구현 전략과 Package.json

이메일 인증

  • 입력한 이메일이 중복되는지 확인
  • random 코드 생성 (ascii code를 사용하여 6자리 뽑기)
  • 입력한 이메일로 메일을 전송
  • 보낸 코드와 서버의 코드를 비교하여 인증된 사용자 판별

회원가입 및 로그인

  • 이메일과 아이디 중복확인
  • 회원가입 시에 DB에 정보 저장
  • 로그인 시에 DB에서 사용자 확인
  • 로그인 성공 시에 토큰 발행
  • API 요청 시에 토큰으로 로그인 유효시간 설정 및 유저 확인

아이디, 비밀번호 찾기

  • 이메일, 실명, 전화번호가 일치하여 사용자 확인
  • 확인된 사용자에 한하여 뒷자리 4자리를 제외한 아이디를 메일로 발송
  • 이메일, 아이디, 전화번호가 일치한지 확인
  • 확인된 사용자에 한하여 이메일로 임시 비밀번호를 부여 및 DB에서 비밀번호 수정

카페, 사용자, Q&A CRUD

  • 카페와 사용자의 정보 조회, 추가, 수정, 삭제 구현
  • 카페 기능 사용 시에 접속한 사람이 본인 카페인지 확인
  • 내정보 기능 사용 시에 접속한 사람이 본인인지 확인
  • 질문 게시판은 고객(점주)만 이용이 가능하며, 답변된 글에 대하여 수정 및 제거 불가
  • 관리자에 한하여 질문 제거 가능
  • 제거 시에 DB에 남아있도록 함(paranoid)
  • 답변 기능은 관리자 계정(root 계정)만 사용 가능
  • 답변은 수정이 불가

Stamp CRUD

  • 도장에는 고객의 전화번호가 담겨있음
  • 고객의 도장이 없으면 만들어주고 조회
  • 추가 시에 도장의 개수만큼 추가하며 전체 개수 또한 추가함
  • 사용 시에 전체 개수는 변경하지 않으며 사용한 만큼만 x10만큼 삭제
  • 스탬프는 삭제될 수 없음 (특정 기한이 지나면 자동으로 삭제하게 함)

디바이스 통신

  • 도장 조회에 접근 시에 소켓을 연결함
  • 테블릿과 점주의 핸드폰을 서로 소통하게 하는 것이 핵심
  • tablet의 url과 phone의 url에 접근했을 때 각각의 소켓이 연결되도록 함

package.json

{
  // 프로젝트 구성
  "scripts": {
    "start": "nodemon app",
  },
  "author": "Cupon Cafe",
  "license": "MIT",
  "dependencies": {
    "@popperjs/core": "^2.11.6",
    "bcrypt": "^5.0.0",
    "cookie-parser": "^1.4.5",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "express-rate-limit": "^6.6.0",
    "express-session": "^1.17.1",
    "jsonwebtoken": "^8.5.1",
    "morgan": "^1.10.0",
    "multer": "^1.4.4",
    "mysql2": "^2.2.5",
    "nodemailer": "^6.8.0",
    "passport": "^0.6.0",
    "passport-kakao": "^1.0.1",
    "passport-local": "^1.0.0",
    "sequelize": "^6.3.5",
    "sequelize-cli": "^6.2.0",
    "socket.io": "^4.5.3",
    "socketio-jwt": "^4.6.2"
  },
  "devDependencies": {
    "nodemon": "^2.0.3"
  }
}

MVC 패턴과 프로젝트 구조화

백엔드는 기본적으로 MVC패턴(models, views, controllers)으로 이루어져있다.
이번 프로젝트 역시 MVC 패턴을 따르려 노력했으며, 다음은 프로젝트의 구조를 나타낸 것이다.

├── config                          
│   └── corsConfig.json             # cors 설정 파일
├── controllers                     # 로직 폴더
│   ├── answer.js
│   ├── auth.js
│   ├── cafe.js
│   ├── find.js
│   ├── mail.js
│   ├── profile.js
│   ├── question.js
│   └── stamp.js
├── lib                             # 자체 제작한 라이브러리 모음 폴더
│   ├── error.js                    # error 처리용 유틸
│   └── util.js                     # 인증, 만료기한 모듈 모음
├── models                          # DB를 모델링하는 sequelize의 모델 함수용 폴더
│   ├── cafe.js                     
│   ├── index.js                    # sequelize를 이용한 DB설정 파일
│   ├── owner.js
│   ├── question.js                    
│   ├── question.js
│   └── stamp.js
├── routes                          # Router 폴더
│   ├── answer.js
│   ├── auth.js
│   ├── cafe.js
│   ├── customer.js
│   ├── find.js
│   ├── mail.js
│   ├── main.js
│   ├── profile.js
│   ├── question.js
│   └── stamp.js
├── .env                            # (개발용)환경설정 파일(직접 생성)
├── app.js                          # 앱 실행 메인 파일
├── package.json
└── socket.js                       # socket.io 실행 파일

models

models는 mysql로 이루어져 있으며, sequelize를 사용하여 DB를 구성하였다.
구성 자체는 크게 어렵지 않고, 연관관계 역시 앞전에 설명했기 때문에 넘어가도록 한다.

Routes

routes는 app.js에서 들어온 요청을 url로 분류한 후에 접근하는 파일들을 모아놓은 폴더이다. route라는 이름에 맞게 필요한 기능에 따라 restAPI를 사용하였고, 작동하는 기능은 controllers에서 수행하도록 하였다.
다음은 route에 대한 예시이다.

const express = require("express");
const {
  signup,
  signin,
  checkEmail,
  checkUserId,
} = require("../controllers/auth");

const router = express.Router();

// 회원가입 기능
router.post("/join", signup);
// 로그인 기능
router.post("/login", signin);
// 이메일에 대한 중복 체크
router.get("/check-email/:email", checkEmail);
// 아이디에 대한 중복 체크
router.get("/check-id/:userId", checkUserId);

module.exports = router;

Controllers

controller는 기능을 수행하는 함수들을 꺼내서 사용하도록 폴더에 모아놓은 것이다. 대부분의 요청에 대한 처리를 이곳에서 하며, 알고리즘이나 계산이 필요할 함수들은 libs/util에 넣어두었다.
다음은 controllers에 대한 예시이다.

const { Owner } = require("../models");
const bcrypt = require("bcrypt");
const resCode = require("../libs/error");

exports.Ownerinfo = async (req, res, next) => {
  try {
    const myprofile = await Owner.findOne({ where: { id: req.decoded.id } });
    if (!myprofile) {
      const error = resCode.BAD_REQUEST_NO_USER;
      console.error("ERROR RESPONSE -", error);
      return res.status(error.code).json(error);
    } else {
      const response = JSON.parse(JSON.stringify(resCode.REQUEST_SUCCESS));
      response.data = myprofile;
      return res.status(response.code).json(response);
    }
  } catch (error) {
    console.error("ERROR RESPONSE -", error);
    error.statusCode = 500;
    next(error);
  }
};
exports.updateOwner = async (req, res, next) => {
  try {
    const { newPassword, currentPassword, ownerPhone } = req.body;

    let passwordEncoding = null;
    // password가 있으면 암호화 시켜야함
    const owner = await Owner.findOne({ where: { id: req.decoded.id } });
    // owner가 없을 때의 예외처리
    if (!owner) {
      const error = resCode.UNAUTHORIZED_ERROR;
      console.error("ERROR RESPONSE -", error);
      return res.status(error.code).json(error);
    } else {
      // 현재 패스워드와 새로운 패스워드가 모두 입력 되었을때 비밀번호 변경에 접근이 가능
      if (newPassword && currentPassword) {
        // 현재 비밀번호와 DB의 비밀번호를 크립토로 비교
        if (!(await bcrypt.compare(currentPassword, owner.password))) {
          const error = resCode.BAD_REQUEST_WRONG_DATA;
          console.log("ERROR RESPONSE -", error);
          return res.status(error.code).json(error);
        }
        // 비교 성공 시에 비밀번호를 암호화하여 넣음
        passwordEncoding = await bcrypt.hash(newPassword, 12);
      }
      if (
        !(await Owner.update(
          {
            password: passwordEncoding ? passwordEncoding : owner.password,
            ownerPhone: ownerPhone ? ownerPhone : owner.ownerPhone,
          },
          {
            where: { id: req.decoded.id },
          }
        ))
      ) {
        const error = resCode.BAD_REQUEST_WRONG_DATA;
        console.error("ERROR RESPONSE -", error);
        return res.status(error.code).json(error);
      } else {
        const response = resCode.REQUEST_SUCCESS;
        return res.status(response.code).json(response);
      }
    }
  } catch (error) {
    console.error("ERROR RESPONSE -", error);
    error.statusCode = 500;
    next(error);
  }
};
exports.withdrawOwner = async (req, res, next) => {
  try {
    const { email } = req.params;
    // 암호화된 코드는 url에 담으면 안됨
    const owner = await Owner.findOne({ where: { email, id: req.decoded.id } });
    if (!owner) {
      const error = resCode.FORBIDDEN_ERROR;
      console.error("ERROR RESPONSE -", error);
      return res.status(error.status).json(error);
    } else {
      if (!(await Owner.destroy({ where: { email, id: req.decoded.id } }))) {
        const error = resCode.BAD_REQUEST_WRONG_DATA;
        console.error("ERROR RESPONSE -", error);
        return res.status(error.code).json(error);
      } else {
        const response = resCode.REQUEST_SUCCESS;
        return res.status(response.code).json(response);
      }
    }
  } catch (error) {
    console.error("ERROR RESPONSE -", error);
    error.statusCode = 500;
    next(error);
  }
};

여담

이번 프로젝트를 진행하면서 백엔드의 구조화에 대해 많은 생각을 해보았다. 앞으로 설명할 response와 프로젝트, 코드의 구조화를 많이 생각해보는 계기가 되어 이후의 프로젝트에도 적용해볼 생각이다.
아직 수정이 필요한 프로젝트이기에 포스팅을 하면서 프로젝트를 조금 더 다듬을 생각이다.

profile
끄적끄적 코딩일기

0개의 댓글