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>
main
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.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.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.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개,, 원진성형외과,프리미엄/하이타깃 후보,강남,,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개,,

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개

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 { motion } from 'motion/react';
import { ArrowRight } from 'lucide-react'; import MultiChannelInput, { type AnalyzePayload } from './MultiChannelInput';
export default function CTA() { export default function CTA() {
const [url, setUrl] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const handleAnalyze = () => { const handleAnalyze = (payload: AnalyzePayload) => {
if (url.trim()) navigate('/report/loading', { state: { url } }); navigate('/report/loading', {
state: {
url: payload.primaryUrl,
manualChannels: payload.manualChannels,
},
});
}; };
return ( return (
@ -24,7 +37,7 @@ export default function CTA() {
transition={{ duration: 0.6 }} 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]" 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.h2>
<motion.p <motion.p
@ -34,7 +47,7 @@ export default function CTA() {
transition={{ duration: 0.6, delay: 0.1 }} 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" className="text-lg md:text-xl text-purple-200 mb-10 max-w-2xl mx-auto font-light"
> >
URL . . URL AI .
</motion.p> </motion.p>
<motion.div <motion.div
@ -42,27 +55,19 @@ export default function CTA() {
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }} 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 <MultiChannelInput variant="cta" onAnalyze={handleAnalyze} />
type="url"
placeholder="Enter Your URL" {/* 보조 CTA — 가격 플랜 */}
value={url} <div className="mt-6 flex justify-center">
onChange={(e) => setUrl(e.target.value)} <Link
onKeyDown={(e) => e.key === 'Enter' && handleAnalyze()} to="/pricing?from=cta"
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" className="inline-flex items-center gap-2 text-sm font-semibold text-purple-200 hover:text-white transition-colors underline-offset-4 hover:underline"
/> >
<button
onClick={handleAnalyze} </Link>
disabled={!url.trim()} </div>
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>
</motion.div> </motion.div>
</div> </div>
</section> </section>

View File

@ -1,15 +1,18 @@
import { useState } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { ArrowRight } from 'lucide-react';
import { PrismFilled } from './icons/FilledIcons'; import { PrismFilled } from './icons/FilledIcons';
import MultiChannelInput, { type AnalyzePayload } from './MultiChannelInput';
export default function Hero() { export default function Hero() {
const [url, setUrl] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const handleAnalyze = () => { const handleAnalyze = (payload: AnalyzePayload) => {
if (url.trim()) navigate('/report/loading', { state: { url } }); navigate('/report/loading', {
state: {
url: payload.primaryUrl,
manualChannels: payload.manualChannels,
},
});
}; };
return ( 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" 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" /> <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.div>
<motion.h1 <motion.h1
@ -38,39 +41,30 @@ export default function Hero() {
<span className="text-gradient">Marketing Engine.</span> <span className="text-gradient">Marketing Engine.</span>
</motion.h1> </motion.h1>
{/* PART II : Strategic Planner
"Marketing that learns... 쓸수록 더 정교해지는..."
Generation/Distribution Mock .
Phase 1-4 (DiscoverCollectReportPlan) . */}
<motion.p <motion.p
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }} 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" 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"/> The Strategic Planner for Premium Medical Marketing. <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> <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.p>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }} 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"> <MultiChannelInput variant="hero" onAnalyze={handleAnalyze} />
<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>
</motion.div> </motion.div>
</div> </div>

View File

@ -1,74 +1,76 @@
import React from 'react'; import React from 'react';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
// PART II 피봇: 1·5번은 Product 1.0 Available, 2·3·4는 Coming Soon.
// 카피는 Intelligence + Planning 중심으로 조정 (구조 5개 카드는 유지).
const modules = [ const modules = [
{ {
step: "1", step: "1",
title: "Marketing Intelligence", title: "Marketing Intelligence",
items: [ items: [
"브랜딩, 마케팅 현황 분석", "브랜드·온라인 프레즌스 진단",
"타겟 고객 분석", "유튜브·인스타·네이버·강남언니 실측",
"키워드 분석", "경쟁 병원 벤치마크 (최대 10곳)",
"경쟁 및 포지셔닝 분석", "키워드·해시태그 트렌드 분석",
"SEO 전략 & 채널별 콘텐츠 기획" "Vision AI 기반 의료진·슬로건 추출",
], ],
highlight: "AI 기반 시장 통찰력 도출", highlight: "10분 진단 → 전략 기획의 출발점",
color: "bg-[#021341]", color: "bg-[#021341]",
textColor: "text-indigo-600" textColor: "text-indigo-600"
}, },
{ {
step: "2", step: "2",
title: "Content Creation", title: "Strategic Planning",
items: [ items: [
"블로그 콘텐츠 생성", "12개월 마케팅 로드맵",
"SEO 콘텐츠 생성", "4~8주 콘텐츠 캘린더 + 필러 5종",
"SNS 콘텐츠 생성", "KPI 대시보드 (3·12개월 목표)",
"마케팅 카피 생성", "주간 KPI 달성도 → 전략 조정 제안",
"Human-in-the loop 프로세스" "브랜드 가이드 (톤·컬러·로고) 자동 추출",
], ],
highlight: "고품질 맞춤형 콘텐츠 자동화", highlight: "관찰을 실행 가능한 전략으로 전환",
color: "bg-[#021341]", color: "bg-[#021341]",
textColor: "text-indigo-600" textColor: "text-indigo-600"
}, },
{ {
step: "3", step: "3",
title: "Video Automation", title: "Content Creation",
items: [ items: [
"블로그 → 영상 변환", "블로그·SEO·SNS 카피 자동 생성",
"숏폼 콘텐츠 생성", "브랜드 톤 일관성 보장",
"유튜브 콘텐츠 제작", "Human-in-the-loop 편집 워크플로우",
"SNS 영상 제작", "콘텐츠 필러·시즌별 테마 매핑",
"멀티모달 AI 엔진: 영상 + 음악 + 카피" "Coming Soon · Q4 2026",
], ],
highlight: "원클릭 영상 제작 시스템", highlight: "전략을 실행 가능한 콘텐츠로",
color: "bg-[#021341]", color: "bg-[#021341]",
textColor: "text-indigo-600" textColor: "text-indigo-600"
}, },
{ {
step: "4", step: "4",
title: "Distribution Engine", title: "Video Automation",
items: [ items: [
"블로그 게시", "블로그 → 숏폼·유튜브 영상 변환",
"SNS 자동 게시", "Creatomate 기반 자동 렌더링",
"유튜브 업로드", "음악·자막·썸네일 AI 생성",
"콘텐츠 일정 관리", "시즌별 템플릿 + 병원 CI 반영",
"SEO, AEO 자동 최적화" "Coming Soon · Q4 2026",
], ],
highlight: "전 채널 통합 배포 및 최적화", highlight: "영상 제작 리소스 부담 해소",
color: "bg-[#021341]", color: "bg-[#021341]",
textColor: "text-indigo-600" textColor: "text-indigo-600"
}, },
{ {
step: "5", step: "5",
title: "Performance Intelligence", title: "Distribution & Performance",
items: [ items: [
"SEO 성과 분석", "멀티 채널 자동 게시 (블로그·SNS·유튜브)",
"콘텐츠 성과 분석", "SEO·AEO 자동 최적화",
"채널 성과 분석", "실시간 성과 트래킹 + 리포트",
"AI 콘텐츠 개선 전략 추천", "KPI 달성 → 다음 주기 전략 피드백",
"데이터 기반 효과 검증" "Coming Soon · Q4 2026",
], ],
highlight: "실시간 성과 추적 및 개선", highlight: "전략-실행-성과의 자율 루프 완성",
color: "bg-[#021341]", color: "bg-[#021341]",
textColor: "text-indigo-600" 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"> <h2 className="text-4xl md:text-5xl font-serif font-bold text-primary-900 mb-6">
Core Modules Core Modules
</h2> </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> </p>
</motion.div> </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 { Link } from 'react-router';
import { motion } from 'motion/react'; import { ArrowRight } from 'lucide-react';
import { buildContactMailto } from '../lib/contact';
export default function Navbar() { export default function Navbar() {
return ( 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"> <div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
{/* 로고 */}
<Link to="/" className="flex items-center gap-2"> <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> </Link>
{/* 가운데 메뉴 */}
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600"> <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="/#solution" className="hover:text-primary-900 transition-colors">
<a href="#modules" className="hover:text-primary-900 transition-colors">Modules</a> Product
<a href="#use-cases" className="hover:text-primary-900 transition-colors">Use Cases</a> </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>
<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 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>
</div> </div>
</nav> </nav>

View File

@ -1,18 +1,19 @@
import { motion } from 'motion/react'; import { motion } from 'motion/react';
// PART II 피봇: 현장 고충 중심으로 재작성. #3 "데이터 기반의 마케팅 부족"은 유지(사용자 피드백).
const problems = [ const problems = [
{ {
title: "콘텐츠 생산의 한계", title: "콘텐츠, 소진되는 비용과 시간",
desc: "블로그, SEO, 유튜브, 숏폼 등 지속적 생산이 필요하지만 인력과 비용, 시간 부족으로 제한됩니다." desc: "매주 쏟아지는 콘텐츠 제작 요구에 인력·시간·예산이 빠르게 소진됩니다. ROI를 측정할 여력도 부족합니다."
}, },
{ {
title: "영상 콘텐츠 제작 비용", title: "트렌드와 경쟁사 분석 부재",
desc: "영상은 중요하지만 촬영, 편집, 기획 비용이 높습니다. 특히 숏폼 콘텐츠는 지속적인 제작이 어렵습니다.", desc: "강남언니·네이버·유튜브에서 경쟁 병원이 어떤 콘텐츠·메시지로 움직이는지 체계적으로 추적할 방법이 없습니다.",
highlight: true highlight: true
}, },
{ {
title: "데이터 기반의 마케팅 부족", 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" 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" /> <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.div>
<motion.h2 <motion.h2
@ -26,20 +26,21 @@ export default function Solution() {
transition={{ duration: 0.6, delay: 0.1 }} 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]" 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> </motion.h2>
{/* PART II : " AI "
AGDP . */}
<motion.p <motion.p
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }} 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" /> <br className="hidden md:block" />
Infinite Marketing Premium Medical Business Marketing Agency AI Marketing Automation Platform. INFINITH <strong className="text-white font-medium">Audit Generation Direction Planning</strong> AGDP . ··· 12 KPI .
INFINITH , , , , Self-Improving Marketing Engine .
</motion.p> </motion.p>
{/* Circular Loop Diagram */} {/* 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="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)]"> <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> <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>
</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="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)]"> <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> <span className="text-3xl md:text-4xl font-bold text-purple-300">A</span>
</div> </div>
<div className="text-center"> <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>
</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="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)]"> <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> <span className="text-3xl md:text-4xl font-bold text-purple-300">G</span>
@ -93,23 +94,23 @@ export default function Solution() {
</div> </div>
</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="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)]"> <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> <span className="text-3xl md:text-4xl font-bold text-purple-300">D</span>
</div> </div>
<div className="text-center"> <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>
</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="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)]"> <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> <span className="text-3xl md:text-4xl font-bold text-purple-300">P</span>
</div> </div>
<div className="text-center"> <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>
</div> </div>
@ -118,9 +119,11 @@ export default function Solution() {
<defs> <defs>
<path id="rewardPath" d="M 10.6 56.9 A 40 40 0 0 0 43.1 89.4" fill="none" /> <path id="rewardPath" d="M 10.6 56.9 A 40 40 0 0 0 43.1 89.4" fill="none" />
</defs> </defs>
{/* "Reward Signal" "KPI Feedback" .
KPI . */}
<text fontSize="3.5" className="font-medium uppercase tracking-widest" fill="#d8b4fe" opacity="0.8"> <text fontSize="3.5" className="font-medium uppercase tracking-widest" fill="#d8b4fe" opacity="0.8">
<textPath href="#rewardPath" startOffset="50%" textAnchor="middle"> <textPath href="#rewardPath" startOffset="50%" textAnchor="middle">
Reward SIGNAL KPI FEEDBACK
</textPath> </textPath>
</text> </text>
</svg> </svg>
@ -135,8 +138,8 @@ export default function Solution() {
className="max-w-3xl mx-auto mt-12 text-center" 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"> <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"> <p className="text-sm md:text-base text-slate-300 break-keep">
<span className="font-bold text-purple-300">AGDP Cycle:</span> (Analysis) (Generation) (Distribution) (Performance) (CTR) <span className="text-white font-medium"> </span>. <span className="font-bold text-purple-300">AGDP Cycle:</span> (Audit) (Generation) (Direction) KPI (Planning) <span className="text-white font-medium"> KPI </span> .
</p> </p>
</div> </div>
</motion.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"> <h2 className="text-3xl md:text-5xl font-serif font-bold text-primary-900 mb-4">
Who is Infinite Marketing for Who is Infinite Marketing for
</h2> </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> </p>
</motion.div> </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" 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> <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"> <p className="text-slate-600 mb-10 leading-relaxed text-lg break-keep">
LTV LTV <strong className="text-primary-900"> </strong>. ··· 10 12 .
</p> </p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{['피부과', '성형외과', '치과', '안과', '헬스케어 클리닉', '피트니스'].map((item, i) => ( {['피부과', '성형외과', '치과', '안과', '헬스케어 클리닉', '피트니스'].map((item, i) => (
@ -47,9 +48,16 @@ export default function TargetAudience() {
transition={{ duration: 0.6, delay: 0.2 }} 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" 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> <div className="flex items-center gap-3 mb-6 flex-wrap">
<p className="text-slate-600 mb-10 leading-relaxed text-lg"> <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> </p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{['병원 마케팅 대행사', '콘텐츠 마케팅 Agency', '영상 마케팅 Agency', '광고 운영 대행사'].map((item, i) => ( {['병원 마케팅 대행사', '콘텐츠 마케팅 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"> <h2 className="text-3xl md:text-5xl font-serif font-bold text-primary-900 mb-4">
Use Cases Use Cases
</h2> </h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto font-medium"> {/* PART II 피봇: "만드는 실질적인 변화" (제품 중심) → 역할별 가치 (고객 중심)로 전환. */}
Infinite Marketing ! <p className="text-lg text-slate-600 max-w-2xl mx-auto font-medium break-keep">
INFINITH .
</p> </p>
</motion.div> </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" 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> <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"> <ul className="space-y-6">
{[ {[
'SEO 콘텐츠 자동 생산으로 검색 상위 노출 달성', '10분 진단으로 채널별 강·약점 파악, 투자 우선순위 즉시 도출',
'비용 부담 없이 고품질 영상 콘텐츠 대량 확대', '12개월 마케팅 로드맵과 콘텐츠 캘린더로 기획 리소스 70% 절감',
'자연 검색 유입 증가로 인한 환자 전환율 상승' '주간 KPI 달성도 → 전략 조정 루프로 환자 전환 흐름 자율 최적화',
].map((item, i) => ( ].map((item, i) => (
<li key={i} className="flex items-start gap-4 text-slate-700 font-medium group"> <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" /> <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" 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> <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"> <ul className="space-y-6">
{[ {[
'AI 기반 콘텐츠 제작 자동화로 생산성 극대화', '진단 리포트와 경쟁사 벤치마크로 제안서 품질 강화, 수주 경쟁력 상승',
'블로그 텍스트 기반 영상 제작 자동화로 리소스 절감', '병원별 12개월 로드맵으로 전략 자문 일관성과 리텐션 확보',
'다수 클라이언트 계정의 통합 운영 효율화' '다수 병원 클라이언트 통합 대시보드로 운영 효율 극대화',
].map((item, i) => ( ].map((item, i) => (
<li key={i} className="flex items-start gap-4 text-slate-700 font-medium group"> <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" /> <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 ─── // ─── 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. * Phase 1: Discover & verify social channels from website URL.
* Returns verified handles + reportId for subsequent phases. * 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( const response = await fetch(
`${supabaseUrl}/functions/v1/discover-channels`, `${supabaseUrl}/functions/v1/discover-channels`,
{ {
method: "POST", method: "POST",
headers: fnHeaders(), 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 { BrowserRouter, Routes, Route } from 'react-router';
import App from './App.tsx'; import App from './App.tsx';
import LandingPage from './pages/LandingPage.tsx'; import LandingPage from './pages/LandingPage.tsx';
import PricingPage from './pages/PricingPage.tsx';
import AnalysisLoadingPage from './pages/AnalysisLoadingPage.tsx'; import AnalysisLoadingPage from './pages/AnalysisLoadingPage.tsx';
import ReportPage from './pages/ReportPage.tsx'; import ReportPage from './pages/ReportPage.tsx';
import MarketingPlanPage from './pages/MarketingPlanPage.tsx'; import MarketingPlanPage from './pages/MarketingPlanPage.tsx';
@ -13,6 +14,7 @@ import PerformancePage from './pages/PerformancePage.tsx';
import DataValidationPage from './pages/DataValidationPage.tsx'; import DataValidationPage from './pages/DataValidationPage.tsx';
import ClinicProfilePage from './pages/ClinicProfilePage.tsx'; import ClinicProfilePage from './pages/ClinicProfilePage.tsx';
import ApiDashboardPage from './pages/ApiDashboardPage.tsx'; import ApiDashboardPage from './pages/ApiDashboardPage.tsx';
import LoginPage from './pages/LoginPage.tsx';
import './index.css'; import './index.css';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
@ -21,6 +23,8 @@ createRoot(document.getElementById('root')!).render(
<Routes> <Routes>
<Route element={<App />}> <Route element={<App />}>
<Route index element={<LandingPage />} /> <Route index element={<LandingPage />} />
<Route path="pricing" element={<PricingPage />} />
<Route path="login" element={<LoginPage />} />
<Route path="report/loading" element={<AnalysisLoadingPage />} /> <Route path="report/loading" element={<AnalysisLoadingPage />} />
<Route path="report/loading/:reportId" element={<AnalysisLoadingPage />} /> <Route path="report/loading/:reportId" element={<AnalysisLoadingPage />} />
<Route path="report/:id" element={<ReportPage />} /> <Route path="report/:id" element={<ReportPage />} />

View File

@ -57,7 +57,12 @@ export default function AnalysisLoadingPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { reportId: urlReportId } = useParams<{ reportId?: string }>(); 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 hasStarted = useRef(false);
const phaseIndex = PHASE_STEPS.findIndex(s => s.key === phase); const phaseIndex = PHASE_STEPS.findIndex(s => s.key === phase);
@ -76,7 +81,9 @@ export default function AnalysisLoadingPage() {
if (startPhase === 'discovering') { if (startPhase === 'discovering') {
if (!startUrl) throw new Error('No URL provided'); if (!startUrl) throw new Error('No URL provided');
setPhase('discovering'); 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'); if (!discovery.success) throw new Error(discovery.error || 'Channel discovery failed');
reportId = discovery.reportId; reportId = discovery.reportId;
clinicId = discovery.clinicId; clinicId = discovery.clinicId;
@ -146,7 +153,7 @@ export default function AnalysisLoadingPage() {
setErrorDomain((err as { domain?: string }).domain || null); setErrorDomain((err as { domain?: string }).domain || null);
} }
} }
}, [navigate]); }, [navigate, manualChannels]);
// Retry from the current failed phase // Retry from the current failed phase
const handleRetry = useCallback(() => { 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 { interface DiscoverRequest {
url: string; url: string;
clinicName?: 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 { function extractHandle(raw: string, platform: string): string | null {
@ -154,7 +167,7 @@ Deno.serve(async (req) => {
} }
try { try {
const { url, clinicName: inputClinicName } = (await req.json()) as DiscoverRequest; const { url, clinicName: inputClinicName, manualChannels } = (await req.json()) as DiscoverRequest;
if (!url) { if (!url) {
return new Response( return new Response(
JSON.stringify({ error: "URL is required" }), JSON.stringify({ error: "URL is required" }),
@ -776,9 +789,32 @@ Deno.serve(async (req) => {
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// STAGE C: Merge ALL sources + Verify // 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 = { const cleanHandles = {
instagram: [...new Set(merged.instagram.map(h => extractHandle(h, 'instagram')).filter((h): h is string => h !== null))], 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, 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) // Save to DB (supabase client reused from registry check above)
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════