From 200497fa1e00f6a770a52563b9769c7555dc693b Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Thu, 2 Apr 2026 14:30:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20P1-5/6/7=20=E2=80=94=20AI=20KPI=20targe?= =?UTF-8?q?ts,=20website=20tech=20audit,=20dynamic=20clinic=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P1-5: Add kpiTargets schema to AI prompt, use AI-generated goals instead of hardcoded multipliers - P1-6: Extend website channelAnalysis with trackingPixels, snsLinksOnSite, additionalDomains, mainCTA - P1-7: ClinicProfilePage fetches data from DB by report ID instead of hardcoded VIEW clinic data Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/transformReport.ts | 51 +++++++++--- src/pages/ClinicProfilePage.tsx | 90 +++++++++++++++++++-- supabase/functions/generate-report/index.ts | 5 +- 3 files changed, 126 insertions(+), 20 deletions(-) diff --git a/src/lib/transformReport.ts b/src/lib/transformReport.ts index 876889c..2c5a95c 100644 --- a/src/lib/transformReport.ts +++ b/src/lib/transformReport.ts @@ -24,6 +24,10 @@ interface ApiReport { reviews?: number; issues?: string[]; recommendation?: string; + trackingPixels?: { name: string; installed: boolean }[]; + snsLinksOnSite?: boolean; + additionalDomains?: { domain: string; purpose: string }[]; + mainCTA?: string; }>; competitors?: { name: string; @@ -51,6 +55,12 @@ interface ApiReport { asIs?: string; toBe?: string; }[]; + kpiTargets?: { + metric?: string; + current?: string; + target3Month?: string; + target12Month?: string; + }[]; marketTrends?: string[]; } @@ -252,10 +262,16 @@ export function transformApiReport( websiteAudit: { primaryDomain: new URL(metadata.url).hostname, - additionalDomains: [], - snsLinksOnSite: false, - trackingPixels: [], - mainCTA: '', + additionalDomains: (r.channelAnalysis?.website?.additionalDomains || []).map(d => ({ + domain: (d as { domain?: string }).domain || '', + purpose: (d as { purpose?: string }).purpose || '', + })), + snsLinksOnSite: r.channelAnalysis?.website?.snsLinksOnSite ?? false, + trackingPixels: (r.channelAnalysis?.website?.trackingPixels || []).map(p => ({ + name: (p as { name?: string }).name || '', + installed: (p as { installed?: boolean }).installed ?? false, + })), + mainCTA: r.channelAnalysis?.website?.mainCTA || '', }, problemDiagnosis: buildDiagnosis(r), @@ -304,15 +320,24 @@ export function transformApiReport( .map(rec => ({ task: rec.title || rec.description || '', completed: false })), })), - kpiDashboard: [ - { metric: '종합 점수', current: `${r.overallScore ?? '-'}`, target3Month: `${Math.min((r.overallScore ?? 50) + 15, 100)}`, target12Month: `${Math.min((r.overallScore ?? 50) + 30, 100)}` }, - ...(r.channelAnalysis?.instagram ? [ - { metric: 'Instagram 팔로워', current: `${r.channelAnalysis.instagram.followers ?? 0}`, target3Month: `${Math.round((r.channelAnalysis.instagram.followers ?? 0) * 1.3)}`, target12Month: `${Math.round((r.channelAnalysis.instagram.followers ?? 0) * 2)}` }, - ] : []), - ...(r.channelAnalysis?.youtube ? [ - { metric: 'YouTube 구독자', current: `${r.channelAnalysis.youtube.subscribers ?? 0}`, target3Month: `${Math.round((r.channelAnalysis.youtube.subscribers ?? 0) * 1.5)}`, target12Month: `${Math.round((r.channelAnalysis.youtube.subscribers ?? 0) * 3)}` }, - ] : []), - ], + kpiDashboard: r.kpiTargets?.length + ? r.kpiTargets + .filter((k): k is { metric?: string; current?: string; target3Month?: string; target12Month?: string } => !!k?.metric) + .map(k => ({ + metric: k.metric || '', + current: k.current || '-', + target3Month: k.target3Month || '-', + target12Month: k.target12Month || '-', + })) + : [ + { metric: '종합 점수', current: `${r.overallScore ?? '-'}`, target3Month: `${Math.min((r.overallScore ?? 50) + 15, 100)}`, target12Month: `${Math.min((r.overallScore ?? 50) + 30, 100)}` }, + ...(r.channelAnalysis?.instagram ? [ + { metric: 'Instagram 팔로워', current: `${r.channelAnalysis.instagram.followers ?? 0}`, target3Month: '-', target12Month: '-' }, + ] : []), + ...(r.channelAnalysis?.youtube ? [ + { metric: 'YouTube 구독자', current: `${r.channelAnalysis.youtube.subscribers ?? 0}`, target3Month: '-', target12Month: '-' }, + ] : []), + ], screenshots: [], }; diff --git a/src/pages/ClinicProfilePage.tsx b/src/pages/ClinicProfilePage.tsx index 5f5c5a7..707e1e4 100644 --- a/src/pages/ClinicProfilePage.tsx +++ b/src/pages/ClinicProfilePage.tsx @@ -1,3 +1,5 @@ +import { useState, useEffect } from 'react'; +import { useParams } from 'react-router'; import { motion } from 'motion/react'; import { YoutubeFilled, @@ -25,10 +27,11 @@ import { Heart, TrendingUp, } from 'lucide-react'; +import { fetchReportById } from '../lib/supabase'; -// ─── 수집 데이터 기반 병원 프로필 (정적 데이터) ─── +// ─── 기본값 (DB 데이터로 덮어쓸 수 있음) ─── -const CLINIC = { +const CLINIC: Record = { name: '뷰성형외과의원', nameEn: 'VIEW Plastic Surgery', logo: '/assets/clients/view-clinic/logo-circle.png', @@ -70,9 +73,9 @@ const CLINIC = { ], }; -// ─── 의료진 (강남언니 실제 데이터) ─── +// ─── 의료진 ─── -const DOCTORS = [ +const DOCTORS: Record[] = [ { name: '최순우', title: '대표원장', specialty: '가슴성형', credentials: '서울대 출신, 의학박사', rating: 9.4, reviews: 1812, featured: true }, { name: '정재현', title: '원장', specialty: '가슴성형', credentials: '성형외과 전문의', rating: 9.6, reviews: 3177, featured: false }, { name: '김정민', title: '원장', specialty: '리프팅 · 눈성형', credentials: '성형외과 전문의', rating: 9.7, reviews: 878, featured: false }, @@ -81,9 +84,9 @@ const DOCTORS = [ { name: '김도형', title: '원장', specialty: '눈성형 · 코성형', credentials: '성형외과 전문의', rating: 9.7, reviews: 191, featured: false }, ]; -// ─── 플랫폼별 평점 (집계) ─── +// ─── 플랫폼별 평점 ─── -const RATINGS = [ +const RATINGS: Record[] = [ { platform: '강남언니', rating: '9.5', scale: '/10', reviews: '18,961건', color: '#FF6B8A', pct: 95 }, { platform: '네이버 플레이스', rating: '4.6', scale: '/5', reviews: '324건', color: '#03C75A', pct: 92 }, { platform: 'Google Maps', rating: '4.3', scale: '/5', reviews: '187건', color: '#4285F4', pct: 86 }, @@ -105,6 +108,81 @@ const PROCEDURES = [ // ─── Component ─── export default function ClinicProfilePage() { + const { id } = useParams<{ id: string }>(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Override CLINIC data with DB data when available + useEffect(() => { + if (!id) { setIsLoading(false); return; } + fetchReportById(id) + .then((row) => { + const report = row.report as Record; + const clinicInfo = report.clinicInfo as Record | undefined; + const scrapeData = row.scrape_data as Record | undefined; + const clinic = (scrapeData?.clinic as Record) || {}; + + if (clinicInfo?.name) CLINIC.name = clinicInfo.name as string; + if (clinicInfo?.address) CLINIC.location = clinicInfo.address as string; + if (clinicInfo?.phone) CLINIC.phone = clinicInfo.phone as string; + if (clinicInfo?.services) CLINIC.specialties = clinicInfo.services as string[]; + if (clinicInfo?.doctors) { + const docs = clinicInfo.doctors as { name: string; specialty: string }[]; + DOCTORS.length = 0; + docs.forEach((d) => { + DOCTORS.push({ name: d.name, title: '원장', specialty: d.specialty, credentials: '성형외과 전문의', rating: 0, reviews: 0, featured: false }); + }); + if (DOCTORS.length > 0) DOCTORS[0].featured = true; + } + CLINIC.nameEn = (row.clinic_name || '').includes('의원') ? '' : row.clinic_name || ''; + + // Update website + const domain = new URL(row.url).hostname; + CLINIC.websites = [{ label: '공식 홈페이지', url: domain, primary: true }]; + + // Update social from socialHandles + const handles = report.socialHandles as Record | undefined; + if (handles) { + CLINIC.socialChannels = []; + if (handles.instagram) CLINIC.socialChannels.push({ platform: 'Instagram', handle: `@${handles.instagram}`, url: `instagram.com/${handles.instagram}`, followers: '-', videos: '', icon: InstagramFilled, color: '#E1306C' }); + if (handles.youtube) CLINIC.socialChannels.push({ platform: 'YouTube', handle: `@${handles.youtube}`, url: `youtube.com/${handles.youtube}`, followers: '-', videos: '', icon: YoutubeFilled, color: '#FF0000' }); + if (handles.facebook) CLINIC.socialChannels.push({ platform: 'Facebook', handle: handles.facebook, url: `facebook.com/${handles.facebook}`, followers: '-', videos: '', icon: FacebookFilled, color: '#1877F2' }); + } + + // Update ratings from channel analysis + const chAnalysis = report.channelAnalysis as Record> | undefined; + if (chAnalysis) { + RATINGS.length = 0; + if (chAnalysis.gangnamUnni?.rating) RATINGS.push({ platform: '강남언니', rating: `${chAnalysis.gangnamUnni.rating}`, scale: '/5', reviews: `${chAnalysis.gangnamUnni.reviews ?? '-'}건`, color: '#FF6B8A', pct: ((chAnalysis.gangnamUnni.rating as number) / 5) * 100 }); + if (chAnalysis.naverPlace?.rating) RATINGS.push({ platform: '네이버 플레이스', rating: `${chAnalysis.naverPlace.rating}`, scale: '/5', reviews: `${chAnalysis.naverPlace.reviews ?? '-'}건`, color: '#03C75A', pct: ((chAnalysis.naverPlace.rating as number) / 5) * 100 }); + } + }) + .catch((err) => setError(err.message)) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
+
+

병원 프로필을 불러오는 중...

+
+
+ ); + } + + if (error) { + return ( +
+
+

오류가 발생했습니다

+

{error}

+
+
+ ); + } + return (
diff --git a/supabase/functions/generate-report/index.ts b/supabase/functions/generate-report/index.ts index f07caf6..a26a89b 100644 --- a/supabase/functions/generate-report/index.ts +++ b/supabase/functions/generate-report/index.ts @@ -102,7 +102,7 @@ ${JSON.stringify(analyzeResult.data?.analysis || {}, null, 2)} "youtube": { "score": 0-100, "status": "active|inactive|weak", "subscribers": 0, "recommendation": "추천사항" }, "naverPlace": { "score": 0-100, "rating": 0, "reviews": 0, "recommendation": "추천사항" }, "gangnamUnni": { "score": 0-100, "rating": 0, "reviews": 0, "recommendation": "추천사항" }, - "website": { "score": 0-100, "issues": [], "recommendation": "추천사항" } + "website": { "score": 0-100, "issues": [], "recommendation": "추천사항", "trackingPixels": [{"name": "Google Analytics|Facebook Pixel|Naver Analytics|etc", "installed": true}], "snsLinksOnSite": true, "additionalDomains": [{"domain": "example.com", "purpose": "용도"}], "mainCTA": "주요 전환 유도 요소" } }, "competitors": [ { "name": "경쟁병원명", "strengths": ["강점1"], "weaknesses": ["약점1"], "marketingChannels": ["채널1"] } @@ -123,6 +123,9 @@ ${JSON.stringify(analyzeResult.data?.analysis || {}, null, 2)} { "area": "해시태그/키워드", "asIs": "현재 해시태그 전략", "toBe": "최적화된 해시태그 세트" }, { "area": "포지셔닝", "asIs": "현재 시장 포지셔닝", "toBe": "목표 포지셔닝" } ], + "kpiTargets": [ + { "metric": "지표명 (종합 점수, Instagram 팔로워, YouTube 구독자, 네이버 블로그 방문자, 월간 상담 문의 등)", "current": "현재 수치", "target3Month": "3개월 목표 (현실적)", "target12Month": "12개월 목표 (도전적)" } + ], "recommendations": [ { "priority": "high|medium|low", "category": "카테고리", "title": "제목", "description": "설명", "expectedImpact": "기대 효과" } ],