Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_8
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/* M02 일위대가 수량 · 반영 · 합(`M02_MasterTemplete_Labor_Calc.ts` `settle`) 거울 헬퍼 —
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림 · stdin = {답, 문서, 값} · stdout = settle 결과 JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const file = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"M02_MasterTemplete",
|
||||
"M02_MasterTemplete_Labor_Calc.ts",
|
||||
);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, require);
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
process.stdout.write(JSON.stringify(box.exports.settle(input.답, input.문서, input.값)));
|
||||
@@ -26,7 +26,7 @@ from resources.tester.test_m02_structure import write_sample_table
|
||||
from resources.tester.test_m02_tree import Tree, _app
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
NEW = re.compile(r"^S[DAM]-\d{6}$")
|
||||
NEW = re.compile(r"^S[DAMU]-\d{6}$")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -164,7 +164,7 @@ def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world):
|
||||
doc = json.loads(path.read_bytes())
|
||||
path.write_text(json.dumps({**doc, key: f"{head}{code}"}), encoding="utf-8")
|
||||
old = {cid: _numbers(system, cid) for cid in store._column_ids()}
|
||||
assert old["rv"] == {"SD01010100", "SA01010100", "SM01010100"}
|
||||
assert old["rv"] == {"SD01010100", "SU01010100", "SA01010100", "SM01010100"}
|
||||
first = max(int(n[3:]) for nums in old.values() for n in nums if NEW.match(n)) + 1
|
||||
with store.at(system):
|
||||
got = store.read("table", store.STRUCTURE_TABLE)
|
||||
@@ -191,7 +191,7 @@ def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world):
|
||||
got["문서"]["열"].append({"id": "y", "머리": ["y"], "꼴": "수"})
|
||||
store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"])
|
||||
last = f"-{first + 120:06d}" # 지운 x119 번호(first + 119) 다음
|
||||
assert _numbers(system, "y") == {f"SD{last}", f"SA{last}", f"SM{last}"}
|
||||
assert _numbers(system, "y") == {f"SD{last}", f"SU{last}", f"SA{last}", f"SM{last}"}
|
||||
|
||||
|
||||
def test_층마다_번호는_따로_파일은_열_id_로(world):
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""일위대가 수량 · 반영 · 합 거울 시험 — `M02_Labor_Doc.settle`(파이썬) ↔
|
||||
`M02_MasterTemplete_Labor_Calc.ts` `settle`(TS · Node) 가 같은 답(CLAUDE.md 5장 ①).
|
||||
|
||||
기준은 서버가 한 번 셈(`calc`) · 그 답에 다른 알약 값을 넣어 둘이 같은지 봄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
TEN, THOUSAND, PLAIN, MAT = "GC000421", "GF000017", "GC000422", "MT000008"
|
||||
|
||||
|
||||
def _ts(answer: dict, doc: dict, values: dict) -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_m02_labor_calc.cjs")],
|
||||
input=json.dumps({"답": answer, "문서": doc, "값": values}, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _same(a, b) -> None:
|
||||
if isinstance(a, dict):
|
||||
assert set(a) == set(b)
|
||||
for k in a:
|
||||
_same(a[k], b[k])
|
||||
elif isinstance(a, list):
|
||||
assert len(a) == len(b)
|
||||
for x, y in zip(a, b):
|
||||
_same(x, y)
|
||||
elif isinstance(a, (int, float)) and not isinstance(a, bool):
|
||||
assert b == pytest.approx(a, rel=1e-9)
|
||||
else:
|
||||
assert a == b
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"values",
|
||||
[
|
||||
{}, # 수량 없음 — 반영 null · 손만
|
||||
{"사석 수상고르기|㎡": 2.5, f"자재|{MAT}": "3"},
|
||||
{"사석 수상고르기|㎡": "1,200", "나무식재|본": 7, f"자재|{MAT}": "글"}, # 쉼표 · 수 아님
|
||||
],
|
||||
)
|
||||
def test_파이썬과_TS_가_같은_답(values):
|
||||
doc = labor.clean(
|
||||
{
|
||||
"열": "rv",
|
||||
"담은로직": [
|
||||
{"로직": TEN, "수량": None},
|
||||
{"로직": THOUSAND, "수량": {"손": 4}},
|
||||
{"로직": PLAIN, "수량": None},
|
||||
],
|
||||
"자재": [{"코드": MAT, "수량": None}],
|
||||
}
|
||||
)
|
||||
answer = labor.calc(doc, {})
|
||||
keys = {line["알약"] for line in answer["줄"]}
|
||||
assert {"사석 수상고르기|㎡", "나무식재|본"} <= keys # 알약 키가 시험 값과 맞음
|
||||
py = labor.settle(answer, doc, values)
|
||||
_same(py, _ts(answer, doc, values))
|
||||
assert py == labor.calc(doc, values) # settle 만 다시 = 처음부터 다시
|
||||
@@ -0,0 +1,213 @@
|
||||
"""일위대가 문서(`labor/<열id>.json` · PLAN 39-1a) — 정리 · 검사 · 계산 · 알약 · 저장 때 계산값.
|
||||
|
||||
로직 · 자재는 M01 마스터(읽기만) · 층은 견본 표(`write_sample_table`)를 임시 폴더에.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from resources.tester.test_m02_structure import write_sample_table
|
||||
|
||||
TEN = "GC000421" # 원/10㎡ — 배 10
|
||||
THOUSAND = "GF000017" # 원/1,000본 — 배 1000
|
||||
PLAIN = "GC000422" # ㎡/시간 — 돈 아닌 로직
|
||||
STONE = "GF000219" # 원/㎡ · 입력 뒷길이 · 돌 · 쌓기 · 높이 · 초과증가율
|
||||
MAT = "MT000008" # 자재품목 · 물가정보 83000
|
||||
|
||||
|
||||
def _doc(**extra):
|
||||
return {"열": "rv", "담은로직": [], "자재": [], **extra}
|
||||
|
||||
|
||||
def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것():
|
||||
got = labor.clean(
|
||||
_doc(
|
||||
담은로직=[
|
||||
{"로직": STONE, "입력값": {"돌": "깬돌", "모름": 1, "높이": ""}, "수량": None}
|
||||
],
|
||||
자재=[{"코드": MAT, "이름": "아무 이름", "수량": {"손": "2"}}],
|
||||
단위="개소",
|
||||
)
|
||||
)
|
||||
assert got["담은로직"] == [
|
||||
{"로직": STONE, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None}
|
||||
]
|
||||
assert got["자재"][0]["이름"] == "치 즐(0.2㎥)" and got["자재"][0]["수량"] == {"손": 2}
|
||||
assert (got["종류"], got["단위"], got["본뜬"]) == ("일위대가", "개소", None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad",
|
||||
[
|
||||
_doc(담은로직=[{"로직": STONE}, {"로직": STONE}]), # 같은 로직 두 번
|
||||
_doc(담은로직=[{"로직": "GF999999"}]), # 없는 로직
|
||||
_doc(담은로직=[{"로직": STONE, "수량": {"손": "열"}}]), # 수 아님
|
||||
_doc(담은로직=[{"로직": STONE, "수량": 3}]), # 꼴 틀림
|
||||
_doc(자재=[{"코드": "LB000001"}]), # 자재품목 아님
|
||||
_doc(자재=[{"코드": MAT}, {"코드": MAT}]), # 같은 자재 두 번
|
||||
"문서 아님",
|
||||
],
|
||||
)
|
||||
def test_틀린_문서는_400(bad):
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
labor.clean(bad)
|
||||
assert e.value.status == 400
|
||||
|
||||
|
||||
def test_빈_일위대가도_됨():
|
||||
got = labor.server(_doc())
|
||||
assert got["계산값"]["기준합"]["계"] == 0 and got["계산값"]["반영합"]["계"] == 0
|
||||
|
||||
|
||||
def test_PER_천_단위_쉼표는_배():
|
||||
pills = combo_store.combo_pills({"담은로직": [{"로직": THOUSAND}, {"로직": TEN}]})["물량"]
|
||||
assert {p["단위"]: p.get("배") for p in pills} == {"본": 1000, "㎡": 10}
|
||||
|
||||
|
||||
def test_반영은_기준_곱_수량_나누기_배_돈_아닌_로직은_반영_없음():
|
||||
doc = labor.clean(
|
||||
_doc(담은로직=[{"로직": TEN, "수량": {"손": 4}}, {"로직": PLAIN, "수량": None}])
|
||||
)
|
||||
got = labor.calc(doc, {})
|
||||
ten, plain = got["줄"]
|
||||
assert (ten["배"], ten["수량"], ten["수량출처"]) == (10, 4, "손")
|
||||
for k in ("노무비", "재료비", "경비", "계"):
|
||||
assert ten["반영"][k] == pytest.approx(ten["기준"][k] * 4 / 10)
|
||||
assert plain["기준"] is None and plain["반영"] is None and "결과" in plain
|
||||
assert got["반영합"]["계"] == pytest.approx(ten["반영"]["계"])
|
||||
|
||||
|
||||
def test_수량_손이_칸보다_앞섬_칸_값은_알약_키로():
|
||||
doc = labor.clean(
|
||||
_doc(
|
||||
담은로직=[{"로직": TEN, "수량": None}, {"로직": THOUSAND, "수량": {"손": 1}}],
|
||||
자재=[{"코드": MAT, "수량": None}],
|
||||
)
|
||||
)
|
||||
keys = {p["키"] for p in labor.pills(doc)}
|
||||
ten_key = next(k for k in keys if k.endswith("|㎡"))
|
||||
thousand_key = next(k for k in keys if k.endswith("|본"))
|
||||
assert f"자재|{MAT}" in keys
|
||||
got = labor.calc(doc, {ten_key: "2.5", thousand_key: 99, f"자재|{MAT}": "3"})
|
||||
ten, thousand = got["줄"]
|
||||
assert (ten["수량"], ten["수량출처"]) == (2.5, "칸")
|
||||
assert (thousand["수량"], thousand["수량출처"]) == (1, "손")
|
||||
mat = got["자재"][0]
|
||||
assert (mat["기준단가"], mat["수량"], mat["반영"]) == (83000, 3, 249000)
|
||||
nothing = labor.calc(doc, {})["줄"][0]
|
||||
assert (nothing["수량출처"], nothing["반영"]) == ("없음", None)
|
||||
|
||||
|
||||
def test_면적_값_식_칸은_계산값_그_밖은_칸_값():
|
||||
area = {
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"칸": {"B6": {"값": "1,200"}, "C1": {"값": "글"}},
|
||||
"연결": [{"키": "a", "칸": "B6"}, {"키": "b", "칸": "D2"}, {"키": "c", "칸": "C1"}],
|
||||
}
|
||||
],
|
||||
"계산값": {"s1": {"D2": {"수": "7.5"}}},
|
||||
}
|
||||
got = labor.area_values(area)
|
||||
assert {k: float(v) for k, v in got.items()} == {"a": 1200, "b": 7.5}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def system(tmp_path, monkeypatch):
|
||||
write_sample_table(tmp_path)
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
store.create_structure("rv")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_구조물_만들면_빈_일위대가_같은_뒷자리(system):
|
||||
doc = store.read("labor", "rv")["문서"]
|
||||
assert doc["일위대가번"] == "SU" + store.read("structure", "rv")["문서"]["도번"][2:]
|
||||
assert doc["담은로직"] == [] and doc["자재"] == []
|
||||
|
||||
|
||||
def test_저장_때_서버가_계산값_번호는_그대로_면적_저장이_계산값_새로(system):
|
||||
have = store.read("labor", "rv")
|
||||
sent = {
|
||||
**have["문서"],
|
||||
"일위대가번": "SU-999999",
|
||||
"담은로직": [{"로직": TEN, "수량": None}],
|
||||
"계산값": {"브라우저": "값"},
|
||||
}
|
||||
store.write("labor", "rv", have["판"], sent)
|
||||
doc = store.read("labor", "rv")["문서"]
|
||||
assert doc["일위대가번"] == have["문서"]["일위대가번"]
|
||||
assert doc["계산값"]["줄"][0]["수량출처"] == "없음"
|
||||
pill = labor.pills(doc)[0]["키"]
|
||||
area = store.read("area", "rv")
|
||||
sheet = area["문서"]["시트"][0]
|
||||
sheet["칸"] = {"B6": {"값": "5"}}
|
||||
sheet["연결"] = [{"키": pill, "이름": "x", "칸": "B6", "단위": "㎡"}]
|
||||
store.write("area", "rv", area["판"], area["문서"])
|
||||
line = store.read("labor", "rv")["문서"]["계산값"]["줄"][0]
|
||||
assert (line["수량"], line["수량출처"]) == (5, "칸")
|
||||
assert line["반영"]["계"] == pytest.approx(line["기준"]["계"] * 5 / 10)
|
||||
|
||||
|
||||
def test_틀린_일위대가_저장은_400_파일_그대로(system):
|
||||
have = store.read("labor", "rv")
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [{"로직": "GF999999"}]})
|
||||
assert e.value.status == 400
|
||||
assert json.loads((system / "labor" / "rv.json").read_bytes())["담은로직"] == []
|
||||
|
||||
|
||||
# ── 길(`M02_Labor_Router`) ─────────────────────────────────────────────
|
||||
@pytest.fixture()
|
||||
def client(tmp_path, monkeypatch):
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from config import config_system
|
||||
from M02_MasterTemplete import M02_Labor_Router as labor_router
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
system = tmp_path / "system"
|
||||
system.mkdir()
|
||||
write_sample_table(system)
|
||||
monkeypatch.setattr(store, "FOLDER", system)
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||||
(tmp_path / "storage").mkdir()
|
||||
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
|
||||
with store.at(system):
|
||||
store.create_structure("rv")
|
||||
app = FastAPI()
|
||||
app.include_router(labor_router.router)
|
||||
app.dependency_overrides[verify_session] = lambda: {
|
||||
"user_id": 42,
|
||||
"company_id": 7,
|
||||
"role": "USER",
|
||||
"is_master": False,
|
||||
}
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_길_calc_는_저장_안_한_문서와_알약_값으로_셈_알약도_같이(client):
|
||||
url = "/api/m02/layers/system/labor/rv/calc"
|
||||
sent = {"담은로직": [{"로직": TEN, "수량": None}], "자재": [{"코드": MAT, "수량": {"손": 1}}]}
|
||||
got = client.post(url, json={"doc": sent, "수량": {"사석 수상고르기|㎡": 20}}).json()
|
||||
assert (got["줄"][0]["수량출처"], got["자재"][0]["반영"]) == ("칸", 83000)
|
||||
assert {p["키"] for p in got["알약"]} == {"사석 수상고르기|㎡", f"자재|{MAT}"}
|
||||
saved = client.post(url, json={}).json() # 저장본 = 빈 일위대가
|
||||
assert saved["줄"] == [] and saved["알약"] == []
|
||||
assert client.get("/api/m02/layers/system/labor/rv/pills").json() == {"열": "rv", "알약": []}
|
||||
|
||||
|
||||
def test_길_틀린_문서_400_없는_열_404_남의_개인_층_403(client):
|
||||
url = "/api/m02/layers/{}/labor/{}/calc"
|
||||
bad = {"doc": {"담은로직": [{"로직": "GF999999"}]}}
|
||||
assert client.post(url.format("system", "rv"), json=bad).status_code == 400
|
||||
assert client.post(url.format("system", "없는열"), json={}).status_code == 404
|
||||
other = client.post(url.format("personal", "rv") + "?owner=7/99", json={})
|
||||
assert other.status_code == 403
|
||||
@@ -64,8 +64,13 @@ def client(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def _num(code: str) -> dict:
|
||||
"""세 번호 — 같은 8자리."""
|
||||
return {"도번": f"SD{code}", "면적번": f"SA{code}", "재료번": f"SM{code}"}
|
||||
"""열 번호 — 같은 뒷자리(일위대가번 SU · 39장)."""
|
||||
return {
|
||||
"도번": f"SD{code}",
|
||||
"일위대가번": f"SU{code}",
|
||||
"면적번": f"SA{code}",
|
||||
"재료번": f"SM{code}",
|
||||
}
|
||||
|
||||
|
||||
def test_새로_도번_자동_409_없는_열(client):
|
||||
|
||||
@@ -86,7 +86,9 @@ def test_집계표_저장_때_새_열_지운_열_고친_열을_맞춤_키_유지
|
||||
assert (rv["바인딩"], rv["펼침"]) == ({"종류": "revetment"}, True)
|
||||
# 번호는 세 파일과 같음 — 새 열도 [저장] 한 번에 SD · SA · SM 이 섬
|
||||
numbers = client.get("/api/m02/structures/numbers").json()
|
||||
assert {k: rows["new1"][k] for k in ("도번", "면적번", "재료번")} == numbers["new1"]
|
||||
assert {k: rows["new1"][k] for k in ("도번", "일위대가번", "면적번", "재료번")} == numbers[
|
||||
"new1"
|
||||
]
|
||||
assert rows["new1"]["도번"].startswith("SD")
|
||||
|
||||
# 또 저장해도(바뀐 것 없음) 키 · 다음 그대로
|
||||
|
||||
Reference in New Issue
Block a user