/* ============================================================================= * logic/B01_Dashboard_Home_Layout.ts * 홈 꼴 고르기 · 묶어 세기(PLAN 547-2) — 순수 함수 · 시험 `test_547_2_home_layout.py` * 진행 중 1 = one · 2~3 = few · 4~8 = list · 9 넘음 = many · 0 = empty * 고정 small = list(9 넘음 = many) · large = one(1) / few(2 넘음) * ========================================================================== */ export type HomeLayout = "empty" | "one" | "few" | "list" | "many"; export type HomeFixed = "auto" | "small" | "large"; export const STALE_DAYS = 14; export const MANY_FROM = 9; export function homeLayout(count: number, fixed: HomeFixed = "auto"): HomeLayout { if (count <= 0) return "empty"; if (fixed === "large") return count === 1 ? "one" : "few"; if (fixed === "small") return count >= MANY_FROM ? "many" : "list"; if (count === 1) return "one"; if (count <= 3) return "few"; return count < MANY_FROM ? "list" : "many"; } interface StageLike { stage_index: number; stage_total: number; stale_days?: number | null; } export const isStale = (card: StageLike): boolean => (card.stale_days ?? 0) >= STALE_DAYS; /** 단계 분포 — 시작 전(첫 단계) · 설계 중 · 확정 대기(마지막 단계) */ export function stageBuckets(cards: StageLike[]): { start: number; working: number; last: number } { const out = { start: 0, working: 0, last: 0 }; for (const c of cards) { if (c.stage_index <= 0) out.start += 1; else if (c.stage_index >= c.stage_total - 1) out.last += 1; else out.working += 1; } return out; } /** 묶어 세기 — 많은 차례 · 같으면 이름 차례 */ export function countBy( items: T[], key: (item: T) => string, ): { label: string; count: number }[] { const counts = new Map(); for (const item of items) counts.set(key(item), (counts.get(key(item)) ?? 0) + 1); return [...counts] .map(([label, count]) => ({ label, count })) .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)); } /** 앞 limit 개 + 나머지 하나로 묶음(「그 밖 N 곳」) */ export function topWithRest( rows: { label: string; count: number }[], limit: number, ): { label: string; count: number; rest?: number }[] { if (rows.length <= limit) return rows; const rest = rows.slice(limit - 1); return [ ...rows.slice(0, limit - 1), { label: "", count: rest.reduce((sum, r) => sum + r.count, 0), rest: rest.length }, ]; } /** 원 그래프 조각 — 둘레 100 기준 stroke-dasharray · stroke-dashoffset(12 시 시작 · 시계 방향) */ export function donutArcs(values: number[]): { dash: string; offset: number }[] { const total = values.reduce((sum, v) => sum + v, 0); if (total <= 0) return []; let start = 0; return values.map((v) => { const len = (v / total) * 100; const arc = { dash: `${round2(len)} ${round2(100 - len)}`, offset: round2(25 - start) }; start += len; return arc; }); } const round2 = (v: number): number => Math.round(v * 100) / 100;