diff --git a/src/components/ClinicNav.tsx b/src/components/ClinicNav.tsx
index 4965e34..bbfef69 100644
--- a/src/components/ClinicNav.tsx
+++ b/src/components/ClinicNav.tsx
@@ -23,6 +23,8 @@ const STEPS: Step[] = [
/** 작업 공간 경로면 병원 id 를 돌려준다. 아니면 null. */
export function clinicIdFromPath(pathname: string): string | null {
+ // /discovery/sample 은 병원 작업 공간이 아니라 가상 데이터 예시 리포트라 일반 헤더를 쓴다.
+ if (/^\/discovery\/sample\/?$/.test(pathname)) return null;
for (const s of STEPS) {
if (s.match.test(pathname)) return pathname.split('/')[2] ?? null;
}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index e5955ed..60bd0ea 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -24,7 +24,8 @@ import { Link, useLocation } from 'react-router';
import { ArrowRight } from 'lucide-react';
import ContactModal from './ContactModal';
import ClinicNav, { clinicIdFromPath } from './ClinicNav';
-import { DISCOVERY_SITE } from '../lib/site';
+import { DISCOVERY_SITE, SHOW_PRICING, SHOW_USE_CASES } from '../lib/site';
+import { USE_CASES } from '../data/useCases';
export default function Navbar() {
const { pathname } = useLocation();
@@ -46,14 +47,14 @@ export default function Navbar() {
{/* 가운데 메뉴 */}
- {/* Product·Use Cases 는 INFINITH 랜딩의 섹션 앵커라 그 섹션이 없는 공개 배포(webforai.kr)에서는 뺀다 */}
+ {/* Product 는 INFINITH 랜딩의 섹션 앵커라 공개 배포(webforai.kr)에서는 뺀다.
+ Pricing·Use Cases 는 src/lib/site.ts 스위치를 따른다. */}
{!DISCOVERY_SITE && (
Product
)}
- {/* 요금은 아직 확정 전이라 공개 배포(webforai.kr)에서는 숨긴다 (2026-10-07) */}
- {!DISCOVERY_SITE && (
+ {SHOW_PRICING && (
)}
- {!DISCOVERY_SITE && (
+ {/* 공개 배포에서는 사례가 있어야 섹션이 생기므로 메뉴도 그때만 보인다 */}
+ {SHOW_USE_CASES && (!DISCOVERY_SITE || USE_CASES.length > 0) && (
Use Cases
diff --git a/src/components/discovery/DiscoveryHero.tsx b/src/components/discovery/DiscoveryHero.tsx
index 4c13312..7bcb775 100644
--- a/src/components/discovery/DiscoveryHero.tsx
+++ b/src/components/discovery/DiscoveryHero.tsx
@@ -4,6 +4,8 @@
* 시안: docs/reports/landing/ai_discovery_landing.html (픽셀 기준)
*/
import { motion } from 'motion/react';
+import { Link } from 'react-router';
+import { SHOW_SAMPLE_REPORT } from '../../lib/site';
import { ArrowRight } from 'lucide-react';
import AbmrLoop from './AbmrLoop';
import type { AxisSummary } from './AbmrLoop';
@@ -86,6 +88,15 @@ export default function DiscoveryHero({
AEO, GEO 진단 요청하기
+ {/* 가상 데이터 예시 리포트로만 연결한다. 실제 병원 리포트로 연결하지 않는다 (src/lib/site.ts) */}
+ {SHOW_SAMPLE_REPORT && (
+
+ 샘플 리포트 보기
+
+ )}
{/* ABMR loop + AI answer mock */}
diff --git a/src/components/discovery/UseCasesSection.tsx b/src/components/discovery/UseCasesSection.tsx
new file mode 100644
index 0000000..6fe6fd2
--- /dev/null
+++ b/src/components/discovery/UseCasesSection.tsx
@@ -0,0 +1,42 @@
+/**
+ * Use Cases 섹션 (공개 랜딩). 사례는 src/data/useCases.ts 에서 읽는다.
+ * 헤더의 Use Cases 메뉴(/#use-cases)가 이 섹션으로 온다. 사례가 없으면 랜딩에서 그리지 않는다.
+ */
+import { motion } from 'motion/react';
+import type { UseCase } from '../../data/useCases';
+
+export default function UseCasesSection({ cases }: { cases: UseCase[] }) {
+ return (
+
+
+
USE CASES
+
+ 진단 이후 무엇이 달라졌는지
+
+
+ {cases.map((c, index) => (
+
+
{c.specialty}
+
{c.clinicLabel}
+
{c.challenge}
+
+ {c.actions.map((a) => (
+
{a}
+ ))}
+
+
{c.outcome}
+
{c.measuredAt}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/data/useCases.ts b/src/data/useCases.ts
new file mode 100644
index 0000000..c30c2ff
--- /dev/null
+++ b/src/data/useCases.ts
@@ -0,0 +1,27 @@
+/**
+ * 공개 랜딩(webforai.kr)의 Use Cases 사례.
+ *
+ * 비어 있으면 섹션과 헤더 메뉴가 나오지 않는다. 공개 배포에서 보이려면 사례를 넣고 src/lib/site.ts 의 SHOW_USE_CASES 를 켠다.
+ * 원칙 (docs/WEBFORAI_FEATURE_FLAGS.md 3단계):
+ * - 병원은 비식별 표기만 쓴다 (예: "강남 A성형외과"). 실제 병원명·도메인·의료진명·고유 수치는 넣지 않는다.
+ * 빌드 끝의 공개 번들 검사(plugins/public-bundle-denylist.json)가 실제 병원명을 찾으면 빌드가 실패한다.
+ * - 수치는 실측값만, 측정일과 함께 쓴다. 효과 보장·최상급 표현을 쓰지 않는다.
+ * - 병원 동의를 받은 사례만 싣는다.
+ */
+export interface UseCase {
+ id: string;
+ /** 비식별 표기. 예: "강남 A성형외과" */
+ clinicLabel: string;
+ /** 진료 분야. 예: "성형외과" */
+ specialty: string;
+ /** 진단 전 문제 한 문장 */
+ challenge: string;
+ /** 한 일 (2~4개) */
+ actions: string[];
+ /** 결과 한 문장. 실측 수치와 측정 시점을 함께 쓴다 */
+ outcome: string;
+ /** 측정일 또는 기간. 예: "2026-11 재측정" */
+ measuredAt: string;
+}
+
+export const USE_CASES: UseCase[] = [];
diff --git a/src/lib/site.ts b/src/lib/site.ts
index 20b71e1..cbbc836 100644
--- a/src/lib/site.ts
+++ b/src/lib/site.ts
@@ -1,6 +1,22 @@
/**
- * 배포 구분. Vercel 프로젝트 환경변수 VITE_SITE 로 정한다.
- * 'discovery' 로 빌드한 배포(webforai.kr)는 공개 사이트라 랜딩·로그인·약관만 연다. 요금은 확정 전이라 열지 않는다.
- * 실제 병원의 진단 결과와 내부 작업 화면은 이 배포에서 열지 않는다 (2026-10-07 haewon 결정).
+ * 배포 구분과 공개 기능 스위치. Vercel 프로젝트 환경변수 VITE_SITE 로 배포를 정한다.
+ * 'discovery' 로 빌드한 배포(webforai.kr)는 공개 사이트라 랜딩·로그인·약관만 연다 (2026-10-07 haewon 결정).
+ *
+ * 공개 배포에서 기능을 다시 열 때는 아래 SHOW_* 값만 바꾸고 배포한다. 라우트(main.tsx), 헤더(Navbar),
+ * 랜딩 버튼이 모두 이 값을 본다. 값은 빌드 때 정해지는 상수라 꺼진 기능은 공개 번들에서 통째로 빠진다.
+ * 켜기 전 조건과 순서는 docs/WEBFORAI_FEATURE_FLAGS.md 에 있다. 빌드 끝에 공개 번들 검사(vite.config.ts)가
+ * 실제 병원명·비밀 키를 찾으면 빌드를 실패시킨다.
*/
export const DISCOVERY_SITE = import.meta.env.VITE_SITE === 'discovery';
+
+/** 요금(/pricing) 페이지와 헤더 메뉴. 요금 확정 후 공개 배포에서 켠다. */
+export const SHOW_PRICING = !DISCOVERY_SITE;
+
+/** 비식별 사례(Use Cases) 섹션과 헤더 메뉴. 공개 배포에서는 src/data/useCases.ts 에 사례가 있어야 보인다. */
+export const SHOW_USE_CASES = !DISCOVERY_SITE;
+
+/** 가상 데이터 예시 리포트(/discovery/sample)와 랜딩의 "샘플 리포트 보기" 버튼. */
+export const SHOW_SAMPLE_REPORT = !DISCOVERY_SITE;
+
+// 실제 병원 진단 결과(/discovery/:id 등 작업 화면)는 스위치로 열지 않는다.
+// 로그인과 데이터베이스 권한 정리가 끝난 뒤 별도 작업으로 연다 (docs/WEBFORAI_FEATURE_FLAGS.md 4단계).
diff --git a/src/main.tsx b/src/main.tsx
index b4564a7..d190a97 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -23,7 +23,8 @@ import ImageReviewPage from './pages/ImageReviewPage.tsx';
import SiteBuildPage from './pages/SiteBuildPage.tsx';
import PrivacyPage from './pages/PrivacyPage.tsx';
import TermsPage from './pages/TermsPage.tsx';
-import { DISCOVERY_SITE } from './lib/site';
+import SampleReportPage from './pages/SampleReportPage.tsx';
+import { DISCOVERY_SITE, SHOW_PRICING, SHOW_SAMPLE_REPORT } from './lib/site';
import './index.css';
@@ -33,12 +34,14 @@ createRoot(document.getElementById('root')!).render(
}>
{/* 배포마다 여는 화면이 다르다. VITE_SITE=discovery 로 빌드한 공개 배포(webforai.kr)는 AI Discovery 랜딩을
- 루트에 두고 공개 페이지(랜딩·로그인·약관)만 연다. 실제 병원 진단 결과·내부 작업 화면 경로는 홈으로 돌린다.
+ 루트에 두고 공개 페이지(랜딩·로그인·약관)만 연다. 요금·예시 리포트는 src/lib/site.ts 스위치가 켜졌을 때만 연다.
+ 실제 병원 진단 결과·내부 작업 화면 경로는 홈으로 돌린다.
값이 없으면 지금까지처럼 INFINITH 랜딩과 전체 화면을 연다. */}
{DISCOVERY_SITE ? (
<>
} />
- {/* 요금(/pricing)은 확정 전이라 공개 배포에서 열지 않는다. 아래 '*' 규칙으로 홈에 돌아간다 */}
+ {SHOW_PRICING && } />}
+ {SHOW_SAMPLE_REPORT && } />}
} />
} />
} />
@@ -61,6 +64,7 @@ createRoot(document.getElementById('root')!).render(
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/pages/DiscoveryLandingPage.tsx b/src/pages/DiscoveryLandingPage.tsx
index 7098473..fcfb13b 100644
--- a/src/pages/DiscoveryLandingPage.tsx
+++ b/src/pages/DiscoveryLandingPage.tsx
@@ -21,6 +21,9 @@ import Roadmap from '../components/discovery/Roadmap';
import HonestyStrip from '../components/discovery/HonestyStrip';
import DiscoveryCta from '../components/discovery/DiscoveryCta';
import ContactModal from '../components/ContactModal';
+import UseCasesSection from '../components/discovery/UseCasesSection';
+import { USE_CASES } from '../data/useCases';
+import { SHOW_USE_CASES } from '../lib/site';
export default function DiscoveryLandingPage() {
// 진단 요청은 문의하기와 같은 폼(구글시트 기록 + 메일 알림)으로 받는다. source 로 어느 버튼에서 왔는지 남긴다.
@@ -55,6 +58,7 @@ export default function DiscoveryLandingPage() {
+ {SHOW_USE_CASES && USE_CASES.length > 0 && }
export default function DiscoveryReportPage() {
const { id } = useParams<{ id: string }>();
const result = id ? DISCOVERY_RESULTS[id] : undefined;
- const overall = useMemo(
- () => (result ? scoreDiscovery(AEO_GEO_RUBRIC, result) : null),
- [result],
- );
- const overallV2 = useMemo(
- () => (result ? scoreDiscoveryV2(AEO_GEO_RUBRIC_V2, result) : null),
- [result],
- );
// 진단은 AEO/GEO 36항목 실측 결과가 있어야 그린다. 사이트 빌드와는 별개 산출물이라
// 빌드만 끝난 병원은 여기에 들어올 수 있다. 막다른 길로 두지 않고 다음 단계로 잇는다.
- if (!result || !overall) {
+ if (!result) {
return ;
}
+ return ;
+}
+
+/**
+ * 리포트 본문. 실제 병원 리포트(/discovery/:id)와 공개 예시 리포트(/discovery/sample)가 함께 쓴다.
+ * sample 이면 가상 데이터라는 표시를 붙인다. 이 컴포넌트는 실제 병원 목록(DISCOVERY_RESULTS)을 읽지 않는다.
+ */
+export function DiscoveryReportView({ result, sample = false }: { result: DiscoveryResult; sample?: boolean }) {
+ const overall = useMemo(() => scoreDiscovery(AEO_GEO_RUBRIC, result), [result]);
+ const overallV2 = useMemo(() => scoreDiscoveryV2(AEO_GEO_RUBRIC_V2, result), [result]);
const resultById = new Map(result.results.map((r) => [r.criterionId, r]));
@@ -108,14 +111,21 @@ export default function DiscoveryReportPage() {
실측일 {result.auditedAt} · 실측 항목 {overall.categories.reduce((s, c) => s + c.verifiedCount, 0)} /
{' '}{AEO_GEO_RUBRIC.criteria.length} · AEO·GEO 각 100점 독립 지표 · 미검증 항목은 분모 제외
diff --git a/src/pages/SampleReportPage.tsx b/src/pages/SampleReportPage.tsx
new file mode 100644
index 0000000..c21e101
--- /dev/null
+++ b/src/pages/SampleReportPage.tsx
@@ -0,0 +1,11 @@
+/**
+ * 공개 예시 리포트 (/discovery/sample).
+ * 가상 데이터(discovery_sample)만 그린다. 실제 병원 리포트 목록(discoveryResults)은 import 하지 않는다.
+ * 공개 배포에서는 src/lib/site.ts 의 SHOW_SAMPLE_REPORT 가 true 일 때만 라우트와 랜딩 버튼이 열린다.
+ */
+import { DiscoveryReportView } from './DiscoveryReportPage';
+import { DISCOVERY_SAMPLE } from '../data/discovery_sample';
+
+export default function SampleReportPage() {
+ return ;
+}