import { fileURLToPath, URL } from 'node:url' 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(({ 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, // 루트 .env 의 FRONTEND_PORT 를 따른다. // 컨테이너로 띄울 때는 compose 의 `--port`(컨테이너 내부 고정 포트)가 이 값을 덮어쓴다. port: frontendPort, // 포트가 이미 사용 중이면 조용히 다음 포트로 옮기지 않고 실패한다. // 대개 다른 vite(컨테이너 vite 포함 — compose 가 같은 FRONTEND_PORT 를 매핑한다)가 이미 떠 있다는 뜻이라, // 말없이 5174 로 옮겨가면 브라우저는 여전히 5173 의 엉뚱한 서버를 보게 된다 (fail-fast). strictPort: true, // 도커 바인드 마운트에서는 inotify 가 호스트 쪽 변경을 잡지 못해 폴링이 필요하다 watch: usePolling ? { usePolling: true } : undefined, // 개발 서버 API 프록시 — 운영 리버스 프록시(Nginx Proxy Manager 의 Custom location /api)와 같은 역할. // 브라우저는 개발·운영 모두 같은 출처의 /api 로만 호출하므로 CORS 가 끼어들지 않는다. // 접두어 매칭이라 /api-docs(Swagger)도 백엔드로 간다 — 운영의 location /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, }, }, }, } })