fix: 기존 코드의 타입·린트 정합성 보강 (lint·type-check 통과)
- backend: HttpExceptionFilter status 를 HttpStatus 로 타입 지정(enum 비교), TransformInterceptor 를 Observable<T> 로, main.ts expressApp 캐스팅·bootstrap void 처리 - frontend: useApi 의 ErrorCodeLexicon 폴백 타입 보강(noUncheckedIndexedAccess) - eslint --fix 포맷 정리(user 컨트롤러/서비스, 페이지) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,11 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
let status: number = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
// 비교 대상인 HttpStatus enum 과 동일 타입으로 선언 (no-unsafe-enum-comparison 대응)
|
||||
let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let code = 'SYS_001';
|
||||
let message = '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';
|
||||
let message =
|
||||
'일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
@@ -39,7 +41,8 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
status === HttpStatus.UNPROCESSABLE_ENTITY
|
||||
) {
|
||||
code = 'VAL_001';
|
||||
message = '입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)';
|
||||
message =
|
||||
'입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)';
|
||||
} else {
|
||||
code = 'BIZ_001';
|
||||
message = '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.';
|
||||
@@ -50,7 +53,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
exceptionResponse !== null &&
|
||||
'message' in exceptionResponse
|
||||
) {
|
||||
const responseMessage = (exceptionResponse as { message: unknown }).message;
|
||||
const responseMessage = exceptionResponse.message;
|
||||
if (typeof responseMessage === 'string') {
|
||||
message = responseMessage;
|
||||
}
|
||||
|
||||
@@ -9,22 +9,24 @@ import { map } from 'rxjs/operators';
|
||||
|
||||
export interface StandardResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
// 핸들러가 undefined/null 을 반환하면 null 로 정규화되므로 nullable
|
||||
data: T | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TransformInterceptor<T>
|
||||
implements NestInterceptor<T, StandardResponse<T>>
|
||||
{
|
||||
export class TransformInterceptor<T> implements NestInterceptor<
|
||||
T,
|
||||
StandardResponse<T>
|
||||
> {
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<StandardResponse<T>> {
|
||||
// 이미 Http 응답객체인 경우 등 예외처리가 필요할 수 있으나 기본적으로 data 래핑
|
||||
return next.handle().pipe(
|
||||
return (next.handle() as Observable<T>).pipe(
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data: data !== undefined ? data : null,
|
||||
data: data ?? null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
+14
-6
@@ -5,7 +5,11 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import type { Request as ExpressRequest, Response as ExpressResponse } from 'express';
|
||||
import type {
|
||||
Express,
|
||||
Request as ExpressRequest,
|
||||
Response as ExpressResponse,
|
||||
} from 'express';
|
||||
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||
@@ -17,7 +21,7 @@ async function bootstrap() {
|
||||
});
|
||||
|
||||
// 리버스 프록시(Nginx, Docker 배포 환경 등) 뒤에서 올바른 클라이언트 IP 식별 지원을 위해 설정합니다.
|
||||
const expressApp = app.getHttpAdapter().getInstance();
|
||||
const expressApp = app.getHttpAdapter().getInstance() as Express;
|
||||
expressApp.set('trust proxy', 1);
|
||||
|
||||
// 글로벌 prefix — 모든 라우트가 /api/* 로 정규화됨 (Flutter, Web 양쪽 baseURL과 일치)
|
||||
@@ -66,8 +70,9 @@ async function bootstrap() {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'SYS_001',
|
||||
message: '일시적인 시스템 오류가 발생했습니다. (요청 한도 초과) 잠시 후 다시 시도해 주세요.', // 글로벌 통합 에러 포맷
|
||||
}
|
||||
message:
|
||||
'일시적인 시스템 오류가 발생했습니다. (요청 한도 초과) 잠시 후 다시 시도해 주세요.', // 글로벌 통합 에러 포맷
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -98,6 +103,9 @@ async function bootstrap() {
|
||||
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');
|
||||
Logger.log(
|
||||
`📘 Swagger docs at http://localhost:${port}/api-docs`,
|
||||
'Bootstrap',
|
||||
);
|
||||
}
|
||||
bootstrap();
|
||||
void bootstrap();
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { Body, Controller, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { UserService } from './user.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import { CreateUserDto } from './dto/create-user.dto';
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
// 임시 인메모리 저장소 — Repository 도입 시 교체
|
||||
private readonly users = new Map<number, { id: number; email: string; name: string }>();
|
||||
private readonly users = new Map<
|
||||
number,
|
||||
{ id: number; email: string; name: string }
|
||||
>();
|
||||
|
||||
findOne(id: number) {
|
||||
const user = this.users.get(id);
|
||||
|
||||
@@ -15,7 +15,8 @@ export interface StandardResponse<T = unknown> {
|
||||
}
|
||||
|
||||
// 에러 코드 사전 — CLAUDE.md / 백엔드 HttpExceptionFilter 와 1:1 매핑
|
||||
const ErrorCodeLexicon: Record<string, string> = {
|
||||
// SYS_001 은 폴백 메시지로 항상 존재함을 타입으로 보장 (noUncheckedIndexedAccess 대응)
|
||||
const ErrorCodeLexicon: Record<string, string> & { SYS_001: string } = {
|
||||
SYS_001: '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
AUTH_001: '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.',
|
||||
AUTH_002: '안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요.',
|
||||
@@ -27,7 +28,6 @@ const ErrorCodeLexicon: Record<string, string> = {
|
||||
|
||||
// 사용자 노출용 알림 — Toast 라이브러리 도입 시 이 함수만 교체하면 됨
|
||||
const showErrorUI = (message: string) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
window.alert(message)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ const message = ref<string>('You did it!')
|
||||
<h1>{{ message }}</h1>
|
||||
<p>
|
||||
Visit
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a>
|
||||
<a
|
||||
href="https://vuejs.org/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>vuejs.org</a>
|
||||
to read the documentation
|
||||
</p>
|
||||
</main>
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="not-found" role="alert">
|
||||
<main
|
||||
class="not-found"
|
||||
role="alert"
|
||||
>
|
||||
<h1>404</h1>
|
||||
<p>요청하신 정보나 페이지를 찾을 수 없습니다.</p>
|
||||
<RouterLink to="/">홈으로 돌아가기</RouterLink>
|
||||
<RouterLink to="/">
|
||||
홈으로 돌아가기
|
||||
</RouterLink>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user