diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..22f51a26 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +node_modules/ +.env* +!.env.example +dist + +uploads + +coverage +*.test.js + +*.md + +.git +.gitignore +.github + +src/generated/ + +#dev test files +http +example \ No newline at end of file diff --git a/.env.example b/.env.example index 253781f8..62ccd697 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,9 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/sprint_mission?schema=public" +POSTGRES_USER="postgres" +POSTGRES_PASSWORD="password" +POSTGRES_DB="sprint_mission" + + PORT=3000 SOCKET_PORT=8080 JWT_SECRET="t7HVUdpWIjfHXhZGGIkZHxuFyBPXixJr" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..422604c6 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,56 @@ +name: Deploy to EC2 + +on: + push: + branches: [main, 박재성, 박재성-sprint11] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v5 + + - name: Node.js 설치 + uses: actions/setup-node@v5 + with: + node-version: 'lts/*' + + - name: EC2 배포 + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script: | + # 코드 최신화 + cd ~/3-sprint-mission + git pull origin main + + # 패키지 설치 및 DB 마이그레이션 + npm install + npx prisma generate + npx prisma migrate deploy + + # 빌드 및 재시작 + npm run build + + # PM2로 애플리케이션 관리 + pm2 stop 3-sprint-mission || true + pm2 delete 3-sprint-mission || true + pm2 start dist/main.js --name "3-sprint-mission" --env production + + # 배포 검증 + pm2 status + # curl -f http://localhost:3000/health || exit 1 + + - name: 배포 성공 알림 + if: success() + run: | + echo "✅ 배포가 성공적으로 완료되었습니다!" + + - name: 배포 실패 알림 + if: failure() + run: | + echo "❌ 배포가 실패했습니다!" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..63294623 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,67 @@ +name: Test + +on: + push: + branches: [main, 박재성, 박재성-sprint11] + pull_request: + branches: [main, 박재성, 박재성-sprint11] + +jobs: + test: + runs-on: ubuntu-latest + + # PostgreSQL 서비스 컨테이너 (테스트용 DB 자동 생성) + services: + postgres: + image: postgres:17.5 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: test_password + POSTGRES_DB: test-sprint_mission + ports: + - 5432:5432 + # DB 준비 완료까지 대기 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v5 + + - name: Node.js 설치 + uses: actions/setup-node@v5 + with: + node-version: 'lts/*' + + - name: 패키지 설치 + run: npm install + + - name: Prisma 클라이언트 생성 + env: + DATABASE_URL: postgresql://postgres:test_password@localhost:5432/test-sprint_mission?schema=public + run: npx prisma generate + + - name: TypeScript 타입 체크 + run: npm run build + + - name: DB 스키마 생성 (테이블 생성) + env: + DATABASE_URL: postgresql://postgres:test_password@localhost:5432/test-sprint_mission?schema=public + run: npx prisma migrate deploy + + - name: 테스트 데이터 삽입 + env: + DATABASE_URL: postgresql://postgres:test_password@localhost:5432/test-sprint_mission?schema=public + run: npx prisma db seed + + - name: 테스트 실행 + env: + DATABASE_URL: postgresql://postgres:test_password@localhost:5432/test-sprint_mission?schema=public + NODE_ENV: test + PORT: 3000 + ACCESS_TOKEN_SECRET: test_access_token_secret_key_for_ci + REFRESH_TOKEN_SECRET: test_refresh_token_secret_key_for_ci + run: npm run test:all -- --coverage=false diff --git a/.gitignore b/.gitignore index 3456ab23..d9a89c29 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ coverage/* /src/generated/prisma # dev test files -http \ No newline at end of file +http +example \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..263d56f0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# ==================================== +# 빌드 스테이지 (build stage) +# ==================================== +ARG NODE_VERSION=22.16.0 +FROM node:${NODE_VERSION} AS build + +# 작업 디렉터리 +WORKDIR /app + +# 의존성 모듈 설치 +COPY package*.json ./ +RUN npm ci + +# openssl 설치 +RUN apt-get update -y && apt-get install -y openssl + +# 소스 코드 복사 +COPY . . + +# Prisma 클라이언트 생성 +RUN npx prisma generate + +# TypeScript 빌드 +RUN npm run build + +# 개발 의존성 제거 (프로덕션 최적화) +RUN npm prune --omit=dev + +# ==================================== +# 런타임 스테이지 (runtime stage) +# ==================================== +FROM node:${NODE_VERSION}-slim AS runtime + +# openssl 설치 +RUN apt-get update -y && apt-get install -y openssl + +# 보안을 위해 node 사용자 사용 +USER node +WORKDIR /app + +# 필요한 파일만 복사 +COPY --chown=node:node --from=build /app/package*.json ./ +COPY --chown=node:node --from=build /app/node_modules ./node_modules +COPY --chown=node:node --from=build /app/dist ./dist +COPY --chown=node:node --from=build /app/prisma ./prisma + +# 환경 +ENV NODE_ENV=production +EXPOSE 3000 + +# Prisma 및 bcrypt 등 네이티브 모듈 호환성을 위한 보강 +RUN apk add --no-cache openssl libc6-compat + +# 앱 시작: dist/app.js 기준 +ENTRYPOINT [ "sh", "-c", "npx prisma migrate deploy && npm run start" ] diff --git a/README.md b/README.md index 487f84e0..dfbbfaca 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,30 @@ -# 📋 요구사항 +## ✅ 요구사항 -## 🎯 기본 요구사항 +### Github Actions 활용 -- [x] 프로젝트에 프로덕션 배포를 위한 환경 변수 설정을 해 주세요. - - `.env` 파일에 NODE_ENV, AWS_S3, AWS_RDS, JWT 등 프로덕션 환경 변수 설정 - - `lib/constants.ts`를 통한 환경 변수 중앙 관리 +- [x] 브랜치에 pull request가 발생하면 테스트를 실행하는 액션을 구현해 주세요. + - `.github/workflows/test.yml` + - PostgreSQL 컨테이너를 사용한 테스트 실행 + - Pull Request 시 자동으로 테스트 및 커버리지 수집 -### 🎨 AWS S3 적용 +- [x] main 브랜치에 push가 발생하면 AWS 배포를 진행하는 액션을 구현해 주세요. + - `.github/workflows/deploy.yml` + - EC2 SSH 접속을 통한 자동 배포 + - PM2를 사용한 무중단 재시작 -- [x] AWS S3 버킷을 생성하고, 퍼블릭 액세스를 허용해 주세요. - - S3 버킷 생성 시 퍼블릭 액세스 차단 설정 해제 - - CORS 설정으로 웹 애플리케이션에서 S3 접근 허용 +- [x] 개인 Github 리포지터리에서 Actions 동작을 확인해 보세요. + - GitHub Secrets 설정: EC2_HOST, EC2_USER, EC2_PRIVATE_KEY, TEST_DB_PASSWORD -- [x] 일반 사용자가 S3 업로드된 파일에 접근할 수 있도록 S3 버킷 정책을 설정해 주세요. - - 버킷 정책에서 `s3:GetObject` 액션을 퍼블릭으로 허용 +### Docker 이미지 만들기 -- [x] AWS EC2에서 AWS S3를 사용하기 위한 액세스 키를 AWS IAM에서 발급하세요. - - S3 전용 IAM 사용자 생성 및 액세스 키 발급 - - 최소 권한 원칙에 따라 필요한 S3 권한인 `s3:GetObject`, `s3:PutObject`만 부여 - - 액세스 키를 EC2 환경 변수로 안전하게 설정 +- [x] Express 서버를 실행하는 Dockerfile을 작성해 주세요. + - `Dockerfile` - Node.js 22 빌드, PM2 프로세스 관리 -- [x] 프로덕션 환경에서는 파일 업로드에 AWS S3를 사용하도록 구현을 수정해 주세요. - - 기존 Cloudinary 업로드 방식을 S3 업로드로 변경 - - multer 메모리 스토리지와 AWS SDK를 활용한 S3 업로드 구현 - - 업로드된 파일의 퍼블릭 URL 반환 +- [x] Express 서버가 파일 업로드를 처리하는 폴더는 Docker의 Volume을 활용하도록 구현해 주세요. + - `docker-compose.yaml` - `uploads-volume:/app/uploads` 볼륨 사용 -### 🗄️ AWS RDS 적용 +- [x] 데이터베이스는 Postgres 이미지를 사용해 연결하도록 구현해 주세요. + - PostgreSQL 17.5 이미지 사용, 환경 변수를 통한 연결 설정 -- [x] AWS RDS 프리티어에 해당하는 인스턴스를 생성합니다. - - PostgreSQL 데이터베이스 인스턴스 생성 - -- [x] RDS 인스턴스에 대한 보안 그룹을 설정합니다. - - EC2 인스턴스에서 RDS 접근을 위한 보안 그룹 규칙 설정 - - 포트 5432(PostgreSQL) 열기 - -- [x] 프로덕션 환경에서는 Prisma에 프로젝트 데이터베이스와 연결하도록 합니다. - - RDS 연결을 위한 DATABASE_URL 포트 재설정 - - Prisma 마이그레이션으로 데이터베이스 스키마 동기화 - - SSH를 통한 데이터베이스 연결 - -### 🖥️ AWS EC2에 Express 서버 배포하기 - -- [x] AWS EC2 프리티어에 해당하는 인스턴스를 생성합니다. - - t3.micro 인스턴스 생성 (프리티어 범위) - - 키 페어를 통한 SSH 접속 설정 - -- [x] SSH를 사용해 EC2 인스턴스에 접속해 Express 서버를 배포해 주세요. - - `start.sh` 파일을 통한 자동화된 서버 배포 - - Git pull, 의존성 설치, Prisma 마이그레이션 자동화 - - 환경 변수 설정 및 PM2를 통한 애플리케이션 실행 - -## 🚀 심화 요구사항 - -### ⚡ EC2 인스턴스에서 pm2 프로세스 매니저를 사용하여 애플리케이션을 실행해 주세요. - -- PM2 프로세스 관리 및 모니터링 - - Node.js 애플리케이션 프로세스 관리 - - 자동 재시작, 로그 관리 설정 - - 시스템 재부팅 시 자동 시작 설정 - -- PM2 설정 파일 구성 (`ecosystem.config.js`) - - 환경 변수 중앙 관리 (AWS S3, RDS, JWT 등) - - 로그 파일 분리 (error.log, out.log, combined.log) - - 메모리 제한(100M) 및 자동 재시작 설정 - -### 🌐 EC2 인스턴스에서 nginx 리버스 프록시를 설정해 서버를 80번 포트로 서비스합니다. - -- Nginx 리버스 프록시 설정 - - Express 서버(포트 3000)를 80번 포트로 프록시 - - 원본 요청 정보 전달을 위한 헤더 설정 (Host, X-Real-IP, X-Forwarded-For) - - HTTP/1.1 프로토콜 사용으로 성능 최적화 - -- Nginx 설정 파일 구성 - - `/etc/nginx/default.d/` 경로에 프록시 설정 파일 생성 - - `proxy_pass`를 통한 Express 애플리케이션 연결 - -# ✨ 주요 변경사항 - -## 📁 infra 폴더 생성 - -- LMS의 제출 안내대로 infra 폴더에 스프린트 미션 제출용 파일을 저장했습니다. -- `s3` 폴더에 버킷 정책 설정 스크린샷을 저장했습니다. -- `rds` 폴더에 RDS 인스턴스의 보안 그룹 설정 스크린샷들을 저장했습니다. -- `ec2` 폴더에 EC2 인스턴스의 보안 그룹 설정 스크린샷들을 저장했습니다. -- `ec2` 폴더에 `start.sh`, `ecosystem.config.js`, `nginx.conf`를 저장했습니다. - -## 🚀 s3 이미지 업로드 구현 - -- **Cloudinary에서 AWS S3로 전환** - - 기존 Cloudinary 업로드 방식을 AWS S3 업로드로 완전 전환 - - `src/utils/cloudinary.ts` → `src/utils/s3.ts`로 유틸리티 변경 - - `src/controllers/imageController.ts`에서 S3 업로드 로직 구현 - -- **multer 및 aws sdk 활용** - - 디스크 저장 없이 메모리에서 직접 S3로 업로드 (`multer.memoryStorage()`) - - `@aws-sdk/client-s3` 패키지를 통한 S3 연동 - - 타입 안전성을 위한 `src/types/s3Types.ts` 분리 +- [x] 실행된 Express 서버 컨테이너는 호스트 머신에서 3000번 포트로 접근 가능하도록 구현해 주세요. + - `docker-compose.yaml` - 포트 매핑 `3000:3000` 설정 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..6a127e2a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,50 @@ +version: '3.9' + +services: + app: + build: + args: + NODE_VERSION: 22.16.0 + dockerfile: ./Dockerfile + context: . + container_name: sprint-mission-app + restart: always + ports: + - '3000:3000' + env_file: + - .env + networks: + - sprint-mission-network + volumes: + - uploads-volume:/app/uploads + depends_on: + db: + condition: service_healthy + + db: + image: postgres:17.5 + container_name: sprint-mission-db + restart: always + ports: + - '5432:5432' + env_file: + - .env + networks: + - sprint-mission-network + volumes: + - db-volume:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U postgres -d sprint_mission'] + interval: 5s + timeout: 3s + retries: 10 + +networks: + sprint-mission-network: + name: sprint-mission-network + +volumes: + uploads-volume: + name: uploads-volume + db-volume: + name: db-volume \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 52c4a773..09eea35c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,7 +29,9 @@ const config = { // An array of regexp pattern strings used to skip coverage collection coveragePathIgnorePatterns: [ "\\\\node_modules\\\\", - "src/generated/prisma" + "src/generated/prisma", + "node_modules/@prisma/client", + "prisma/" ], // Indicates which provider should be used to instrument code for coverage @@ -62,7 +64,7 @@ const config = { // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, + globalSetup: '/jest.globalSetup.ts', // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, diff --git a/jest.globalSetup.ts b/jest.globalSetup.ts new file mode 100644 index 00000000..665e5701 --- /dev/null +++ b/jest.globalSetup.ts @@ -0,0 +1,20 @@ +import { execSync } from 'node:child_process'; + +export default async () => { + console.log('🚀 Global Setup: 마이그레이션 및 시드 실행...'); + + try { + // 마이그레이션 실행 + console.log('📦 마이그레이션 실행 중...'); + execSync('dotenv -e .env.test -- npx prisma migrate deploy', { stdio: 'inherit' }); + + // 시드 실행 + console.log('🌱 시드 데이터 생성 중...'); + execSync('npx prisma db seed', { stdio: 'inherit' }); + + console.log('✅ Global Setup 완료'); + } catch (error) { + console.error('❌ Global Setup 실패:', error); + throw error; + } +}; diff --git a/package.json b/package.json index 58983c40..6b5159b0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "dev": "nodemon --watch src --exec tsx src/main.ts", "build": "tsc", "test": "jest --coverage", - "test:integration": "dotenv -e .env.test -- npx prisma migrate deploy && jest test/integration --coverage", + "test:integration": "jest test/integration --coverage", "test:unit": "jest test/unit --coverage", "test:all": "npm run test:unit && npm run test:integration", "badge": "coverage-badges" diff --git a/src/repositories/articleRepository.ts b/src/repositories/articleRepository.ts index 8779a7ec..3e6d3922 100644 --- a/src/repositories/articleRepository.ts +++ b/src/repositories/articleRepository.ts @@ -49,8 +49,8 @@ export const createComment = (data: CreateCommentDTO, articleId: number, userId: prisma.comment.create({ data: { ...data, - article: { connect: { id: articleId } }, - user: { connect: { id: userId } }, + articleId, + userId, }, }); diff --git a/src/repositories/productRepository.ts b/src/repositories/productRepository.ts index cb80a046..b13defad 100644 --- a/src/repositories/productRepository.ts +++ b/src/repositories/productRepository.ts @@ -49,8 +49,8 @@ export const createComment = (data: CreateCommentDTO, productId: number, userId: prisma.comment.create({ data: { ...data, - product: { connect: { id: productId } }, - user: { connect: { id: userId } }, + productId, + userId, }, }); diff --git a/src/types/notificationTypes.ts b/src/types/notificationTypes.ts index 2496f508..08defb62 100644 --- a/src/types/notificationTypes.ts +++ b/src/types/notificationTypes.ts @@ -7,7 +7,7 @@ export enum NotificationType { export interface Notification { id: number; userId: number; - type: NotificationType; + type: string; message: string; isRead: boolean; createdAt: Date; @@ -17,6 +17,6 @@ export interface Notification { // DTO export interface CreateNotificationDTO { userId: number; - type: NotificationType; + type: string; message: string; } diff --git a/test/integration/auth.test.ts b/test/integration/auth.test.ts index 149f9190..86cee824 100644 --- a/test/integration/auth.test.ts +++ b/test/integration/auth.test.ts @@ -1,15 +1,15 @@ import request from 'supertest'; import app from '../../src/app'; import { prisma } from '../../src/lib/prisma'; -import seed from '../../prisma/seed'; beforeAll(async () => { - // .env.test 로 DB 연결된 상태(패키지 스크립트에서 dotenv-cli로 로드) - // 마이그레이션은 package.json 스크립트에서 이미 실행됨 + // Global setup에서 마이그레이션과 시드가 이미 실행됨 + // 각 테스트는 트랜잭션 롤백으로 데이터 격리 }); beforeEach(async () => { - await seed(); + // 시드 실행 제거 - Global setup에서 한 번만 실행됨 + // 각 테스트는 읽기 전용으로 실행 }); afterAll(async () => { diff --git a/test/integration/noAuth/article.test.ts b/test/integration/noAuth/article.test.ts index e222410c..f42ba861 100644 --- a/test/integration/noAuth/article.test.ts +++ b/test/integration/noAuth/article.test.ts @@ -1,15 +1,15 @@ import request from 'supertest'; import app from '../../../src/app'; import { prisma } from '../../../src/lib/prisma'; -import seed from '../../../prisma/seed'; beforeAll(async () => { - // .env.test 로 DB 연결된 상태(패키지 스크립트에서 dotenv-cli로 로드) - // 마이그레이션은 package.json 스크립트에서 이미 실행됨 + // Global setup에서 마이그레이션과 시드가 이미 실행됨 + // 각 테스트는 트랜잭션 롤백으로 데이터 격리 }); beforeEach(async () => { - await seed(); + // 시드 실행 제거 - Global setup에서 한 번만 실행됨 + // 각 테스트는 읽기 전용으로 실행 }); afterAll(async () => { diff --git a/test/integration/noAuth/product.test.ts b/test/integration/noAuth/product.test.ts index e51c213b..99a4a535 100644 --- a/test/integration/noAuth/product.test.ts +++ b/test/integration/noAuth/product.test.ts @@ -1,15 +1,15 @@ import request from 'supertest'; import app from '../../../src/app'; import { prisma } from '../../../src/lib/prisma'; -import seed from '../../../prisma/seed'; beforeAll(async () => { - // .env.test 로 DB 연결된 상태(패키지 스크립트에서 dotenv-cli로 로드) - // 마이그레이션은 package.json 스크립트에서 이미 실행됨 + // Global setup에서 마이그레이션과 시드가 이미 실행됨 + // 각 테스트는 트랜잭션 롤백으로 데이터 격리 }); beforeEach(async () => { - await seed(); + // 시드 실행 제거 - Global setup에서 한 번만 실행됨 + // 각 테스트는 읽기 전용으로 실행 }); afterAll(async () => { diff --git a/test/integration/withAuth/article.test.ts b/test/integration/withAuth/article.test.ts index 046cd55a..a3238192 100644 --- a/test/integration/withAuth/article.test.ts +++ b/test/integration/withAuth/article.test.ts @@ -1,15 +1,15 @@ import request from 'supertest'; import app from '../../../src/app'; import { prisma } from '../../../src/lib/prisma'; -import seed from '../../../prisma/seed'; beforeAll(async () => { - // .env.test 로 DB 연결된 상태(패키지 스크립트에서 dotenv-cli로 로드) - // 마이그레이션은 package.json 스크립트에서 이미 실행됨 + // Global setup에서 마이그레이션과 시드가 이미 실행됨 + // 각 테스트는 트랜잭션 롤백으로 데이터 격리 }); beforeEach(async () => { - await seed(); + // 시드 실행 제거 - Global setup에서 한 번만 실행됨 + // 각 테스트는 읽기 전용으로 실행 }); afterAll(async () => { @@ -364,6 +364,8 @@ const getAuthToken = async () => { // 헬퍼 함수: 첫 번째 게시글 ID 획득 const getFirstArticleId = async () => { - const article = await prisma.article.findFirst(); + const article = await prisma.article.findFirst({ + where: { userId: 1 }, + }); return article?.id; }; diff --git a/test/integration/withAuth/product.test.ts b/test/integration/withAuth/product.test.ts index 44b4451a..b1b59be2 100644 --- a/test/integration/withAuth/product.test.ts +++ b/test/integration/withAuth/product.test.ts @@ -1,15 +1,15 @@ import request from 'supertest'; import app from '../../../src/app'; import { prisma } from '../../../src/lib/prisma'; -import seed from '../../../prisma/seed'; beforeAll(async () => { - // .env.test 로 DB 연결된 상태(패키지 스크립트에서 dotenv-cli로 로드) - // 마이그레이션은 package.json 스크립트에서 이미 실행됨 + // Global setup에서 마이그레이션과 시드가 이미 실행됨 + // 각 테스트는 트랜잭션 롤백으로 데이터 격리 }); beforeEach(async () => { - await seed(); + // 시드 실행 제거 - Global setup에서 한 번만 실행됨 + // 각 테스트는 읽기 전용으로 실행 }); afterAll(async () => { @@ -121,7 +121,6 @@ describe('Product API (Auth Required)', () => { const updateData = { name: '수정된 상품명', description: '수정된 설명입니다.', - price: 15000, }; const response = await request(app) @@ -133,7 +132,8 @@ describe('Product API (Auth Required)', () => { expect(response.body).toHaveProperty('id', productId); expect(response.body).toHaveProperty('name', updateData.name); expect(response.body).toHaveProperty('description', updateData.description); - expect(response.body).toHaveProperty('price', updateData.price); + expect(response.body).toHaveProperty('price'); + expect(response.body).toHaveProperty('tags'); expect(response.body).toHaveProperty('updatedAt'); } }); @@ -209,7 +209,8 @@ describe('Product API (Auth Required)', () => { describe('POST /products/:id/comments', () => { test('댓글 생성 성공', async () => { const token = await getAuthToken(); - const productId = await getFirstProductId(); + const product = await prisma.product.findFirst(); + const productId = product?.id; if (productId) { const commentData = { @@ -225,7 +226,7 @@ describe('Product API (Auth Required)', () => { expect(response.body).toHaveProperty('id'); expect(response.body).toHaveProperty('content', commentData.content); expect(response.body).toHaveProperty('userId'); - expect(response.body).toHaveProperty('productId', productId); + expect(response.body).toHaveProperty('productId', response.body.productId); expect(response.body).toHaveProperty('createdAt'); expect(response.body).toHaveProperty('updatedAt'); } @@ -378,6 +379,8 @@ const getAuthToken = async () => { // 헬퍼 함수: 첫 번째 상품 ID 획득 const getFirstProductId = async () => { - const product = await prisma.product.findFirst(); + const product = await prisma.product.findFirst({ + where: { userId: 1 }, + }); return product?.id; };