본문 바로가기
TIL

Nodejs 숙련 이력서 개인과제 완료(3)

by 황민도 2024. 6. 3.

내일배움캠프 스파르타 코딩클럽

이어서 라우터와 에러 로그 인증인가 처리를 한 미들웨어를 작성해 보도록 하겠다.

 

< users.router.js >

import express from 'express';
import joi from 'joi';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
import authMiddleware from '../middlewares/auth.middleware.js';

const router = express.Router();

const createdUsersSchema = joi.object({
  email: joi.string().email().required(),
  password: joi.string().min(6).required(),
  pwCheck: joi.string().required(),
  name: joi.string().required(),
});

// 회원가입 API
router.post('/sign-up', async (req, res, next) => {
  try {
    // body 에서 가져온 내용 유효성 검사
    const validation = await createdUsersSchema.validateAsync(req.body);
    const { email, password, pwCheck, name } = validation;

    // 이미 사용되고 있는 email 검증
    const isExistUser = await prisma.users.findFirst({
      where: { email },
    });
    if (isExistUser) {
      throw new Error('same email');
    }

    // password 확인
    if (password !== pwCheck) {
      throw new Error('not same password');
    }
    const part = 'APPLICANT';
    const salt = 10;
    const hashPw = await bcrypt.hash(password, salt);

    // 데이터베이스에 저장
    const user = await prisma.users.create({
      data: {
        email,
        password: hashPw,
        name,
        part,
      },
    });

    // 데이터 찾기
    const data = await prisma.users.findFirst({
      where: { email },
      select: {
        userId: true,
        email: true,
        name: true,
        part: true,
        createdAt: true,
        updatedAt: true,
      },
    });
    return res
      .status(201)
      .json({ message: '회원가입이 완료되었습니다.', data });
  } catch (error) {
    next(error);
  }
});

const createdUsersSchema2 = joi.object({
  email: joi.string().email().required(),
  password: joi.string().min(6).required(),
});

// 로그인 API
router.post('/sign-in', async (req, res, next) => {
  try {
    const validation = await createdUsersSchema2.validateAsync(req.body);
    const { email, password } = validation;

    const user = await prisma.users.findFirst({
      where: { email },
    });
    if (!user) {
      throw new Error('can not');
    }
    if (!(await bcrypt.compare(password, user.password))) {
      throw new Error('can not');
    }
    const accessToken = jwt.sign(
      { userId: user.userId },
      process.env.ACCESS_TOKEN_SECRET_KEY,
      { expiresIn: '12h' }
    );
    res.cookie('authorization', `Bearer ${accessToken}`);
    return res
      .status(200)
      .json({ message: 'Token이 정상적으로 발급되었습니다.' });
  } catch (error) {
    next(error);
  }
});

// 내 정보 조회 API
router.get('/users', authMiddleware, async (req, res, next) => {
  const { userId } = req.user;
  const user = await prisma.users.findFirst({
    where: { userId: +userId },
    select: {
      userId: true,
      email: true,
      name: true,
      part: true,
      createdAt: true,
      updatedAt: true,
    },
  });
  return res.status(200).json({ data: user });
});

export default router;

회원가입과 로그인 내정보 API를 작성해 주었다.

 

< resumes.router.js >

import express from 'express';
import joi from 'joi';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
import authMiddleware from '../middlewares/auth.middleware.js';

const router = express.Router();

const createdUsersSchema = joi.object({
  title: joi.string().required(),
  introduce: joi.string().min(150).required(),
});

// 이력서 생성 API
router.post('/resumes', authMiddleware, async (req, res, next) => {
  try {
    const { userId } = req.user;
    // body 에서 가져온 내용 유효성 검사
    const validation = await createdUsersSchema.validateAsync(req.body);
    const { title, introduce } = validation;
    const state = 'APPlY';

    // 데이터베이스에 저장
    const user = await prisma.resumes.create({
      data: {
        UserId: userId,
        title,
        introduce,
        state,
      },
    });

    // 데이터 찾기
    const data = await prisma.resumes.findFirst({
      where: { UserId: userId },
      select: {
        resumeId: true,
        UserId: true,
        title: true,
        introduce: true,
        state: true,
        createdAt: true,
        updatedAt: true,
      },
    });
    return res
      .status(201)
      .json({ message: '이력서 생성이 완료되었습니다.', data });
  } catch (error) {
    next(error);
  }
});

// 이력서 목록 조회 API
router.get('/resumes', authMiddleware, async (req, res, next) => {
  try {
    const { userId } = req.user;
    const sort = req.query.sort ? req.query.sort.toLowerCase() : 'desc';
    const orderBy = sort === 'asc' ? 'asc' : 'desc';
    if (orderBy !== 'asc' && orderBy !== 'desc') {
      throw new Error('not asc or desc');
    }
    const resumes = await prisma.users.findMany({
      where: { userId },
      select: {
        name: true,
        Resumes: {
          select: {
            resumeId: true,
            title: true,
            introduce: true,
            state: true,
            createdAt: true,
            updatedAt: true,
          },
        },
      },
      orderBy: { createdAt: orderBy },
    });
    return res.status(200).json({ data: resumes });
  } catch (error) {
    next(error);
  }
});

