Files
Aislo/resources/tester/spreadsheet/e_harness.ts
T
eomsangdon fddaa4edfb feat(spreadsheet): E 화면 시험 진짜 A · B 엔진으로 다시 · 클립보드 HTML 견본 추가
- 화면 시험 틀 가짜(fakes/) 걷고 진짜 `applyCommand` · `createCalcEngine` · `createHistory` · 주소 · 숫자 형식으로 재시험
- 서식 · 병합 · 탭 이름 바꾸면 다른 시트 참조 식도 옮김 · 안↔안 클립보드 수식 상대 이동을 실제 엔진 계산값으로 확인
- 엑셀(<table> + mso 스타일 · colspan · x:str/x:num · 「수식 표시」) · 구글(google-sheets-html-origin · data-sheets-formula) 클립보드 HTML 견본 추가 · `parseClipboard` 로 붙여넣기 시험
- 이 PC 는 엑셀 앱 없음 · 구글 시트는 로그인 상태로 새 문서까지 열었으나 자동화 창이 OS 포커스를 못 받아(Document is not focused) 실제 클립보드 왕복은 못 함 — 견본 파일 시험으로 갈음
- toolbar · tabs · menu · clipboard 머리 주석에 D(`spreadsheet.ts`) 잇는 자리 설명 추가
2026-09-27 20:54:17 +09:00

160 lines
5.4 KiB
TypeScript

