import type { Milestone, MilestonePeriodEntry } from '../types'; import { routineStageBody } from './routineMilestone'; export type { MilestonePeriodEntry }; export function newPeriodEntry(): MilestonePeriodEntry { return { id: `period-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, startDate: '', dueDate: '', note: '', }; } function toDateInput(iso: string | null | undefined) { if (!iso) return ''; return new Date(iso).toISOString().slice(0, 10); } export function normalizePeriodEntries(raw: unknown): MilestonePeriodEntry[] { if (!Array.isArray(raw)) return []; const entries: MilestonePeriodEntry[] = []; for (const item of raw) { if (!item || typeof item !== 'object') continue; const row = item as Record; const startDate = typeof row.startDate === 'string' ? row.startDate.trim() : ''; const dueDate = typeof row.dueDate === 'string' ? row.dueDate.trim() : ''; const note = typeof row.note === 'string' ? row.note : ''; if (!startDate && !dueDate && !note.trim()) continue; entries.push({ id: typeof row.id === 'string' && row.id ? row.id : newPeriodEntry().id, startDate, dueDate, note, }); } return entries; } function legacyDescriptionNote(description: string | null | undefined): string { return routineStageBody(description).trim(); } export function parseMilestonePeriods( milestone: Pick | null | undefined, ): MilestonePeriodEntry[] { if (!milestone) return []; const legacyNote = legacyDescriptionNote(milestone.description); const fromJson = normalizePeriodEntries(milestone.periodEntries); if (fromJson.length > 0) { if (legacyNote && !fromJson.some((p) => p.note.trim())) { return fromJson.map((p, i) => (i === 0 ? { ...p, note: legacyNote } : p)); } return fromJson; } if (milestone.startDate || milestone.dueDate || legacyNote) { return [ { id: milestone.id ? `period-legacy-${milestone.id}` : newPeriodEntry().id, startDate: toDateInput(milestone.startDate), dueDate: toDateInput(milestone.dueDate), note: legacyNote, }, ]; } return []; } export function serializePeriodEntries(entries: MilestonePeriodEntry[]): MilestonePeriodEntry[] { return entries .map((entry) => ({ id: entry.id, startDate: entry.startDate.trim(), dueDate: entry.dueDate.trim(), note: entry.note.trim(), })) .filter((entry) => entry.startDate || entry.dueDate || entry.note); } export function fmtPeriodRange(entry: Pick) { const fmt = (iso: string) => { const d = new Date(iso); return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`; }; if (entry.startDate && entry.dueDate) return `${fmt(entry.startDate)} ~ ${fmt(entry.dueDate)}`; if (entry.dueDate) return fmt(entry.dueDate); if (entry.startDate) return `${fmt(entry.startDate)} ~`; return ''; } /** 업무내용 기간 선택 — MM.DD~MM.DD (연도 생략) */ export function fmtPeriodPickerLabel( entry: Pick, index: number, ): string { const fmt = (iso: string) => { const d = new Date(iso); return `${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`; }; if (entry.startDate && entry.dueDate) return `${fmt(entry.startDate)}~${fmt(entry.dueDate)}`; if (entry.dueDate) return fmt(entry.dueDate); if (entry.startDate) return `${fmt(entry.startDate)}~`; return `기간 ${index + 1}`; } export function parsePeriodNoteLines(text: string | null | undefined): string[] { if (!text) return []; return text .split('\n') .map((line) => line.replace(/^[•·\-]\s*/, '').trim()) .filter(Boolean); } /** 최신 기간 우선 — 종료일·시작일 내림차순 */ export function sortPeriodsByRecent(periods: MilestonePeriodEntry[]): MilestonePeriodEntry[] { return [...periods].sort((a, b) => { const ta = a.dueDate || a.startDate || ''; const tb = b.dueDate || b.startDate || ''; if (ta && tb) return tb.localeCompare(ta); if (tb) return 1; if (ta) return -1; return 0; }); } export function pickLatestPeriodId(periods: MilestonePeriodEntry[]): string | null { return sortPeriodsByRecent(periods)[0]?.id ?? null; } /** 목록·카드용 — 최신(마지막) 기간 또는 요약 */ export function fmtMilestonePeriodSummary( milestone: Pick, ): string { const periods = parseMilestonePeriods(milestone); if (periods.length === 0) return ''; if (periods.length === 1) return fmtPeriodRange(periods[0]) || ''; const last = periods[periods.length - 1]; const lastLabel = fmtPeriodRange(last); return lastLabel ? `${lastLabel} 외 ${periods.length - 1}건` : `기간 ${periods.length}건`; } export interface MilestoneContentBlock { key: string; label: string; lines: string[]; } /** @deprecated MilestoneContentList에서 기간 선택 UI로 대체 */ export function buildMilestoneContentBlocks( milestone: Pick | null | undefined, ): MilestoneContentBlock[] { if (!milestone) return []; const blocks: MilestoneContentBlock[] = []; const periods = sortPeriodsByRecent(parseMilestonePeriods(milestone)).reverse(); periods.forEach((period, index) => { const lines = parsePeriodNoteLines(period.note); if (lines.length === 0) return; blocks.push({ key: period.id, label: fmtPeriodPickerLabel(period, index), lines, }); }); return blocks; }