From 6e8f6940bff985550982bb0d038f24ae93f0313c Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Tue, 7 Apr 2026 10:29:10 +0900 Subject: [PATCH] fix: gangnamUnni always-try + leadDoctor in Perplexity prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - collect-channel-data: gangnamUnni scraping no longer requires verified=true. Fallback: Firecrawl search for gangnamunni.com URL when discover-channels failed to verify. Solves empty ratings/reviews. - generate-report: Perplexity prompt now explicitly requests leadDoctor (name, specialty, rating, reviewCount) and staffCount in clinicInfo. - transformReport: clinicInfo type extended with leadDoctor + staffCount; transformation prefers clinic.leadDoctor over doctors[0] fallback. Root cause: clinic_registry table not yet in DB → discover-channels always falls back to API search → gangnamUnni URL not found → collect-channel-data skips gangnamUnni → all clinic metrics empty. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/transformReport.ts | 12 +++--- .../functions/collect-channel-data/index.ts | 43 ++++++++++++++++--- supabase/functions/generate-report/index.ts | 4 +- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/lib/transformReport.ts b/src/lib/transformReport.ts index e0e5bdd..b28f524 100644 --- a/src/lib/transformReport.ts +++ b/src/lib/transformReport.ts @@ -12,7 +12,9 @@ interface ApiReport { address?: string; phone?: string; services?: string[]; - doctors?: { name: string; specialty: string }[]; + doctors?: { name: string; specialty: string; rating?: number; reviews?: number }[]; + leadDoctor?: { name: string; specialty: string; rating?: number; reviewCount?: number }; + staffCount?: number; }; newChannelProposals?: { channel?: string; priority?: string; rationale?: string }[]; executiveSummary?: string; @@ -516,7 +518,7 @@ export function transformApiReport( ): MarketingReport { const r = apiReport; const clinic = r.clinicInfo || {}; - const doctor = clinic.doctors?.[0]; + const doctor = clinic.leadDoctor || clinic.doctors?.[0]; return { id: reportId, @@ -530,12 +532,12 @@ export function transformApiReport( // Registry foundedYear takes priority over AI-generated value (Registry = human-verified) established: clinic.established || '', yearsInBusiness: clinic.established ? new Date().getFullYear() - parseInt(clinic.established) : 0, - staffCount: 0, + staffCount: typeof clinic.staffCount === 'number' ? clinic.staffCount : (clinic.doctors?.length ?? 0), leadDoctor: { name: doctor?.name || '', credentials: doctor?.specialty || '', - rating: 0, - reviewCount: 0, + rating: doctor?.rating ?? 0, + reviewCount: doctor?.reviewCount ?? doctor?.reviews ?? 0, }, // 강남언니 is 10-point scale. AI sometimes gives 5-point — auto-correct. overallRating: (() => { diff --git a/supabase/functions/collect-channel-data/index.ts b/supabase/functions/collect-channel-data/index.ts index 5f559d9..f6ea7e7 100644 --- a/supabase/functions/collect-channel-data/index.ts +++ b/supabase/functions/collect-channel-data/index.ts @@ -380,15 +380,48 @@ Deno.serve(async (req) => { })); } - // ─── 4. 강남언니 ─── + // ─── 4. 강남언니 (항상 시도 — verified 여부 무관) ─── const guVerified = verified.gangnamUnni as Record | null; - if (FIRECRAWL_API_KEY && guVerified?.verified && guVerified.url) { + if (FIRECRAWL_API_KEY && clinicName) { channelTasks.push(wrapChannelTask("gangnamUnni", async () => { + let gangnamUnniUrl = (guVerified?.verified && guVerified.url) ? String(guVerified.url) : ""; + + // Fallback: 강남언니 URL을 Firecrawl 검색으로 직접 찾기 + if (!gangnamUnniUrl) { + const shortName = clinicName.replace(/성형외과|의원|병원|클리닉|피부과/g, '').trim(); + const searchQueries = [ + `${clinicName} site:gangnamunni.com`, + `${shortName} 성형외과 site:gangnamunni.com`, + `${clinicName} 강남언니 병원`, + ]; + for (const q of searchQueries) { + try { + const sRes = await fetch("https://api.firecrawl.dev/v1/search", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${FIRECRAWL_API_KEY}` }, + body: JSON.stringify({ query: q, limit: 5 }), + }); + const sData = await sRes.json(); + const found = (sData.data || []) + .map((r: Record) => r.url) + .find((u: string) => u?.includes('gangnamunni.com/hospitals/')); + if (found) { gangnamUnniUrl = found; break; } + } catch { /* try next query */ } + } + if (gangnamUnniUrl) { + console.log(`[gangnamUnni] Fallback search found: ${gangnamUnniUrl}`); + } + } + + if (!gangnamUnniUrl) { + throw new Error("강남언니 URL을 찾을 수 없습니다 (검색 실패)"); + } + const scrapeRes = await fetchWithRetry("https://api.firecrawl.dev/v1/scrape", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${FIRECRAWL_API_KEY}` }, body: JSON.stringify({ - url: guVerified!.url as string, + url: gangnamUnniUrl, formats: ["json"], jsonOptions: { prompt: "Extract: hospital name, overall rating (강남언니 rating is always out of 10, NOT out of 5), total review count, doctors with names/ratings/review counts/specialties, procedures offered, address, certifications/badges", @@ -412,13 +445,11 @@ Deno.serve(async (req) => { channelData.gangnamUnni = { name: hospital.hospitalName, rawRating: hospital.rating, - // 강남언니 rating is always /10 (enforced in Firecrawl prompt) — trust the value directly. - // Do NOT multiply by 2: a score of 4.8 means 4.8/10, not 9.6/10. rating: typeof hospital.rating === 'number' && hospital.rating > 0 ? hospital.rating : null, ratingScale: "/10", totalReviews: hospital.totalReviews, doctors: (hospital.doctors || []).slice(0, 10), procedures: hospital.procedures || [], address: hospital.address, - badges: hospital.badges || [], sourceUrl: guVerified!.url as string, + badges: hospital.badges || [], sourceUrl: gangnamUnniUrl, }; } else { throw new Error("강남언니 scrape returned no hospital data"); diff --git a/supabase/functions/generate-report/index.ts b/supabase/functions/generate-report/index.ts index 490a741..17ef24b 100644 --- a/supabase/functions/generate-report/index.ts +++ b/supabase/functions/generate-report/index.ts @@ -90,7 +90,9 @@ ${JSON.stringify(scrapeData.branding || {}, null, 2).slice(0, 1000)} "address": "주소", "phone": "전화번호", "services": ["시술1", "시술2"], - "doctors": [{"name": "의사명", "specialty": "전문분야"}] + "doctors": [{"name": "의사명", "specialty": "전문분야"}], + "leadDoctor": {"name": "대표원장 이름", "specialty": "전문분야/학력", "rating": 0, "reviewCount": 0}, + "staffCount": 0 }, "executiveSummary": "경영진 요약 (3-5문장)", "overallScore": 0-100,