first commit

This commit is contained in:
2026-06-16 17:12:08 +09:00
commit e1bc5a1dfc
257 changed files with 49823 additions and 0 deletions
@@ -0,0 +1,14 @@
import { HttpException, HttpStatus } from '@nestjs/common';
// 비즈니스 예외 — 에러 코드와 사용자 노출 문구를 명시적으로 지정한다.
// HttpExceptionFilter 가 { code, message } 를 그대로 표준 응답으로 변환한다.
// (status 기반 자동 매핑보다 우선 적용됨)
export class BusinessException extends HttpException {
constructor(
code: string,
message: string,
status: HttpStatus = HttpStatus.OK,
) {
super({ code, message }, status);
}
}
@@ -0,0 +1,101 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Response } from 'express';
// 전역 Exception Filter — CLAUDE.md 에러 코드 맵에 따라 표준 응답으로 변환
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
let code = 'SYS_001';
let message =
'일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
// 0) 명시적 { code, message } 를 가진 비즈니스 예외 — status 매핑보다 우선
// (BusinessException 등에서 사용. 코드/문구를 그대로 사용자에게 노출)
const explicit = this.extractExplicitError(exceptionResponse);
if (explicit) {
response.status(status).json({ success: false, error: explicit });
return;
}
// 1) HTTP 상태 → 에러 코드 매핑
if (status === HttpStatus.UNAUTHORIZED) {
code = 'AUTH_001';
message = '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.';
} else if (status === HttpStatus.FORBIDDEN) {
code = 'AUTH_003';
message = '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.';
} else if (status === HttpStatus.NOT_FOUND) {
code = 'RES_001';
message = '요청하신 정보나 페이지를 찾을 수 없습니다.';
} else if (
status === HttpStatus.BAD_REQUEST ||
status === HttpStatus.UNPROCESSABLE_ENTITY
) {
code = 'VAL_001';
message =
'입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)';
} else {
code = 'BIZ_001';
message = '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.';
// 비즈니스 예외에서는 사용자에게 보일 메시지를 재정의 가능
const overridden = this.extractMessage(exceptionResponse);
if (overridden) {
message = overridden;
}
}
} else {
// 정의되지 않은 예외 → 운영팀이 추적 가능하도록 로그 + SYS_001 응답 유지
this.logger.error('Unhandled Exception', exception as Error);
}
response.status(status).json({
success: false,
error: {
code,
message,
},
});
}
// 예외 응답에서 명시적 { code, message } 추출 (둘 다 문자열일 때만)
private extractExplicitError(
res: string | object,
): { code: string; message: string } | null {
if (typeof res === 'object' && res !== null) {
const r = res as { code?: unknown; message?: unknown };
if (typeof r.code === 'string' && typeof r.message === 'string') {
return { code: r.code, message: r.message };
}
}
return null;
}
// 예외 응답에서 사용자 노출 메시지(문자열) 추출
private extractMessage(res: string | object): string | null {
if (typeof res === 'object' && res !== null && 'message' in res) {
const responseMessage = res.message;
if (typeof responseMessage === 'string') {
return responseMessage;
}
}
return null;
}
}
@@ -0,0 +1,32 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface StandardResponse<T> {
success: boolean;
data: T | null;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<
T,
StandardResponse<T>
> {
intercept(
_context: ExecutionContext,
next: CallHandler<T>,
): Observable<StandardResponse<T>> {
// 이미 Http 응답객체인 경우 등 예외처리가 필요할 수 있으나 기본적으로 data 래핑
return next.handle().pipe(
map((data: T) => ({
success: true,
data: data ?? null,
})),
);
}
}