/** * MultiChannelInput — 랜딩 Hero / CTA 공용 멀티 URL 입력 컴포넌트. * * 설계 배경 (PART III 피봇 + 2026-04 입력 분할 개편): * - 영업 현장에서는 병원이 본인들의 YouTube/Instagram/FB/네이버플레이스/블로그/강남언니 URL을 * 이미 알고 있는 경우가 많아 이를 직접 받는 편이 정확도·속도 면에서 우월합니다. * - 단일 textarea + 자동 분류 방식은 사용자가 "어떻게 입력해야 하는지" 직관이 없어 * 공란이 되거나 잘못된 URL이 섞이는 경우가 많았습니다. * - 따라서 채널별로 7개 필드를 명시적으로 노출 — 빈 칸을 보면 "여기에 무엇을 넣어야 하는지"가 명확합니다. * * 동작: * 1) 7개 필드 각각에 URL 입력 (한 줄에 하나) * 2) 입력 시 `classifyUrls()`로 검증 — 해당 채널 패턴과 매치되는지 실시간 확인 * 3) 매치 성공: 우측에 체크 아이콘, 실패: 경고 아이콘 * 4) 1개 이상 유효 입력 시 Analyze 버튼 활성화 * 5) Submit 시 onAnalyze(payload) 호출 (부모가 navigation 처리) * * DS 준수: * - Filled Icons Only (lucide 무단 사용 금지) * - DS Primary pill: rounded-full + gradient `from-[#4F1DA1] to-[#021341]` * - variant='hero': 글래스 배경 (랜딩 Hero) * - variant='cta': 다크 배경 (CTA 섹션) */ import { useMemo, useState } from 'react'; import { ArrowRight } from 'lucide-react'; import { CheckFilled, WarningFilled } from '@/shared/icons/FilledIcons'; import { classifyUrls, hasAnalyzableChannels, pickPrimaryUrl, type ClassifiedUrls, } from '../lib/classifyUrls'; import { Button } from '@/shared/ui/button'; import { PLATFORM_META } from '@/features/clinics/components/PlatformChips'; import type { PlatformKey } from '@/features/clinics/types/workspace'; /** 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; } type ChannelKey = keyof Omit; /** ChannelKey(분류기 결과 키) → 공통 PlatformKey 매핑 — 'homepage' ↔ 'website' 만 다름 */ const CHANNEL_TO_PLATFORM: Record = { homepage: 'website', youtube: 'youtube', instagram: 'instagram', facebook: 'facebook', naverPlace: 'naverPlace', naverBlog: 'naverBlog', gangnamUnni: 'gangnamUnni', }; const PLACEHOLDER: Record = { homepage: 'viewclinic.com', youtube: 'youtube.com/@ViewclinicKR', instagram: 'instagram.com/viewplastic', facebook: 'facebook.com/viewps1', naverPlace: 'place.naver.com/hospital/11709005', naverBlog: 'blog.naver.com/viewclinicps', gangnamUnni: 'gangnamunni.com/hospitals/189', }; /** 채널 입력 순서 — 공통 PLATFORM_META 의 label/color/Icon 을 그대로 사용 */ const CHANNEL_ORDER: ChannelKey[] = [ 'homepage', 'youtube', 'instagram', 'facebook', 'naverPlace', 'naverBlog', 'gangnamUnni', ]; type ChannelUrlInputs = Record; const EMPTY_URLS: ChannelUrlInputs = { homepage: '', youtube: '', instagram: '', facebook: '', naverPlace: '', naverBlog: '', gangnamUnni: '', }; /** * 단일 입력값 검증 — 해당 채널 패턴과 매치되는지. * 빈 값: 'empty', 매치 성공: 'valid', SNS인데 다른 채널: 'wrong', 파싱 실패: 'invalid' */ function validateField(value: string, expected: ChannelKey): 'empty' | 'valid' | 'wrong' | 'invalid' { if (!value.trim()) return 'empty'; const c = classifyUrls(value); if (c[expected].length > 0) return 'valid'; // 다른 채널로 분류됐으면 'wrong', unknown이면 'invalid' for (const k of Object.keys(c) as Array) { if (k !== 'unknown' && k !== expected && c[k].length > 0) return 'wrong'; } return 'invalid'; } export default function MultiChannelInput({ variant = 'hero', onAnalyze }: MultiChannelInputProps) { const [urls, setUrls] = useState(EMPTY_URLS); // 통합 분류 결과 — 7개 필드 값을 join해 classifyUrls에 한 번에 통과시켜 manualChannels 구성. const aggregated = useMemo(() => { const joined = Object.values(urls).filter(Boolean).join('\n'); return classifyUrls(joined); }, [urls]); const canAnalyze = hasAnalyzableChannels(aggregated); const primaryUrl = pickPrimaryUrl(aggregated); const handleSubmit = () => { if (!canAnalyze || !primaryUrl) return; const payload: AnalyzePayload = { primaryUrl, manualChannels: { youtube: aggregated.youtube.length ? aggregated.youtube : undefined, instagram: aggregated.instagram.length ? aggregated.instagram : undefined, facebook: aggregated.facebook.length ? aggregated.facebook : undefined, naverPlace: aggregated.naverPlace.length ? aggregated.naverPlace : undefined, naverBlog: aggregated.naverBlog.length ? aggregated.naverBlog : undefined, gangnamUnni: aggregated.gangnamUnni.length ? aggregated.gangnamUnni : undefined, }, rawInput: Object.entries(urls) .filter(([, v]) => v.trim()) .map(([k, v]) => `${k}: ${v}`) .join('\n'), }; onAnalyze(payload); }; // variant별 스타일 토큰 const isHero = variant === 'hero'; const inputClass = isHero ? 'w-full pl-11 pr-10 py-2.5 text-sm bg-white/80 backdrop-blur-sm border border-slate-200 rounded-xl 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' : 'w-full pl-11 pr-10 py-2.5 text-sm bg-white/5 backdrop-blur-sm border border-white/15 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/60 text-white placeholder:text-white/40 transition-all'; const labelClass = isHero ? 'text-slate-600' : 'text-white/70'; const helperClass = isHero ? 'text-slate-500' : 'text-white/60'; return (
{/* 7개 채널별 입력 필드 — 한 줄에 하나, 좌측 아이콘 + 우측 검증 상태 */}
{CHANNEL_ORDER.map((key) => { const meta = PLATFORM_META[CHANNEL_TO_PLATFORM[key]]; const Icon = meta.Icon; const value = urls[key]; const status = validateField(value, key); const inactiveColor = isHero ? '#94a3b8' /* slate-400 */ : 'rgba(255,255,255,0.4)'; return (
{/* 좌측 채널 아이콘 — 입력 시 브랜드 컬러, 빈 칸일 땐 회색. z-10 으로 input 의 bg/backdrop-blur 위에 올림 (없으면 input 배경이 아이콘을 가림) */}
setUrls((prev) => ({ ...prev, [key]: e.target.value }))} placeholder={`${meta.label} · ${PLACEHOLDER[key]}`} aria-label={meta.label} className={inputClass} spellCheck={false} autoComplete="off" /> {/* 우측 검증 상태 아이콘 — 동일하게 z-10 */}
{status === 'valid' && ( )} {(status === 'wrong' || status === 'invalid') && ( )}
); })}
{/* 보조 안내 — 1개 이상 입력 시 어느 채널이 분석 대상인지 요약 */}

7개 채널 중 알고 계신 URL만 입력해주세요 — 1개만 입력하셔도 분석이 시작됩니다.

{/* 분석 시작 버튼 — DS Primary pill */} {/* 보조 안내 (하단) */}

네이버 블로그 · 플레이스 · 소셜미디어 등 Online Presence 종합 분석 리포트를 제공합니다.

); }