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:
2026-06-28 13:58:02 +09:00
parent b35ddfc9c3
commit 3b397b7788
8 changed files with 61 additions and 24 deletions
@@ -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
View File
@@ -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();
+14 -2
View File
@@ -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';
+4 -1
View File
@@ -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);
+2 -2
View File
@@ -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)
}
+5 -1
View File
@@ -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>
+7 -2
View File
@@ -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>