/* =============================================================================
* e_harness.ts — E(도구 모음 · 탭 · 메뉴 · 클립보드) 화면 시험 틀.
* A(주소 · 명령 · 엔진 · 참조 옮김) · B(숫자 형식) 가 이제 실제로 있어 진짜 모듈을 그대로 씀
* (`vite.harness.config.ts` 는 `@ui` 별칭만 · 가짜 없음). dispatch 는 진짜 `applyCommand` →
* 진짜 `createCalcEngine` → 화면 갱신 순서로 실제 앱과 같은 길을 탐.
* ========================================================================== */
import { mountToolbar } from "../../../A00_Common/spreadsheet/spreadsheet_toolbar";
import { mountTabs } from "../../../A00_Common/spreadsheet/spreadsheet_tabs";
import { attachMenu } from "../../../A00_Common/spreadsheet/spreadsheet_menu";
import {
attachClipboard,
parseClipboard,
} from "../../../A00_Common/spreadsheet/spreadsheet_clipboard";
import { applyCommand, emptyWorkbook } from "../../../A00_Common/spreadsheet/spreadsheet_commands";
import { createCalcEngine } from "../../../A00_Common/spreadsheet/spreadsheet_graph";
import { createHistory } from "../../../A00_Common/spreadsheet/spreadsheet_history";
import type { Workbook } from "../../../A00_Common/spreadsheet/spreadsheet_types";
import type {
GridHandle,
Selection,
SpreadsheetContext,
} from "../../../A00_Common/spreadsheet/spreadsheet_view_types";
const log: string[] = [];
const logEl = document.querySelector<HTMLPreElement>("#log")!;
function say(msg: string): void {
log.push(msg);
logEl.textContent = log.join("\n");
}
const book: Workbook = emptyWorkbook("test", "측구수로400");
book.서식 = [{}, { 굵게: true }];
book.시트[0].칸 = {
A1: { 값: "터파기 계산서", 서식: 1 },
B1: { 값: 12.5 },
C1: { 식: "B1*2" },
};
// 탭 이름 바꾸기가 다른 시트 참조 글도 고치는지 보는 둘째 시트.
book.시트.push({
id: "s2",
이름: "둘째",
칸: { A1: { 식: "측구수로400!B1+1" } },
});
const engine = createCalcEngine(book);
const history = createHistory();
const selection: Selection = {
시트: "s1",
범위: [{ r0: 0, c0: 0, r1: 0, c1: 0 }],
활성: { r: 0, c: 0 },
기준: { r: 0, c: 0 },
};
const root = document.querySelector<HTMLDivElement>("#ss-root")!;
const grid: GridHandle = {
root,
render() {},
invalidate() {},
renderSelection() {},
setRefHighlights() {},
cellBox: () => ({ x: 0, y: 0, w: 60, h: 20 }),
hitTest: (clientX) => {
const box = root.getBoundingClientRect();
const x = clientX - box.left;
if (x < 30) return { kind: "rowHead", r: 0, c: 0 };
if (x < 60) return { kind: "colHead", r: 0, c: 0 };
return { kind: "cell", r: selection.활성.r, c: selection.활성.c };
},
reveal() {},
visibleRange: () => ({ r0: 0, c0: 0, r1: 10, c1: 10 }),
editorSlot: () => ({ layer: root, box: { x: 0, y: 0, w: 60, h: 20 } }),
destroy() {},
};
function applyAndRefresh(command: import("../../../A00_Common/spreadsheet/spreadsheet_types").Command): void {
const effect = applyCommand(book, command);
if (effect.rebuild) engine.rebuild(book);
else engine.update(effect.cells);
toolbar.refresh();
tabs.refresh();
}
const ctx: SpreadsheetContext = {
root,
book,
readOnly: false,
engine,
history,
selection,
sheet: () => book.시트.find((s) => s.id === selection.시트)!,
dispatch(command) {
say(`dispatch: ${JSON.stringify(command)}`);
const effect = applyCommand(book, command);
history.push(command, effect.undo);
if (effect.rebuild) engine.rebuild(book);
else engine.update(effect.cells);
toolbar.refresh();
tabs.refresh();
},
undo() {
const cmd = history.undo();
say(`undo: ${cmd ? JSON.stringify(cmd) : "(없음)"}`);
if (cmd) applyAndRefresh(cmd);
},
redo() {
const cmd = history.redo();
say(`redo: ${cmd ? JSON.stringify(cmd) : "(없음)"}`);
if (cmd) applyAndRefresh(cmd);
},
select(sel) {
Object.assign(selection, sel);
say(`select: ${JSON.stringify(sel)}`);
toolbar.refresh();
},
showSheet(sheetId) {
book.활성 = sheetId;
selection.시트 = sheetId;
selection.범위 = [{ r0: 0, c0: 0, r1: 0, c1: 0 }];
selection.활성 = { r: 0, c: 0 };
say(`showSheet: ${sheetId}`);
tabs.refresh();
toolbar.refresh();
},
grid,
editor: {
editing: () => false,
begin() {},
commit() {},
cancel() {},
sync() {},
},
};
const toolbar = mountToolbar(ctx);
const tabs = mountTabs(ctx);
attachMenu(ctx);
attachClipboard(ctx);
document.querySelector("#toolbar-slot")!.append(toolbar.root!);
document.querySelector("#tabs-slot")!.append(tabs.root!);
say("mounted — 도구 모음 · 탭 · 메뉴 · 클립보드 붙음(진짜 A · B 엔진)");
(window as unknown as { __ctx: unknown; __book: unknown }).__ctx = ctx;
(window as unknown as { __ctx: unknown; __book: unknown }).__book = book;
// 클립보드 밖 → 안 시험 — 자리 표시자에 실제로 붙여넣기(Ctrl+V)를 받아 결과를 보임.
const pasteZone = document.querySelector<HTMLDivElement>("#paste-zone")!;
const pasteOut = document.querySelector<HTMLPreElement>("#paste-out")!;
pasteZone.addEventListener("paste", (ev) => {
ev.preventDefault();
const html = ev.clipboardData?.getData("text/html") ?? "";
const text = ev.clipboardData?.getData("text/plain") ?? "";
const block = parseClipboard(html, text, { r: 0, c: 0 });
pasteOut.textContent = `html:\n${html}\n\ntext:\n${text}\n\nblock:\n${JSON.stringify(block, null, 2)}`;
});