TIL

Node.js 웹서버 심화 주차 개인 과제 (3)

황민도 2024. 6. 17. 21:59

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

일단 내용을 작성하기 전에 아래 모든 내용은 완전한 코드가 아니므로 나중에 수정작업을 통해 완전한 코드로 다시 구현해볼것이다. 이번 내용은 불완전한 코드로 되는대로 해본 임의적인 코드이기 때문에 사용하기엔 부적합하다.

 

 

[ 피드백 ]

레이어 분리

Controller가 Service 역할까지 담당하고 있습니다.

MDSTART-SHOP-RESUME-LAYERED/src/controllers/auth.controller.js

Lines 21 to 32 in 823e0d4

    const existedUser = await this.authService.findUsers(email); 
  
    if (existedUser) { 
      throw new HttpError.Conflict(MESSAGES.AUTH.COMMON.EMAIL.DUPLICATED); 
    } 
 const hashedPassword = bcrypt.hashSync(password, HASH_SALT_ROUNDS); 
  
    const createdUser = await this.authService.createDatas( 
      email, 
      hashedPassword, 
      name, 
    );

MDSTART-SHOP-RESUME-LAYERED/src/controllers/auth.controller.js

Lines 48 to 60 in 823e0d4

 const user = await this.authService.findUsers(email); 
 const isPasswordMatched = 
   user && bcrypt.compareSync(password, user.password); 
  
 if (!isPasswordMatched) { 
   throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.UNAUTHORIZED); 
 } 
  
 const payload = { id: user.id }; 
  
 const accessToken = jwt.sign(payload, ACCESS_TOKEN_SECRET, { 
   expiresIn: ACCESS_TOKEN_EXPIRES_IN, 
 });

 

 

< src.errors. http.error.js >

import { HTTP_STATUS } from '../constants/http-status.constant.js';

class BadRequest {
  constructor(message = BadRequest.name) {
    this.message = message;
    this.status = HTTP_STATUS.BAD_REQUEST;
  }
}

class Unauthorized {
  constructor(message = Unauthorized.name) {
    this.message = message;
    this.status = HTTP_STATUS.UNAUTHORIZED;
  }
}

class Forbidden {
  constructor(message = Forbidden.name) {
    this.message = message;
    this.status = HTTP_STATUS.FORBIDDEN;
  }
}

class NotFound {
  constructor(message = NotFound.name) {
    this.message = message;
    this.status = HTTP_STATUS.NOT_FOUND;
  }
}

class Conflict {
  constructor(message = Conflict.name) {
    this.message = message;
    this.status = HTTP_STATUS.CONFLICT;
  }
}

class InternalServerError {
  constructor(message = InternalServerError.name) {
    this.message = message;
    this.status = HTTP_STATUS.INTERNAL_SERVER_ERROR;
  }
}

export const HttpError = {
  BadRequest,
  Unauthorized,
  Forbidden,
  NotFound,
  Conflict,
  InternalServerError,
};

 

< src.routers. index.js >

import express from 'express';
import { authRouter } from './auth.router.js';
import { usersRouter } from './users.router.js';
import { resumesRouter } from './resumes.router.js';
import { requireAccessToken } from '../middlewares/require-access-token.middleware.js';

const apiRouter = express.Router();

apiRouter.use('/auth', authRouter);
apiRouter.use('/users', usersRouter);
apiRouter.use('/resumes', requireAccessToken, resumesRouter);

export { apiRouter };

 

[ 기존 src.routers. auth.router.js ]

authRouter.post('/sign-up', signUpValidator, async (req, res, next) => {
  try {
    const { email, password, name } = req.body;

    const existedUser = await prisma.user.findUnique({ where: { email } });

    // 이메일이 중복된 경우
    if (existedUser) {
      return res.status(HTTP_STATUS.CONFLICT).json({
        status: HTTP_STATUS.CONFLICT,
        message: MESSAGES.AUTH.COMMON.EMAIL.DUPLICATED,
      });
    }

    const hashedPassword = bcrypt.hashSync(password, HASH_SALT_ROUNDS);

    const data = await prisma.user.create({
      data: {
        email,
        password: hashedPassword,
        name,
      },
    });

    data.password = undefined;

    return res.status(HTTP_STATUS.CREATED).json({
      status: HTTP_STATUS.CREATED,
      message: MESSAGES.AUTH.SIGN_UP.SUCCEED,
      data,
    });
  } catch (error) {
    next(error);
  }
});

