- M02_DF_Standards.ts — renderStandards(host, on) · standardsList()(단위 고르기 · 끝수 제안 · 동기) · loadStandards · 서버 GET/PUT/DELETE /api/m02/df/standards - 시험 test_m02_df_standards.py 에 standardsList 목록(단위 반올림 · 금액 지위 → 자리 · 버림) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
146 lines
6.3 KiB
Python
146 lines
6.3 KiB
Python
"""M02 단가산출 흐름도 — 단위 · 금액 표준 표(35-9 · 정함 11) 시험.
|
|
|
|
· `_standard/단위표준.json` — 산림품셈 1-2-2 「설계서의 단위 및 소수의 표준」 표 줄 수 · 병합 칸 폄 · 「-」 바꾸기 · 주 ①~③
|
|
· `_standard/금액단위표준.json` — 「금액의 단위표준」 다섯 줄 · 미만버림
|
|
· 원문 줄 번호가 원문의 그 줄을 가리킴 · 원문 흠은 고치지 않음(사용자가 시험판에서 고침)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
STD = ROOT / "M02_MasterTemplete" / "DataFlow" / "_standard"
|
|
SRC = (
|
|
ROOT
|
|
/ "resources/knowledge/original/원가계산/산림_표준품셈/본문/제01장_적용기준/1-02_설계_및_수량.md"
|
|
)
|
|
UNIT = json.loads((STD / "단위표준.json").read_text(encoding="utf-8"))
|
|
MONEY = json.loads((STD / "금액단위표준.json").read_text(encoding="utf-8"))
|
|
LINES = SRC.read_text(encoding="utf-8").split("\n")
|
|
|
|
|
|
def _cells(no: int) -> list[str]:
|
|
return [c.strip() for c in LINES[no - 1].strip().strip("|").split("|")]
|
|
|
|
|
|
def test_단위표준_줄_수_병합_원문줄() -> None:
|
|
rows = UNIT["줄"]
|
|
# 원문 표 = 머리 · 가름 · 몸 1행(단위 · 소수자리) 다음 73 줄 — 병합 칸 10 줄(주석의 14~15 · 17~20 · 32~33 · 35~36 · 45~47 · 61~63)
|
|
assert len(rows) == 73
|
|
assert sum(1 for r in rows if r.get("병합")) == 10
|
|
spread = {r["종목"] for r in rows if r.get("병합")}
|
|
assert spread == {
|
|
"견치돌, 깬돌",
|
|
"돌쌓기 및 돌붙임",
|
|
"목재(판재)",
|
|
"말뚝",
|
|
"돌망태",
|
|
"관류(管類)",
|
|
}
|
|
for r in rows:
|
|
cells = _cells(r["원문 줄"])
|
|
assert cells[0] == ("" if r.get("병합") else r["종목"])
|
|
assert [r["종목"] for r in rows[:3]] == ["공사연장", "공사폭원", "직공인부"]
|
|
assert rows[-1]["종목"] == "마대"
|
|
|
|
|
|
def test_단위표준_바꾸기() -> None:
|
|
"""소수자리 「-」 = 0 · 단위 「-」 = 「없음」 · 빈 칸 = null · 원문 글자는 그대로"""
|
|
for r in UNIT["줄"]:
|
|
src = _cells(r["원문 줄"])
|
|
for i, key in [(2, "규격 소수자리"), (4, "단위수량 소수자리")]:
|
|
want = 0 if src[i] == "-" else (int(src[i]) if src[i] else None)
|
|
assert r[key] == want, (r["원문 줄"], key)
|
|
for i, key in [(1, "규격 단위"), (3, "단위수량 단위")]:
|
|
want = "없음" if src[i] == "-" else (src[i] or None)
|
|
assert r[key] == want, (r["원문 줄"], key)
|
|
공사연장 = UNIT["줄"][0]
|
|
assert (공사연장["규격 단위"], 공사연장["규격 소수자리"], 공사연장["단위수량 소수자리"]) == (
|
|
"m",
|
|
2,
|
|
0,
|
|
)
|
|
말뚝 = [r for r in UNIT["줄"] if r["종목"] == "말뚝"]
|
|
assert 말뚝[1]["단위수량 단위"] is None # 원문 빈 칸 그대로
|
|
assert [n[:1] for n in UNIT["주"]] == ["①", "②", "③"]
|
|
|
|
|
|
def test_금액_다섯_줄() -> None:
|
|
rows = MONEY["줄"]
|
|
assert [(r["종목"], r["지위"]) for r in rows] == [
|
|
("설계서의 총액", 1000),
|
|
("설계서의 소계", 1),
|
|
("설계서의 금액란", 1),
|
|
("일위대가표의 계금", 1),
|
|
("일위대가표의 금액란", 0.1),
|
|
]
|
|
assert all(r["단위"] == "원" and r["비고"] == "미만버림" for r in rows)
|
|
assert all(_cells(r["원문 줄"])[0] == r["종목"] for r in rows)
|
|
|
|
|
|
# ── standardsList(단위 고르기 · 끝수 제안이 씀) — Node 로 `M02_DF_Standards.ts` 를 바로 돌림 ──────────
|
|
HOOKS = r"""
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { stripTypeScriptTypes } from "node:module";
|
|
export async function load(url, ctx, next) {
|
|
if (!url.endsWith(".ts")) return next(url, ctx);
|
|
let src = readFileSync(fileURLToPath(url), "utf8");
|
|
src = src.replace(/import\s+"[^"]+\.css";/g, "");
|
|
src = src.replace(/import\s+(\w+)\s+from\s+"\.\/_standard\/([^"]+)\.json";/g, "const $1 = globalThis.__std[\"$2\"];");
|
|
src = src.replace(/import\s*\{([^}]*)\}\s*from\s*"([^"]+)";/g, (m, names) => {
|
|
const list = names.split(",").map((x) => x.trim()).filter((x) => x && !x.startsWith("type "));
|
|
return list.length ? `const { ${list.join(", ")} } = globalThis.__stub;` : "";
|
|
});
|
|
return { format: "module", source: stripTypeScriptTypes(src, { mode: "transform" }), shortCircuit: true };
|
|
}
|
|
"""
|
|
|
|
|
|
def _node(body: str) -> object:
|
|
import shutil
|
|
import subprocess
|
|
|
|
assert shutil.which("node")
|
|
script = (
|
|
"import { register } from 'node:module';\n"
|
|
f"register('data:text/javascript,' + encodeURIComponent({json.dumps(HOOKS)}));\n"
|
|
"globalThis.__stub = new Proxy({}, { get: (_, k) => k === 'currentLanguageIndex' ? 0 : k === 'API_BASE_URL' ? '/api' : () => undefined });\n" # noqa: E501
|
|
f"globalThis.__std = {json.dumps({'단위표준': UNIT, '금액단위표준': MONEY}, ensure_ascii=False)};\n"
|
|
f"const S = await import({json.dumps((STD.parent / 'M02_DF_Standards.ts').as_uri())});\n"
|
|
"const out = (x) => process.stdout.write(JSON.stringify(x));\n" + body
|
|
)
|
|
result = subprocess.run(
|
|
["node", "--no-warnings", "--input-type=module", "-e", script],
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60, cwd=ROOT,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr[-2000:]
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def test_standardsList_목록() -> None:
|
|
got = _node("out(S.standardsList());")
|
|
units = [
|
|
(r["종목"], g, u, p)
|
|
for r in UNIT["줄"]
|
|
for g, u, p in [
|
|
("규격", r["규격 단위"], r["규격 소수자리"]),
|
|
("단위수량", r["단위수량 단위"], r["단위수량 소수자리"]),
|
|
]
|
|
if u and u != "없음"
|
|
]
|
|
assert [
|
|
(x["종목"], x["갈래"], x["단위"], x["소수자리"]) for x in got if x["갈래"] != "금액"
|
|
] == [(n, g, u, p or 0) for n, g, u, p in units]
|
|
assert all(x["방법"] == "반올림" for x in got if x["갈래"] != "금액")
|
|
money = [x for x in got if x["갈래"] == "금액"]
|
|
assert [(x["지위"], x["소수자리"], x["방법"]) for x in money] == [
|
|
(1000, -3, "버림"),
|
|
(1, 0, "버림"),
|
|
(1, 0, "버림"),
|
|
(1, 0, "버림"),
|
|
(0.1, 1, "버림"),
|
|
]
|