perf(spreadsheet): D 크기 줄임 — sub2 · sub8 병합 뒤 화면 예산 넘침 해소

- 함수 자동 완성 · 인자 도움말, 이름 상자 · 이름 관리, 서식 붓, 메모 편집(새 spreadsheet_memo.ts), 열 너비 맞춤을 처음 쓸 때 불러옴
- 화면 30,126 B → 28,102 B(예산 28,672) · 합 62.6 KB

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-28 00:46:08 +09:00
co-authored by Claude Opus 5.5
parent 0f7617f99d
commit 87417b3a31
5 changed files with 99 additions and 48 deletions
@@ -17,7 +17,6 @@ import { autoSumRange } from "./spreadsheet_autosum";
import { isError } from "./spreadsheet_eval";
import type { FilterHandle } from "./spreadsheet_filter";
import { createCondFmtPanel } from "./spreadsheet_condfmt";
import { createNameManagerPanel } from "./spreadsheet_names";
import { createSheetExtraPanel } from "./spreadsheet_sheet_extra";
import { mountActions } from "./spreadsheet_actions";
import type { ErrorTipHandle } from "./spreadsheet_errcheck";
@@ -174,7 +173,14 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
[TEXT.filterItem, () => (toggleFilter(), back())],
[TEXT.condfmt, () => void openPanel(TEXT.condfmt, async () => createCondFmtPanel)],
[TEXT.validation, () => void openPanel(TEXT.validation, async () => createValidationPanel)],
[TEXT.names, () => void openPanel(TEXT.names, async () => createNameManagerPanel)],
[
TEXT.names,
() =>
void openPanel(
TEXT.names,
async () => (await import("./spreadsheet_names")).createNameManagerPanel,
),
],
[TEXT.sheets, () => void openPanel(TEXT.sheets, async () => createSheetExtraPanel)],
[TEXT.grid, () => (toggleGridlines(), back())],
...act.items,
+8 -6
View File
@@ -11,8 +11,7 @@
import { el, showToast } from "@ui/ui_template_elements";
import { moveFormula } from "./spreadsheet_refshift";
import { colName, parseA1, parseRange, rangeToA1, toA1 } from "./spreadsheet_address";
import { funcHelp, suggestFunctions, type FuncHelp } from "./spreadsheet_func_help";
import { defineName, resolveName } from "./spreadsheet_names";
import type { FuncHelp } from "./spreadsheet_func_help";
import { parseFormula, tokenize } from "./spreadsheet_parser";
import { cellsOf, move, selectCell } from "./spreadsheet_selection";
import { checkValue, findValidation } from "./spreadsheet_validation";
@@ -284,13 +283,15 @@ export function createEditor(ctx: SpreadsheetContext): Editor {
let sugg: FuncHelp[] = [];
let pick = 0;
let typed = "";
let fh: typeof import("./spreadsheet_func_help") | null = null; // 식을 처음 칠 때 불러옴 — 크기 예산
function showHint(): void {
const node = src();
const h = st ? formulaHint(node.value.slice(0, node.selectionEnd)) : null;
sugg = h && "prefix" in h ? suggestFunctions(h.prefix).slice(0, 8) : [];
if (h && !fh) return void import("./spreadsheet_func_help").then((m) => ((fh = m), showHint()));
sugg = h && "prefix" in h ? fh!.suggestFunctions(h.prefix).slice(0, 8) : [];
typed = h && "prefix" in h ? h.prefix : "";
const help = h && "func" in h ? funcHelp(h.func) : null;
const help = h && "func" in h ? fh!.funcHelp(h.func) : null;
if (!sugg.length && !help) return void hint.setAttribute("hidden", "");
pick = Math.min(pick, Math.max(0, sugg.length - 1));
hint.replaceChildren(
@@ -489,7 +490,8 @@ export function createEditor(ctx: SpreadsheetContext): Editor {
});
/** 이름 상자(sub4) — 있는 이름이면 그 범위로 · 없으면 지금 고름에 이름을 붙임(엑셀) */
function nameBox(name: string): void {
async function nameBox(name: string): Promise<void> {
const { defineName, resolveName } = await import("./spreadsheet_names"); // 늦게 — 크기 예산
const hit = resolveName(ctx.book, ctx.selection.시트, name);
if (hit) {
if (hit.시트 !== ctx.selection.시트) ctx.showSheet(hit.시트);
@@ -514,7 +516,7 @@ export function createEditor(ctx: SpreadsheetContext): Editor {
const body = value.slice(bang + 1).replace(/\$/g, "");
const one = parseA1(body);
const g = parseRange(body);
if (!g) return nameBox(value.trim());
if (!g) return void nameBox(value.trim());
if (one) return ctx.select(selectCell(sheet, one.r, one.c), true);
const at = { r: g.r0, c: g.c0 };
ctx.select({ 시트: sheet.id, 범위: [g], 활성: at, 기준: { ...at } }, true);
+20 -38
View File
@@ -13,8 +13,9 @@ import { attachComments, getComment, setComment } from "./spreadsheet_comments";
import { mountDataMenu } from "./spreadsheet_data_menu";
import type { Editor } from "./spreadsheet_editor";
import type { FindPanelHandle } from "./spreadsheet_find_panel";
import { createFormatPainter } from "./spreadsheet_format_painter";
import type { FormatPainterHandle } from "./spreadsheet_format_painter";
import type { KeyHooks } from "./spreadsheet_keys";
import type { Memo } from "./spreadsheet_memo";
import type { MouseHooks } from "./spreadsheet_mouse";
import type { PasteSpecialMode } from "./spreadsheet_paste_special";
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
@@ -24,7 +25,6 @@ const TEXT = {
painter: "서식 붓 — 한 번 누르면 한 번 · 두 번 누르면 Esc 까지 계속",
pasteTitle: "골라 붙여넣기",
modes: { 값: "값만", 서식: "서식만", 수식: "수식만" } as Record<PasteSpecialMode, string>,
memo: "메모 — Ctrl+Enter 또는 밖을 누르면 적용 · Esc 취소",
};
export interface Extras {
@@ -62,40 +62,20 @@ export function attachExtras(
// ── 메모 — 풍선 · 편집 ─────────────────────────────────────────────────
const comments = attachComments(ctx);
let hovered = "";
const memo = el("textarea", { className: "ss-memo", attrs: { hidden: "", title: TEXT.memo } });
ctx.root.append(memo);
let memoAt: { r: number; c: number } | null = null;
const closeMemo = (save: boolean): void => {
if (!memoAt) return;
const at = memoAt;
memoAt = null;
memo.setAttribute("hidden", "");
if (save && memo.value !== (getComment(ctx.sheet(), at.r, at.c) ?? ""))
setComment(ctx, at.r, at.c, memo.value.trim());
back();
};
memo.addEventListener("keydown", (e) => {
if (e.key === "Escape" || (e.key === "Enter" && e.ctrlKey)) {
e.preventDefault();
e.stopPropagation();
closeMemo(e.key === "Enter");
}
});
memo.addEventListener("blur", () => closeMemo(true));
function editMemo(): void {
let memo: Memo | null = null; // 처음 쓸 때 불러옴 — 크기 예산
async function editMemo(): Promise<void> {
if (ctx.readOnly) return;
const { r, c } = ctx.selection.활성;
memoAt = { r, c };
memo ??= (await import("./spreadsheet_memo")).createMemo(ctx, back, {
get: getComment,
set: setComment,
});
comments.hideBalloon();
memo.value = getComment(ctx.sheet(), r, c) ?? "";
memo.removeAttribute("hidden");
put(memo, r, c);
memo.focus();
memo.open(r, c, (node) => put(node, r, c));
}
// ── 서식 붓 ─────────────────────────────────────────────────────────────
const painter = createFormatPainter(ctx);
let painter: FormatPainterHandle | null = null; // 처음 누를 때 불러옴 — 크기 예산
let sticky = false;
const brush = el("button", {
className: "ss-brush",
@@ -103,10 +83,12 @@ export function attachExtras(
attrs: { type: "button", title: TEXT.painter },
});
const showBrush = (): void => {
brush.classList.toggle("is-on", painter.active());
ctx.root.classList.toggle("ss--painting", painter.active());
const on = !!painter?.active();
brush.classList.toggle("is-on", on);
ctx.root.classList.toggle("ss--painting", on);
};
brush.addEventListener("click", (e) => {
brush.addEventListener("click", async (e) => {
painter ??= (await import("./spreadsheet_format_painter")).createFormatPainter(ctx);
if (e.detail === 2) sticky = true;
else if (painter.active()) (painter.clear(), (sticky = false));
else (painter.pick(), (sticky = false));
@@ -198,7 +180,7 @@ export function attachExtras(
toolbarRoot?.removeEventListener("click", onToolbarClick);
toolbarRoot?.removeEventListener("change", back);
find?.destroy();
memo.remove();
memo?.destroy();
picker.remove();
brush.remove();
fmt.remove();
@@ -209,18 +191,18 @@ export function attachExtras(
keys: {
find: (replace) => void openFind(replace),
pasteSpecial: () => (armed = true),
comment: editMemo,
comment: () => void editMemo(),
formatDialog: () => void openFormat(),
...data.keys,
escape() {
painter.clear();
painter?.clear();
sticky = false;
showBrush();
},
},
mouse: {
selected() {
if (!painter.active()) return;
if (!painter?.active()) return;
painter.paint(ctx.selection.범위[0], sticky);
showBrush();
},
@@ -228,7 +210,7 @@ export function attachExtras(
const key = hit?.kind === "cell" ? `${hit.r},${hit.c}` : "";
if (key === hovered) return;
hovered = key;
if (!hit || !key || memoAt || !getComment(ctx.sheet(), hit.r, hit.c))
if (!hit || !key || memo?.busy() || !getComment(ctx.sheet(), hit.r, hit.c))
return comments.hideBalloon();
comments.showBalloon(hit.r, hit.c);
put(comments.root!, hit.r, hit.c); // showBalloon 의 cellBox 자리 → 화면 자리로 고침
@@ -0,0 +1,59 @@
/* =============================================================================
* spreadsheet_memo.ts (주인 D)
* 메모 편집(Shift+F2) — 칸 옆 글상자 · Ctrl+Enter 또는 밖을 누르면 적용 · Esc 취소.
* 처음 쓸 때 `spreadsheet_extras.ts` 가 불러옴(크기 예산).
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import type { getComment, setComment } from "./spreadsheet_comments";
import type { SpreadsheetContext } from "./spreadsheet_view_types";
const TITLE = "메모 — Ctrl+Enter 또는 밖을 누르면 적용 · Esc 취소";
export interface Memo {
/** (r, c) 메모를 열고 `place` 로 칸 옆에 둠 */
open(r: number, c: number, place: (node: HTMLElement) => void): void;
/** 편집 중인가 — 풍선을 띄우지 않음 */
busy(): boolean;
destroy(): void;
}
/** 메모 읽기 · 쓰기는 부른 쪽이 넘김 — 여기서 끌어오면 메모 조각이 따로 쪼개져 크기가 늘어남 */
export function createMemo(
ctx: SpreadsheetContext,
back: () => void,
io: { get: typeof getComment; set: typeof setComment },
): Memo {
const memo = el("textarea", { className: "ss-memo", attrs: { hidden: "", title: TITLE } });
ctx.root.append(memo);
let memoAt: { r: number; c: number } | null = null;
const close = (save: boolean): void => {
if (!memoAt) return;
const at = memoAt;
memoAt = null;
memo.setAttribute("hidden", "");
if (save && memo.value !== (io.get(ctx.sheet(), at.r, at.c) ?? ""))
io.set(ctx, at.r, at.c, memo.value.trim());
back();
};
memo.addEventListener("keydown", (e) => {
if (e.key === "Escape" || (e.key === "Enter" && e.ctrlKey)) {
e.preventDefault();
e.stopPropagation();
close(e.key === "Enter");
}
});
memo.addEventListener("blur", () => close(true));
return {
open(r, c, place) {
memoAt = { r, c };
memo.value = io.get(ctx.sheet(), r, c) ?? "";
memo.removeAttribute("hidden");
place(memo);
memo.focus();
},
busy: () => !!memoAt,
destroy: () => memo.remove(),
};
}
+4 -2
View File
@@ -6,7 +6,6 @@
* 머리 끝(폭 · 높이 끌기)은 C 몫 — 여기서 건드리지 않음.
* ========================================================================== */
import { autofitColumn } from "./spreadsheet_autofit";
import { fillCells } from "./spreadsheet_fill";
import {
hasValue,
@@ -242,7 +241,10 @@ export function attachMouse(
// 고른 열 전체의 경계면 고른 열 모두(엑셀)
const g = ctx.selection.범위[0];
const whole = g.r0 === 0 && g.r1 === LAST_R && inRange(g, 0, hit.c);
for (let c = whole ? g.c0 : hit.c; c <= (whole ? g.c1 : hit.c); c++) autofitColumn(ctx, c);
void import("./spreadsheet_autofit").then((m) => {
for (let c = whole ? g.c0 : hit.c; c <= (whole ? g.c1 : hit.c); c++)
m.autofitColumn(ctx, c);
}); // 늦게 — 크기 예산
return;
}
if (hit.kind === "cell" && !ed.editing()) ed.begin();