import type { CalendarWeek, CalendarEntry } from '../types/plan'; /** * Returns the Monday date of a given ISO week number in a year. * Week 1 = the week containing the first Thursday of the year (ISO 8601). */ function isoWeekToDate(year: number, week: number, dayOffset: number): Date { // Jan 4 is always in week 1 const jan4 = new Date(year, 0, 4); const dayOfWeek = jan4.getDay() || 7; // convert Sun=0 to 7 const monday = new Date(jan4); monday.setDate(jan4.getDate() - (dayOfWeek - 1) + (week - 1) * 7); monday.setDate(monday.getDate() + dayOffset); return monday; } function formatICSDate(date: Date): string { const pad = (n: number) => String(n).padStart(2, '0'); return ( `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` + `T${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}` ); } function escapeICS(str: string): string { return str .replace(/\\/g, '\\\\') .replace(/;/g, '\\;') .replace(/,/g, '\\,') .replace(/\n/g, '\\n'); } function buildVEvent( entry: CalendarEntry, weekNumber: number, year: number, uid: string, ): string { const startDate = isoWeekToDate(year, weekNumber, entry.dayOfWeek); // All-day event: DTSTART is DATE only, DTEND is next day const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + 1); const formatDate = (d: Date) => { const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; }; const lines = [ 'BEGIN:VEVENT', `UID:${uid}`, `DTSTAMP:${formatICSDate(new Date())}Z`, `DTSTART;VALUE=DATE:${formatDate(startDate)}`, `DTEND;VALUE=DATE:${formatDate(endDate)}`, `SUMMARY:${escapeICS(`[${entry.channel}] ${entry.title}`)}`, entry.description ? `DESCRIPTION:${escapeICS(entry.description)}` : null, `CATEGORIES:${escapeICS(entry.contentType.toUpperCase())}`, `STATUS:${entry.status === 'published' ? 'CONFIRMED' : entry.status === 'approved' ? 'TENTATIVE' : 'NEEDS-ACTION'}`, 'END:VEVENT', ].filter(Boolean) as string[]; return lines.join('\r\n'); } export function exportCalendarToICS( weeks: CalendarWeek[], calendarName = 'INFINITH 콘텐츠 캘린더', ): void { const year = new Date().getFullYear(); const vEvents = weeks.flatMap((week) => week.entries.map((entry, idx) => buildVEvent( entry, week.weekNumber, year, `infinith-${week.weekNumber}-${entry.id ?? idx}@infinith.ai`, ), ), ); const icsContent = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//INFINITH//Marketing Content Calendar//KO', `X-WR-CALNAME:${escapeICS(calendarName)}`, 'X-WR-TIMEZONE:Asia/Seoul', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', ...vEvents, 'END:VCALENDAR', ].join('\r\n'); const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'infinith-content-calendar.ics'; a.click(); URL.revokeObjectURL(url); }