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) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-03 16:16:37 +09:00
parent ad625e08ee
commit a7d8aeeddc
5 changed files with 85 additions and 2 deletions

View File

@ -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) => {

View File

@ -39,6 +39,7 @@ export interface EnrichChannelsRequest {
instagramHandle?: string;
instagramHandles?: string[];
youtubeChannelId?: string;
facebookHandle?: string;
address?: string;
}

View File

@ -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;

View File

@ -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]);

View File

@ -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<string, unknown>[])[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) {