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.clinicName}

{result.industry}

- - {result.url} - + {result.url && ( + + {result.url} + + )} + {sample && ( +

+ 가상 데이터로 만든 예시 리포트입니다. 실제 병원의 진단 결과가 아닙니다. +

+ )}

실측일 {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 ; +}