Files
Aislo/A00_Common/spreadsheet/spreadsheet_statusbar.ts
T
eomsangdonandClaude Sonnet 5 ede12fbc0e feat(spreadsheet): 상태 표시줄 확대 비율 — sub6 zoom() 이음
sub6 격자 확대 커밋(origin/sub_laptop_6 7b294adc) 받아 GridHandle.zoom() 으로 처음 값 · 격자 root 「aislo-grid-zoom」 사건으로 바뀔 때마다 「확대 N%」 다시 씀.
가짜 SpreadsheetContext(test_stage2_helpers.ts) 격자에 zoom() 스텁 더함(0 계약 필수 자리라 안 채우면 모양 안 맞음).
ORCA(포트 5179) — 실제 createSpreadsheet 를 띄워 Ctrl+휠(WheelEvent ctrlKey) 로 확인: 「확대 100%」→「확대 332%」로 실시간 바뀜.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
2026-09-27 23:12:57 +09:00

133 lines
4.9 KiB
TypeScript

/* =============================================================================
* spreadsheet_statusbar.ts (3단계 · 주인 sub_laptop_4)
* 아래 상태 표시줄 — 고른 범위의 합계 · 평균 · 개수(COUNTA) · 숫자 개수(COUNT) · 최소 · 최대.
* 엑셀처럼 우클릭으로 보일 줄 고름(이 표시줄만의 취향 — 문서에는 안 남음 · 계약 손 안 댐).
* 확대 비율 — `ctx.grid.zoom()` 으로 처음 값 · 격자 root 의 `aislo-grid-zoom` 사건(sub6, 2026-09-27)을
* 들어 Ctrl+휠로 바뀔 때마다 다시 씀. `setZoom` 은 아직 계약에 없음(격자만 바꿈 · 상태줄은 읽기만).
* 화면 붙이기는 `spreadsheet.ts`(D)가 `createStatusBar(ctx)` 로 이미 함(root 를 격자 아래).
* ========================================================================== */
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<number>();
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 });
}
function zoomText(ratio: number): string {
return `확대 ${Math.round(ratio * 100)}%`;
}
/** 아래 상태 표시줄 — 우클릭 메뉴로 보일 줄 고름. */
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: zoomText(ctx.grid.zoom()),
});
const menu = createMapContextMenu("ss-statusbar");
root.append(stats, zoom, menu.element);
const onZoom = (ev: Event): void => {
const ratio = (ev as CustomEvent<{ zoom: number }>).detail?.zoom;
if (typeof ratio === "number") zoom.textContent = zoomText(ratio);
};
ctx.grid.root.addEventListener("aislo-grid-zoom", onZoom);
let visible = new Set<StatKey>(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 {
ctx.grid.root.removeEventListener("aislo-grid-zoom", onZoom);
menu.close();
root.remove();
},
};
}