fix: KPI dashboard always generates comprehensive 10+ metrics

Previously AI-provided kpiTargets (often only 3-4 items) would
completely replace our channel-based KPI generation. Now we always
build the full set (YouTube, Instagram, Naver, 강남언니, Google Maps,
cross-platform) and merge AI extras that don't overlap.

Also adds 강남언니 평점/리뷰, 네이버 플레이스 평점 as standard KPIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-03 22:22:40 +09:00
parent 5239ad7382
commit ff82c9f9d5
1 changed files with 59 additions and 13 deletions

View File

@ -332,19 +332,8 @@ function buildRoadmap(r: ApiReport): import('../types/report').RoadmapMonth[] {
} }
function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[] { function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[] {
// If AI provided explicit KPIs, use them // Always build comprehensive KPIs from channel data.
if (r.kpiTargets?.length) { // AI-provided kpiTargets are merged in (fill gaps) but don't replace our metrics.
return r.kpiTargets
.filter((k): k is { metric?: string; current?: string; target3Month?: string; target12Month?: string } => !!k?.metric)
.map(k => ({
metric: k.metric || '',
current: k.current || '-',
target3Month: k.target3Month || '-',
target12Month: k.target12Month || '-',
}));
}
// Build comprehensive KPI from channel data
const channels = r.channelAnalysis || {}; const channels = r.channelAnalysis || {};
const metrics: import('../types/report').KPIMetric[] = []; const metrics: import('../types/report').KPIMetric[] = [];
@ -404,6 +393,36 @@ function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[]
}); });
} }
// 강남언니
if (channels.gangnamUnni) {
const rating = channels.gangnamUnni.rating ?? 0;
const correctedRating = typeof rating === 'number' && rating > 0 && rating <= 5 ? rating * 2 : rating;
metrics.push({
metric: '강남언니 평점',
current: correctedRating > 0 ? `${correctedRating}/10` : '-',
target3Month: correctedRating > 0 ? `${Math.min(correctedRating + 0.5, 10).toFixed(1)}/10` : '8.0/10',
target12Month: correctedRating > 0 ? `${Math.min(correctedRating + 1.0, 10).toFixed(1)}/10` : '9.0/10',
});
if (channels.gangnamUnni.reviews) {
metrics.push({
metric: '강남언니 리뷰 수',
current: fmt(channels.gangnamUnni.reviews as number),
target3Month: fmt(Math.round((channels.gangnamUnni.reviews as number) * 1.15)),
target12Month: fmt(Math.round((channels.gangnamUnni.reviews as number) * 1.5)),
});
}
}
// Google Maps
if (channels.naverPlace) {
metrics.push({
metric: '네이버 플레이스 평점',
current: channels.naverPlace.rating ? `${channels.naverPlace.rating}/5` : '-',
target3Month: '4.5/5',
target12Month: '4.8/5',
});
}
// Cross-platform // Cross-platform
metrics.push({ metrics.push({
metric: '웹사이트 + SNS 유입', metric: '웹사이트 + SNS 유입',
@ -419,6 +438,33 @@ function buildKpiDashboard(r: ApiReport): import('../types/report').KPIMetric[]
target12Month: '월 50건', target12Month: '월 50건',
}); });
// Merge AI-provided KPIs that we didn't already cover
if (r.kpiTargets?.length) {
const existingNames = new Set(metrics.map(m => m.metric.toLowerCase()));
for (const k of r.kpiTargets) {
if (!k?.metric) continue;
// Skip if we already have a similar metric
const lower = k.metric.toLowerCase();
if (existingNames.has(lower)) continue;
if (lower.includes('youtube') && existingNames.has('youtube 구독자')) continue;
if (lower.includes('instagram') && existingNames.has('instagram kr 팔로워')) continue;
if (lower.includes('강남언니') || lower.includes('gangnam')) {
// Use AI's gangnamunni data — update existing or add
const guIdx = metrics.findIndex(m => m.metric.includes('강남언니'));
if (guIdx >= 0) {
metrics[guIdx] = { metric: k.metric, current: k.current || '-', target3Month: k.target3Month || '-', target12Month: k.target12Month || '-' };
continue;
}
}
metrics.push({
metric: k.metric,
current: k.current || '-',
target3Month: k.target3Month || '-',
target12Month: k.target12Month || '-',
});
}
}
return metrics; return metrics;
} }