authRouter.post('/sign-in', signInValidator, async (req, res, next) => {
  try {
    const { email, password } = req.body;

    const user = await prisma.user.findUnique({ where: { email } });

    const isPasswordMatched =
      user && bcrypt.compareSync(password, user.password);

    if (!isPasswordMatched) {
      return res.status(HTTP_STATUS.UNAUTHORIZED).json({
        status: HTTP_STATUS.UNAUTHORIZED,
        message: MESSAGES.AUTH.COMMON.UNAUTHORIZED,
      });
    }

    const payload = { id: user.id };

    const accessToken = jwt.sign(payload, ACCESS_TOKEN_SECRET, {
      expiresIn: ACCESS_TOKEN_EXPIRES_IN,
    });

    return res.status(HTTP_STATUS.OK).json({
      status: HTTP_STATUS.OK,
      message: MESSAGES.AUTH.SIGN_IN.SUCCEED,
      data: { accessToken },
    });
  } catch (error) {
    next(error);
  }
});

 

< src.routers. auth.router.js >

import express from 'express';
import { signUpValidator } from '../middlewares/validators/sign-up-validator.middleware.js';
import { signInValidator } from '../middlewares/validators/sign-in-validator.middleware.js';
import { AuthController } from '../controllers/auth.controller.js';

const authRouter = express.Router();
const authController = new AuthController();

// 회원가입
authRouter.post('/sign-up', signUpValidator, authController.createUsers);

// 로그인
authRouter.post('/sign-in', signInValidator, authController.getTokens);



export { authRouter };

 

< src.controllers. auth.controller.js >

import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import {
  ACCESS_TOKEN_EXPIRES_IN,
  HASH_SALT_ROUNDS,
} from '../constants/auth.constant.js';
import { ACCESS_TOKEN_SECRET } from '../constants/env.constant.js';

import { HTTP_STATUS } from '../constants/http-status.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
import { AuthService } from '../services/auth.service.js';
import { HttpError } from '../errors/http.error.js';

export class AuthController {
  authService = new AuthService();

  // 회원가입
  createUsers = async (req, res, next) => {
    try {
      const { email, password, name } = req.body;
      const existedUser = await this.authService.findUsers(email);

      if (existedUser) {
        throw new HttpError.Conflict(MESSAGES.AUTH.COMMON.EMAIL.DUPLICATED);
      }
    const hashedPassword = bcrypt.hashSync(password, HASH_SALT_ROUNDS);

      const createdUser = await this.authService.createDatas(
        email,
        hashedPassword,
        name,
      );

      return res.status(HTTP_STATUS.CREATED).json({
        status: HTTP_STATUS.CREATED,
        message: MESSAGES.AUTH.SIGN_UP.SUCCEED,
        data: createdUser,
      });
    } catch (error) {
      next(error);
    }
  };

  // 로그인
  getTokens = async (req, res, next) => {
    try {
      const { email, password } = req.body;
      const user = await this.authService.findUsers(email);
      const isPasswordMatched =
        user && bcrypt.compareSync(password, user.password);

      if (!isPasswordMatched) {
        throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.UNAUTHORIZED);
      }

      const payload = { id: user.id };

      const accessToken = jwt.sign(payload, ACCESS_TOKEN_SECRET, {
        expiresIn: ACCESS_TOKEN_EXPIRES_IN,
      });

      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.AUTH.SIGN_IN.SUCCEED,
        data: { accessToken },
      });
    } catch (error) {
      next(error);
    }
  };
}

 

< src.services. auth.service.js >

import { UsersRepository } from '../repositories/users.repository.js';

export class AuthService {
  usersRepository = new UsersRepository();

  findUsers = async (email) => {
    const findedUser = await this.usersRepository.findUser(email);

    return findedUser;
  };

  createDatas = async (email, hashedPassword, name) => {
    const createdData = await this.usersRepository.createData(
      email,
      hashedPassword,
      name,
    );

    return {
      id: createdData.id,
      email: createdData.email,
      name: createdData.name,
      role: createdData.role,
      createdAt: createdData.createdAt,
      updatedAt: createdData.updatedAt,
    };
  };
}

 

[ 기존 src.routers. users.router.js ]

usersRouter.get('/me', requireAccessToken, (req, res, next) => {
  try {
    const data = req.user;

    return res.status(HTTP_STATUS.OK).json({
      status: HTTP_STATUS.OK,
      message: MESSAGES.USERS.READ_ME.SUCCEED,
      data,
    });
  } catch (error) {
    next(error);
  }
});

 

< src.routers. users.router.js >

