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
parent
64669888c2
commit
c74832d764
|
|
@ -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
|
||||||
|
const baseName = resolvedName.replace(/성형외과|병원|의원|클리닉|피부과/g, '').trim().toLowerCase();
|
||||||
|
const baseNameEn = (clinic.clinicNameEn || '').replace(/\s+/g, '').toLowerCase();
|
||||||
|
const candidates: string[] = [];
|
||||||
|
if (baseNameEn && baseNameEn.length >= 3) {
|
||||||
|
candidates.push(baseNameEn, `${baseNameEn}_official`, `${baseNameEn}_ps`, `${baseNameEn}_clinic`);
|
||||||
|
}
|
||||||
|
if (baseName && /^[a-zA-Z]/.test(baseName)) {
|
||||||
|
candidates.push(baseName, `${baseName}_official`, `${baseName}_ps`);
|
||||||
|
}
|
||||||
|
// Also try domain-based
|
||||||
|
const domainBase = new URL(url).hostname.replace('www.', '').split('.')[0].toLowerCase();
|
||||||
|
if (domainBase.length >= 3 && !candidates.includes(domainBase)) {
|
||||||
|
candidates.push(domainBase, `${domainBase}_official`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick check each candidate with Apify
|
||||||
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ usernames: [handle], resultsLimit: 1 }),
|
||||||
model: "sonar",
|
}
|
||||||
messages: [
|
);
|
||||||
{ role: "system", content: "You are a medical platform researcher. Search the web for clinic listings. Respond ONLY with valid JSON, no explanation." },
|
const run = await apifyRes.json();
|
||||||
{ 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}}` },
|
const datasetId = run.data?.defaultDatasetId;
|
||||||
],
|
if (!datasetId) continue;
|
||||||
temperature: 0.1,
|
|
||||||
}),
|
const itemsRes = await fetch(`${APIFY_BASE}/datasets/${datasetId}/items?token=${APIFY_TOKEN}&limit=1`);
|
||||||
});
|
const items = await itemsRes.json();
|
||||||
const data = await res.json();
|
const profile = (items as Record<string, unknown>[])[0];
|
||||||
let text = data.choices?.[0]?.message?.content || "";
|
|
||||||
const jsonMatch = text.match(/```(?:json)?\n?([\s\S]*?)```/);
|
if (profile && !profile.error && (profile.followersCount as number) >= 50) {
|
||||||
if (jsonMatch) text = jsonMatch[1];
|
apiHandles.instagram!.push(profile.username as string || handle);
|
||||||
const parsed = JSON.parse(text);
|
break; // Found one valid — stop searching
|
||||||
if (parsed.gangnamUnni?.url) gangnamUnniHintUrl = parsed.gangnamUnni.url;
|
}
|
||||||
|
} catch { /* try next */ }
|
||||||
|
}
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue