feat(spreadsheet): D 잇기 — 수식 분석 · 자동 합계 · 상태 표시줄 · 시트 숨김/탭 색(sub3 · sub4)

- spreadsheet_data_menu.ts — 데이터 ▾ 안 「수식 분석」(선행 · 종속 추적 · 화살표 지우기 · 수식 계산) · 오류 ⚠ 부품 · Σ 단추 · 「시트 숨기기 · 탭 색…」 창
- spreadsheet_keys.ts — Alt+= 자동 합계(=SUM(잡은 범위) 로 편집을 열고 Enter 는 사용자)
- spreadsheet.ts — 상태 표시줄(합계 · 평균 · 개수 …)을 맨 아래

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
This commit is contained in:
2026-09-27 21:21:46 +09:00
co-authored by Claude Opus 5.5
parent d887a8cec3
commit 16c1443b93
3 changed files with 59 additions and 9 deletions
+6 -3
View File
@@ -6,7 +6,7 @@
* 키 · 마우스)을 잇음. 한글 조합 = 숨은 textarea 에 늘 초점 · compositionstart 로 편집 시작(keydown 글자 방식 금지).
* ⚠ M02 만 `import()` 로 불러옴 — 정적 import 금지(다른 페이지 번들에 안 섞이게).
* 저장은 부른 쪽 몫(`onChange` 로 문서 · 자동저장 없음 · [저장] 때 서버가 Node 로 다시 풂).
* 화면 차례: 도구 모음(E) · 주소 상자 + 수식 입력줄(D) · 격자(C) · 시트 탭(E).
* 화면 차례: 도구 모음(E) · 주소 상자 + 수식 입력줄(D) · 격자(C) · 시트 탭(E) · 상태 표시줄(sub4).
* 2단계 부품(찾기 · 메모 · 서식 붓 · 골라 붙여넣기)은 `spreadsheet_extras.ts` 로 이음.
* ========================================================================== */
@@ -24,6 +24,7 @@ import { attachKeys } from "./spreadsheet_keys";
import { attachMenu } from "./spreadsheet_menu";
import { attachMouse } from "./spreadsheet_mouse";
import { LAST_C, LAST_R, selectCell } from "./spreadsheet_selection";
import { createStatusBar } from "./spreadsheet_statusbar";
import { mountTabs } from "./spreadsheet_tabs";
import { mountToolbar } from "./spreadsheet_toolbar";
import type {
@@ -128,10 +129,12 @@ export function createSpreadsheet(
const tabs = mountTabs(ctx);
const menu: PartHandle | null = readOnly ? null : attachMenu(ctx);
const clip = attachClipboard(ctx);
const others = [toolbar, tabs, menu, clip].filter((p): p is PartHandle => !!p);
const status = createStatusBar(ctx);
const others = [toolbar, tabs, menu, clip, status].filter((p): p is PartHandle => !!p);
grid.root.classList.add("ss-grid-host");
for (const node of [toolbar?.root, ed.bar, grid.root, tabs.root]) if (node) root.append(node);
for (const node of [toolbar?.root, ed.bar, grid.root, tabs.root, status.root])
if (node) root.append(node);
const extras = attachExtras(ctx, ed, toolbar?.root ?? null);
others.push(...extras.parts);
const detachKeys = attachKeys(ctx, ed, extras.keys);
@@ -2,18 +2,24 @@
* spreadsheet_data_menu.ts (주인 D)
* 데이터 단추 · 메뉴 잇기 — 수식 입력줄 앞 [↑] [↓] [필터] [데이터 ▾].
* 정렬 · 자동 필터(sub3 `spreadsheet_sort_dialog` · `spreadsheet_filter`) ·
* 조건부 서식 · 데이터 유효성 · 이름 관리(sub4 패널 — 부품 오른위 창에 띄움) · 눈금선 보이기/숨기기.
* 조건부 서식 · 데이터 유효성 · 이름 관리 · 시트 숨김/탭 색(sub4 패널 — 부품 오른위 창에 띄움) · 눈금선 ·
* 수식 분석(sub3 — 선행 · 종속 추적 화살표 · 화살표 지우기 · 수식 계산 · 오류 ⚠ 풍선) · 자동 합계(Σ · Alt+=).
* 유효성 목록 칸은 고르면 칸 옆 ▼ · Alt+↓ 로 값 목록(필터 머리 칸이면 필터 목록이 먼저).
* ========================================================================== */
import "./spreadsheet_stage3.css"; // sub4 패널 스타일(잇는 쪽이 붙임)
import { createMapContextMenu } from "@ui/ui_template_context_menu";
import { el } from "@ui/ui_template_elements";
import { parseRange, toA1 } from "./spreadsheet_address";
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
import { el, showToast } from "@ui/ui_template_elements";
import { parseRange, rangeToA1, toA1 } from "./spreadsheet_address";
import { autoSumRange } from "./spreadsheet_autosum";
import { createCondFmtPanel } from "./spreadsheet_condfmt";
import { attachErrorTip } from "./spreadsheet_errcheck";
import { openEvalSteps } from "./spreadsheet_eval_steps";
import { attachFilter, toggleFilter } from "./spreadsheet_filter";
import { createNameManagerPanel } from "./spreadsheet_names";
import { createSheetExtraPanel } from "./spreadsheet_sheet_extra";
import { openSortDialog, sortSelection } from "./spreadsheet_sort_dialog";
import { attachTrace } from "./spreadsheet_trace";
import {
createValidationDropdown,
createValidationPanel,
@@ -35,17 +41,32 @@ const TEXT = {
grid: "눈금선 보이기 · 숨기기",
close: "닫기",
list: "값 목록(Alt+↓)",
sheets: "시트 숨기기 · 탭 색…",
audit: "수식 분석",
prec: "선행 참조 추적",
dep: "종속 참조 추적",
clear: "화살표 지우기",
steps: "수식 계산…",
noMore: "더 추적할 칸 없음",
sum: "자동 합계(Alt+=)",
};
export interface DataMenu {
/** 수식 입력줄 앞에 둘 단추들 */
buttons: HTMLElement[];
keys: Pick<KeyHooks, "filter" | "dropdown">;
keys: Pick<KeyHooks, "filter" | "dropdown" | "autosum">;
part: PartHandle;
}
export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMenu {
const filter = attachFilter(ctx);
const trace = attachTrace(ctx);
const tip = attachErrorTip(ctx, trace);
let closeSteps: (() => void) | null = null;
const traced = (more: boolean): void => {
if (!more) showToast(TEXT.noMore, "info");
back();
};
const menu = createMapContextMenu("ss");
ctx.root.append(menu.element);
let closeSort: (() => void) | null = null;
@@ -97,13 +118,23 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
function openMenu(anchor: HTMLElement): void {
const r = anchor.getBoundingClientRect();
const items: [string, () => void][] = [
const items: MapContextMenuItem[] = [
[TEXT.sort, () => ((closeSort = openSortDialog(ctx)), undefined)],
[TEXT.filterItem, () => (toggleFilter(ctx), back())],
[TEXT.condfmt, () => openPanel(TEXT.condfmt, createCondFmtPanel)],
[TEXT.validation, () => openPanel(TEXT.validation, createValidationPanel)],
[TEXT.names, () => openPanel(TEXT.names, createNameManagerPanel)],
[TEXT.sheets, () => openPanel(TEXT.sheets, createSheetExtraPanel)],
[TEXT.grid, () => (toggleGridlines(), back())],
{
label: TEXT.audit,
children: [
[TEXT.prec, () => traced(trace.precedents())],
[TEXT.dep, () => traced(trace.dependents())],
[TEXT.clear, () => (trace.clear(), back())],
[TEXT.steps, () => (closeSteps?.(), (closeSteps = openEvalSteps(ctx)))],
],
},
];
if (!ctx.readOnly) menu.open(r.left, r.bottom, items);
}
@@ -161,12 +192,19 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
};
document.addEventListener("mousedown", onOutside, true);
/** 자동 합계 — 엑셀처럼 =SUM(잡은 범위) 로 편집을 열고 Enter 는 사용자 몫 */
function autosum(): void {
const g = autoSumRange(ctx);
ctx.editor.begin(g ? `=SUM(${rangeToA1(g)})` : "=SUM()");
}
const buttons = ctx.readOnly
? []
: [
button("↑", TEXT.asc, () => sortSelection(ctx, false)),
button("↓", TEXT.desc, () => sortSelection(ctx, true)),
button("필터", TEXT.filter, () => toggleFilter(ctx)),
button("Σ", TEXT.sum, autosum, true),
button(TEXT.menu, TEXT.menu, openMenu, true),
];
@@ -174,6 +212,7 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
buttons,
keys: {
filter: () => toggleFilter(ctx),
autosum,
dropdown() {
const f = ctx.sheet().필터;
const g = f && parseRange(f.범위);
@@ -186,6 +225,8 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
root: null,
refresh() {
filter.refresh();
trace.refresh();
tip.refresh();
panel?.refresh();
closeDrop();
const { r, c } = ctx.selection.활성;
@@ -203,6 +244,9 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
closePanel();
closeDrop();
filter.destroy();
closeSteps?.();
trace.destroy();
tip.destroy();
menu.element.remove();
dialog.remove();
arrow.remove();
@@ -43,6 +43,8 @@ export interface KeyHooks {
escape?(): void;
/** Ctrl+1 셀 서식 창 */
formatDialog?(): void;
/** Alt+= 자동 합계 */
autosum?(): void;
/** Ctrl+Shift+L 필터 켜기 · 끄기 */
filter?(): void;
/** Alt+↓ — 필터 머리 칸이면 값 목록을 열고 true */
@@ -106,6 +108,7 @@ export function attachKeys(ctx: SpreadsheetContext, ed: Editor, hooks: KeyHooks
const sel = ctx.selection;
const arrow = ARROWS[e.key];
if (e.altKey && e.key === "ArrowDown" && hooks.dropdown?.()) return true;
if (e.altKey && e.key === "=" && hooks.autosum && !ctx.readOnly) return (hooks.autosum(), true);
if (arrow && !e.altKey) {
go(move(sheet, sel, arrow[0], arrow[1], { extend: e.shiftKey, jump: ctrl }));
return true;