import express from 'express';
import { requireAccessToken } from '../middlewares/require-access-token.middleware.js';
import { UsersController } from '../controllers/users.controller.js';

const usersRouter = express.Router();
const usersController = new UsersController();

usersRouter.get('/me', requireAccessToken, usersController.getUsers);

export { usersRouter };

 

< src.controllers. users.controller.js >

import { HTTP_STATUS } from '../constants/http-status.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
export class UsersController {
  getUsers = async (req, res, next) => {
    try {
      const data = req.user;

      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.USERS.READ_ME.SUCCEED,
        data,
      });
    } catch (error) {
      next(error);
    }
  };
}

 

< src.repositories. users.repository.js >

import { prisma } from '../utils/prisma.util.js';

export class UsersRepository {
  // auth findUser
  findUser = async (email) => {
    const findedUser = await prisma.user.findUnique({ where: { email } });

    return findedUser;
  };

  // auth createData
  createData = async (email, hashedPassword, name) => {
    await prisma.user.create({
      data: {
        email,
        password: hashedPassword,
        name,
      },
    });

    const data = await prisma.user.findFirst({ where: { email } });

    return data;
  };

  // require-access-token findUser blind password
  findUserBpw = async (id) => {
    const findedUserBpw = await prisma.user.findUnique({
      where: { id },
      omit: { password: true },
    });

    return findedUserBpw;
  };
}

 

[ 기존 src.routers. resumes.router.js ]

// 이력서 생성
resumesRouter.post('/', createResumeValidator, async (req, res, next) => {
  try {
    const user = req.user;
    const { title, content } = req.body;
    const authorId = user.id;

    const data = await prisma.resume.create({
      data: {
        authorId,
        title,
        content,
      },
    });

    return res.status(HTTP_STATUS.CREATED).json({
      status: HTTP_STATUS.CREATED,
      message: MESSAGES.RESUMES.CREATE.SUCCEED,
      data,
    });
  } catch (error) {
    next(error);
  }
});

// 이력서 목록 조회
resumesRouter.get('/', async (req, res, next) => {
  try {
    const user = req.user;
    const authorId = user.id;

    let { sort } = req.query;

    sort = sort?.toLowerCase();

    if (sort !== 'desc' && sort !== 'asc') {
      sort = 'desc';
    }

    let data = await prisma.resume.findMany({
      where: { authorId },
      orderBy: {
        createdAt: sort,
      },
      include: {
        author: true,
      },
    });

    data = data.map((resume) => {
      return {
        id: resume.id,
        authorName: resume.author.name,
        title: resume.title,
        content: resume.content,
        status: resume.status,
        createdAt: resume.createdAt,
        updatedAt: resume.updatedAt,
      };
    });

    return res.status(HTTP_STATUS.OK).json({
      status: HTTP_STATUS.OK,
      message: MESSAGES.RESUMES.READ_LIST.SUCCEED,
      data,
    });
  } catch (error) {
    next(error);
  }
});

// 이력서 상세 조회
resumesRouter.get('/:id', async (req, res, next) => {
  try {
    const user = req.user;
    const authorId = user.id;

    const { id } = req.params;

    let data = await prisma.resume.findUnique({
      where: { id: +id, authorId },
      include: { author: true },
    });

    if (!data) {
      return res.status(HTTP_STATUS.NOT_FOUND).json({
        status: HTTP_STATUS.NOT_FOUND,
        message: MESSAGES.RESUMES.COMMON.NOT_FOUND,
      });
    }

    data = {
      id: data.id,
      authorName: data.author.name,
      title: data.title,
      content: data.content,
      status: data.status,
      createdAt: data.createdAt,
      updatedAt: data.updatedAt,
    };

    return res.status(HTTP_STATUS.OK).json({
      status: HTTP_STATUS.OK,
      message: MESSAGES.RESUMES.READ_DETAIL.SUCCEED,
      data,
    });
  } catch (error) {
    next(error);
  }
});

// 이력서 수정
resumesRouter.put('/:id', updateResumeValidator, async (req, res, next) => {
  try {
    const user = req.user;
    const authorId = user.id;

    const { id } = req.params;

    const { title, content } = req.body;

    let existedResume = await prisma.resume.findUnique({
      where: { id: +id, authorId },
    });

    if (!existedResume) {
      return res.status(HTTP_STATUS.NOT_FOUND).json({
        status: HTTP_STATUS.NOT_FOUND,
        message: MESSAGES.RESUMES.COMMON.NOT_FOUND,
      });
    }

    const data = await prisma.resume.update({
      where: { id: +id, authorId },
      data: {
        ...(title && { title }),
        ...(content && { content }),
      },
    });

    return res.status(HTTP_STATUS.OK).json({
      status: HTTP_STATUS.OK,
      message: MESSAGES.RESUMES.UPDATE.SUCCEED,
      data,
    });
  } catch (error) {
    next(error);
  }
});

