diff --git a/.claude/rules/checklist.md b/.claude/rules/checklist.md index 1d73b0d..ee5ed19 100644 --- a/.claude/rules/checklist.md +++ b/.claude/rules/checklist.md @@ -13,8 +13,10 @@ — **이 파일이 없으면 `flutter analyze` 가 asset 경고를 낸다** - [ ] 프로젝트 식별 값 교체 (`PROJECT_NAME` 등 루트 env 키) - [ ] 의존성 설치: `cd backend && npm ci`, `cd frontend && npm ci`, `cd flutter && flutter pub get` -- [ ] Docker 개발 환경 기동으로 동작 확인 ([[docker]]) - (Redis 는 기본 비활성이다 — 필요하면 루트 `.env` 에 `COMPOSE_PROFILES=redis`) +- [ ] Docker 개발 환경 기동으로 동작 확인 — backend + db ([[docker]]) + (Redis 와 프론트엔드 컨테이너는 기본 비활성이다 — 필요하면 루트 `.env` 에 `COMPOSE_PROFILES=redis,frontend`) +- [ ] 프론트엔드 dev 서버는 **호스트에서** 기동 — `cd frontend && npm run dev` (→ ) + 컨테이너 vite 는 바인드 마운트 때문에 첫 서빙까지 수 분이 걸려 profile 뒤로 숨겨 두었다 ([[docker]]) ## 2. 커밋 전 공통 점검 (매 작업) diff --git a/.claude/rules/env-structure.md b/.claude/rules/env-structure.md index 3558b5f..7ad4a9b 100644 --- a/.claude/rules/env-structure.md +++ b/.claude/rules/env-structure.md @@ -14,6 +14,8 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{ ├─▶ backend : env_file + environment 로 DB/Redis/CORS/포트 주입 │ (backend/.env.{APP_ENV} 는 "존재"만 하면 됨, 내용 최소) ├─▶ frontend : BACKEND_API_URL → build arg(VITE_API_BASE_URL) 로 주입 + │ (호스트에서 `npm run dev` 로 띄울 때는 compose 를 거치지 않으므로 + │ vite.config.ts 가 이 파일을 `loadEnv` 로 직접 읽어 FRONTEND_PORT/BACKEND_PORT 사용) ├─▶ db : DB_USER/DB_PASSWORD/DB_NAME, DB_DATA_DIR 볼륨 └─▶ redis : REDIS_PASSWORD, REDIS_DATA_DIR 볼륨 ``` @@ -26,6 +28,10 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{ 숫자 설정값은 `shared/utils/env.ts` 의 `parsePositiveInt()` 처럼 빈 문자열까지 방어하는 파서를 거친다. (기존 `.env` 를 쓰는 사람이 새 키를 추가하지 않아도 안전해야 한다) - `docker-compose.yml` 의 `env_file` 지시자 때문에 `backend/.env.{APP_ENV}` 파일은 비어 있어도 **존재해야 한다.** +- **프론트엔드 dev 서버는 compose 를 거치지 않는다** — 호스트에서 직접 띄우므로([[docker]]) + `vite.config.ts` 가 루트 `.env.{mode}` 를 `loadEnv` 로 읽어 포트·프록시 대상을 정한다. + 포트를 `frontend/.env` 나 `vite.config.ts` 에 중복 작성하지 말 것 — 루트가 SSOT 다. + (읽은 값은 dev 서버 설정에만 쓰고 클라이언트 번들에는 넣지 않는다 — `VITE_` 없는 값의 번들 노출 금지, [[security]]) - **Flutter 는 예외** — docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립적으로 관리한다. - 모든 env 파일은 커밋 금지, `*.example` 만 커밋한다. (`.gitignore` 의 `!*.example` 재포함 규칙) - **설정이 잘못되면 서버가 뜨지 않아야 한다(fail-fast).** 조용히 기본값으로 굴러가다 운영에서 터지는 것이 더 위험하다. diff --git a/.claude/skills/docker/SKILL.md b/.claude/skills/docker/SKILL.md index 07306c1..89d6b6c 100644 --- a/.claude/skills/docker/SKILL.md +++ b/.claude/skills/docker/SKILL.md @@ -12,8 +12,12 @@ description: Docker Compose 기반 로컬·운영 오케스트레이션 실행/ ## 개발 환경 +> **개발 환경에서 프론트엔드는 컨테이너로 뜨지 않는다.** compose profile `frontend` 뒤에 숨겨져 있다. +> vite 를 컨테이너에서 돌리면 소스가 바인드 마운트(`./frontend:/app`) 너머라 첫 서빙까지 수 분이 걸린다. +> 프론트엔드는 호스트에서 `cd frontend && npm run dev` 로 띄운다 (아래 "프론트엔드는 호스트에서" 참조). + ```bash -# 코드만 수정했을 때 (의존성 변경 없음) +# 코드만 수정했을 때 (의존성 변경 없음) — backend + db 가 뜬다 docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d # 의존성 변경 시에만 --build @@ -35,6 +39,44 @@ sudo docker compose --env-file .env.production logs -f backend --- +## 프론트엔드는 호스트에서 (개발 환경) + +```bash +cd frontend && npm install && npm run dev # → http://localhost:5173 +``` + +- **포트는 루트 `.env.{APP_ENV}` 가 SSOT 다** ([[env-structure]]). `vite.config.ts` 가 루트 env 를 직접 읽어 + `FRONTEND_PORT` 로 열고, `/api` 프록시 대상은 `http://localhost:${BACKEND_PORT}`(백엔드 컨테이너가 + 호스트에 노출한 포트)로 잡는다. 포트를 바꿀 때 루트 env 한 줄만 고치면 된다. + 백엔드를 아예 다른 주소에 띄웠다면 `VITE_DEV_API_PROXY` 로 대상을 덮어쓴다. +- `strictPort: true` 라 포트가 이미 사용 중이면 **다음 포트로 옮기지 않고 실패**한다. + `CORS_ORIGIN` 과 compose 포트 매핑이 그 포트에 묶여 있어, 조용히 5174 로 옮겨가면 + API 요청이 원인 모르게 막힌다 (fail-fast). +- 폴링 감시(`watch.usePolling`)는 **컨테이너에서만** 켜진다 (`CHOKIDAR_USEPOLLING` 로 판별). + 호스트에서는 OS 파일 감시 이벤트로 충분하며, 폴링을 켜면 기동이 느려지고 CPU 만 쓴다. +- 의존성 설치는 호스트에서 해도 된다 — 프론트엔드 dev 는 컨테이너를 쓰지 않으므로 + lock 파일이 깨지는 문제([[nestjs]] 의 Windows 주의 항목)는 **backend 에만** 해당한다. + 단 `frontend` 프로필이나 운영 이미지를 빌드할 계획이라면 같은 주의가 적용된다. + +### 컨테이너로 띄워야 할 때 (선택) + +호스트에 Node 를 둘 수 없는 환경 등에서만 쓴다. 느린 것은 정상이다. + +```bash +# 일회성 +docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development --profile frontend up -d + +# 상시 — 루트 .env.development 에 (redis 와 함께 켤 때는 frontend,redis) +COMPOSE_PROFILES=frontend +``` + +- 호스트 vite 와 컨테이너 vite 는 같은 호스트 포트(`FRONTEND_PORT`)를 쓴다 — **동시에 띄우지 말 것.** +- 컨테이너 **내부** 포트는 5173 고정이다 (compose 의 `--port`·healthcheck·포트 매핑이 함께 묶여 있다). + 루트 env 는 컨테이너 안에 없으므로 `vite.config.ts` 는 기본값으로 떨어지고, compose 의 `--port` 가 이긴다. +- 운영(`docker-compose.yml` 단독)에는 profile 이 없으므로 nginx 정적 호스팅이 그대로 동작한다. + +--- + ## 자주 쓰는 보조 명령 > 개발 prefix가 길어 아래는 다음 별칭으로 줄여 표기한다. @@ -103,6 +145,7 @@ docker compose -f docker-compose.runner.yml --env-file .env up -d ```bash # 켜기 — 루트 .env.{APP_ENV} 한 줄 수정 (이후 평소 명령 그대로) COMPOSE_PROFILES=redis +# (프론트엔드 컨테이너도 함께 켤 때는 COMPOSE_PROFILES=redis,frontend) # 또는 일회성 dc --profile redis up -d diff --git a/.claude/skills/vue3/SKILL.md b/.claude/skills/vue3/SKILL.md index f9e5508..d80e4ce 100644 --- a/.claude/skills/vue3/SKILL.md +++ b/.claude/skills/vue3/SKILL.md @@ -28,6 +28,12 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API | 라우터 | `src/router/index.ts` | lazy import + `auth.store` 연동 `requiresAuth` 가드 | | env 타입 선언 | `frontend/env.d.ts` | `VITE_API_BASE_URL` | | dev API 프록시 | `vite.config.ts` 의 `server.proxy` | `/api` → 백엔드. 없으면 vite 가 SPA 폴백으로 **HTML 을 200 으로** 돌려줘 조용히 실패한다 | +| dev 서버 포트 | `vite.config.ts` 의 `loadEnv` | 루트 `.env.{mode}` 의 `FRONTEND_PORT`/`BACKEND_PORT` 를 읽는다. `strictPort` 로 포트 충돌 시 fail-fast ([[env-structure]]) | + +> **개발 서버는 호스트에서 띄운다** — `cd frontend && npm run dev` (→ `http://localhost:${FRONTEND_PORT}`). +> 컨테이너 vite 는 바인드 마운트 때문에 첫 서빙에 수 분이 걸려 compose profile `frontend` 뒤로 숨겼다 ([[docker]]). +> 백엔드·DB 는 평소대로 컨테이너로 띄우고, `/api` 는 vite 프록시가 `localhost:${BACKEND_PORT}` 로 넘긴다. +> 두 포트 모두 `vite.config.ts` 가 **루트 `.env.{mode}` 에서 직접 읽는다** — 프론트 쪽에 포트를 중복 작성하지 않는다. > **확장 시 따르는 규칙:** > - **로그인 연동**: 실제 `/auth/login` 엔드포인트 연결 → 성공 시 `useAuth` 가 `authStore.markAuthenticated()` 호출 @@ -217,5 +223,8 @@ router.beforeEach((to) => { - [ ] **lint/test**: 커밋 전 `npm run lint` (`--max-warnings 0`) + `npm run type-check` + `npm run test:unit` 통과. 자동 수정은 `npm run lint:fix`. - [ ] **린트로 강제되는 항목**: Options API·인라인 스타일·`console`·`any` 는 빌드가 실패한다. - [ ] **env**: `VITE_API_BASE_URL` 은 빌드 시 루트 `BACKEND_API_URL` 에서 주입된다 — `frontend/.env` 에 별도 작성 불필요. +- [ ] **dev 서버**: 프론트엔드는 호스트에서 `npm run dev` 로 띄운다 (컨테이너는 `--profile frontend` 선택, [[docker]]). +- [ ] **포트**: `vite.config.ts` 에 포트를 새로 하드코딩하지 않는다 — 루트 `.env.{mode}` 의 `FRONTEND_PORT`/`BACKEND_PORT` 가 SSOT ([[env-structure]]). - [ ] **dev 에서 API 가 안 될 때**: 응답이 JSON 이 아니라 HTML 이면 vite 프록시(`server.proxy`) 문제다. `baseURL` 이 `/api` 상대경로라 프록시가 없으면 index.html 이 200 으로 돌아온다. + 백엔드 컨테이너가 떠 있는지(`docker compose ... ps`), 프록시 대상 포트가 맞는지 확인한다. diff --git a/.env.development.example b/.env.development.example index 9f5fa3b..0fcef22 100644 --- a/.env.development.example +++ b/.env.development.example @@ -9,6 +9,10 @@ BACKEND_DIR=backend # ========================================== # 포트 설정 +# +# 컨테이너 포트 매핑뿐 아니라 **호스트에서 띄우는 vite dev 서버**도 이 값을 따른다. +# (frontend/vite.config.ts 가 이 파일을 직접 읽는다 — FRONTEND_PORT 로 열고 +# /api 프록시는 BACKEND_PORT 로 보낸다. 포트를 바꾸면 CORS_ORIGIN 도 함께 고칠 것) # ========================================== FRONTEND_PORT=5173 BACKEND_PORT=3000 @@ -57,10 +61,16 @@ DB_SYNCHRONIZE=false # ========================================== # 선택적 서비스 (Compose profiles) # -# 기본값(비어 있음) = redis 를 기동하지 않는다. -# 캐시·세션 공유가 필요해지면 아래를 redis 로 바꾸면 컨테이너가 함께 뜬다. -# COMPOSE_PROFILES=redis +# 기본값(비어 있음) = redis 와 frontend 컨테이너를 기동하지 않는다. +# +# redis 캐시·세션 공유가 필요해질 때 → COMPOSE_PROFILES=redis +# frontend vite 를 컨테이너로 띄워야 할 때 → COMPOSE_PROFILES=frontend +# 둘 다 → COMPOSE_PROFILES=redis,frontend +# # 일회성으로 켜려면: docker compose --profile redis ... up -d +# +# ⚠️ 개발 환경의 프론트엔드는 호스트에서 띄우는 것이 기본이다 (`cd frontend && npm run dev`). +# 컨테이너 vite 는 바인드 마운트 때문에 첫 서빙까지 수 분이 걸린다. # ========================================== COMPOSE_PROFILES= diff --git a/README.md b/README.md index d48947a..4beaa01 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,14 @@ cp .env.development.example .env.development # ← 여기 cp backend/.env.development.example backend/.env.development # (존재 필요: JWT 등 고유 시크릿) # frontend 는 compose 가 빌드 시 값을 주입하므로 자체 env 복사 불필요 -# 2) 개발 환경 기동 (코드만 수정 시) +# 2) 백엔드 스택 기동 (backend + db) docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d # 3) 의존성 변경 시에만 --build docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d --build + +# 4) 프론트엔드는 호스트에서 직접 띄운다 (컨테이너보다 수십 배 빠르다) +cd frontend && npm install && npm run dev ``` 기동 후: @@ -41,8 +44,31 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.d - API: - Swagger 문서: +> **개발 환경에서 프론트엔드는 컨테이너로 뜨지 않습니다.** +> vite 를 컨테이너에서 돌리면 소스가 Windows 바인드 마운트 너머에 있어 첫 서빙까지 수 분이 걸립니다. +> 호스트에서 `npm run dev` 로 띄우면 1~2초이고, `/api` 요청은 vite 프록시가 백엔드 컨테이너로 넘깁니다. +> 컨테이너로 띄워야 한다면 `--profile frontend` 를 붙이세요 (아래 "프론트엔드 실행 방식" 참조). +> 운영(`docker-compose.yml` 단독)은 그대로 nginx 정적 호스팅으로 동작합니다. + > Redis 는 기본적으로 뜨지 않습니다. 필요해지면 루트 `.env.{env}` 에 `COMPOSE_PROFILES=redis` 를 설정하세요. +### 프론트엔드 실행 방식 + +| 방식 | 명령 | 첫 서빙 | 용도 | +| --- | --- | --- | --- | +| **호스트 vite (개발 기본)** | `cd frontend && npm run dev` | 1~2초 (최초 의존성 사전번들 10초 내외) | 일상 개발 | +| 컨테이너 vite (선택) | `docker compose ... --profile frontend up -d` | 수 분 (Windows 바인드 마운트) | 호스트에 Node 를 둘 수 없을 때 | +| nginx 정적 (운영) | `docker compose --env-file .env.production up -d --build` | - | 배포 | + +> 컨테이너 방식을 상시 사용하려면 루트 `.env.development` 에 `COMPOSE_PROFILES=frontend` 를 넣습니다. +> (redis 와 함께 켤 때는 `COMPOSE_PROFILES=frontend,redis`) +> 호스트 vite 와 컨테이너 vite 는 같은 호스트 포트를 쓰므로 **동시에 띄우지 마세요.** + +> **포트는 루트 `.env.{env}` 를 따릅니다.** 호스트에서 띄우는 vite 도 `FRONTEND_PORT` 로 열리고, +> `/api` 프록시 대상은 `BACKEND_PORT` 에서 나옵니다 — `vite.config.ts` 가 루트 env 를 직접 읽습니다. +> 포트가 이미 사용 중이면 조용히 다른 포트로 옮기지 않고 **실패**합니다 +> (`CORS_ORIGIN` 과 compose 포트 매핑이 그 값에 묶여 있어, 말없이 옮겨가면 원인 모르게 어긋납니다). + 전체 실행/배포 명령어는 [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md) 참조. ## 모바일 앱 실행 (Flutter) @@ -112,7 +138,7 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.d # 2) backend — DB_HOST 기본값이 localhost 라 그대로 붙는다 cd backend && npm ci && npm run start:dev -# 3) frontend — vite dev 서버가 /api 를 localhost:3000 으로 프록시한다 +# 3) frontend — vite dev 서버가 /api 를 localhost:3000 으로 프록시한다 (개발 기본 방식) cd frontend && npm ci && npm run dev # 4) flutter diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f832449..35bd0c5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,20 @@ services: + # ---------------------------------------- + # frontend — dev 에서는 기본 기동하지 않는다 (profile 로 숨김) + # + # vite 를 컨테이너에서 돌리면 소스가 Windows 바인드 마운트(./frontend:/app) 너머에 있어 + # 수천 개 파일을 매 요청마다 가상 파일시스템으로 읽는다. 여기에 폴링 감시까지 겹쳐 + # 첫 서빙까지 수 분이 걸린다. 호스트에서 직접 띄우면 같은 작업이 1~2초다. + # + # cd frontend && npm install && npm run dev # → http://localhost:5173 + # + # vite dev 프록시가 /api 를 localhost:${BACKEND_PORT} 로 넘기므로 백엔드는 컨테이너 그대로 쓴다. + # 그래도 컨테이너로 띄워야 한다면 (호스트에 Node 를 깔 수 없는 환경 등): + # docker compose ... --profile frontend up -d + # 또는 루트 .env.development 에 COMPOSE_PROFILES=frontend + # ---------------------------------------- frontend: + profiles: ["frontend"] # dev에서는 Docker Desktop 시작 시 자동 실행 방지 (수동 up 때만 실행) restart: "no" build: @@ -12,6 +27,8 @@ services: healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5173/ || exit 1"] environment: + # 바인드 마운트 너머의 변경은 inotify 가 잡지 못하므로 컨테이너에서만 폴링을 켠다 + # (vite.config.ts 가 이 값을 보고 watch.usePolling 을 결정한다 — 호스트 실행 시엔 꺼진다) - CHOKIDAR_USEPOLLING=true - WATCHPACK_POLLING=true # vite dev 서버가 /api 요청을 백엔드 컨테이너로 프록시한다 (vite.config.ts) @@ -60,4 +77,4 @@ services: # base의 proxy 외부 네트워크 의존을 dev에서 제거 networks: - proxy: !reset null \ No newline at end of file + proxy: !reset null diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index f4a00fc..b5e625a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,39 +1,80 @@ import { fileURLToPath, URL } from 'node:url' -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import vueDevTools from 'vite-plugin-vue-devtools' +/** 루트 .env 에 값이 없을 때만 쓰는 기본 포트 (.env.*.example 과 같은 값) */ +const DEFAULT_FRONTEND_PORT = 5173 +const DEFAULT_BACKEND_PORT = 3000 + +/** + * 환경변수를 양의 정수로 파싱한다. + * 값이 없거나(undefined·빈 문자열) 숫자가 아니거나 0 이하면 기본값을 반환한다. + * (compose 는 정의되지 않은 변수를 빈 문자열로 주입한다 — `Number('') === 0` 방어. + * backend 의 `shared/utils/env.ts` 와 같은 규칙) + */ +function parsePositiveInt(value: string | undefined, fallback: number): number { + if (value === undefined || value.trim() === '') return fallback + + const parsed = Number(value) + if (!Number.isInteger(parsed) || parsed <= 0) return fallback + + return parsed +} + // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - vue(), - vueJsx(), - vueDevTools(), - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) +export default defineConfig(({ mode }) => { + // 포트·주소의 SSOT 는 루트 `.env.{mode}` 다 (.claude/rules/env-structure.md). + // 프론트엔드 디렉터리 밖이라 vite 가 자동으로 읽지 않으므로 여기서 직접 로드한다. + // 접두어 ''(전체)로 읽지만, 읽은 값은 **dev 서버 설정에만** 쓰고 클라이언트 번들에는 넣지 않는다 + // (VITE_ 접두어가 없는 값을 번들에 넣으면 시크릿이 노출된다 — .claude/rules/security.md). + // 파일이 없으면(클론 직후 등) process.env 만 반영되고 아래 기본값으로 떨어진다. + const rootEnv = loadEnv(mode, fileURLToPath(new URL('..', import.meta.url)), '') + + const frontendPort = parsePositiveInt(rootEnv.FRONTEND_PORT, DEFAULT_FRONTEND_PORT) + const backendPort = parsePositiveInt(rootEnv.BACKEND_PORT, DEFAULT_BACKEND_PORT) + + // 컨테이너(바인드 마운트) 환경에서만 true — docker-compose.dev.yml 이 주입한다. + // 호스트에서 직접 `npm run dev` 로 띄울 때는 OS 파일 감시 이벤트가 정상 동작하므로 + // 폴링을 켜면 CPU 만 쓰고 기동이 느려진다. + const usePolling = rootEnv.CHOKIDAR_USEPOLLING === 'true' + + return { + plugins: [ + vue(), + vueJsx(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, }, - }, - server: { - // ngrok 등 외부 도메인 접근 허용 (DNS Rebinding 보호 비활성화) - allowedHosts: true, - // 도커/윈도우 환경 파일 핫 리로딩 강제 활성화 - watch: { - usePolling: true, - }, - // 개발 서버 API 프록시. - // useApi 의 baseURL 은 '/api'(상대경로)라 프록시가 없으면 vite 가 SPA 폴백으로 - // index.html 을 200 으로 돌려준다 — 요청이 조용히 HTML 을 받아 실패한다. - // - // target 기본값은 호스트에서 직접 띄울 때 기준이고, - // docker 개발 환경에서는 compose 가 VITE_DEV_API_PROXY=http://backend:3000 을 주입한다. - proxy: { - '/api': { - target: process.env.VITE_DEV_API_PROXY || 'http://localhost:3000', - changeOrigin: true, + server: { + // ngrok 등 외부 도메인 접근 허용 (DNS Rebinding 보호 비활성화) + allowedHosts: true, + // 루트 .env 의 FRONTEND_PORT 를 따른다. + // 컨테이너로 띄울 때는 compose 의 `--port`(컨테이너 내부 고정 포트)가 이 값을 덮어쓴다. + port: frontendPort, + // 포트가 이미 사용 중이면 조용히 다음 포트로 옮기지 않고 실패한다. + // 루트 .env 의 CORS_ORIGIN 과 compose 포트 매핑이 이 포트에 묶여 있어, + // 말없이 5174 로 옮겨가면 API 요청이 원인 모르게 막힌다 (fail-fast). + strictPort: true, + // 도커 바인드 마운트에서는 inotify 가 호스트 쪽 변경을 잡지 못해 폴링이 필요하다 + watch: usePolling ? { usePolling: true } : undefined, + // 개발 서버 API 프록시. + // useApi 의 baseURL 은 '/api'(상대경로)라 프록시가 없으면 vite 가 SPA 폴백으로 + // index.html 을 200 으로 돌려준다 — 요청이 조용히 HTML 을 받아 실패한다. + // + // 기본 대상은 호스트에서 띄울 때 기준이고(백엔드 컨테이너가 BACKEND_PORT 를 호스트에 노출한다), + // 프론트까지 docker 로 띄울 때는 compose 가 VITE_DEV_API_PROXY=http://backend:${BACKEND_PORT} 를 주입한다. + proxy: { + '/api': { + target: rootEnv.VITE_DEV_API_PROXY || `http://localhost:${backendPort}`, + changeOrigin: true, + }, }, }, } diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index c328717..84cc9f1 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -2,8 +2,11 @@ import { fileURLToPath } from 'node:url' import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' import viteConfig from './vite.config' +// vite.config 는 mode 별 설정(포트·프록시)을 계산하는 함수다. +// mergeConfig 는 함수를 병합하지 못하므로 테스트용 ConfigEnv 로 먼저 평가한다. +// (테스트는 dev 서버를 띄우지 않아 포트·프록시 값에 영향받지 않는다) export default mergeConfig( - viteConfig, + viteConfig({ command: 'serve', mode: 'test' }), defineConfig({ test: { environment: 'jsdom',