o2o-infinith-demo/vite.config.ts
Haewon Kam c90aef654f feat: webforai.kr 방문 측정 (GTM·GA4) 연결과 처리방침 반영
- discovery 빌드에만 GTM(GTM-WQNTZVZH) 스니펫을 넣는다. GA4(G-013Z3BFY35)와 키 이벤트는 컨테이너 안에서 설정한다
- 문의 접수 성공 시 dataLayer 에 generate_lead(lead_source) 를 보낸다. 개인정보는 넣지 않는다
- GTM 가져오기 파일(docs/analytics/gtm_webforai_import.json): GA4 기본 태그, generate_lead·click_tel·click_email
- 개인정보처리방침 제1·5·8조에 Google 애널리틱스 분석 쿠키, 수집 항목, 국외 이전, 거부 방법을 반영한다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:40:31 +09:00

127 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
? ['<meta name="naver-site-verification" content="6ebcb18640acf31bb3653822d178fcf071b94009" />']
: [];
// 링크 공유 미리보기 이미지와 파비콘(scripts/make_share_assets.py discovery 로 생성).
// og:image 는 절대 주소여야 카카오톡·슬랙이 읽는다. public/ 은 infinith-demo 와 공용이라 discovery 빌드에만 넣는다.
const share = isDiscovery
? [
'<meta property="og:url" content="https://webforai.kr" />',
'<meta property="og:image" content="https://webforai.kr/og-discovery.jpg" />',
'<meta property="og:image:width" content="1200" />',
'<meta property="og:image:height" content="630" />',
'<meta name="twitter:card" content="summary_large_image" />',
'<link rel="icon" href="/discovery-favicon.ico" sizes="48x48" />',
'<link rel="icon" type="image/png" sizes="32x32" href="/discovery-favicon-32.png" />',
'<link rel="icon" type="image/png" sizes="192x192" href="/discovery-favicon-192.png" />',
'<link rel="apple-touch-icon" href="/discovery-apple-touch-icon.png" />',
]
: [];
// 방문 측정: Google 태그 관리자(GTM) 컨테이너. GA4(G-013Z3BFY35)와 키 이벤트는 컨테이너 안에서 설정한다.
// 공개 배포(webforai.kr)에만 넣는다. 컨테이너 ID 는 공개 값이다. 개인정보처리방침 제5·8조와 함께 관리한다.
const GTM_ID = 'GTM-WQNTZVZH';
const gtmHead = isDiscovery
? [`<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${GTM_ID}');</script>`]
: [];
const gtmBody = isDiscovery
? `<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>`
: '';
const headHtml = [
`<title>${head.title}</title>`,
`<meta name="description" content="${head.description}" />`,
`<meta property="og:type" content="website" />`,
`<meta property="og:title" content="${head.title}" />`,
`<meta property="og:description" content="${head.description}" />`,
`<meta property="og:locale" content="ko_KR" />`,
...share,
...verify,
...gtmHead,
].join('\n ');
return {
plugins: [
react(),
{
// 루트 /favicon.ico. 미리보기 카드와 일부 브라우저는 <link rel="icon"> 을 보지 않고 루트를 그대로 요청한다.
// 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';
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(/<title>[\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 만으로 할 일이 없는 모듈이다. 쓰는 곳이 없으면 번들에서 뺀다.
// 파생 목업의 최상단 객체 펼치기를 부수효과로 보고 실제 병원 목업이 공개 번들에 남던 문제를 막는다.
moduleSideEffects: (id: string) => !/[\\/]src[\\/](data|services)[\\/]/.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',
},
};
});