o2o-infinith-demo/src/pages/PerformancePage.tsx
Haewon Kam 14c4b82155 refactor(design): 요청문과 설명문을 텍스트 역할 토큰으로 가른다
"한 장씩 봐 주세요" 같은 요청문이 읽고 넘어가는 설명문과 같은 무게로 놓여 있어
지나치기 쉬웠다. 역할별 토큰과 클래스를 두고 화면에 적용한다.

  .ui-ask   요청문. 사람이 무엇을 해야 하는지 말하는 문장 (17px·600·인디고)
  .ui-ask-block  문단으로 서는 요청문. 왼쪽 규칙선으로 설명과 구분한다
  .ui-body  설명 본문 (16px·slate-600)
  .ui-note  보조·출처 (14px·slate-400)

크기는 기존 화면을 세어 맞췄다. text-base 167회·text-sm 58회가 지배적이라
body 를 16px, note 를 14px 로 두면 치환해도 크기가 변하지 않고 역할과 색만 정리된다.

한글 줄바꿈: word-break keep-all 로 어절을 지키고, 긴 URL·영문 토큰만
overflow-wrap anywhere 로 자른다. text-wrap 으로 마지막 줄에 한 어절만 남는 것을
줄이고, 62ch 로 줄 길이를 묶는다. 제목에도 keep-all 과 balance 를 건다.

다크 섹션은 .on-dark 로 값을 뒤집고, 그 안의 흰 카드는 .on-light 로 되돌린다.
이걸 두지 않으면 흰 글씨가 흰 배경에 얹혀 보이지 않는다(디자인 시스템 기록된 실수).

사진 확인 화면의 썸네일 줄이 누를 수 있는 것으로 안 읽혀 같이 고친다. 72px 카드,
호버 반응, 순번 배지, 현재 위치 표시, 안 정한 것 아래 띠, 범례, 이전·다음 이동을 둔다.
상태가 안 보이는 것이 크기보다 큰 문제였다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:56:52 +09:00

