- 112-1 면적 표 변환을 공용 파일(spreadsheet_area_table.ts)로 빼 서버 Node 와 브라우저가 한 벌로 씀 · 도면 패널이 칸 고침마다(값 바뀐 표만 · 펼친 동안만) CAD 연결 표 갈기를 보기 전용 갈음(preview)으로 보냄 - 112-2 보낸 범위 이름은 도면 [저장] 요청 안에서 서버가 면적 문서에 둠(놓기만 하고 저장 안 하면 안 생김) · [보내기 취소]는 화면에서만 빼고 면적 [저장] 때 서버가 연결표 줄 · 이름을 뺌 · /name · /unlink 길 삭제 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
263 lines
12 KiB
Python
263 lines
12 KiB
Python
"""면적산출 범위 → 도면 표(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,
|
|
readTemplate: async () => ({ 문서: { 연결표: [
|
|
{ 연결: "T1", 시트: "s1", 범위: "A1:A1", 이름: "도면_T1", 글자크기: 3, 값: {} },
|
|
{ 연결: "T2", 시트: "s1", 범위: "B1:B1", 글자크기: 3, 값: {} },
|
|
] } }),
|
|
},
|
|
};
|
|
const box = { exports: {} };
|
|
new Function("exports", "module", "require", js)(box.exports, box, (n) => stubs[n] ?? require(n));
|
|
const { createAreaToDrawing, sentRanges } = 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([]));
|
|
const other = placed.length - before;
|
|
// 여러 장(82-3) — 둘째 장 T2 가 있으니 새 표는 T3 · 저장 연결표 줄 장 = 표가 놓인 장id
|
|
ad.select({ 시트: "산출", 칸: "A1", 범위: "A1" });
|
|
await slot.kids[0].on.click();
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
const multi = drawing([table([["a"]], { 연결: "T1" })], [old]);
|
|
multi.더한장 = [{ 장id: "s2", 이름: "2장", 도면: { entities: [table([["b"]], { 연결: "T2" })] } }];
|
|
ad.onDrawing(handle, { ...area, kind: "structure" }, multi);
|
|
const multiId = placed.at(-1)[1];
|
|
const multiSaved = ad.withLinks({ ...multi, 연결표: [old, { ...old, 연결: "T2" }] }).연결표.map((r) => [r.연결, r.장]);
|
|
// 도면에서 Esc(놓지 않고 그만둠) — 기다림 풀림 · 그 보내기 기록은 저장 때 안 올라감
|
|
ad.select({ 시트: "산출", 칸: "A1", 범위: "A1" });
|
|
await slot.kids[0].on.click();
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
ad.onDrawing(handle, { ...area, kind: "structure" }, drawing([]));
|
|
const escId = placed.at(-1)[1];
|
|
const escWaiting = ad.waiting();
|
|
ad.cancelPlace();
|
|
const esc = { before: escWaiting, after: ad.waiting(), rows: (ad.withLinks(drawing([table([["z"]], { 연결: escId })], [old])).연결표 ?? []).map((r) => r.연결) };
|
|
// 빈 칸만 고른 범위 — 크기를 묻기 전에 막고 알림 · 도면 안 엶
|
|
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) };
|
|
// 112-2 — 이름은 도면 [저장] 요청 안에서 서버가 두니 놓을 때 서버를 안 부름(표 변환 길만)
|
|
const urls = calls.map((c) => c[0]);
|
|
// 112-2 — [보내기 취소]: 화면에서만(서버 안 부름) · 저장 안 한 고침 · 면적 저장 문서에 보내기취소[] · 저장 뒤 비움
|
|
const callsBefore = calls.length;
|
|
const marks = await sentRanges(area, (id) => ad.cancel(id));
|
|
const cancelOk = await marks[0].취소();
|
|
const cancelWaiting = ad.waiting();
|
|
const cancelSet = [...ad.cancelled()];
|
|
const withKey = ad.withCancels({ 열: "item1" });
|
|
const second = ad.withCancels({ 열: "item1", 보내기취소: ["옛"] }); // 앞 저장 것은 떼고 다시 담음
|
|
ad.cancel("T2");
|
|
const both = ad.withCancels({ 열: "item1" }).보내기취소;
|
|
ad.drop();
|
|
const cancel = { ok: cancelOk, serverCalls: calls.length - callsBefore, waiting: cancelWaiting, set: cancelSet, withKey, second, both, after: ad.waiting(), none: ad.withCancels({ 열: "item1", 보내기취소: ["옛"] }), labels: marks.map((m) => [m.이름, m.범위이름 ?? null, !!m.취소]) };
|
|
console.log(JSON.stringify({ urls, cancel, esc, ro, buttons: slot.kids.length, sent, placed, notice: notice.map((x) => (typeof x === "string" ? x : x.label)), saved: saved.연결표, multiId, multiSaved, plain: "연결표" in plain, other, 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",
|
|
"이름": "도면_T2", # 104-1 — 새로 놓은 표만 이름정의(옛 T1 기록은 그대로 범위만)
|
|
"글자크기": 2.5,
|
|
"값": now,
|
|
},
|
|
]
|
|
assert got["plain"] is False
|
|
|
|
|
|
def test_여러_장_새_번호는_모든_장에서_안_쓴_것_연결표_장(got):
|
|
assert got["multiId"] == "T3"
|
|
assert got["multiSaved"] == [["T1", ""], ["T2", "s2"]]
|
|
|
|
|
|
def test_다른_열_도면에는_안_놓음(got):
|
|
assert got["other"] == 0
|
|
|
|
|
|
def test_이름은_놓을_때_서버에_안_씀_도면_저장_때_서버가_둠_112_2(got):
|
|
assert all(url.endswith("/table") for url in got["urls"]) # …/name 을 부르는 길이 없음
|
|
src = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts").read_text("utf-8")
|
|
assert "defineAreaName" not in src and "/unlink" not in src and "/name" not in src
|
|
assert got["saved"][1]["이름"] == "도면_T2" # 새 줄 이름은 저장 문서 연결표에 실려 감
|
|
|
|
|
|
def test_보내기_취소는_화면에서만_저장_안_한_고침_저장에_실려_감_112_2(got):
|
|
cancel = got["cancel"]
|
|
assert cancel["labels"] == [["T1 도면", "도면_T1", True], ["T2 도면", None, True]]
|
|
assert cancel["ok"] is True and cancel["serverCalls"] == 0 # 서버 안 부름
|
|
assert cancel["waiting"] is True and cancel["set"] == ["T1"] # 면적 화면 「저장 안 한 고침」
|
|
assert cancel["withKey"] == {"열": "item1", "보내기취소": ["T1"]}
|
|
assert cancel["second"] == {"열": "item1", "보내기취소": ["T1"]} # 앞 저장 칸은 떼고 다시
|
|
assert cancel["both"] == ["T1", "T2"]
|
|
assert cancel["after"] is False # 저장 · 떠남(drop) 뒤 비움
|
|
assert cancel["none"] == {"열": "item1"}
|
|
|
|
|
|
def test_편집_칸은_갈고리만():
|
|
main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8")
|
|
# select · onArea · onDrawing · withLinks · waiting(저장 안 한 고침) · drop(떠나기 확인 · 저장 끝) ×2 ·
|
|
# cancelPlace(Esc) · cancel · cancelled · withCancels
|
|
assert main.count("toDrawing.") == 11
|
|
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", "보낼 칸에 내용이 없음"]}
|
|
|
|
|
|
def test_Esc로_놓지_않고_그만두면_기다림이_풀리고_기록도_뺌(got):
|
|
assert got["esc"] == {"before": True, "after": False, "rows": ["T1"]}
|