import { RequestMethod, ValidationPipe } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import type { NestExpressApplication } from '@nestjs/platform-express'; import cookieParser from 'cookie-parser'; import helmet from 'helmet'; import { HttpExceptionFilter } from '@/common/filters/http-exception.filter'; import { TransformInterceptor } from '@/common/interceptors/transform.interceptor'; import { requestIdMiddleware } from '@/common/middleware/request-id.middleware'; import { resolveTrustProxy } from '@/shared/utils/trust-proxy'; /** * JSON 본문 최대 크기. * 메일 발송 본문은 100,000자까지 받는다 — 한글은 UTF-8 로 3바이트라 약 300KB 다 (기본값 100KB 로는 잘린다). */ export const JSON_BODY_LIMIT = '1mb'; /** * HTTP 파이프라인 전역 설정 — main.ts 와 e2e 테스트가 **같은 함수**를 쓴다 * (따로 적으면 e2e 가 실제 응답과 다른 설정으로 돌게 된다). * * 앱은 `{ bodyParser: false }` 로 만들어야 한다 — 본문 파서를 여기서 직접 등록한다. * CORS·Swagger·종료 훅은 배포 환경에 묶인 설정이라 main.ts 에 둔다. */ export function configureApp(app: NestExpressApplication): void { // 요청 추적 ID — **가장 먼저** 붙인다. 본문 파서·helmet 보다 앞이라 그 단계의 실패(413·400·415)에도 // 응답 traceId 와 로그가 남고, 이후 모든 처리가 요청 컨텍스트(traceTag) 안에서 돈다 app.use(requestIdMiddleware); // 믿을 프록시 뒤에서 실제 클라이언트 IP 를 읽는다 — 로그인 실패 한도·요청 수 한도의 기준 (TRUST_PROXY). // 운영 체인: 클라이언트 → 외부 리버스 프록시 → 프론트 nginx → 백엔드. 값이 없으면(비운영) 1 app.set('trust proxy', resolveTrustProxy(process.env.TRUST_PROXY)); // 글로벌 prefix — 모든 라우트가 /api/* (클라이언트 baseURL 과 일치) app.setGlobalPrefix('api', { exclude: [{ path: '/', method: RequestMethod.GET }], }); // 보안 헤더 app.use(helmet()); // 인증 쿠키(access_token·refresh_token)를 읽는다 app.use(cookieParser()); // JSON 만 받는다 — urlencoded 파서를 등록하지 않으면 다른 사이트의
으로 로그인·발송을 시도하는 // 요청(CSRF)은 본문이 비어 VAL_001 이 된다. JSON 요청은 CORS 사전 확인을 거치므로 화이트리스트가 막는다. // inflate=false: 압축된 요청 본문을 받지 않는다 (브라우저는 보내지 않는다 — 압축 폭탄·body-parser DoS 경로 차단) app.useBodyParser('json', { limit: JSON_BODY_LIMIT, inflate: false }); // 전역 DTO 검증 app.useGlobalPipes( new ValidationPipe({ whitelist: true, // DTO 에 없는 값은 버린다 forbidNonWhitelisted: true, // DTO 에 없는 필드가 오면 400 transform: true, // DTO·기본 타입으로 변환 }), ); // 표준 실패·성공 응답 app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new TransformInterceptor(app.get(Reflector))); }