diff --git a/data/clinic-registry/clinic_registry_working.csv b/data/clinic-registry/clinic_registry_working.csv
index a7c689d..f9ef1f9 100644
--- a/data/clinic-registry/clinic_registry_working.csv
+++ b/data/clinic-registry/clinic_registry_working.csv
@@ -1,6 +1,6 @@
-hospital_name,brand_group,district,branches,website_kr,website_en,youtube_url,youtube_note,instagram_kr_url,instagram_followers,instagram_posts,instagram_kr_note,instagram_en_url,instagram_en_note,facebook_url,facebook_note,tiktok_url,tiktok_note,gangnam_unni_url,gangnam_unni_note,gangnam_unni_rating,gangnam_unni_reviews,gangnam_unni_badges,gangnam_unni_procedures,lead_doctor,naver_blog_url,naver_blog_note,naver_place_url,naver_place_reviews_note,google_maps_url,google_reviews_note
+hospital_name,brand_group,district,branches,website_kr,website_en,youtube_url,youtube_note,instagram_kr_url,instagram_followers,instagram_posts,instagram_kr_note,instagram_en_url,instagram_en_note,facebook_url,facebook_note,tiktok_url,tiktok_note,gangnam_unni_url,gangnam_unni_note,gangnam_unni_rating,gangnam_unni_reviews,gangnam_unni_badges,gangnam_unni_procedures,lead_doctor,naver_blog_url,naver_blog_note,naver_place_url,naver_place_reviews_note,google_maps_url,google_reviews_note
바노바기성형외과,프리미엄/하이타깃 후보,강남,,https://www.banobagi.com,,https://www.youtube.com/c/banobagips,,https://www.instagram.com/banobagi_ps/,4124,196,,,,https://www.facebook.com/BanobagiPlasticSurgery,,,,https://www.gangnamunni.com/hospitals/23,,9.2,6843,수술실 CCTV;마취과 전문의 상주;여성 의사 진료;분야별 공동 진료;시술 후 관리;의료진 실명 공개;환자 숙박 정보 제공;전용 휴식 공간;입원 시설;응급 대응 체계;미용 의료 시술 진료;야간진료,눈성형;코성형;안면윤곽/양악;가슴성형;지방성형;필러;보톡스;리프팅;모발이식,반재상,https://blog.naver.com/banobagips,,https://m.place.naver.com/hospital/21033469,리뷰 773개,,
-뷰성형외과,프리미엄/하이타깃 후보,강남,뷰성형외과 역삼센터(역삼),https://www.viewclinic.com,,https://www.youtube.com/@ViewclinicKR,,https://www.instagram.com/viewplastic/,14072,1417,,,,https://www.facebook.com/viewps1/,,,,https://www.gangnamunni.com/hospitals/189,,9.1,18840,수술실 CCTV;마취과 전문의 상주;분야별 공동 진료;의료진 실명 공개;야간진료;여성 의사 진료;응급 대응 체계;시술 후 관리;입원 시설;전용 휴식 공간,눈성형;코성형;안면윤곽/양악;가슴성형;지방성형;필러;보톡스;피부리프팅;기타,최순우,https://blog.naver.com/viewclinicps,,https://m.place.naver.com/hospital/11709005,리뷰 776개,,
+뷰성형외과,프리미엄/하이타깃 후보,강남,뷰성형외과 역삼센터(역삼),https://www.viewclinic.com,,https://www.youtube.com/@ViewclinicKR,,https://www.instagram.com/viewplastic/,14072,1417,,,,https://www.facebook.com/viewps1/,,,,https://www.gangnamunni.com/hospitals/189,,9.5,19007,수술실 CCTV;마취과 전문의 상주;분야별 공동 진료;의료진 실명 공개;야간진료;여성 의사 진료;응급 대응 체계;시술 후 관리;입원 시설;전용 휴식 공간,눈성형;코성형;안면윤곽/양악;가슴성형;지방성형;필러;보톡스;피부리프팅;기타,최순우,https://blog.naver.com/viewclinicps,,https://m.place.naver.com/hospital/11709005,리뷰 776개,,
아이디병원,프리미엄/하이타깃 후보,강남,아이디병원 별관(역삼),https://www.idhospital.com,,https://www.youtube.com/user/IDhospital,,https://www.instagram.com/idhospital,10120,811,,,,https://www.facebook.com/idhospital0050,,,,https://www.gangnamunni.com/hospitals/257,,9.5,14933,수술실 CCTV;시술 후 관리;의료진 실명 공개;여성 의사 진료;분야별 공동 진료;입원 시설;전용 휴식 공간;환자 숙박 정보 제공;야간진료;마취과 전문의 상주;응급 대응 체계,양악수술;안면윤곽;눈/코성형;가슴성형;리프팅;피부클리닉;쁘띠성형;치과,박상훈,https://blog.naver.com/idfacial,,https://m.place.naver.com/hospital/11548359,,,
그랜드성형외과,프리미엄/하이타깃 후보,강남,,https://www.grandsurgery.com,,https://www.youtube.com/channel/UCU2o_aHqsNFuqwtdzVM3xbQ,,https://www.instagram.com/grand_korea/,4015,1148,,,,https://www.facebook.com/grandps.korea,,,,https://www.gangnamunni.com/hospitals/62,,9.8,1531,분야별 공동 진료;응급 대응 체계;시술 후 관리;전용 휴식 공간;입원 시설;마취과 전문의 상주;의료진 실명 공개;성형외과 전문의 진료;여성 의사 진료;미용 의료 시술 진료;환자 숙박 정보 제공;역에서 도보 5분;학회 활동 의사,피부;코성형;눈성형;보톡스;필러;리프팅;가슴성형;지방성형,이세환,https://blog.naver.com/grandprs,,https://m.place.naver.com/hospital/12322994,,,
원진성형외과,프리미엄/하이타깃 후보,강남,,https://www.k-wonjin.co.kr,,https://www.youtube.com/@wjwonjin,,https://www.instagram.com/wonjin_official/,23445,1844,,,,https://www.facebook.com/KwonjinPS,,https://www.tiktok.com/@wonjin_official,,https://www.gangnamunni.com/hospitals/2500,,9.3,11789,고객평가우수병원;분야별 공동 진료;응급 대응 체계;시술 후 관리;전용 휴식 공간;수술실 CCTV;여성 의사 진료;마취과 전문의 상주;야간진료;의료진 실명 공개,눈성형;코성형;안면윤곽/양악;가슴성형;지방성형;기타;모발이식;리프팅;보톡스;필러,강문석,https://blog.naver.com/popokpop,,https://m.place.naver.com/hospital/11887873,리뷰 9개,,
@@ -71,4 +71,4 @@
마노성형외과,프리미엄/하이타깃 후보,서초,,https://manops.co.kr,,https://www.youtube.com/@mano_ps,,https://www.instagram.com/manops_official,1006,239,,,,,,,,https://www.gangnamunni.com/hospitals/3429,,9.5,1034,응급 대응 체계;시술 후 관리;전용 휴식 공간;입원 시설;의료진 실명 공개;환자 숙박 정보 제공;분야별 공동 진료;야간진료,입술성형;귀성형;인중축소;안면윤곽;가슴성형;눈성형;코성형;리프팅;지방성형;기타,정태광,https://blog.naver.com/tkhr4747,,,,,
에스엠성형외과,프리미엄/하이타깃 후보,서초,,https://www.sm-ps.co.kr,,https://www.youtube.com/@smpsclinic,,https://www.instagram.com/smps_plastic_surgery/,1866,222,,,,https://www.facebook.com/thammySM3707,,,,https://www.gangnamunni.com/hospitals/413,,9.6,883,고객평가우수병원;시술 후 관리;의료진 실명 공개,눈성형;코성형;지방성형;가슴성형,이무영,https://blog.naver.com/smps1004,,https://m.place.naver.com/hospital/32876182,,,
서울미작성형외과,프리미엄/하이타깃 후보,서초,,https://mijakclinic.co.kr,,https://www.youtube.com/channel/UCld-NgVeydtDRVTt6xQRbmA,,https://www.instagram.com/mijak2444/,4990,2487,,,,https://www.facebook.com/mijaknose,,,,,,,,,,,https://blog.naver.com/mijak3444,,,,,
-더원성형외과,프리미엄/하이타깃 후보,서초,,https://www.theoneclinic.co.kr,,,,https://www.instagram.com/theone_plastic_surgery/,13127,372,,,,https://www.facebook.com/pages/category/Hospital/더원성형외과-1539415639613021/,,,,https://www.gangnamunni.com/hospitals/5636,,10,1,고객평가우수병원,코성형;눈성형;안면윤곽/양악;보톡스;필러;리프팅;가슴성형;지방성형;제모;기타;치아;모발이식;한방,방난석,https://blog.naver.com/brs0714,,,,,
+더원성형외과,프리미엄/하이타깃 후보,서초,,https://www.theoneclinic.co.kr,,,,https://www.instagram.com/theone_plastic_surgery/,13127,372,,,,https://www.facebook.com/pages/category/Hospital/더원성형외과-1539415639613021/,,,,https://www.gangnamunni.com/hospitals/5636,,10,1,고객평가우수병원,코성형;눈성형;안면윤곽/양악;보톡스;필러;리프팅;가슴성형;지방성형;제모;기타;치아;모발이식;한방,방난석,https://blog.naver.com/brs0714,,,,,
\ No newline at end of file
diff --git a/src/components/CTA.tsx b/src/components/CTA.tsx
index 0f6f544..86e0bd1 100644
--- a/src/components/CTA.tsx
+++ b/src/components/CTA.tsx
@@ -1,14 +1,27 @@
-import { useState } from 'react';
-import { useNavigate } from 'react-router';
+/**
+ * CTA — 랜딩 페이지 하단 최종 전환 섹션.
+ *
+ * PART III 피봇:
+ * - Hero와 동일한 입력 로직(url state/navigate)을 중복 구현하던 문제 해소.
+ * 이제 ` ` 한 줄로 통일.
+ * - "무료 진단" 언급 삭제 — 계약 기반 모델 반영.
+ * - 헤드라인은 전략 소유권 메시지로 피봇 ("Own Your Marketing Strategy.").
+ * - 보조 CTA "가격 플랜 보기" 추가 (/pricing?from=cta).
+ */
+import { useNavigate, Link } from 'react-router';
import { motion } from 'motion/react';
-import { ArrowRight } from 'lucide-react';
+import MultiChannelInput, { type AnalyzePayload } from './MultiChannelInput';
export default function CTA() {
- const [url, setUrl] = useState('');
const navigate = useNavigate();
- const handleAnalyze = () => {
- if (url.trim()) navigate('/report/loading', { state: { url } });
+ const handleAnalyze = (payload: AnalyzePayload) => {
+ navigate('/report/loading', {
+ state: {
+ url: payload.primaryUrl,
+ manualChannels: payload.manualChannels,
+ },
+ });
};
return (
@@ -24,7 +37,7 @@ export default function CTA() {
transition={{ duration: 0.6 }}
className="text-4xl md:text-5xl font-serif font-bold mb-4 leading-tight text-transparent bg-clip-text bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff]"
>
- Ready to Transform Your Marketing?
+ Own Your Marketing Strategy.
- URL 하나로 시작하는 완벽한 마케팅 자동화. 지금 바로 무료 진단을 받아보세요.
+ URL 하나로 시작하는 AI 마케팅 전략 플래너.
- setUrl(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && handleAnalyze()}
- className="w-full px-8 py-4 text-base font-medium bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] border border-white/20 rounded-full focus:outline-none focus:ring-2 focus:ring-white/50 shadow-sm text-center text-primary-900 placeholder:text-primary-900/60"
- />
-
- Analyze
-
-
-
- 네이버 블로그, 플레이스, 소셜미디어 종합 분석 리포트 받아보기
-
+
+
+ {/* 보조 CTA — 가격 플랜 */}
+
+
+ 가격 플랜 보기 →
+
+
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index 0602ea9..c0490ad 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -1,15 +1,18 @@
-import { useState } from 'react';
import { useNavigate } from 'react-router';
import { motion } from 'motion/react';
-import { ArrowRight } from 'lucide-react';
import { PrismFilled } from './icons/FilledIcons';
+import MultiChannelInput, { type AnalyzePayload } from './MultiChannelInput';
export default function Hero() {
- const [url, setUrl] = useState('');
const navigate = useNavigate();
- const handleAnalyze = () => {
- if (url.trim()) navigate('/report/loading', { state: { url } });
+ const handleAnalyze = (payload: AnalyzePayload) => {
+ navigate('/report/loading', {
+ state: {
+ url: payload.primaryUrl,
+ manualChannels: payload.manualChannels,
+ },
+ });
};
return (
@@ -25,7 +28,7 @@ export default function Hero() {
className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 shadow-sm mb-8"
>
- Agentic AI Marketing Automation for Premium Medical Business & Marketing Agency
+ AI Marketing Strategist · Built for Premium Clinics
Marketing Engine.
+ {/* 서브 카피 — PART II 피봇: Strategic Planner 포지셔닝
+ ▸ 이전 "Marketing that learns... 쓸수록 더 정교해지는..." 은 풀스택 자동화 약속이
+ Generation/Distribution Mock 상태와 맞지 않았음.
+ ▸ Phase 1-4 (Discover→Collect→Report→Plan) 실제 구현 범위에 정직하게 일치시킴. */}
- Marketing that learns, improves, and accelerates — automatically.
- 쓸수록 더 정교해지는 {' '}AI 마케팅 엔진. {' '}콘텐츠 기획, {' '}생성, {' '}영상 제작, {' '}채널 배포, {' '}데이터 분석까지 {' '}하나로.
+ The Strategic Planner for Premium Medical Marketing.
+ 유튜브·인스타·네이버·강남언니를 {' '}
+ 10분 만에 진단하고, {' '}
+ 병원의 12개월 마케팅 전략을 {' '}
+ 설계합니다.
-
- setUrl(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && handleAnalyze()}
- className="w-full px-8 py-5 text-base font-medium bg-white/80 backdrop-blur-sm border border-slate-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent/40 shadow-sm text-center text-primary-900 placeholder:text-slate-400 transition-all group-hover:border-slate-300"
- />
-
-
- Analyze Marketing Performance
-
-
-
- 네이버 블로그, 플레이스, 소셜미디어 등 Online Presence 종합 분석 리포트를 제공합니다.
-
+
diff --git a/src/components/Modules.tsx b/src/components/Modules.tsx
index 0784cda..ba6cd2a 100644
--- a/src/components/Modules.tsx
+++ b/src/components/Modules.tsx
@@ -1,74 +1,76 @@
import React from 'react';
import { motion } from 'motion/react';
+// PART II 피봇: 1·5번은 Product 1.0 Available, 2·3·4는 Coming Soon.
+// 카피는 Intelligence + Planning 중심으로 조정 (구조 5개 카드는 유지).
const modules = [
{
step: "1",
title: "Marketing Intelligence",
items: [
- "브랜딩, 마케팅 현황 분석",
- "타겟 고객 분석",
- "키워드 분석",
- "경쟁 및 포지셔닝 분석",
- "SEO 전략 & 채널별 콘텐츠 기획"
+ "브랜드·온라인 프레즌스 진단",
+ "유튜브·인스타·네이버·강남언니 실측",
+ "경쟁 병원 벤치마크 (최대 10곳)",
+ "키워드·해시태그 트렌드 분석",
+ "Vision AI 기반 의료진·슬로건 추출",
],
- highlight: "AI 기반 시장 통찰력 도출",
+ highlight: "10분 진단 → 전략 기획의 출발점",
color: "bg-[#021341]",
textColor: "text-indigo-600"
},
{
step: "2",
- title: "Content Creation",
+ title: "Strategic Planning",
items: [
- "블로그 콘텐츠 생성",
- "SEO 콘텐츠 생성",
- "SNS 콘텐츠 생성",
- "마케팅 카피 생성",
- "Human-in-the loop 프로세스"
+ "12개월 마케팅 로드맵",
+ "4~8주 콘텐츠 캘린더 + 필러 5종",
+ "KPI 대시보드 (3·12개월 목표)",
+ "주간 KPI 달성도 → 전략 조정 제안",
+ "브랜드 가이드 (톤·컬러·로고) 자동 추출",
],
- highlight: "고품질 맞춤형 콘텐츠 자동화",
+ highlight: "관찰을 실행 가능한 전략으로 전환",
color: "bg-[#021341]",
textColor: "text-indigo-600"
},
{
step: "3",
- title: "Video Automation",
+ title: "Content Creation",
items: [
- "블로그 → 영상 변환",
- "숏폼 콘텐츠 생성",
- "유튜브 콘텐츠 제작",
- "SNS 영상 제작",
- "멀티모달 AI 엔진: 영상 + 음악 + 카피"
+ "블로그·SEO·SNS 카피 자동 생성",
+ "브랜드 톤 일관성 보장",
+ "Human-in-the-loop 편집 워크플로우",
+ "콘텐츠 필러·시즌별 테마 매핑",
+ "Coming Soon · Q4 2026",
],
- highlight: "원클릭 영상 제작 시스템",
+ highlight: "전략을 실행 가능한 콘텐츠로",
color: "bg-[#021341]",
textColor: "text-indigo-600"
},
{
step: "4",
- title: "Distribution Engine",
+ title: "Video Automation",
items: [
- "블로그 게시",
- "SNS 자동 게시",
- "유튜브 업로드",
- "콘텐츠 일정 관리",
- "SEO, AEO 자동 최적화"
+ "블로그 → 숏폼·유튜브 영상 변환",
+ "Creatomate 기반 자동 렌더링",
+ "음악·자막·썸네일 AI 생성",
+ "시즌별 템플릿 + 병원 CI 반영",
+ "Coming Soon · Q4 2026",
],
- highlight: "전 채널 통합 배포 및 최적화",
+ highlight: "영상 제작 리소스 부담 해소",
color: "bg-[#021341]",
textColor: "text-indigo-600"
},
{
step: "5",
- title: "Performance Intelligence",
+ title: "Distribution & Performance",
items: [
- "SEO 성과 분석",
- "콘텐츠 성과 분석",
- "채널 성과 분석",
- "AI 콘텐츠 개선 전략 추천",
- "데이터 기반 효과 검증"
+ "멀티 채널 자동 게시 (블로그·SNS·유튜브)",
+ "SEO·AEO 자동 최적화",
+ "실시간 성과 트래킹 + 리포트",
+ "KPI 달성 → 다음 주기 전략 피드백",
+ "Coming Soon · Q4 2026",
],
- highlight: "실시간 성과 추적 및 개선",
+ highlight: "전략-실행-성과의 자율 루프 완성",
color: "bg-[#021341]",
textColor: "text-indigo-600"
}
@@ -133,8 +135,11 @@ export default function Modules() {
Core Modules
-
- 성능 개선 반영 자율 순환 마케팅 시스템
+ {/* PART II 피봇: "자율 순환 마케팅 시스템" → 실제 Phase 1-4 구현 범위로 정직하게 조정.
+ Product 1.0 Available 모듈 2개(Marketing Intelligence + Strategic Planning)와
+ Coming Soon 모듈 3개(Content/Video/Distribution)로 완성도 신호를 분명히. */}
+
+ 진단부터 전략 설계까지 Product 1.0 으로 출시된 모듈 과 콘텐츠 실행을 위한 후속 로드맵입니다.
diff --git a/src/components/MultiChannelInput.tsx b/src/components/MultiChannelInput.tsx
new file mode 100644
index 0000000..87d4b53
--- /dev/null
+++ b/src/components/MultiChannelInput.tsx
@@ -0,0 +1,213 @@
+/**
+ * MultiChannelInput — 랜딩 Hero / CTA 공용 멀티 URL 입력 컴포넌트.
+ *
+ * 설계 배경 (PART III 피봇):
+ * - 기존 Hero/CTA의 단일 URL input은 "홈페이지 URL → 자동 SNS 발견"을 전제로 했지만,
+ * 영업 현장에서는 병원이 본인들의 YouTube/Instagram/FB/네이버플레이스/블로그/강남언니 URL을
+ * 이미 알고 있는 경우가 많아 이를 직접 받는 편이 정확도·속도 면에서 우월합니다.
+ * - Hero와 CTA가 동일한 입력 로직(url state + handleAnalyze + navigate)을 **중복 구현**하던
+ * 문제를 이 컴포넌트 하나로 해소합니다.
+ *
+ * 동작:
+ * 1) Textarea에 URL을 공백/쉼표/줄바꿈으로 구분해 붙여넣기
+ * 2) `classifyUrls()`로 실시간 7채널 분류 (`useMemo`)
+ * 3) 채널별 칩 프리뷰 — 검출 건수 표시, unknown URL은 경고
+ * 4) "채널 분석 시작" 버튼 클릭 → onAnalyze(payload) 호출 (부모가 navigation 처리)
+ *
+ * DS 준수:
+ * - Filled Icons Only (lucide 무단 사용 금지 — 이모지·outlined 금지)
+ * - DS Primary pill: rounded-full + gradient `from-[#4F1DA1] to-[#021341]`
+ * - variant='hero': 글래스 배경 (랜딩 Hero)
+ * - variant='cta': 다크 배경 (CTA 섹션)
+ */
+
+import { useMemo, useState, type ReactElement } from 'react';
+import { ArrowRight } from 'lucide-react';
+import {
+ GlobeFilled,
+ YoutubeFilled,
+ InstagramFilled,
+ FacebookFilled,
+ DatabaseFilled,
+ FileTextFilled,
+ MessageFilled,
+ WarningFilled,
+} from './icons/FilledIcons';
+import {
+ classifyUrls,
+ hasAnalyzableChannels,
+ pickPrimaryUrl,
+ type ClassifiedUrls,
+} from '../lib/classifyUrls';
+
+/** discover-channels Edge Function에 전달되는 수동 채널 URL 묶음. */
+export interface ManualChannels {
+ youtube?: string[];
+ instagram?: string[];
+ facebook?: string[];
+ naverPlace?: string[];
+ naverBlog?: string[];
+ gangnamUnni?: string[];
+}
+
+export interface AnalyzePayload {
+ /** discover-channels `url` 필드 (필수) — 홈페이지 우선, 없으면 첫 SNS URL. */
+ primaryUrl: string;
+ /** 사용자가 제공한 채널별 URL — Edge Function이 Firecrawl discovery를 스킵. */
+ manualChannels: ManualChannels;
+ /** 사용자 원본 입력 (디버깅·재실행용) */
+ rawInput: string;
+}
+
+interface MultiChannelInputProps {
+ variant?: 'hero' | 'cta';
+ onAnalyze: (payload: AnalyzePayload) => void;
+}
+
+/** 채널 칩 메타 — 렌더링 순서·아이콘·한글명. */
+const CHANNEL_META: Array<{
+ key: keyof Omit;
+ label: string;
+ Icon: (props: { size?: number; className?: string }) => ReactElement;
+ color: string; // 활성화 시 텍스트/아이콘 색
+}> = [
+ { key: 'homepage', label: '홈페이지', Icon: GlobeFilled, color: 'text-[#4F1DA1]' },
+ { key: 'youtube', label: 'YouTube', Icon: YoutubeFilled, color: 'text-[#FF0000]' },
+ { key: 'instagram', label: 'Instagram', Icon: InstagramFilled,color: 'text-[#E1306C]' },
+ { key: 'facebook', label: 'Facebook', Icon: FacebookFilled, color: 'text-[#1877F2]' },
+ { key: 'naverPlace', label: '네이버 플레이스', Icon: DatabaseFilled, color: 'text-[#03C75A]' },
+ { key: 'naverBlog', label: '네이버 블로그', Icon: FileTextFilled, color: 'text-[#03C75A]' },
+ { key: 'gangnamUnni', label: '강남언니', Icon: MessageFilled, color: 'text-[#FF5C89]' },
+];
+
+export default function MultiChannelInput({ variant = 'hero', onAnalyze }: MultiChannelInputProps) {
+ const [text, setText] = useState('');
+
+ // 실시간 분류 — text가 바뀔 때만 재계산.
+ const classified = useMemo(() => classifyUrls(text), [text]);
+
+ const canAnalyze = hasAnalyzableChannels(classified);
+ const primaryUrl = pickPrimaryUrl(classified);
+
+ const handleSubmit = () => {
+ if (!canAnalyze || !primaryUrl) return;
+ const payload: AnalyzePayload = {
+ primaryUrl,
+ manualChannels: {
+ // homepage는 primaryUrl로 들어가므로 manualChannels에서는 제외 (Edge Function에서 `url`을 쓰므로)
+ youtube: classified.youtube.length ? classified.youtube : undefined,
+ instagram: classified.instagram.length ? classified.instagram : undefined,
+ facebook: classified.facebook.length ? classified.facebook : undefined,
+ naverPlace: classified.naverPlace.length ? classified.naverPlace : undefined,
+ naverBlog: classified.naverBlog.length ? classified.naverBlog : undefined,
+ gangnamUnni: classified.gangnamUnni.length ? classified.gangnamUnni : undefined,
+ },
+ rawInput: text,
+ };
+ onAnalyze(payload);
+ };
+
+ // variant별 스타일 토큰
+ // ▸ `text-center` 추가 (원본 Hero 단일 URL input과 일관): Form 필드가 아닌
+ // "검색창 같은 자유 입력" 시그널로 전환. placeholder 여러 줄도 중앙 정렬.
+ const isHero = variant === 'hero';
+ const textareaClass = isHero
+ ? 'w-full px-6 py-4 text-sm md:text-base text-center bg-white/80 backdrop-blur-sm border border-slate-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent/40 shadow-sm text-primary-900 placeholder:text-slate-400 transition-all resize-none leading-relaxed'
+ : 'w-full px-6 py-4 text-sm md:text-base text-center bg-white/5 backdrop-blur-sm border border-white/15 rounded-2xl focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/60 text-white placeholder:text-white/40 transition-all resize-none leading-relaxed';
+
+ const helperClass = isHero ? 'text-slate-500' : 'text-white/60';
+ const chipInactiveBg = isHero ? 'bg-slate-50 border-slate-100' : 'bg-white/5 border-white/10';
+ const chipInactiveText = isHero ? 'text-slate-400' : 'text-white/40';
+ const chipActiveBg = isHero ? 'bg-white border-slate-200 shadow-sm' : 'bg-white/10 border-white/25';
+ const chipActiveText = isHero ? 'text-primary-900' : 'text-white';
+
+ return (
+
+ {/* URL Textarea
+ Placeholder 카피 원칙: "한 줄씩" 같은 입력 규칙 제거.
+ classifyUrls.ts가 공백·쉼표·줄바꿈·임의 텍스트 섞임 모두 처리하므로
+ 사용자에게 입력 형식 고민을 넘기지 않고, "뭉치 → 자동 분류" 를 제품 약속으로 선언.
+ 정렬: `text-center` — 원본 Hero의 단일 URL input과 일관, Form 필드 인상을 줄여
+ "검색창 같은 자유 입력" 느낌으로 전환. */}
+
+ );
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 006951e..6cd9cc1 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -1,22 +1,69 @@
+/**
+ * Navbar — 전역 상단 네비게이션.
+ *
+ * PART III 피봇:
+ * - "Free Report" CTA 완전 제거 (계약 기반 영업 모델로 전환).
+ * - Login 버튼 복원 — 계약 완료된 병원에 별도 발급된 자격 정보로 로그인 (스캐폴딩 단계).
+ * - 주 CTA는 `문의하기` (mailto:contact@o2o.kr) — 모든 리드 단일 채널화.
+ *
+ * 라우팅 구성:
+ * - 로고 "INFINITH" → `/` (홈)
+ * - Product / Use Cases → `/#solution`, `/#use-cases` 앵커
+ * - Pricing → `/pricing?from=header` (유입 추적용 쿼리)
+ * - Login → `/login` (Secondary pill — 계약 병원 전용)
+ * - 문의하기 → mailto (Primary pill — 신규 리드)
+ *
+ * ⚠️ 앵커 링크를 `/#solution` 형태로 쓰는 이유:
+ * 단순 `#solution`은 현재 페이지 내부 해시로 해석됩니다.
+ * 예컨대 `/pricing`에서 `#solution`을 누르면 /pricing 페이지 내의 #solution을 찾다가 실패합니다.
+ * `/#solution`은 React Router가 홈으로 이동시킨 뒤 브라우저가 해시 스크롤을 처리합니다.
+ */
import { Link } from 'react-router';
-import { motion } from 'motion/react';
+import { ArrowRight } from 'lucide-react';
+import { buildContactMailto } from '../lib/contact';
export default function Navbar() {
return (
-
+
+ {/* 로고 */}
-
INFINITH
+
+ INFINITH
+
+
+ {/* 가운데 메뉴 */}
-
-
+
+ {/* 우측 CTA — Login(Secondary) + 문의하기(Primary) */}
+
diff --git a/src/components/Problems.tsx b/src/components/Problems.tsx
index f403546..37ce561 100644
--- a/src/components/Problems.tsx
+++ b/src/components/Problems.tsx
@@ -1,18 +1,19 @@
import { motion } from 'motion/react';
+// PART II 피봇: 현장 고충 중심으로 재작성. #3 "데이터 기반의 마케팅 부족"은 유지(사용자 피드백).
const problems = [
{
- title: "콘텐츠 생산의 한계",
- desc: "블로그, SEO, 유튜브, 숏폼 등 지속적 생산이 필요하지만 인력과 비용, 시간 부족으로 제한됩니다."
+ title: "콘텐츠, 소진되는 비용과 시간",
+ desc: "매주 쏟아지는 콘텐츠 제작 요구에 인력·시간·예산이 빠르게 소진됩니다. ROI를 측정할 여력도 부족합니다."
},
{
- title: "영상 콘텐츠 제작 비용",
- desc: "영상은 중요하지만 촬영, 편집, 기획 비용이 높습니다. 특히 숏폼 콘텐츠는 지속적인 제작이 어렵습니다.",
+ title: "트렌드와 경쟁사 분석 부재",
+ desc: "강남언니·네이버·유튜브에서 경쟁 병원이 어떤 콘텐츠·메시지로 움직이는지 체계적으로 추적할 방법이 없습니다.",
highlight: true
},
{
title: "데이터 기반의 마케팅 부족",
- desc: "어떤 콘텐츠가 효과적인지, 어떤 키워드가 유입을 만드는지, 어떤 채널이 성과가 좋은지 알기 어렵고, 각 플랫폼들의 데이터들을 한눈에 파악할 수 없습니다."
+ desc: "콘텐츠·광고·채널 성과가 어디에서 어떻게 작동하는지 데이터로 증명하기 어렵고, 의사결정은 감에 의존합니다."
}
];
diff --git a/src/components/Solution.tsx b/src/components/Solution.tsx
index fc35716..b9ba9db 100644
--- a/src/components/Solution.tsx
+++ b/src/components/Solution.tsx
@@ -16,7 +16,7 @@ export default function Solution() {
className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 mb-8"
>
-
AI Marketing Engine
+
Strategic Planning Engine
- Infinite Marketing Engine
+ The Strategic Planning Engine
+ {/* PART II 피봇: "관찰이 아닌 실행 가능한 전략을 출력하는 AI 플래너"
+ AGDP 키워드는 유지하되 의미를 전략 설계 중심으로 재해석. */}
- Infinite Marketing for Premium Medical Business & Marketing Agency
+ 관찰이 아닌, 실행 가능한 전략을 출력합니다.
- Infinite Marketing은 Premium Medical Business와 Marketing Agency를 위한 AI Marketing Automation Platform입니다.
- INFINITH는 마케팅 분석, 콘텐츠 생성, 영상 콘텐츠 제작, 채널 배포, 성과 분석과 피드백 적용을 하나의 Self-Improving Marketing Engine으로 제공합니다.
+ INFINITH는 Audit → Generation → Direction → Planning 의 AGDP 루프로 병원의 마케팅 의사결정을 설계합니다. 유튜브·인스타·네이버·강남언니 실측 데이터를 기반으로 12개월 전략과 주간 KPI 조정까지.
{/* Circular Loop Diagram */}
@@ -69,21 +70,21 @@ export default function Solution() {
AGDP
-
Infinite Marketing
+ Strategic Planning
- {/* Node A: Analysis (Left) */}
+ {/* Node A: Audit (Left) — 구 Analysis. 병원·채널 진단 리포트 */}
- {/* Node G: Generation (Top) */}
+ {/* Node G: Generation (Top) — 전략·로드맵 문서 생성 (AI 콘텐츠 생성 아님) */}
G
@@ -93,23 +94,23 @@ export default function Solution() {
- {/* Node D: Distribution (Right) */}
+ {/* Node D: Direction (Right) — 구 Distribution. 채널별 전략·우선순위 설계 */}
D
- Distribution
+ Direction
- {/* Node P: Performance (Bottom) */}
+ {/* Node P: Planning (Bottom) — 구 Performance. KPI 목표 설정 + 주간 조정 */}
P
- Performance
+ Planning
@@ -118,9 +119,11 @@ export default function Solution() {
+ {/* 구 "Reward Signal" → "KPI Feedback" 로 피봇.
+ 주간 KPI 달성도가 전략 재조정의 입력 시그널이 된다는 의미. */}
- ← Reward SIGNAL
+ ← KPI FEEDBACK
@@ -135,8 +138,8 @@ export default function Solution() {
className="max-w-3xl mx-auto mt-12 text-center"
>
-
- AGDP Cycle: 분석(Analysis) → 생성(Generation) → 배포(Distribution) → 성과(Performance)의 무한 루프를 통해 콘텐츠 품질(CTR)을 자율 최적화 합니다.
+
+ AGDP Cycle: 진단(Audit) → 전략 생성(Generation) → 채널 방향성(Direction) → KPI 계획(Planning)의 무한 루프로 주간 KPI 달성도 → 전략 재조정 을 자율 반복합니다.
diff --git a/src/components/TargetAudience.tsx b/src/components/TargetAudience.tsx
index 3d7f9dc..4e0a1b6 100644
--- a/src/components/TargetAudience.tsx
+++ b/src/components/TargetAudience.tsx
@@ -14,8 +14,9 @@ export default function TargetAudience() {
Who is Infinite Marketing for
-
- 프리미엄 의료 서비스와 전문 마케팅 에이전시를 위한 최적의 솔루션
+ {/* PART II 피봇: 프리미엄 병원 메인, 대행사 보조. "전략 파트너·전략 자문" 포지셔닝 반영. */}
+
+ 프리미엄 병원의 전략 기획 과 대행사 전략 자문 을 위한 AI 플래너입니다.
@@ -28,8 +29,8 @@ export default function TargetAudience() {
className="glass-card p-10 md:p-12 bg-gradient-to-br from-slate-50 to-white border border-slate-100 rounded-3xl"
>
Premium Medical Business
-
- 고객 LTV가 높고 브랜드 경쟁이 심해 콘텐츠 마케팅이 필수적인 프리미엄 의료 서비스 제공 병원
+
+ 고객 LTV가 높은 프리미엄 성형외과의 전략 파트너 . 유튜브·인스타·네이버·강남언니를 10분 만에 진단해 12개월 로드맵을 설계합니다.
{['피부과', '성형외과', '치과', '안과', '헬스케어 클리닉', '피트니스'].map((item, i) => (
@@ -47,9 +48,16 @@ export default function TargetAudience() {
transition={{ duration: 0.6, delay: 0.2 }}
className="glass-card p-10 md:p-12 bg-gradient-to-br from-purple-50 to-white border border-purple-100/50 rounded-3xl"
>
-
Medical Marketing Agency
-
- 콘텐츠 제작 비용 부담과 인력 의존도가 높고 영상 제작 생산성 개선이 필요한 병원 마케팅 대행사
+
+
Medical Marketing Agency
+ {/* Partner Program 배지 — "준비중" 결핍 메시지 → "신청하세요" waitlist 희소성 프레임으로 전환.
+ 대행사를 2차 페르소나가 아닌 early-access 대상으로 포지셔닝. */}
+
+ Partner Program · 신청하세요
+
+
+
+ 병원 마케팅 대행사의 전략 자문 . 진단 리포트와 경쟁사 벤치마크로 제안 품질을 높이고 수주 경쟁력을 강화합니다.
{['병원 마케팅 대행사', '콘텐츠 마케팅 Agency', '영상 마케팅 Agency', '광고 운영 대행사'].map((item, i) => (
diff --git a/src/components/UseCases.tsx b/src/components/UseCases.tsx
index 4d46382..5469689 100644
--- a/src/components/UseCases.tsx
+++ b/src/components/UseCases.tsx
@@ -15,8 +15,9 @@ export default function UseCases() {
Use Cases
-
- Infinite Marketing이 만드는 실질적인 변화를 확인해보세요!
+ {/* PART II 피봇: "만드는 실질적인 변화" (제품 중심) → 역할별 가치 (고객 중심)로 전환. */}
+
+ 프리미엄 병원과 전문 대행사가 INFINITH로 만드는 전략적 변화입니다.
@@ -29,11 +30,12 @@ export default function UseCases() {
className="glass-card p-10 md:p-12 bg-gradient-to-br from-blue-50/50 to-white border border-blue-100/30"
>
Premium Medical Business
+ {/* 체크리스트 재설계: 진단(Audit) · 전략(Planning) · 운영(Weekly KPI) 3축. */}
{[
- 'SEO 콘텐츠 자동 생산으로 검색 상위 노출 달성',
- '비용 부담 없이 고품질 영상 콘텐츠 대량 확대',
- '자연 검색 유입 증가로 인한 환자 전환율 상승'
+ '10분 진단으로 채널별 강·약점 파악, 투자 우선순위 즉시 도출',
+ '12개월 마케팅 로드맵과 콘텐츠 캘린더로 기획 리소스 70% 절감',
+ '주간 KPI 달성도 → 전략 조정 루프로 환자 전환 흐름 자율 최적화',
].map((item, i) => (
@@ -51,11 +53,12 @@ export default function UseCases() {
className="glass-card p-10 md:p-12 bg-gradient-to-br from-purple-50/50 to-white border border-purple-100/30"
>
Marketing Agency
+ {/* 대행사 가치축: 수주(Pitch) · 전략 품질(Strategy) · 포트폴리오 확장(Portfolio). */}
{[
- 'AI 기반 콘텐츠 제작 자동화로 생산성 극대화',
- '블로그 텍스트 기반 영상 제작 자동화로 리소스 절감',
- '다수 클라이언트 계정의 통합 운영 및 효율화'
+ '진단 리포트와 경쟁사 벤치마크로 제안서 품질 강화, 수주 경쟁력 상승',
+ '병원별 12개월 로드맵으로 전략 자문 일관성과 리텐션 확보',
+ '다수 병원 클라이언트 통합 대시보드로 운영 효율 극대화',
].map((item, i) => (
diff --git a/src/components/pricing/FAQ.tsx b/src/components/pricing/FAQ.tsx
new file mode 100644
index 0000000..40fecad
--- /dev/null
+++ b/src/components/pricing/FAQ.tsx
@@ -0,0 +1,134 @@
+/**
+ * FAQ — Pricing 페이지 하단 자주 묻는 질문.
+ *
+ * 계약 기반 영업 모델에 맞춰 카피 재작성 (plan 섹션 15-E 변형):
+ * - 온라인 결제 관련 질문 제거
+ * - 세금계산서 / 계약서 / 분원 4개↑ 커스텀 / 의료법 심의 / 강남언니 데이터 / 품질 보장 / Export 포함
+ *
+ * 인터랙션:
+ * - 단일 오픈(single-open) 아코디언 — `useState` 로 현재 열린 질문 ID 추적
+ * - 같은 버튼 재클릭 시 닫힘 (`prev === id ? null : id`)
+ * - motion/react AnimatePresence로 높이 전환 애니메이션
+ *
+ * DS 준수:
+ * - 아이콘은 lucide Plus/Minus 대신 단순 회전 ChevronDown(기본 lucide는 동반 가능 — outlined 단색)
+ * ※ DS "Filled Icons Only"는 '아이콘 면적 채움' 규칙이며 UI utility(chevron·arrow)는 허용 범위.
+ * 이미 CTA 버튼도 lucide ArrowRight를 사용 중이므로 일관.
+ */
+import { useState } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import { ChevronDown } from 'lucide-react';
+
+interface FaqItem {
+ id: string;
+ q: string;
+ a: string;
+}
+
+/**
+ * FAQ 항목 목록.
+ *
+ * ※ 해지·환불 관련 세부 정책(구 `refund`, `quality` 항목)은 추후 세부 정책 확정 후
+ * 재도입 예정. 현재는 계약·결제·운영 프로세스 + 의료법/데이터 수집 범위만 노출.
+ */
+const items: FaqItem[] = [
+ {
+ id: 'contract',
+ q: '계약은 어떻게 진행하나요?',
+ a: '상담 문의 → 요건·범위 확정 → 계약서 및 세금계산서 발행 순서로 진행됩니다. 계약 기간은 월·분기·연 단위 중 선택 가능하며, 연 계약 시 월 환산 20% 할인이 적용됩니다.',
+ },
+ {
+ id: 'payment',
+ q: '결제 방식은 어떻게 되나요?',
+ a: '온라인 카드 결제는 제공하지 않으며, 세금계산서 기반 계좌 입금으로 진행됩니다. 법인·개인사업자 모두 지원하며, 결제 조건(선불/후불)은 계약서에서 협의합니다.',
+ },
+ {
+ id: 'multi-clinic',
+ q: '분원이 4개 이상인데 어떻게 하나요?',
+ a: 'INTELLIGENCE+는 최대 3개 분원까지 통합 대시보드를 제공합니다. 4개 이상이거나 그룹사 단위로 분석이 필요하신 경우 커스텀 플랜 상담을 통해 데이터 구조·리포팅 범위를 맞춤 설계해 드립니다.',
+ },
+ {
+ id: 'medical-law',
+ q: '의료법 광고 심의와 충돌하지 않나요?',
+ a: 'INFINITH 리포트는 내부 마케팅 전략 문서이며 환자 대상 광고물이 아닙니다. 리포트 내용을 실제 광고·SNS 게시물로 활용하실 경우 의료광고심의위원회 심의가 별도로 필요하며, 이는 고객사(병원) 및 대행사 책임입니다.',
+ },
+ {
+ id: 'gangnamunni',
+ q: '강남언니 데이터는 어떻게 수집하나요?',
+ a: '강남언니 앱 및 웹의 공개 병원 페이지에서 병원명·리뷰 수·평점·대표 시술 등 공개 정보만 수집합니다. 개인 식별 정보(PII)는 일절 수집하지 않으며, 수집 주기와 범위는 서비스 약관에 명시되어 있습니다.',
+ },
+ {
+ id: 'data-export',
+ q: '리포트 데이터를 내보낼 수 있나요?',
+ a: '모든 플랜에서 PDF 내보내기를 지원합니다. INTELLIGENCE+는 병원 CI(로고·컬러) 반영 커스텀 템플릿을 제공합니다. JSON·CSV 다운로드는 상담 시 요청하실 수 있습니다.',
+ },
+];
+
+export default function FAQ() {
+ const [openId, setOpenId] = useState(null);
+
+ return (
+
+ {/* 헤드라인 */}
+
+
+ 자주 묻는 질문
+
+
+ 계약·결제·운영 관련해 자주 받는 질문을 모았습니다.
+
+
+
+ {/* 아코디언 리스트 — 콘텐츠 가독성을 위해 inner max-w-3xl, outer w-full */}
+
+ {items.map((item) => {
+ const isOpen = openId === item.id;
+ return (
+
+
setOpenId((prev) => (prev === item.id ? null : item.id))}
+ aria-expanded={isOpen}
+ aria-controls={`faq-panel-${item.id}`}
+ className="w-full flex items-start gap-4 px-6 py-5 text-left hover:bg-slate-50/60 transition-colors"
+ >
+
+ {item.q}
+
+
+
+
+
+ {isOpen && (
+
+
+ {item.a}
+
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/pricing/FeatureComparisonTable.tsx b/src/components/pricing/FeatureComparisonTable.tsx
new file mode 100644
index 0000000..cf08cca
--- /dev/null
+++ b/src/components/pricing/FeatureComparisonTable.tsx
@@ -0,0 +1,271 @@
+/**
+ * FeatureComparisonTable — 3-Tier 기능 세부 비교표.
+ *
+ * 설계 원칙:
+ * - Cell 값 3종:
+ * true → CheckFilled (accent) 포함
+ * false → CrossFilled (slate-300) 미포함
+ * string → 짧은 레이블 ("월 1회", "고급" 등)
+ * - INSIGHT가 전 채널을 커버하므로 차별화 축은 "빈도 · 깊이 · 전략 자산 · 범위"
+ * - 중앙 INTELLIGENCE 컬럼은 `bg-accent/5` 로 시각적 강조 (Tier Cards의 ring-accent와 톤 매칭)
+ * - 모바일에서는 가로 스크롤 (min-w-[720px])
+ *
+ * 카테고리 헤더는 배경 strip으로 섹션 경계를 분명히 하여 스캔 가능성(scannability) 확보.
+ */
+import { motion } from 'motion/react';
+import { CheckFilled, CrossFilled } from '../icons/FilledIcons';
+
+type CellValue = true | false | string;
+
+interface FeatureRow {
+ label: string;
+ insight: CellValue;
+ intelligence: CellValue;
+ intelligencePlus: CellValue;
+ /** 추가 설명(헬프 텍스트) — 필요 시 행 아래 회색 부연 */
+ hint?: string;
+}
+
+interface FeatureCategory {
+ name: string;
+ rows: FeatureRow[];
+}
+
+/**
+ * 기능 매트릭스 (plan 섹션 15-D 기준, INSIGHT 전채널화 반영)
+ *
+ * ⚠️ Tier Cards의 bullets과 "차별화 메시지"가 일치해야 신뢰가 유지됩니다.
+ * 카드 bullets를 수정할 경우 이 매트릭스도 함께 업데이트하세요.
+ */
+const categories: FeatureCategory[] = [
+ {
+ name: '분석',
+ rows: [
+ {
+ label: '월 리포트 수',
+ insight: '1회',
+ intelligence: '2회 + on-demand 4회',
+ intelligencePlus: '월 20회',
+ hint: 'on-demand는 영업일 기준 24시간 내 재실행',
+ },
+ {
+ label: '전 채널 커버리지',
+ insight: true,
+ intelligence: true,
+ intelligencePlus: true,
+ hint: '홈페이지 · 강남언니 · YouTube · Instagram · Facebook · 네이버 플레이스 · 블로그',
+ },
+ {
+ label: 'Vision AI (의료진·슬로건·인증 자동 추출)',
+ insight: false,
+ intelligence: true,
+ intelligencePlus: true,
+ },
+ {
+ label: '스크린샷 증거 기반 심층 분석',
+ insight: '기본',
+ intelligence: '심층',
+ intelligencePlus: '심층 + 변화 추적',
+ },
+ ],
+ },
+ {
+ name: '전략',
+ rows: [
+ {
+ label: '콘텐츠 플랜',
+ insight: '4주',
+ intelligence: '8주 + 주간 조정',
+ intelligencePlus: '12개월 + 월간 리뷰',
+ },
+ {
+ label: 'KPI 대시보드',
+ insight: '기본',
+ intelligence: '고급 (3/12개월 목표)',
+ intelligencePlus: '커스텀',
+ },
+ {
+ label: '브랜드 가이드 + 콘텐츠 필러',
+ insight: false,
+ intelligence: '5종',
+ intelligencePlus: '10종',
+ },
+ ],
+ },
+ {
+ name: '경쟁',
+ rows: [
+ {
+ label: '경쟁사 추적',
+ insight: '1개',
+ intelligence: '3개',
+ intelligencePlus: '10개',
+ },
+ {
+ label: '변동 알림',
+ insight: false,
+ intelligence: '주간',
+ intelligencePlus: '일간',
+ },
+ ],
+ },
+ {
+ name: '조직',
+ rows: [
+ {
+ label: '멀티 분원 통합 대시보드',
+ insight: false,
+ intelligence: false,
+ intelligencePlus: '최대 3개',
+ hint: '4개 이상은 커스텀 플랜 상담',
+ },
+ {
+ label: 'PDF 내보내기',
+ insight: true,
+ intelligence: true,
+ intelligencePlus: '병원 CI 커스텀 템플릿',
+ },
+ ],
+ },
+ {
+ name: '기타',
+ rows: [
+ {
+ label: '신규 기능 베타 우선 접근',
+ insight: false,
+ intelligence: false,
+ intelligencePlus: true,
+ },
+ {
+ label: '지원',
+ insight: '이메일',
+ intelligence: '이메일 + 화상 월 1회',
+ intelligencePlus: '전담 CSM',
+ },
+ ],
+ },
+];
+
+/** 셀 값 렌더러 — 3종(true/false/string)을 DS 아이콘 규칙으로 일관되게 표현 */
+function Cell({ value, isHighlight }: { value: CellValue; isHighlight?: boolean }) {
+ if (value === true) {
+ return (
+
+
+
+ );
+ }
+ if (value === false) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+ {value}
+
+ );
+}
+
+export default function FeatureComparisonTable() {
+ return (
+
+ {/* 섹션 헤드라인 */}
+
+
+ 플랜 세부 비교
+
+
+ 병원 규모·마케팅 예산에 맞는 플랜을 한눈에 비교해 보세요.
+
+
+
+ {/* 테이블 — 모바일 가로 스크롤 */}
+
+
+
+ {/* 상단 헤더 행 */}
+
+
기능
+
+
+
+
INTELLIGENCE
+
메인 타겟
+
+
+
INTELLIGENCE+
+
대형·멀티 분원
+
+
+
+ {/* 카테고리별 렌더링 */}
+ {categories.map((cat) => (
+
+ {/* 카테고리 스트립 */}
+
+
+ {/* 각 기능 행 */}
+ {cat.rows.map((row, idx) => (
+
+
+
+ {row.label}
+
+ {row.hint && (
+
+ {row.hint}
+
+ )}
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ ))}
+
+ ))}
+
+
+
+
+ {/* 보조 안내 */}
+
+ 모든 플랜은 계약 기반이며, 세부 조건은 상담 시 맞춤 설계됩니다.
+
+
+ );
+}
diff --git a/src/lib/classifyUrls.ts b/src/lib/classifyUrls.ts
new file mode 100644
index 0000000..47c02c6
--- /dev/null
+++ b/src/lib/classifyUrls.ts
@@ -0,0 +1,259 @@
+/**
+ * classifyUrls — 랜딩 MultiChannelInput 전용 URL 분류 유틸.
+ *
+ * 사용자가 textarea에 붙여넣은 여러 URL을 7개 채널(holder)로 분류합니다.
+ * - homepage · youtube · instagram · facebook · naverPlace · naverBlog · gangnamUnni · unknown
+ *
+ * 설계 결정:
+ * 1) 결정론적 regex 분류 (AI 추측 없음) — `supabase/functions/_shared/extractSocialLinks.ts`
+ * 의 패턴을 브라우저로 복제 (Vite가 Supabase 디렉토리를 번들에 끌고 들어가는 것을 방지).
+ * ⚠️ 두 파일의 패턴은 **동일하게 유지**되어야 합니다. SNS 플랫폼 URL 구조 변경 시 같이 수정하세요.
+ *
+ * 2) 우선순위: naverPlace / gangnamUnni (고유 도메인) → YouTube / Instagram / Facebook / naverBlog
+ * → 미매치이면서 `new URL()` 성공한 URL 중 **호스트네임이 SNS 도메인이 아닌 경우만** homepage.
+ * 호스트네임이 SNS인데 프로필 패턴 불일치 (예: instagram.com/p/XYZ, youtube.com/watch)는 `unknown`.
+ * ▸ "homepage" 필드가 분석 첫 진입점이라서, 여기에 잘못된 URL이 들어가면 전체 파이프라인이
+ * 잘못된 축으로 돌아갑니다. 따라서 방어적으로 분류합니다.
+ *
+ * 3) 값은 **원본 URL 문자열**을 그대로 보관 — backend `discover-channels`의 `manualChannels`는
+ * URL을 받아서 내부에서 handle을 추출하므로, 프론트에서 미리 handle로 변환하지 않습니다.
+ * (extractHandleFromUrl in supabase/functions/discover-channels/index.ts)
+ *
+ * 4) 중복 제거 — 공백/줄바꿈/쉼표로 분리 후, 정규화된 URL(소문자 + trailing slash 제거) 기준으로 dedup.
+ */
+
+export interface ClassifiedUrls {
+ homepage: string[];
+ youtube: string[];
+ instagram: string[];
+ facebook: string[];
+ naverPlace: string[];
+ naverBlog: string[];
+ gangnamUnni: string[];
+ /** 파싱 실패 or SNS 도메인이지만 프로필이 아닌 URL (포스트/비디오 등) */
+ unknown: string[];
+}
+
+/** SNS 호스트네임 집합 — 이들 도메인인데 프로필 패턴에 매치되지 않으면 `unknown`으로 강제. */
+const SNS_HOSTNAMES = new Set([
+ 'instagram.com',
+ 'www.instagram.com',
+ 'm.instagram.com',
+ 'youtube.com',
+ 'www.youtube.com',
+ 'm.youtube.com',
+ 'youtu.be',
+ 'facebook.com',
+ 'www.facebook.com',
+ 'm.facebook.com',
+ 'fb.com',
+ 'blog.naver.com',
+ 'm.blog.naver.com',
+ 'place.naver.com',
+ 'm.place.naver.com',
+ 'map.naver.com',
+ 'gangnamunni.com',
+ 'm.gangnamunni.com',
+ 'www.gangnamunni.com',
+]);
+
+/** Instagram 프로필이 아닌 경로 (포스트/릴/스토리 등) */
+const IG_SKIP = new Set([
+ 'p', 'reel', 'reels', 'stories', 'explore', 'accounts',
+ 'about', 'developer', 'legal', 'privacy', 'terms',
+]);
+
+/** Facebook 페이지가 아닌 경로 */
+const FB_SKIP = new Set([
+ 'sharer', 'share', 'login', 'help', 'pages', 'events', 'groups',
+ 'marketplace', 'watch', 'gaming', 'privacy', 'policies', 'tr',
+ 'dialog', 'plugins', 'photo', 'video', 'reel',
+]);
+
+/** YouTube 프로필 패턴: `@handle`, `channel/UC...`, `c/custom`, `user/...` */
+const YT_PROFILE_RE = /youtube\.com\/(?:@[a-zA-Z0-9._-]+|channel\/UC[a-zA-Z0-9_-]+|c\/[a-zA-Z0-9._-]+|user\/[a-zA-Z0-9._-]+)/i;
+
+/** Naver Place: m.place.naver.com/hospital/{id}, place.naver.com/hospital/{id}, map.naver.com/p/entry/place/{id} */
+const NAVER_PLACE_RE = /(?:m\.)?place\.naver\.com\/[a-z]+\/\d+|map\.naver\.com\/p\/entry\/place\/\d+/i;
+
+/** Naver Blog: blog.naver.com/{id} (또는 m.blog.naver.com) */
+const NAVER_BLOG_RE = /(?:m\.)?blog\.naver\.com\/[a-zA-Z0-9_-]+/i;
+
+/** 강남언니: gangnamunni.com/hospitals/{id-or-slug} */
+const GANGNAMUNNI_RE = /(?:m\.|www\.)?gangnamunni\.com\/hospitals?\/[a-zA-Z0-9_-]+/i;
+
+/** Instagram 프로필: instagram.com/{handle} — 포스트/릴 등은 아래 IG_SKIP로 걸러냄 */
+const IG_RE = /(?:www\.|m\.)?instagram\.com\/([a-zA-Z0-9._]+)\/?/i;
+
+/** Facebook 페이지: facebook.com/{page} */
+const FB_RE = /(?:www\.|m\.)?facebook\.com\/([a-zA-Z0-9._-]+)\/?/i;
+
+/**
+ * URL 문자열을 정규화해 중복 체크 키로 사용.
+ * - 소문자, trailing slash 제거, scheme 유지
+ * - URL 파싱 실패 시 원본 trim.
+ */
+function normalizeForDedup(url: string): string {
+ try {
+ const u = new URL(url);
+ const path = u.pathname.replace(/\/+$/, '');
+ return `${u.protocol}//${u.hostname.toLowerCase()}${path}${u.search}`.toLowerCase();
+ } catch {
+ return url.trim().toLowerCase();
+ }
+}
+
+/**
+ * 단일 URL 토큰을 분류해서 어느 버킷에 넣을지 결정.
+ * Returns [bucketKey, originalUrl] 또는 null (이미 중복).
+ */
+function classifySingle(
+ rawToken: string,
+): { bucket: keyof ClassifiedUrls; value: string } | null {
+ const token = rawToken.trim();
+ if (!token || token.length < 4) return null;
+
+ // URL 파싱 시도 — 실패하면 unknown
+ let parsed: URL;
+ try {
+ // scheme 없는 경우 https:// 추가 (e.g., "instagram.com/foo")
+ const withScheme = /^https?:\/\//i.test(token) ? token : `https://${token}`;
+ parsed = new URL(withScheme);
+ } catch {
+ return { bucket: 'unknown', value: token };
+ }
+
+ const hostname = parsed.hostname.toLowerCase();
+ const fullUrl = parsed.toString();
+
+ // 1) Naver Place (고유 도메인)
+ if (NAVER_PLACE_RE.test(fullUrl)) {
+ return { bucket: 'naverPlace', value: fullUrl };
+ }
+
+ // 2) 강남언니 (고유 도메인)
+ if (GANGNAMUNNI_RE.test(fullUrl)) {
+ return { bucket: 'gangnamUnni', value: fullUrl };
+ }
+
+ // 3) Naver Blog
+ if (NAVER_BLOG_RE.test(fullUrl)) {
+ return { bucket: 'naverBlog', value: fullUrl };
+ }
+
+ // 4) YouTube — 프로필 패턴만 매치
+ if (hostname.endsWith('youtube.com') || hostname === 'youtu.be') {
+ if (YT_PROFILE_RE.test(fullUrl)) {
+ return { bucket: 'youtube', value: fullUrl };
+ }
+ // youtube.com 인데 프로필 아님 (watch URL 등) → unknown
+ return { bucket: 'unknown', value: fullUrl };
+ }
+
+ // 5) Instagram
+ if (hostname.endsWith('instagram.com')) {
+ const m = fullUrl.match(IG_RE);
+ const firstSeg = m?.[1];
+ if (firstSeg && !IG_SKIP.has(firstSeg.toLowerCase())) {
+ return { bucket: 'instagram', value: fullUrl };
+ }
+ return { bucket: 'unknown', value: fullUrl };
+ }
+
+ // 6) Facebook
+ if (hostname.endsWith('facebook.com') || hostname.endsWith('fb.com')) {
+ const m = fullUrl.match(FB_RE);
+ const firstSeg = m?.[1];
+ if (firstSeg && !FB_SKIP.has(firstSeg.toLowerCase())) {
+ return { bucket: 'facebook', value: fullUrl };
+ }
+ return { bucket: 'unknown', value: fullUrl };
+ }
+
+ // 7) 남은 케이스: SNS 도메인이면 unknown (프로필 패턴 미매치), 아니면 homepage
+ if (SNS_HOSTNAMES.has(hostname)) {
+ return { bucket: 'unknown', value: fullUrl };
+ }
+
+ return { bucket: 'homepage', value: fullUrl };
+}
+
+/**
+ * 사용자 textarea 입력 → 7채널 분류 결과.
+ * 공백/쉼표/줄바꿈으로 분리된 각 토큰을 개별 URL로 간주합니다.
+ */
+export function classifyUrls(input: string): ClassifiedUrls {
+ const result: ClassifiedUrls = {
+ homepage: [],
+ youtube: [],
+ instagram: [],
+ facebook: [],
+ naverPlace: [],
+ naverBlog: [],
+ gangnamUnni: [],
+ unknown: [],
+ };
+
+ if (!input || typeof input !== 'string') return result;
+
+ // 공백/쉼표/줄바꿈으로 분리
+ const tokens = input.split(/[\s,]+/).map((t) => t.trim()).filter(Boolean);
+
+ // 버킷별 중복 체크 — 같은 URL이 textarea에 두 번 나와도 한 번만
+ const seen: Record> = {
+ homepage: new Set(),
+ youtube: new Set(),
+ instagram: new Set(),
+ facebook: new Set(),
+ naverPlace: new Set(),
+ naverBlog: new Set(),
+ gangnamUnni: new Set(),
+ unknown: new Set(),
+ };
+
+ for (const token of tokens) {
+ const classified = classifySingle(token);
+ if (!classified) continue;
+
+ const key = normalizeForDedup(classified.value);
+ if (seen[classified.bucket].has(key)) continue;
+ seen[classified.bucket].add(key);
+ result[classified.bucket].push(classified.value);
+ }
+
+ return result;
+}
+
+/**
+ * 채널 중 하나라도 분석 대상이 있는지 — 분석 시작 버튼 활성화 조건.
+ * homepage·SNS 중 최소 1건이 있으면 true. unknown은 제외.
+ */
+export function hasAnalyzableChannels(classified: ClassifiedUrls): boolean {
+ return (
+ classified.homepage.length > 0 ||
+ classified.youtube.length > 0 ||
+ classified.instagram.length > 0 ||
+ classified.facebook.length > 0 ||
+ classified.naverPlace.length > 0 ||
+ classified.naverBlog.length > 0 ||
+ classified.gangnamUnni.length > 0
+ );
+}
+
+/**
+ * 분석 파이프라인의 "primary URL" 결정.
+ * 홈페이지가 있으면 최우선, 없으면 검출된 첫 SNS URL 반환.
+ * `discover-channels` Edge Function의 `url` 필드 (필수)로 사용됩니다.
+ */
+export function pickPrimaryUrl(classified: ClassifiedUrls): string | null {
+ if (classified.homepage[0]) return classified.homepage[0];
+ // 홈페이지 없으면 SNS 중 아무거나 대표값으로
+ const fallback =
+ classified.naverPlace[0] ||
+ classified.instagram[0] ||
+ classified.youtube[0] ||
+ classified.facebook[0] ||
+ classified.naverBlog[0] ||
+ classified.gangnamUnni[0];
+ return fallback || null;
+}
diff --git a/src/lib/contact.ts b/src/lib/contact.ts
new file mode 100644
index 0000000..379fef9
--- /dev/null
+++ b/src/lib/contact.ts
@@ -0,0 +1,23 @@
+/**
+ * contact — 회사 연락처 정보 중앙 관리.
+ *
+ * PricingPage / Navbar / LoginPage / CTA 등 여러 파일에서
+ * `contact@o2o.kr`과 mailto 쿼리를 중복 하드코딩하던 것을 일원화했습니다.
+ *
+ * 변경 이력 (PART III 피봇):
+ * - "Free Report" 진입점 제거 → 모든 리드를 문의하기 단일 채널로 통일
+ * - 계약 기반 B2B 영업 모델 반영 (온라인 결제/무료 체험 없음)
+ */
+
+export const CONTACT_EMAIL = 'contact@o2o.kr';
+
+/**
+ * INFINITH 태그를 자동으로 prefix한 mailto 링크 생성.
+ * @param subject 이메일 제목에 들어갈 문구 (자동으로 "[INFINITH] " prefix 추가됨).
+ *
+ * 예: buildContactMailto("도입 문의") → mailto:contact@o2o.kr?subject=%5BINFINITH%5D%20%EB%8F%84%EC%9E%85%20%EB%AC%B8%EC%9D%98
+ */
+export function buildContactMailto(subject: string): string {
+ const normalizedSubject = subject.trim() || '문의';
+ return `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent(`[INFINITH] ${normalizedSubject}`)}`;
+}
diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts
index 36b50f5..edeb2cf 100644
--- a/src/lib/supabase.ts
+++ b/src/lib/supabase.ts
@@ -121,17 +121,40 @@ export async function scrapeWebsite(url: string, clinicName?: string) {
// ─── Pipeline V2 API Functions ───
+/**
+ * 사용자가 직접 붙여넣은 채널 URL 묶음.
+ * 전달 시 Edge Function이 해당 플랫폼의 Firecrawl discovery를 스킵하고
+ * 제공된 URL을 `verified_channels`에 직접 주입합니다.
+ */
+export interface ManualChannels {
+ youtube?: string[];
+ instagram?: string[];
+ facebook?: string[];
+ naverPlace?: string[];
+ naverBlog?: string[];
+ gangnamUnni?: string[];
+}
+
/**
* Phase 1: Discover & verify social channels from website URL.
* Returns verified handles + reportId for subsequent phases.
+ *
+ * @param url 병원 홈페이지 URL (필수).
+ * @param clinicName 병원명 (선택) — 수동 입력 또는 clinic_registry 매칭용.
+ * @param manualChannels (선택) 사용자가 직접 붙여넣은 채널 URL. 존재하는 플랫폼은
+ * Firecrawl discovery를 스킵하고 사용자 제공 값을 우선 사용합니다.
*/
-export async function discoverChannels(url: string, clinicName?: string) {
+export async function discoverChannels(
+ url: string,
+ clinicName?: string,
+ manualChannels?: ManualChannels,
+) {
const response = await fetch(
`${supabaseUrl}/functions/v1/discover-channels`,
{
method: "POST",
headers: fnHeaders(),
- body: JSON.stringify({ url, clinicName }),
+ body: JSON.stringify({ url, clinicName, manualChannels }),
}
);
diff --git a/src/main.tsx b/src/main.tsx
index 6517381..3e04bb9 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router';
import App from './App.tsx';
import LandingPage from './pages/LandingPage.tsx';
+import PricingPage from './pages/PricingPage.tsx';
import AnalysisLoadingPage from './pages/AnalysisLoadingPage.tsx';
import ReportPage from './pages/ReportPage.tsx';
import MarketingPlanPage from './pages/MarketingPlanPage.tsx';
@@ -13,6 +14,7 @@ import PerformancePage from './pages/PerformancePage.tsx';
import DataValidationPage from './pages/DataValidationPage.tsx';
import ClinicProfilePage from './pages/ClinicProfilePage.tsx';
import ApiDashboardPage from './pages/ApiDashboardPage.tsx';
+import LoginPage from './pages/LoginPage.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
@@ -21,6 +23,8 @@ createRoot(document.getElementById('root')!).render(
}>
} />
+ } />
+ } />
} />
} />
} />
diff --git a/src/pages/AnalysisLoadingPage.tsx b/src/pages/AnalysisLoadingPage.tsx
index 4e7f3fc..2cf7c29 100644
--- a/src/pages/AnalysisLoadingPage.tsx
+++ b/src/pages/AnalysisLoadingPage.tsx
@@ -57,7 +57,12 @@ export default function AnalysisLoadingPage() {
const navigate = useNavigate();
const location = useLocation();
const { reportId: urlReportId } = useParams<{ reportId?: string }>();
- const url = (location.state as { url?: string })?.url;
+ const locState = (location.state as {
+ url?: string;
+ manualChannels?: import('../lib/supabase').ManualChannels;
+ }) ?? {};
+ const url = locState.url;
+ const manualChannels = locState.manualChannels;
const hasStarted = useRef(false);
const phaseIndex = PHASE_STEPS.findIndex(s => s.key === phase);
@@ -76,7 +81,9 @@ export default function AnalysisLoadingPage() {
if (startPhase === 'discovering') {
if (!startUrl) throw new Error('No URL provided');
setPhase('discovering');
- const discovery = await discoverChannels(startUrl);
+ // manualChannels가 있으면 Edge Function이 Firecrawl discovery를 스킵하고
+ // 사용자 제공 URL을 직접 verified_channels에 주입합니다.
+ const discovery = await discoverChannels(startUrl, undefined, manualChannels);
if (!discovery.success) throw new Error(discovery.error || 'Channel discovery failed');
reportId = discovery.reportId;
clinicId = discovery.clinicId;
@@ -146,7 +153,7 @@ export default function AnalysisLoadingPage() {
setErrorDomain((err as { domain?: string }).domain || null);
}
}
- }, [navigate]);
+ }, [navigate, manualChannels]);
// Retry from the current failed phase
const handleRetry = useCallback(() => {
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..f0b1db5
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,130 @@
+/**
+ * LoginPage — 계약 병원 전용 로그인 (스캐폴딩).
+ *
+ * 현재 상태:
+ * - UI·라우트 스캐폴딩만 제공합니다. 실제 Supabase Auth 연동은 후속 작업입니다.
+ * - 제출 시 안내 메시지만 표시하고, 리드는 문의하기로 유도합니다.
+ *
+ * 향후 작업:
+ * - Supabase `signInWithPassword` 연동
+ * - 세션 훅 (`useSession`) + ProtectedRoute 적용
+ * - 로그인 후 대시보드 리다이렉트
+ */
+import { useState, type FormEvent } from 'react';
+import { Link } from 'react-router';
+import { ArrowRight } from 'lucide-react';
+import { buildContactMailto } from '../lib/contact';
+
+export default function LoginPage() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [notice, setNotice] = useState(null);
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ setNotice(
+ '로그인 기능은 준비 중입니다. 계약을 희망하시는 경우 contact@o2o.kr 로 문의 주세요.',
+ );
+ };
+
+ return (
+
+
+ {/* 헤드라인 */}
+
+
+ INFINITH
+
+
+ 계약 병원 전용 로그인
+
+
+ 계약 완료 시 별도 제공되는 자격 정보로 로그인하실 수 있습니다.
+
+
+
+ {/* 준비중 배너 */}
+
+
+ 현재 로그인 기능은 준비 중입니다.
+
+ 신규 도입 문의는 하단 "문의하기"를 이용해 주세요.
+
+
+
+ {/* 폼 카드 */}
+
+
+ {/* 하단 문의하기 */}
+
+
+
+ );
+}
diff --git a/src/pages/PricingPage.tsx b/src/pages/PricingPage.tsx
new file mode 100644
index 0000000..2e1d6b6
--- /dev/null
+++ b/src/pages/PricingPage.tsx
@@ -0,0 +1,451 @@
+/**
+ * PricingPage — INFINITH Product 1.0 가격 안내 페이지.
+ *
+ * 구조 (plan 섹션 15-A):
+ * 1. Hero (타이틀 + 한 줄 태그라인)
+ * 2. Billing Toggle (월간 / 연간 20% 할인)
+ * 3. 3 Tier Cards (INSIGHT / INTELLIGENCE⭐ / INTELLIGENCE+)
+ * 4. Feature Comparison Table ← Step 3에서 구현
+ * 5. Free Trial 강조 박스
+ * 6. Launch Promotion 배너
+ * 7. FAQ ← Step 3에서 구현
+ * 8. Enterprise Contact CTA
+ *
+ * 유입 추적:
+ * - `?from=header | footer | cta | hero` 파라미터 읽어 console.log
+ * (analytics 연동은 Supabase `analytics_events` 테이블 도입 후 후속 작업)
+ *
+ * 핵심 데이터 소스:
+ * - src/data/pricingTiers.ts ← (이 파일 내부에 임시 정의, Step 3에서 분리 고려)
+ */
+import React, { useEffect, useState } from 'react';
+import { useSearchParams } from 'react-router';
+import { motion } from 'motion/react';
+import { ArrowRight } from 'lucide-react';
+import {
+ CheckFilled,
+ RocketFilled,
+ BoltFilled,
+ PrismFilled,
+} from '../components/icons/FilledIcons';
+import Badge from '../components/Badge';
+import FeatureComparisonTable from '../components/pricing/FeatureComparisonTable';
+import FAQ from '../components/pricing/FAQ';
+import { buildContactMailto } from '../lib/contact';
+
+// ─── Tier 데이터 정의 ──────────────────────────────────────────────
+// plan 섹션 2·5 기준. Feature Comparison Table(Step 3)에서도 재사용 예정.
+type TierId = 'insight' | 'intelligence' | 'intelligence-plus';
+
+interface Tier {
+ id: TierId;
+ name: string;
+ tagline: string;
+ monthlyKRW: number; // 원화 (월 단가 · 계약 기준)
+ annualMonthlyKRW: number; // 원화 (연 계약 시 월 환산)
+ annualTotalKRW: number; // 원화 (연 계약 총액)
+ isPopular?: boolean;
+ /** 모든 Tier는 계약 기반 영업 — 온라인 결제 없음. 상담 문의 mailto로 통일. */
+ ctaLabel: string;
+ bullets: string[];
+ footnote?: string;
+}
+
+const tiers: Tier[] = [
+ {
+ id: 'insight',
+ name: 'INSIGHT',
+ tagline: '매월 1번, 병원의 온라인 좌표를 점검하세요',
+ monthlyKRW: 490_000,
+ annualMonthlyKRW: 390_000,
+ annualTotalKRW: 4_700_000,
+ ctaLabel: '상담 문의',
+ bullets: [
+ '월 1회 분석 리포트',
+ '전 채널 분석 (홈페이지 · 강남언니 · YouTube · Instagram · Facebook · 네이버 플레이스 · 블로그)',
+ '4주 콘텐츠 플랜',
+ '경쟁사 추적 1개',
+ 'PDF 내보내기',
+ ],
+ footnote: '신규 개업의 · 1인 의원 추천',
+ },
+ {
+ id: 'intelligence',
+ name: 'INTELLIGENCE',
+ tagline: '경쟁사가 지금 무엇을 바꾸는지, 월 2번 확인하세요',
+ monthlyKRW: 1_490_000,
+ annualMonthlyKRW: 1_190_000,
+ annualTotalKRW: 14_300_000,
+ isPopular: true,
+ ctaLabel: '상담 문의',
+ bullets: [
+ '월 2회 + on-demand 재실행 (월 4회까지)',
+ '8주 콘텐츠 캘린더 + 주간 KPI 기반 조정',
+ 'Vision AI (의료진·슬로건·인증 자동 추출)',
+ '경쟁사 추적 3개 · 주간 변동 알림',
+ 'KPI 대시보드 (3/12개월 목표)',
+ '브랜드 가이드 + 콘텐츠 필러 5종',
+ '스크린샷 증거 기반 심층 리포트',
+ ],
+ footnote: '중형 성형외과 · 메인 타겟',
+ },
+ {
+ id: 'intelligence-plus',
+ name: 'INTELLIGENCE+',
+ tagline: '매일 변하는 시장에 즉시 대응하세요',
+ monthlyKRW: 3_990_000,
+ annualMonthlyKRW: 3_190_000,
+ annualTotalKRW: 38_300_000,
+ ctaLabel: '상담 문의',
+ bullets: [
+ '월 20회 분석 리포트 (주간 자동 + on-demand)',
+ '12개월 로드맵 + 월간 전략 리뷰',
+ '최대 3개 분원 통합 대시보드',
+ '경쟁사 추적 10개 · 일간 변동 모니터링',
+ '브랜드 가이드 + 콘텐츠 필러 10종',
+ '커스텀 리포트 템플릿 (병원 CI 반영)',
+ '신규 기능 베타 우선 접근',
+ ],
+ footnote: '대형 · 멀티 분원 병원',
+ },
+];
+
+// ─── 가격 포맷터 ──────────────────────────────────────────────────
+// 149_0000원 → "149만원" 포맷. 1000원 단위까지는 표기 안 함 (B2B 가격은 만원 단위)
+function formatKRW(amount: number): string {
+ const man = amount / 10_000;
+ // 소수점 없는 정수 표기 우선. 999,999 아래면 만원, 이상이면 억 단위까지 확장 가능.
+ if (Number.isInteger(man)) return `${man.toLocaleString('ko-KR')}만원`;
+ return `${man.toLocaleString('ko-KR', { maximumFractionDigits: 1 })}만원`;
+}
+
+// ─── Billing Toggle 컴포넌트 ──────────────────────────────────────
+interface BillingToggleProps {
+ value: 'monthly' | 'annual';
+ onChange: (v: 'monthly' | 'annual') => void;
+}
+
+const BillingToggle: React.FC = ({ value, onChange }) => {
+ return (
+
+ onChange('monthly')}
+ className={`px-5 py-2 rounded-full text-sm font-semibold transition-all ${
+ value === 'monthly'
+ ? 'bg-primary-900 text-white shadow'
+ : 'text-slate-600 hover:text-primary-900'
+ }`}
+ >
+ 월간 결제
+
+ onChange('annual')}
+ className={`px-5 py-2 rounded-full text-sm font-semibold transition-all flex items-center gap-2 ${
+ value === 'annual'
+ ? 'bg-primary-900 text-white shadow'
+ : 'text-slate-600 hover:text-primary-900'
+ }`}
+ >
+ 연간 결제
+
+ 20% 할인
+
+
+
+ );
+};
+
+// ─── Tier Card 컴포넌트 ──────────────────────────────────────────
+interface TierCardProps {
+ tier: Tier;
+ billing: 'monthly' | 'annual';
+ onSelect: (tier: Tier) => void;
+}
+
+const TierCard: React.FC = ({ tier, billing, onSelect }) => {
+ const price = billing === 'monthly' ? tier.monthlyKRW : tier.annualMonthlyKRW;
+ const isAnnual = billing === 'annual';
+
+ return (
+
+ {/* Popular 배지 */}
+ {tier.isPopular && (
+
+
+
+ )}
+
+ {/* 이름 + 태그라인 */}
+
+
{tier.name}
+
{tier.tagline}
+
+
+ {/* 가격 */}
+
+
+
+ {formatKRW(price)}
+
+ /월
+
+ {isAnnual && (
+
+ 연 {formatKRW(tier.annualTotalKRW)} · 20% 절약
+
+ )}
+ {!isAnnual && (
+
+ 연 결제 시 월 {formatKRW(tier.annualMonthlyKRW)}
+
+ )}
+
+
+ {/* CTA — DS Primary: gradient + rounded-full (pill) */}
+ onSelect(tier)}
+ className={`w-full px-8 py-3.5 rounded-full font-medium text-sm text-white transition-shadow duration-150 shadow-md hover:shadow-xl flex items-center justify-center gap-2 group bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:from-[#AF90FF] hover:to-[#AF90FF] active:scale-[0.98] mb-6 ${
+ tier.isPopular ? 'shadow-lg' : ''
+ }`}
+ >
+ {tier.ctaLabel}
+
+
+
+ {/* Bullet points — DS FilledIcon(CheckFilled) */}
+
+ {tier.bullets.map((bullet, i) => (
+
+
+ {bullet}
+
+ ))}
+
+
+ {/* Footnote */}
+ {tier.footnote && (
+
+ {tier.footnote}
+
+ )}
+
+ );
+};
+
+// ─── 먼저 문의하기 강조 카드 ─────────────────────────────────────
+// DS 레퍼런스: CTA 카드 배경에 3-stop warm gradient (Section 2.2)
+// + 상단 filled 아이콘 squircle + Primary/Secondary pill 버튼 병렬
+//
+// PART III 피봇: "첫 리포트 무료 / 카드 등록 불필요" 표현 삭제 → 계약 기반 영업으로 일원화.
+function ContactFirstBox() {
+ return (
+
+ {/* 상단 아이콘 squircle — DS filled icon 규칙 */}
+
+
+
+
+
+ 먼저 대화부터 시작하세요
+
+
+ 계약 전에 병원 규모·마케팅 현황을 공유해 주시면, 전담 담당자가 적합한 플랜과
+ 리포트 샘플을 정리해 회신 드립니다.
+
+
+ {/* DS 듀얼 버튼: Primary + Secondary — 모두 rounded-full (pill) */}
+
+
+ );
+}
+
+// ─── Launch Promotion 배너 ───────────────────────────────────────
+// DS: 🎁 이모지 금지(Principle: No Emoji) → RocketFilled 대체
+// dark primary gradient card + Secondary pill 버튼
+function PromotionBanner() {
+ return (
+
+ {/* Filled icon squircle — 🎁 대체 */}
+
+
+
+
+
+ 런칭 프로모션 · 선착순 20병원
+
+
+ INTELLIGENCE · INTELLIGENCE+ 3개월 30% 할인
+
+
+
+ 혜택 문의
+
+
+ );
+}
+
+// ─── Enterprise Contact CTA ──────────────────────────────────────
+// DS: outlined 패턴은 DS에 없음 → Primary pill(gradient + rounded-full)
+function EnterpriseContact() {
+ return (
+ // outer: w-full로 섹션 정렬 / inner 콘텐츠만 max-w-2xl로 가독성 유지
+
+
+
+ 더 많은 분원, 커스텀 플랜이 필요하신가요?
+
+
+ 4개 이상 분원을 운영하시거나 데이터 커스터마이징이 필요한 경우, 전담 담당자가 상담해 드립니다.
+
+
+ 커스텀 플랜 문의
+
+
+
+
+ );
+}
+
+// ─── PricingPage 본체 ────────────────────────────────────────────
+export default function PricingPage() {
+ const [searchParams] = useSearchParams();
+ const [billing, setBilling] = useState<'monthly' | 'annual'>('annual');
+
+ // 유입 소스 추적 — 추후 Supabase `analytics_events` 테이블로 전송
+ useEffect(() => {
+ const from = searchParams.get('from');
+ if (from) {
+ // TODO(analytics): Supabase analytics_events insert
+ console.info(`[pricing] referred from: ${from}`);
+ }
+ }, [searchParams]);
+
+ /**
+ * Tier 선택 핸들러 — 계약 기반 영업.
+ * 온라인 결제 없이 모든 Tier를 상담 문의 mailto로 통일.
+ * Subject에 Tier 이름을 넣어 영업팀이 유입 경로를 구분.
+ */
+ const handleTierSelect = (tier: Tier) => {
+ window.location.href = buildContactMailto(`${tier.name} 플랜 상담 문의`);
+ };
+
+ return (
+
+ {/* Background — 랜딩 Hero와 톤 통일 */}
+
+
+ {/* ── Section 1 · Hero ─────────────────────────── */}
+
+
+
+
+ Strategic Planning,
+ At Your Scale.
+
+
+ 병원 규모에 맞는 전략 파트너를 선택하세요. 계약·결제·운영 조건은 상담에서 맞춤 설계됩니다.
+
+
+
+ {/* ── Section 2 · Billing Toggle ────────────── */}
+
+
+
+
+
+ {/* ── Section 3 · 3 Tier Cards ─────────────────── */}
+
+
+ {tiers.map((tier) => (
+
+ ))}
+
+
+
+ {/* ── Section 4 · Feature Comparison Table ─── */}
+
+
+ {/* ── Section 5 · 먼저 문의하기 강조 ───────────── */}
+ {/* outer max-w-7xl로 Tier Cards 섹션과 동일 정렬 (반응형 자동 축소) */}
+
+
+ {/* ── Section 6 · Launch Promotion ───────────── */}
+
+
+ {/* ── Section 7 · FAQ ─────────────────────────── */}
+
+
+ {/* ── Section 8 · Enterprise Contact ─────────── */}
+
+
+ );
+}
diff --git a/supabase/functions/discover-channels/index.ts b/supabase/functions/discover-channels/index.ts
index a84a715..52461ff 100644
--- a/supabase/functions/discover-channels/index.ts
+++ b/supabase/functions/discover-channels/index.ts
@@ -93,6 +93,19 @@ function registryToVerifiedChannels(reg: RegistryRow): import("../_shared/verify
interface DiscoverRequest {
url: string;
clinicName?: string;
+ /**
+ * 사용자가 랜딩 MultiChannelInput에서 직접 붙여넣은 채널 URL 묶음.
+ * 존재하는 플랫폼은 Firecrawl/Perplexity discovery 결과보다 우선순위 높게 병합됩니다.
+ * naverPlace는 verifyAllHandles로 검증되지 않으므로 결과에 직접 주입합니다.
+ */
+ manualChannels?: {
+ youtube?: string[];
+ instagram?: string[];
+ facebook?: string[];
+ naverPlace?: string[];
+ naverBlog?: string[];
+ gangnamUnni?: string[];
+ };
}
function extractHandle(raw: string, platform: string): string | null {
@@ -154,7 +167,7 @@ Deno.serve(async (req) => {
}
try {
- const { url, clinicName: inputClinicName } = (await req.json()) as DiscoverRequest;
+ const { url, clinicName: inputClinicName, manualChannels } = (await req.json()) as DiscoverRequest;
if (!url) {
return new Response(
JSON.stringify({ error: "URL is required" }),
@@ -776,9 +789,32 @@ Deno.serve(async (req) => {
// ═══════════════════════════════════════════
// STAGE C: Merge ALL sources + Verify
+ //
+ // manualChannels는 landing MultiChannelInput에서 사용자가 직접 붙여넣은 URL입니다.
+ // Firecrawl/Perplexity 결과보다 **우선순위 최상위**로 병합합니다 (mergeSocialLinks는
+ // 중복 제거 시 첫 번째 인자의 값을 유지).
+ // naverPlace / gangnamUnni는 extractSocialLinks가 지원하지 않으므로 아래 별도 처리.
// ═══════════════════════════════════════════
- const merged = mergeSocialLinks(linkHandles, firecrawlHandles, buttonHandles, apiHandles);
+ // 사용자 제공 URL → extractSocialLinks 포맷 (instagram/youtube/facebook/naverBlog/tiktok/kakao만)
+ const manualSocialUrls: string[] = manualChannels
+ ? [
+ ...(manualChannels.youtube || []),
+ ...(manualChannels.instagram || []),
+ ...(manualChannels.facebook || []),
+ ...(manualChannels.naverBlog || []),
+ ]
+ : [];
+ const manualHandles = manualSocialUrls.length > 0 ? extractSocialLinks(manualSocialUrls) : null;
+
+ // 사용자가 강남언니 URL을 제공하면 기존 힌트를 덮어씀
+ if (manualChannels?.gangnamUnni?.[0]) {
+ gangnamUnniHintUrl = manualChannels.gangnamUnni[0];
+ }
+
+ const merged = manualHandles
+ ? mergeSocialLinks(manualHandles, linkHandles, firecrawlHandles, buttonHandles, apiHandles)
+ : mergeSocialLinks(linkHandles, firecrawlHandles, buttonHandles, apiHandles);
const cleanHandles = {
instagram: [...new Set(merged.instagram.map(h => extractHandle(h, 'instagram')).filter((h): h is string => h !== null))],
@@ -801,6 +837,17 @@ Deno.serve(async (req) => {
cleanHandles, resolvedName, gangnamUnniHintUrl,
);
+ // 사용자 제공 Naver Place URL이 있으면 직접 주입 (verifyAllHandles는 Place를 다루지 않음).
+ // placeId는 URL 말미의 숫자 ID를 추출 (e.g. ".../hospital/11709005" → "11709005").
+ if (manualChannels?.naverPlace?.[0]) {
+ const placeUrl = manualChannels.naverPlace[0];
+ const placeIdMatch = placeUrl.match(/\/(\d+)\/?(?:\?|$)/);
+ (verified as VerifiedChannels).naverPlace = {
+ url: placeUrl,
+ placeId: placeIdMatch?.[1],
+ };
+ }
+
// ═══════════════════════════════════════════
// Save to DB (supabase client reused from registry check above)
// ═══════════════════════════════════════════