Commit Graph

9 Commits

Author SHA1 Message Date
ttipo 52e671cedb fix(backend): 운영 공개 URL 에 로컬 주소가 남으면 기동을 실패시킴
.env.production.example 이 개발용 값(localhost)을 그대로 담고 있어, 복사해 배포하면
서버는 정상 기동하지만 CORS 화이트리스트가 실제 도메인을 막고 프론트 번들은
localhost 를 호출한다 — 브라우저에서만 조용히 실패해 원인을 찾기 어렵다.

- CORS_ORIGIN·BACKEND_API_URL 에 로컬 주소나 예시 도메인이 있으면 운영에서 기동 실패
- 예제 값을 도메인 형태로 교체 (그대로 두면 이 검사에 걸린다)
- BACKEND_API_URL 은 앱이 쓰지 않지만 검사를 위해 compose 가 backend 에 주입한다
- 로컬에서 운영 이미지를 검증할 때만 ALLOW_LOCALHOST_URL=true 로 건너뛴다

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 11:42:23 +09:00
ttipo aba274e56f feat(backend): 파일 업로드 공통 골격 추가 (로컬 디스크)
CI / Backend (lint · test · e2e · build) (push) Has been cancelled
CI / Frontend (lint · type-check · test) (push) Has been cancelled
CI / Flutter (analyze · test) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
템플릿이 나중에 반드시 필요해질 공용 기능을 미리 갖춘다.
multer 취약점을 해소한 김에, 안전한 업로드 기준도 함께 세운다.

- modules/upload — 단일 파일 업로드 엔드포인트(POST /api/files)와 재사용 가능한 검증 설정
- 확장자와 MIME 을 **둘 다** 검사하는 허용목록 방식.
  확장자만 보면 위장이 쉽고, MIME 만 보면 클라이언트가 보낸 값이라 신뢰할 수 없다.
  .html/.svg/.js 등 브라우저가 실행할 수 있는 형식은 허용 목록에서 제외.
- 저장 파일명은 서버가 UUID 로 생성한다. 클라이언트 파일명을 경로에 쓰면 경로 탈출이 가능하다.
- 크기 한도(UPLOAD_MAX_SIZE_MB, 기본 5MB) 초과 시 FILE_002(413).
- 도메인 에러 코드 FILE_001/FILE_002 추가 후 세 플랫폼 사전 동기화.

함께 고친 것:

- 전역 필터가 프레임워크의 영문 메시지를 그대로 노출하고 있었다.
  크기 초과 시 multer 의 "File too large" 가 사용자에게 보였다 —
  이제 코드에 대응하는 레지스트리 문구만 사용하며, 재정의는 BusinessException 으로만 가능하다.
- sync:error-codes 가 컨테이너 안에서 실행되면 형제 디렉터리를 찾지 못해
  cryptic 한 mkdir 오류를 냈다. 원인과 해결 방법을 알리도록 보강.
- 업로드 볼륨 경로에 compose 기본값을 주어 기존 .env 에 키가 없어도 동작하게 함.

실제 multipart 요청으로 정상 업로드·형식 거부·MIME 위장·경로 탈출 시도·크기 초과·
파일 누락 시나리오를 모두 검증했다.

알려진 한계(확장자·MIME 검사는 파일 내용을 보지 않는다)는 security.md 에 명시하고,
업로드 디렉터리를 정적 서빙하지 말 것을 규칙으로 남겼다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 21:47:10 +09:00
ttipo 32eb4fafca fix(frontend): dev 서버 API 프록시 추가 — 프론트가 백엔드를 호출하지 못하던 문제
CI / Backend (lint · test · e2e · build) (push) Has been cancelled
CI / Frontend (lint · type-check · test) (push) Has been cancelled
CI / Flutter (analyze · test) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
useApi 의 baseURL 이 '/api' 상대경로인데 vite dev 서버에 프록시가 없어,
브라우저의 API 요청이 백엔드에 닿지 않고 SPA 폴백으로 index.html 을 받고 있었다.

더 나쁜 것은 조용히 실패한다는 점이다 — HTTP 200 에 Content-Type: text/html 이라
인터셉터의 success 판별을 통과하지 못하고 HTML 문자열이 그대로 반환된다.
템플릿의 화면이 아직 API 를 호출하지 않아 드러나지 않았을 뿐이다.

- vite.config.ts 에 /api 프록시 추가. 대상은 VITE_DEV_API_PROXY 로 조정 가능하며
  docker 개발 환경에서는 compose 가 http://backend:3000 을 주입한다.
- DB_HOST 기본값을 'db' 에서 'localhost' 로 변경.
  compose 는 항상 DB_HOST=db 를 주입하므로 이 기본값은 호스트에서 직접 실행할 때만
  쓰인다 — 'db' 는 호스트에서 해석되지 않아 로컬 실행이 불가능했다.
- README 의 "Docker 없이 로컬 개발" 절차가 위 두 문제로 동작하지 않았다. 실제로 되는 절차로 교체.

프록시 경유 요청이 JSON 을 반환하고 SPA 라우팅도 유지되는 것을 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:28:14 +09:00
ttipo a217d13483 feat(backend): DB 마이그레이션 워크플로 추가
CI / Backend (lint · test · e2e · build) (push) Has been cancelled
CI / Frontend (lint · type-check · test) (push) Has been cancelled
CI / Flutter (analyze · test) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
엔티티를 추가하면 개발에서는 동작하지만 운영 배포 시 테이블이 생성되지 않는
구조적 결함이 있었다. app.module 은 운영에서 migrationsRun 을 켜두었지만
마이그레이션을 만들 수단(DataSource·CLI 스크립트·디렉터리)이 전혀 없었고,
규칙 문서도 "운영에서는 마이그레이션이 강제된다"는 원칙만 있고 절차가 없었다.

