spreadsheet_statusbar.ts 새로 만듦 — 고른 범위 합계·평균·개수·숫자개수·최소·최대, 우클릭으로 보일 줄 고름, 확대 비율 자리(아직 안 이어짐). 계약 안 건드림 · 엔진 A·격자 C 안 건드림 — 화면 붙이기·확대 이음은 sub7 몫(머리 주석에 적음). 시험 resources/tester/spreadsheet/test_stage3_statusbar.ts 9개 통과. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
123 lines
4.7 KiB
TypeScript
123 lines
4.7 KiB
TypeScript
/* =============================================================================
|
|
* 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<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 });
|
|
}
|
|
|
|
/** 아래 상태 표시줄 — 우클릭 메뉴로 보일 줄 고름. */
|
|
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<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 {
|
|
menu.close();
|
|
root.remove();
|
|
},
|
|
};
|
|
}
|