o2o-infinith-demo/src/lib/calendarExport.ts

102 lines
3.1 KiB
TypeScript

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);
}