feat: Perplexity Online Presence 종합 분석 + Apify Instagram 검색

B4 Perplexity: rewrote from narrow "find social accounts" to broad
"Online Presence 종합 분석" — finds Instagram, YouTube, Facebook,
TikTok, Naver, Kakao, 강남언니, 바비톡 in one query.

B5 Apify Instagram: generates handle candidates from clinic name
(english name, domain, _official, _ps, _clinic variants) and directly
checks each via Apify instagram-profile-scraper. Finds accounts that
web search misses.

Removed redundant B4b (platform presence) — now merged into B4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-04 01:24:56 +09:00
parent 64669888c2
commit c74832d764
1 changed files with 88 additions and 37 deletions

View File

@ -283,7 +283,7 @@ Deno.serve(async (req) => {
} catch { /* skip */ } } catch { /* skip */ }
})()); })());
// ─── B4. Perplexity: Supplement — catch what APIs missed ─── // ─── B4. Perplexity: Online Presence 종합 분석 ───
if (PERPLEXITY_API_KEY) { if (PERPLEXITY_API_KEY) {
stageBTasks.push((async () => { stageBTasks.push((async () => {
try { try {
@ -293,10 +293,36 @@ Deno.serve(async (req) => {
body: JSON.stringify({ body: JSON.stringify({
model: "sonar", model: "sonar",
messages: [ messages: [
{ role: "system", content: "You are a social media researcher. Search the web and find social media accounts. Respond ONLY with valid JSON, no explanation." }, { role: "system", content: "You are a digital marketing analyst specializing in Korean medical clinics. Search the web thoroughly and provide a comprehensive online presence report. Respond ONLY with valid JSON." },
{ role: "user", content: `${resolvedName} 병원의 인스타그램, 유튜브, 페이스북, 틱톡, 네이버블로그, 카카오채널 계정을 검색해서 찾아줘. 검색 결과에서 발견된 계정을 모두 알려줘. 인스타그램은 여러 계정이 있을 수 있어.\n\n{"instagram": ["handle1", "handle2"], "youtube": "channel URL or @handle", "facebook": "page URL or name", "tiktok": "@handle", "naverBlog": "blog ID", "kakao": "channel ID"}` }, { role: "user", content: `"${resolvedName}" 병원의 Online Presence를 종합 분석해줘.
:
- ( , , )
- ( , Q&A )
-
-
- ( )
-
- URL
-
-
/URL/ .
JSON format:
{
"instagram": ["handle1", "handle2", "handle3"],
"youtube": ["channel URL or @handle1", "channel2"],
"facebook": "page URL or name",
"tiktok": "@handle",
"naverBlog": "blog ID",
"kakao": "channel ID",
"gangnamUnni": {"url": "https://gangnamunni.com/hospitals/...", "registered": true},
"naverPlace": {"registered": true},
"babitok": {"registered": true}
}` },
], ],
temperature: 0.1, temperature: 0.2,
}), }),
}); });
const data = await res.json(); const data = await res.json();
@ -305,44 +331,69 @@ Deno.serve(async (req) => {
if (jsonMatch) text = jsonMatch[1]; if (jsonMatch) text = jsonMatch[1];
const parsed = JSON.parse(text); const parsed = JSON.parse(text);
const ph = { // Extract social handles
instagram: Array.isArray(parsed.instagram) ? parsed.instagram : parsed.instagram ? [parsed.instagram] : [], const igArr = Array.isArray(parsed.instagram) ? parsed.instagram : parsed.instagram ? [parsed.instagram] : [];
youtube: parsed.youtube ? [parsed.youtube] : [], const ytArr = Array.isArray(parsed.youtube) ? parsed.youtube : parsed.youtube ? [parsed.youtube] : [];
facebook: parsed.facebook ? [parsed.facebook] : [], if (igArr.length) apiHandles.instagram!.push(...igArr);
naverBlog: parsed.naverBlog ? [parsed.naverBlog] : [], if (ytArr.length) apiHandles.youtube!.push(...ytArr);
tiktok: parsed.tiktok ? [parsed.tiktok] : [], if (parsed.facebook) apiHandles.facebook!.push(typeof parsed.facebook === 'string' ? parsed.facebook : '');
kakao: parsed.kakao ? [parsed.kakao] : [], if (parsed.naverBlog) apiHandles.naverBlog!.push(typeof parsed.naverBlog === 'string' ? parsed.naverBlog : '');
}; if (parsed.tiktok) apiHandles.tiktok!.push(typeof parsed.tiktok === 'string' ? parsed.tiktok : '');
if (ph.instagram.length) apiHandles.instagram!.push(...ph.instagram); if (parsed.kakao) apiHandles.kakao!.push(typeof parsed.kakao === 'string' ? parsed.kakao : '');
if (ph.youtube.length) apiHandles.youtube!.push(...ph.youtube);
if (ph.facebook.length) apiHandles.facebook!.push(...ph.facebook); // Extract platform presence hints
if (ph.naverBlog.length) apiHandles.naverBlog!.push(...ph.naverBlog); if (parsed.gangnamUnni?.url) gangnamUnniHintUrl = parsed.gangnamUnni.url;
if (ph.tiktok.length) apiHandles.tiktok!.push(...ph.tiktok);
if (ph.kakao.length) apiHandles.kakao!.push(...ph.kakao);
} catch { /* skip */ } } catch { /* skip */ }
})()); })());
// B4b. Platform presence (강남언니, 바비톡) }
// ─── B5. Apify Instagram: Direct profile search by clinic name variants ───
const APIFY_TOKEN = Deno.env.get("APIFY_API_TOKEN") || "";
if (APIFY_TOKEN) {
stageBTasks.push((async () => { stageBTasks.push((async () => {
try { try {
const res = await fetch("https://api.perplexity.ai/chat/completions", { // Generate handle candidates from clinic name
method: "POST", const baseName = resolvedName.replace(/성형외과|병원|의원|클리닉|피부과/g, '').trim().toLowerCase();
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` }, const baseNameEn = (clinic.clinicNameEn || '').replace(/\s+/g, '').toLowerCase();
body: JSON.stringify({ const candidates: string[] = [];
model: "sonar", if (baseNameEn && baseNameEn.length >= 3) {
messages: [ candidates.push(baseNameEn, `${baseNameEn}_official`, `${baseNameEn}_ps`, `${baseNameEn}_clinic`);
{ role: "system", content: "You are a medical platform researcher. Search the web for clinic listings. Respond ONLY with valid JSON, no explanation." }, }
{ role: "user", content: `${resolvedName} 병원이 강남언니(gangnamunni.com), 네이버 플레이스, 바비톡에 등록되어 있는지 검색해줘. URL도 찾아줘.\n\n{"gangnamUnni": {"registered": true, "url": "https://gangnamunni.com/hospitals/...", "rating": 9.5}, "naverPlace": {"registered": true}, "babitok": {"registered": false}}` }, if (baseName && /^[a-zA-Z]/.test(baseName)) {
], candidates.push(baseName, `${baseName}_official`, `${baseName}_ps`);
temperature: 0.1, }
}), // Also try domain-based
}); const domainBase = new URL(url).hostname.replace('www.', '').split('.')[0].toLowerCase();
const data = await res.json(); if (domainBase.length >= 3 && !candidates.includes(domainBase)) {
let text = data.choices?.[0]?.message?.content || ""; candidates.push(domainBase, `${domainBase}_official`);
const jsonMatch = text.match(/```(?:json)?\n?([\s\S]*?)```/); }
if (jsonMatch) text = jsonMatch[1];
const parsed = JSON.parse(text); // Quick check each candidate with Apify
if (parsed.gangnamUnni?.url) gangnamUnniHintUrl = parsed.gangnamUnni.url; for (const handle of candidates.slice(0, 6)) {
try {
const apifyRes = await fetch(
`${APIFY_BASE}/acts/apify~instagram-profile-scraper/runs?token=${APIFY_TOKEN}&waitForFinish=30`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ usernames: [handle], resultsLimit: 1 }),
}
);
const run = await apifyRes.json();
const datasetId = run.data?.defaultDatasetId;
if (!datasetId) continue;
const itemsRes = await fetch(`${APIFY_BASE}/datasets/${datasetId}/items?token=${APIFY_TOKEN}&limit=1`);
const items = await itemsRes.json();
const profile = (items as Record<string, unknown>[])[0];
if (profile && !profile.error && (profile.followersCount as number) >= 50) {
apiHandles.instagram!.push(profile.username as string || handle);
break; // Found one valid — stop searching
}
} catch { /* try next */ }
}
} catch { /* skip */ } } catch { /* skip */ }
})()); })());
} }