"""면적산출 범위 → 도면 표(PLAN 41-3 · `M02_MasterTemplete_AreaToDrawing.ts`) — Node 로 TS 를 돌려 판정. 못박는 것 - 면적 상세 [도면으로 보내기] = 고칠 수 있는 층만(읽기 · 프로젝트 층 단추 0). - 보냄 → 표 변환 길(POST …/area/{열}/table) → 같은 열 도면 상세를 엶 → placeTable(style.연결 = T번호). - 다른 열 도면이면 안 놓음 · 기존 기록 · 표지와 안 겹치는 id(T1 이 있으면 T2). - [저장] 문서 `연결표` = 도면에 남은 표만(지운 표는 빠짐) · 값 = 그 표 칸 글 · 기록 없던 도면은 칸 안 만듦. - 다시 열 때 지금 면적 값 ≠ 마지막 값이면 알림 + [업데이트] → updateTable(id, 칸). """ 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 ts = require(process.argv[1] + "/config/node_modules/typescript"); const src = fs.readFileSync(process.argv[1] + "/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts", "utf8"); const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; // 화면 부품 · 길은 가짜 — 단추 · 창 · 알림만 흉내 const made = []; const button = (o) => { const b = { label: o.label, title: "", dataset: {}, on: {}, addEventListener(k, f) { this.on[k] = f; } }; made.push(b); return b; }; const toasts = []; const stubs = { "@config/config_frontend": { API_BASE_URL: "/api" }, "@ui/ui_template_elements": { createButton: button, createInputField: (o) => ({ root: {}, input: { value: o.value, step: "", min: "", select() {}, addEventListener() {} } }), el: (tag, o) => ({ tag, ...o }), showConfirmDialog: async () => true, showToast: (m, k) => toasts.push([k, m]), }, "@ui/ui_template_modal": { // 창이 뜨자마자 [보내기] openModal: (o) => { const body = { parts: [], append(...x) { this.parts.push(...x); } }; const res = { close() {} }; setTimeout(() => o.mount(body, res.close), 0); setTimeout(() => made.at(-1).on.click(), 1); return res; }, }, "./M02_MasterTemplete_Api_Fetch": { getActiveOwner: () => null }, }; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, (n) => stubs[n] ?? require(n)); const { createAreaToDrawing } = box.exports; const table = (texts, style = {}) => ({ type: "Table", shapeData: { cells: texts.map((r) => r.map((t) => ({ text: t }))), style }, }); let now = [["1.5", "2"]]; const calls = []; globalThis.fetch = async (url, init) => { calls.push([url, JSON.parse(init.body)]); const b = JSON.parse(init.body); return { ok: true, json: async () => ({ 표: table(now), 시트: "s1", 범위: b.범위, 값: { "s1!B2": now[0][0], "s1!C2": now[0][1] } }) }; }; (async () => { const opened = []; const notices = []; const ad = createAreaToDrawing({ open: async (s) => opened.push(s), save: async () => {}, isDirty: () => false, showNotice: (n) => notices.push(n), }); const slot = { kids: [], append(x) { this.kids.push(x); } }; const area = { layer: "system", projectId: null, kind: "area", name: "item1", label: "기슭막이" }; ad.onArea({ ...area, layer: "project" }, false, slot); ad.onArea(area, true, slot); const ro = slot.kids.length; ad.onArea(area, false, slot); ad.select({ 시트: "산출", 칸: "B2", 범위: "B2:C2" }); await slot.kids[0].on.click(); await new Promise((r) => setTimeout(r, 20)); const sent = { calls: calls.slice(), opened: opened.slice() }; // 같은 열 도면 — 기록 T1 이 있으니 새 표는 T2 const placed = []; const handle = { placeTable: (e, id) => placed.push([e, id]), updateTable: async (id, c) => (placed.push(["u", id, c]), 1) }; const old = { 연결: "T1", 시트: "s1", 범위: "A1:A1", 글자크기: 3, 값: { "s1!A1": "옛" } }; const drawing = (ents, links) => ({ 양식: "구조물도면", 종류: "structure", 판: 1, 열: "item1", 도번: "SD-1", 도면: { entities: ents }, ...(links ? { 연결표: links } : {}) }); ad.onDrawing(handle, { ...area, kind: "structure" }, drawing([table([["옛"]], { 연결: "T1" })], [old])); const waitPlaced = ad.waiting(); // 놓기 기다림 = 저장 안 한 고침 ad.drop(); const waitDropped = ad.waiting(); await new Promise((r) => setTimeout(r, 20)); const notice = notices.at(-1) ?? []; const upd = notice.find((x) => x && x.on); if (upd) await upd.on.click(); // 저장 — T1(업데이트로 값 바뀜) · T2 · 지운 T3 기록은 빠짐 const t3 = { ...old, 연결: "T3" }; const saved = ad.withLinks(drawing([{ type: "Line" }, table([["옛"]], { 연결: "T1" }), table([["9", "8"]], { 연결: "T2" })], [old, t3])); const plain = ad.withLinks(drawing([table([["x"]])])); // 다른 열 도면이면 안 놓음 const before = placed.length; ad.select({ 시트: "산출", 칸: "A1", 범위: "A1" }); await slot.kids[0].on.click(); await new Promise((r) => setTimeout(r, 20)); ad.onDrawing(handle, { ...area, kind: "structure", name: "item2" }, drawing([])); // 빈 칸만 고른 범위 — 크기를 묻기 전에 막고 알림 · 도면 안 엶 now = [["", " "]]; const openedBefore = opened.length; ad.select({ 시트: "산출", 칸: "E1", 범위: "E1:F1" }); await slot.kids[0].on.click(); await new Promise((r) => setTimeout(r, 20)); const empty = { opened: opened.length - openedBefore, toast: toasts.at(-1) }; console.log(JSON.stringify({ ro, buttons: slot.kids.length, sent, placed, notice: notice.map((x) => (typeof x === "string" ? x : x.label)), saved: saved.연결표, plain: "연결표" in plain, other: placed.length - before, waitPlaced, waitDropped, waitSaved: ad.waiting(), empty })); })(); """ @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): assert got["ro"] == 0 and got["buttons"] == 1 def test_보냄은_표_변환_길_뒤_같은_열_도면을_엶(got): url, body = got["sent"]["calls"][0] assert url == "/api/m02/layers/system/area/item1/table" assert body == {"시트": "산출", "범위": "B2:C2", "글자크기": 2.5} assert got["sent"]["opened"][0]["kind"] == "structure" assert got["sent"]["opened"][0]["name"] == "item1" def test_놓을_표는_겹치지_않는_연결_id(got): entity, link = got["placed"][0] assert link == "T2" and entity["shapeData"]["style"]["연결"] == "T2" def test_다시_열면_다름_알림과_업데이트(got): assert got["notice"][0].startswith("면적산출 값이 바뀐 표 1") assert got["notice"][1] == "업데이트" assert ["u", "T1", [[{"text": "1.5"}, {"text": "2"}]]] in got["placed"] def test_저장_연결표는_남은_표만(got): now = {"s1!B2": "1.5", "s1!C2": "2"} assert got["saved"] == [ {"연결": "T1", "시트": "s1", "범위": "A1:A1", "글자크기": 3, "값": now}, # [업데이트] 값 {"연결": "T2", "시트": "s1", "범위": "B2:C2", "글자크기": 2.5, "값": now}, ] assert got["plain"] is False def test_다른_열_도면에는_안_놓음(got): assert got["other"] == 0 def test_편집_칸은_갈고리만(): main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8") # select · onArea · onDrawing · withLinks · waiting(저장 안 한 고침) · drop(떠나기 확인) assert main.count("toDrawing.") == 6 assert "|| toDrawing.waiting()" in main.split("const busy = ")[1].splitlines()[0] def test_놓기_기다림은_저장_안_한_고침(got): assert got["waitPlaced"] is True and got["waitDropped"] is False and got["waitSaved"] is False def test_빈_범위는_막고_알림(got): assert got["empty"] == {"opened": 0, "toast": ["error", "보낼 칸에 내용이 없음"]}