fix: gangnamUnni always-try + leadDoctor in Perplexity prompt
- 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) <noreply@anthropic.com>claude/bold-hawking
parent
2cda26a649
commit
6e8f6940bf
|
|
@ -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: (() => {
|
||||
|
|
|
|||
|
|
@ -380,15 +380,48 @@ Deno.serve(async (req) => {
|
|||
}));
|
||||
}
|
||||
|
||||
// ─── 4. 강남언니 ───
|
||||
// ─── 4. 강남언니 (항상 시도 — verified 여부 무관) ───
|
||||
const guVerified = verified.gangnamUnni as Record<string, unknown> | 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<string, string>) => 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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue