feat(spreadsheet): 공개 손잡이 addSheet · 되돌리기 한 번 · 탭 없이 띄운 시트에 탭 줄 붙임 · 도면 표 → 면적 새 시트 검증(83-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user