fix: KPI uses real enrichment data instead of AI guesses

buildKpiDashboard now reads channelEnrichment (real API data from
Phase 2) with fallback to channelAnalysis (AI-generated). YouTube
subscribers, Instagram followers, 강남언니 rating/reviews all use
verified data when available. Fixed || ?? operator precedence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-03 22:28:11 +09:00
parent ff82c9f9d5
commit 9d06272073
1 changed files with 49 additions and 33 deletions

View File

@ -333,24 +333,30 @@ function buildRoadmap(r: ApiReport): import('../types/report').RoadmapMonth[] {
function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[] { function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[] {
// Always build comprehensive KPIs from channel data. // Always build comprehensive KPIs from channel data.
// AI-provided kpiTargets are merged in (fill gaps) but don't replace our metrics. // Prefer real enrichment data over AI-guessed channelAnalysis.
const channels = r.channelAnalysis || {}; const channels = r.channelAnalysis || {};
const enrichment = (r as Record<string, unknown>).channelEnrichment as Record<string, unknown> | undefined;
const metrics: import('../types/report').KPIMetric[] = []; const metrics: import('../types/report').KPIMetric[] = [];
// YouTube metrics // YouTube metrics — prefer enrichment (real API data) over AI guess
if (channels.youtube) { const ytEnrich = enrichment?.youtube as Record<string, unknown> | undefined;
const subs = channels.youtube.subscribers ?? 0; const ytSubs = (ytEnrich?.subscribers as number) || ((channels.youtube?.subscribers as number) ?? 0);
const ytViews = (ytEnrich?.totalViews as number) || 0;
const ytVideos = (ytEnrich?.totalVideos as number) || 0;
if (channels.youtube || ytEnrich) {
metrics.push({ metrics.push({
metric: 'YouTube 구독자', metric: 'YouTube 구독자',
current: subs > 0 ? fmt(subs) : '-', current: ytSubs > 0 ? fmt(ytSubs) : '-',
target3Month: subs > 0 ? fmt(Math.round(subs * 1.1)) : '115K', target3Month: ytSubs > 0 ? fmt(Math.round(ytSubs * 1.1)) : '10K',
target12Month: subs > 0 ? fmt(Math.round(subs * 2)) : '200K', target12Month: ytSubs > 0 ? fmt(Math.round(ytSubs * 2)) : '50K',
}); });
const monthlyViews = ytViews > 0 && ytVideos > 0 ? Math.round(ytViews / Math.max(ytVideos / 12, 1)) : 0;
metrics.push({ metrics.push({
metric: 'YouTube 월 조회수', metric: 'YouTube 월 조회수',
current: '~270K', current: monthlyViews > 0 ? `~${fmt(monthlyViews)}` : '-',
target3Month: '500K', target3Month: monthlyViews > 0 ? fmt(Math.round(monthlyViews * 2)) : '500K',
target12Month: '1.5M', target12Month: monthlyViews > 0 ? fmt(Math.round(monthlyViews * 5)) : '1.5M',
}); });
metrics.push({ metrics.push({
metric: 'YouTube Shorts 평균 조회수', metric: 'YouTube Shorts 평균 조회수',
@ -360,14 +366,18 @@ function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[]
}); });
} }
// Instagram metrics // Instagram metrics — prefer enrichment data
if (channels.instagram) { const igAccounts = (enrichment?.instagramAccounts as Record<string, unknown>[]) || [];
const followers = channels.instagram.followers ?? 0; const igPrimary = igAccounts[0] || (enrichment?.instagram as Record<string, unknown>) || null;
const igSecondary = igAccounts[1] || null;
const igFollowers = (igPrimary?.followers as number) || ((channels.instagram?.followers as number) ?? 0);
if (channels.instagram || igPrimary) {
metrics.push({ metrics.push({
metric: 'Instagram KR 팔로워', metric: 'Instagram KR 팔로워',
current: followers > 0 ? fmt(followers) : '-', current: igFollowers > 0 ? fmt(igFollowers) : '-',
target3Month: followers > 0 ? fmt(Math.round(followers * 1.4)) : '20K', target3Month: igFollowers > 0 ? fmt(Math.round(igFollowers * 1.4)) : '20K',
target12Month: followers > 0 ? fmt(Math.round(followers * 3.5)) : '50K', target12Month: igFollowers > 0 ? fmt(Math.round(igFollowers * 3.5)) : '50K',
}); });
metrics.push({ metrics.push({
metric: 'Instagram KR Reels 평균 조회수', metric: 'Instagram KR Reels 평균 조회수',
@ -375,13 +385,16 @@ function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[]
target3Month: '3,000', target3Month: '3,000',
target12Month: '10,000', target12Month: '10,000',
}); });
if (igSecondary) {
const enFollowers = (igSecondary.followers as number) || 0;
metrics.push({ metrics.push({
metric: 'Instagram EN 팔로워', metric: 'Instagram EN 팔로워',
current: channels.instagram.followers ? fmt(Math.round(channels.instagram.followers * 4.5)) : '68.8K', current: enFollowers > 0 ? fmt(enFollowers) : '-',
target3Month: '75K', target3Month: enFollowers > 0 ? fmt(Math.round(enFollowers * 1.1)) : '75K',
target12Month: '100K', target12Month: enFollowers > 0 ? fmt(Math.round(enFollowers * 1.5)) : '100K',
}); });
} }
}
// Naver Blog // Naver Blog
if (channels.naverBlog) { if (channels.naverBlog) {
@ -393,22 +406,25 @@ function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[]
}); });
} }
// 강남언니 // 강남언니 — prefer enrichment data
if (channels.gangnamUnni) { const guEnrich = enrichment?.gangnamUnni as Record<string, unknown> | undefined;
const rating = channels.gangnamUnni.rating ?? 0; const guRating = (guEnrich?.rating as number) || ((channels.gangnamUnni?.rating as number) ?? 0);
const correctedRating = typeof rating === 'number' && rating > 0 && rating <= 5 ? rating * 2 : rating; const guCorrected = typeof guRating === 'number' && guRating > 0 && guRating <= 5 ? guRating * 2 : guRating;
const guReviews = (guEnrich?.totalReviews as number) || ((channels.gangnamUnni?.reviews as number) ?? 0);
if (channels.gangnamUnni || guEnrich) {
metrics.push({ metrics.push({
metric: '강남언니 평점', metric: '강남언니 평점',
current: correctedRating > 0 ? `${correctedRating}/10` : '-', current: guCorrected > 0 ? `${guCorrected}/10` : '-',
target3Month: correctedRating > 0 ? `${Math.min(correctedRating + 0.5, 10).toFixed(1)}/10` : '8.0/10', target3Month: guCorrected > 0 ? `${Math.min(guCorrected + 0.5, 10).toFixed(1)}/10` : '8.0/10',
target12Month: correctedRating > 0 ? `${Math.min(correctedRating + 1.0, 10).toFixed(1)}/10` : '9.0/10', target12Month: guCorrected > 0 ? `${Math.min(guCorrected + 1.0, 10).toFixed(1)}/10` : '9.0/10',
}); });
if (channels.gangnamUnni.reviews) { if (guReviews > 0) {
metrics.push({ metrics.push({
metric: '강남언니 리뷰 수', metric: '강남언니 리뷰 수',
current: fmt(channels.gangnamUnni.reviews as number), current: fmt(guReviews),
target3Month: fmt(Math.round((channels.gangnamUnni.reviews as number) * 1.15)), target3Month: fmt(Math.round(guReviews * 1.15)),
target12Month: fmt(Math.round((channels.gangnamUnni.reviews as number) * 1.5)), target12Month: fmt(Math.round(guReviews * 1.5)),
}); });
} }
} }