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
parent
517075b3ef
commit
938ebacbf9
|
|
@ -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,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: "콘텐츠·광고·채널 성과가 어디에서 어떻게 작동하는지 데이터로 증명하기 어렵고, 의사결정은 감에 의존합니다."
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`)}`;
|
||||
}
|
||||
|
|
@ -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 }),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)
|
||||
// ═══════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Reference in New Issue