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;
|
address?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
services?: 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 }[];
|
newChannelProposals?: { channel?: string; priority?: string; rationale?: string }[];
|
||||||
executiveSummary?: string;
|
executiveSummary?: string;
|
||||||
|
|
@ -516,7 +518,7 @@ export function transformApiReport(
|
||||||
): MarketingReport {
|
): MarketingReport {
|
||||||
const r = apiReport;
|
const r = apiReport;
|
||||||
const clinic = r.clinicInfo || {};
|
const clinic = r.clinicInfo || {};
|
||||||
const doctor = clinic.doctors?.[0];
|
const doctor = clinic.leadDoctor || clinic.doctors?.[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: reportId,
|
id: reportId,
|
||||||
|
|
@ -530,12 +532,12 @@ export function transformApiReport(
|
||||||
// Registry foundedYear takes priority over AI-generated value (Registry = human-verified)
|
// Registry foundedYear takes priority over AI-generated value (Registry = human-verified)
|
||||||
established: clinic.established || '',
|
established: clinic.established || '',
|
||||||
yearsInBusiness: clinic.established ? new Date().getFullYear() - parseInt(clinic.established) : 0,
|
yearsInBusiness: clinic.established ? new Date().getFullYear() - parseInt(clinic.established) : 0,
|
||||||
staffCount: 0,
|
staffCount: typeof clinic.staffCount === 'number' ? clinic.staffCount : (clinic.doctors?.length ?? 0),
|
||||||
leadDoctor: {
|
leadDoctor: {
|
||||||
name: doctor?.name || '',
|
name: doctor?.name || '',
|
||||||
credentials: doctor?.specialty || '',
|
credentials: doctor?.specialty || '',
|
||||||
rating: 0,
|
rating: doctor?.rating ?? 0,
|
||||||
reviewCount: 0,
|
reviewCount: doctor?.reviewCount ?? doctor?.reviews ?? 0,
|
||||||
},
|
},
|
||||||
// 강남언니 is 10-point scale. AI sometimes gives 5-point — auto-correct.
|
// 강남언니 is 10-point scale. AI sometimes gives 5-point — auto-correct.
|
||||||
overallRating: (() => {
|
overallRating: (() => {
|
||||||
|
|
|
||||||
|
|
@ -380,15 +380,48 @@ Deno.serve(async (req) => {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 4. 강남언니 ───
|
// ─── 4. 강남언니 (항상 시도 — verified 여부 무관) ───
|
||||||
const guVerified = verified.gangnamUnni as Record<string, unknown> | null;
|
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 () => {
|
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", {
|
const scrapeRes = await fetchWithRetry("https://api.firecrawl.dev/v1/scrape", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${FIRECRAWL_API_KEY}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${FIRECRAWL_API_KEY}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: guVerified!.url as string,
|
url: gangnamUnniUrl,
|
||||||
formats: ["json"],
|
formats: ["json"],
|
||||||
jsonOptions: {
|
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",
|
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 = {
|
channelData.gangnamUnni = {
|
||||||
name: hospital.hospitalName,
|
name: hospital.hospitalName,
|
||||||
rawRating: hospital.rating,
|
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,
|
rating: typeof hospital.rating === 'number' && hospital.rating > 0 ? hospital.rating : null,
|
||||||
ratingScale: "/10",
|
ratingScale: "/10",
|
||||||
totalReviews: hospital.totalReviews, doctors: (hospital.doctors || []).slice(0, 10),
|
totalReviews: hospital.totalReviews, doctors: (hospital.doctors || []).slice(0, 10),
|
||||||
procedures: hospital.procedures || [], address: hospital.address,
|
procedures: hospital.procedures || [], address: hospital.address,
|
||||||
badges: hospital.badges || [], sourceUrl: guVerified!.url as string,
|
badges: hospital.badges || [], sourceUrl: gangnamUnniUrl,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error("강남언니 scrape returned no hospital data");
|
throw new Error("강남언니 scrape returned no hospital data");
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@ ${JSON.stringify(scrapeData.branding || {}, null, 2).slice(0, 1000)}
|
||||||
"address": "주소",
|
"address": "주소",
|
||||||
"phone": "전화번호",
|
"phone": "전화번호",
|
||||||
"services": ["시술1", "시술2"],
|
"services": ["시술1", "시술2"],
|
||||||
"doctors": [{"name": "의사명", "specialty": "전문분야"}]
|
"doctors": [{"name": "의사명", "specialty": "전문분야"}],
|
||||||
|
"leadDoctor": {"name": "대표원장 이름", "specialty": "전문분야/학력", "rating": 0, "reviewCount": 0},
|
||||||
|
"staffCount": 0
|
||||||
},
|
},
|
||||||
"executiveSummary": "경영진 요약 (3-5문장)",
|
"executiveSummary": "경영진 요약 (3-5문장)",
|
||||||
"overallScore": 0-100,
|
"overallScore": 0-100,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue