From 3b397b7788e3ebec5b6ee75bc8e7e1f986d9d873 Mon Sep 17 00:00:00 2001 From: TtiPo Date: Sun, 28 Jun 2026 13:58:02 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=ED=83=80=EC=9E=85=C2=B7=EB=A6=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9=EC=84=B1=20=EB=B3=B4=EA=B0=95=20(lint=C2=B7t?= =?UTF-8?q?ype-check=20=ED=86=B5=EA=B3=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend: HttpExceptionFilter status 를 HttpStatus 로 타입 지정(enum 비교), TransformInterceptor 를 Observable 로, main.ts expressApp 캐스팅·bootstrap void 처리 - frontend: useApi 의 ErrorCodeLexicon 폴백 타입 보강(noUncheckedIndexedAccess) - eslint --fix 포맷 정리(user 컨트롤러/서비스, 페이지) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../common/filters/http-exception.filter.ts | 11 ++++++---- .../interceptors/transform.interceptor.ts | 14 +++++++------ backend/src/main.ts | 20 +++++++++++++------ backend/src/modules/user/user.controller.ts | 16 +++++++++++++-- backend/src/modules/user/user.service.ts | 5 ++++- frontend/src/composables/useApi.ts | 4 ++-- frontend/src/pages/HomePage.vue | 6 +++++- frontend/src/pages/NotFoundPage.vue | 9 +++++++-- 8 files changed, 61 insertions(+), 24 deletions(-) diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index 5db91ff..224d5a5 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -17,9 +17,11 @@ export class HttpExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - 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; } diff --git a/backend/src/common/interceptors/transform.interceptor.ts b/backend/src/common/interceptors/transform.interceptor.ts index dcaa5a8..e1b3353 100644 --- a/backend/src/common/interceptors/transform.interceptor.ts +++ b/backend/src/common/interceptors/transform.interceptor.ts @@ -9,22 +9,24 @@ import { map } from 'rxjs/operators'; export interface StandardResponse { success: boolean; - data: T; + // 핸들러가 undefined/null 을 반환하면 null 로 정규화되므로 nullable + data: T | null; } @Injectable() -export class TransformInterceptor - implements NestInterceptor> -{ +export class TransformInterceptor implements NestInterceptor< + T, + StandardResponse +> { intercept( _context: ExecutionContext, next: CallHandler, ): Observable> { // 이미 Http 응답객체인 경우 등 예외처리가 필요할 수 있으나 기본적으로 data 래핑 - return next.handle().pipe( + return (next.handle() as Observable).pipe( map((data) => ({ success: true, - data: data !== undefined ? data : null, + data: data ?? null, })), ); } diff --git a/backend/src/main.ts b/backend/src/main.ts index 8ce0756..5531264 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -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(); diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index 3033f33..2c31ad2 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -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'; diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index 62fdca5..6fbdad2 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -5,7 +5,10 @@ import { CreateUserDto } from './dto/create-user.dto'; @Injectable() export class UserService { // 임시 인메모리 저장소 — Repository 도입 시 교체 - private readonly users = new Map(); + private readonly users = new Map< + number, + { id: number; email: string; name: string } + >(); findOne(id: number) { const user = this.users.get(id); diff --git a/frontend/src/composables/useApi.ts b/frontend/src/composables/useApi.ts index 50b6e9f..8a7e69d 100644 --- a/frontend/src/composables/useApi.ts +++ b/frontend/src/composables/useApi.ts @@ -15,7 +15,8 @@ export interface StandardResponse { } // 에러 코드 사전 — CLAUDE.md / 백엔드 HttpExceptionFilter 와 1:1 매핑 -const ErrorCodeLexicon: Record = { +// SYS_001 은 폴백 메시지로 항상 존재함을 타입으로 보장 (noUncheckedIndexedAccess 대응) +const ErrorCodeLexicon: Record & { SYS_001: string } = { SYS_001: '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', AUTH_001: '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.', AUTH_002: '안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요.', @@ -27,7 +28,6 @@ const ErrorCodeLexicon: Record = { // 사용자 노출용 알림 — Toast 라이브러리 도입 시 이 함수만 교체하면 됨 const showErrorUI = (message: string) => { - // eslint-disable-next-line no-alert window.alert(message) } diff --git a/frontend/src/pages/HomePage.vue b/frontend/src/pages/HomePage.vue index 6d6538a..879e215 100644 --- a/frontend/src/pages/HomePage.vue +++ b/frontend/src/pages/HomePage.vue @@ -10,7 +10,11 @@ const message = ref('You did it!')

{{ message }}

Visit - vuejs.org + vuejs.org to read the documentation

diff --git a/frontend/src/pages/NotFoundPage.vue b/frontend/src/pages/NotFoundPage.vue index 29ffa1f..68a6705 100644 --- a/frontend/src/pages/NotFoundPage.vue +++ b/frontend/src/pages/NotFoundPage.vue @@ -3,10 +3,15 @@