import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import fs from 'fs'; import path from 'path'; import {defineConfig, loadEnv} from 'vite'; import { supabaseSync } from './plugins/vite-plugin-supabase-sync'; export default defineConfig(({mode}) => { const env = loadEnv(mode, '.', ''); // 배포별 head 메타. VITE_SITE=discovery 로 빌드한 배포(webforai.kr)는 AI Discovery 랜딩이 루트라 // 제목·설명도 그에 맞아야 한다. index.html 은 두 배포가 공용이므로 빌드 때 갈아끼운다. // 답변엔진이 읽는 첫 신호라 비워 두지 않는다. const isDiscovery = env.VITE_SITE === 'discovery'; const head = isDiscovery ? { title: 'AI Discovery · 우리 병원은 AI 답변에 나오고 있습니까', description: '병원 홈페이지 주소 하나로 AI 답변 준비도를 진단하고, 무엇을 고쳐야 하는지 근거와 함께 알려 드립니다.', } : { title: 'INFINITH - Infinite Marketing', description: 'AI 마케팅 분석 플랫폼. 채널을 진단하고 전략과 로드맵을 제안합니다.', }; // 검색엔진 소유 확인. 배포마다 값이 다르므로 여기서 갈아끼운다. // 네이버 서치어드바이저는 DNS 를 받지 않아 메타 태그로만 확인된다. // 구글은 webforai.kr 도메인 속성을 DNS TXT 로 확인했으므로 메타가 필요 없다. const verify = isDiscovery ? [''] : []; // 링크 공유 미리보기 이미지와 파비콘(scripts/make_share_assets.py discovery 로 생성). // og:image 는 절대 주소여야 카카오톡·슬랙이 읽는다. public/ 은 infinith-demo 와 공용이라 discovery 빌드에만 넣는다. const share = isDiscovery ? [ '', '', '', '', '', '', '', '', '', ] : []; // 방문 측정: Google 태그 관리자(GTM) 컨테이너. GA4(G-013Z3BFY35)와 키 이벤트는 컨테이너 안에서 설정한다. // 공개 배포(webforai.kr)에만 넣는다. 컨테이너 ID 는 공개 값이다. 개인정보처리방침 제5·8조와 함께 관리한다. const GTM_ID = 'GTM-WQNTZVZH'; const gtmHead = isDiscovery ? [``] : []; const gtmBody = isDiscovery ? `` : ''; const headHtml = [ `${head.title}`, ``, ``, ``, ``, ``, ...share, ...verify, ...gtmHead, ].join('\n '); return { plugins: [ react(), { // 루트 /favicon.ico. 미리보기 카드와 일부 브라우저는 을 보지 않고 루트를 그대로 요청한다. // vercel.json 의 SPA 리라이트가 모든 경로를 index.html 로 보내므로, 파일이 없으면 HTML 이 아이콘 자리에 온다. // public/ 은 infinith-demo 배포와 공용이라 파일명을 나눠 두었고, 빌드 때 이 배포의 것만 루트 이름으로 복사한다. name: 'infinith-root-favicon', apply: 'build' as const, writeBundle(options: {dir?: string}) { if (!isDiscovery) return; const out = options.dir ?? 'dist'; // public/ 은 infinith-demo 와 공용이라 실제 병원 리포트 원본(reports/)과 병원 채널 스크린샷(assets/clients/)이 들어 있다. // 공개 배포(webforai.kr)에서는 링크가 없어도 주소만 알면 열리므로 빌드 결과에서 지운다 (2026-10-07). for (const dir of ['reports', path.join('assets', 'clients')]) { fs.rmSync(path.resolve(out, dir), { recursive: true, force: true }); } for (const [from, to] of [ ['discovery-favicon.ico', 'favicon.ico'], ['discovery-favicon-32.png', 'favicon-32.png'], ['discovery-favicon-192.png', 'favicon-192.png'], ['discovery-apple-touch-icon.png', 'apple-touch-icon.png'], ]) { const src = path.resolve(out, from); if (fs.existsSync(src)) fs.copyFileSync(src, path.resolve(out, to)); } }, }, { name: 'infinith-head', transformIndexHtml(html: string) { return html .replace(/[\s\S]*?<\/title>/, headHtml) .replace('<body>', gtmBody ? `<body>\n ${gtmBody}` : '<body>'); }, }, tailwindcss(), supabaseSync({ supabaseUrl: env.VITE_SUPABASE_URL ?? '', serviceRoleKey: env.SUPABASE_SERVICE_ROLE_KEY ?? env.VITE_SUPABASE_ANON_KEY ?? '', }), ], define: { // 공개 배포(webforai.kr)에는 이미지 생성 기능이 없으므로 키를 번들에 넣지 않는다. 브라우저로 내려간 값은 공개다. 'process.env.GEMINI_API_KEY': JSON.stringify(isDiscovery ? '' : env.GEMINI_API_KEY), }, build: { rollupOptions: { treeshake: { // src/data(목업·진단 데이터)와 src/services 는 import 만으로 할 일이 없는 모듈이다. 쓰는 곳이 없으면 번들에서 뺀다. // 파생 목업의 최상단 객체 펼치기를 부수효과로 보고 실제 병원 목업이 공개 번들에 남던 문제를 막는다. // src/lib/supabase 도 같다. 최상단 createClient() 때문에 공개 배포에 Supabase 접속 정보가 남았다 (2026-10-07). moduleSideEffects: (id: string) => !/[\\/]src[\\/](data|services)[\\/]/.test(id) && !/[\\/]src[\\/]lib[\\/]supabase\.ts$/.test(id), }, }, }, resolve: { alias: { '@': path.resolve(__dirname, '.'), }, }, server: { // HMR is disabled in AI Studio via DISABLE_HMR env var. // Do not modify—file watching is disabled to prevent flickering during agent edits. hmr: process.env.DISABLE_HMR !== 'true', }, }; });