500 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { motion } from 'motion/react';
import {
YoutubeFilled,
InstagramFilled,
FacebookFilled,
GlobeFilled,
TiktokFilled,
VideoFilled,
FileTextFilled,
ShareFilled,
} from '../components/icons/FilledIcons';
import type { ComponentType } from 'react';
// ─── Types ───
interface ChannelMetric {
id: string;
name: string;
icon: ComponentType<{ size?: number; className?: string }>;
brandColor: string;
bgColor: string;
followers: string;
followersDelta: string;
views: string;
viewsDelta: string;
engagement: string;
engagementDelta: string;
posts: number;
score: number;
}
interface ContentPerformance {
id: string;
title: string;
channel: string;
type: 'video' | 'blog' | 'social';
views: string;
likes: string;
comments: string;
ctr: string;
publishedAt: string;
}
// ─── Mock Data ───
const CHANNELS: ChannelMetric[] = [
{ id: 'youtube', name: 'YouTube', icon: YoutubeFilled, brandColor: '#FF0000', bgColor: '#FFF0F0', followers: '103K', followersDelta: '+2.1K', views: '270K', viewsDelta: '+18%', engagement: '4.2%', engagementDelta: '+0.8%', posts: 12, score: 65 },
{ id: 'instagram_kr', name: 'Instagram KR', icon: InstagramFilled, brandColor: '#E1306C', bgColor: '#FFF0F5', followers: '14K', followersDelta: '+890', views: '45K', viewsDelta: '+32%', engagement: '3.1%', engagementDelta: '+1.2%', posts: 24, score: 35 },
{ id: 'instagram_en', name: 'Instagram EN', icon: InstagramFilled, brandColor: '#E1306C', bgColor: '#FFF0F5', followers: '68.8K', followersDelta: '+1.2K', views: '120K', viewsDelta: '+8%', engagement: '5.6%', engagementDelta: '+0.3%', posts: 18, score: 55 },
{ id: 'tiktok', name: 'TikTok', icon: TiktokFilled, brandColor: '#000000', bgColor: '#F5F5F5', followers: '0', followersDelta: 'NEW', views: '0', viewsDelta: '-', engagement: '0%', engagementDelta: '-', posts: 0, score: 0 },
{ id: 'facebook', name: 'Facebook', icon: FacebookFilled, brandColor: '#1877F2', bgColor: '#F0F4FF', followers: '341', followersDelta: '+12', views: '2.1K', viewsDelta: '-5%', engagement: '0.8%', engagementDelta: '-0.2%', posts: 6, score: 40 },
{ id: 'naver', name: 'Naver Blog', icon: GlobeFilled, brandColor: '#03C75A', bgColor: '#F0FFF5', followers: '-', followersDelta: '-', views: '8.2K', viewsDelta: '+45%', engagement: '2.4%', engagementDelta: '+1.1%', posts: 8, score: 72 },
];
const TOP_CONTENT: ContentPerformance[] = [
{ id: '1', title: '한번에 성공하는 코성형, VIEW의 비결', channel: 'YouTube', type: 'video', views: '12.4K', likes: '342', comments: '28', ctr: '8.2%', publishedAt: '3일 전' },
{ id: '2', title: '안면윤곽 수술 종류와 회복기간', channel: 'Naver Blog', type: 'blog', views: '3.2K', likes: '-', comments: '12', ctr: '12.5%', publishedAt: '5일 전' },
{ id: '3', title: 'Reel: 윤곽 전후 변화', channel: 'Instagram KR', type: 'social', views: '8.7K', likes: '567', comments: '45', ctr: '6.1%', publishedAt: '2일 전' },
{ id: '4', title: 'Shorts: 사각턱 축소 과정', channel: 'YouTube', type: 'video', views: '5.1K', likes: '189', comments: '15', ctr: '4.8%', publishedAt: '4일 전' },
{ id: '5', title: '코성형 가이드: 내 얼굴에 맞는 코', channel: 'Naver Blog', type: 'blog', views: '2.8K', likes: '-', comments: '8', ctr: '15.2%', publishedAt: '6일 전' },
];
const OVERVIEW_STATS = [
{ label: '총 노출', value: '445K', delta: '+24%', positive: true },
{ label: '총 조회', value: '89.2K', delta: '+18%', positive: true },
{ label: '평균 참여율', value: '3.8%', delta: '+0.6%', positive: true },
{ label: '콘텐츠 발행', value: '68건', delta: '+12건', positive: true },
{ label: '신규 팔로워', value: '+4.3K', delta: '+32%', positive: true },
{ label: '전환 (상담)', value: '47건', delta: '+15건', positive: true },
];
// ─── Funnel Data ───
const FUNNEL_STEPS = [
{ label: '노출', labelEn: 'Impressions', value: 445000, display: '445K', color: '#6C5CE7' },
{ label: '클릭', labelEn: 'Clicks', value: 89200, display: '89.2K', color: '#7C6DD8' },
{ label: '웹사이트 유입', labelEn: 'Website Visits', value: 12400, display: '12.4K', color: '#9B8AD4' },
{ label: '상담 문의', labelEn: 'Inquiries', value: 478, display: '478', color: '#B8A9E8' },
{ label: '예약 전환', labelEn: 'Conversions', value: 47, display: '47', color: '#D5CDF5' },
];
// ─── Channel Trend Data (4 weeks) ───
const CHANNEL_TREND = [
{ week: 'W1', youtube: 85, instagram: 32, naver: 18, facebook: 8 },
{ week: 'W2', youtube: 92, instagram: 41, naver: 24, facebook: 7 },
{ week: 'W3', youtube: 78, instagram: 55, naver: 31, facebook: 9 },
{ week: 'W4', youtube: 105, instagram: 68, naver: 38, facebook: 6 },
];
const TREND_CHANNELS = [
{ key: 'youtube' as const, label: 'YouTube', color: 'rgba(155,138,212,0.35)' },
{ key: 'instagram' as const, label: 'Instagram', color: 'rgba(212,168,186,0.3)' },
{ key: 'naver' as const, label: 'Naver', color: 'rgba(160,200,180,0.3)' },
{ key: 'facebook' as const, label: 'Facebook', color: 'rgba(160,180,220,0.25)' },
];
// ─── Heatmap Data (Day × Time Slot) ───
const DAYS = ['월', '화', '수', '목', '금', '토', '일'];
const TIME_SLOTS = ['오전 (6-12)', '오후 (12-18)', '저녁 (18-24)', '심야 (0-6)'];
// Engagement rate by day × time slot (0-10 scale)
const HEATMAP_DATA = [
[3, 7, 8, 2], // 월
[4, 6, 9, 1], // 화
[5, 8, 7, 2], // 수
[6, 9, 8, 1], // 목
[4, 7, 10, 3], // 금
[2, 5, 6, 4], // 토
[1, 4, 5, 3], // 일
];
// ─── Component ───
const typeIcons: Record<string, ComponentType<{ size?: number; className?: string }>> = {
video: VideoFilled,
blog: FileTextFilled,
social: ShareFilled,
};
const typeColors: Record<string, { bg: string; text: string }> = {
video: { bg: 'bg-[#F3F0FF]', text: 'text-[#4A3A7C]' },
blog: { bg: 'bg-[#EFF0FF]', text: 'text-[#3A3F7C]' },
social: { bg: 'bg-[#FFF6ED]', text: 'text-[#7C5C3A]' },
};
function heatmapColor(value: number): string {
if (value >= 9) return 'bg-[#2d2640] text-white';
if (value >= 7) return 'bg-[#4a4460] text-white';
if (value >= 5) return 'bg-[#8e89a8] text-white';
if (value >= 3) return 'bg-[#c8c4d8] text-[#4a4460]';
return 'bg-[#f0eef5] text-[#8e89a8]';
}
export default function PerformancePage() {
const [period, setPeriod] = useState<'7d' | '30d' | '90d'>('30d');
const funnelMax = FUNNEL_STEPS[0].value;
const trendMax = Math.max(...CHANNEL_TREND.flatMap(w => [w.youtube, w.instagram, w.naver, w.facebook]));
return (
<div className="pt-20 min-h-screen">
{/* Header */}
<div className="on-dark bg-[#0A1128] py-14 px-6 relative overflow-hidden">
<div className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full bg-[#6C5CE7]/10 blur-[120px]" />
<div className="absolute bottom-0 left-0 w-[300px] h-[300px] rounded-full bg-purple-500/5 blur-[100px]" />
<div className="max-w-6xl mx-auto relative">
<p className="text-xs font-semibold text-purple-300 tracking-widest uppercase mb-3">Performance Intelligence</p>
<h1 className="font-serif text-3xl md:text-4xl font-bold text-white mb-3">성과 대시보드</h1>
<p className="text-purple-200/70 max-w-xl mb-8">모든 채널의 마케팅 성과를 실시간으로 모니터링합니다.</p>
<div className="flex gap-2">
{([
{ key: '7d' as const, label: '7일' },
{ key: '30d' as const, label: '30일' },
{ key: '90d' as const, label: '90일' },
]).map(p => (
<button
key={p.key}
onClick={() => setPeriod(p.key)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
period === p.key ? 'bg-white text-[#0A1128]' : 'bg-white/10 text-purple-200 hover:bg-white/20'
}`}
>
{p.label}
</button>
))}
</div>
</div>
</div>
<div className="max-w-6xl mx-auto px-6 py-10">
{/* Overview Stats */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-10">
{OVERVIEW_STATS.map((stat, i) => (
<motion.div
key={stat.label}
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-4"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: i * 0.05 }}
>
<p className="text-xs text-slate-500 mb-1">{stat.label}</p>
<p className="text-xl font-bold text-[#0A1128]">{stat.value}</p>
<p className={`text-xs font-medium mt-1 ${stat.positive ? 'text-[#4A3A7C]' : 'text-[#7C3A4B]'}`}>{stat.delta}</p>
</motion.div>
))}
</div>
{/* ═══ Section 1: Marketing Funnel ═══ */}
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="font-serif font-bold text-xl text-[#0A1128]">마케팅 퍼널</h3>
<p className="text-xs text-slate-500 mt-1">노출부터 전환까지 — 어디서 이탈하는지 파악</p>
</div>
</div>
<div className="space-y-3">
{FUNNEL_STEPS.map((step, i) => {
const widthPct = Math.max((step.value / funnelMax) * 100, 8);
const convRate = i > 0
? ((step.value / FUNNEL_STEPS[i - 1].value) * 100).toFixed(1)
: null;
return (
<motion.div
key={step.label}
className="flex items-center gap-4"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: i * 0.1 }}
>
{/* Label */}
<div className="w-24 shrink-0 text-right">
<p className="text-sm font-medium text-[#0A1128]">{step.label}</p>
<p className="text-xs text-slate-400">{step.labelEn}</p>
</div>
{/* Bar */}
<div className="flex-1 relative">
<motion.div
className="h-10 rounded-xl flex items-center px-4"
style={{ backgroundColor: step.color }}
initial={{ width: 0 }}
animate={{ width: `${widthPct}%` }}
transition={{ duration: 0.7, delay: i * 0.12 }}
>
<span className="text-sm font-bold text-white whitespace-nowrap">{step.display}</span>
</motion.div>
</div>
{/* Conversion Rate */}
<div className="w-16 shrink-0 text-right">
{convRate ? (
<span className={`text-xs font-semibold px-2 py-1 rounded-full ${
parseFloat(convRate) >= 10
? 'bg-[#F3F0FF] text-[#4A3A7C]'
: 'bg-[#FFF6ED] text-[#7C5C3A]'
}`}>
{convRate}%
</span>
) : (
<span className="text-xs text-slate-300">—</span>
)}
</div>
</motion.div>
);
})}
</div>
{/* Funnel insight */}
<div className="mt-6 p-4 rounded-xl bg-[#FFF6ED] border border-[#F5E0C5]">
<p className="text-sm text-[#7C5C3A]">
<span className="font-semibold">병목 구간:</span> 클릭 → 웹사이트 유입 전환율 <span className="font-bold">13.9%</span> — 랜딩 페이지 최적화가 필요합니다. 업계 평균 20% 대비 낮음.
</p>
</div>
</div>
{/* ═══ Section 2: Channel Trend (Stacked Bar) ═══ */}
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="font-serif font-bold text-xl text-[#0A1128]">채널별 주간 트렌드</h3>
<p className="text-xs text-slate-500 mt-1">채널별 조회수 추이 비교 (단위: K)</p>
</div>
{/* Legend */}
<div className="flex gap-3">
{TREND_CHANNELS.map(ch => (
<div key={ch.key} className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: ch.color }} />
<span className="text-xs text-slate-500">{ch.label}</span>
</div>
))}
</div>
</div>
<div className="flex items-end justify-center gap-10 h-[220px] px-8">
{CHANNEL_TREND.map((week, wi) => {
const total = week.youtube + week.instagram + week.naver + week.facebook;
return (
<div key={week.week} className="flex flex-col items-center gap-2" style={{ width: '80px' }}>
{/* Total label above bar */}
<p className="text-xs text-slate-500 font-medium">{total}K</p>
{/* Stacked bar */}
<div className="w-full flex flex-col-reverse items-stretch rounded-xl overflow-hidden" style={{ height: `${(total / (trendMax * 1.5)) * 160}px` }}>
{TREND_CHANNELS.map(ch => {
const val = week[ch.key];
const segH = (val / total) * 100;
return (
<motion.div
key={ch.key}
className="w-full relative group"
style={{ height: `${segH}%`, backgroundColor: ch.color }}
initial={{ scaleY: 0 }}
animate={{ scaleY: 1 }}
transition={{ duration: 0.5, delay: wi * 0.1 }}
>
<div className="on-dark absolute -top-8 left-1/2 -translate-x-1/2 hidden group-hover:block bg-[#0A1128] text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10">
{ch.label}: {val}K
</div>
</motion.div>
);
})}
</div>
{/* Week label */}
<p className="text-xs font-medium text-slate-600">{week.week}</p>
</div>
);
})}
</div>
{/* Trend insight */}
<div className="mt-6 p-4 rounded-xl bg-[#F3F0FF] border border-[#D5CDF5]">
<p className="text-sm text-[#4A3A7C]">
<span className="font-semibold">성장 채널:</span> Instagram 조회수 <span className="font-bold">+112%</span> (W1→W4). Naver Blog <span className="font-bold">+111%</span> 동반 성장. YouTube는 안정적 유지.
</p>
</div>
</div>
{/* ═══ Section 3: Day × Time Heatmap ═══ */}
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="font-serif font-bold text-xl text-[#0A1128]">최적 게시 시간</h3>
<p className="text-xs text-slate-500 mt-1">요일×시간대별 참여율 히트맵 — 진할수록 성과가 높음</p>
</div>
{/* Scale legend */}
<div className="flex items-center gap-1">
<span className="text-xs text-slate-400">낮음</span>
{[1, 3, 5, 7, 9].map(v => (
<div key={v} className={`w-4 h-4 rounded ${heatmapColor(v)}`} />
))}
<span className="text-xs text-slate-400">높음</span>
</div>
</div>
{/* Heatmap grid */}
<div className="overflow-x-auto">
<div className="min-w-[500px]">
{/* Time slot headers */}
<div className="grid grid-cols-[60px_repeat(4,1fr)] gap-2 mb-2">
<div />
{TIME_SLOTS.map(slot => (
<div key={slot} className="text-center text-xs text-slate-500 font-medium">{slot}</div>
))}
</div>
{/* Rows */}
{DAYS.map((day, di) => (
<motion.div
key={day}
className="grid grid-cols-[60px_repeat(4,1fr)] gap-2 mb-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: di * 0.05 }}
>
<div className="flex items-center justify-center text-sm font-medium text-[#0A1128]">{day}</div>
{HEATMAP_DATA[di].map((val, ti) => (
<div
key={ti}
className={`h-12 rounded-xl flex items-center justify-center text-sm font-semibold transition-all hover:scale-105 cursor-default ${heatmapColor(val)}`}
>
{val > 0 ? `${val * 10}%` : '-'}
</div>
))}
</motion.div>
))}
</div>
</div>
{/* Heatmap insight */}
<div className="mt-6 p-4 rounded-xl bg-[#F3F0FF] border border-[#D5CDF5]">
<p className="text-sm text-[#4A3A7C]">
<span className="font-semibold">최적 시간:</span> <span className="font-bold">금요일 저녁 (18-24시)</span> 참여율 최고. 평일 오후 (12-18시)가 전반적으로 높음. 주말 오전은 피하세요.
</p>
</div>
</div>
{/* Channel Performance Grid */}
<h3 className="font-serif font-bold text-xl text-[#0A1128] mb-4">채널별 성과</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-10">
{CHANNELS.map((ch, i) => {
const Icon = ch.icon;
return (
<motion.div
key={ch.id}
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-5"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: i * 0.08 }}
>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: ch.bgColor }}>
<Icon size={20} style={{ color: ch.brandColor }} />
</div>
<div className="flex-1">
<h4 className="text-sm font-semibold text-[#0A1128]">{ch.name}</h4>
<p className="text-xs text-slate-400">{ch.posts}개 콘텐츠</p>
</div>
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold ${
ch.score >= 70 ? 'bg-[#F3F0FF] text-[#4A3A7C]' :
ch.score >= 40 ? 'bg-[#FFF6ED] text-[#7C5C3A]' :
ch.score > 0 ? 'bg-[#FFF0F0] text-[#7C3A4B]' :
'bg-slate-50 text-slate-400'
}`}>
{ch.score || '-'}
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<MetricCell label="팔로워" value={ch.followers} delta={ch.followersDelta} />
<MetricCell label="조회수" value={ch.views} delta={ch.viewsDelta} />
<MetricCell label="참여율" value={ch.engagement} delta={ch.engagementDelta} />
</div>
</motion.div>
);
})}
</div>
{/* Top Content */}
<h3 className="font-serif font-bold text-xl text-[#0A1128] mb-4">인기 콘텐츠 TOP 5</h3>
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden mb-10">
<div className="on-dark grid grid-cols-[1fr_100px_80px_80px_60px_70px_70px] gap-2 px-5 py-3 bg-[#0A1128] text-white text-xs font-medium">
<span>콘텐츠</span>
<span>채널</span>
<span className="text-right">조회수</span>
<span className="text-right">좋아요</span>
<span className="text-right">댓글</span>
<span className="text-right">CTR</span>
<span className="text-right">게시일</span>
</div>
{TOP_CONTENT.map((content, i) => {
const TypeIcon = typeIcons[content.type] ?? FileTextFilled;
const colors = typeColors[content.type] ?? typeColors.blog;
return (
<motion.div
key={content.id}
className={`grid grid-cols-[1fr_100px_80px_80px_60px_70px_70px] gap-2 px-5 py-4 items-center ${
i % 2 === 0 ? 'bg-white' : 'bg-slate-50/50'
} border-b border-slate-50 last:border-0`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: i * 0.08 }}
>
<div className="flex items-center gap-2 min-w-0">
<div className={`w-7 h-7 rounded-lg flex items-center justify-center shrink-0 ${colors.bg}`}>
<TypeIcon size={14} className={colors.text} />
</div>
<span className="text-sm text-[#0A1128] truncate">{content.title}</span>
</div>
<span className="text-xs text-slate-500">{content.channel}</span>
<span className="text-sm font-medium text-[#0A1128] text-right">{content.views}</span>
<span className="text-sm text-slate-600 text-right">{content.likes}</span>
<span className="text-sm text-slate-600 text-right">{content.comments}</span>
<span className={`text-sm font-medium text-right ${
parseFloat(content.ctr) >= 10 ? 'text-[#4A3A7C]' : 'text-slate-600'
}`}>{content.ctr}</span>
<span className="text-xs text-slate-400 text-right">{content.publishedAt}</span>
</motion.div>
);
})}
</div>
{/* AI Recommendations */}
<div className="bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] rounded-2xl p-8">
<h3 className="font-serif font-bold text-xl text-[#021341] mb-4">AI 개선 추천</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[
{ title: 'YouTube Shorts 확대', desc: 'Shorts 조회수가 Long-form 대비 3.2배 높습니다. 주 3회 이상 Shorts 업로드를 권장합니다.' },
{ title: 'Instagram Reels 시작', desc: 'KR 계정에 Reels 0개입니다. 경쟁 병원 평균 주 5개 — 즉시 시작이 필요합니다.' },
{ title: '랜딩 페이지 최적화', desc: '클릭→유입 전환율 13.9%로 업계 평균 20% 대비 낮음. CTA 버튼 위치와 페이지 속도 개선 필요.' },
].map((rec, i) => (
<div key={i} className="bg-white/70 backdrop-blur-sm rounded-xl border border-white/40 p-5">
<h4 className="font-semibold text-[#021341] mb-2">{rec.title}</h4>
<p className="text-sm text-[#021341]/60">{rec.desc}</p>
</div>
))}
</div>
</div>
</div>
</div>
);
}
function MetricCell({ label, value, delta }: { label: string; value: string; delta: string }) {
const isPositive = delta.startsWith('+');
const isNew = delta === 'NEW' || delta === '-';
return (
<div className="text-center">
<p className="text-xs text-slate-400 mb-1">{label}</p>
<p className="text-sm font-semibold text-[#0A1128]">{value}</p>
<p className={`text-xs font-medium ${
isNew ? 'text-slate-400' : isPositive ? 'text-[#4A3A7C]' : 'text-[#7C3A4B]'
}`}>{delta}</p>
</div>
);
}