- 등록부 home 줄 = mountHome · 썸네일 선 · 단계 점 · 「다음: 지금 단계」 · [이어서 하기] = 그 단계 화면 · [자세히] = 상세 - 4~8 = 팀원별 · 단계 분포 · 14 일 넘게 그대로 표시 · 9 넘음 = 요약 숫자 · 회사별 막대 · 종류 원(인라인 SVG) - 오른쪽 = 처리할 일 · 임시 보관함 · 회사(회사 관리자) · 예정노선(산림청) · 처음 = 세 걸음 · 꼴 고정(자동 · 작게 · 크게) 기억 - 홈 역할 차례 = 셸 roleOf 와 같게(회사 관리자가 산림청 앞) - test_546 등록부 시험 — import 한 mount 이름을 빈 함수로 저절로(페이지 줄 갈아 끼워도 그대로) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
/* =============================================================================
|
|
* 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<T>(
|
|
items: T[],
|
|
key: (item: T) => string,
|
|
): { label: string; count: number }[] {
|
|
const counts = new Map<string, number>();
|
|
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;
|