// 이력서 상세 조회 API
router.get('/resumes/:resumeId', authMiddleware, async (req, res, next) => {
  try {
    const { userId } = req.user;
    const { resumeId } = req.params;
    if (!resumeId) {
      throw new Error('undefind resumeId');
    }
    console.log(resumeId);
    const resumes = await prisma.users.findFirst({
      where: {
        userId,
        Resumes: {
          some: { resumeId: +resumeId },
        },
      },
      select: {
        name: true,
        Resumes: {
          where: {
            resumeId: +resumeId,
          },
          select: {
            resumeId: true,
            title: true,
            introduce: true,
            state: true,
            createdAt: true,
            updatedAt: true,
          },
        },
      },
    });
    if (!resumes || resumes.Resumes.length === 0) {
      throw new Error('undefind Resume');
    }
    return res.status(200).json({ data: resumes });
  } catch (error) {
    next(error);
  }
});

const createdUsersSchema2 = joi.object({
  title: joi.string(),
  introduce: joi.string().min(150),
});

// 이력서 수정 API
router.patch('/resumes/:resumeId', authMiddleware, async (req, res, next) => {
  try {
    const { userId } = req.user;
    const { resumeId } = req.params;
    const validation = await createdUsersSchema2.validateAsync(req.body);
    const { title, introduce } = validation;
    if (!title && !introduce) {
      throw new Error('some inner');
    }
    // 요청된 이력서가 존재하고 사용자가 소유한 이력서인지 확인
    const resume = await prisma.resumes.findUnique({
      where: {
        resumeId: +resumeId,
        UserId: userId,
      },
    });

    // 이력서가 존재하지 않으면 에러 처리
    if (!resume) {
      throw new Error('undefind Resume');
    }

    // 이력서 업데이트
    const updatedResume = await prisma.resumes.update({
      where: {
        resumeId: +resumeId,
        UserId: userId,
      },
      data: {
        title: title || undefined, // 제목이 제공되면 업데이트
        introduce: introduce || undefined, // 자기소개가 제공되면 업데이트
      },
      select: {
        resumeId: true,
        UserId: true,
        title: true,
        introduce: true,
        state: true,
        createdAt: true,
        updatedAt: true,
      },
    });

    res.status(200).json({ data: updatedResume });
  } catch (error) {
    next(error);
  }
});

// 이력서 삭제 API
router.delete('/resumes/:resumeId', authMiddleware, async (req, res, next) => {
  try {
    const { userId } = req.user;
    const { resumeId } = req.params;

    // 요청된 이력서가 존재하고 사용자가 소유한 이력서인지 확인
    const resume = await prisma.resumes.findUnique({
      where: {
        resumeId: +resumeId,
        UserId: userId,
      },
    });

    // 이력서가 존재하지 않으면 에러 처리
    if (!resume) {
      throw new Error('undefind Resume');
    }

    // 데이터 삭제
    await prisma.resumes.delete({
      where: {
        resumeId: +resumeId,
      },
    });

    res.status(200).json({ deletedResumeId: +resumeId });
  } catch (error) {
    next(error);
  }
});

export default router;

이력서를 RESTful 하게 CRUD 기능을 모두하는 이력서 기능을 위와같이 구현하였다.

 

< log.middleware.js >

import winston from 'winston';

// winston 을 통해 어떤 로그를 관리 할 지 설정하는 부분
const logger = winston.createLogger({
  level: 'info', // 로그 레벨을 'info'로 설정합니다. 로그의 중요도를 나타냄
  format: winston.format.json(), // 로그 포맷을 JSON 형식으로 설정합니다.
  transports: [
    new winston.transports.Console(), // 로그를 콘솔에 출력합니다.
  ],
});

// 미들웨어가 실행되는 부분
export default function (req, res, next) {
  // 클라이언트의 요청이 시작된 시간을 기록합니다.
  const start = new Date().getTime();

  // 응답이 완료되면 로그를 기록합니다.
  res.on('finish', () => {
    const duration = new Date().getTime() - start;
    logger.info(
      `Method: ${req.method}, URL: ${req.url}, Status: ${res.statusCode}, Duration: ${duration}ms`
    );
  });

  next();
}

winston 을 활용하여 위와같이 요청의 로그와 처리시간을 기록하게끔 하였다.

 

< auth.middleware.js >

import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';

