fix: clinic_registry CSV 임포트 + NaverPlace 검색 개선

- VerifiedChannels에 naverPlace 필드 추가 (registry URL → placeId 전달)
- registryToVerifiedChannels: naver_place_url → placeId 추출하여 포함
- collect-channel-data NaverPlace 매칭 완화: exact match → contains match, 의원/병원 suffix 제거 shortName 사용, placeId 힌트로 검색 보강
- clinic_registry에 73개 병원 CSV 데이터 임포트 (올바른 YouTube/Blog/GangnamUnni/NaverPlace URL)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
Haewon Kam 2026-04-10 14:22:59 +09:00
parent e81c4cfee9
commit cd2463fb2d
3 changed files with 25 additions and 5 deletions

View File

@ -17,6 +17,7 @@ export interface VerifiedChannels {
naverBlog: VerifiedChannel | null; naverBlog: VerifiedChannel | null;
gangnamUnni: VerifiedChannel | null; gangnamUnni: VerifiedChannel | null;
tiktok: VerifiedChannel | null; tiktok: VerifiedChannel | null;
naverPlace?: { url: string; placeId?: string } | null; // registry-sourced place URL
} }
/** /**

View File

@ -560,23 +560,35 @@ Deno.serve(async (req) => {
const npVerified = (verified as Record<string, unknown>).naverPlace as Record<string, unknown> | null; const npVerified = (verified as Record<string, unknown>).naverPlace as Record<string, unknown> | null;
channelTasks.push(wrapChannelTask("naverPlace", async () => { channelTasks.push(wrapChannelTask("naverPlace", async () => {
// ── Fast path: already verified in DB ── // ── Fast path 1: already fully verified in DB (has name) ──
if (npVerified?.name) { if (npVerified?.name) {
console.log(`[naverPlace] Using verified DB data: ${npVerified.name}`); console.log(`[naverPlace] Using verified DB data: ${npVerified.name}`);
channelData.naverPlace = npVerified; channelData.naverPlace = npVerified;
return; return;
} }
// ── Slow path: first-time discovery via domain-matched search ── // ── Fast path 2: registry-sourced place URL → use Naver Place ID directly ──
const registryPlaceUrl = (verified as Record<string, unknown>).naverPlace as { url?: string; placeId?: string } | null;
const registryPlaceId = registryPlaceUrl?.placeId;
if (registryPlaceId) {
console.log(`[naverPlace] Using registry place ID: ${registryPlaceId}`);
// Build the Naver local search with placeId as a hint — use clinic name to fetch details
// We'll pass through to search but prime the query with clinic name
}
// ── Slow path: search via Naver Local API ──
const normalize = (s: string) => (s || '').replace(/<[^>]*>/g, '').toLowerCase(); const normalize = (s: string) => (s || '').replace(/<[^>]*>/g, '').toLowerCase();
let clinicDomain = ''; let clinicDomain = '';
try { clinicDomain = new URL(row.url || '').hostname.replace('www.', ''); } catch { /* skip */ } try { clinicDomain = new URL(row.url || '').hostname.replace('www.', ''); } catch { /* skip */ }
const districtMatch = address.match(/([가-힣]+(구|동))/); const districtMatch = address.match(/([가-힣]+(구|동))/);
const district = districtMatch?.[1] || ''; const district = districtMatch?.[1] || '';
// Strip 의원/병원 suffixes for broader search match
const shortName = clinicName.replace(/의원$|병원$/, '').trim();
const queries = [ const queries = [
...(district ? [`${clinicName} ${district}`, `${clinicName} 성형 ${district}`] : []), ...(district ? [`${clinicName} ${district}`, `${shortName} ${district}`] : []),
`${clinicName} 성형외과`, `${clinicName} 성형외과`,
`${shortName} 성형외과`,
`${clinicName} 성형`, `${clinicName} 성형`,
]; ];
@ -591,9 +603,10 @@ Deno.serve(async (req) => {
const data = await res.json(); const data = await res.json();
const items = (data.items || []) as Record<string, string>[]; const items = (data.items || []) as Record<string, string>[];
// Match by official domain first (most reliable), then exact name + 성형 category // Match priority: (1) domain match, (2) registry place ID in link, (3) name contains match + 성형 category
const match = (clinicDomain ? items.find(i => (i.link || '').includes(clinicDomain)) : null) const match = (clinicDomain ? items.find(i => (i.link || '').includes(clinicDomain)) : null)
?? items.find(i => normalize(i.title) === clinicName.toLowerCase() && (i.category || '').includes('성형')) ?? (registryPlaceId ? items.find(i => (i.link || '').includes(registryPlaceId)) : null)
?? items.find(i => normalize(i.title).includes(shortName.toLowerCase()) && (i.category || '').includes('성형'))
?? null; ?? null;
if (match) { if (match) {

View File

@ -74,6 +74,11 @@ function registryToVerifiedChannels(reg: RegistryRow): import("../_shared/verify
const blogHandle = extractHandleFromUrl(reg.naver_blog_url || '', 'naverBlog'); const blogHandle = extractHandleFromUrl(reg.naver_blog_url || '', 'naverBlog');
const ttHandle = extractHandleFromUrl(reg.tiktok_url || '', 'tiktok'); const ttHandle = extractHandleFromUrl(reg.tiktok_url || '', 'tiktok');
// Extract Naver Place ID from URL (e.g. https://m.place.naver.com/hospital/11709005 → "11709005")
const naverPlaceId = reg.naver_place_url
? (reg.naver_place_url.match(/\/(\d+)\/?$/) || [])[1] || undefined
: undefined;
return { return {
instagram: igHandles, instagram: igHandles,
youtube: ytHandle ? { handle: ytHandle, verified: true, url: reg.youtube_url! } : null, youtube: ytHandle ? { handle: ytHandle, verified: true, url: reg.youtube_url! } : null,
@ -81,6 +86,7 @@ function registryToVerifiedChannels(reg: RegistryRow): import("../_shared/verify
naverBlog: blogHandle ? { handle: blogHandle, verified: true, url: reg.naver_blog_url! } : null, naverBlog: blogHandle ? { handle: blogHandle, verified: true, url: reg.naver_blog_url! } : null,
gangnamUnni: reg.gangnam_unni_url ? { handle: reg.gangnam_unni_url, verified: true, url: reg.gangnam_unni_url } : null, gangnamUnni: reg.gangnam_unni_url ? { handle: reg.gangnam_unni_url, verified: true, url: reg.gangnam_unni_url } : null,
tiktok: ttHandle ? { handle: ttHandle, verified: true, url: reg.tiktok_url! } : null, tiktok: ttHandle ? { handle: ttHandle, verified: true, url: reg.tiktok_url! } : null,
naverPlace: reg.naver_place_url ? { url: reg.naver_place_url, placeId: naverPlaceId } : null,
}; };
} }