From a7d8aeeddcd53e396064d875b0218d6dbbda7a77 Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Fri, 3 Apr 2026 16:16:37 +0900 Subject: [PATCH] feat: Facebook page data collection via Apify scraper - enrich-channels: add Facebook Pages Scraper (apify~facebook-pages-scraper) - Collects: pageName, followers, likes, categories, email, phone, website, intro, rating - transformReport: merge Facebook data into facebookAudit.pages[] (auto-shows section) - Frontend: pass facebookHandle through enrichment pipeline - EnrichChannelsRequest: add facebookHandle parameter Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/useEnrichment.ts | 2 + src/lib/supabase.ts | 1 + src/lib/transformReport.ts | 39 +++++++++++++++++++ src/pages/ReportPage.tsx | 3 ++ supabase/functions/enrich-channels/index.ts | 42 ++++++++++++++++++++- 5 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/hooks/useEnrichment.ts b/src/hooks/useEnrichment.ts index b98cb0c..7588dee 100644 --- a/src/hooks/useEnrichment.ts +++ b/src/hooks/useEnrichment.ts @@ -16,6 +16,7 @@ interface EnrichmentParams { instagramHandle?: string; instagramHandles?: string[]; youtubeChannelId?: string; + facebookHandle?: string; address?: string; } @@ -45,6 +46,7 @@ export function useEnrichment( instagramHandle: params.instagramHandle, instagramHandles: params.instagramHandles, youtubeChannelId: params.youtubeChannelId, + facebookHandle: params.facebookHandle, address: params.address, }) .then((result) => { diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 15083b7..2cf40ed 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -39,6 +39,7 @@ export interface EnrichChannelsRequest { instagramHandle?: string; instagramHandles?: string[]; youtubeChannelId?: string; + facebookHandle?: string; address?: string; } diff --git a/src/lib/transformReport.ts b/src/lib/transformReport.ts index 9130394..16a3a4e 100644 --- a/src/lib/transformReport.ts +++ b/src/lib/transformReport.ts @@ -439,6 +439,20 @@ export interface EnrichmentData { mapx?: string; mapy?: string; }; + facebook?: { + pageName?: string; + pageUrl?: string; + followers?: number; + likes?: number; + categories?: string[]; + email?: string; + phone?: string; + website?: string; + address?: string; + intro?: string; + rating?: number; + profilePictureUrl?: string; + }; } /** @@ -616,6 +630,31 @@ export function mergeEnrichment( } } + // Facebook enrichment + if (enrichment.facebook) { + const fb = enrichment.facebook; + merged.facebookAudit = { + ...merged.facebookAudit, + pages: [{ + url: fb.pageUrl || '', + pageName: fb.pageName || '', + language: 'KR', + label: '메인', + followers: fb.followers ?? 0, + following: 0, + category: fb.categories?.join(', ') || '', + bio: fb.intro || '', + logo: '', + logoDescription: '', + link: fb.website || '', + linkedDomain: fb.website || '', + reviews: 0, + recentPostAge: '', + hasWhatsApp: false, + }], + }; + } + // 네이버 블로그 enrichment if (enrichment.naverBlog) { const nb = enrichment.naverBlog; diff --git a/src/pages/ReportPage.tsx b/src/pages/ReportPage.tsx index f6f879b..ce9899b 100644 --- a/src/pages/ReportPage.tsx +++ b/src/pages/ReportPage.tsx @@ -63,11 +63,14 @@ export default function ReportPage() { baseData.youtubeAudit?.handle || undefined; + const fbHandle = handles?.facebook || undefined; + return { reportId: baseData.id, clinicName: baseData.clinicSnapshot.name, instagramHandles: igHandles.length > 0 ? igHandles : undefined, youtubeChannelId: ytHandle || undefined, + facebookHandle: fbHandle as string | undefined, address: baseData.clinicSnapshot.location || undefined, }; }, [baseData, isEnriched, dbSocialHandles, location.state]); diff --git a/supabase/functions/enrich-channels/index.ts b/supabase/functions/enrich-channels/index.ts index a17174d..edc8eae 100644 --- a/supabase/functions/enrich-channels/index.ts +++ b/supabase/functions/enrich-channels/index.ts @@ -16,6 +16,7 @@ interface EnrichRequest { instagramHandle?: string; instagramHandles?: string[]; youtubeChannelId?: string; + facebookHandle?: string; address?: string; } @@ -48,7 +49,7 @@ Deno.serve(async (req) => { } try { - const { reportId, clinicName, instagramHandle, instagramHandles, youtubeChannelId, address } = + const { reportId, clinicName, instagramHandle, instagramHandles, youtubeChannelId, facebookHandle, address } = (await req.json()) as EnrichRequest; // Build list of IG handles to try: explicit array > single handle > empty @@ -327,7 +328,44 @@ Deno.serve(async (req) => { } } - // 5. YouTube Channel (using YouTube Data API v3) + // 5. Facebook Page (using Apify) + if (facebookHandle) { + tasks.push( + (async () => { + // Normalize: strip URL parts to get page name, then build full URL + let fbUrl = facebookHandle; + if (!fbUrl.startsWith("http")) { + fbUrl = fbUrl.replace(/^@/, ""); + fbUrl = `https://www.facebook.com/${fbUrl}`; + } + + const items = await runApifyActor( + "apify~facebook-pages-scraper", + { startUrls: [{ url: fbUrl }] }, + APIFY_TOKEN + ); + const page = (items as Record[])[0]; + if (page && page.title) { + enrichment.facebook = { + pageName: page.title, + pageUrl: page.pageUrl || fbUrl, + followers: page.followers, + likes: page.likes, + categories: page.categories, + email: page.email, + phone: page.phone, + website: page.website, + address: page.address, + intro: page.intro, + rating: page.rating, + profilePictureUrl: page.profilePictureUrl, + }; + } + })() + ); + } + + // 6. YouTube Channel (using YouTube Data API v3) if (youtubeChannelId) { const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY"); if (YOUTUBE_API_KEY) {