EENE Dashboard upload to Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
168
frontend/src/lib/milestonePeriods.ts
Normal file
168
frontend/src/lib/milestonePeriods.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user