Skip to content

[백은결] sprint6#18

Open
eungyeolbaek wants to merge 7 commits intocodeit-sprint-fullstack:express-백은결from
eungyeolbaek:express-백은결-sprint6

Hidden character warning

The head ref may contain hidden characters: "express-\ubc31\uc740\uacb0-sprint6"
Open

[백은결] sprint6#18
eungyeolbaek wants to merge 7 commits intocodeit-sprint-fullstack:express-백은결from
eungyeolbaek:express-백은결-sprint6

Conversation

@eungyeolbaek
Copy link
Copy Markdown
Collaborator

요구사항

기본 요구사항

중고마켓

  • mongoDB에서 PostgreSQL을 사용하도록 코드를 마이그레이션 해주세요.

공통

  • PostgreSQL를 이용해 주세요.
  • 데이터 모델 간의 관계를 고려하여 onDelete를 설정해 주세요.
  • 데이터베이스 시딩 코드를 작성해 주세요.
  • 각 API에 적절한 에러 처리를 해 주세요.
  • 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요.

자유게시판

  • Article 스키마를 작성해 주세요.
  • id, title, content, createdAt, updatedAt 필드를 가집니다.
  • 게시글 등록 API를 만들어 주세요.
  • title, content를 입력해 게시글을 등록합니다.
  • 게시글 조회 API를 만들어 주세요.
  • id, title, content, createdAt를 조회합니다.
  • 게시글 수정 API를 만들어 주세요.
  • 게시글 삭제 API를 만들어 주세요.
  • 게시글 목록 조회 API를 만들어 주세요.
  • id, title, content, createdAt를 조회합니다.
  • offset 방식의 페이지네이션 기능을 포함해 주세요.
  • 최신순(recent)으로 정렬할 수 있습니다.
  • title, content에 포함된 단어로 검색할 수 있습니다.

댓글

  • 댓글 등록 API를 만들어 주세요.
  • content를 입력하여 댓글을 등록합니다.
  • 중고마켓, 자유게시판 댓글 등록 API를 따로 만들어 주세요.
  • 댓글 수정 API를 만들어 주세요.
  • PATCH 메서드를 사용해 주세요.
  • 댓글 삭제 API를 만들어 주세요.
  • 댓글 목록 조회 API를 만들어 주세요.
  • id, content, createdAt 를 조회합니다.
  • cursor 방식의 페이지네이션 기능을 포함해 주세요.
  • 중고마켓, 자유게시판 댓글 목록 조회 API를 따로 만들어 주세요.

🔧 주요 변경사항

  • mongoDBPostgreSQL 마이그레이션
  • Prisma 기반 스키마 설계 및 마이그레이션 적용
  • Article 스키마와 요구사항에는 없으나 도메인상 필요하다고 판단하여 Comment 스키마 추가
  • 테스트를 위한 시딩 코드 추가
  • repository 레이어 까지 관심사 분리

💬 멘토님께 남기는 메시지

  • 시딩 코드 작성 시, 테스트 데이터 생성을 위해 Faker + AI를 활용했습니다.
  • 라우터/레포지토리/스키마 간 역할분리는 어느정도 감이 이해가 된 것 같은데 항상 server.js에서 마운트 하는 부분과 라우터 간의 연결이 너무 어렵습니다ㅠㅠ
  • 댓글 생성, 조회 API는 상품 댓글 / 게시글 댓글로 각각 분리하고,
    댓글 수정, 삭제 API는 전역 댓글 라우터에서 처리했습니다. 이 구조가 적절한지 궁금합니다.
  • Article, Product, Comment 모두 cuid를 사용해서 ID를 만들었는데 실무에서도 uuidcuid를 주로 사용하는지, 아니면 쓰임새에 따라 int형 ID를 사용하는 경우도 있는지 궁금합니다.

- MongoDB에서 PostgreSQL로 마이그레이션 (Prisma 사용)
- Article 스키마 작성 및 CRUD API 구현 (Offset 페이지네이션 및 검색 포함)
- 중고마켓/자유게시판 댓글 API 분리 및 구현 (Cursor 페이지네이션 포함)
- 데이터 모델 간 onDelete 설정 및 데이터베이스 시딩 코드 작성
- 공통 에러 처리 및 적절한 HTTP 상태 코드 적용
@eungyeolbaek eungyeolbaek self-assigned this Jan 25, 2026
@eungyeolbaek eungyeolbaek added 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 최종제출 스프린트 미션 최종 제출 PR입니다. 코드리뷰 및 평가해주세요! labels Jan 25, 2026
@reach0908
Copy link
Copy Markdown
Collaborator

  1. 시딩 코드 작성 시, 테스트 데이터 생성을 위해 Faker + AI를 활용했습니다.

