feat(spreadsheet): D 잇기 — 진짜 A 엔진과 재검증 · inspect 선택(window.__aisloSheet · 임시) · 시험 틀 한 번 시나리오

- spreadsheet.ts — `inspect` 선택이면 문맥을 window.__aisloSheet 에 걺(M02 [스프레드시트 시험] 단추 전용 · destroy 때 뗌 · 1단계 검증 뒤 지움)
- 시험 틀 — ssScenario()(페이지 안 한 번에 한글 · 가리키기 · 재계산 · 되돌리기 · Delete · Ctrl+Enter · 수식 입력줄 · F2) · 가짜 격자 data-addr · 진짜 분수 값 보임 · 오류 모음

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 20:33:55 +09:00
co-authored by Claude Opus 5.5
parent bc5b4217c1
commit 48a2135132
4 changed files with 133 additions and 2 deletions
+12
View File
@@ -48,6 +48,16 @@ export interface SpreadsheetOptions {
onChange?: (book: Workbook) => void;
/** 고름이 바뀜 — 시트 id · 활성 칸 A1 · 범위 A1 */
onSelect?: (selection: { 시트: string; 칸: string; 범위: string }) => void;
/** ⚠ 임시 — 브레인 브라우저 검증용: 문맥을 `window.__aisloSheet` 에 걺(book · 고름 · 명령 읽기).
* M02 [스프레드시트 시험] 단추만 켬 · 산출근거 카드는 안 켬 · 1단계 검증 뒤 지움. */
inspect?: boolean;
}
declare global {
interface Window {
/** ⚠ 임시 — `inspect` 로 연 표의 문맥(브레인 검증용) */
__aisloSheet?: SpreadsheetContext;
}
}
export interface SpreadsheetHandle {
@@ -139,6 +149,7 @@ export function createSpreadsheet(
grid.render();
select(ctx.selection);
if (opts.inspect) window.__aisloSheet = ctx; // ⚠ 임시 — 브레인 검증용
function notify(message: string): void {
toast.textContent = message;
@@ -244,6 +255,7 @@ export function createSpreadsheet(
return ctx.engine.snapshot();
},
destroy() {
if (window.__aisloSheet === ctx) delete window.__aisloSheet;
clearTimeout(toastTimer);
detachKeys();
detachMouse();
@@ -10,8 +10,116 @@ window.ssSample = JSON.parse(JSON.stringify(sample));
window.ss = createSpreadsheet(host, JSON.parse(JSON.stringify(sample)), {
onChange: () => window.ssChanges++,
onSelect: (s) => (window.ssSelect = s),
inspect: true,
});
window.ssCreate = createSpreadsheet;
/** 칸 입력 값(A1) */
window.ssCell = (a1, sheet = "s1") =>
window.ss.getDoc().시트.find((s) => s.id === sheet).칸[a1] ?? null;
/* 한 번에 도는 시나리오 — 탭이 새로 읽혀도 끊기지 않게 페이지 안에서 합성 사건으로 조작하고 수치를 모음.
* (믿음 입력 — 진짜 키 · 마우스 · 한글 — 은 ORCA keypress · type · mouse 로 따로 잼) */
window.ssScenario = () => {
const ta = () => document.querySelector(".ss-input");
const at = (a1) => {
const d = document.querySelector(`[data-a1="${a1}"]`).getBoundingClientRect();
return {
clientX: d.left + d.width / 2,
clientY: d.top + d.height / 2,
bubbles: true,
button: 0,
};
};
const grid = () => document.querySelector(".ss-grid-host");
const click = (a1, extra = {}) => {
grid().dispatchEvent(new MouseEvent("mousedown", { ...at(a1), ...extra }));
window.dispatchEvent(new MouseEvent("mouseup", at(a1)));
};
const dragTo = (from, to) => {
grid().dispatchEvent(new MouseEvent("mousedown", from));
window.dispatchEvent(new MouseEvent("mousemove", to));
window.dispatchEvent(new MouseEvent("mouseup", to));
};
const key = (k, o = {}) =>
document.activeElement.dispatchEvent(
new KeyboardEvent("keydown", { key: k, bubbles: true, cancelable: true, ...o }),
);
const type = (text) => {
const t = document.activeElement;
t.setRangeText(text, t.selectionStart, t.selectionEnd, "end");
t.dispatchEvent(new Event("input", { bubbles: true }));
};
const shown = (a1) => document.querySelector(`[data-a1="${a1}"]`).textContent;
const sel = () => `${ssSelect.칸}|${ssSelect.범위}`;
const out = {};
// 한글 첫 글자 — compositionstart 로 편집 시작 · 같은 textarea 그대로
click("L2");
ta().dispatchEvent(new CompositionEvent("compositionstart", { data: "" }));
out.imeEditing =
document.querySelector(".ss").classList.contains("ss--editing") &&
document.activeElement === ta();
type("한");
key("Enter");
out.ime = [ssCell("L2"), sel()];
// 수 · 식 · 참조 가리키기(방향키 + 누르기) · 재계산
click("L3");
type("2");
key("Enter");
type("=");
key("ArrowUp");
out.pointKey = ta().value;
type("*");
click("J5");
out.pointClick = [
ta().value,
[...document.querySelectorAll(".fg-ref")].map((x) => x.dataset.ref),
];
key("Enter");
out.calc = [ssCell("L4"), shown("L4")];
click("L3");
type("5");
key("Enter");
out.recalc = shown("L4");
// 되돌리기 · 다시
key("z", { ctrlKey: true });
out.undo = [ssCell("L3"), shown("L4")];
key("y", { ctrlKey: true });
out.redo = [ssCell("L3"), shown("L4")];
// Delete(서식 남김) · 되돌림
click("B4");
key("Delete");
out.del = ssCell("B4");
key("z", { ctrlKey: true });
out.delUndo = ssCell("B4");
// 병합 · Shift/Ctrl 방향
click("A4");
key("ArrowRight", { shiftKey: true });
key("ArrowRight", { shiftKey: true });
out.shift = sel();
click("A3");
key("ArrowDown", { ctrlKey: true });
out.ctrl = sel();
// 끌어 고르기 · Ctrl+Enter
dragTo(at("P2"), at("Q3"));
out.drag = sel();
type("=L3+1");
key("Enter", { ctrlKey: true });
out.ctrlEnter = ["P2", "Q2", "P3", "Q3"].map((k) => ssCell(k)?.식);
// 수식 입력줄로 고치기
click("N2");
const bar = document.querySelector(".ss-formula");
bar.focus();
bar.value = "=J4*10";
bar.dispatchEvent(new Event("input", { bubbles: true }));
key("Enter");
out.bar = [ssCell("N2"), shown("N2"), sel()];
// F2 — 방향키가 글자 자리만
click("N2");
key("F2");
const before = sel();
key("ArrowLeft");
out.f2 = [before === sel(), document.querySelector(".ss").classList.contains("ss--editing")];
key("Escape");
out.errors = window.ssErrors;
return JSON.stringify(out);
};
@@ -23,6 +23,12 @@
<body>
<div id="fakes"></div>
<div id="host"></div>
<script>
window.ssErrors = [];
window.addEventListener("error", (e) =>
window.ssErrors.push(String(e.error?.stack || e.message)),
);
</script>
<script src="../../../tmp/spreadsheet_d/harness.js"></script>
<script>
document.getElementById("fakes").textContent =
@@ -326,6 +326,7 @@ function createGrid(ctx) {
});
function show(v) {
if (v === null || v === undefined) return "";
if (typeof v === "object" && "n" in v) return String(Number(v.n) / Number(v.d)); // 진짜 엔진 분수
if (typeof v === "object") return v.error;
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
return String(v);
@@ -351,13 +352,15 @@ function createGrid(ctx) {
if (m && (m.r0 !== r || m.c0 !== c)) continue;
const d = el("div", {
className: "fg-cell",
attrs: { "data-a1": toA1(r, c) },
attrs: { "data-a1": toA1(r, c), "data-addr": toA1(r, c) }, // data-addr = C 격자와 같은 꼴
text: show(ctx.engine.value(sheet.id, r, c)),
});
place(d, cellBox(r, c));
d.style.cssText +=
";border-right:1px solid #eee;border-bottom:1px solid #eee;padding:2px 3px;overflow:hidden;white-space:nowrap";
if (typeof ctx.engine.value(sheet.id, r, c) === "number") d.style.textAlign = "right";
const v = ctx.engine.value(sheet.id, r, c);
if (typeof v === "number" || (v && typeof v === "object" && "n" in v))
d.style.textAlign = "right";
cellLayer.append(d);
}
renderSelection();
@@ -461,6 +464,8 @@ function mountTabs(ctx) {
module.exports = {
el: { el },
// B 함수 — 없음(진짜 엔진이 부르면 #NAME?)
functions: { getFunction: () => undefined, functionNames: () => [] },
address,
parser,
refshift,