- 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>
127 lines
6.5 KiB
TypeScript
127 lines
6.5 KiB
TypeScript
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',
|
||
},
|
||
};
|
||
});
|