Files
eene_dashboard/frontend/src/lib/milestonePeriods.ts
EENE Dashboard b3f2da203b EENE Dashboard upload to Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:59:34 +09:00

169 lines
5.6 KiB
TypeScript

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<string, unknown>;
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<Milestone, 'id' | 'periodEntries' | 'startDate' | 'dueDate' | 'description'> | 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<MilestonePeriodEntry, 'startDate' | 'dueDate'>) {
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<MilestonePeriodEntry, 'startDate' | 'dueDate'>,
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<Milestone, 'periodEntries' | 'startDate' | 'dueDate'>,
): 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<Milestone, 'id' | 'periodEntries' | 'startDate' | 'dueDate' | 'description'> | 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;
}