Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ apps/mobile/

# Documentation
*.md
!README.md
myDocs/

# Tests
coverage/
Expand Down
60 changes: 41 additions & 19 deletions .env.docker.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,64 @@ PORT=8080
# --- Database ---
# 호스트명 'db'는 docker-compose.dev.yml의 서비스명
DATABASE_URL=postgresql://postgres:postgres@db:5432/aido
# 호스트 포트 (기본 5432와 충돌 방지)
# 호스트 포트 (로컬 5432와 충돌 방지)
DB_PORT=5433

# --- JWT (개발용 기본값) ---
JWT_SECRET=dev-jwt-secret-minimum-32-characters-long
JWT_REFRESH_SECRET=dev-jwt-refresh-secret-minimum-32-chars
# --- JWT ---
JWT_SECRET=dev-jwt-secret-key-minimum-32-characters-long-for-security
JWT_REFRESH_SECRET=dev-refresh-secret-key-minimum-32-characters-long-here
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# --- Security ---
TOKEN_ENCRYPTION_KEY=dev-token-encryption-key-32chars!
TOKEN_ENCRYPTION_KEY=dev-token-encryption-key-minimum-32-characters-long
CORS_ORIGINS=http://localhost:3000,http://localhost:8081,exp://localhost:8081
THROTTLE_TTL=60000
THROTTLE_LIMIT=1000

# --- OAuth (선택사항 - 필요한 것만 설정) ---
# --- OAuth: Google ---
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# GOOGLE_CALLBACK_URL=http://localhost:8080/auth/google/callback
# GOOGLE_CALLBACK_URL=http://localhost:8080/v1/auth/google/web-callback

# --- OAuth: Apple ---
# APPLE_TEAM_ID=
# APPLE_CLIENT_ID=
# APPLE_KEY_ID=
# APPLE_PRIVATE_KEY=
# APPLE_CALLBACK_URL=http://localhost:8080/v1/auth/apple/callback

# --- OAuth: Kakao ---
# KAKAO_CLIENT_ID=
# KAKAO_CLIENT_SECRET=
# KAKAO_CALLBACK_URL=http://localhost:8080/auth/kakao/callback
# KAKAO_CALLBACK_URL=http://localhost:8080/v1/auth/kakao/web-callback

# --- OAuth: Naver ---
# NAVER_CLIENT_ID=
# NAVER_CLIENT_SECRET=
# NAVER_CALLBACK_URL=http://localhost:8080/auth/naver/callback

# APPLE_CLIENT_ID=
# APPLE_SERVICE_ID=
# APPLE_TEAM_ID=
# APPLE_KEY_ID=
# APPLE_PRIVATE_KEY=
# APPLE_CALLBACK_URL=http://localhost:8080/auth/apple/callback
# NAVER_CALLBACK_URL=http://localhost:8080/v1/auth/naver/web-callback

# --- Email (선택사항) ---
# --- Email ---
# RESEND_API_KEY=
# EMAIL_FROM=hello@your-domain.com
# EMAIL_FROM_NAME=Aido

# --- External Services (선택사항) ---
# EXPO_ACCESS_TOKEN=
# --- External Services ---
# GOOGLE_GENERATIVE_AI_API_KEY=
# DISCORD_SIGNUP_WEBHOOK_URL=

# --- RevenueCat ---
# REVENUECAT_SECRET_API_KEY=
# REVENUECAT_WEBHOOK_SECRET=

# --- Cache ---
# CACHE_TYPE=memory
# CACHE_DEFAULT_TTL_MS=60000
# CACHE_MAX_ITEMS=1000
# CACHE_CLEANUP_INTERVAL_MS=30000

# --- Push Notifications ---
# EXPO_ACCESS_TOKEN=

# --- Monitoring ---
# SENTRY_DSN=
43 changes: 25 additions & 18 deletions .env.docker.prod.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ NODE_ENV=production
PORT=8080

# --- Database ---
# DB_USER, DB_PASSWORD, DB_NAME을 변경하면 DATABASE_URL도 맞춰 수정하세요
# ⚠️ DB_PASSWORD 변경 시 DATABASE_URL도 동일한 값으로 맞추세요
DB_USER=postgres
DB_PASSWORD=CHANGE_ME
DB_NAME=aido
Expand All @@ -28,25 +28,27 @@ CORS_ORIGINS=https://your-domain.com
THROTTLE_TTL=60000
THROTTLE_LIMIT=100

