import React, { useState, useCallback } from 'react'; import { exportCalendarToICS } from '../../lib/calendarExport'; import { VideoFilled, FileTextFilled, ShareFilled, MegaphoneFilled, YoutubeFilled, InstagramFilled, FacebookFilled, GlobeFilled, TiktokFilled, MessageFilled, CalendarFilled, } from '../icons/FilledIcons'; import { SectionWrapper } from '../report/ui/SectionWrapper'; import EditEntryModal from './EditEntryModal'; import type { CalendarData, ContentCategory, CalendarEntry } from '../../types/plan'; interface ContentCalendarProps { data: CalendarData; planId?: string; onEntryUpdate?: (entryId: string, updates: Partial) => void; } const contentTypeColors: Record = { video: { bg: 'bg-[#F3F0FF]', text: 'text-[#6C5CE7]', entry: 'bg-[#EDE5FF] border-[#B8A8E8]', border: 'border-[#D5CDF5]', shadow: 'shadow-[2px_3px_8px_rgba(155,138,212,0.15)]' }, blog: { bg: 'bg-[#EFF0FF]', text: 'text-[#3A3F7C]', entry: 'bg-[#E2E5FF] border-[#A8B0E8]', border: 'border-[#C5CBF5]', shadow: 'shadow-[2px_3px_8px_rgba(122,132,212,0.15)]' }, social: { bg: 'bg-[#FFF6ED]', text: 'text-[#7C5C3A]', entry: 'bg-[#FFEED9] border-[#E8C896]', border: 'border-[#F5E0C5]', shadow: 'shadow-[2px_3px_8px_rgba(212,168,114,0.15)]' }, ad: { bg: 'bg-[#FFF0F0]', text: 'text-[#7C3A4B]', entry: 'bg-[#FFE0E0] border-[#E8A8B4]', border: 'border-[#F5D5DC]', shadow: 'shadow-[2px_3px_8px_rgba(212,136,154,0.15)]' }, }; const contentTypeLabels: Record = { video: 'Video', blog: 'Blog', social: 'Social', ad: 'Ad', }; const contentTypeIcons: Record = { video: VideoFilled, blog: FileTextFilled, social: ShareFilled, ad: MegaphoneFilled, }; // FilledIcon map for channel identification in calendar entries const channelIconMap: Record = { youtube: YoutubeFilled, instagram: InstagramFilled, facebook: FacebookFilled, blog: FileTextFilled, globe: GlobeFilled, video: TiktokFilled, star: GlobeFilled, map: GlobeFilled, messagesquare: MessageFilled, }; // Semantic status colors (no raw green/red per design system) const statusDotColors: Record = { draft: 'bg-slate-400', approved: 'bg-[#9B8AD4]', published: 'bg-[#7A84D4]', }; const dayHeaders = ['월', '화', '수', '목', '금', '토', '일']; export default function ContentCalendar({ data, planId, onEntryUpdate }: ContentCalendarProps) { const [weeks, setWeeks] = useState(data.weeks); const [editingEntry, setEditingEntry] = useState(null); const [creatingForWeek, setCreatingForWeek] = useState(null); const [filterType, setFilterType] = useState(null); const [viewMode, setViewMode] = useState<'weekly' | 'monthly'>('weekly'); // DnD state: track which entry is being dragged and from which week const [draggedEntry, setDraggedEntry] = useState<{ entry: CalendarEntry; weekNumber: number } | null>(null); const [dropTargetDay, setDropTargetDay] = useState<{ weekNumber: number; dayIdx: number } | null>(null); const handleEntryClick = useCallback((entry: CalendarEntry) => { setCreatingForWeek(null); setEditingEntry(entry); }, []); // Open modal in create mode for a given week const handleAddEntry = useCallback((weekNumber: number) => { setCreatingForWeek(weekNumber); setEditingEntry({ id: '__new__', dayOfWeek: 0, channel: 'YouTube', channelIcon: 'youtube', contentType: 'video', title: '', status: 'draft', }); }, []); // Drag handlers — move an entry to a different day within the same week const handleDragStart = useCallback((entry: CalendarEntry, weekNumber: number) => { setDraggedEntry({ entry, weekNumber }); }, []); const handleDragOver = useCallback((e: React.DragEvent, weekNumber: number, dayIdx: number) => { e.preventDefault(); // required to allow drop setDropTargetDay({ weekNumber, dayIdx }); }, []); const handleDragLeave = useCallback(() => { setDropTargetDay(null); }, []); const handleDrop = useCallback((weekNumber: number, dayIdx: number) => { if (!draggedEntry || draggedEntry.weekNumber !== weekNumber) return; setWeeks((prev) => prev.map((week) => week.weekNumber === weekNumber ? { ...week, entries: week.entries.map((e) => e.id && e.id === draggedEntry.entry.id ? { ...e, dayOfWeek: dayIdx, isManualEdit: true } : e ), } : week ) ); setDraggedEntry(null); setDropTargetDay(null); }, [draggedEntry]); const handleDragEnd = useCallback(() => { setDraggedEntry(null); setDropTargetDay(null); }, []); const handleSave = useCallback((updated: CalendarEntry) => { if (updated.id === '__new__' && creatingForWeek !== null) { // Create mode: append new entry with a generated id const newEntry: CalendarEntry = { ...updated, id: `entry-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, }; setWeeks((prev) => prev.map((week) => week.weekNumber === creatingForWeek ? { ...week, entries: [...week.entries, newEntry] } : week ) ); setCreatingForWeek(null); } else { // Edit mode: update existing entry by id setWeeks((prev) => prev.map((week) => ({ ...week, entries: week.entries.map((e) => e.id && e.id === updated.id ? updated : e ), })) ); if (onEntryUpdate && updated.id) { onEntryUpdate(updated.id, updated); } } setEditingEntry(null); }, [onEntryUpdate, creatingForWeek]); const handleClose = useCallback(() => { setEditingEntry(null); setCreatingForWeek(null); }, []); const toggleFilter = (type: ContentCategory) => { setFilterType((prev) => (prev === type ? null : type)); }; // Monthly view: flatten all weeks into one 7-day grid const renderMonthlyView = () => { const allEntries = weeks.flatMap((w) => w.entries); const filtered = filterType ? allEntries.filter((e) => e.contentType === filterType) : allEntries; const dayCells: CalendarEntry[][] = Array.from({ length: 7 }, () => []); for (const entry of filtered) { if (entry.dayOfWeek >= 0 && entry.dayOfWeek <= 6) { dayCells[entry.dayOfWeek].push(entry); } } return (

