템플릿 점검에서 나온 항목을 한 번에 반영한다. 항목별 이유:
- Node 20 → 24 LTS (Dockerfile·CI): Node 20 은 2026-04-30 에 지원이 끝나 보안 패치가 없다
- backend·frontend .dockerignore 추가: `COPY . .` 가 호스트 node_modules(Windows 바이너리)·dist·
.env*(시크릿)를 이미지에 넣고 있었다
- 업로드 디렉터리 쓰기 검사(fail-fast): Linux 에서 Docker 가 root 소유로 만든 바인드 마운트에
비-root 앱이 쓰지 못해 첫 업로드에서야 SYS_001 로 터졌다. 기동 시 해결 명령을 안내한다
- DB 커넥션 풀 100 → 10 (DB_POOL_MAX): PostgreSQL 기본 max_connections(100)를 앱 하나가 다 잡았다
- Swagger 운영 기본 비활성 (SWAGGER_ENABLED): API 구조 전체가 공개 도메인에 노출됐다
- 헬스체크 운영 응답에서 오류 상세 제거: 드라이버 오류 문구에 내부 주소·DB 계정명이 담긴다.
원인은 error 로그로 남긴다
- nginx: location 의 add_header 때문에 정적 자원에서 보안 헤더가 빠지던 문제와,
index.html 이 캐시돼 배포 직후 빈 화면이 날 수 있던 문제를 고친다
- vite allowedHosts: true 제거 (DEV_ALLOWED_HOSTS): DNS rebinding 보호가 꺼져 있었다
- 전역 설정을 configureApp() 으로 분리해 main.ts 와 e2e 가 공유: e2e 가 설정을 복사해 재현해
실제 설정과 어긋나도 통과할 수 있었다. PORT 도 빈 값을 방어하는 파서를 거친다
- 쓰이지 않던 WEBSOCKET_PORT 제거: 별도 포트는 같은 도메인 라우팅과 맞지 않는다
- 예제 useUser 가 메시지 대신 에러 코드를 보관하도록 수정, testing.md 표 깨짐 수정,
settings.json 허용 목록에서 npm install 제거 (Windows lock 파일 규칙과 충돌)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows 바인드 마운트(./frontend:/app) 너머의 소스를 vite 가 모듈마다 읽는 데다
폴링 감시까지 항상 켜져 있어 첫 서빙에 수 분이 걸렸다. 호스트에서 띄우면 2초다.
- dev 오버라이드의 frontend 를 compose profile `frontend` 뒤로 숨긴다 (운영은 그대로)
- 폴링 감시는 CHOKIDAR_USEPOLLING 으로 컨테이너에서만 켠다
- dev 서버 포트·프록시 대상을 루트 .env.{mode} 에서 읽는다 (포트 SSOT 유지)
포트 충돌 시 조용히 옮기지 않도록 strictPort — CORS_ORIGIN·포트 매핑이 묶여 있다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
템플릿이 나중에 반드시 필요해질 공용 기능을 미리 갖춘다.
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>
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>
security.md 가 금지한다고 적어둔 것을 실제 설정이 어기고 있었다.
- 프론트엔드 운영 컨테이너가 root 로 nginx 를 실행하고 있었다.
nginx-unprivileged 로 전환하고 포트를 8080 으로 맞춤(비-root 는 1024 미만 바인드 불가).
- dev 오버라이드가 사용하는 빌드 스테이지에도 USER node 적용.
익명 볼륨이 node 소유로 생성되도록 chown 을 USER 전환 앞에 둔다
(그렇지 않으면 vite 가 node_modules/.vite 캐시를 쓰지 못한다).
- dev 의 DB/Redis 포트가 0.0.0.0 에 바인딩되어 같은 LAN 에 노출돼 있었다 → 127.0.0.1 로 한정.
- CSP 헤더 추가. 대신 현대 브라우저에서 권장되지 않는 X-XSS-Protection 은 제거.
- CORS 차단 시 Error 를 던져 500(SYS_001)이 되던 것을 헤더 미부여 방식으로 변경.
- 헬스체크가 localhost 를 ::1 로 해석해 항상 실패하던 문제 수정(127.0.0.1 명시).
- graceful shutdown 을 위해 init(tini)과 stop_grace_period 적용.
- *.example 의 실제같은 비밀번호를 placeholder 로 교체하고,
비어 있던 서비스 env 예시에 안내를 채움.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>