From 04183fd1cadc2611affc245b4ef663c9f4860f57 Mon Sep 17 00:00:00 2001 From: qkrwotjd1731 <58104546+qkrwotjd1731@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:10:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EC=8B=A4=ED=8C=A8=ED=95=98=EB=8A=94?= =?UTF-8?q?=20test=EC=9D=98=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.js | 2 +- jest.globalSetup.ts | 20 ++++++++++++++++++++ package.json | 2 +- src/repositories/articleRepository.ts | 4 ++-- src/repositories/productRepository.ts | 4 ++-- src/types/notificationTypes.ts | 4 ++-- test/integration/auth.test.ts | 8 ++++---- test/integration/noAuth/article.test.ts | 8 ++++---- test/integration/noAuth/product.test.ts | 8 ++++---- test/integration/withAuth/article.test.ts | 12 +++++++----- test/integration/withAuth/product.test.ts | 21 ++++++++++++--------- 11 files changed, 59 insertions(+), 34 deletions(-) create mode 100644 jest.globalSetup.ts diff --git a/jest.config.js b/jest.config.js index 52c4a773..0a0329b2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -62,7 +62,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; }; From be94f58afda01ea5e0a7d4be25d609a050bc092d Mon Sep 17 00:00:00 2001 From: qkrwotjd1731 <58104546+qkrwotjd1731@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:45:58 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EC=8A=A4=ED=94=84=EB=A6=B0?= =?UTF-8?q?=ED=8A=B8=20=EB=AF=B8=EC=85=98=2011=201=EC=B0=A8=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 21 +++++++ .env.example | 5 ++ .github/workflows/deploy.yml | 56 ++++++++++++++++++ .github/workflows/test.yml | 60 +++++++++++++++++++ .gitignore | 3 +- Dockerfile | 55 ++++++++++++++++++ README.md | 110 +++++++---------------------------- docker-compose.yaml | 50 ++++++++++++++++ 8 files changed, 270 insertions(+), 90 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/test.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yaml 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..480895bb --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,56 @@ +name: Deploy to EC2 + +on: + push: + branches: [main] + +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..fcddc53a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Test + +on: + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + # PostgreSQL μ„œλΉ„μŠ€ μ»¨ν…Œμ΄λ„ˆ (ν…ŒμŠ€νŠΈμš© DB μžλ™ 생성) + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${{ secrets.TEST_DB_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: TypeScript νƒ€μž… 체크 + run: npm run build + + - name: DB μŠ€ν‚€λ§ˆ 생성 (ν…Œμ΄λΈ” 생성) + env: + DATABASE_URL: postgresql://postgres:${{ secrets.TEST_DB_PASSWORD }}@localhost:5432/test-sprint_mission?schema=public + run: npx prisma migrate deploy + + - name: ν…ŒμŠ€νŠΈ 데이터 μ‚½μž… + env: + DATABASE_URL: postgresql://postgres:${{ secrets.TEST_DB_PASSWORD }}@localhost:5432/test-sprint_mission?schema=public + run: npx prisma db seed + + - name: ν…ŒμŠ€νŠΈ μ‹€ν–‰ + env: + DATABASE_URL: postgresql://postgres:${{ secrets.TEST_DB_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 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 From bae61b00d4c08dad2b8ee398c0eb179768916ef6 Mon Sep 17 00:00:00 2001 From: qkrwotjd1731 <58104546+qkrwotjd1731@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:51:25 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20yml=20=ED=8C=8C=EC=9D=BC=20on=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- .github/workflows/test.yml | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 480895bb..422604c6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to EC2 on: push: - branches: [main] + branches: [main, λ°•μž¬μ„±, λ°•μž¬μ„±-sprint11] jobs: deploy: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fcddc53a..563ef1a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,10 @@ name: Test on: + push: + branches: [main, λ°•μž¬μ„±, λ°•μž¬μ„±-sprint11] pull_request: - branches: [main] + branches: [main, λ°•μž¬μ„±, λ°•μž¬μ„±-sprint11] jobs: test: @@ -11,10 +13,10 @@ jobs: # PostgreSQL μ„œλΉ„μŠ€ μ»¨ν…Œμ΄λ„ˆ (ν…ŒμŠ€νŠΈμš© DB μžλ™ 생성) services: postgres: - image: postgres:17 + image: postgres:17.5 env: POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }} + POSTGRES_PASSWORD: test_password POSTGRES_DB: test-sprint_mission ports: - 5432:5432 @@ -42,17 +44,17 @@ jobs: - name: DB μŠ€ν‚€λ§ˆ 생성 (ν…Œμ΄λΈ” 생성) env: - DATABASE_URL: postgresql://postgres:${{ secrets.TEST_DB_PASSWORD }}@localhost:5432/test-sprint_mission?schema=public + DATABASE_URL: postgresql://postgres:test_password@localhost:5432/test-sprint_mission?schema=public run: npx prisma migrate deploy - name: ν…ŒμŠ€νŠΈ 데이터 μ‚½μž… env: - DATABASE_URL: postgresql://postgres:${{ secrets.TEST_DB_PASSWORD }}@localhost:5432/test-sprint_mission?schema=public + DATABASE_URL: postgresql://postgres:test_password@localhost:5432/test-sprint_mission?schema=public run: npx prisma db seed - name: ν…ŒμŠ€νŠΈ μ‹€ν–‰ env: - DATABASE_URL: postgresql://postgres:${{ secrets.TEST_DB_PASSWORD }}@localhost:5432/test-sprint_mission?schema=public + 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 From 79a7bf1a1cc432db63e3f6c09e6120abaa6df8b5 Mon Sep 17 00:00:00 2001 From: qkrwotjd1731 <58104546+qkrwotjd1731@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:56:51 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20test.yml=EC=97=90=20prisma=20generat?= =?UTF-8?q?e=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 7 ++++++- jest.config.js | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 563ef1a6..63294623 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,11 @@ jobs: - 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 @@ -59,4 +64,4 @@ jobs: 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 + run: npm run test:all -- --coverage=false diff --git a/jest.config.js b/jest.config.js index 0a0329b2..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