feat: webforai.kr 공개 전 정리 (비식별 샘플·실제 리포트 비공개·약관)

- 랜딩 샘플 리포트·AI 답변 예시·문의 서식 예시를 가상 A성형외과로 바꾼다 (discovery_sample, 고정 시드 무작위 판정)
- VITE_SITE=discovery 배포는 랜딩·요금·로그인·약관만 연다. 실제 병원 진단 결과와 내부 화면 경로는 홈으로 돌린다
- src/data·src/services 를 부수효과 없는 모듈로 표시해 쓰지 않는 실제 병원 목업이 공개 번들에서 빠지게 한다
- 공개 배포 번들에 GEMINI_API_KEY 를 넣지 않는다
- 헤더 Product·Use Cases 는 공개 배포에서 숨기고, 푸터에 개인정보처리방침·이용약관 페이지를 연결한다
- 기준표 설명의 실제 병원 예시 문구를 일반 예시로 바꾼다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-10-07 11:23:41 +09:00
parent 63e71b73cc
commit f03c6a5b90
14 changed files with 442 additions and 50 deletions

View File

@ -200,7 +200,7 @@ export default function ContactModal({ open, onClose, source }: ContactModalProp
type="text"
value={company}
onChange={(e) => setCompany(e.target.value)}
placeholder="예: 뷰성형외과"
placeholder="예: OO성형외과"
className={`${INPUT_CLASS} mb-4`}
/>

View File

@ -35,8 +35,8 @@ export default function Footer() {
Infinite Marketing for Premium Medical Business &amp; Marketing Agency
</div>
<div className="flex items-center gap-6 text-sm font-medium text-slate-600 shrink-0">
<a href="#" className="hover:text-primary-900 transition-colors">Privacy Policy</a>
<a href="#" className="hover:text-primary-900 transition-colors">Terms of Service</a>
<Link to="/privacy" className="font-bold text-primary-900 hover:opacity-80 transition-opacity">개인정보처리방침</Link>
<Link to="/terms" className="hover:text-primary-900 transition-colors">이용약관</Link>
<button
type="button"
onClick={() => setContactOpen(true)}

View File

@ -0,0 +1,61 @@
/**
* LegalLayout: 개인정보처리방침·이용약관 공통 틀.
* 조항은 { title, body } 배열로 받고, body 의 각 줄은 문단 또는 목록 항목으로 그린다.
* '- ' 로 시작하는 줄은 목록 항목이다.
*/
import type { ReactNode } from 'react';
export interface LegalSection {
title: string;
body: (string | ReactNode)[];
}
interface LegalLayoutProps {
title: string;
intro: string;
effectiveDate: string;
sections: LegalSection[];
}
function renderBody(body: LegalSection['body']) {
const out: ReactNode[] = [];
let list: ReactNode[] = [];
const flush = () => {
if (list.length) {
out.push(<ul key={`ul-${out.length}`} className="list-disc pl-5 space-y-1.5">{list}</ul>);
list = [];
}
};
body.forEach((line, i) => {
if (typeof line === 'string' && line.startsWith('- ')) {
list.push(<li key={i}>{line.slice(2)}</li>);
return;
}
flush();
out.push(<p key={i}>{line}</p>);
});
flush();
return out;
}
export default function LegalLayout({ title, intro, effectiveDate, sections }: LegalLayoutProps) {
return (
<section className="min-h-screen pt-32 pb-24 px-6 bg-white [word-break:keep-all]">
<div className="max-w-3xl mx-auto">
<h1 className="font-serif text-3xl md:text-4xl font-bold text-primary-900 mb-4">{title}</h1>
<p className="text-[15px] leading-[1.8] text-slate-600 mb-3">{intro}</p>
<p className="text-sm text-slate-400 mb-12">시행일 {effectiveDate}</p>
<div className="flex flex-col gap-10">
{sections.map((s, i) => (
<div key={s.title}>
<h2 className="text-lg font-bold text-primary-900 mb-3">
제{i + 1}조 {s.title}
</h2>
<div className="flex flex-col gap-2.5 text-[15px] leading-[1.8] text-slate-600">{renderBody(s.body)}</div>
</div>
))}
</div>
</div>
</section>
);
}

View File

@ -24,6 +24,7 @@ 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';
export default function Navbar() {
const { pathname } = useLocation();
@ -45,18 +46,23 @@ export default function Navbar() {
{/* 가운데 메뉴 */}
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
{/* Product·Use Cases 는 INFINITH 랜딩의 섹션 앵커라 그 섹션이 없는 공개 배포(webforai.kr)에서는 뺀다 */}
{!DISCOVERY_SITE && (
<a href="/#solution" className="hover:text-primary-900 transition-colors">
Product
</a>
)}
<Link
to="/pricing?from=header"
className="hover:text-primary-900 transition-colors"
>
Pricing
</Link>
{!DISCOVERY_SITE && (
<a href="/#use-cases" className="hover:text-primary-900 transition-colors">
Use Cases
</a>
)}
</div>
{/* 우측 CTA — Login(Secondary) + 문의하기(Primary) */}

View File

@ -5,7 +5,8 @@
*/
import { motion } from 'motion/react';
const CITED_DOMAINS = ['viewclinic.com', 'gangnamunni.com', 'youtube.com'];
// 예시 카드라 실제 병원 도메인을 쓰지 않는다. 병원 사이트 자리는 일반 명칭으로 둔다.
const CITED_DOMAINS = ['병원 공식 홈페이지', 'gangnamunni.com', 'youtube.com'];
interface AiAnswerMockProps {
clinicName: string;
@ -45,12 +46,11 @@ export default function AiAnswerMock({ clinicName, criteriaCount }: AiAnswerMock
{/* 목표 상태의 답변 예시 */}
<p className="text-[15px] leading-[1.8] text-slate-600 mb-4 break-keep">
모티바 공식 KOL 의료진이 있는{' '}
의료진 14명의 약력을 공개한{' '}
<span className="bg-[#F3F0FF] text-[#4A3A7C] font-semibold px-1.5 py-0.5 rounded-md">
{clinicName}
</span>
는 강남언니 후기 19,316건과 의료진 26명 정보가 공개되어 있으며, 시술별 FAQ와 비교표를
제공합니다.
는 후기 7,800여 건이 공개되어 있고, 시술별 FAQ와 회복 일정표를 제공합니다.
</p>
{/* 인용 도메인 칩 + 목표 상태 라벨 (제거 금지) */}

View File

@ -1,5 +1,5 @@
/**
* 샘플 리포트 섹션 — 뷰성형외과 실측 스냅샷.
* 샘플 리포트 섹션. 가상 데이터(discovery_sample)로 만든 예시이며 실제 병원 진단 결과가 아니다.
* 점수·등급·항목 %·항목 수는 전부 discoveryScore(v1 실측)·discoveryScoreV2(AEO/GEO) 계산값에서 렌더 (하드코딩 금지).
* 발견 칩 3장의 카피는 result.keyFindings를 랜딩용으로 압축한 시안 확정 문안이다.
*/
@ -16,28 +16,28 @@ const GRADE_BADGE_STYLE: Record<OverallScore['grade'], string> = {
D: 'bg-[#FFF0F0] border-[#F5D5DC] text-[#7C3A4B]',
};
/** keyFindings 압축본 (시안 확정 카피). 수치는 2026-09-07 재검증(v1.1) 스냅샷과 일치한다. */
/** 발견 칩 예시 카피. 특정 병원을 알아볼 수 없도록 고유 명칭을 빼고 수치는 가상 값으로 둔다. */
const FINDING_CHIPS = [
{
label: '치명',
chipClass: 'bg-[#FFF0F0] border-[#F5D5DC] text-[#7C3A4B]',
dotColor: '#D4889A',
title: 'AI가 누구인지 확정 못 함',
detail: 'MedicalClinic 스키마 없음. 시술 목록 JSON-LD는 문법 오류로 통째로 버려집니다.',
detail: 'MedicalClinic 스키마가 없고, 시술 정보 JSON-LD는 문법 오류로 읽히지 않습니다.',
},
{
label: '보완',
chipClass: 'bg-[#FFF6ED] border-[#F5E0C5] text-[#7C5C3A]',
dotColor: '#D4A872',
title: '인용할 문장이 없음',
detail: '정의문 0개, HTML 표 0개, FAQ는 프리저베 한 페이지뿐. 답변엔진이 잘라 갈 단락이 없습니다.',
detail: '정의문 2개, HTML 표 0개, FAQ는 시술 페이지 한 곳뿐. 답변엔진이 잘라 갈 단락이 적습니다.',
},
{
label: '강점',
chipClass: 'bg-[#F3F0FF] border-[#D5CDF5] text-[#4A3A7C]',
dotColor: '#9B8AD4',
title: '표면 자산은 이미 강함',
detail: '강남언니 후기 19,373건, GBP 4.9점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.',
detail: '후기 플랫폼 후기 7,800여 건, 지도 평점 4.7점. 홈페이지가 따라가면 바로 인용 근거가 됩니다.',
},
];
@ -62,13 +62,13 @@ export default function SampleReport({ overall, axes, result }: SampleReportProp
transition={{ duration: 0.6 }}
>
<div className="text-sm font-bold tracking-[0.24em] text-accent mb-4">
SAMPLE REPORT · 실제 사례
SAMPLE REPORT · 예시
</div>
<h2 className="font-serif text-3xl md:text-[40px] font-bold leading-tight text-primary-900 mb-3 break-keep">
읽을 수는 있지만, 인용할 근거가 없습니다.
</h2>
<p className="text-[17px] leading-[1.7] text-slate-500 mb-12 break-keep">
실제 1차 진단 결과입니다. 답변 준비도(AEO)와 출처 준비도(GEO)를 각각 100점으로 보고,
가상의 A성형외과로 만든 예시 리포트입니다. 답변 준비도(AEO)와 출처 준비도(GEO)를 각각 100점으로 보고,
어디가 강하고 어디가 비어 있는지 항목 단위로 드러냅니다.
</p>
</motion.div>

View File

@ -279,7 +279,7 @@ export const AEO_GEO_RUBRIC: Rubric = {
id: 'C3',
category: 'content',
name: '제목 계층 의미성',
rationale: 'H1은 페이지당 1개, 주제를 서술해야 한다. "VIEW SELFIE" 같은 라벨형 H1은 AI에게 아무 정보도 주지 않는다.',
rationale: 'H1은 페이지당 1개, 주제를 서술해야 한다. "MAIN VISUAL" 같은 라벨형 H1은 AI에게 아무 정보도 주지 않는다.',
weight: 3,
evidenceGrade: 'measured',
control: 'direct',

View File

@ -0,0 +1,61 @@
/**
* 랜딩용 예시 진단 결과 (가상 데이터).
* 실제 병원의 진단 결과가 아니다. 병원명·주소·수치는 모두 비식별 가상 값이고, 항목 판정은 고정 시드(20261007)로 무작위 생성했다.
* 공개 사이트(webforai.kr)에는 실제 병원 리포트를 싣지 않는다 (2026-10-07 haewon 결정).
*/
import type { DiscoveryResult } from '../types/discovery';
export const DISCOVERY_SAMPLE: DiscoveryResult = {
id: 'sample',
clinicName: 'A성형외과',
url: '',
industry: '성형외과 (예시)',
auditedAt: '2026-10-01',
rubricVersion: '2.0',
conditions: ['가상 데이터로 만든 예시 리포트입니다. 실제 병원의 진단 결과가 아닙니다.'],
results: [
{ criterionId: 'A1', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'A2', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'A3', level: 1, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'A4', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'A5', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'A6', level: 'unverified', evidence: '예시 데이터: 병원 계정 접근이 필요한 항목이라 미검증으로 둡니다' },
{ criterionId: 'A7', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'B1', level: 0, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'B2', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'B3', level: 0, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'B4', level: 1, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'B5', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'B6', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'B7', level: 1, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C1', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C2', level: 1, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C3', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C4', level: 0, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C5', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C6', level: 0, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C7', level: 0, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'C8', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'D1', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'D2', level: 'unverified', evidence: '예시 데이터: 병원 계정 접근이 필요한 항목이라 미검증으로 둡니다' },
{ criterionId: 'D3', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'D4', level: 3, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'D5', level: 0, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'D6', level: 1, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'E1', level: 'unverified', evidence: '예시 데이터: 병원 계정 접근이 필요한 항목이라 미검증으로 둡니다' },
{ criterionId: 'E2', level: 'unverified', evidence: '예시 데이터: 병원 계정 접근이 필요한 항목이라 미검증으로 둡니다' },
{ criterionId: 'E3', level: 'unverified', evidence: '예시 데이터: 병원 계정 접근이 필요한 항목이라 미검증으로 둡니다' },
{ criterionId: 'E4', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'F1', level: 2, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'F2', level: 1, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'F3', level: 0, evidence: '예시 데이터 (가상 값)' },
{ criterionId: 'F4', level: 'unverified', evidence: '예시 데이터: 병원 계정 접근이 필요한 항목이라 미검증으로 둡니다' },
],
keyFindings: [
{ title: 'AI가 병원을 하나의 기관으로 확정하지 못함', detail: '예시 데이터: 병원 정보를 구조화한 스키마가 없습니다.', severity: 'critical' },
{ title: '인용할 문장이 부족함', detail: '예시 데이터: 정의문·비교표·FAQ가 적습니다.', severity: 'warning' },
{ title: '외부 평판 자산은 있음', detail: '예시 데이터: 후기 플랫폼과 지도 서비스의 공개 정보가 충분합니다.', severity: 'good' },
],
actions: [],
notDoing: [],
};

6
src/lib/site.ts Normal file
View File

@ -0,0 +1,6 @@
/**
* 배포 구분. Vercel 프로젝트 환경변수 VITE_SITE 로 정한다.
* 'discovery' 로 빌드한 배포(webforai.kr)는 공개 사이트라 랜딩·요금·로그인·약관만 연다.
* 실제 병원의 진단 결과와 내부 작업 화면은 이 배포에서 열지 않는다 (2026-10-07 haewon 결정).
*/
export const DISCOVERY_SITE = import.meta.env.VITE_SITE === 'discovery';

View File

@ -1,6 +1,6 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router';
import App from './App.tsx';
import LandingPage from './pages/LandingPage.tsx';
import PricingPage from './pages/PricingPage.tsx';
@ -21,20 +21,32 @@ import ActionPlanPage from './pages/ActionPlanPage.tsx';
import SupportersBuildPage from './pages/SupportersBuildPage.tsx';
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 './index.css';
// 배포별 루트 페이지 분기. Vercel 프로젝트의 환경변수로 정한다.
const DISCOVERY_SITE = import.meta.env.VITE_SITE === 'discovery';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route element={<App />}>
{/* 루트에서 무엇을 그릴지는 배포마다 다르다. VITE_SITE=discovery 로 빌드한 배포(webforai.kr)는
AI Discovery 랜딩을 루트에 놓고, 값이 없으면 지금까지처럼 INFINITH 랜딩을 놓는다.
한 코드베이스로 두 사이트를 내되 기존 배포의 동작은 바꾸지 않는다. */}
<Route index element={DISCOVERY_SITE ? <DiscoveryLandingPage /> : <LandingPage />} />
{/* 배포마다 여는 화면이 다르다. VITE_SITE=discovery 로 빌드한 공개 배포(webforai.kr)는 AI Discovery 랜딩을
루트에 두고 공개 페이지만 연다. 실제 병원 진단 결과·내부 작업 화면 경로는 홈으로 돌린다.
값이 없으면 지금까지처럼 INFINITH 랜딩과 전체 화면을 연다. */}
{DISCOVERY_SITE ? (
<>
<Route index element={<DiscoveryLandingPage />} />
<Route path="pricing" element={<PricingPage />} />
<Route path="login" element={<LoginPage />} />
<Route path="privacy" element={<PrivacyPage />} />
<Route path="terms" element={<TermsPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</>
) : (
<>
<Route index element={<LandingPage />} />
<Route path="pricing" element={<PricingPage />} />
<Route path="login" element={<LoginPage />} />
<Route path="report/loading" element={<AnalysisLoadingPage />} />
@ -54,6 +66,10 @@ createRoot(document.getElementById('root')!).render(
<Route path="supporters/:id" element={<SupportersBuildPage />} />
<Route path="images/:id" element={<ImageReviewPage />} />
<Route path="build/:id" element={<SiteBuildPage />} />
<Route path="privacy" element={<PrivacyPage />} />
<Route path="terms" element={<TermsPage />} />
</>
)}
</Route>
</Routes>
</BrowserRouter>

View File

@ -8,7 +8,7 @@
*/
import { useMemo, useState } from 'react';
import { AEO_GEO_RUBRIC } from '../data/aeoGeoRubric';
import { DISCOVERY_VIEWCLINIC } from '../data/discovery_viewclinic';
import { DISCOVERY_SAMPLE } from '../data/discovery_sample';
import { scoreDiscovery, nextGradeTarget } from '../lib/discoveryScore';
import { AEO_GEO_RUBRIC_V2 } from '../data/aeoGeoRubricV2';
import { scoreDiscoveryV2 } from '../lib/discoveryScoreV2';
@ -25,7 +25,8 @@ import ContactModal from '../components/ContactModal';
export default function DiscoveryLandingPage() {
// 진단 요청은 문의하기와 같은 폼(구글시트 기록 + 메일 알림)으로 받는다. source 로 어느 버튼에서 왔는지 남긴다.
const [leadSource, setLeadSource] = useState<string | null>(null);
const result = DISCOVERY_VIEWCLINIC;
// 공개 랜딩은 가상 예시 데이터만 쓴다. 실제 병원 진단 결과를 싣지 않는다.
const result = DISCOVERY_SAMPLE;
const overall = useMemo(() => scoreDiscovery(AEO_GEO_RUBRIC, result), [result]);
const overallV2 = useMemo(() => scoreDiscoveryV2(AEO_GEO_RUBRIC_V2, result), [result]);
const axes: AxisSummary[] = [overallV2.aeo, overallV2.geo].map((ax) => ({

122
src/pages/PrivacyPage.tsx Normal file
View File

@ -0,0 +1,122 @@
/**
* 개인정보처리방침 (/privacy).
* 이 사이트가 실제로 처리하는 것만 적는다: 문의·진단 요청 서식(이름·연락처·회사·문의 내용) → /api/lead(Vercel) →
* Google Apps Script → 구글 시트 저장 + 알림 메일. 방문 분석 쿠키는 아직 쓰지 않는다. GA4 등을 붙이면 제8조를 고친다.
* 구조는 의료 분야 SaaS 방침(메디팔·애프터닥)의 조항 구성을 참고했고, 문장은 이 서비스에 맞춰 새로 썼다.
*/
import LegalLayout, { type LegalSection } from '../components/LegalLayout';
import { COMPANY_LEGAL_NAME, COMPANY_HQ, CONTACT_PHONE } from '../lib/contact';
const PRIVACY_EMAIL = 'o2oteam@o2o.kr';
const SECTIONS: LegalSection[] = [
{
title: '수집하는 개인정보 항목과 수집 방법',
body: [
'회사는 웹사이트의 문의하기 및 AEO, GEO 진단 요청 서식을 통해 아래 항목을 수집합니다.',
'- 필수: 성함, 연락처(전화번호 또는 이메일 중 하나 이상)',
'- 선택: 회사(병원)명, 문의 내용',
'서비스 이용 과정에서 접속 IP 주소, 브라우저 정보, 접속 일시가 웹 호스팅 서버의 로그로 자동 생성될 수 있습니다.',
'회사는 만 14세 미만 아동의 개인정보를 수집하지 않으며, 환자의 건강 정보 등 민감정보를 서식으로 받지 않습니다. 문의 내용에 환자 개인정보를 적지 말아 주십시오.',
],
},
{
title: '개인정보의 수집 및 이용 목적',
body: [
'- 문의 및 진단 요청에 대한 회신과 상담',
'- 요청하신 AEO, GEO 진단의 수행과 결과 전달',
'- 서비스 도입 관련 안내(요청하신 범위에 한함)',
'회사는 위 목적 외의 용도로 개인정보를 이용하지 않으며, 목적이 바뀌는 경우 미리 동의를 받습니다.',
],
},
{
title: '개인정보의 보유 및 이용 기간',
body: [
'회사는 문의 처리가 끝난 날부터 1년간 개인정보를 보관한 뒤 지체 없이 파기합니다. 정보주체가 삭제를 요청하면 그 전이라도 파기합니다.',
'서비스 계약을 체결한 경우에는 관계 법령에 따라 아래 기간 동안 보관합니다.',
'- 계약 또는 청약철회 등에 관한 기록: 5년 (전자상거래 등에서의 소비자보호에 관한 법률)',
'- 대금결제 및 재화 등의 공급에 관한 기록: 5년 (같은 법)',
'- 소비자의 불만 또는 분쟁처리에 관한 기록: 3년 (같은 법)',
'- 웹사이트 방문 기록: 3개월 (통신비밀보호법)',
],
},
{
title: '개인정보의 제3자 제공',
body: ['회사는 정보주체의 개인정보를 제3자에게 제공하지 않습니다. 법령에 특별한 규정이 있거나 정보주체가 동의한 경우에만 예외로 합니다.'],
},
{
title: '개인정보 처리업무의 위탁 및 국외 이전',
body: [
'회사는 서비스 운영을 위해 아래와 같이 개인정보 처리업무를 위탁하며, 수탁자의 서버가 국외에 있어 개인정보가 국외로 이전됩니다.',
'- Google LLC (미국): 문의 내용 저장(Google 스프레드시트) 및 접수 알림 메일 발송. 이전 항목은 제1조의 서식 입력 항목이며, 서식을 제출하는 시점에 네트워크로 전송됩니다. 보유 기간은 제3조와 같습니다.',
'- Vercel Inc. (미국): 웹사이트 호스팅 및 서식 전송 처리. 이전 항목은 서식 입력 항목과 접속 기록이며, 웹사이트 이용 및 서식 제출 시 네트워크로 전송됩니다. 보유 기간은 위탁 계약 종료 시까지입니다.',
'국외 이전을 원하지 않으시면 서식 대신 이메일 또는 전화로 문의하실 수 있습니다. 다만 이 경우에도 회신 메일은 회사의 메일 서비스를 통해 처리됩니다.',
],
},
{
title: '정보주체의 권리와 행사 방법',
body: [
'정보주체는 언제든지 자신의 개인정보에 대한 열람, 정정, 삭제, 처리정지를 요청할 수 있습니다.',
`요청은 이메일(${PRIVACY_EMAIL}) 또는 전화(${CONTACT_PHONE})로 하실 수 있으며, 회사는 본인 확인 후 10일 이내에 조치하고 결과를 알려 드립니다.`,
'법정대리인이나 위임을 받은 사람을 통해서도 권리를 행사할 수 있습니다.',
],
},
{
title: '개인정보의 파기 절차 및 방법',
body: [
'보유 기간이 끝나거나 처리 목적이 달성된 개인정보는 지체 없이 파기합니다.',
'- 전자 파일: 복구할 수 없는 방법으로 영구 삭제',
'- 종이 문서: 분쇄 또는 소각',
],
},
{
title: '쿠키 등 자동 수집 장치',
body: [
'회사는 현재 이 웹사이트에서 광고 또는 방문 분석을 위한 쿠키를 사용하지 않습니다.',
'방문 분석 도구를 도입하는 경우 이 방침을 개정해 수집 항목, 목적, 거부 방법을 미리 알려 드립니다.',
],
},
{
title: '개인정보의 안전성 확보 조치',
body: [
'- 개인정보에 접근할 수 있는 담당자를 최소한으로 지정하고 접근 권한을 관리합니다.',
'- 서식 전송 구간을 HTTPS로 암호화합니다.',
'- 서식 전송에 쓰는 인증 정보는 서버에만 보관하고 웹 페이지에 노출하지 않습니다.',
'- 자동 입력 방지 장치와 전송 횟수 제한으로 무단 접근을 막습니다.',
],
},
{
title: '개인정보 보호책임자',
body: [
'회사는 개인정보 처리에 관한 업무를 총괄하고 정보주체의 불만 처리와 피해 구제를 위해 아래와 같이 개인정보 보호 담당을 두고 있습니다.',
`- 담당: ${COMPANY_LEGAL_NAME} 개인정보 보호 담당`,
`- 이메일: ${PRIVACY_EMAIL}`,
`- 전화: ${CONTACT_PHONE}`,
],
},
{
title: '권익침해 구제 방법',
body: [
'개인정보 침해에 대한 신고나 상담이 필요하시면 아래 기관에 문의하실 수 있습니다.',
'- 개인정보분쟁조정위원회: 1833-6972 (www.kopico.go.kr)',
'- 개인정보침해신고센터: 118 (privacy.kisa.or.kr)',
'- 대검찰청 사이버수사과: 1301 (www.spo.go.kr)',
'- 경찰청 사이버수사국: 182 (ecrm.police.go.kr)',
],
},
{
title: '개인정보처리방침의 변경',
body: ['이 방침은 2026년 10월 7일부터 적용됩니다. 내용이 바뀌면 시행 7일 전부터 웹사이트에 알리며, 정보주체의 권리에 중요한 변경은 30일 전부터 알립니다.'],
},
];
export default function PrivacyPage() {
return (
<LegalLayout
title="개인정보처리방침"
intro={`${COMPANY_LEGAL_NAME}(이하 "회사")는 INFINITH AI Discovery 웹사이트(webforai.kr)를 운영하면서 개인정보 보호법에 따라 정보주체의 개인정보를 보호하고 관련 고충을 원활하게 처리하기 위해 다음과 같이 개인정보처리방침을 둡니다. 회사 소재지: ${COMPANY_HQ}.`}
effectiveDate="2026년 10월 7일"
sections={SECTIONS}
/>
);
}

109
src/pages/TermsPage.tsx Normal file
View File

@ -0,0 +1,109 @@
/**
* 이용약관 (/terms).
* 공개 웹사이트(진단 소개·문의 접수)의 이용 조건을 정한다. 유료 서비스의 범위·요금·기간은 별도 계약이 우선한다.
* 구조는 의료 분야 SaaS 약관(메디팔·애프터닥)의 조항 구성을 참고했고, 문장은 이 서비스에 맞춰 새로 썼다.
*/
import LegalLayout, { type LegalSection } from '../components/LegalLayout';
import { COMPANY_LEGAL_NAME } from '../lib/contact';
const SECTIONS: LegalSection[] = [
{
title: '목적',
body: [`이 약관은 ${COMPANY_LEGAL_NAME}(이하 "회사")가 운영하는 INFINITH AI Discovery 웹사이트(webforai.kr, 이하 "사이트")와 사이트를 통해 제공하는 서비스의 이용 조건, 회사와 이용자의 권리·의무를 정하는 것을 목적으로 합니다.`],
},
{
title: '정의',
body: [
'- "서비스"란 회사가 사이트에서 제공하는 AEO(답변엔진 최적화)·GEO(생성형 검색 최적화) 진단 소개, 진단 요청 및 문의 접수, 그리고 별도 계약에 따라 제공하는 진단·구축·측정 서비스를 말합니다.',
'- "이용자"란 사이트에 접속해 서비스를 이용하는 개인 또는 의료기관 등 사업자를 말합니다.',
'- "진단 결과"란 회사가 기준표에 따라 이용자의 웹사이트와 공개 채널을 측정해 제공하는 점수, 항목별 판정, 개선 권고를 말합니다.',
],
},
{
title: '약관의 효력과 변경',
body: [
'이 약관은 사이트에 게시함으로써 효력이 생깁니다.',
'회사는 관계 법령을 위반하지 않는 범위에서 약관을 바꿀 수 있으며, 바꾸는 경우 시행일 7일 전부터 사이트에 알립니다. 이용자에게 불리한 변경은 30일 전부터 알립니다.',
'이 약관에 정하지 않은 사항은 관계 법령과 일반적인 상관례를 따릅니다.',
],
},
{
title: '서비스의 내용',
body: [
'- 사이트: 서비스 소개, 예시 리포트, 요금 안내, 문의 및 진단 요청 접수',
'- 별도 계약 서비스: 진단 리포트 작성, 공식 답변 허브 구축, 정기 측정 및 보고',
'별도 계약 서비스의 범위, 요금, 기간, 산출물은 회사와 이용자가 맺는 개별 계약에서 정하며, 개별 계약이 이 약관과 다르면 개별 계약이 우선합니다.',
],
},
{
title: '진단 결과의 성격',
body: [
'진단 결과는 측정한 날짜와 측정 조건을 기준으로 한 것이며, 측정 이후 웹사이트나 외부 서비스가 바뀌면 결과도 달라질 수 있습니다.',
'AI 답변엔진(ChatGPT, Gemini, Perplexity 등)의 답변 내용과 노출 여부는 각 서비스 운영사가 정합니다. 회사는 진단과 개선 작업을 통해 AI가 정보를 정확히 읽고 인용할 수 있도록 정비하지만, 특정 답변에 노출되거나 특정 순위에 오르는 것을 보장하지 않습니다.',
'사이트에 실린 예시 리포트는 가상 데이터로 만든 것이며 실제 의료기관의 진단 결과가 아닙니다.',
],
},
{
title: '의료 관련 고지',
body: [
'서비스는 의료기관의 온라인 정보 정비를 돕는 마케팅 서비스이며, 의학적 진단이나 치료 판단을 제공하지 않습니다.',
'의료기관이 서비스 산출물을 광고 목적으로 게시하는 경우, 의료법에 따른 의료광고 사전심의 등 필요한 절차는 해당 의료기관이 책임지고 이행해야 합니다. 회사의 검토는 이 절차를 대신하지 않습니다.',
],
},
{
title: '이용자의 의무',
body: [
'- 문의 및 진단 요청 시 사실에 맞는 정보를 입력해야 합니다.',
'- 권한이 없는 타인의 웹사이트나 정보를 자신의 것처럼 진단 요청해서는 안 됩니다.',
'- 문의 내용에 환자의 개인정보나 건강 정보를 적어서는 안 됩니다.',
'- 사이트의 정상적인 운영을 방해하거나, 자동화된 수단으로 서식을 반복 제출해서는 안 됩니다.',
],
},
{
title: '회사의 의무',
body: [
'회사는 관계 법령과 이 약관을 지키며, 안정적으로 서비스를 제공하기 위해 노력합니다.',
'회사는 개인정보처리방침에 따라 이용자의 개인정보를 보호하며, 이용자의 정당한 의견과 불만은 지체 없이 처리합니다.',
],
},
{
title: '지식재산권',
body: [
'사이트의 글, 디자인, 진단 기준표, 리포트 양식 등에 대한 권리는 회사에 있습니다. 이용자는 회사의 사전 동의 없이 이를 복제, 배포, 2차 가공해 영리 목적으로 이용할 수 없습니다.',
'별도 계약에 따른 산출물의 권리 귀속은 개별 계약에서 정합니다.',
],
},
{
title: '서비스의 변경과 중단',
body: [
'회사는 운영상, 기술상 필요한 경우 사이트의 내용을 바꿀 수 있습니다.',
'설비 점검, 장애, 천재지변 등 부득이한 사유가 있으면 사이트 제공을 일시 중단할 수 있으며, 가능한 경우 미리 알립니다.',
],
},
{
title: '책임의 제한',
body: [
'- 회사는 천재지변, 외부 서비스(호스팅·AI 답변엔진·검색엔진 등)의 장애나 정책 변경 등 회사가 통제할 수 없는 사유로 생긴 손해에 대해 책임을 지지 않습니다.',
'- 회사는 이용자가 입력한 정보의 오류나 이용자의 귀책사유로 생긴 손해에 대해 책임을 지지 않습니다.',
'- 회사는 무료로 제공하는 사이트 정보와 예시 리포트의 이용으로 생긴 손해에 대해 고의 또는 중대한 과실이 없는 한 책임을 지지 않습니다.',
],
},
{
title: '분쟁 해결과 관할',
body: [
'회사와 이용자는 서비스와 관련한 분쟁을 성실히 협의해 해결합니다.',
'협의로 해결되지 않는 분쟁은 대한민국 법을 따르며, 민사소송법에 따른 관할 법원에서 해결합니다.',
],
},
];
export default function TermsPage() {
return (
<LegalLayout
title="이용약관"
intro={`${COMPANY_LEGAL_NAME}가 운영하는 INFINITH AI Discovery 웹사이트의 이용 조건입니다.`}
effectiveDate="2026년 10월 7일"
sections={SECTIONS}
/>
);
}

View File

@ -88,7 +88,17 @@ export default defineConfig(({mode}) => {
}),
],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
// 공개 배포(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: {