Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_3

This commit is contained in:
2026-11-15 18:31:03 +09:00
101 changed files with 447 additions and 32 deletions
@@ -137,6 +137,9 @@ COLS = {
}
COLS["한국은행환율"] = COLS["유가전력"]
ALIAS = "별칭" # 요소 줄 덧칸(815-2) — 열 한 벌 밖 · 있으면 빈 글 아님
OFF = (
"비활성" # 기계 줄 맨 위 칸(842-2) — 있으면 true 만 · 없음 = 켜짐 · 찾기 · 고르기 · 잇기에서 뺌
)
def _cols_of(row: dict) -> tuple:
@@ -300,6 +303,8 @@ def check_form(name: str, data: dict) -> list[str]:
out += _check_labor(where, row)
if ALIAS in row and not (isinstance(row[ALIAS], str) and row[ALIAS].strip()):
out.append(f"{where} · 「{ALIAS}」 이 빈 글 · 글 아님(빈 칸이면 칸을 두지 않음)")
if OFF in row and (row[OFF] is not True or data.get("그룹") != "기계"):
out.append(f"{where} · 「{OFF}」 는 기계 줄에만 true(켜짐이면 칸을 두지 않음)")
if cols and _cols_of(row) != cols:
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(cols)}」")
if book == "자재품목" and row.get(mm.LOAD) not in mm.LOADS:
@@ -1,5 +1,6 @@
{
"키": "EQ000541",
"비활성": true,
"원문번호": "9010-0003",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000542",
"비활성": true,
"원문번호": "9010-0006",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000543",
"비활성": true,
"원문번호": "9010-0010",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000544",
"비활성": true,
"원문번호": "9010-0012",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000545",
"비활성": true,
"원문번호": "9010-0020",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000546",
"비활성": true,
"원문번호": "9010-0022",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000547",
"비활성": true,
"원문번호": "9010-0033",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000548",
"비활성": true,
"원문번호": "9010-0040",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000549",
"비활성": true,
"원문번호": "9010-0044",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000550",
"비활성": true,
"원문번호": "9010-0060",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000551",
"비활성": true,
"원문번호": "9010-0080",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000552",
"비활성": true,
"원문번호": "9010-0120",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000553",
"비활성": true,
"원문번호": "9010-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000554",
"비활성": true,
"원문번호": "9020-0010",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000555",
"비활성": true,
"원문번호": "9020-0015",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000556",
"비활성": true,
"원문번호": "9020-0016",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000557",
"비활성": true,
"원문번호": "9020-0022",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000558",
"비활성": true,
"원문번호": "9020-0035",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000559",
"비활성": true,
"원문번호": "9020-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000560",
"비활성": true,
"원문번호": "9020-0072",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000561",
"비활성": true,
"원문번호": "9020-0160",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000562",
"비활성": true,
"원문번호": "9020-0180",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000563",
"비활성": true,
"원문번호": "9020-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000564",
"비활성": true,
"원문번호": "9030-0016",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000565",
"비활성": true,
"원문번호": "9030-0018",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000566",
"비활성": true,
"원문번호": "9030-0025",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000567",
"비활성": true,
"원문번호": "9030-0035",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000568",
"비활성": true,
"원문번호": "9030-0045",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000569",
"비활성": true,
"원문번호": "9030-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000570",
"비활성": true,
"원문번호": "9030-0080",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000571",
"비활성": true,
"원문번호": "9030-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000572",
"비활성": true,
"원문번호": "9030-0240",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000573",
"비활성": true,
"원문번호": "9040-0010",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000574",
"비활성": true,
"원문번호": "9040-0030",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000575",
"비활성": true,
"원문번호": "9040-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000576",
"비활성": true,
"원문번호": "9040-0060",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000577",
"비활성": true,
"원문번호": "9040-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000578",
"비활성": true,
"원문번호": "9040-0120",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000579",
"비활성": true,
"원문번호": "9040-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000580",
"비활성": true,
"원문번호": "9040-0250",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000581",
"비활성": true,
"원문번호": "9040-0300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000582",
"비활성": true,
"원문번호": "9040-0380",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000583",
"비활성": true,
"원문번호": "9040-0680",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000584",
"비활성": true,
"원문번호": "9050-0075",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000585",
"비활성": true,
"원문번호": "9050-0150",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000586",
"비활성": true,
"원문번호": "9050-0450",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000587",
"비활성": true,
"원문번호": "9050-0750",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000588",
"비활성": true,
"원문번호": "9050-0850",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000589",
"비활성": true,
"원문번호": "9060-0060",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000590",
"비활성": true,
"원문번호": "9060-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000591",
"비활성": true,
"원문번호": "9060-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000592",
"비활성": true,
"원문번호": "9060-0300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000593",
"비활성": true,
"원문번호": "9060-0500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000594",
"비활성": true,
"원문번호": "9060-0600",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000595",
"비활성": true,
"원문번호": "9070-0015",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000596",
"비활성": true,
"원문번호": "9070-0020",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000597",
"비활성": true,
"원문번호": "9080-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000598",
"비활성": true,
"원문번호": "9080-0080",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000599",
"비활성": true,
"원문번호": "9080-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000600",
"비활성": true,
"원문번호": "9080-0120",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000601",
"비활성": true,
"원문번호": "9080-0150",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000602",
"비활성": true,
"원문번호": "9080-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000603",
"비활성": true,
"원문번호": "9080-0300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000604",
"비활성": true,
"원문번호": "9080-0500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000605",
"비활성": true,
"원문번호": "9080-0700",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000606",
"비활성": true,
"원문번호": "9080-1000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000607",
"비활성": true,
"원문번호": "9080-1100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000608",
"비활성": true,
"원문번호": "9080-1400",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000609",
"비활성": true,
"원문번호": "9080-1500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000610",
"비활성": true,
"원문번호": "9080-1750",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000611",
"비활성": true,
"원문번호": "9080-2000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000612",
"비활성": true,
"원문번호": "9080-3000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000613",
"비활성": true,
"원문번호": "9090-0800",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000614",
"비활성": true,
"원문번호": "9090-1000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000615",
"비활성": true,
"원문번호": "9090-1200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000616",
"비활성": true,
"원문번호": "9090-1300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000617",
"비활성": true,
"원문번호": "9090-1400",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000618",
"비활성": true,
"원문번호": "9090-1500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000619",
"비활성": true,
"원문번호": "9090-1600",
"원문": "건설품셈",
"구분": "[90]해상장비",
+25 -3
View File
@@ -35,8 +35,8 @@ FILES = {
#: 영월 = 내역 줄이 코드로만 가리킴(번 채움) · STmate 하위 호표 반올림 끝수 28 줄
#: (줄당 1 원/단위 · 브레인 정함 = 그래도 이음)
WANT = {
# 841-1 이은 안내판 줄 = M02 사본 셈(엑셀보다 50,000 적음 · 노임 · 기계 덮개 걷음)
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 50000),
# 842-1 이은 안내판 줄 표지판지주 = 별도 → 대가 안 0 · 자재대 50,000 (직접비 합 = 엑셀)
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 0),
"대흥": (34, 171, 143, 28, 0, 26, 45, 710359136, 0, 0),
"소광": (25, 117, 98, 19, 0, 0, 0, 173311386, 0, 0),
"영월": (23, 138, 135, 3, 0, 21, 34, 767148700, 28, 2951),
@@ -117,6 +117,28 @@ def test_실무_엑셀_통째_빠짐_0_합계_같음_층_쓰기_0(world, name):
assert not [p for p in world["storage"].rglob("*") if p.is_file()] # 층 쓰기 0
@pytest.mark.parametrize("name", ["거창", "대흥", "소광", "영월"])
def test_대가_안_0_원_재료_자재대에_한_번(world, name):
"""842-1 전수 — 별도 · 관급 이은 자재(재료 줄 · 고정 줄)마다 집계 `aside` 줄 · 4 자재대 · 관급
자재대 수량 = 그 모음(사라짐 0) · 재료 줄 아닌 같은 자재 이중 집계 0."""
from B09_Estimation.B09_Estimation_Summary import block_lines
got = world["run"](name)["payload"]
sheets, aside = got["sheets"], got["inputs"].get("aside_qty") or {}
apart = {r["key"] for r in sheets["compare"] if r["supply"] in ("별도", "관급")}
items = {r["ref"]["key"]: r["qty"] for r in sheets["bill"]["rows"]
if r.get("kind") == "item" and str(r["key"]).startswith("mat:")} # fmt: skip
for tab in ("sg", "daega_blocks"):
for block in sheets[tab]:
lines = block_lines([block])[str(block["key"])]["lines"]
for ln in block["lines"]:
key = (ln.get("ref") or {}).get("key") or (ln.get("link") or {}).get("material")
if key in apart and ln["kind"] in ("material", "fixed") and ln.get("qty"):
assert any(x["kind"] == "aside" and x["key"] == key for x in lines), ln["name"]
assert all(x["kind"] != "material" or x["key"] != key for x in lines)
assert items == {k: v for k, v in aside.items() if k in apart}
def test_대흥_총공사비_엑셀과_같음_하위는_참조사본(world):
got = world["run"]("대흥")
assert got["합계"]["우리총"] == got["합계"]["엑셀총"] == 1201879000
@@ -145,7 +167,7 @@ def test_다시_부르기_같은_사본_두_번_셈_0(world):
first = world["run"]("거창")
again = world["run"]("거창", fresh=False)
assert _libs(again["root"]) == _libs(first["root"])
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384866227
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384916227
rows = again["user"]["bill"]["rows"]
assert len({r["key"] for r in rows}) == len(rows)
@@ -35,8 +35,11 @@ def test_기계_이름_표_문서_목록_전수() -> None:
def test_옵션_부품_글만_바꿈() -> None:
src = (ROOT / "A00_Common/formula_doc_options.ts").read_text(encoding="utf-8")
assert "known[x] ? `${known[x]} (${x})` : x" in src
assert "list.map((x) => ({ value: x, text: optionText(x) }))" in src
assert "known[x] ? `${known[x]} (${x})${off.has(x)" in src # 842-2 끈 기계 꼬리
assert (
'liveOptions(list, String(now ?? "")).map((x) => ({ value: x, text: optionText(x) }))'
in src
)
assert "nameMachines(list, () => f.setOptions(items(), f.select.value));" in src
api = (ROOT / "M02_MasterTemplete/DataFlow/M02_DF_Api.ts").read_text(encoding="utf-8")
assert "machineNames ??= fetch(" in api # 한 번만 받음
@@ -0,0 +1,82 @@
"""842-1 B09 — 대가 안 0 원이 된 재료는 자재대(`aside`)에 꼭 한 번 · 겹쳐 세지 않음.
고정 줄이 이은 자재(`link.material`)가 별도 · 관급이면 대가 안 0 원 + 그 몫 = 줄 `aside` →
집계표 `aside` 줄(자재대 수량) · 재료 줄 · 붙인 줄 · 관급도 같은 길 · 직접계산 = `aside` 없음.
거울: `B09_Estimation_Calc_Labor.py` ↔ `.ts` (`fixedApart`).
"""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402
from B09_Estimation.B09_Estimation_Summary import block_lines, totals # noqa: E402
from resources.tester.test_145_2_b09_labor_calc import _b, _l, _same, _ts # noqa: E402
SHEETS = {
"labor": [],
"material": [
{"key": "MA", "price": 50000, "supply": "별도"},
{"key": "MB", "price": 800, "supply": "관급"},
{"key": "MC", "price": 300, "supply": None},
],
"machine": [],
"gaps": [],
}
def _fix(name, qty, mat, price, **link):
own = {"labor": 0.0, "material": price, "expense": 0.0}
return {
"kind": "fixed",
"name": name,
"qty": qty,
"own": own,
"link": {"material": mat, **link},
}
DAEGA = {
"blocks": [
_b(
"DG1",
"품셈",
"안내판",
[
_fix("지주", 1, "MA", 50000), # 별도 → 대가 안 0 · 자재대 50000
_fix("관급 줄", 2, "MB", 800), # 관급 → 자재대(관급)
_fix("직접 줄", 3, "MC", 300), # 직접계산 → 대가 안
_fix(
"기계 겸", 1, "MA", 50000, machine="X1"
), # 기계 이음이 먼저여도 자재대 몫은 감
{**_l("material", "MA", 4), "own": {"material": 50000}}, # 재료 줄도 같은 자재
],
),
],
"owners": {"x": "DG1"},
}
def test_고정_줄_자재_대가_안_0_자재대에_한_번():
got = compute_labor(DAEGA, SHEETS)["daega_blocks"][0]
by = {ln["name"]: ln for ln in got["lines"]}
assert by["지주"]["amounts"]["material"] == 0 and by["지주"]["aside"] == 50000
assert by["관급 줄"]["amounts"]["material"] == 0 and by["관급 줄"]["aside"] == 1600
assert by["직접 줄"]["amounts"]["material"] == 900 and "aside" not in by["직접 줄"]
assert got["material"] == 900 # 대가 안 = 직접계산 줄만
lines = block_lines([got])[got["key"]]["lines"]
assert [x for x in lines if x["kind"] == "material"] == [ # 직접 줄만 일반 재료 줄
{"kind": "material", "key": "MC", "qty": 3}
]
aside = totals({"bill": {"rows": [{"kind": "item", "qty": 1, "ref": {"key": got["key"]}}]}},
{got["key"]: {"lines": lines}})[0]["aside"] # fmt: skip
assert aside == {"MA": 1 + 1 + 4, "MB": 2} # 재료 줄 4 + 고정 줄 둘 = 한 번씩 모음
def test_거울_파이썬_TS():
py = json.loads(json.dumps(compute_labor(DAEGA, SHEETS), ensure_ascii=False))
assert _same(py, _ts(DAEGA, SHEETS, {})["out"])
+118
View File
@@ -0,0 +1,118 @@
"""842-2 기계 비활성 — 지킴 시험(쓰기는 임시 사본만).
① 틀 검사 — 기계 줄 맨 위 칸 `비활성` = true 만 · 다른 그룹 줄 · false 는 오류
② 저장 왕복 — M01 저장 길이 표지를 받고(그 대 파일 하나) 지우면 칸이 사라짐 · 셈 값 그대로
③ 끈 기계는 찾기 · 고르기 · 잇기에서 안 나옴 — 요소 찾기 창 · 비슷한 기계 · 불러오기 기계 잇기 ·
목록 거름 「켜짐」 · 끈 원문번호 목록 · 셈(시간당)은 그대로
"""
from __future__ import annotations
import copy
import shutil
import sys
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as store
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts"))
import master_machine_files as mmf # noqa: E402
from M01_MasterData import M01_MasterData_Machine as m01m # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mfind # noqa: E402
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
from M02_MasterTemplete.DataFlow import M02_DF_Router as dfr # noqa: E402
REAL = store.FOLDER
cm, mk, M = store.cm, store.mk, mmf.NAME
@pytest.fixture
def master(tmp_path: Path, monkeypatch) -> Path:
for path in REAL.glob("*.json"):
if not path.name.startswith("_") or path.name == mk.BOOK.name:
shutil.copy(path, tmp_path / path.name)
shutil.copytree(REAL / mmf.DIR, tmp_path / mmf.DIR)
monkeypatch.setattr(store, "FOLDER", tmp_path)
return tmp_path
def _rows() -> list[dict]:
return store.read(M)[0]["줄"]
def _part(*changes: dict) -> dict:
return {"file": M, "version": store.read(M)[1], "changes": list(changes)}
# ① 틀 검사 ──────────────────────────────────────────────────────────
def test_form_check() -> None:
data = copy.deepcopy(store.read(M)[0])
data["줄"] = data["줄"][:3]
assert cm.check_form(M, data) == []
data["줄"][0][cm.OFF] = True
assert cm.check_form(M, data) == []
data["줄"][0][cm.OFF] = False
assert any(cm.OFF in e for e in cm.check_form(M, data))
labor = copy.deepcopy(store.read("인력.json")[0])
labor["줄"] = labor["줄"][:1]
labor["줄"][0][cm.OFF] = True
assert any(cm.OFF in e for e in cm.check_form("인력.json", labor))
# ② 저장 왕복 ───────────────────────────────────────────────────────
def test_save_round_trip(master: Path) -> None:
key = next(r["키"] for r in _rows() if not r.get(cm.OFF) and r["이름"].startswith("불도저"))
row = copy.deepcopy(next(r for r in _rows() if r["키"] == key))
before = mh.hourly(key)
snap = {p.name: p.read_bytes() for p in (master / mmf.DIR).glob("*.json")}
store.save([_part({"op": "edit", "key": key, "row": {**row, cm.OFF: True}})])
after = {p.name: p.read_bytes() for p in (master / mmf.DIR).glob("*.json")}
assert {n for n in after if after[n] != snap[n]} == {f"{key}.json"}
got = next(r for r in _rows() if r["키"] == key)
assert got[cm.OFF] is True and got["값"] == row["값"]
assert mh.hourly(key) == before # 셈은 그대로(쓰던 단가산출 · 사본이 깨지지 않음)
assert key not in {x["ref"] for x in store.elements("기계", "불도저", 200)["items"]}
store.save([_part({"op": "edit", "key": key, "row": row})])
assert (master / mmf.DIR / f"{key}.json").read_bytes() == snap[f"{key}.json"]
# ③ 찾기 · 고르기 · 잇기에서 뺌 ──────────────────────────────────────
def _off() -> list[dict]:
got = [r for r in _rows() if r.get(cm.OFF)]
if not got:
pytest.skip("끈 기계 없음")
return got
def test_hidden_everywhere() -> None:
off = _off()
keys = {r["키"] for r in off}
for r in off[:: max(1, len(off) // 6)]:
name = str(r["상세구분"] or r["이름"]).replace(" ", "")
assert not keys & {x["ref"] for x in store.elements("기계", name, 200)["items"]}
assert not keys & {x["키"] for x in m01m.find(name, 200)}
with mfind.m01_only():
got = mfind.find(r["이름"], r.get("규격") or "")
assert got["키"] not in keys and not keys & {x["키"] for x in got["후보"]}
listed = store.rows(M, 1, 500, "", state="켜짐")
assert not keys & {r["키"] for r in listed["rows"]}
assert listed["total"] == len(_rows()) - len(keys)
assert store.rows(M, 1, 1, "")["total"] == len(_rows()) # 같이 보기 = 전부
codes = {str(r["원문번호"]) for r in off} - {
str(r["원문번호"]) for r in _rows() if not r.get(cm.OFF)
}
assert set(dfr.get_machine_off()) == codes
assert all(c in dfr.get_machine_names() for c in codes) # 고른 값 글은 그대로
def test_sea_machines_off() -> None:
"""842-2 ㉣ 해상 기계([90]해상장비 = 선원류 칸 있는 대)는 끔 — 사용자가 다시 켤 수 있으므로 켠 대는 알림만."""
sea = [r for r in _rows() if "해상" in str(r.get("구분"))]
assert len(sea) == 79
on = [r["키"] for r in sea if not r.get(cm.OFF)]
if on:
pytest.skip(f"다시 켠 해상 기계 {len(on)} 대 — 사용자 판단")