feat: PART III 랜딩 피봇 — 다채널 URL 입력 + Strategic Planner 포지셔닝

- MultiChannelInput: URL 뭉치 붙여넣기 → classifyUrls로 7개 채널 자동 분류
  (homepage·YouTube·Instagram·Facebook·네이버플레이스·블로그·강남언니)
  · "Analyze" pill 버튼 복원 + variant별 색 분기 (hero=dark brand,
    cta=#fff3eb→#e4cfff→#f5f9ff warm 3-stop)
  · placeholder 중앙 정렬 + "한 줄씩" 규칙 제거 (유연 파싱 노출)

- Navbar: Free Report CTA 제거 → Login + 문의하기 (contact@o2o.kr) duo
- LoginPage: 계약 고객용 스캐폴딩 페이지 신규 추가
- PricingPage: 계약 기반 영업 반영, FAQ에서 해지·환불 항목 제거
  (세부 정책 미확정 → 후속 추가)

- Landing 카피 Strategic Planner 포지셔닝 피봇:
  · Hero sub: "10분 진단 → 12개월 전략 설계"
  · Solution AGDP: Audit / Generation / Direction / Planning 재해석
  · Modules: Intelligence + Planning Available, 나머지 Coming Soon 정직화
  · TargetAudience: 전략 파트너 / 전략 자문 + Partner Program 신청 waitlist
  · Problems: 콘텐츠 소진 / 경쟁사 분석 부재 / 데이터 부족 3축
  · UseCases: 진단·전략·KPI(Medical) · 수주·자문·포트폴리오(Agency)

- discover-channels Edge Function: manualChannels 수용 — 사용자 붙여넣은
  URL이 Firecrawl 스크래핑보다 우선, naverPlace/gangnamUnni 직접 주입

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-04-17 09:01:41 +09:00
parent 517075b3ef
commit 938ebacbf9
20 changed files with 1774 additions and 146 deletions

View File

@ -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,,,,,
1 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
2 바노바기성형외과 프리미엄/하이타깃 후보 강남 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개
3 뷰성형외과 프리미엄/하이타깃 후보 강남 뷰성형외과 역삼센터(역삼) 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 9.5 18840 19007 수술실 CCTV;마취과 전문의 상주;분야별 공동 진료;의료진 실명 공개;야간진료;여성 의사 진료;응급 대응 체계;시술 후 관리;입원 시설;전용 휴식 공간 눈성형;코성형;안면윤곽/양악;가슴성형;지방성형;필러;보톡스;피부리프팅;기타 최순우 https://blog.naver.com/viewclinicps https://m.place.naver.com/hospital/11709005 리뷰 776개
4 아이디병원 프리미엄/하이타깃 후보 강남 아이디병원 별관(역삼) 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
5 그랜드성형외과 프리미엄/하이타깃 후보 강남 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
6 원진성형외과 프리미엄/하이타깃 후보 강남 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 마노성형외과 프리미엄/하이타깃 후보 서초 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
72 에스엠성형외과 프리미엄/하이타깃 후보 서초 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
73 서울미작성형외과 프리미엄/하이타깃 후보 서초 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
74 더원성형외과 프리미엄/하이타깃 후보 서초 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

View File

@ -1,14 +1,27 @@
import { useState } from 'react';
import { useNavigate } from 'react-router';
/**
* CTA — 랜딩 페이지 하단 최종 전환 섹션.
*
* PART III 피봇:
* - Hero와 동일한 입력 로직(url state/navigate)을 중복 구현하던 문제 해소.
* 이제 `<MultiChannelInput variant="cta" />` 한 줄로 통일.
* - "무료 진단" 언급 삭제 — 계약 기반 모델 반영.
* - 헤드라인은 전략 소유권 메시지로 피봇 ("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.
</motion.h2>
<motion.p
@ -34,7 +47,7 @@ export default function CTA() {
transition={{ duration: 0.6, delay: 0.1 }}
className="text-lg md:text-xl text-purple-200 mb-10 max-w-2xl mx-auto font-light"
>
URL 하나로 시작하는 완벽한 마케팅 자동화. 지금 바로 무료 진단을 받아보세요.
URL 하나로 시작하는 AI 마케팅 전략 플래너.
</motion.p>
<motion.div
@ -42,27 +55,19 @@ export default function CTA() {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex flex-col items-center justify-center gap-4 max-w-md mx-auto"
className="w-full"
>
<input
type="url"
placeholder="Enter Your URL"
value={url}
onChange={(e) => 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"
/>
<button
onClick={handleAnalyze}
disabled={!url.trim()}
className={`w-full px-10 py-4 text-lg font-medium text-white rounded-full transition-shadow duration-150 shadow-xl hover:shadow-2xl 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] ${!url.trim() ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Analyze
<ArrowRight className="w-5 h-5 group-hover:translate-x-1" />
</button>
<p className="text-sm text-purple-200/80 mt-2">
네이버 블로그, 플레이스, 소셜미디어 종합 분석 리포트 받아보기
</p>
<MultiChannelInput variant="cta" onAnalyze={handleAnalyze} />
{/* 보조 CTA — 가격 플랜 */}
<div className="mt-6 flex justify-center">
<Link
to="/pricing?from=cta"
className="inline-flex items-center gap-2 text-sm font-semibold text-purple-200 hover:text-white transition-colors underline-offset-4 hover:underline"
>
가격 플랜 보기 →
</Link>
</div>
</motion.div>
</div>
</section>

View File