- CLI 전용 DataSource(src/data-source.ts)와 migration:* 스크립트 추가.
  ts-node 에 tsconfig-paths 를 등록해 엔티티가 @/ alias 를 써도 동작한다.
- DB 접속 설정을 database.config.ts 로 모아 앱과 CLI 가 공유하게 함.
  두 곳에 나눠 적으면 반드시 어긋난다.
- synchronize 기본값을 false 로 변경 (DB_SYNCHRONIZE=true 로 개발 한정 opt-in).
  켜져 있으면 개발 DB 가 항상 최신이라 migration:generate 가 "변경 없음"만 내놓아
  마이그레이션을 만들 수 없다 — 이것이 위 결함의 직접 원인이었다.
  운영에서는 이 값과 무관하게 항상 비활성화된다.
- 엔티티 추가 절차와 주의사항(생성 SQL 검토, 커밋 후 수정 금지)을 문서화하고
  커밋 전 체크리스트에 스키마 변경 항목 추가.

generate → run → revert 전 과정을 임시 엔티티로 검증했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:34:16 +09:00
ttipo bf102d758f feat: 기동·종료·헬스체크 견고성 보강
운영에서 조용히 문제를 일으킬 수 있는 지점들을 정리한다.

- env 검증(fail-fast): 설정이 잘못되면 서버가 뜨지 않는다.
  운영에서 DB_* 누락과 'change-me' 같은 예시 값 배포를 차단하고,
  문제를 하나씩이 아니라 한 번에 모아 보여준다.
  DB_PASSWORD 가 없을 때 'password' 로 조용히 동작하던 폴백도 제거.
- graceful shutdown: SIGTERM 시 요청·커넥션을 정리한 뒤 종료한다.
  종료 로그는 OnApplicationShutdown 훅으로 남긴다 —
  process.on(signal) 로 직접 잡으면 Nest 의 종료 흐름에 간섭해 종료 코드가 달라진다.
- 헬스체크 두 가지 결함 수정:
  pingCheck 기본 타임아웃 1초가 짧아 유휴 커넥션 재연결 시 정상 DB 를 down 으로 오판했고,
  실패 응답이 SYS_001 로 덮여 어느 구성요소가 죽었는지 알 수 없었다.
  모니터링이 읽는 엔드포인트이므로 표준 래퍼·에러 코드에서 제외한다.
- Rate limiting 을 @nestjs/throttler 로 일원화 (express-rate-limit 제거).
  두 리미터가 겹쳐 헤더의 한도와 실제 동작이 어긋나 있었다.
- e2e 를 응답 계약 검증으로 교체. 기존 테스트는 없는 경로를 검사해 이미 실패 상태였다.
- 로깅 규칙(logging.md) 신설 — 레벨 기준·민감정보 마스킹·traceId

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:30:29 +09:00
ttipo c5d165275c feat: 응답 계약 정의와 페이지네이션·추적 ID 추가
세 클라이언트가 공유하는 계약 중 정의가 빠져 있던 부분을 채운다.

- 목록 응답 형태를 {items, pageInfo} 로 고정하고 커서/오프셋 쿼리 DTO 제공
- @SkipTransform() 으로 스트림·204 등 래핑하면 안 되는 응답을 제외
- ApiStandardResponse/ApiErrorResponse 로 Swagger 문서를 실응답과 일치시킴
  (컨트롤러는 raw 를 반환하지만 실제 응답은 래핑되어 문서가 어긋나 있었다)
- RequestIdMiddleware 로 모든 요청에 x-request-id 부여
- API 설계 규약(api-design.md) 신설 — REST 경로·쿼리·날짜/ID 표현·버전 관리

또한 .gitignore 의 data/ 패턴이 모든 depth 에 매칭되어
flutter/lib/features/user/data/ 의 소스가 커밋되지 않던 문제를 함께 고친다.
(user_provider.dart 가 import 하는 파일이 저장소에 없어 flutter analyze 가 깨져 있었다)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:30:07 +09:00
ttipo f7ea60b8e7 feat: 에러 코드 SSOT 체계 도입
사용자에게 노출되는 실패가 에러 코드 없이 만들어질 수 없도록 구조를 바꾼다.

기존에는 예외 필터가 HTTP 상태로 코드를 추론해 도메인별 에러를 표현할 수 없었고,
세 플랫폼의 에러 사전을 손으로 맞춰야 해서 어긋나기 쉬웠다.

- 코드↔메시지↔HTTP 상태를 error-codes.ts 레지스트리 한 곳으로 통합
- BusinessException 은 에러 코드를 생성자 필수 인자로 받아 코드 누락을 타입으로 차단
- sync:error-codes 스크립트가 프론트·플러터 사전을 자동 생성 (수동 복사 제거)
- 429 전용 코드 REQ_001 추가 (기존에는 "시스템 오류"로 잘못 안내)
- AUTH_002(세션 만료)를 클라이언트에서 실제로 발행하도록 구현
- 응답에 traceId 를 실어 로그와 대조 가능하게 함

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:29:15 +09:00
ttipo 3b397b7788 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>
2026-06-28 13:58:02 +09:00
ttipo 4d70b1428a first commit 2026-05-18 11:25:51 +09:00