export default async function (req, res, next) {
  try {
    // 1. 클라이언트로 부터 쿠키(Cookie) 전달받기
    const { authorization } = req.cookies;

    if (!authorization) {
      throw new Error('인증 정보가 없습니다.');
    }
    // 2. 쿠키(Cookie)가 Bearer 토큰 형식인지 확인
    const [tokenType, token] = authorization.split(' ');
    if (tokenType !== 'Bearer') {
      throw new Error('지원하지 않는 인증 방식입니다.');
    }
    const key = process.env.ACCESS_TOKEN_SECRET_KEY;

    // 3. 서버에서 발급한 JWT가 맞는지 검증
    const decodedToken = jwt.verify(token, key);
    const userId = decodedToken.userId;

    // 4. JWT의 `userId`를 이용해 사용자를 조회
    const user = await prisma.users.findFirst({
      where: { userId: +userId },
    });
    if (!user) {
      res.clearCookie('authorization');
      // 쿠키를 받았는데 없다면 정상적이지 않은 쿠키이기 때문에 삭제를 진행해야함
      throw new Error('인증 정보와 일치하는 사용자가 없습니다.');
    }

    // 5. `req.user` 에 조회된 사용자 정보를 할당합니다.
    req.user = user;

    // 6. 다음 미들웨어를 실행합니다.
    next();
  } catch (error) {
    res.clearCookie('authorization'); // 특정 쿠키를 삭제시킨다.

    switch (error.name) {
      case 'TokenExpiredError': // 토큰이 만료되었을 때, 발생하는 에러
        return res
          .status(401)
          .json({ errorMessage: '인증 정보가 만료되었습니다.' });
      case 'JsonWebTokenError': // 토큰에 검증이 실패했을 때, 발생하는 에러
        return res
          .status(401)
          .json({ errorMessage: '토큰이 유효하지 않습니다.' });
      default: // 그 외 모든 예외적인 에러 처리
        return res
          .status(401)
          .json({ errorMessage: error.message ?? '비정상적인 요청입니다.' });
      // error.message ?? 의 ??연산자는 왼쪽 피연산자가 null or undefined 일때만 오른쪽 피연산자를 반환 한다.
    }
  }
}

인증을 위해 필요한 작업을 위와같이 쿠키를 활용하여 처리를 하고 process.env.ACCESS_TOKEN_SECRET_KEY   ㄴ

는 .env 를 활용하여 잘감춰 주었다.

그리고 에러처리는 그즉시 인증에서 잘 실행될 수 있도록 따로 에러 미들웨어에서 처리하지 않았다.

 

< error-handler.middleware.js >

export default (err, req, res, next) => {
  console.log('에러 처리 미들웨어가 실행되었습니다.');
  console.error(err.message);
  // joi 에러
  if (err.isJoi) {
    if (err.message === '"email" must be a valid email') {
      return res
        .status(400)
        .json({ errorMessage: '이메일 형식이 올바르지 않습니다.' });
    }
    if (err.message.includes('is required')) {
      const splitErr = err.message.split(' ')[0].replace(/"/g, '');
      // .replace(/"/g, '') 를 사용하면 joi의 오류메세지 따옴표를 제거가능
      return res
        .status(400)
        .json({ errorMessage: `${splitErr}을(를) 입력해주세요.` });
    }
    if (err.message.includes('characters long')) {
      const splitErr = err.message.split(' ')[0].replace(/"/g, '');
      const splitNum = err.message.split(' ')[6];
      return res
        .status(400)
        .json({
          errorMessage: `${splitErr}${splitNum}자 이상이어야 합니다.`,
        });
    }
  }
  // throw 에러
  if (err.message === 'same email') {
    return res.status(400).json({ errorMessage: '이미 가입 된 사용자입니다.' });
  }
  if (err.message === 'not same password') {
    return res
      .status(400)
      .json({ errorMessage: '입력 한 두 비밀번호가 일치하지 않습니다.' });
  }
  if (err.message === 'can not') {
    return res
      .status(401)
      .json({ errorMessage: '인증 정보가 유효하지 않습니다.' });
  }
  if (err.message === 'not asc or desc') {
    return res
      .status(400)
      .json({ errorMessage: 'query에 입력한 값이 asc/desc 가 아닙니다.' });
  }
  if (err.message === 'undefind resumeId') {
    return res.status(400).json({ errorMessage: 'resumeId를 입력해주세요.' });
  }
  if (err.message === 'undefind Resume') {
    return res
      .status(401)
      .json({ errorMessage: '이력서가 존재하지 않습니다.' });
  }
  if (err.message === 'some inner') {
    return res
      .status(400)
      .json({ errorMessage: '수정 할 정보를 입력해 주세요' });
  }

  return res.status(500).json({
    errorMessage: '예상치 못한 에러가 발생했습니다. 관리자에게 문의해 주세요.',
  });
};

에러처리를 모두 담당하는 미들웨어를 작성해 주었다. 

 

이렇게 해서 이번 개인과제가 잘 마무리 되었다. 선택부분도 있었지만 시간관계상 구현하기 힘들어서

다음에 꼭 구현을 해보도록 하겠다.