import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Logger, RequestMethod, ValidationPipe } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { WinstonModule } from 'nest-winston'; import helmet from 'helmet'; import type { Express, Request as ExpressRequest, Response as ExpressResponse, } from 'express'; import { Reflector } from '@nestjs/core'; import { HttpExceptionFilter } from '@/common/filters/http-exception.filter'; import { TransformInterceptor } from '@/common/interceptors/transform.interceptor'; import { winstonConfig } from '@/shared/logger/winston.config'; import { parseList } from '@/shared/utils/env'; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: WinstonModule.createLogger(winstonConfig), }); // 리버스 프록시(Nginx, Docker 배포 환경 등) 뒤에서 올바른 클라이언트 IP 식별 지원을 위해 설정합니다. const expressApp = app.getHttpAdapter().getInstance() as Express; expressApp.set('trust proxy', 1); // 글로벌 prefix — 모든 라우트가 /api/* 로 정규화됨 (Flutter, Web 양쪽 baseURL과 일치) app.setGlobalPrefix('api', { exclude: [ // 헬스체크 / 루트 상태 엔드포인트는 prefix 미적용 { path: '/', method: RequestMethod.GET }, // 업체 단축 링크는 짧아야 하므로 /api 없이 /r/{코드} (PartnerRedirectController) { path: 'r/:code', method: RequestMethod.GET }, ], }); // 기본 주소('/') 접속 시 상태 확인용 텍스트 응답 추가 expressApp.get('/', (_req: ExpressRequest, res: ExpressResponse) => { res.send('Backend API Server is running 정상 구동 중입니다.'); }); // 1. 보안 헤더 (Helmet) - XSS 방지 및 기본 웹 보안 app.use(helmet()); // 2. 화이트리스트 기반 CORS 정책 // CORS_ORIGIN 은 콤마로 여러 개를 줄 수 있다 // (예: 아임웹 관리자 페이지 https://www.moneyguider.co.kr,https://moneyguider.co.kr) const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173'; const whitelist = [ frontendUrl, 'http://localhost:3000', ...parseList(process.env.CORS_ORIGIN), ]; app.enableCors({ origin: ( origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void, ) => { // 서버간의 통신(origin이 undefined)이거나 명시된 리스트일 때만 허용 if (!origin || whitelist.indexOf(origin) !== -1) { callback(null, true); } else { // Error 를 넘기면 500(SYS_001)이 되어 원인 파악이 어려우므로, // CORS 헤더를 부여하지 않아 브라우저가 차단하도록 한다. callback(null, false); } }, credentials: true, // 쿠키 교환 허용 }); // 3. Rate Limiting 은 AppModule 의 ThrottlerGuard(APP_GUARD)로 일원화되어 있다. // 라우트 단위 제어(@Throttle / @SkipThrottle)가 가능하고, 한도 초과는 REQ_001(429)로 응답된다. // (전역 예외 필터가 429 → REQ_001 로 매핑한다 — .claude/rules/error-codes.md) // 4. 전역 DTO 유효성 파이프 (강력한 검증 단계) app.useGlobalPipes( new ValidationPipe({ whitelist: true, // DTO에 불필요한 값이 오면 삭제 forbidNonWhitelisted: true, // DTO에 없는 필드가 주입되면 400 에러 발생 transform: true, // 네트워크 데이터를 DTO나 기본 타입으로 자동 변환 }), ); // 5. 프론트엔드 연동을 위한 규격화된 에러 & 성공 페이로드 인터셉터 적용 app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new TransformInterceptor(app.get(Reflector))); // 6. Swagger API 문서 설정 const config = new DocumentBuilder() .setTitle('Backend API Documentation') .setDescription('The API documentation conforming to the global guidelines') .setVersion('1.0') .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api-docs', app, document); // 7. Graceful shutdown — SIGTERM 수신 시 새 요청을 받지 않고 // 처리 중인 요청을 마친 뒤 DB/Redis 커넥션을 정리하고 종료한다. // (이 설정이 없으면 배포·재시작 때 처리 중이던 요청이 그대로 끊긴다) app.enableShutdownHooks(); // 종료 로그는 ShutdownLogger(OnApplicationShutdown)가 남긴다. const port = Number(process.env.PORT ?? 3000); await app.listen(port, '0.0.0.0'); Logger.log(`🚀 Backend running on http://localhost:${port}/api`, 'Bootstrap'); Logger.log( `📘 Swagger docs at http://localhost:${port}/api-docs`, 'Bootstrap', ); } void bootstrap();