From 13f8c362c2555e3285f2369d0a92997c2e2fa949 Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Fri, 28 Aug 2026 15:43:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=9E=9C=EB=94=A9=20AI=20=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EA=B0=80=EB=8F=85=EC=84=B1=20=E2=80=94=20?= =?UTF-8?q?SSR=20=ED=94=84=EB=A6=AC=EB=A0=8C=EB=8D=94=20+=20=EB=A9=94?= =?UTF-8?q?=ED=83=80/JSON-LD=20+=20robots/sitemap/llms.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/entry-server.tsx + scripts/prerender.ts: 빌드 시 / 를 renderToString으로 정적 HTML 주입 (크롤러 본문 4단어 → 800+단어) - main.tsx: 프리렌더된 랜딩은 hydrateRoot, 나머지 라우트는 createRoot - index.html: lang=ko, description/OG/canonical, Organization·SoftwareApplication·WebSite JSON-LD - public/: robots.txt(AI 크롤러 명시 허용), sitemap.xml, llms.txt (SPA 리라이트로 index.html 반환되던 문제 해소) - Badge.tsx: 메인 체크아웃에만 있고 미커밋이던 컴포넌트 추가 (빌드 실패 원인) Co-Authored-By: Claude Fable 5 --- index.html | 59 +++++++++++++++++++++++- package.json | 2 +- public/llms.txt | 25 +++++++++++ public/robots.txt | 28 ++++++++++++ public/sitemap.xml | 15 +++++++ scripts/prerender.ts | 19 ++++++++ src/components/Badge.tsx | 96 ++++++++++++++++++++++++++++++++++++++++ src/entry-server.tsx | 23 ++++++++++ src/main.tsx | 14 ++++-- 9 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 public/llms.txt create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml create mode 100644 scripts/prerender.ts create mode 100644 src/components/Badge.tsx create mode 100644 src/entry-server.tsx 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); +}