# --- OAuth (프로덕션에서 최소 1개 필수) ---
# --- OAuth: Google ---
GOOGLE_CLIENT_ID=CHANGE_ME
GOOGLE_CLIENT_SECRET=CHANGE_ME
GOOGLE_CALLBACK_URL=https://your-domain.com/auth/google/callback
GOOGLE_CALLBACK_URL=https://your-domain.com/v1/auth/google/web-callback

# --- OAuth: Apple ---
# APPLE_TEAM_ID=
# APPLE_CLIENT_ID=
# APPLE_KEY_ID=
# APPLE_PRIVATE_KEY=
# APPLE_CALLBACK_URL=https://your-domain.com/v1/auth/apple/callback

# --- OAuth: Kakao ---
# KAKAO_CLIENT_ID=
# KAKAO_CLIENT_SECRET=
# KAKAO_CALLBACK_URL=
# KAKAO_CALLBACK_URL=https://your-domain.com/v1/auth/kakao/web-callback

# --- OAuth: Naver ---
# NAVER_CLIENT_ID=
# NAVER_CLIENT_SECRET=
# NAVER_CALLBACK_URL=

# APPLE_CLIENT_ID=
# APPLE_SERVICE_ID=
# APPLE_TEAM_ID=
# APPLE_KEY_ID=
# APPLE_PRIVATE_KEY=
# APPLE_CALLBACK_URL=
# NAVER_CALLBACK_URL=https://your-domain.com/v1/auth/naver/web-callback

# --- Email (프로덕션 필수) ---
RESEND_API_KEY=CHANGE_ME
Expand All @@ -55,16 +57,21 @@ EMAIL_FROM_NAME=Aido
SUPPORT_EMAIL=support@your-domain.com

# --- External Services ---
# EXPO_ACCESS_TOKEN=
# GOOGLE_GENERATIVE_AI_API_KEY=
# DISCORD_SIGNUP_WEBHOOK_URL=

# --- RevenueCat ---
# REVENUECAT_SECRET_API_KEY=
# REVENUECAT_WEBHOOK_SECRET=

# --- Cache ---
# CACHE_TYPE=memory
# REDIS_HOST=
# REDIS_PORT=6379
# REDIS_PASSWORD=
# REDIS_DB=0
CACHE_TYPE=memory
CACHE_DEFAULT_TTL_MS=60000
CACHE_MAX_ITEMS=1000
CACHE_CLEANUP_INTERVAL_MS=30000

# --- Push Notifications ---
# EXPO_ACCESS_TOKEN=

# --- Monitoring ---
# SENTRY_DSN=
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Deploy to EC2

on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]

concurrency:
group: deploy-production
cancel-in-progress: false

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}

steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
script: ~/apps/deploy.sh
28 changes: 11 additions & 17 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
# ==============================================================================
# Stage 1: base - Node + pnpm
# ==============================================================================
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate
WORKDIR /app

# ==============================================================================

# Stage 2: deps - 의존성 설치
# ==============================================================================
FROM base AS deps

COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
Expand All @@ -25,9 +22,7 @@ COPY tooling/vitest/package.json ./tooling/vitest/

RUN pnpm install --frozen-lockfile --ignore-scripts

# ==============================================================================
# Stage 3: builder - 빌드
# ==============================================================================
FROM base AS builder

COPY --from=deps /app ./
Expand All @@ -40,27 +35,28 @@ RUN pnpm --filter @aido/errors build && \
pnpm --filter @aido/validators build

# Prisma generate + API build
RUN pnpm --filter @aido/api prisma generate
RUN cd apps/api && pnpm prisma generate
RUN pnpm --filter @aido/api build

# Production 의존성만 추출
RUN pnpm --filter @aido/api deploy --prod /app/production
RUN pnpm --filter @aido/api deploy --legacy --prod /app/production


# ==============================================================================
# Stage 4: migrate - Prisma 마이그레이션 (ECS 별도 태스크)
# ==============================================================================
# Stage 4: migrate - Prisma 마이그레이션
FROM base AS migrate

COPY --from=deps /app ./
COPY apps/api/prisma ./apps/api/prisma
COPY apps/api/prisma.config.ts ./apps/api/

WORKDIR /app/apps/api

RUN pnpm prisma generate

CMD ["pnpm", "prisma", "migrate", "deploy"]

# ==============================================================================

# Stage 5: development - 개발 핫리로드
# ==============================================================================
FROM base AS development

COPY --from=deps /app ./
Expand All @@ -70,7 +66,7 @@ RUN pnpm --filter @aido/errors build && \
pnpm --filter @aido/utils build && \
pnpm --filter @aido/validators build

