- 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
225 lines
8.4 KiB
Python
225 lines
8.4 KiB
Python
"""면적 상세 오른쪽 도면 패널 — 저장 전 실시간 표 값(PLAN 112-1) — Node 로 TS 를 돌려 판정.
|
|
|
|
못박는 것
|
|
- 브라우저 `areaTable`(`spreadsheet_area_table.ts`)이 서버 Node 번들 `area_tables` 와 같은 표 · 값 · 까닭을 냄
|
|
(한 벌의 코드 — 서버 파일이 그 파일을 가져다 씀).
|
|
- `AreaDrawing.sync` — 패널이 펼쳐진 동안만 · 값이 바뀐 연결 표만 · CAD 연결 표 갈기를 보기 전용 갈음(preview)으로 ·
|
|
이름이 지워진 표는 건너뜀 · 보내기 취소한 연결은 뺌 · 접었다 펴면 따라잡음 · 도면 문서는 안 건드림.
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from common_util import common_util_spreadsheet as sheet_node
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
SHEET = {
|
|
"id": "s1",
|
|
"이름": "면적산출",
|
|
"칸": {
|
|
"A1": {"값": "제목", "서식": 2},
|
|
"B1": {"값": 1234.5, "서식": 1},
|
|
"A2": {"값": "합"},
|
|
"B2": {"식": "B1*2", "서식": 1},
|
|
"A3": {"값": "숨김행"},
|
|
"C1": {"값": "x"},
|
|
},
|
|
"병합": ["A1:A2", "C1:E5"],
|
|
"행": {"3": {"숨김": True}},
|
|
"열": {"D": {"숨김": True}, "B": {"폭": 12}},
|
|
"이름정의": [{"이름": "도면_T1", "범위": {"r0": 0, "c0": 1, "r1": 1, "c1": 2}}],
|
|
}
|
|
BOOK = {
|
|
"종류": "통합문서",
|
|
"판": 1,
|
|
"열": "rv",
|
|
"서식": [{}, {"형식": "#,##0.00", "굵게": True}, {"가로": "center"}],
|
|
"시트": [SHEET],
|
|
"활성": "s1",
|
|
}
|
|
ASKS = [
|
|
{"시트": "면적산출", "범위": "A1:B2"},
|
|
{"시트": "s1", "범위": "A1:D4", "글자크기": 5},
|
|
{"시트": "s1", "범위": "", "이름": "도면_T1"},
|
|
{"시트": "s1", "범위": "A1", "이름": "도면_없음"},
|
|
{"시트": "없음", "범위": "A1"},
|
|
{"시트": "s1", "범위": "가나"},
|
|
{"시트": "s1", "범위": "A3"},
|
|
]
|
|
|
|
LOADER = r"""
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ROOT = process.argv[1];
|
|
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
|
const cache = new Map();
|
|
function loadTs(file, stubs = {}) {
|
|
const full = path.resolve(file);
|
|
if (cache.has(full)) return cache.get(full).exports;
|
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
}).outputText;
|
|
const mod = { exports: {} };
|
|
cache.set(full, mod);
|
|
const req = (r) => {
|
|
if (r in stubs) return stubs[r];
|
|
if (r.endsWith(".css")) return {};
|
|
if (r.startsWith("@ui/")) return loadTs(path.join(ROOT, "ui_template", `${r.slice(4)}.ts`));
|
|
if (r.startsWith(".")) return loadTs(path.join(path.dirname(full), `${r}.ts`));
|
|
return require(r);
|
|
};
|
|
new Function("exports", "module", "require", js)(mod.exports, mod, req);
|
|
return mod.exports;
|
|
}
|
|
const SS = (name) => path.join(ROOT, "A00_Common", "spreadsheet", `${name}.ts`);
|
|
"""
|
|
|
|
TABLE_SCRIPT = (
|
|
LOADER
|
|
+ r"""
|
|
const { areaTable } = loadTs(SS("spreadsheet_area_table"));
|
|
const { book, asks } = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
const out = asks.map((ask) => {
|
|
try {
|
|
return { ok: true, ...areaTable({ book: structuredClone(book), ...ask }) };
|
|
} catch (e) {
|
|
return { ok: false, 까닭: e.message };
|
|
}
|
|
});
|
|
console.log(JSON.stringify(out));
|
|
"""
|
|
)
|
|
|
|
LIVE_SCRIPT = (
|
|
LOADER
|
|
+ r"""
|
|
const base = JSON.parse(process.argv[2]);
|
|
const book = (cells = {}) => ({ ...base, 시트: [{ ...base.시트[0], 칸: { ...base.시트[0].칸, ...cells } }] });
|
|
const rows = [
|
|
{ 연결: "T1", 시트: "s1", 범위: "A1:B2", 이름: "도면_T1", 글자크기: 2.5, 값: {} },
|
|
{ 연결: "T2", 시트: "s1", 범위: "A1:A1", 이름: "도면_없음", 글자크기: 2.5, 값: {} },
|
|
{ 연결: "T3", 시트: "s1", 범위: "B1:B1", 글자크기: 2.5, 값: {} },
|
|
];
|
|
const same = (a = {}, b = {}) => JSON.stringify(a) === JSON.stringify(b);
|
|
const calls = [];
|
|
const handle = {
|
|
updateTable: async (link, cells, preview) => (calls.push({ link, cells, preview }), 1),
|
|
destroy() {},
|
|
};
|
|
const fake = (tag, o = {}) => ({ tag, dataset: {}, ...o, append() {}, replaceChildren() {}, querySelector: () => null, remove() {}, addEventListener() {} });
|
|
globalThis.window = { addEventListener() {}, removeEventListener() {}, location: { origin: "" }, setTimeout, clearTimeout };
|
|
const { createAreaDrawing } = loadTs(path.join(ROOT, "M02_MasterTemplete", "M02_MasterTemplete_AreaDrawing.ts"), {
|
|
"@ui/ui_template_elements": { createButton: (o) => fake("button", o), el: fake },
|
|
"./M02_MasterTemplete_Api_Fetch": { readTemplate: async () => ({ 문서: { 도면: { entities: [] }, 연결표: rows } }) },
|
|
"./M02_MasterTemplete_Drawing": { mountStructureSheets: () => handle },
|
|
"./M02_MasterTemplete_AreaToDrawing": { sameValues: same },
|
|
});
|
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const take = () => calls.splice(0).map((c) => [c.link, c.preview, c.cells.at(-1)[0]?.text ?? null]);
|
|
|
|
(async () => {
|
|
const cancelled = new Set();
|
|
const ad = createAreaDrawing("system", "rv", { cancelled: () => cancelled });
|
|
const log = {};
|
|
ad.sync(() => book());
|
|
await wait(400);
|
|
log.closed = take(); // 패널이 한 번도 안 열림 — 건너뜀
|
|
ad.show();
|
|
await wait(500);
|
|
log.opened = take(); // 열리면 따라잡음 — 이름이 지워진 T2 는 건너뜀
|
|
ad.sync(() => book());
|
|
await wait(400);
|
|
log.same = take(); // 같은 값 — 도면에 안 보냄
|
|
ad.sync(() => book({ B1: { 값: 10, 서식: 1 } }));
|
|
await wait(400);
|
|
log.edited = take(); // 값이 바뀐 표만
|
|
ad.hide();
|
|
ad.sync(() => book({ B1: { 값: 20, 서식: 1 } }));
|
|
await wait(400);
|
|
log.hidden = take(); // 접힌 동안은 건너뜀
|
|
ad.show();
|
|
await wait(400);
|
|
log.again = take(); // 다시 펴면 따라잡음
|
|
cancelled.add("T1");
|
|
ad.sync(() => book({ B1: { 값: 30, 서식: 1 } }));
|
|
await wait(400);
|
|
log.cancelled = take(); // 보내기 취소한 연결은 뺌
|
|
ad.destroy();
|
|
console.log(JSON.stringify(log));
|
|
})();
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def node() -> str:
|
|
if shutil.which("node") is None:
|
|
pytest.skip("node 가 없음")
|
|
return "node"
|
|
|
|
|
|
def test_브라우저_표_변환은_서버_번들과_같은_표_값_까닭(node, tmp_path):
|
|
src = tmp_path / "ask.json"
|
|
src.write_text(json.dumps({"book": BOOK, "asks": ASKS}, ensure_ascii=False), encoding="utf-8")
|
|
proc = subprocess.run(
|
|
[node, "-e", TABLE_SCRIPT, str(ROOT), str(src)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
browser = json.loads(proc.stdout.strip().splitlines()[-1])
|
|
server = sheet_node.area_tables([{"book": BOOK, **ask} for ask in ASKS])
|
|
assert browser == server
|
|
assert [x["ok"] for x in browser] == [True, True, True, False, False, False, False]
|
|
assert browser[2]["범위"] == "B1:C2" # 이름정의가 가리키는 지금 범위
|
|
|
|
|
|
def test_서버_번들은_그_파일을_가져다_씀():
|
|
src = (ROOT / "common_util/common_util_spreadsheet_node.ts").read_text("utf-8")
|
|
assert 'from "../A00_Common/spreadsheet/spreadsheet_area_table"' in src
|
|
assert "function areaTable" not in src # 두 벌로 갈라지지 않음
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def live(node):
|
|
proc = subprocess.run(
|
|
[node, "-e", LIVE_SCRIPT, str(ROOT), json.dumps(BOOK, ensure_ascii=False)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout.strip().splitlines()[-1])
|
|
|
|
|
|
def test_패널이_닫혀_있으면_건너뜀(live):
|
|
assert live["closed"] == []
|
|
|
|
|
|
def test_열리면_따라잡음_보기_전용_갈음_이름_지워진_표는_건너뜀(live):
|
|
# [연결, preview, 마지막 줄 첫 칸 글] — T1 = B1:C2 이름 범위 · T3 = B1 한 칸
|
|
assert [x[:2] for x in live["opened"]] == [["T1", True], ["T3", True]]
|
|
assert live["opened"][0][2] == "2,469.00"
|
|
|
|
|
|
def test_같은_값이면_안_보내고_바뀐_표만_보냄(live):
|
|
assert live["same"] == []
|
|
assert [x[0] for x in live["edited"]] == ["T1", "T3"] # B1 이 T1 · T3 범위 안
|
|
assert live["edited"][0][2] == "20.00" # 식 칸 B2 = B1*2
|
|
|
|
|
|
def test_접은_동안은_건너뛰고_다시_펴면_따라잡음(live):
|
|
assert live["hidden"] == []
|
|
assert [x[0] for x in live["again"]] == ["T1", "T3"]
|
|
assert live["again"][0][2] == "40.00"
|
|
|
|
|
|
def test_보내기_취소한_연결은_뺌(live):
|
|
assert [x[0] for x in live["cancelled"]] == ["T3"]
|