generated from ttipo/project-base
7bef853664
상품마다 업체에 단축 링크를 발급하고, 그 링크로 들어온 회원의 신청·구매를
업체별로 세기 위해서다. 아임웹은 주문 완료를 알려주지 않으므로 구매는
주문 API 를 주기적으로 훑어 찾는다.
- 아임웹 상품 동기화(imweb_products) — 회원 동기화와 같은 주기, 실패해도 회원 동기화는 계속
- 단축 링크 /r/{코드}: 사람의 클릭만 기록하고 클릭 ID(mg_ref)를 붙여 상품 페이지로 이동
(이동 주소는 아임웹 사이트 안만 허용 — open redirect 방지)
- 회원 ↔ 클릭 연결(POST /api/partner-touches), 신청 기록에 클릭 ID 추가
- 마지막 클릭·30일 귀속, 결제완료 주문만 구매로 집계하고 환불·취소는 제외 (금액은 관리하지 않음)
- 관리자 API: 업체·링크 관리, 링크별 실적 상세, 구매 집계 새로고침
- 권한 요청에 product:read·order:read 추가, 토큰 권한 부족은 IMWEB_006
- 에러 코드 IMWEB_006, PARTNER_001~004 추가 및 클라이언트 사전 동기화
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
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();
|