- 공용 탭 줄: 마지막 한 탭 닫기 없음 · 지금 탭 미저장 점 · 탭 하나만 저장 표시 지움 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
234 lines
11 KiB
Python
234 lines
11 KiB
Python
"""CAD 공용 탭 줄(PLAN 82-1 `cad_host_tabs.ts`) · M02 구조물 도면 여러 장(82-2 `mountStructureSheets`)
|
|
— Node 로 TS 를 돌려 판정(가짜 DOM · 가짜 CAD).
|
|
|
|
못박는 것
|
|
- 탭 바꿀 때 지금 장을 받아 두고(capture) 다음 장을 실음 · 탭 셋 왕복 뒤 장마다 엔티티 수 그대로.
|
|
- 고정 탭(1장) 앞으로는 못 끎 · 지우기 · 같은 이름 막음.
|
|
- 여러 장 문서 getDoc = `도면`(1장) + `더한장[{장id, 이름, 도면}]` · 새 장 · 장 도번 SD-1-2 · 백업 칸 장마다.
|
|
- 안 실린 장 표 갈기(updateTable) = 받아 둔 장에서.
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPT = r"""
|
|
const fs = require("fs");
|
|
const root = process.argv[1];
|
|
const ts = require(root + "/config/node_modules/typescript");
|
|
const load = (file, stubs) => {
|
|
const src = fs.readFileSync(root + "/" + file, "utf8");
|
|
const js = ts.transpileModule(src, {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
new Function("exports", "module", "require", js)(box.exports, box, (n) =>
|
|
n.endsWith(".css") ? {} : (stubs[n] ?? require(n)),
|
|
);
|
|
return box.exports;
|
|
};
|
|
// 가짜 DOM — 쓰는 것만
|
|
const node = (tag) => ({
|
|
tag, kids: [], dataset: {}, on: {}, attrs: {}, hidden: false, textContent: "", title: "", className: "",
|
|
classList: { add() {}, toggle() {} }, style: {},
|
|
append(...x) { this.kids.push(...x); }, prepend(...x) { this.kids.unshift(...x); },
|
|
replaceChildren(...x) { this.kids = x; }, replaceWith() {}, remove() {}, select() {}, focus() {},
|
|
setAttribute(k, v) { this.attrs[k] = v; }, addEventListener(k, f) { this.on[k] = f; },
|
|
getBoundingClientRect: () => ({ left: 0, width: 100 }), querySelector: () => null,
|
|
});
|
|
globalThis.document = { createElement: node };
|
|
const toasts = [];
|
|
const confirms = [];
|
|
const elements = {
|
|
showToast: (m, k) => toasts.push([k, m]),
|
|
showConfirmDialog: async (m) => (confirms.push(m), true),
|
|
createButton: () => node("button"),
|
|
createInputField: (o) => ({ root: node("div"), input: { value: o.value, disabled: false, addEventListener() {} }, setError() {} }),
|
|
};
|
|
const tabsMod = load("ui_template/cad_host/cad_host_tabs.ts", { "@ui/ui_template_elements": elements });
|
|
|
|
(async () => {
|
|
const out = {};
|
|
// ① 탭 셋 왕복 — CAD 하나에 장을 갈아 실음
|
|
let shown = { id: "a", n: 3 };
|
|
const log = [];
|
|
const tabs = tabsMod.createCadTabs({
|
|
features: { add: true, rename: true, remove: true, reorder: true },
|
|
capture: async () => ({ drawing: { ...shown, n: shown.n + (shown.id === "b" ? 1 : 0) }, dirty: shown.id === "b" }),
|
|
show: (tab) => (log.push(tab.id), (shown = tab.drawing)),
|
|
onChange: () => log.push("change"),
|
|
});
|
|
tabs.set([
|
|
{ id: "a", name: "1장", drawing: { id: "a", n: 3 }, fixed: true },
|
|
{ id: "b", name: "2장", drawing: { id: "b", n: 5 } },
|
|
{ id: "c", name: "3장", drawing: { id: "c", n: 0 } },
|
|
], "a");
|
|
for (const id of ["b", "c", "a", "b", "a"]) await tabs.select(id);
|
|
const all = await tabs.collect();
|
|
out.roundtrip = all.map((t) => [t.id, t.drawing.n, !!t.dirty]);
|
|
out.shownOrder = log.filter((x) => x !== "change");
|
|
// ② 끌기 — c 를 a(고정) 앞에 놓아도 a 뒤
|
|
const tabNode = (id) => tabs.element.kids.find((k) => k.dataset.tabId === id);
|
|
tabNode("c").on.dragstart({ dataTransfer: { setData() {} } });
|
|
tabNode("a").on.dragover({ preventDefault() {}, clientX: 10 });
|
|
tabNode("a").on.drop({ preventDefault() {} });
|
|
out.order = tabs.tabs().map((t) => t.id);
|
|
out.fixedDraggable = !!tabNode("a").draggable;
|
|
out.fixedX = tabNode("a").kids.length; // 고정 탭 = 글만(지우기 없음)
|
|
// ③ 지우기 — 물음 뒤 · 지금 탭이면 다음 탭을 실음
|
|
await tabs.select("c");
|
|
const xc = tabNode("c").kids.find((k) => k.className === "cad-tabs__x");
|
|
await xc.on.click({ stopPropagation() {} });
|
|
out.afterRemove = { ids: tabs.tabs().map((t) => t.id), active: tabs.active().id, confirms: confirms.length };
|
|
// ④ [+] — create 없으면 아무것도 안 함 · 닫기 기능(B07) = 미저장 탭만 물음
|
|
const b07 = [];
|
|
const closer = tabsMod.createCadTabs({
|
|
features: { close: true },
|
|
capture: async () => ({ drawing: "편집본", dirty: true }),
|
|
show: (tab) => b07.push(["show", tab.id, tab.drawing ?? null, !!tab.dirty]),
|
|
onClosed: (tab) => b07.push(["closed", tab.id]),
|
|
});
|
|
await closer.open({ id: "d1", name: "종단" });
|
|
await closer.open({ id: "d2", name: "횡단" });
|
|
await closer.open({ id: "d1", name: "종단" });
|
|
const before = confirms.length;
|
|
const d2 = closer.element.kids.find((k) => k.dataset.tabId === "d2");
|
|
await d2.kids.find((k) => k.className === "cad-tabs__x").on.click({ stopPropagation() {} });
|
|
const last = closer.element.kids.find((k) => k.dataset.tabId === "d1").kids.length; // 마지막 탭 = 닫기 없음
|
|
out.b07 = { log: b07, asked: confirms.length - before, ids: closer.tabs().map((t) => t.id), last };
|
|
|
|
// ⑤ M02 여러 장 — 가짜 CAD(requestSave = 지금 실린 도면 + 편집)
|
|
const cadLoads = [];
|
|
let inCad = null;
|
|
const fakeCad = {
|
|
element: node("div"),
|
|
beginLoading() {},
|
|
load: (drawing, meta, frameEdit, fields, extra) => (cadLoads.push(extra.recoveryScope), (inCad = drawing)),
|
|
requestSave: async () => ({ drawing: inCad, quantityTable: null }),
|
|
placeTable() {},
|
|
updateTable: async () => 1,
|
|
destroy() {},
|
|
};
|
|
const urls = [];
|
|
globalThis.fetch = async (url) => (urls.push(url), { ok: true, json: async () => ({ entities: [{ type: "Text", url, layerId: "m02-structure-frame" }], layer: { id: "m02-structure-frame" } }) });
|
|
const drawingMod = load("M02_MasterTemplete/M02_MasterTemplete_Drawing.ts", {
|
|
"@config/config_frontend": { API_BASE_URL: "/api" },
|
|
"@ui/cad_host/cad_host": { createCadHost: () => fakeCad },
|
|
"@ui/cad_host/cad_host_tabs": tabsMod,
|
|
"@ui/ui_template_elements": elements,
|
|
"@ui/ui_template_modal": { openModal: () => ({ close() {} }) },
|
|
});
|
|
const table = (link) => ({ type: "Table", shapeData: { cells: [[{ text: "옛" }]], style: { 연결: link } } });
|
|
const doc = {
|
|
도번: "SD-1",
|
|
도면: { format: 6, entities: [{ type: "Line" }, { type: "Line" }], layers: [] },
|
|
더한장: [{ 장id: "s2", 이름: "단면", 도면: { format: 6, entities: [table("T2")], layers: [] } }],
|
|
};
|
|
const dirt = [];
|
|
const host = node("div");
|
|
const sheets = drawingMod.mountStructureSheets(host, doc, {
|
|
name: "rv", layer: "system", noArea: true, frame: true, onChanged: (d) => dirt.push(d),
|
|
});
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
const bar = host.kids[0].kids.find((k) => k.className === "cad-tabs");
|
|
const plus = bar.kids.find((k) => k.className === "cad-tabs__add");
|
|
await plus.on.click();
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
inCad = { ...inCad, entities: [...inCad.entities, { type: "Circle" }] }; // 새 장에 하나 그림
|
|
const third = bar.kids.find((k) => k.dataset.tabId && k.dataset.active === "true").dataset.tabId;
|
|
await bar.kids.find((k) => k.dataset.tabId === "#1").on.click();
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
out.updated = await sheets.updateTable("T2", [[{ text: "새" }]]);
|
|
const got = await sheets.getDoc();
|
|
out.m02 = {
|
|
first: got.도면.entities.length,
|
|
more: got.더한장.map((s) => [s.이름, s.도면.entities.length]),
|
|
t2: got.더한장[0].도면.entities[0].shapeData.cells[0][0].text,
|
|
newId: got.더한장[1].장id === third,
|
|
scopes: cadLoads,
|
|
active: sheets.activeSheet(),
|
|
dirty: dirt.at(-1),
|
|
number: drawingMod.sheetNumber("SD-1", 2),
|
|
frames: urls,
|
|
};
|
|
console.log(JSON.stringify(out));
|
|
})();
|
|
"""
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def got():
|
|
if shutil.which("node") is None:
|
|
pytest.skip("node 가 없음")
|
|
proc = subprocess.run(
|
|
["node", "-e", SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8"
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout.strip().splitlines()[-1])
|
|
|
|
|
|
def test_탭_셋_왕복_엔티티_수_그대로_고친_장만_표시(got):
|
|
# b 는 들를 때마다 하나씩 더 그림(+1 두 번) · a · c 는 그대로
|
|
assert got["roundtrip"] == [["a", 3, False], ["b", 7, True], ["c", 0, False]]
|
|
assert got["shownOrder"] == ["b", "c", "a", "b", "a"]
|
|
|
|
|
|
def test_고정_탭_앞으로는_못_끎(got):
|
|
assert got["order"] == ["a", "c", "b"]
|
|
assert got["fixedDraggable"] is False and got["fixedX"] == 1
|
|
|
|
|
|
def test_지우기는_물음_뒤_다음_탭을_실음(got):
|
|
assert got["afterRemove"] == {"ids": ["a", "b"], "active": "b", "confirms": 1}
|
|
|
|
|
|
def test_닫기_기능은_미저장_탭만_물음(got):
|
|
b07 = got["b07"]
|
|
assert b07["log"][:3] == [
|
|
["show", "d1", None, False],
|
|
["show", "d2", None, False],
|
|
["show", "d1", "편집본", True], # 받아 둔 편집본으로 돌아옴
|
|
]
|
|
assert b07["asked"] == 1 and ["closed", "d2"] in b07["log"] and b07["ids"] == ["d1"]
|
|
assert b07["last"] == 1
|
|
|
|
|
|
def test_B07_목록에서_연_도면이_탭_미저장_편집_지킴():
|
|
page = (ROOT / "B07_DesignDetail/B07_DesignDetail_UI_Page.ts").read_text("utf-8")
|
|
assert "features: { close: true }," in page # [+] · 지우기 없음(서버 길 없음)
|
|
assert "dirty: cadDirty }" in page # 떠날 때 받아 둠
|
|
assert "tab.dirty ? tab.drawing : undefined" in page # 돌아오면 받아 둔 편집본
|
|
assert "cadDirty = !!stash;" in page and "cad.load(stash ?? response.drawing" in page
|
|
assert "if (!cadDirty && !tabs.anyDirty()) return true;" in page
|
|
assert "void openTab(drawings[target]);" in page # 이전 · 다음 = 그 도면 탭
|
|
|
|
|
|
def test_여러_장_문서_장_도번_백업_칸(got):
|
|
m02 = got["m02"]
|
|
assert m02["first"] == 2
|
|
assert m02["more"] == [["단면", 1], ["3장", 1]]
|
|
assert m02["newId"] is True and m02["active"] == ""
|
|
assert m02["scopes"][0] == "m02:system:rv" and m02["scopes"][1].startswith("m02:system:rv#s")
|
|
assert m02["number"] == "SD-1-3"
|
|
q = "/api/m02/structure-frame?number="
|
|
assert m02["frames"] == [q + "SD-1", q + "SD-1-3"] # 장 도번마다 한 번(1장은 다시 안 받음)
|
|
assert m02["dirty"] is True # 장 더함 = 저장 안 한 고침
|
|
|
|
|
|
def test_안_실린_장_표도_갈음(got):
|
|
assert got["updated"] == 2 # 받아 둔 둘째 장 1 + 실린 장(가짜 CAD) 1
|
|
assert got["m02"]["t2"] == "새"
|
|
|
|
|
|
def test_카드는_1장_그리고_장_수():
|
|
card = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts").read_text("utf-8")
|
|
assert "const sheets = 1 + (doc.더한장?.length ?? 0);" in card
|
|
assert "badge.textContent = `장 ${sheets}`;" in card
|
|
main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8")
|
|
assert '"mountStructureSheets"' in main
|
|
assert '...(edited as Pick<StructureDoc, "도면" | "더한장">),' in main
|