-> 잘 하셨습니다! 좋은 방법이에요

  1. 라우터/레포지토리/스키마 간 역할분리는 어느정도 감이 이해가 된 것 같은데 항상 server.js에서 마운트 하는 부분과 라우터 간의 연결이 너무 어렵습니다ㅠㅠ

->

server.js
  └── app.use('/api', apiRouter)
        │
        └── routes/index.js
              ├── router.use('/products', productRouter)
              │     └── products/index.js
              │           ├── productRouter.use('/', productsRouter)        → /api/products/*
              │           └── productRouter.use('/:id/comments', ...)       → /api/products/:id/comments/*
              │
              ├── router.use('/articles', articleRouter)
              │     └── articles/index.js
              │           ├── articleRouter.use('/', articlesRouter)        → /api/articles/*
              │           └── articleRouter.use('/:id/comments', ...)       → /api/articles/:id/comments/*
              │
              └── router.use('/comments', commentRouter)                     → /api/comments/*

멘토링에서 mergeParams에 관해 이야기하긴했지만 아래 내용들을 잘 알고 계시면 좋을 것 같아요! 항상 이해가 안될때는 AI에게 mermaid, excllidraw와 같은 시각화 툴을 이용해서 지금 어떻게 되어있는 시각화를 그려달라고 하면 좋습니다.

  • 경로는 누적됩니다: /api + /products + /:id = /api/products/:id
  • mergeParams: true를 설정하면 부모 라우터의 params를 자식에서 사용 가능
  1. 댓글 생성, 조회 API는 상품 댓글 / 게시글 댓글로 각각 분리하고, 댓글 수정, 삭제 API는 전역 댓글 라우터에서 처리했습니다. 이 구조가 적절한지 궁금합니다.

-> 라우터 자체는 적절히 잘 설계된 것 같으나 추후 댓글이라는 기능이 다른 도메인에서도 사용된다고 하면 중복되는 코드들이 많아질 수도 있을 것 같아요 그때는 댓글 router를 별도로 파서 request 바디에 어디에 해당하는 댓글인지 받아서 처리하는 방법도 있을 것 같습니다. 문제는 아직 없고 좋은 것 같습니다!

  1. Article, Product, Comment 모두 cuid를 사용해서 ID를 만들었는데 실무에서도 uuid나 cuid를 주로 사용하는지, 아니면 쓰임새에 따라 int형 ID를 사용하는 경우도 있는지 궁금합니다.

-> 이제 UUID에도 버전에 따라 갖고있는 특성이 다르다보니 적절한 아이디들을 잘 설정하시면 좋습니다. UUIDv4를 기준으로 설명했을 때 완전 랜덤이고 ID기반 정렬이나 조회가 ID만으로 이뤄지는 경우 혹은 기타 다른 필드를 통해 조회가 이뤄지면 괜찮고 CUID의 경우 시간순으로 정렬할 수 있다보니 Id

추후 DB에서 인덱스라는 것을 통해 성능 최적화 측면에서 고려되어야할 부분이기도 합니다. 대부분의 도메인에 이미 적절한 ID 선정 모범사례들이 있으니 잘 리서치하고 적용하시면 좋습니다.

| 유형 | 예시 | 길이 | 특징 |
|------|------|------|------|
| Auto-increment INT | 1, 2, 3 | 짧음 | 예측 가능, 단일 DB에서 효율적 |
| UUID v4 | 550e8400-e29b-41d4-a716-446655440000 | 36자 | 완전 랜덤, 분산 시스템에 적합 |
| CUID | cjld2cjxh0000qzrmn831i7rn | 25자 | 시간순 정렬 가능, URL-safe |
| ULID | 01ARZ3NDEKTSV4RRFFQ69G5FAV | 26자 | 시간순 정렬, UUID와 호환 |
| Snowflake | 1541815603606036480 | 19자(숫자) | Twitter 개발, 분산 시스템 |

Copy link
Copy Markdown
Collaborator

@reach0908 reach0908 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 리뷰 - [백은결] sprint6

전체적으로 MongoDB에서 PostgreSQL로의 마이그레이션이 잘 진행되었습니다. Repository 패턴 도입, zod를 활용한 유효성 검사, Graceful shutdown 등 좋은 설계가 많이 보입니다.

요약

  • 잘한 점: Repository 패턴, validate 미들웨어, onDelete Cascade, 에러 핸들링 구조
  • 개선 필요: 라우트 경로 슬래시, 댓글 필터링, Article 스키마 필드

아래 인라인 코멘트들을 확인해주세요. 👇

@@ -0,0 +1,96 @@
import { prisma } from '#db/prisma.js';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[좋습니다 👏] Repository 패턴 도입이 훌륭합니다!

데이터베이스 로직을 별도 레이어로 분리하여:

  • 라우트 핸들러가 깔끔해짐
  • 테스트 시 mock 주입 용이
  • 나중에 ORM 변경 시 이 레이어만 수정하면 됨

좋은 아키텍처 선택입니다.

if (!['body', 'query', 'params'].includes(target)) {
throw new Error(
`[validate middleware] Invalid target: "${target}". Expected "body", "query", or "params".`,
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[좋습니다 👏] 범용 validate 미들웨어가 매우 잘 설계되어 있습니다!

  • target 파라미터로 body/query/params 유연하게 처리
  • zod 스키마와 깔끔하게 통합
  • 개발/운영 환경별 에러 응답 분리

재사용성이 높고 확장하기 좋은 패턴입니다.

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[좋습니다 👍] onDelete: Cascade 설정이 잘 되어 있습니다! 부모 레코드(Article/Product) 삭제 시 연관 댓글이 자동으로 삭제되어 데이터 무결성이 유지됩니다.


model Article {
id String @id @default(cuid())
title String
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[개선 필요] 요구사항에 따르면 Article에 image 필드와 좋아요순 정렬을 위한 likeCount 필드가 필요합니다.

model Article {
  id        String    @id @default(cuid())
  title     String
  content   String
  image     String?   // 이미지 URL (optional)
  likeCount Int       @default(0)  // 좋아요순 정렬용
  comments  Comment[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

@@ -0,0 +1,64 @@
// Prisma 에러 코드 상수
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[좋습니다 👏] 에러 메시지를 상수로 중앙 관리하는 것이 좋습니다!

  • 일관된 에러 메시지 유지
  • 다국어 지원 시 이 파일만 수정하면 됨
  • 오타 방지

HTTP_STATUS와 함께 사용하여 응답의 일관성이 높습니다.


// GET /api/products/:productId/comments - 특정 상품의 댓글 목록 조회
productCommentsRouter.get(
'/',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[개선 필요] 특정 상품의 댓글만 조회해야 하는데, productId로 필터링하지 않고 전체 댓글을 조회하고 있습니다.

req.params에서 productId (또는 mergeParams로 전달된 id)를 가져와서 필터링해야 합니다.

const { id: productId } = req.params;  // mergeParams로 부모의 :id 접근

const comments = await commentRepository.getProductCommentsWithCursor({
  productId,  // 필터 추가
  cursorId,
  limit,
});


// GET /api/articles/:articleId/comments - 특정 게시글의 댓글 목록 조회
articleCommentsRouter.get(
'/',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[개선 필요] 상품 댓글과 동일한 이슈입니다. articleId로 필터링이 필요합니다.

const { id: articleId } = req.params;

const comments = await commentRepository.getArticleCommentsWithCursor({
  articleId,
  cursorId,
  limit,
});

async function getProductCommentsWithCursor({ cursorId, limit = 10 }) {
const comments = await prisma.comment.findMany({
take: limit,
skip: cursorId ? 1 : 0, // cursor 포함 여부 조절
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[버그 🐛] Comment의 id는 cuid() (문자열)인데, Number(cursorId)로 변환하고 있습니다. cuid는 숫자가 아니므로 항상 NaN이 됩니다.

- cursor: cursorId ? { id: Number(cursorId) } : undefined,
+ cursor: cursorId ? { id: cursorId } : undefined,  // 문자열 그대로 사용

description: {
contains: search,
mode: 'insensitive',
},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[사소함] CommentsCommnets로 오타가 있습니다.

- function findProductWithCommnets(id)
+ function findProductWithComments(id)

"main": "src/server.js",
"type": "module",
"engines": {
"node": ">=22.0.0"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[좋습니다 👏] subpath imports (#constants, #repository 등) 설정이 깔끔합니다!

상대 경로 지옥(../../../)을 피하고 import 경로가 훨씬 읽기 쉬워집니다. 모던 Node.js 프로젝트의 좋은 패턴입니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 최종제출 스프린트 미션 최종 제출 PR입니다. 코드리뷰 및 평가해주세요!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants