diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts index ba56a878..05764e6f 100644 --- a/A00_Common/spreadsheet/spreadsheet.ts +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -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); diff --git a/A00_Common/spreadsheet/spreadsheet_data_menu.ts b/A00_Common/spreadsheet/spreadsheet_data_menu.ts index 4b023d90..63721d63 100644 --- a/A00_Common/spreadsheet/spreadsheet_data_menu.ts +++ b/A00_Common/spreadsheet/spreadsheet_data_menu.ts @@ -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; + keys: Pick; 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(); diff --git a/A00_Common/spreadsheet/spreadsheet_keys.ts b/A00_Common/spreadsheet/spreadsheet_keys.ts index b90a88b0..ed815923 100644 --- a/A00_Common/spreadsheet/spreadsheet_keys.ts +++ b/A00_Common/spreadsheet/spreadsheet_keys.ts @@ -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;