본 문서는 PLAN.md (v8) 및 SCHEMA.md (v6)를 기반으로, NestJS 서버의 초기 환경 설정, CI/CD 파이프라인, 그리고 운영 전략의 세부 사항을 정의합니다.
NestJS 서버(server/)의 루트에 .env 파일이 필요합니다. (Value는 실제 배포 시점에 채웁니다.)
# -——————————–
# NestJS & Server
# -——————————–
NODE_ENV=development
SERVER_PORT=3000
# -——————————–
# Auth & Security (A-120)
# -——————————–
# 콤마(,)로 구분된 복수의 키를 지원 (키 회전용)
GLOBAL_SECRET_KEY=primary_key,secondary_key
# Throttler (Rate Limit) - 1분에 60개 요청
THROTTLE_TTL=60
THROTTLE_LIMIT=60
# -——————————–
# Databases (P1-1, P1-3)
# -——————————–
# MongoDB Atlas (M0) 연결 문자열
DB_URI=mongodb+srv://…
# Redis (로컬 Docker 또는 ElastiCache)
REDIS_HOST=localhost
REDIS_PORT=6379
# -——————————–
# AWS Credentials (P1-2, M-050)
# -——————————–
# (권장) EC2 Instance Role을 사용하면 아래 2개는 필요 없음. 로컬 개발용.
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=ap-northeast-2
S3_BUCKET_NAME=miji-nft-assets-dev
# -——————————–
# Blockchain & Secrets (P1-2, B-100)
# -——————————–
# (핵심) 서버 지갑 PK가 저장된 Secrets Manager의 ARN
PK_SECRET_ARN=arn:aws:secretsmanager:ap-northeast-2:….
# (핵심) PK 캐시 TTL (초) (M1 제안)
PK_CACHE_TTL_SECONDS=300
# -——————————–
# Chain RPC Endpoints (B-100)
# -——————————–
# 체인 ID를 키로 사용 (e.g., Infura, Alchemy)
CHAIN_RPC_URL_43114=[https://api.avax.network/ext/bc/C/rpc](https://api.avax.network/ext/bc/C/rpc)
CHAIN_RPC_URL_137=[https://polygon-rpc.com](https://polygon-rpc.com)
# -——————————–
# Webhooks (H-200)
# -——————————–
# (B2) Miji 서버가 수신할 기본 웹훅 URL
WEBHOOK_URL_DEFAULT=[https://api.miji.service.com/v1/webhook/nft](https://api.miji.service.com/v1/webhook/nft)
# (B2) 웹훅 서명 인증을 위한 비밀키
WEBHOOK_SECRET=whsec_…
제안서(M1)에 따라 PK는 5분(300초)간 인메모리 캐싱합니다.
PK_SECRET_ARN 환경변수가 가리키는 Secret은 다음 JSON 구조를 따르는 것을 권장합니다. MVP에서는 단일 지갑(default_wallet_pk)만 사용합니다.
{
“default_wallet_pk”: “0xabc123…”
}
PLAN.md (v8)에 따라 3개의 큐를 @nestjs/bull로 등록합니다. 각 큐는 작업의 중요도와 성격에 따라 다른 기본 옵션을 가집니다.
| 큐 이름 | PLAN.md ID | 목적 | 기본 Job 옵션 (제안) |
|---|---|---|---|
| minting-queue | (T-110) | (핵심) 민팅 트랜잭션 처리 | attempts: 3, backoff: { type: ‘exponential’, delay: 5000 } (5초 후 재시도) |
| deployment-queue | (C-130) | (핵심) 컨트랙트 배포 처리 | attempts: 2, backoff: { type: ‘exponential’, delay: 10000 } (배포는 재시도 간격 길게) |
| webhook-queue | (H-200) | (B2) 외부 알림 발송 | attempts: 5, backoff: { type: ‘exponential’, delay: 10000 } (10초 후 재시도) |
t4g.small (ARM64) 단일 플랫폼 배포를 전제로 합니다. **멀티 스테이지 빌드(Multi-stage Build)**를 사용하며, 컨트랙트 아티팩트(ABI/Bytecode)를 최종 이미지에 포함시킵니다.
/server/Dockerfile (초안)
# -——————————–
# Stage 1: Builder
# -——————————–
FROM node:18-alpine AS builder
WORKDIR /app
# 모노레포 루트의 package.json, pnpm-lock.yaml 등을 복사
COPY package.json pnpm-lock.yaml ./
COPY pnpm-workspace.yaml ./
# server/ 와 contracts/ 의 package.json 복사
COPY server/package.json ./server/
COPY contracts/package.json ./contracts/
# 의존성 설치 (pnpm 사용)
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
# 전체 소스 코드 복사
COPY . .
# (★수정★) 컨트랙트 빌드 (ABI/Bytecode 생성)
RUN pnpm --filter contracts build
# NestJS 서버 애플리케이션 빌드
RUN pnpm --filter server build
# -——————————–
# Stage 2: Final Runtime
# -——————————–
FROM node:18-alpine
WORKDIR /app
# Builder 스테이지에서 빌드 결과물만 복사
COPY --from=builder /app/server/dist ./dist
# (★수정★) 컨트랙트 아티팩트(ABI/Bytecode) 복사
COPY --from=builder /app/contracts/artifacts ./artifacts
# (중요) Production 의존성만 복사 (또는 재설치)
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/server/node_modules ./server/node_modules
# NestJS 서버 실행
CMD [“node”, “dist/main.js”]
배경: PLAN.md (v8)의 핵심 요구사항은 “새 NFT 컬렉션을 위한 동적 컨트랙트 배포 API(C-130)”입니다.
워크플로우:
로컬 개발(pnpm run start:dev) 및 E2E 테스트(pnpm test:e2e)를 위한 인프라 구성입니다.
/docker-compose.yml (로컬 테스트용)
version: ‘3.8’
services:
# 1. NestJS API 서버
api:
build:
context: .
dockerfile: server/Dockerfile # (4번에서 정의한 Dockerfile)
ports:
- “3000:3000”
env_file:
- server/.env # 로컬 .env 파일 사용
depends_on:
- redis
# 2. Redis 서버 (큐/캐시용)
redis:
image: redis:7-alpine
ports:
- “6379:6379”
volumes:
- redis_data:/data # Redis 데이터 영속성
volumes:
redis_data:
PLAN.md (v8)의 AuthGuard 외에 두 가지를 추가합니다.
Rate Limiting: Nginx 대신 NestJS 내장에서 처리하는 것이 MVP에 더 간단합니다. @nestjs/throttler 모듈을 app.module.ts에 글로벌하게 적용합니다.
// app.module.ts
import { ThrottlerModule } from ‘@nestjs/throttler’;
@Module({
imports: [
ThrottlerModule.forRoot([{
ttl: 60, // 1분
limit: 60, // 60회
}]),
// …
],
})
export class AppModule {}
SCHEMA.md (v6)를 기반으로 DTO(Data Transfer Object)에서 최소한의 구조를 검증해야 합니다. Miji 정의서(Source: “아발란체 디앱 개발, 티켓 쿠폰 메타데이터 상세”)의 누락 필드를 반영하여 DTO를 수정합니다.
server/src/modules/metadata/dto/create-metadata.dto.ts (수정 초안)
import { Type } from ‘class-transformer’;
import { IsIn, IsObject, IsString, IsUrl, ValidateNested, IsBoolean, IsOptional } from ‘class-validator’;
// 티켓/쿠폰별 최소 검증
class UtilityDataTicketDto {
@IsString() status: string;
@IsObject() event: Record<string, any>;
@IsObject() seat: Record<string, any>;
}
class UtilityDataCouponDto {
@IsString() status: string;
@IsString() brand: string;
}
class CreateUtilityDto {
@IsString()
@IsIn([‘Ticket’, ‘Coupon’])
type: string;
// (★보완 제안 반영★)
@IsOptional()
@IsUrl()
validation_url?: string;
// (★보완 제안 반영★)
@IsOptional()
@IsBoolean()
kyc_required?: boolean = false;
// ‘type’ 필드 값에 따라 다른 DTO를 동적으로 적용
@ValidateNested()
@Type(({ object }) =>
object.type === ‘Ticket’ ? UtilityDataTicketDto : UtilityDataCouponDto
)
data: UtilityDataTicketDto | UtilityDataCouponDto;
}
export class CreateMetadataDto {
@IsUrl()
image_url: string; // (M-100) 외부 URL을 직접 받음
// (★보완 제안 반영★)
@IsOptional()
@IsUrl()
external_url?: string;
@IsString()
name: string;
@IsString()
description: string;
@ValidateNested()
@Type(() => CreateUtilityDto)
utility: CreateUtilityDto;
}
(B2) webhook-queue가 Miji 서버(WEBHOOK_URL_DEFAULT)로 전송할 페이로드(Payload)와 헤더 규격을 제안합니다.