feat(spreadsheet): D 잇기 — 데이터 메뉴(조건부 서식 · 유효성 · 이름 · 눈금선) · 유효성 검사 · 이름 상자 · 여러 범위

- spreadsheet_data_menu.ts(새) — [↑] [↓] [필터] [데이터 ▾] · 패널 창(sub4 조건부 서식 · 유효성 · 이름 관리) · 눈금선 · 유효성 목록 ▼ · Alt+↓
- spreadsheet_editor.ts — 확정 때 유효성 검사(막음 = 편집 유지 · 경고 = 알림) · 주소 상자에 이름 = 그 범위로 · 새 이름 = 지금 고름에 정의
- spreadsheet_mouse.ts · spreadsheet_selection.ts — Ctrl+누르기 여러 범위 · Shift 는 마지막 범위
- spreadsheet_keys.ts · spreadsheet.ts — 유효성에 막히면 이동 · 시트 바꿈 멈춤
- spreadsheet_extras.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:13:10 +09:00
co-authored by Claude Opus 5.5
parent 015c35ad0e
commit 61d9c0de58
8 changed files with 320 additions and 59 deletions
+31 -6
View File
@@ -8,12 +8,14 @@
* 수식 입력줄(방향키가 글자 사이를 옮김). F2 가 둘을 바꿈.
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
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 { tokenize } from "./spreadsheet_parser";
import { cellsOf, move, selectCell } from "./spreadsheet_selection";
import { checkValue, findValidation } from "./spreadsheet_validation";
import type { Cell, CellAddress, CellStyle, Sheet } from "./spreadsheet_types";
import type {
CellBox,
@@ -329,10 +331,12 @@ export function createEditor(ctx: SpreadsheetContext): Editor {
focus();
}
function write(cells: CellAddress[], value: string, from: CellAddress): void {
if (!st) return;
/** 칸에 적음 — 유효성(sub4) 「막음」 을 어기면 안 적고 false(편집 그대로 · 엑셀처럼 다시 치게) */
function write(cells: CellAddress[], value: string, from: CellAddress): boolean {
if (!st) return true;
const sheet = sheetOf(st.sheet);
const out: Record<string, Cell | null> = {};
const warn = new Set<string>();
for (const a of cells) {
let v = value;
if (v.length > 1 && v[0] === "=" && (a.r !== from.r || a.c !== from.c))
@@ -340,21 +344,29 @@ export function createEditor(ctx: SpreadsheetContext): Editor {
const key = toA1(a.r, a.c);
const next = textToCell(v, sheet.칸[key]);
if (next === null && !sheet.칸[key]) continue;
const rule = next?.값 !== undefined ? findValidation(sheet, a.r, a.c) : undefined;
const check = rule && checkValue(rule, next!.값!);
if (check && !check.ok) {
if (check.어기면 === "막음") return (showToast(check.메시지, "error"), false);
warn.add(check.메시지);
}
out[key] = next;
}
for (const m of warn) showToast(m, "warning");
if (Object.keys(out).length) ctx.dispatch({ 종류: "칸", 시트: sheet.id, 칸: out });
return true;
}
function commit(): void {
if (!st) return;
const value = text();
if (value !== st.original) write([st.at], value, st.at);
if (value !== st.original && !write([st.at], value, st.at)) return;
end();
}
function commitAll(): void {
if (!st) return;
write(cellsOf(sheetOf(st.sheet), ctx.selection), text(), st.at);
if (!write(cellsOf(sheetOf(st.sheet), ctx.selection), text(), st.at)) return;
end();
}
@@ -441,6 +453,19 @@ export function createEditor(ctx: SpreadsheetContext): Editor {
focus();
});
/** 이름 상자(sub4) — 있는 이름이면 그 범위로 · 없으면 지금 고름에 이름을 붙임(엑셀) */
function nameBox(name: string): void {
const hit = resolveName(ctx.book, ctx.selection.시트, name);
if (hit) {
if (hit.시트 !== ctx.selection.시트) ctx.showSheet(hit.시트);
const at = { r: hit.범위.r0, c: hit.범위.c0 };
return ctx.select({ 시트: hit.시트, 범위: [hit.범위], 활성: at, 기준: { ...at } }, true);
}
const why = ctx.readOnly ? null : defineName(ctx, name, ctx.selection.범위[0]);
if (why) showToast(why, "error");
sync();
}
function goTo(value: string): void {
const bang = value.lastIndexOf("!");
let sheet = ctx.sheet();
@@ -454,7 +479,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 sync();
if (!g) return 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);