diff --git a/supabase/functions/discover-channels/index.ts b/supabase/functions/discover-channels/index.ts index 0189f5c..bcef139 100644 --- a/supabase/functions/discover-channels/index.ts +++ b/supabase/functions/discover-channels/index.ts @@ -283,7 +283,7 @@ Deno.serve(async (req) => { } catch { /* skip */ } })()); - // ─── B4. Perplexity: Supplement — catch what APIs missed ─── + // ─── B4. Perplexity: Online Presence 종합 분석 ─── if (PERPLEXITY_API_KEY) { stageBTasks.push((async () => { try { @@ -293,10 +293,36 @@ Deno.serve(async (req) => { body: JSON.stringify({ model: "sonar", 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: "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: "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}" 병원의 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(); @@ -305,44 +331,69 @@ Deno.serve(async (req) => { if (jsonMatch) text = jsonMatch[1]; const parsed = JSON.parse(text); - const ph = { - instagram: Array.isArray(parsed.instagram) ? parsed.instagram : parsed.instagram ? [parsed.instagram] : [], - youtube: parsed.youtube ? [parsed.youtube] : [], - facebook: parsed.facebook ? [parsed.facebook] : [], - naverBlog: parsed.naverBlog ? [parsed.naverBlog] : [], - tiktok: parsed.tiktok ? [parsed.tiktok] : [], - kakao: parsed.kakao ? [parsed.kakao] : [], - }; - if (ph.instagram.length) apiHandles.instagram!.push(...ph.instagram); - if (ph.youtube.length) apiHandles.youtube!.push(...ph.youtube); - if (ph.facebook.length) apiHandles.facebook!.push(...ph.facebook); - if (ph.naverBlog.length) apiHandles.naverBlog!.push(...ph.naverBlog); - if (ph.tiktok.length) apiHandles.tiktok!.push(...ph.tiktok); - if (ph.kakao.length) apiHandles.kakao!.push(...ph.kakao); + // Extract social handles + const igArr = Array.isArray(parsed.instagram) ? parsed.instagram : parsed.instagram ? [parsed.instagram] : []; + const ytArr = Array.isArray(parsed.youtube) ? parsed.youtube : parsed.youtube ? [parsed.youtube] : []; + if (igArr.length) apiHandles.instagram!.push(...igArr); + if (ytArr.length) apiHandles.youtube!.push(...ytArr); + if (parsed.facebook) apiHandles.facebook!.push(typeof parsed.facebook === 'string' ? parsed.facebook : ''); + 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 (parsed.kakao) apiHandles.kakao!.push(typeof parsed.kakao === 'string' ? parsed.kakao : ''); + + // Extract platform presence hints + if (parsed.gangnamUnni?.url) gangnamUnniHintUrl = parsed.gangnamUnni.url; } 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 () => { try { - const res = await fetch("https://api.perplexity.ai/chat/completions", { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` }, - body: JSON.stringify({ - 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." }, - { 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}}` }, - ], - temperature: 0.1, - }), - }); - const data = await res.json(); - let text = data.choices?.[0]?.message?.content || ""; - const jsonMatch = text.match(/```(?:json)?\n?([\s\S]*?)```/); - if (jsonMatch) text = jsonMatch[1]; - const parsed = JSON.parse(text); - if (parsed.gangnamUnni?.url) gangnamUnniHintUrl = parsed.gangnamUnni.url; + // 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", + 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[])[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 */ } })()); }