2025년 1월 7일

김동환·2025년 1월 7일

오늘의 코드

import express from 'express';
import { HTTP_STATUS } from '../constants/http-status.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
import { createResumeValidator } from '../middlewares/validators/create-resume-validator.middleware.js';
import { prisma } from '../utils/prisma.util.js';
import { updateResumeValidator } from '../middlewares/validators/update-resume-validator.middleware.js';

const resumesRouter = express.Router();

// 이력서 생성 
resumesRouter.post('/', createResumeValidator, async (req, res, next) => {
  try {
    const user = req.user; // 인증된 사용자 정보
    const { title, content } = req.body; // 제목과 내용을 바디로 받는다.
    const authorId = user.id; // 작성자 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; // 작성자 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; // 작성자 ID

    const { id } = req.params; // 경로 파라미터에서 ID 추출

    // 특정 이력서 조회
    let data = await prisma.resume.findUnique({
      where: { id: +id, authorId },
      include: { author: true }, // 작성자 정보 포함
    });

    // 데이터가 없을 경우 404 응답
    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; // 작성자 ID

    const { id } = req.params; // 경로 파라미터에서 ID 추출
    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; // 작성자 ID

    const { id } = req.params; // 경로 파라미터에서 ID 추출

    // 기존 이력서 존재 여부 확인
    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); // 오류 처리 미들웨어로 전달
  }
});

export { resumesRouter }; // 이력서 라우터 내보내기

TIL: Express와 Prisma를 사용한 이력서 관리 API 구현

오늘은 Express.jsPrisma를 사용해 이력서 관리 기능을 제공하는 RESTful API를 구현했습니다. 아래는 이번 작업에서 구현한 기능, 경험한 문제점, 그리고 얻은 교훈입니다.


구현한 기능

  1. 이력서 생성 (POST /):

    • createResumeValidator 미들웨어로 요청 데이터 검증.
    • 인증된 사용자를 기반으로 prisma.resume.create를 사용해 새 이력서를 생성.
    • 생성된 이력서 데이터를 포함한 201 Created 상태 응답 반환.
  2. 이력서 목록 조회 (GET /):

    • 인증된 사용자가 작성한 모든 이력서 조회.
    • asc 또는 desc 정렬 기준을 지원하는 쿼리 파라미터(sort) 처리.
    • Prisma의 include 옵션을 사용해 작성자 세부 정보를 포함.
    • 반환 데이터의 형식을 변환하여 사용자 친화적인 응답 제공.
  3. 이력서 상세 조회 (GET /:id):

    • 인증된 사용자의 특정 이력서를 prisma.resume.findUnique로 조회.
    • 이력서가 존재하지 않을 경우 404 Not Found 상태 응답 반환.
  4. 이력서 수정 (PUT /:id):

    • updateResumeValidator 미들웨어를 사용해 요청 데이터 검증.
    • 이력서가 존재하고 사용자 소유인지 확인.
    • title 또는 content와 같은 필드만 부분적으로 업데이트 가능.
    • 업데이트된 이력서 데이터를 포함한 성공 응답 반환.
  5. 이력서 삭제 (DELETE /:id):

    • 삭제할 이력서가 존재하는지 확인 후, 사용자 소유일 경우 삭제.
    • 삭제된 이력서의 ID를 포함한 성공 응답 반환.

핵심 개념과 모범 사례

  1. 미들웨어 활용:

    • 유효성 검사: createResumeValidator, updateResumeValidator와 같은 미들웨어를 통해 데이터 유효성 보장.
    • 오류 처리: next(error)를 사용해 오류를 중앙 오류 처리 미들웨어로 전달.
  2. 인증 정보 활용:

    • req.user를 통해 인증된 사용자의 정보를 가져와 사용자별 작업 수행.
  3. Prisma ORM:

    • prisma.resume.create, findMany, findUnique, update, delete를 사용한 데이터베이스 작업.
    • Prisma의 include 기능을 사용해 작성자 정보를 함께 조회.
  4. 유연한 쿼리 처리:

    • 정렬 기준(sort)과 같은 선택적 쿼리 파라미터를 지원.
    • 잘못된 쿼리 값에 대해 기본값을 설정하여 안전한 동작 보장.
  5. 오류 처리:

    • 존재하지 않는 이력서를 처리할 때 적절한 HTTP 상태 코드(404 Not Found) 반환.
    • try-catch 블록으로 예상치 못한 오류를 처리하고 이를 중앙 오류 처리로 전달.
  6. 동적 업데이트 로직:

    • 스프레드 연산자(...)를 사용해 조건부로 필드를 업데이트.

직면한 문제점

  1. 데이터 무결성 보장:

    • 사용자가 소유하지 않은 이력서에 접근하거나 수정하려는 시도를 처리.
    • 요청 본문의 데이터가 불완전하거나 잘못된 경우에 대한 처리.
  2. 일관된 응답 구조:

    • 모든 엔드포인트가 동일한 응답 형식(status, message, data)을 반환하도록 설정.
  3. Prisma 관련 오류 처리:

    • Prisma가 특정 오류(예: 존재하지 않는 레코드 조회)를 발생시키는 경우를 처리하여 안정성을 확보.

얻은 교훈

  1. 재사용 가능한 미들웨어:

    • 유효성 검사 미들웨어를 사용함으로써 중복된 검증 로직을 줄일 수 있었습니다.
  2. 구조적이고 읽기 쉬운 코드:

    • 라우트를 resumesRouter로 모듈화하여 코드베이스를 더 체계적으로 관리.
  3. 유연한 쿼리 처리:

    • 쿼리 파라미터를 소문자로 변환하고 기본값을 제공함으로써 API의 안정성을 강화.
  4. 효율적인 오류 처리 전략:

    • 중앙 오류 처리 방식을 통해 디버깅과 유지보수를 단순화.

향후 개선 방안

  1. 페이지네이션:
    • 대규모 데이터셋을 효율적으로 처리하기 위해 이력서 목록에 페이지네이션 도입.
  2. 소프트 삭제:
    • 데이터를 복구할 수 있도록 소프트 삭제 기능 구현.
  3. 고급 필터링:
    • 이력서 상태나 제목을 기준으로 한 필터링 기능 추가.

이번 API를 구현하면서 Express.js 라우팅, Prisma ORM, 그리고 스케일링 가능한 REST API 설계에 대한 이해를 심화할 수 있었습니다.

profile
Node.js 7기

0개의 댓글