/* ============================================================================= * spreadsheet_statusbar.ts (3단계 · 주인 sub_laptop_4) * 아래 상태 표시줄 — 고른 범위의 합계 · 평균 · 개수(COUNTA) · 숫자 개수(COUNT) · 최소 · 최대. * 엑셀처럼 우클릭으로 보일 줄 고름(이 표시줄만의 취향 — 문서에는 안 남음 · 계약 손 안 댐). * 확대 비율 자리는 칸만 둠 — 실제 격자 확대와는 아직 안 이어짐(아래 「잇는 법」 2). * * 잇는 법(D · C · sub7 — 지금은 하나도 안 이어짐): * 1) `spreadsheet.ts` 가 `createStatusBar(ctx)` 를 한 번 부르고 `root` 를 화면 맨 아래 붙임. * `ctx.select()` 뒤(그리고 `dispatch` 로 값이 바뀐 뒤) `refresh()` 를 부름 — 스스로 구독 안 함. * 2) 확대 비율을 실제로 쓰려면 `GridHandle` 에 `zoom()` · `setZoom()` 을 계약(0)에 더하고(격자 C 몫), * 이 파일의 확대 자리를 그 값으로 채우고 바꾸면 `setZoom` 을 부르게 이음. * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu"; import { isFrac, fracToDouble } from "./spreadsheet_functions_math"; import type { CellRange, Scalar } from "./spreadsheet_types"; import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; // css 는 stage3.css 에 얹음(잇는 법 머리 참고) — 실제로 붙일 때 `import "./spreadsheet_stage3.css";` 더함. export type StatKey = "합계" | "평균" | "개수" | "숫자개수" | "최소" | "최대"; export const STAT_KEYS: StatKey[] = ["합계", "평균", "개수", "숫자개수", "최소", "최대"]; export interface SelectionStats { 합계: number | null; 평균: number | null; 개수: number; 숫자개수: number; 최소: number | null; 최대: number | null; } /** 고른 범위(여럿) 안 칸 값을 모아 통계를 냄 — 엔진 값 기준(식 칸도 풀린 값). */ export function computeSelectionStats(ctx: SpreadsheetContext): SelectionStats { const sheet = ctx.selection.시트; const nums: number[] = []; let 개수 = 0; const seen = new Set(); const visit = (range: CellRange): void => { for (let r = range.r0; r <= range.r1; r++) { for (let c = range.c0; c <= range.c1; c++) { const key = r * 1_000_000 + c; if (seen.has(key)) continue; seen.add(key); const value: Scalar = ctx.engine.value(sheet, r, c); if (value === null) continue; 개수++; if (isFrac(value)) nums.push(fracToDouble(value)); } } }; for (const range of ctx.selection.범위) visit(range); return { 합계: nums.length ? nums.reduce((a, b) => a + b, 0) : null, 평균: nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : null, 개수, 숫자개수: nums.length, 최소: nums.length ? Math.min(...nums) : null, 최대: nums.length ? Math.max(...nums) : null, }; } function statText(key: StatKey, stats: SelectionStats): string { const value = stats[key]; if (key === "개수" || key === "숫자개수") return `${key}: ${value}`; return `${key}: ${value === null ? "—" : formatNumber(value)}`; } function formatNumber(n: number): string { return Number.isInteger(n) ? String(n) : n.toLocaleString(undefined, { maximumFractionDigits: 4 }); } /** 아래 상태 표시줄 — 우클릭 메뉴로 보일 줄 고름. */ export function createStatusBar(ctx: SpreadsheetContext): PartHandle { const root = el("div", { className: "ss-statusbar" }); const stats = el("div", { className: "ss-statusbar__stats" }); const zoom = el("span", { className: "ss-statusbar__zoom", text: "확대 100%" }); const menu = createMapContextMenu("ss-statusbar"); root.append(stats, zoom, menu.element); let visible = new Set(STAT_KEYS); function render(): void { const s = computeSelectionStats(ctx); stats.replaceChildren( ...STAT_KEYS.filter((k) => visible.has(k)).map((k) => el("span", { className: "ss-statusbar__item", text: statText(k, s) }), ), ); } root.addEventListener("contextmenu", (ev) => { ev.preventDefault(); const box = root.getBoundingClientRect(); const items: MapContextMenuItem[] = STAT_KEYS.map((k) => [ `${visible.has(k) ? "✓ " : ""}${k}`, () => { if (visible.has(k)) visible.delete(k); else visible.add(k); render(); }, ]); menu.open(ev.clientX - box.left, ev.clientY - box.top, items); }); render(); return { root, refresh: render, destroy(): void { menu.close(); root.remove(); }, }; }