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
133 lines
4.9 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|