// 이력서 삭제
resumesRouter.delete('/:id', async (req, res, next) => {
  try {
    const user = req.user;
    const authorId = user.id;

    const { id } = req.params;

    let existedResume = await prisma.resume.findUnique({
      where: { id: +id, authorId },
    });

    if (!existedResume) {
      return res.status(HTTP_STATUS.NOT_FOUND).json({
        status: HTTP_STATUS.NOT_FOUND,
        message: MESSAGES.RESUMES.COMMON.NOT_FOUND,
      });
    }

    const data = await prisma.resume.delete({ where: { id: +id, authorId } });

    return res.status(HTTP_STATUS.OK).json({
      status: HTTP_STATUS.OK,
      message: MESSAGES.RESUMES.DELETE.SUCCEED,
      data: { id: data.id },
    });
  } catch (error) {
    next(error);
  }
});

 

< src.routers. resumes.router.js >

import express from 'express';
import { createResumeValidator } from '../middlewares/validators/create-resume-validator.middleware.js';
import { updateResumeValidator } from '../middlewares/validators/updated-resume-validator.middleware.js';
import { ResumesController } from '../controllers/resumes.controller.js';

const resumesRouter = express.Router();
const resumesController = new ResumesController();

// 이력서 생성
resumesRouter.post('/', createResumeValidator, resumesController.createResumes);

// 이력서 목록 조회
resumesRouter.get('/', resumesController.getAllResumes);

// 이력서 상세 조회
resumesRouter.get('/:id', resumesController.getResumes);

// 이력서 수정
resumesRouter.put('/:id', updateResumeValidator, resumesController.putResumes);

// 이력서 삭제
resumesRouter.delete('/:id', resumesController.deleteResumes)


export { resumesRouter };

 

< src.controllers. resumes.controller.js >

import { HTTP_STATUS } from '../constants/http-status.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
import { ResumesService } from '../services/resumes.service.js';
import { HttpError } from '../errors/http.error.js';
export class ResumesController {
  resumesService = new ResumesService();

  // 이력서 생성
  createResumes = async (req, res, next) => {
    try {
      const user = req.user;
      const { title, content } = req.body;
      const authorId = user.id;

      const createdResume = await this.resumesService.createResume(
        authorId,
        title,
        content,
      );

      return res.status(HTTP_STATUS.CREATED).json({
        status: HTTP_STATUS.CREATED,
        message: MESSAGES.RESUMES.CREATE.SUCCEED,
        data: createdResume,
      });
    } catch (error) {
      next(error);
    }
  };

  // 이력서 목록 조회
  getAllResumes = async (req, res, next) => {
    try {
      const user = req.user;
      const authorId = user.id;
      let { sort } = req.query;
      sort = sort?.toLowerCase();

      const getedAllResume = await this.resumesService.getAllResume(
        authorId,
        sort,
      );
      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.RESUMES.READ_LIST.SUCCEED,
        data: getedAllResume,
      });
    } catch (error) {
      next(error);
    }
  };

  // 이력서 상세 조회
  getResumes = async (req, res, next) => {
    try {
      const user = req.user;
      const authorId = user.id;
      const { id } = req.params;

      const getedResume = await this.resumesService.getResume(authorId, id);

      if (!getedResume) {
        throw new HttpError.NotFound(MESSAGES.RESUMES.COMMON.NOT_FOUND);
      }

      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.RESUMES.READ_DETAIL.SUCCEED,
        data: getedResume,
      });
    } catch (error) {
      next(error);
    }
  };

  // 이력서 수정
  putResumes = async (req, res, next) => {
    try {
      const user = req.user;
      const authorId = user.id;
      const { id } = req.params;
      const { title, content } = req.body;

      const existedResume = await this.resumesService.findId(authorId, id);

      if (!existedResume) {
        throw new HttpError.NotFound(MESSAGES.RESUMES.COMMON.NOT_FOUND);
      }

      const putInResume = await this.resumesService.putResume(
        authorId,
        id,
        title,
        content,
      );
      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.RESUMES.UPDATE.SUCCEED,
        data: putInResume,
      });
    } catch (error) {
      next(error);
    }
  };

  // 이력서 삭제
  deleteResumes = async (req, res, next) => {
    try {
      const user = req.user;
      const authorId = user.id;
      const { id } = req.params;
      const existedResume = await this.resumesService.findId(authorId, id);

      if (!existedResume) {
        throw new HttpError.NotFound(MESSAGES.RESUMES.COMMON.NOT_FOUND);
      }

      const deletedResume = await this.resumesService.deleteResume(authorId, id);

      return res.status(HTTP_STATUS.OK).json({
        status: HTTP_STATUS.OK,
        message: MESSAGES.RESUMES.DELETE.SUCCEED,
        data: deletedResume.id,
      });
    } catch (error) {
      next(error);
    }
  };
}

 