RUN pnpm --filter @aido/api prisma generate
RUN cd apps/api && pnpm prisma generate

WORKDIR /app/apps/api

Expand All @@ -79,9 +75,7 @@ EXPOSE 8080

CMD ["pnpm", "dev"]

# ==============================================================================
# Stage 6: production - 최종 런타임
# ==============================================================================
FROM node:22-alpine AS production

WORKDIR /app
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@nestjs/throttler": "^6.5.0",
"@prisma/adapter-pg": "^7.4.0",
"@prisma/client": "^7.4.0",
"@sentry/nestjs": "^10.39.0",
"ai": "^6.0.86",
"argon2": "^0.44.0",
"date-fns": "^4.1.0",
Expand Down
10 changes: 7 additions & 3 deletions apps/api/prisma.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import path from "node:path";
import { config } from "dotenv";
import { defineConfig } from "prisma/config";

// .env 파일에서 환경변수 로드
config({ path: path.resolve(__dirname, ".env") });
// .env 파일에서 환경변수 로드 (dotenv가 없는 Docker 빌드 환경에서도 동작)
try {
const { config } = require("dotenv");
config({ path: path.resolve(__dirname, ".env") });
} catch {
// dotenv 미설치 환경 (Docker build) — DATABASE_URL fallback 사용
}

// DATABASE_URL (prisma generate는 실제 연결 불필요, placeholder 허용)
const DATABASE_URL =
Expand Down
10 changes: 7 additions & 3 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConfigService } from "@nestjs/config";
import { APP_GUARD } from "@nestjs/core";
import { EventEmitterModule } from "@nestjs/event-emitter";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { SentryModule } from "@sentry/nestjs/setup";
import {
AppConfigModule,
CacheModule,
Expand Down Expand Up @@ -38,7 +39,10 @@ import { AppService } from "./app.service";
// 1. Configuration (Must be loaded first)
AppConfigModule,

// 2. Infrastructure
// 2. Monitoring
SentryModule.forRoot(),

// 3. Infrastructure
DatabaseModule,
EncryptionModule,
CacheModule.forRoot(),
Expand All @@ -52,7 +56,7 @@ import { AppService } from "./app.service";
ignoreErrors: false,
}),

// 3. Global Modules
// 4. Global Modules
LoggerModule.forRootAsync(),
ExceptionModule,
ResponseModule,
Expand All @@ -67,7 +71,7 @@ import { AppService } from "./app.service";
],
}),

// 4. Features
// 5. Features
AdminModule,
AdminNotificationModule,
AiModule,
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/common/config/schemas/email.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export const emailSchema = z.object({
RESEND_API_KEY: z.string().optional(),

/** 발신자 이메일 주소 */
EMAIL_FROM: z.email().default("dydals3440@gmail.com"),
EMAIL_FROM: z.email().default("noreply@aido.kr"),

/** 발신자 이름 */
EMAIL_FROM_NAME: z.string().default("Aido"),

/** 문의 수신 이메일 */
SUPPORT_EMAIL: z.email().default("dydals3440@gmail.com"),
SUPPORT_EMAIL: z.email().default("support@aido.kr"),
});

export type EmailConfig = z.infer<typeof emailSchema>;
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/common/config/schemas/external.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { z } from "zod";
*/
export const externalSchema = z.object({
// RevenueCat 구독 관리
REVENUECAT_API_KEY: z.string().optional(),
REVENUECAT_SECRET_API_KEY: z.string().optional(),
REVENUECAT_WEBHOOK_SECRET: z.string().optional(),

// Redis 캐시/세션 (선택) - 빈 문자열 허용
REDIS_URL: z
Expand Down
7 changes: 5 additions & 2 deletions apps/api/src/common/config/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,11 @@ export class TypedConfigService {
return this.get("EXPO_ACCESS_TOKEN");
}

get revenueCatApiKey(): string | undefined {
return this.get("REVENUECAT_API_KEY");
get revenuecat() {
return {
secretApiKey: this.get("REVENUECAT_SECRET_API_KEY"),
webhookSecret: this.get("REVENUECAT_WEBHOOK_SECRET"),
};
}

get redisUrl(): string | undefined {
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/common/exception/exception.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GlobalExceptionFilter } from "./filters/global-exception.filter";
/**
* Exception 모듈
* 전역 예외 처리를 담당
* GlobalExceptionFilter에서 Sentry 캡처를 직접 수행
*/
@Module({
providers: [
Expand Down
Loading