154 lines
5.8 KiB
TypeScript
154 lines
5.8 KiB
TypeScript
import { motion } from 'motion/react';
|
|
import {
|
|
VideoFilled,
|
|
FileTextFilled,
|
|
ShareFilled,
|
|
MegaphoneFilled,
|
|
} from '../icons/FilledIcons';
|
|
import { SectionWrapper } from '../report/ui/SectionWrapper';
|
|
import type { CalendarData, ContentCategory, CalendarEntry } from '../../types/plan';
|
|
|
|
interface ContentCalendarProps {
|
|
data: CalendarData;
|
|
}
|
|
|
|
const contentTypeColors: Record<ContentCategory, { bg: string; text: string; entry: string; border: string; shadow: string }> = {
|
|
video: { bg: 'bg-[#F3F0FF]', text: 'text-[#6C5CE7]', entry: 'bg-[#F3F0FF] border-[#D5CDF5]', border: 'border-[#D5CDF5]', shadow: 'shadow-[2px_3px_8px_rgba(155,138,212,0.15)]' },
|
|
blog: { bg: 'bg-[#EFF0FF]', text: 'text-[#3A3F7C]', entry: 'bg-[#EFF0FF] border-[#C5CBF5]', border: 'border-[#C5CBF5]', shadow: 'shadow-[2px_3px_8px_rgba(122,132,212,0.15)]' },
|
|
social: { bg: 'bg-[#FFF6ED]', text: 'text-[#7C5C3A]', entry: 'bg-[#FFF6ED] border-[#F5E0C5]', border: 'border-[#F5E0C5]', shadow: 'shadow-[2px_3px_8px_rgba(212,168,114,0.15)]' },
|
|
ad: { bg: 'bg-[#FFF0F0]', text: 'text-[#7C3A4B]', entry: 'bg-[#FFF0F0] border-[#F5D5DC]', border: 'border-[#F5D5DC]', shadow: 'shadow-[2px_3px_8px_rgba(212,136,154,0.15)]' },
|
|
};
|
|
|
|
const contentTypeLabels: Record<ContentCategory, string> = {
|
|
video: 'Video',
|
|
blog: 'Blog',
|
|
social: 'Social',
|
|
ad: 'Ad',
|
|
};
|
|
|
|
const contentTypeIcons: Record<ContentCategory, typeof VideoFilled> = {
|
|
video: VideoFilled,
|
|
blog: FileTextFilled,
|
|
social: ShareFilled,
|
|
ad: MegaphoneFilled,
|
|
};
|
|
|
|
const dayHeaders = ['월', '화', '수', '목', '금', '토', '일'];
|
|
|
|
export default function ContentCalendar({ data }: ContentCalendarProps) {
|
|
return (
|
|
<SectionWrapper
|
|
id="content-calendar"
|
|
title="Content Calendar"
|
|
subtitle="콘텐츠 캘린더 (월간)"
|
|
dark
|
|
>
|
|
{/* Monthly Summary */}
|
|
<div className="flex flex-wrap gap-4 mb-8">
|
|
{data.monthlySummary.map((item) => {
|
|
const colors = contentTypeColors[item.type];
|
|
return (
|
|
<motion.div
|
|
key={item.type}
|
|
className={`flex-1 min-w-[140px] rounded-2xl border p-4 ${colors.bg} ${colors.border} ${colors.shadow}`}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span
|
|
className="w-3 h-3 rounded-full"
|
|
style={{ backgroundColor: item.color }}
|
|
/>
|
|
<span className={`text-sm font-medium ${colors.text}`}>{item.label}</span>
|
|
</div>
|
|
<span className={`text-2xl font-bold ${colors.text}`}>{item.count}</span>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Weekly Calendar Grid */}
|
|
{data.weeks.map((week, weekIdx) => {
|
|
const dayCells: CalendarEntry[][] = Array.from({ length: 7 }, () => []);
|
|
for (const entry of week.entries) {
|
|
const dayIndex = entry.dayOfWeek;
|
|
if (dayIndex >= 0 && dayIndex <= 6) {
|
|
dayCells[dayIndex].push(entry);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<motion.div
|
|
key={week.weekNumber}
|
|
className="bg-white rounded-2xl p-5 mb-4 shadow-[3px_4px_12px_rgba(0,0,0,0.06)]"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.4, delay: weekIdx * 0.1 }}
|
|
>
|
|
<p className="text-sm font-semibold text-[#0A1128] mb-3">{week.label}</p>
|
|
<div className="grid grid-cols-7 gap-2">
|
|
{/* Day headers */}
|
|
{dayHeaders.map((day) => (
|
|
<div
|
|
key={day}
|
|
className="text-xs text-slate-400 uppercase tracking-wide text-center mb-1 font-medium"
|
|
>
|
|
{day}
|
|
</div>
|
|
))}
|
|
|
|
{/* Day cells */}
|
|
{dayCells.map((entries, dayIdx) => (
|
|
<div
|
|
key={dayIdx}
|
|
className={`min-h-[80px] rounded-xl p-2 ${
|
|
entries.length > 0
|
|
? 'bg-slate-50/50 border border-slate-100'
|
|
: 'border border-dashed border-slate-200/60'
|
|
}`}
|
|
>
|
|
{entries.map((entry, entryIdx) => {
|
|
const colors = contentTypeColors[entry.contentType];
|
|
const Icon = contentTypeIcons[entry.contentType];
|
|
return (
|
|
<div
|
|
key={entryIdx}
|
|
className={`${colors.entry} border rounded-lg p-2 mb-1 shadow-[2px_2px_6px_rgba(0,0,0,0.04)]`}
|
|
>
|
|
<div className="flex items-center gap-1 mb-1">
|
|
<Icon size={11} className={colors.text} />
|
|
</div>
|
|
<p className="text-sm text-slate-700 leading-tight">
|
|
{entry.title}
|
|
</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
|
|
{/* Color Legend */}
|
|
<div className="flex flex-wrap gap-3 mt-4">
|
|
{(Object.keys(contentTypeColors) as ContentCategory[]).map((type) => {
|
|
const colors = contentTypeColors[type];
|
|
return (
|
|
<span
|
|
key={type}
|
|
className={`${colors.bg} ${colors.text} border ${colors.border} rounded-full px-3 py-1 text-xs font-medium ${colors.shadow}`}
|
|
>
|
|
{contentTypeLabels[type]}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</SectionWrapper>
|
|
);
|
|
}
|