< src.services. resumes.service.js >

import { ResumesRepository } from '../repositories/resumes.repository.js';
export class ResumesService {
  resumesRepository = new ResumesRepository();

  // 이력서 생성
  createResume = async (authorId, title, content) => {
    const createdResume = await this.resumesRepository.createResume(
      authorId,
      title,
      content,
    );

    return {
      id: createdResume.id,
      authorName: createdResume.author.name,
      title: createdResume.title,
      content: createdResume.content,
      status: createdResume.status,
      createdAt: createdResume.createdAt,
      updatedAt: createdResume.updatedAt,
    };
  };

  // 이력서 목록 조회
  getAllResume = async (authorId, sort) => {
    if (sort !== 'desc' && sort !== 'asc') {
      sort = 'desc';
    }

    const resumes = await this.resumesRepository.getAllResume(authorId, sort);

    return resumes.map((resume) => {
      return {
        id: resume.id,
        authorName: resume.author.name,
        title: resume.title,
        content: resume.content,
        status: resume.status,
        createdAt: resume.createdAt,
        updatedAt: resume.updatedAt,
      };
    });
  };

  // 이력서 상세 조회
  getResume = async (authorId, id) => {
  const getedResume = await this.resumesRepository.getResume(authorId, id);
 
    return {
    id: getedResume.id,
    authorName: getedResume.author.name,
    title: getedResume.title,
    content: getedResume.content,
    status: getedResume.status,
    createdAt: getedResume.createdAt,
    updatedAt: getedResume.updatedAt,
    };
  };

  // findUnique()
  findId = async (authorId, id) => {
  const existedResume = await this.resumesRepository.findId(authorId, id);

  return existedResume;
  };

  // 이력서 수정
  putResume = async (authorId, id, title, content) => {
  const putInResume = await this.resumesRepository.putResume(
        authorId,
        id,
        title,
        content,
      );

    return {
    id: putInResume.id,
    authorName: putInResume.author.name,
    title: putInResume.title,
    content: putInResume.content,
    status: putInResume.status,
    createdAt: putInResume.createdAt,
    updatedAt: putInResume.updatedAt,
    };
  };

  // 이력서 삭제
  deleteResume = async (authorId, id) => {
  const deletedResume = await this.resumesRepository.deleteResume(authorId, id);

  return deletedResume;
  };
}

 

< src.repositories. resumes.repository.js >

import { prisma } from '../utils/prisma.util.js';

export class ResumesRepository {
  // 이력서 생성
  createResume = async (authorId, title, content) => {
    await prisma.resume.create({
      data: {
        authorId,
        title,
        content,
      }
    });

  const data = await prisma.resume.findFirst({
    where: { authorId },
    include: { author: true },
    });

    return data;
  };

  // 이력서 목록 조회
  getAllResume = async (authorId, sort) => {
    const resumes = await prisma.resume.findMany({
      where: { authorId },
      orderBy: {
        createdAt: sort,
      },
      include: {
        author: true,
      },
    });

    return resumes;
  };

  // 이력서 상세 조회
  getResume = async (authorId, id) => {
    const getedResume = await prisma.resume.findUnique({
      where: { id: +id, authorId },
      include: { author: true },
    });

    return getedResume;
  };

  // findUnique()
  findId = async (authorId, id) => {
    const existedResume = await prisma.resume.findUnique({
      where: { id: +id, authorId },
    });

    return existedResume;
  };

  // 이력서 수정
  putResume = async (authorId, id, title, content) => {
    await prisma.resume.update({
      where: { id: +id, authorId },
      data: {
        ...(title && { title }),
        ...(content && { content }),
      },
    });

  const data = await prisma.resume.findFirst({
    where: { authorId },
    include: { author: true },
    });

    return data;
  };

  // 이력서 삭제
  deleteResume = async (authorId, id) => {
    const deletedResume = await prisma.resume.delete({
      where: { id: +id, authorId },
    });

    return deletedResume;
  };
}