diff --git a/index.html b/index.html
index 234bf1e..b5b1053 100644
--- a/index.html
+++ b/index.html
@@ -1,9 +1,64 @@
-
+
- INFINITH - Infinite Marketing
+ INFINITH - 성형외과 AI 마케팅 전략 플랫폼
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index fc23268..159a2f3 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
- "build": "vite build",
+ "build": "vite build && vite build --ssr src/entry-server.tsx --outDir dist/server && tsx scripts/prerender.ts",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit",
diff --git a/public/llms.txt b/public/llms.txt
new file mode 100644
index 0000000..57ee1f1
--- /dev/null
+++ b/public/llms.txt
@@ -0,0 +1,25 @@
+# INFINITH
+
+> 성형외과·피부과를 위한 AI 마케팅 분석 및 전략 기획 플랫폼. 병원의 유튜브, 인스타그램, 페이스북, 네이버 블로그, 네이버 플레이스, 강남언니 채널을 자동 수집·분석해 마케팅 진단 리포트, 채널별 변환 전략, 실행 로드맵, KPI를 제안합니다.
+
+## 핵심 기능
+
+- 다채널 자동 수집: 병원 홈페이지 URL 하나로 SNS 채널을 발견하고 YouTube Data API, Instagram/Facebook, Naver API, 강남언니 데이터를 실측 수집
+- AI 진단 리포트: 채널별 감사(Audit), 강점·약점, 경쟁 포지셔닝을 정량 수치와 함께 제시
+- 전략 기획: 채널별 변환 전략, 90일 실행 로드맵, KPI 설계
+- 콘텐츠 스튜디오: AI 이미지 생성으로 콘텐츠 제작 지원
+- 성과 분석: 배포 이후 채널 성과 추적
+
+## 대상
+
+- 서울 강남권을 포함한 국내 성형외과·피부과
+- 프리미엄 의료 마케팅을 내부 역량으로 운영하려는 병원 마케팅 담당자와 원장
+
+## 페이지
+
+- [홈](https://infinith-demo.vercel.app/): 서비스 소개, 대상, 문제 정의, 해결 방식, 핵심 모듈, 활용 사례
+- [요금제](https://infinith-demo.vercel.app/pricing): 월간·연간 요금
+
+## 연락
+
+- 이메일: team.backstageai@gmail.com
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..9006baa
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,28 @@
+# INFINITH — AI 크롤러 및 검색엔진 전면 허용
+User-agent: *
+Allow: /
+Disallow: /report/loading
+Disallow: /api-dashboard
+Disallow: /data-validation
+
+# AI 답변엔진 크롤러 (명시 허용)
+User-agent: GPTBot
+Allow: /
+User-agent: OAI-SearchBot
+Allow: /
+User-agent: ChatGPT-User
+Allow: /
+User-agent: ClaudeBot
+Allow: /
+User-agent: anthropic-ai
+Allow: /
+User-agent: PerplexityBot
+Allow: /
+User-agent: Google-Extended
+Allow: /
+User-agent: Bingbot
+Allow: /
+User-agent: Yeti
+Allow: /
+
+Sitemap: https://infinith-demo.vercel.app/sitemap.xml
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..c39652f
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,15 @@
+
+
+
+ https://infinith-demo.vercel.app/
+ 2026-08-28
+ weekly
+ 1.0
+
+
+ https://infinith-demo.vercel.app/pricing
+ 2026-08-28
+ monthly
+ 0.8
+
+
diff --git a/scripts/prerender.ts b/scripts/prerender.ts
new file mode 100644
index 0000000..88a1686
--- /dev/null
+++ b/scripts/prerender.ts
@@ -0,0 +1,19 @@
+// vite build(클라이언트) + vite build --ssr(서버) 이후 실행.
+// dist/server/entry-server.js 의 render()로 랜딩 HTML을 만들어 dist/index.html 의 #root 에 주입한다.
+import { readFileSync, writeFileSync, rmSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+const dist = resolve(process.cwd(), 'dist');
+const template = readFileSync(resolve(dist, 'index.html'), 'utf-8');
+const { render } = await import(resolve(dist, 'server/entry-server.js'));
+
+const routes = ['/'];
+for (const url of routes) {
+ const appHtml = render(url);
+ const html = template.replace('', `${appHtml}
`);
+ const out = url === '/' ? 'index.html' : `${url.replace(/^\//, '')}/index.html`;
+ writeFileSync(resolve(dist, out), html);
+ const words = appHtml.replace(/<[^>]*>/g, ' ').split(/\s+/).filter(Boolean).length;
+ console.log(`[prerender] ${url} -> dist/${out} (${words} words)`);
+}
+rmSync(resolve(dist, 'server'), { recursive: true, force: true });
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
new file mode 100644
index 0000000..aa03710
--- /dev/null
+++ b/src/components/Badge.tsx
@@ -0,0 +1,96 @@
+/**
+ * Badge — 작은 상태/라벨 칩 컴포넌트.
+ *
+ * 3종 variant:
+ * - 'available' → Product 1.0에서 이미 동작하는 기능 표시 (Modules·Pricing 공용)
+ * - 'roadmap' → Q4 2026 예정 기능 (Mock/미구현) 표시
+ * - 'popular' → Pricing 카드 중 메인 추천 티어 하이라이트
+ *
+ * 사용처:
+ * - src/components/Modules.tsx — 각 모듈 카드 우측 상단
+ * - src/pages/PricingPage.tsx — INTELLIGENCE 카드 "가장 인기있는 플랜"
+ */
+
+import { CheckCircle2, Clock, Sparkles } from 'lucide-react';
+
+export type BadgeVariant = 'available' | 'roadmap' | 'popular';
+
+interface BadgeProps {
+ variant: BadgeVariant;
+ /** 직접 라벨을 주면 variant 기본값을 오버라이드 */
+ label?: string;
+ size?: 'sm' | 'md';
+ className?: string;
+}
+
+/**
+ * variant별 스타일 & 기본 라벨 매핑.
+ *
+ * ⚠️ 이 매핑은 "랜딩 전체의 시각적 위계"를 결정하는 핵심 지점입니다.
+ * - available: 편안한 성공 톤 (눈이 지치지 않게, 하지만 '확인됨' 신호 명확)
+ * - roadmap: 낮은 채도 (주목받지 말아야 함 — 메인 톤을 갉아먹지 않도록)
+ * - popular: accent 그라디언트 (시선 끌기, 단 1개 카드에만 사용 전제)
+ *
+ * 아래 기본값은 starting point입니다. 랜딩 톤과 맞춰 조정하세요.
+ */
+const variantStyles: Record<
+ BadgeVariant,
+ { bg: string; text: string; border: string; Icon: typeof CheckCircle2; defaultLabel: string }
+> = {
+ available: {
+ // TODO(user): 초록 계열 vs accent 파스텔 톤 중 선택. 현재는 청록 파스텔.
+ bg: 'bg-emerald-50/80',
+ text: 'text-emerald-700',
+ border: 'border-emerald-200/60',
+ Icon: CheckCircle2,
+ defaultLabel: 'Available Now',
+ },
+ roadmap: {
+ // TODO(user): 회색 톤 강도. 너무 흐리면 안 보이고, 너무 진하면 주목받음.
+ bg: 'bg-slate-100/80',
+ text: 'text-slate-500',
+ border: 'border-slate-200/60',
+ Icon: Clock,
+ defaultLabel: 'Q4 2026 Roadmap',
+ },
+ popular: {
+ // TODO(user): 단색 vs 그라디언트. Modules와 겹치지 않게 Pricing 전용 포인트 컬러 권장.
+ bg: 'bg-gradient-to-r from-accent/10 to-purple-200/40',
+ text: 'text-accent',
+ border: 'border-accent/30',
+ Icon: Sparkles,
+ defaultLabel: '가장 인기있는 플랜',
+ },
+};
+
+const sizeStyles = {
+ sm: 'text-xs px-2 py-0.5 gap-1',
+ md: 'text-sm px-3 py-1 gap-1.5',
+} as const;
+
+const iconSize = { sm: 12, md: 14 } as const;
+
+export default function Badge({
+ variant,
+ label,
+ size = 'sm',
+ className = '',
+}: BadgeProps) {
+ const { bg, text, border, Icon, defaultLabel } = variantStyles[variant];
+
+ return (
+
+
+ {label ?? defaultLabel}
+
+ );
+}
diff --git a/src/entry-server.tsx b/src/entry-server.tsx
new file mode 100644
index 0000000..68c8bba
--- /dev/null
+++ b/src/entry-server.tsx
@@ -0,0 +1,23 @@
+// 빌드 시 랜딩(/)만 정적 HTML로 프리렌더하는 서버 엔트리.
+// AI 크롤러(GPTBot/ClaudeBot/PerplexityBot)는 JS를 실행하지 않으므로
+// 본문이 HTML에 직접 들어 있어야 인용 가능하다. scripts/prerender.ts에서 호출.
+import { StrictMode } from 'react';
+import { renderToString } from 'react-dom/server';
+import { StaticRouter } from 'react-router';
+import { Routes, Route } from 'react-router';
+import App from './App.tsx';
+import LandingPage from './pages/LandingPage.tsx';
+
+export function render(url: string): string {
+ return renderToString(
+
+
+
+ }>
+ } />
+
+
+
+ ,
+ );
+}
diff --git a/src/main.tsx b/src/main.tsx
index 3e04bb9..f608d69 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,5 +1,5 @@
import { StrictMode } from 'react';
-import { createRoot } from 'react-dom/client';
+import { createRoot, hydrateRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router';
import App from './App.tsx';
import LandingPage from './pages/LandingPage.tsx';
@@ -17,7 +17,8 @@ import ApiDashboardPage from './pages/ApiDashboardPage.tsx';
import LoginPage from './pages/LoginPage.tsx';
import './index.css';
-createRoot(document.getElementById('root')!).render(
+const container = document.getElementById('root')!;
+const app = (
@@ -39,5 +40,12 @@ createRoot(document.getElementById('root')!).render(
- ,
+
);
+
+// 랜딩(/)은 빌드 시 프리렌더된 HTML이 들어 있으므로 hydrate, 나머지는 클라이언트 렌더
+if (container.hasChildNodes()) {
+ hydrateRoot(container, app);
+} else {
+ createRoot(container).render(app);
+}