@ -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"
>
<PrismFilled size={16} className="text-accent" />
<span className="text-sm font-medium text-slate-700">Agentic AI Marketing Automation for Premium Medical Business & Marketing Agency</span>
<span className="text-sm font-medium text-slate-700">AI Marketing Strategist · Built for Premium Clinics</span>
</motion.div>
<motion.h1
@ -38,39 +41,30 @@ export default function Hero() {
<span className="text-gradient">Marketing Engine.</span>
</motion.h1>
{/* 서브 카피 — PART II 피봇: Strategic Planner 포지셔닝
▸ 이전 "Marketing that learns... 쓸수록 더 정교해지는..." 은 풀스택 자동화 약속이
Generation/Distribution Mock 상태와 맞지 않았음.
▸ Phase 1-4 (Discover→Collect→Report→Plan) 실제 구현 범위에 정직하게 일치시킴. */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-lg md:text-xl text-slate-600 mb-10 max-w-3xl mx-auto leading-relaxed"
>
Marketing that learns, improves, and accelerates — automatically. <br className="hidden md:block"/>
<span className="whitespace-nowrap">쓸수록 더 정교해지는</span>{' '}<span className="whitespace-nowrap">AI 마케팅 엔진.</span>{' '}<span className="whitespace-nowrap">콘텐츠 기획,</span>{' '}<span className="whitespace-nowrap">생성,</span>{' '}<span className="whitespace-nowrap">영상 제작,</span>{' '}<span className="whitespace-nowrap">채널 배포,</span>{' '}<span className="whitespace-nowrap">데이터 분석까지</span>{' '}<span className="whitespace-nowrap">하나로.</span>
The Strategic Planner for Premium Medical Marketing. <br className="hidden md:block" />
<span className="whitespace-nowrap">유튜브·인스타·네이버·강남언니를</span>{' '}
<span className="whitespace-nowrap">10분 만에 진단하고,</span>{' '}
<span className="whitespace-nowrap">병원의 12개월 마케팅 전략을</span>{' '}
<span className="whitespace-nowrap">설계합니다.</span>
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="flex flex-col items-center justify-center gap-5 max-w-lg mx-auto w-full"
className="w-full"
>
<div className="relative w-full group">
<input
type="url"
placeholder="Enter Your Website URL"
value={url}
onChange={(e) => 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"
/>
</div>
<button onClick={handleAnalyze} className={`relative z-10 w-full px-8 py-5 text-base font-bold rounded-2xl transition-all flex items-center justify-center gap-3 group ${url.trim() ? 'bg-gradient-to-r from-accent to-[#8B5CF6] text-white shadow-xl hover:shadow-2xl hover:scale-[1.02] active:scale-[0.98]' : 'bg-gradient-to-r from-[#4F1DA1] to-[#021341] text-white shadow-sm'}`} disabled={!url.trim()}>
Analyze Marketing Performance
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</button>
<p className="text-xs font-medium text-slate-500 mt-2">
네이버 블로그, 플레이스, 소셜미디어 등 Online Presence 종합 분석 리포트를 제공합니다.
</p>
<MultiChannelInput variant="hero" onAnalyze={handleAnalyze} />
</motion.div>
</div>

View File

@ -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() {
<h2 className="text-4xl md:text-5xl font-serif font-bold text-primary-900 mb-6">
Core Modules
</h2>
<p className="text-lg md:text-xl text-slate-600 max-w-2xl mx-auto">
성능 개선 반영 자율 순환 마케팅 시스템
{/* PART II 피봇: "자율 순환 마케팅 시스템" → 실제 Phase 1-4 구현 범위로 정직하게 조정.
Product 1.0 Available 모듈 2개(Marketing Intelligence + Strategic Planning)와
Coming Soon 모듈 3개(Content/Video/Distribution)로 완성도 신호를 분명히. */}
<p className="text-lg md:text-xl text-slate-600 max-w-2xl mx-auto break-keep">
진단부터 전략 설계까지 <strong className="text-primary-900">Product 1.0 으로 출시된 모듈</strong>과 콘텐츠 실행을 위한 후속 로드맵입니다.
</p>
</motion.div>

View File

@ -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<ClassifiedUrls, 'unknown'>;
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 (
<div className="w-full max-w-2xl mx-auto">
{/* URL Textarea
Placeholder 카피 원칙: "한 줄씩" 같은 입력 규칙 제거.
classifyUrls.ts가 공백·쉼표·줄바꿈·임의 텍스트 섞임 모두 처리하므로
사용자에게 입력 형식 고민을 넘기지 않고, "뭉치 → 자동 분류" 를 제품 약속으로 선언.
정렬: `text-center` — 원본 Hero의 단일 URL input과 일관, Form 필드 인상을 줄여
"검색창 같은 자유 입력" 느낌으로 전환. */}
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder={
'URL을 한 번에 붙여넣어 주세요\n홈페이지 · YouTube · Instagram · Facebook · 네이버 플레이스 · 블로그 · 강남언니를 AI가 자동으로 분류합니다'
}
rows={5}
className={textareaClass}
spellCheck={false}
/>
{/* 채널 칩 프리뷰 */}
<div className="flex flex-wrap gap-2 mt-4 justify-center">
{CHANNEL_META.map(({ key, label, Icon, color }) => {
const count = classified[key].length;
const isActive = count > 0;
return (
<div
key={key}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border text-xs font-semibold transition-all ${
isActive ? `${chipActiveBg} ${chipActiveText}` : `${chipInactiveBg} ${chipInactiveText}`
}`}
>
<Icon size={14} className={isActive ? color : ''} />
<span>{label}</span>
{isActive && (
<span className={`ml-0.5 px-1.5 py-0.5 text-[10px] rounded-full bg-accent/10 text-accent`}>
{count}
</span>
)}
</div>
);
})}
</div>
{/* Unknown URL 경고 */}
{classified.unknown.length > 0 && (
<div
className={`flex items-start gap-2 mt-3 px-4 py-3 rounded-xl border ${
isHero
? 'bg-amber-50/80 border-amber-200 text-amber-800'
: 'bg-amber-500/10 border-amber-400/30 text-amber-100'
}`}
>
<WarningFilled size={16} className={isHero ? 'text-amber-500 shrink-0 mt-0.5' : 'text-amber-300 shrink-0 mt-0.5'} />
<div className="text-xs leading-relaxed break-keep">
<span className="font-semibold">분석 대상이 아닌 URL {classified.unknown.length}건</span>이 있어요.
(게시물·검색 페이지·미지원 플랫폼 등) — 해당 URL은 제외하고 진행합니다.
</div>
</div>
)}
{/* 분석 시작 버튼 — "Analyze" pill. 모양·크기는 원본 CTA 스펙 고정.
▸ 모양·크기·너비: `rounded-full px-10 py-4 text-lg font-medium`, `max-w-md` (~448px) 캡
▸ 색 (variant별 분기 — 대비 + 브랜드 시그니처 통일):
- hero (라이트 배경): 다크 브랜드 `#4F1DA1 → #021341` + `text-white`,
hover 시 `#AF90FF` 라벤더 단색 — 밝기 역전
- cta (다크 `bg-primary-900` 배경): 브랜드 시그니처 3-stop warm gradient
`#fff3eb → #e4cfff → #f5f9ff` + `text-primary-900`,
이미 Hero/CTA 헤드라인 텍스트와 동일 팔레트라 섹션 내 색언어 통일.
이미 밝은 톤이라 hover 밝기 역전 불가 → `hover:scale-[1.02]` + `shadow-2xl` 로 부양 피드백.
▸ disabled: HTML `disabled` 속성만, opacity 변화 없음 (46b911d 원칙) */}
<button
onClick={handleSubmit}
disabled={!canAnalyze}
className={`w-full max-w-md mx-auto mt-5 px-10 py-4 text-lg font-medium rounded-full transition-all duration-150 shadow-xl hover:shadow-2xl flex items-center justify-center gap-2 group bg-gradient-to-r active:scale-[0.98] ${
isHero
? 'from-[#4F1DA1] to-[#021341] text-white hover:from-[#AF90FF] hover:to-[#AF90FF]'
: 'from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] text-primary-900 hover:scale-[1.02]'
}`}
>
Analyze
<ArrowRight className="w-5 h-5 group-hover:translate-x-1" />
</button>
{/* 보조 안내 */}
<p className={`text-xs font-medium mt-3 text-center leading-relaxed break-keep ${helperClass}`}>
네이버 블로그 · 플레이스 · 소셜미디어 등 Online Presence 종합 분석 리포트를 제공합니다.
</p>
</div>
);
}

View File

@ -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 (
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/95 border-b border-slate-100">
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/95 border-b border-slate-100 backdrop-blur-md">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
{/* 로고 */}
<Link to="/" className="flex items-center gap-2">
<span className="font-serif text-3xl font-black tracking-[0.05em] bg-gradient-to-r from-[#4F1DA1] to-[#021341] bg-clip-text text-transparent">INFINITH</span>
<span className="font-serif text-3xl font-black tracking-[0.05em] bg-gradient-to-r from-[#4F1DA1] to-[#021341] bg-clip-text text-transparent">
INFINITH
</span>
</Link>
{/* 가운데 메뉴 */}
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
<a href="#solution" className="hover:text-primary-900 transition-colors">Solution</a>
<a href="#modules" className="hover:text-primary-900 transition-colors">Modules</a>
<a href="#use-cases" className="hover:text-primary-900 transition-colors">Use Cases</a>
<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>
<a href="/#use-cases" className="hover:text-primary-900 transition-colors">
Use Cases
</a>
</div>
<div className="flex items-center gap-4">
<button className="px-6 py-3 text-sm font-medium text-white rounded-full transition-all shadow-sm hover:shadow-md bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:opacity-90">
{/* 우측 CTA — Login(Secondary) + 문의하기(Primary) */}
<div className="flex items-center gap-3">
<Link
to="/login"
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-[#021341] bg-white border border-slate-200 rounded-full hover:bg-slate-50 hover:border-slate-300 transition-colors"
>
Login
</button>
</Link>
<a
href={buildContactMailto('도입 문의')}
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-semibold text-white rounded-full transition-all shadow-sm hover:shadow-md bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:opacity-90"
>
문의하기
<ArrowRight className="w-4 h-4" />
</a>
</div>
</div>
</nav>

View File

@ -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: "콘텐츠·광고·채널 성과가 어디에서 어떻게 작동하는지 데이터로 증명하기 어렵고, 의사결정은 감에 의존합니다."
}
];

View File

@ -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"
>
<Sparkles className="w-4 h-4 text-purple-300" />
<span className="text-sm font-medium text-purple-100">AI Marketing Engine</span>
<span className="text-sm font-medium text-purple-100">Strategic Planning Engine</span>
</motion.div>
<motion.h2
@ -26,20 +26,21 @@ export default function Solution() {
transition={{ duration: 0.6, delay: 0.1 }}
className="text-4xl md:text-6xl font-serif font-bold mb-6 leading-tight text-transparent bg-clip-text bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff]"
>
Infinite Marketing Engine
The Strategic Planning Engine
</motion.h2>
{/* PART II 피봇: "관찰이 아닌 실행 가능한 전략을 출력하는 AI 플래너"
AGDP 키워드는 유지하되 의미를 전략 설계 중심으로 재해석. */}
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-xl md:text-2xl text-slate-300 mb-12 max-w-3xl mx-auto leading-relaxed font-light"
className="text-xl md:text-2xl text-slate-300 mb-12 max-w-3xl mx-auto leading-relaxed font-light break-keep"
>
<span className="font-medium text-white">Infinite Marketing for Premium Medical Business & Marketing Agency</span>
<span className="font-medium text-white">관찰이 아닌, 실행 가능한 전략을 출력합니다.</span>
<br className="hidden md:block" />
Infinite Marketing은 Premium Medical Business와 Marketing Agency를 위한 AI Marketing Automation Platform입니다.
INFINITH는 마케팅 분석, 콘텐츠 생성, 영상 콘텐츠 제작, 채널 배포, 성과 분석과 피드백 적용을 하나의 Self-Improving Marketing Engine으로 제공합니다.
INFINITH는 <strong className="text-white font-medium">Audit → Generation → Direction → Planning</strong>의 AGDP 루프로 병원의 마케팅 의사결정을 설계합니다. 유튜브·인스타·네이버·강남언니 실측 데이터를 기반으로 12개월 전략과 주간 KPI 조정까지.
</motion.p>
{/* Circular Loop Diagram */}
@ -69,21 +70,21 @@ export default function Solution() {
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20">
<div className="w-40 h-40 md:w-56 md:h-56 rounded-full bg-primary-900/90 backdrop-blur-xl border border-white/5 flex flex-col items-center justify-center shadow-[0_0_60px_rgba(167,139,250,0.1)]">
<span className="text-4xl md:text-6xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] mb-2">AGDP</span>
<h3 className="text-xl md:text-3xl font-serif font-bold text-white text-center leading-tight">Infinite<br/>Marketing</h3>
<h3 className="text-xl md:text-3xl font-serif font-bold text-white text-center leading-tight">Strategic<br/>Planning</h3>
</div>
</div>
{/* Node A: Analysis (Left) */}
{/* Node A: Audit (Left) — 구 Analysis. 병원·채널 진단 리포트 */}
<div className="absolute top-1/2 left-0 -translate-x-1/2 -translate-y-1/2 z-30 flex flex-col items-center gap-3 md:gap-4">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full border border-purple-500/30 bg-primary-900/80 backdrop-blur-sm flex items-center justify-center shadow-[0_0_20px_rgba(168,85,247,0.15)]">
<span className="text-3xl md:text-4xl font-bold text-purple-300">A</span>
</div>
<div className="text-center">
<span className="block text-lg md:text-2xl font-medium text-purple-200 leading-tight">Analysis</span>
<span className="block text-lg md:text-2xl font-medium text-purple-200 leading-tight">Audit</span>
</div>
</div>
{/* Node G: Generation (Top) */}
{/* Node G: Generation (Top) — 전략·로드맵 문서 생성 (AI 콘텐츠 생성 아님) */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 z-30 flex flex-col items-center gap-3 md:gap-4">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full border border-purple-500/30 bg-primary-900/80 backdrop-blur-sm flex items-center justify-center shadow-[0_0_20px_rgba(168,85,247,0.15)]">
<span className="text-3xl md:text-4xl font-bold text-purple-300">G</span>
@ -93,23 +94,23 @@ export default function Solution() {
</div>
</div>
{/* Node D: Distribution (Right) */}
{/* Node D: Direction (Right) — 구 Distribution. 채널별 전략·우선순위 설계 */}
<div className="absolute top-1/2 right-0 translate-x-1/2 -translate-y-1/2 z-30 flex flex-col items-center gap-3 md:gap-4">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full border border-purple-500/30 bg-primary-900/80 backdrop-blur-sm flex items-center justify-center shadow-[0_0_20px_rgba(168,85,247,0.15)]">
<span className="text-3xl md:text-4xl font-bold text-purple-300">D</span>
</div>
<div className="text-center">
<span className="block text-lg md:text-2xl font-medium text-purple-200 leading-tight">Distribution</span>
<span className="block text-lg md:text-2xl font-medium text-purple-200 leading-tight">Direction</span>
</div>
</div>
{/* Node P: Performance (Bottom) */}
{/* Node P: Planning (Bottom) — 구 Performance. KPI 목표 설정 + 주간 조정 */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 z-30 flex flex-col items-center gap-3 md:gap-4">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full border border-purple-500/30 bg-primary-900/80 backdrop-blur-sm flex items-center justify-center shadow-[0_0_20px_rgba(168,85,247,0.15)]">
<span className="text-3xl md:text-4xl font-bold text-purple-300">P</span>
</div>
<div className="text-center">
<span className="block text-lg md:text-2xl font-medium text-purple-200 leading-tight">Performance</span>
<span className="block text-lg md:text-2xl font-medium text-purple-200 leading-tight">Planning</span>
</div>
</div>
@ -118,9 +119,11 @@ export default function Solution() {
<defs>
<path id="rewardPath" d="M 10.6 56.9 A 40 40 0 0 0 43.1 89.4" fill="none" />
</defs>
{/* 구 "Reward Signal" → "KPI Feedback" 로 피봇.
주간 KPI 달성도가 전략 재조정의 입력 시그널이 된다는 의미. */}
<text fontSize="3.5" className="font-medium uppercase tracking-widest" fill="#d8b4fe" opacity="0.8">
<textPath href="#rewardPath" startOffset="50%" textAnchor="middle">
← Reward SIGNAL
← KPI FEEDBACK
</textPath>
</text>
</svg>
@ -135,8 +138,8 @@ export default function Solution() {
className="max-w-3xl mx-auto mt-12 text-center"
>
<div className="inline-block bg-white/5 border border-white/10 rounded-2xl px-6 py-4 backdrop-blur-sm">
<p className="text-sm md:text-base text-slate-300">
<span className="font-bold text-purple-300">AGDP Cycle:</span> 분석(Analysis) → 생성(Generation) → 배포(Distribution) → 성과(Performance)의 무한 루프를 통해 콘텐츠 품질(CTR)을 <span className="text-white font-medium">자율 최적화</span>합니다.
<p className="text-sm md:text-base text-slate-300 break-keep">
<span className="font-bold text-purple-300">AGDP Cycle:</span> 진단(Audit) → 전략 생성(Generation) → 채널 방향성(Direction) → KPI 계획(Planning)의 무한 루프로 <span className="text-white font-medium">주간 KPI 달성도 → 전략 재조정</span>을 자율 반복합니다.
</p>
</div>
</motion.div>

View File

@ -14,8 +14,9 @@ export default function TargetAudience() {
<h2 className="text-3xl md:text-5xl font-serif font-bold text-primary-900 mb-4">
Who is Infinite Marketing for
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
프리미엄 의료 서비스와 전문 마케팅 에이전시를 위한 최적의 솔루션
{/* PART II 피봇: 프리미엄 병원 메인, 대행사 보조. "전략 파트너·전략 자문" 포지셔닝 반영. */}
<p className="text-lg text-slate-600 max-w-2xl mx-auto break-keep leading-relaxed">
프리미엄 병원의 <strong className="text-primary-900">전략 기획</strong>과 대행사 <strong className="text-primary-900">전략 자문</strong>을 위한 AI 플래너입니다.
</p>
</motion.div>
@ -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"
>
<h3 className="text-4xl md:text-5xl font-serif font-bold text-primary-900 mb-6 leading-tight">Premium Medical Business</h3>
<p className="text-slate-600 mb-10 leading-relaxed text-lg">
고객 LTV가 높고 브랜드 경쟁이 심해 콘텐츠 마케팅이 필수적인 프리미엄 의료 서비스 제공 병원
<p className="text-slate-600 mb-10 leading-relaxed text-lg break-keep">
고객 LTV가 높은 <strong className="text-primary-900">프리미엄 성형외과의 전략 파트너</strong>. 유튜브·인스타·네이버·강남언니를 10분 만에 진단해 12개월 로드맵을 설계합니다.
</p>
<div className="flex flex-wrap gap-3">
{['피부과', '성형외과', '치과', '안과', '헬스케어 클리닉', '피트니스'].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"
>
<h3 className="text-4xl md:text-5xl font-serif font-bold text-primary-900 mb-6 leading-tight">Medical Marketing Agency</h3>
<p className="text-slate-600 mb-10 leading-relaxed text-lg">
콘텐츠 제작 비용 부담과 인력 의존도가 높고 영상 제작 생산성 개선이 필요한 병원 마케팅 대행사
<div className="flex items-center gap-3 mb-6 flex-wrap">
<h3 className="text-4xl md:text-5xl font-serif font-bold text-primary-900 leading-tight">Medical Marketing Agency</h3>
{/* Partner Program 배지 — "준비중" 결핍 메시지 → "신청하세요" waitlist 희소성 프레임으로 전환.
대행사를 2차 페르소나가 아닌 early-access 대상으로 포지셔닝. */}
<span className="inline-flex items-center px-2.5 py-1 rounded-full bg-purple-100 text-purple-700 text-xs font-semibold border border-purple-200">
Partner Program · 신청하세요
</span>
</div>
<p className="text-slate-600 mb-10 leading-relaxed text-lg break-keep">
병원 마케팅 대행사의 <strong className="text-primary-900">전략 자문</strong>. 진단 리포트와 경쟁사 벤치마크로 제안 품질을 높이고 수주 경쟁력을 강화합니다.
</p>
<div className="flex flex-wrap gap-3">
{['병원 마케팅 대행사', '콘텐츠 마케팅 Agency', '영상 마케팅 Agency', '광고 운영 대행사'].map((item, i) => (

View File

@ -15,8 +15,9 @@ export default function UseCases() {
<h2 className="text-3xl md:text-5xl font-serif font-bold text-primary-900 mb-4">
Use Cases
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto font-medium">
Infinite Marketing이 만드는 실질적인 변화를 확인해보세요!
{/* PART II 피봇: "만드는 실질적인 변화" (제품 중심) → 역할별 가치 (고객 중심)로 전환. */}
<p className="text-lg text-slate-600 max-w-2xl mx-auto font-medium break-keep">
프리미엄 병원과 전문 대행사가 INFINITH로 만드는 전략적 변화입니다.
</p>
</motion.div>
@ -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"
>
<h3 className="text-2xl md:text-3xl font-serif font-bold text-primary-900 mb-8">Premium Medical Business</h3>
{/* 체크리스트 재설계: 진단(Audit) · 전략(Planning) · 운영(Weekly KPI) 3축. */}
<ul className="space-y-6">
{[
'SEO 콘텐츠 자동 생산으로 검색 상위 노출 달성',
'비용 부담 없이 고품질 영상 콘텐츠 대량 확대',
'자연 검색 유입 증가로 인한 환자 전환율 상승'
'10분 진단으로 채널별 강·약점 파악, 투자 우선순위 즉시 도출',
'12개월 마케팅 로드맵과 콘텐츠 캘린더로 기획 리소스 70% 절감',
'주간 KPI 달성도 → 전략 조정 루프로 환자 전환 흐름 자율 최적화',
].map((item, i) => (
<li key={i} className="flex items-start gap-4 text-slate-700 font-medium group">
<CheckCircle2 className="w-6 h-6 text-[#7A84D4] shrink-0 mt-1" />
@ -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"
>
<h3 className="text-2xl md:text-3xl font-serif font-bold text-primary-900 mb-8">Marketing Agency</h3>
{/* 대행사 가치축: 수주(Pitch) · 전략 품질(Strategy) · 포트폴리오 확장(Portfolio). */}
<ul className="space-y-6">
{[
'AI 기반 콘텐츠 제작 자동화로 생산성 극대화',
'블로그 텍스트 기반 영상 제작 자동화로 리소스 절감',
'다수 클라이언트 계정의 통합 운영 및 효율화'
'진단 리포트와 경쟁사 벤치마크로 제안서 품질 강화, 수주 경쟁력 상승',
'병원별 12개월 로드맵으로 전략 자문 일관성과 리텐션 확보',
'다수 병원 클라이언트 통합 대시보드로 운영 효율 극대화',
].map((item, i) => (
<li key={i} className="flex items-start gap-4 text-slate-700 font-medium group">
<CheckCircle2 className="w-6 h-6 text-purple-500 shrink-0 mt-1" />

View File

@ -0,0 +1,134 @@
/**
* FAQ — Pricing 페이지 하단 자주 묻는 질문.
*
* 계약 기반 영업 모델에 맞춰 카피 재작성 (plan 섹션 15-E 변형):
* - 온라인 결제 관련 질문 제거
* - 세금계산서 / 계약서 / 분원 4개↑ 커스텀 / 의료법 심의 / 강남언니 데이터 / 품질 보장 / Export 포함
*
* 인터랙션:
* - 단일 오픈(single-open) 아코디언 — `useState<string | null>` 로 현재 열린 질문 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<string | null>(null);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.5 }}
className="w-full"
>
{/* 헤드라인 */}
<div className="text-center mb-10">
<h2 className="text-3xl md:text-4xl font-serif font-bold text-primary-900 mb-3">
자주 묻는 질문
</h2>
<p className="text-slate-600 break-keep">
계약·결제·운영 관련해 자주 받는 질문을 모았습니다.
</p>
</div>
{/* 아코디언 리스트 — 콘텐츠 가독성을 위해 inner max-w-3xl, outer w-full */}
<div className="max-w-3xl mx-auto divide-y divide-slate-200 rounded-2xl bg-white border border-slate-200 shadow-sm overflow-hidden">
{items.map((item) => {
const isOpen = openId === item.id;
return (
<div key={item.id}>
<button
onClick={() => 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"
>
<span className="flex-1 text-base font-semibold text-primary-900 break-keep leading-snug">
{item.q}
</span>
<ChevronDown
className={`w-5 h-5 text-slate-400 shrink-0 mt-0.5 transition-transform duration-200 ${
isOpen ? 'rotate-180 text-accent' : ''
}`}
/>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
key="answer"
id={`faq-panel-${item.id}`}
role="region"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: 'easeInOut' }}
className="overflow-hidden"
>
<div className="px-6 pb-5 text-sm text-slate-600 leading-relaxed break-keep">
{item.a}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</motion.div>
);
}

View File

@ -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 (
<div className="flex items-center justify-center">
<CheckFilled size={18} className={isHighlight ? 'text-accent' : 'text-emerald-500'} />
</div>
);
}
if (value === false) {
return (
<div className="flex items-center justify-center">
<CrossFilled size={18} className="text-slate-300" />
</div>
);
}
return (
<div
className={`text-sm text-center leading-snug break-keep ${
isHighlight ? 'text-primary-900 font-semibold' : 'text-slate-700'
}`}
>
{value}
</div>
);
}
export default function FeatureComparisonTable() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.5 }}
className="w-full"
>
{/* 섹션 헤드라인 */}
<div className="text-center mb-10">
<h2 className="text-3xl md:text-4xl font-serif font-bold text-primary-900 mb-3">
플랜 세부 비교
</h2>
<p className="text-slate-600 break-keep">
병원 규모·마케팅 예산에 맞는 플랜을 한눈에 비교해 보세요.
</p>
</div>
{/* 테이블 — 모바일 가로 스크롤 */}
<div className="rounded-3xl bg-white border border-slate-200 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<div className="min-w-[720px]">
{/* 상단 헤더 행 */}
<div className="grid grid-cols-[1.4fr_1fr_1fr_1fr] border-b border-slate-200">
<div className="px-6 py-5 text-sm font-semibold text-slate-500">기능</div>
<div className="px-4 py-5 text-center">
<div className="text-sm font-bold text-primary-900">INSIGHT</div>
<div className="text-xs text-slate-400 mt-0.5">소형·1인 의원</div>
</div>
<div className="px-4 py-5 text-center bg-accent/5 border-x border-accent/15 relative">
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-accent/50 to-accent" />
<div className="text-sm font-bold text-primary-900">INTELLIGENCE</div>
<div className="text-xs text-accent font-semibold mt-0.5">메인 타겟</div>
</div>
<div className="px-4 py-5 text-center">
<div className="text-sm font-bold text-primary-900">INTELLIGENCE+</div>
<div className="text-xs text-slate-400 mt-0.5">대형·멀티 분원</div>
</div>
</div>
{/* 카테고리별 렌더링 */}
{categories.map((cat) => (
<div key={cat.name}>
{/* 카테고리 스트립 */}
<div className="grid grid-cols-[1.4fr_1fr_1fr_1fr] bg-slate-50/70 border-b border-slate-100">
<div className="px-6 py-2.5 text-xs font-bold tracking-wide text-slate-500 uppercase">
{cat.name}
</div>
<div className="bg-transparent" />
<div className="bg-accent/5 border-x border-accent/15" />
<div className="bg-transparent" />
</div>
{/* 각 기능 행 */}
{cat.rows.map((row, idx) => (
<div
key={row.label}
className={`grid grid-cols-[1.4fr_1fr_1fr_1fr] items-center ${
idx !== cat.rows.length - 1 ? 'border-b border-slate-100' : ''
}`}
>
<div className="px-6 py-4">
<div className="text-sm text-primary-900 font-medium break-keep">
{row.label}
</div>
{row.hint && (
<div className="text-xs text-slate-400 mt-1 leading-relaxed break-keep">
{row.hint}
</div>
)}
</div>
<div className="px-4 py-4">
<Cell value={row.insight} />
</div>
<div className="px-4 py-4 bg-accent/5 border-x border-accent/15 h-full flex items-center justify-center">
<Cell value={row.intelligence} isHighlight />
</div>
<div className="px-4 py-4">
<Cell value={row.intelligencePlus} />
</div>
</div>
))}
</div>
))}
</div>
</div>
</div>
{/* 보조 안내 */}
<p className="text-xs text-slate-400 text-center mt-4 break-keep">
모든 플랜은 계약 기반이며, 세부 조건은 상담 시 맞춤 설계됩니다.
</p>
</motion.div>
);
}

259
src/lib/classifyUrls.ts Normal file
View File

@ -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<keyof ClassifiedUrls, Set<string>> = {
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;
}

23
src/lib/contact.ts Normal file
View File

@ -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}`)}`;
}

View File

@ -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 }),
}
);

View File

@ -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(
<Routes>
<Route element={<App />}>
<Route index element={<LandingPage />} />
<Route path="pricing" element={<PricingPage />} />
<Route path="login" element={<LoginPage />} />
<Route path="report/loading" element={<AnalysisLoadingPage />} />
<Route path="report/loading/:reportId" element={<AnalysisLoadingPage />} />
<Route path="report/:id" element={<ReportPage />} />

View File

@ -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(() => {

130
src/pages/LoginPage.tsx Normal file
View File

@ -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<string | null>(null);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
setNotice(
'로그인 기능은 준비 중입니다. 계약을 희망하시는 경우 contact@o2o.kr 로 문의 주세요.',
);
};
return (
<section className="min-h-screen pt-32 pb-20 px-6 bg-gradient-to-b from-indigo-50/60 via-white to-white">
<div className="max-w-md mx-auto">
{/* 헤드라인 */}
<div className="text-center mb-8">
<Link
to="/"
className="font-serif text-3xl font-black tracking-[0.05em] bg-gradient-to-r from-[#4F1DA1] to-[#021341] bg-clip-text text-transparent"
>
INFINITH
</Link>
<h1 className="text-2xl font-serif font-bold text-primary-900 mt-6 mb-2">
계약 병원 전용 로그인
</h1>
<p className="text-sm text-slate-500 break-keep leading-relaxed">
계약 완료 시 별도 제공되는 자격 정보로 로그인하실 수 있습니다.
</p>
</div>
{/* 준비중 배너 */}
<div className="mb-6 rounded-2xl bg-amber-50/80 border border-amber-200 p-4 text-center">
<p className="text-xs font-semibold text-amber-800 break-keep leading-relaxed">
현재 로그인 기능은 준비 중입니다.
<br />
신규 도입 문의는 하단 "문의하기"를 이용해 주세요.
</p>
</div>
{/* 폼 카드 */}
<form
onSubmit={handleSubmit}
className="rounded-3xl bg-white border border-slate-200 shadow-sm p-8 space-y-5"
>
<div>
<label
htmlFor="login-email"
className="block text-xs font-semibold text-slate-600 mb-2"
>
이메일
</label>
<input
id="login-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="clinic@example.com"
className="w-full px-4 py-3 text-sm bg-slate-50 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent/40 text-primary-900 placeholder:text-slate-400 transition-all"
autoComplete="email"
/>
</div>
<div>
<label
htmlFor="login-password"
className="block text-xs font-semibold text-slate-600 mb-2"
>
비밀번호
</label>
<input
id="login-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full px-4 py-3 text-sm bg-slate-50 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent/40 text-primary-900 placeholder:text-slate-400 transition-all"
autoComplete="current-password"
/>
</div>
<button
type="submit"
className="w-full inline-flex items-center justify-center gap-2 px-6 py-3 text-sm font-bold text-white rounded-full bg-gradient-to-r from-[#4F1DA1] to-[#021341] shadow-sm hover:opacity-90 transition-all"
>
로그인
<ArrowRight className="w-4 h-4" />
</button>
{notice && (
<div
role="status"
className="text-xs text-slate-600 text-center bg-slate-50 rounded-xl p-3 leading-relaxed break-keep"
>
{notice}
</div>
)}
</form>
{/* 하단 문의하기 */}
<div className="mt-6 text-center">
<p className="text-xs text-slate-500 mb-2">계약을 원하시나요?</p>
<a
href={buildContactMailto('도입 문의 (LoginPage)')}
className="inline-flex items-center gap-1 text-sm font-semibold text-[#4F1DA1] hover:text-[#021341] transition-colors underline-offset-4 hover:underline"
>
문의하기 →
</a>
</div>
</div>
</section>
);
}

451
src/pages/PricingPage.tsx Normal file
View File

@ -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<BillingToggleProps> = ({ value, onChange }) => {
return (
<div className="inline-flex items-center gap-1 p-1 rounded-full bg-white/70 border border-slate-200 backdrop-blur-sm shadow-sm">
<button
onClick={() => 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'
}`}
>
월간 결제
</button>
<button
onClick={() => 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'
}`}
>
연간 결제
<span
className={`text-xs px-2 py-0.5 rounded-full font-bold ${
value === 'annual'
? 'bg-white/20 text-white'
: 'bg-accent/10 text-accent'
}`}
>
20% 할인
</span>
</button>
</div>
);
};
// ─── Tier Card 컴포넌트 ──────────────────────────────────────────
interface TierCardProps {
tier: Tier;
billing: 'monthly' | 'annual';
onSelect: (tier: Tier) => void;
}
const TierCard: React.FC<TierCardProps> = ({ tier, billing, onSelect }) => {
const price = billing === 'monthly' ? tier.monthlyKRW : tier.annualMonthlyKRW;
const isAnnual = billing === 'annual';
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className={`relative flex flex-col rounded-3xl p-8 bg-white border shadow-sm transition-all hover:shadow-xl ${
tier.isPopular
? 'border-accent/40 ring-2 ring-accent/30 shadow-lg scale-[1.02]'
: 'border-slate-200'
}`}
>
{/* Popular 배지 */}
{tier.isPopular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge variant="popular" size="md" />
</div>
)}
{/* 이름 + 태그라인 */}
<div className="mb-6">
<h3 className="text-2xl font-serif font-bold text-primary-900 mb-2">{tier.name}</h3>
<p className="text-sm text-slate-500 leading-relaxed break-keep">{tier.tagline}</p>
</div>
{/* 가격 */}
<div className="mb-6">
<div className="flex items-baseline gap-2">
<span className="text-4xl font-bold text-primary-900 tracking-tight">
{formatKRW(price)}
</span>
<span className="text-slate-500 text-sm">/월</span>
</div>
{isAnnual && (
<p className="text-xs text-accent font-semibold mt-2">
연 {formatKRW(tier.annualTotalKRW)} · 20% 절약
</p>
)}
{!isAnnual && (
<p className="text-xs text-slate-400 mt-2">
연 결제 시 월 {formatKRW(tier.annualMonthlyKRW)}
</p>
)}
</div>
{/* CTA — DS Primary: gradient + rounded-full (pill) */}
<button
onClick={() => 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}
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</button>
{/* Bullet points — DS FilledIcon(CheckFilled) */}
<ul className="space-y-3 flex-grow">
{tier.bullets.map((bullet, i) => (
<li key={i} className="flex items-start gap-2.5 text-sm text-slate-700">
<CheckFilled size={16} className="text-accent shrink-0 mt-0.5" />
<span className="leading-relaxed break-keep">{bullet}</span>
</li>
))}
</ul>
{/* Footnote */}
{tier.footnote && (
<p className="mt-6 pt-4 border-t border-slate-100 text-xs text-slate-400 text-center">
{tier.footnote}
</p>
)}
</motion.div>
);
};
// ─── 먼저 문의하기 강조 카드 ─────────────────────────────────────
// DS 레퍼런스: CTA 카드 배경에 3-stop warm gradient (Section 2.2)
// + 상단 filled 아이콘 squircle + Primary/Secondary pill 버튼 병렬
//
// PART III 피봇: "첫 리포트 무료 / 카드 등록 불필요" 표현 삭제 → 계약 기반 영업으로 일원화.
function ContactFirstBox() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="relative w-full rounded-3xl p-8 md:p-12 text-center overflow-hidden bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] border border-white/40 shadow-[3px_4px_12px_rgba(0,0,0,0.06)]"
>
{/* 상단 아이콘 squircle — DS filled icon 규칙 */}
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-white/60 backdrop-blur-sm border border-white/50 mb-6">
<BoltFilled size={26} className="text-accent" />
</div>
<h3 className="text-2xl md:text-3xl font-serif font-bold text-primary-900 mb-3">
먼저 대화부터 시작하세요
</h3>
<p className="text-slate-600 leading-relaxed max-w-xl mx-auto mb-8 break-keep">
계약 전에 병원 규모·마케팅 현황을 공유해 주시면, 전담 담당자가 적합한 플랜과
리포트 샘플을 정리해 회신 드립니다.
</p>
{/* DS 듀얼 버튼: Primary + Secondary — 모두 rounded-full (pill) */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<a
href={buildContactMailto('도입 상담 문의')}
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full font-medium text-sm text-white transition-shadow duration-150 shadow-md hover:shadow-xl group bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:from-[#AF90FF] hover:to-[#AF90FF] active:scale-[0.98]"
>
상담 문의하기
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</a>
<a
href="#tiers"
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full font-medium text-sm bg-white border border-slate-200 text-[#021341] hover:bg-slate-50 transition-colors"
>
플랜 비교 보기
</a>
</div>
</motion.div>
);
}
// ─── Launch Promotion 배너 ───────────────────────────────────────
// DS: 🎁 이모지 금지(Principle: No Emoji) → RocketFilled 대체
// dark primary gradient card + Secondary pill 버튼
function PromotionBanner() {
return (
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="w-full rounded-3xl bg-gradient-to-r from-[#4F1DA1] to-[#021341] text-white p-6 md:p-8 flex flex-col md:flex-row items-center gap-4 md:gap-6"
>
{/* Filled icon squircle — 🎁 대체 */}
<div className="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-white/10 backdrop-blur-sm shrink-0">
<RocketFilled size={24} className="text-white" />
</div>
<div className="flex-1 text-center md:text-left">
<p className="text-xs font-semibold text-purple-200 mb-1 tracking-wide">
런칭 프로모션 · 선착순 20병원
</p>
<p className="text-base md:text-lg font-serif">
INTELLIGENCE · INTELLIGENCE+ <strong>3개월 30% 할인</strong>
</p>
</div>
<a
href={buildContactMailto('런칭 프로모션 문의')}
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-full bg-white border border-slate-200 text-[#021341] font-medium text-sm hover:bg-slate-50 transition-colors whitespace-nowrap"
>
혜택 문의
</a>
</motion.div>
);
}
// ─── Enterprise Contact CTA ──────────────────────────────────────
// DS: outlined 패턴은 DS에 없음 → Primary pill(gradient + rounded-full)
function EnterpriseContact() {
return (
// outer: w-full로 섹션 정렬 / inner 콘텐츠만 max-w-2xl로 가독성 유지
<div className="w-full text-center">
<div className="max-w-2xl mx-auto">
<h3 className="text-2xl font-serif font-bold text-primary-900 mb-3">
더 많은 분원, 커스텀 플랜이 필요하신가요?
</h3>
<p className="text-slate-600 mb-8 break-keep">
4개 이상 분원을 운영하시거나 데이터 커스터마이징이 필요한 경우, 전담 담당자가 상담해 드립니다.
</p>
<a
href={buildContactMailto('커스텀 플랜 문의')}
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full font-medium text-sm text-white transition-shadow duration-150 shadow-md hover:shadow-xl group bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:from-[#AF90FF] hover:to-[#AF90FF] active:scale-[0.98]"
>
커스텀 플랜 문의
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</a>
</div>
</div>
);
}
// ─── 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 (
<main className="relative pt-28 md:pt-32 pb-24 overflow-hidden">
{/* Background — 랜딩 Hero와 톤 통일 */}
<div className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-100 via-purple-50 to-pink-50 opacity-60" />
{/* ── Section 1 · Hero ─────────────────────────── */}
<section className="px-6 text-center max-w-4xl mx-auto mb-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/70 border border-white/40 text-xs font-bold text-accent mb-6">
<PrismFilled size={14} className="text-accent" />
Pricing · 가격 안내
</div>
<h1 className="text-4xl md:text-6xl font-serif font-bold text-primary-900 leading-[1.1] tracking-[-0.02em] mb-5">
Strategic Planning,<br className="hidden md:block" />
<span className="text-gradient">At Your Scale.</span>
</h1>
<p className="text-lg text-slate-600 max-w-2xl mx-auto mb-10 break-keep">
병원 규모에 맞는 전략 파트너를 선택하세요. 계약·결제·운영 조건은 상담에서 맞춤 설계됩니다.
</p>
</motion.div>
{/* ── Section 2 · Billing Toggle ────────────── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<BillingToggle value={billing} onChange={setBilling} />
</motion.div>
</section>
{/* ── Section 3 · 3 Tier Cards ─────────────────── */}
<section id="tiers" className="px-6 max-w-7xl mx-auto mb-20 scroll-mt-24">
<div className="grid md:grid-cols-3 gap-6 md:gap-8 items-stretch">
{tiers.map((tier) => (
<TierCard key={tier.id} tier={tier} billing={billing} onSelect={handleTierSelect} />
))}
</div>
</section>
{/* ── Section 4 · Feature Comparison Table ─── */}
<section className="px-6 mb-20 max-w-7xl mx-auto">
<FeatureComparisonTable />
</section>
{/* ── Section 5 · 먼저 문의하기 강조 ───────────── */}
{/* outer max-w-7xl로 Tier Cards 섹션과 동일 정렬 (반응형 자동 축소) */}
<section className="px-6 mb-16 max-w-7xl mx-auto">
<ContactFirstBox />
</section>
{/* ── Section 6 · Launch Promotion ───────────── */}
<section className="px-6 mb-20 max-w-7xl mx-auto">
<PromotionBanner />
</section>
{/* ── Section 7 · FAQ ─────────────────────────── */}
<section className="px-6 mb-20 max-w-7xl mx-auto">
<FAQ />
</section>
{/* ── Section 8 · Enterprise Contact ─────────── */}
<section className="px-6 max-w-7xl mx-auto">
<EnterpriseContact />
</section>
</main>
);
}

View File

@ -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)
// ═══════════════════════════════════════════