first commit
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user