From 152d97af152f1fd8d906c389cd143a0bcbafddc3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 13:00:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(spreadsheet):=20=EA=B3=B5=EA=B0=9C=20?= =?UTF-8?q?=EC=86=90=EC=9E=A1=EC=9D=B4=20addSheet=20=C2=B7=20=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=ED=95=9C=20=EB=B2=88=20=C2=B7=20?= =?UTF-8?q?=ED=83=AD=20=EC=97=86=EC=9D=B4=20=EB=9D=84=EC=9A=B4=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=ED=83=AD=20=EC=A4=84=20=EB=B6=99=EC=9E=84?= =?UTF-8?q?=20=C2=B7=20=EB=8F=84=EB=A9=B4=20=ED=91=9C=20=E2=86=92=20?= =?UTF-8?q?=EB=A9=B4=EC=A0=81=20=EC=83=88=20=EC=8B=9C=ED=8A=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D(83-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X --- A00_Common/spreadsheet/spreadsheet.ts | 41 +++++ .../tester/ui_checks/83-3_새시트_되돌리기.js | 144 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 resources/tester/ui_checks/83-3_새시트_되돌리기.js diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts index 4c7e91766..403d9ac4a 100644 --- a/A00_Common/spreadsheet/spreadsheet.ts +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -36,6 +36,7 @@ import type { CalcValues, Command, CommandEffect, + Sheet, SheetCellAddress, Workbook, } from "./spreadsheet_types"; @@ -81,6 +82,10 @@ export interface SpreadsheetHandle { /** 알약 키를 지금 시트 `칸`(A1)에 잇기 · 풀기(null) — 알약 누름과 같은 규칙 · 되돌리기 됨 · * 그 칸 다른 알약은 함께 둠(`replace` 면 뺌) · 주소가 틀리거나 읽기만이면 false */ setLink(key: string, cell: string | null, info?: LinkInfo, replace?: boolean): boolean; + /** 시트 하나 덧붙임(끝 자리 · 83-3 도면 → 면적) — 되돌리기 한 번 · onChange · 이름 겹치면 「이름 (n)」 · + * `activate` 면 새 시트로 · 탭 줄 없이 띄운 시트면 탭 줄을 붙임(시트 1장으로 돌아가면 숨김) · + * 붙인 시트 이름 · 읽기만이면 null */ + addSheet(sheet: Sheet, opts?: { activate?: boolean }): string | null; destroy(): void; } @@ -154,6 +159,8 @@ export function createSpreadsheet( others.push(...extras.parts); const detachKeys = attachKeys(ctx, ed, extras.keys); const detachMouse = attachMouse(ctx, ed, extras.mouse); + /** `addSheet` 가 나중에 붙인 탭 줄(탭 없이 띄운 시트) */ + let lateTabs: PartHandle | null = null; const toast = el("div", { className: "ss-toast" }); let toastTimer = 0; /** 알약 막대 · 연결 따라가기 — 선택이 있거나 문서에 연결이 있을 때만 불러옴(크기 예산) */ @@ -303,6 +310,40 @@ export function createSpreadsheet( loadLinks(); // 처음 연결이면 알약 막대 · 칸 세모를 불러옴 return true; }, + addSheet(sheet, add) { + if (readOnly) return null; + const taken = (name: string) => + ctx.book.시트.some((x) => x.이름.toLowerCase() === name.toLowerCase()); + let 이름 = sheet.이름; + for (let n = 2; taken(이름); n += 1) 이름 = `${sheet.이름} (${n})`; + let id = sheet.id; + for (let n = ctx.book.시트.length + 1; ctx.book.시트.some((x) => x.id === id); n += 1) + id = `s${n}`; + const before = ctx.book.시트.length; + dispatch({ 종류: "시트더하기", 시트: { ...sheet, id, 이름 }, 자리: ctx.book.시트.length }); + if (ctx.book.시트.length === before) return null; // 명령이 막힘(알림은 냄) + if (noTabs && !lateTabs) { + // 탭 줄 없이 띄운 시트 — 지금 붙이고 시트가 1장이면(되돌리기) 숨김 + const part = mountTabs(ctx); + const bar = part.root; + const sync = (): void => { + const many = ctx.book.시트.length > 1; + if (bar) bar.hidden = !many; + root.classList.toggle("ss--notabs", !many); + }; + lateTabs = { + root: part.root, + refresh: () => (part.refresh(), sync()), + destroy: () => part.destroy(), + }; + if (bar) status.root ? status.root.before(bar) : root.append(bar); + others.push(lateTabs); + sync(); + } + if (add?.activate) showSheet(id); + else for (const p of others) p.refresh(); + return 이름; + }, destroy() { clearTimeout(toastTimer); detachKeys(); diff --git a/resources/tester/ui_checks/83-3_새시트_되돌리기.js b/resources/tester/ui_checks/83-3_새시트_되돌리기.js new file mode 100644 index 000000000..0022e9f56 --- /dev/null +++ b/resources/tester/ui_checks/83-3_새시트_되돌리기.js @@ -0,0 +1,144 @@ +/* 83-3 — 받은 표 → 면적 새 시트(addSheet) · 탭 줄 · 되돌리기 한 번 · 화면 고침 표시 + * 시작 화면: M02 · 시스템 층 임시 항목(「데스크탑보조임시」 · 시트 1장) 면적산출 상세 · 「도면」 패널 펼침 + * 뒤: [저장] → area/<열>.json 새 시트 병합 B1:C1 · 열 폭 21.6/16.2/16.2 · 행 높이 28.3 · 다시 열기 = 탭 2장 · 임시 항목 지움 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 하는 일: 패널 CAD 에 선 표 견본을 보기 전용으로 싣고(저장 안 됨) 끌어 고른 뒤 [면적산출로 보내기] + * → 탭 · 활성 시트 · 칸 글 · 병합 수를 읽음 → 되돌리기(↶) → 다시 읽음 → 다시 보냄(저장은 부른 쪽) + * 기대 수치: + * - before.tabs = 0(탭 줄 없음) · added.tabs = ["…", "도면에서 1"] · added.active = "도면에서 1" + * - added.cells = 구분 · 수량 · 흙깎기 · 1500 · m3 · 돌쌓기 · 2.5 · m2 · added.widths 비 = 40:30:30(156 · 118 · 118 px) + * - undone.tabsShown = false · undone.sheets = 1(탭 줄 숨음) · again.active = "도면에서 1" + * - 끝에 원래 도면을 다시 실음(restored) + */ +(async () => { + const W = (ms) => new Promise((r) => setTimeout(r, ms)); + const $ = (s, root = document) => root.querySelector(s); + const $$ = (s, root = document) => [...root.querySelectorAll(s)]; + const frame = $('.ui-side-panels__panel[data-panel="drawing"] iframe'); + const btn = $("[data-area-from-drawing]"); + if (!frame || !btn) return JSON.stringify({ error: "도면 패널 · 단추 없음" }); + const win = frame.contentWindow; + const cad = win.__aisloCad; + const L = "L83"; + let n = 0; + const base = { lineColor: "#fff", lineWidth: 1, layerId: L }; + const line = (x1, y1, x2, y2) => ({ + id: `t83-${(n += 1)}`, + type: "Line", + ...base, + shapeData: { startPoint: { x: x1, y: y1 }, endPoint: { x: x2, y: y2 } }, + }); + const text = (label, x, y) => ({ + id: `t83-${(n += 1)}`, + type: "Text", + ...base, + shapeData: { + label, + basePoint: { x, y }, + options: { + textDirection: { x: 1, y: 0 }, + textAlign: "left", + textColor: "#fff", + fontSize: 2.5, + fontFamily: "Noto Sans KR", + }, + }, + }); + const grid = [ + line(100, 60, 200, 60), + line(100, 30, 200, 30), + line(100, 60, 100, 30), + line(200, 60, 200, 30), + line(100, 50, 200, 50), + line(100, 40, 200, 40), + line(140, 60, 140, 30), + line(170, 50, 170, 30), + text("구분", 102, 53), + text("수량", 145, 53), + text("흙깎기", 102, 43), + text("1,500", 142, 43), + text("m3", 172, 43), + text("돌쌓기", 102, 33), + text("2.5", 142, 33), + text("m2", 172, 33), + ]; + const ask = (type, want, extra = {}) => + new Promise((resolve) => { + const on = (e) => { + if (e.source !== win || e.data?.type !== want) return; + window.removeEventListener("message", on); + resolve(e.data); + }; + window.addEventListener("message", on); + win.postMessage({ type, ...extra }, location.origin); + setTimeout(() => resolve(null), 5000); + }); + const original = (await ask("aislo:b08:save-request", "aislo:b08:save-response"))?.drawing; + const load = (drawing) => + ask("aislo:b08:load-drawing", "aislo:b08:drawing-loaded", { + drawing, + readOnly: true, + panelsCollapsed: true, + recoveryScope: "verify83", + }); + await load({ + format: 6, + entities: grid, + layers: [{ id: L, name: "검증83", isVisible: true, isLocked: false }], + }); + await W(400); + const canvas = frame.contentDocument.querySelector("canvas"); + const fire = (type, p) => { + const r = canvas.getBoundingClientRect(); + canvas.dispatchEvent( + new win.MouseEvent(type, { + bubbles: true, + clientX: r.left + p.x, + clientY: r.bottom - p.y, + button: 0, + buttons: type === "mouseup" ? 0 : 1, + }), + ); + }; + const a = cad.screen(95, 25), + b = cad.screen(205, 65); + fire("mousemove", a); + await W(60); + fire("mousedown", a); + await W(60); + fire("mousemove", b); + await W(60); + fire("mouseup", b); + await W(250); + const ss = $(".m02-main__host .ss"); + const state = () => { + const bar = $(".ss-tabs", ss); + return { + tabsShown: !!bar && !bar.hidden, + tabs: bar ? $$(".ss-tabs__tab", bar).map((t) => t.innerText.trim()) : [], + active: $(".ss-tabs__tab.is-active", ss)?.innerText.trim() ?? null, + sheets: bar ? $$(".ss-tabs__tab", bar).length : 1, + cells: $$(".aislo-cell__text", ss) + .map((c) => c.innerText.trim()) + .filter(Boolean) + .slice(0, 12), + widths: $$(".aislo-grid__colhead", ss) + .slice(0, 3) + .map((c) => Math.round(c.getBoundingClientRect().width)), + notabsClass: ss.classList.contains("ss--notabs"), + }; + }; + const out = { selected: cad.selection().length, before: state() }; + btn.click(); + await W(800); + out.added = state(); + const undo = $$("button", ss).find((x) => /되돌리기|실행 취소|↶/.test(x.title + x.innerText)); + undo?.click(); + await W(500); + out.undone = state(); + btn.click(); + await W(800); + out.again = state(); + out.restored = !!(await load(original)); + return JSON.stringify(out); +})();