월간 종합

{dayHeaders.map((day) => (
{day}
))} {dayCells.map((entries, dayIdx) => (
0 ? '' : 'border border-dashed border-slate-200' }`} > {entries.map((entry, entryIdx) => renderEntry(entry, entryIdx))}
))}
); }; const renderEntry = (entry: CalendarEntry, entryIdx: number, weekNumber?: number) => { const colors = contentTypeColors[entry.contentType]; const ContentIcon = contentTypeIcons[entry.contentType]; const ChannelIcon = channelIconMap[entry.channelIcon] ?? GlobeFilled; const statusDot = statusDotColors[entry.status ?? 'draft']; const isDragging = draggedEntry !== null && draggedEntry.entry === entry; return (
handleDragStart(entry, weekNumber) : undefined} onDragEnd={weekNumber !== undefined ? handleDragEnd : undefined} onClick={() => handleEntryClick(entry)} >

{entry.title}

{entry.description && (

{entry.description}

)}
); }; return ( {/* Toolbar: View Toggle + Status Legend */}
{/* View toggle — styled for dark section bg */}
{/* iCal Export Button */} {/* Status legend with design-system colors */}
초안 승인 게시됨
{/* Monthly Summary — compact counter pills (click to filter) */}
{data.monthlySummary.map((item, i) => { const colors = contentTypeColors[item.type]; const Icon = contentTypeIcons[item.type]; const isActive = filterType === item.type; return ( ); })} {filterType && ( )}
{/* Calendar Content */} {viewMode === 'monthly' ? ( renderMonthlyView() ) : ( /* Weekly Calendar Grid */ weeks.map((week, weekIdx) => { const dayCells: CalendarEntry[][] = Array.from({ length: 7 }, () => []); for (const entry of week.entries) { if (filterType && entry.contentType !== filterType) continue; const dayIndex = entry.dayOfWeek; if (dayIndex >= 0 && dayIndex <= 6) { dayCells[dayIndex].push(entry); } } return (

{week.label}

{dayHeaders.map((day) => (
{day}
))} {dayCells.map((entries, dayIdx) => { const isDropTarget = dropTargetDay?.weekNumber === week.weekNumber && dropTargetDay?.dayIdx === dayIdx; return (
0 ? '' : 'border border-dashed border-slate-200' } ${isDropTarget ? 'ring-2 ring-[#6C5CE7]/40 bg-[#F3F0FF] border-[#D5CDF5]' : ''}`} onDragOver={(e) => handleDragOver(e, week.weekNumber, dayIdx)} onDragLeave={handleDragLeave} onDrop={() => handleDrop(week.weekNumber, dayIdx)} > {entries.map((entry, entryIdx) => renderEntry(entry, entryIdx, week.weekNumber))}
); })}
{/* Add Entry Button */}
); }) )} {/* Edit Modal */} {editingEntry && ( )}
); }