Merge remote-tracking branch 'origin/main_laptop_1' into sub_desktop_1
랩탑 메인이 건설 공종 축·목차 오기 고침·파일 쪼개기를 냈음. 충돌 셋 해소: - **열쇠 벌**(`_Keys.py`) — 랩탑의 갈래별 매개(`key_prefix`·`general_roots`)를 취하고, 내 잣대 읽기(`axis_policy.json`)를 되살려 **기본값이 자료에서 옴** ⇒ 랩탑이 코드로 되돌려 둔 `GENERAL_PROVISION_ROOTS` 는 잣대를 못 읽는 자리용 기본값으로만 남김 - **산림 마스터·지문** — 랩탑 것(코덱스 원문 대조로 표 넷 제자리 · 잎 358 → 360) - **이름표** — 랩탑이 건설 kind 를 정식으로 냈으므로 내 「기다리는 자리」(`pending_merged_tables`) 버림 · 공용 `columns` 여덟과 값 낱말 여섯은 살림 · `toc_duplicate_rows` 이름표는 뺌 (오기 고침으로 빈 목록이 되어 「묵은 이름표」가 됨 — 다시 생기면 그때 붙일 것) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsGw1Dz9HmhuAxGisxmDPF
This commit is contained in:
@@ -470,7 +470,8 @@ MERGED_KEYS = (
|
||||
"stone_kind",
|
||||
"masonry_class",
|
||||
"machine_productivity",
|
||||
"work_item_forest", # 2026-09-17 공종 축 산림(건설은 자료가 오면)
|
||||
"work_item_forest", # 2026-09-17 공종 축 산림
|
||||
"work_item_const", # 2026-09-17 공종 축 건설
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ MASTER = OUT_DIR / "work_item_master_2026-01-01.json"
|
||||
|
||||
#: 열쇠를 얹기 **전** 벌(`work_items`)의 지문. 2026-09-17 데스크탑_서브가 뜬 값.
|
||||
#: ⚠ 이 값이 흔들리면 열쇠 작업이 값을 건드렸다는 뜻이다 — 지문을 고치지 말고 원인을 찾을 것.
|
||||
BASELINE_WORK_ITEMS_SHA = "f8b8fba1ab5e6d1d5611b99148eecbed1eaf049c42d7154820f0d9a45e20ffea"
|
||||
#: 2026-09-17 랩탑 메인 — 목차 오기 둘을 본문 번호로 바로잡아(브레인 지시) **네 줄만** 바뀌어 새로 뜸
|
||||
#: (옛 f8b8fba1…). 네 줄 모습은 `test_목차_오기는_본문_번호로_바로잡고_열쇠는_그대로` 가 잼 ·
|
||||
#: 미판정·밑수 목록 파일은 한 글자도 안 바뀜.
|
||||
BASELINE_WORK_ITEMS_SHA = "be88f730b325c290012e5a2a79ad593ce5a74ffd82464226857191fefe6647e3"
|
||||
|
||||
#: 열쇠 작업이 새로 얹은 칸. 지문을 잴 때만 떼어낸다.
|
||||
ADDED_FIELDS = (
|
||||
@@ -130,16 +133,29 @@ def test_열쇠_차례는_목차_차례() -> None:
|
||||
assert nodes[0]["work_item_key"] == format_key(2)
|
||||
|
||||
|
||||
def test_목차_번호가_겹친_줄도_열쇠는_따로(master: dict) -> None:
|
||||
"""⚠ 품셈 목차가 12-17-2·12-24-1 을 두 번 적었음 — 번호를 열쇠로 못 쓰는 실물."""
|
||||
dups = master["toc_duplicate_rows"]
|
||||
assert [d["toc_number"] for d in dups] == ["12-17-2", "12-24-1"]
|
||||
assert [d["slot"] for d in dups] == ["FP-12-17-02#2", "FP-12-24-01#2"]
|
||||
for number in ("12-17-2", "12-24-1"):
|
||||
rows = [it for it in master["work_items"] if it["number"] == number]
|
||||
assert len(rows) == 2
|
||||
assert len({r["work_item_key"] for r in rows}) == 2
|
||||
assert len({r["path_name"] for r in rows}) == 2
|
||||
def test_목차_오기는_본문_번호로_바로잡고_열쇠는_그대로(master: dict) -> None:
|
||||
"""⚠ 품셈 목차가 12-17-2·12-24-1 을 두 번 적었음 — 본문은 12-17-3 무근진동기 제외 · 12-27-1 지수판 설치
|
||||
(2026-09-17 코덱스 원문 대조). 목차를 믿으면 뒤 줄이 앞 표를 덮어써 혼성 줄이 서고 F0364·F0375 가 사라짐.
|
||||
열쇠는 같은 공종이라 그대로(장부에 손으로 이음 · `relinked`)."""
|
||||
assert master["toc_duplicate_rows"] == []
|
||||
assert [(c["toc_number"], c["body_number"]) for c in master["toc_corrections"]] == [
|
||||
("12-17-2", "12-17-3"),
|
||||
("12-24-1", "12-27-1"),
|
||||
]
|
||||
by_key = {it["work_item_key"]: it for it in master["work_items"]}
|
||||
for key, number, name, tables in (
|
||||
("FW-00387", "12-17-2", "철근, 펌프카 0-15m", ["F0363"]),
|
||||
("FW-00388", "12-17-3", "무근진동기 제외", ["F0364"]),
|
||||
("FW-00396", "12-24-1", "뒷채움", ["F0371"]),
|
||||
("FW-00401", "12-27-1", "지수판 설치", ["F0375"]),
|
||||
):
|
||||
it = by_key[key]
|
||||
assert (it["number"], it["name"], [t["pum_table_id"] for t in it["tables"]]) == (
|
||||
number,
|
||||
name,
|
||||
tables,
|
||||
)
|
||||
assert not {o["pum_table_id"] for o in master["orphan_tables"]} & {"F0364", "F0375"}
|
||||
|
||||
|
||||
def test_슬롯_이름() -> None:
|
||||
@@ -205,18 +221,36 @@ def test_총칙_범위는_코드가_아니라_자료가_정함() -> None:
|
||||
const = load_policy("const")
|
||||
assert const["key_prefix"] == "CW"
|
||||
assert const["settled"] is False
|
||||
assert const["general_provision_roots"] == ["01_공통부문/제1장_적용기준.md"]
|
||||
assert [p["chapter"] for p in const["general_provision_pending"]] == [
|
||||
"01_공통부문/제8장_건설기계.md",
|
||||
"05_유지관리부문/제1장_공통.md",
|
||||
]
|
||||
# ⚠ 건설 뿌리는 **부문 자리까지** — 부문마다 장 번호가 1부터 다시 시작한다.
|
||||
assert const["general_provision_roots"] == ["CP-01-01"]
|
||||
assert [p["root"] for p in const["general_provision_pending"]] == ["CP-01-08", "CP-05-01"]
|
||||
# 뿌리를 바꿔 주면 구실도 따라 바뀐다 — 잣대가 자료에 있다는 증거
|
||||
assert (
|
||||
axis_role({"work_item_code": "FP-09-03", "tables": []}, has_children=True, roots=("FP-09",))
|
||||
axis_role({"work_item_code": "FP-09-03", "tables": []}, has_children=True, general_roots=("FP-09",))
|
||||
== "general_provision"
|
||||
)
|
||||
|
||||
|
||||
def test_건설_총칙_뿌리가_실제로_붙음() -> None:
|
||||
"""잣대가 건설 산출에 닿았는가 — 뿌리 아래 37줄이 `general_provision` 으로 섰다."""
|
||||
doc = json.loads(
|
||||
(OUT_DIR / "const_work_item_master_2026-01-01.json").read_text(encoding="utf-8")
|
||||
)
|
||||
roles = doc["stats"]["axis_roles"]
|
||||
assert roles["general_provision"] == 37
|
||||
assert sum(roles.values()) == len(doc["work_items"]) == 1367
|
||||
under = [
|
||||
it
|
||||
for it in doc["work_items"]
|
||||
if it["work_item_code"] == "CP-01-01" or it["work_item_code"].startswith("CP-01-01-")
|
||||
]
|
||||
assert len(under) == 37
|
||||
assert {it["axis_role"] for it in under} == {"general_provision"}
|
||||
# ⬜ 미확정 둘은 아직 공종·묶음으로 서 있다 — 안티그래비티 답이 오면 뿌리에 더한다
|
||||
기계 = next(it for it in doc["work_items"] if it["work_item_code"] == "CP-01-08")
|
||||
assert 기계["axis_role"] != "general_provision"
|
||||
|
||||
|
||||
def test_어느_구실도_지우지_않음() -> None:
|
||||
"""브레인 승인(2026-09-17) — 「덜어내기」가 아니라 「가름만」."""
|
||||
import json as _json
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""건설 공종 축(`CW-00001`) 뽑기 — 2026-09-17 브레인 8-2.
|
||||
|
||||
못박는 것
|
||||
① 목차 글줄 가르기 — 쪽 번호가 다음 번호에 붙은 꼴(「131-2-6」 = 13쪽 + 1-2-6) · 부문마다 장 번호 1부터
|
||||
② 표는 **본문 절 제목**으로 붙음 — 인용(「3-2-4 터파기(기계)’를 참고하여」)은 제목이 아님
|
||||
③ 목차 오기 없음 — 연혁 표시(「('20년 보완)」)가 붙은 본문 제목이 모두 목차 번호·이름과 같음
|
||||
(산림에서 목차가 번호를 잘못 적어 표가 딴 줄에 붙은 병 · 건설도 같은 함정)
|
||||
④ 못 붙인 표는 목록으로 · 열쇠 CW- 안 겹침 · 다시 뽑아도 같은 열쇠
|
||||
⑤ 산출 파일 = 지금 코드로 뽑은 것(산림도 같이 — 두 뽑기가 표 읽기 한 벌을 나눠 씀 · 금액 불변)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from B08_Quantity import B08_Quantity_Build_WorkItemMaster as forest
|
||||
from B08_Quantity import B08_Quantity_Build_WorkItemMaster_Const as const
|
||||
|
||||
OUT = forest.OUT_DIR
|
||||
CONST_MASTER = OUT / "const_work_item_master_2026-01-01.json"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def built() -> tuple:
|
||||
return const.build_const()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master() -> dict:
|
||||
return json.loads(CONST_MASTER.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _same_except_time(fresh: dict, saved: dict) -> None:
|
||||
fresh = json.loads(json.dumps(fresh, ensure_ascii=False))
|
||||
for doc in (fresh, saved):
|
||||
doc.pop("generated_at")
|
||||
doc["stats"].pop("keys_newly_issued") # 첫 뽑기만 발급 · 다시 뽑으면 0
|
||||
assert fresh == saved
|
||||
|
||||
|
||||
def test_목차_글줄_가르기_쪽_번호가_붙은_꼴() -> None:
|
||||
text = "제1장적용기준31-1 일반사항····31-1-1 목적····131-2-6 공구 및 경장비····19제2장가설공사352-1 가설물의 한도 ····35"
|
||||
assert const.toc_entries(text) == [
|
||||
("1", "1", "적용기준"),
|
||||
("1", "1-1", "일반사항"),
|
||||
("1", "1-1-1", "목적"),
|
||||
("1", "1-2-6", "공구 및 경장비"),
|
||||
("2", "2", "가설공사"),
|
||||
("2", "2-1", "가설물의 한도"),
|
||||
]
|
||||
assert const.toc_entries("제11장칠공사61111-1 공통사항····611")[1] == ("11", "11-1", "공통사항")
|
||||
|
||||
|
||||
def test_인용은_제목이_아님() -> None:
|
||||
names = {("공통부문", "3-2-4"): "터파기(기계)", ("공통부문", "3-2-5"): "되메우기"}
|
||||
chapters = {"2", "3", "4"}
|
||||
assert (
|
||||
const.titles_in("’3-2-4 터파기(기계)’를 참고하여 적용한다.", "공통부문", chapters, names)
|
||||
== []
|
||||
)
|
||||
assert const.titles_in("…경우3-2-4 터파기(기계)('25년 신설)", "공통부문", chapters, names) == [
|
||||
"3-2-4"
|
||||
]
|
||||
assert const.titles_in("853-2-5 되메우기Q = n·q", "공통부문", chapters, names) == ["3-2-5"]
|
||||
|
||||
|
||||
def test_목차_부문_다섯_장_마흔다섯(built: tuple) -> None:
|
||||
items = built[0]["work_items"]
|
||||
roots = [it for it in items if it["parent_code"] is None]
|
||||
assert [r["name"] for r in roots] == list(const.DIVISIONS)
|
||||
assert sum(1 for it in items if it["level"] == 2) == 45
|
||||
codes = [it["work_item_code"] for it in items]
|
||||
assert len(set(codes)) == len(codes)
|
||||
|
||||
|
||||
def test_표는_다_붙고_못_붙인_표는_목록(built: tuple) -> None:
|
||||
master = built[0]
|
||||
stats = master["stats"]
|
||||
assert stats["tables_total"] == 2192
|
||||
assert stats["tables_attached"] + len(master["orphan_tables"]) == stats["tables_total"]
|
||||
assert stats["tables_orphan"] == len(master["orphan_tables"]) == 0
|
||||
|
||||
|
||||
def test_목차_오기_없음_연혁_붙은_본문_제목이_목차와_같음(built: tuple) -> None:
|
||||
"""본문 제목 번호가 목차에 없거나 이름이 다르면 그 절 표가 **앞 절에 조용히 붙음** — 빨강."""
|
||||
items = built[0]["work_items"]
|
||||
names = {(it["division"], it["number"]): it["name"] for it in items if it["level"] > 1}
|
||||
titled = re.compile(r"(\d+(?:-\d+)+)\s*([^’'\[\]()]{1,40}?)\s*\(\s*['‘’]\d\d")
|
||||
data = json.loads(const.SOURCE.read_text(encoding="utf-8"))
|
||||
files = sorted({t["source_file"] for t in data["variables"]["pum"]["tables"]})
|
||||
seen, bad = 0, []
|
||||
for source_file in files:
|
||||
division, chapter = const.file_place(source_file)
|
||||
chapters = {str(chapter - 1), str(chapter), str(chapter + 1)}
|
||||
for line in (const.ROOT / source_file).read_text(encoding="utf-8").splitlines():
|
||||
for found in titled.finditer(line):
|
||||
raw, name = found.group(1), "".join(found.group(2).split())
|
||||
number = next(
|
||||
(
|
||||
raw[cut:]
|
||||
for cut in range(len(raw.split("-")[0]))
|
||||
if raw[cut:].split("-")[0] in chapters
|
||||
),
|
||||
None,
|
||||
)
|
||||
if number is None:
|
||||
continue
|
||||
seen += 1
|
||||
toc = "".join(names.get((division, number), "").split())
|
||||
if not toc or not (name.startswith(toc) or toc.startswith(name)):
|
||||
bad.append(
|
||||
f"{division} {number} 「{found.group(2)}」 목차 「{names.get((division, number))}」"
|
||||
)
|
||||
assert seen > 600
|
||||
assert not bad, bad
|
||||
|
||||
|
||||
def test_열쇠_CW_안_겹침_다시_뽑아도_같음(built: tuple, master: dict) -> None:
|
||||
keys = [it["work_item_key"] for it in master["work_items"]]
|
||||
assert all(re.fullmatch(r"CW-\d{5}", k) for k in keys)
|
||||
assert len(set(keys)) == len(keys)
|
||||
assert built[3][1] == [] # 장부가 있으니 새로 안 냄
|
||||
assert [it["work_item_key"] for it in built[0]["work_items"]] == keys
|
||||
|
||||
|
||||
def test_산출_파일이_지금_코드로_뽑은_것(built: tuple, master: dict) -> None:
|
||||
_same_except_time(built[0], master)
|
||||
|
||||
|
||||
def test_산림_산출도_그대로_금액_불변() -> None:
|
||||
"""표 읽기를 떼어내고(`_Table`) 건설과 나눠 써도 산림 산출은 한 글자도 안 바뀜."""
|
||||
saved = json.loads((OUT / "work_item_master_2026-01-01.json").read_text(encoding="utf-8"))
|
||||
master, undetermined, basis_missing, _ = forest.build()
|
||||
_same_except_time(master, saved)
|
||||
for name, fresh in (("form_undetermined", undetermined), ("basis_missing", basis_missing)):
|
||||
saved_items = json.loads((OUT / f"{name}_2026-01-01.json").read_text(encoding="utf-8"))[
|
||||
"items"
|
||||
]
|
||||
assert json.loads(json.dumps(fresh, ensure_ascii=False)) == saved_items
|
||||
|
||||
|
||||
def test_산림을_고르는_쪽이_건설_파일을_안_집음() -> None:
|
||||
"""⚠ `work_item_master_*` 로 끝 파일을 고르는 자리가 있음(B08 밑수 대조 · B09 근거표) —
|
||||
건설 산출을 `work_item_master_const_…` 로 두면 그쪽이 건설을 집어 밑수 대조가 통째로 빠졌음(2026-09-17)."""
|
||||
from B08_Quantity.B08_Quantity_Engine_BasisUnit import load_master
|
||||
|
||||
assert load_master()["dataset_id"] == "work_item_master_forest"
|
||||
assert not list(OUT.glob("work_item_master_*const*.json"))
|
||||
@@ -0,0 +1,64 @@
|
||||
"""공종 축 — **이름과 붙은 표의 절이 어긋나면 빨강** (2026-09-17 브레인 · 코덱스 원문 대조).
|
||||
|
||||
이 병은 한 번 나면 아무도 못 알아챔: 품셈 목차가 같은 번호를 두 번 적자(12-17-2 · 12-24-1)
|
||||
뒤 줄(「무근진동기 제외」)이 앞 표(F0363 「철근, 펌프카 0-15m」)를 물고, 진짜 표(F0364)는 orphan 으로 사라졌음.
|
||||
표 번호로는 안 잡힘(번호는 같았음) — **표 절 제목의 이름이 다른 줄 이름과 똑같은데 그 줄이 아닌 데 붙음**이 그 꼴.
|
||||
⚠ 이름 글자만 조금 다른 자리(목차 「깍기」 · 본문 「깎기」)는 판정하지 않음 — 번호·이름이 딴 줄을 가리킬 때만.
|
||||
⚠ 부록 사례 표 7(F0455·F0456·F0472~F0476)은 번호가 본문 절과 겹쳐 그 절에 붙어 있으나 모두 참조형 —
|
||||
일위대가로 안 읽혀 금액 무관(브레인 전수 판정). 자리 판정은 데스크탑 서브 몫.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MASTER = ROOT / "resources" / "data_work_item_master" / "work_item_master_2026-01-01.json"
|
||||
_TITLE = re.compile(r"^\s*(\d+(?:-\d+){0,3})\.\s+(.+)$")
|
||||
|
||||
|
||||
def _squeeze(text: str) -> str:
|
||||
return "".join(str(text).split())
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master() -> dict:
|
||||
return json.loads(MASTER.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_목차_번호가_안_겹침(master: dict) -> None:
|
||||
numbers = [it["number"] for it in master["work_items"]]
|
||||
assert len(set(numbers)) == len(numbers)
|
||||
|
||||
|
||||
def test_표_절_제목이_다른_줄을_가리키지_않음(master: dict) -> None:
|
||||
items = master["work_items"]
|
||||
by_name: dict[str, set[str]] = {} # 이름 → 그 이름 줄의 열쇠(번호가 겹친 줄도 갈림)
|
||||
for it in items:
|
||||
by_name.setdefault(_squeeze(it["name"]), set()).add(it["work_item_key"])
|
||||
bad = []
|
||||
for it in items:
|
||||
for table in it["tables"]:
|
||||
title = _TITLE.match(table["section"])
|
||||
if title is None:
|
||||
continue
|
||||
number, name = title.group(1), _squeeze(title.group(2))
|
||||
others = by_name.get(name, set()) - {it["work_item_key"]}
|
||||
if number != it["number"] or (others and name != _squeeze(it["name"])):
|
||||
bad.append(
|
||||
f"{table['pum_table_id']} 「{table['section']}」 → {it['number']} {it['name']}"
|
||||
)
|
||||
assert not bad, bad
|
||||
|
||||
|
||||
def test_사라진_표는_목록으로_남음(master: dict) -> None:
|
||||
"""orphan 을 조용히 버리지 않음 — 목록 수와 집계가 같아야 함."""
|
||||
assert master["stats"]["tables_orphan"] == len(master["orphan_tables"])
|
||||
assert (
|
||||
master["stats"]["tables_attached"] + len(master["orphan_tables"])
|
||||
== master["stats"]["tables_total"]
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Z01 공종 축 API 틀 — 산림·건설 두 kind 를 기초단가와 같은 길로(2026-09-17 브레인 · PLAN_공종축 8-4).
|
||||
|
||||
못박는 것: 평평한 잎 목록 · 전체 경로 이름(겹침 없음) · 계산 규칙(choose_one 91 · sum_steps 3) ·
|
||||
불변 열쇠가 오면 `@id` 가 그것으로 · 건설은 자료가 있어야 상자가 섬 · 고치기는 막힘.
|
||||
불변 열쇠가 `@id` · 건설도 같은 길(자료가 있어야 상자가 섬) · 고치기는 막힘.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -13,7 +13,6 @@ import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from Z01_MasterData import Z01_MasterData_BasePrices as base_prices
|
||||
from Z01_MasterData import Z01_MasterData_Overrides as overrides
|
||||
from Z01_MasterData import Z01_MasterData_Router as router_module
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
@@ -39,14 +38,16 @@ def _get(client: TestClient, kind: str, **params) -> dict:
|
||||
def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> None:
|
||||
listed = client.get("/api/master-data/base-prices").json()["kinds"]
|
||||
forest = next(i for i in listed if i["kind"] == "work_item_forest")
|
||||
assert forest["group"] == "work_item" and forest["rows"] == 358
|
||||
assert forest["group"] == "work_item" and forest["rows"] == 360
|
||||
|
||||
table = _get(client, "work_item_forest", size=500)
|
||||
assert table["total"] == 358 and table["stats"] == {"leaves": 358, "units": 618}
|
||||
assert table["total"] == 360 and table["stats"] == {"leaves": 360, "units": 620}
|
||||
assert table["editable"] == [] and set(table["locked"]) == set(table["sortable"])
|
||||
rows = table["rows"]
|
||||
assert len({r["@id"] for r in rows}) == 358
|
||||
assert len({r["name"] for r in rows}) == 358 # 잎 이름 35줄 겹침 — 경로로 풀림
|
||||
assert len({r["@id"] for r in rows}) == 360
|
||||
assert (
|
||||
len({r["name"] for r in rows}) == 360
|
||||
) # 잎 이름 겹침 — 경로로 풀림 · 목차 오기 바로잡아 358 → 360(12-17-2 · 12-24-1)
|
||||
by_code = {r["work_item_code"]: r for r in rows}
|
||||
assert by_code["FP-09-03-02"]["name"] == "토공 › 토사깍기 › 기계"
|
||||
|
||||
@@ -56,19 +57,29 @@ def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> Non
|
||||
assert [s["weight"] for s in blast["steps"]] == ["0.1", "0.9", "1"]
|
||||
assert by_code["FP-09-05-02"]["rule"] == "sum_steps"
|
||||
assert by_code["FP-09-05-02"]["step_weight"] == "0.9"
|
||||
# 2026-09-17 FW- 가 옴(데스크탑 서브) — 알림이 사라지는 것이 곧 열쇠가 섰다는 증거.
|
||||
assert all(r["@id"].startswith("FW-") for r in rows) # 불변 열쇠(데스크탑 서브 7dd2c515)
|
||||
assert not any("불변 열쇠" in line for line in table["notice"])
|
||||
|
||||
found = _get(client, "work_item_forest", q="발파암 ›")
|
||||
assert found["total"] == 3
|
||||
|
||||
|
||||
def test_불변_열쇠가_오면_id_로_씀_건설도_같은_길(
|
||||
def test_건설_공종_축도_같은_길(client: TestClient) -> None:
|
||||
"""건설(2026-09-17 뽑음) — 부문이 뿌리라 경로가 부문부터 · 열쇠 CW- · 계산 규칙은 전부 choose_one."""
|
||||
table = _get(client, "work_item_const", size=500)
|
||||
assert table["total"] == table["stats"]["leaves"] > 900
|
||||
rows = table["rows"]
|
||||
assert all(r["@id"].startswith("CW-") for r in rows)
|
||||
assert all(r["name"].split(" › ")[0].endswith("부문") for r in rows)
|
||||
assert {r["mode"] for r in table["rules"]} == {"choose_one"}
|
||||
assert not any("불변 열쇠" in line for line in table["notice"])
|
||||
found = _get(client, "work_item_const", q="굴착(인력/토사)")
|
||||
assert [r["name"] for r in found["rows"]] == ["공통부문 › 토공사 › 굴착 › 굴착(인력/토사)"]
|
||||
|
||||
|
||||
def test_불변_열쇠가_오면_id_로_씀_자료_파일은_판_id_로_가림(
|
||||
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
if work_items.doc("work_item_construction") is None: # 빈 상자 안 세움
|
||||
assert "work_item_construction" not in base_prices.KINDS
|
||||
|
||||
folder = tmp_path / "data_work_item_master"
|
||||
folder.mkdir()
|
||||
fake = {
|
||||
@@ -83,24 +94,21 @@ def test_불변_열쇠가_오면_id_로_씀_건설도_같은_길(
|
||||
"variant_keys": ["갑", "을"]},
|
||||
],
|
||||
} # fmt: skip
|
||||
(folder / "work_item_master_const_2026-01-01.json").write_text(
|
||||
(folder / "const_work_item_master_2026-01-01.json").write_text(
|
||||
json.dumps(fake, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(work_items, "FOLDER", folder)
|
||||
monkeypatch.setattr(work_items, "KINDS", tuple(work_items.SOURCE_OF))
|
||||
monkeypatch.setattr(base_prices, "KINDS", (*base_prices.KINDS, "work_item_construction"))
|
||||
|
||||
table = _get(client, "work_item_construction")
|
||||
table = _get(client, "work_item_const")
|
||||
assert [r["@id"] for r in table["rows"]] == ["CW-00002"]
|
||||
assert table["rows"][0]["work_item_code"] == "CP-01-01" # 목차 코드는 칸으로 남음
|
||||
assert table["rows"][0]["name"] == "토공 › 인력"
|
||||
assert table["stats"] == {"leaves": 1, "units": 2}
|
||||
assert table["rules"][0]["steps"] == [{"@id": "CW-00002", "weight": "0.5"}]
|
||||
assert not any("불변 열쇠" in line for line in table["notice"])
|
||||
assert _get(client, "work_item_forest")["total"] == 0 # 산림 파일은 이 폴더에 없음
|
||||
|
||||
res = client.put(
|
||||
"/api/master-data/base-prices/work_item_construction/CW-00002",
|
||||
"/api/master-data/base-prices/work_item_const/CW-00002",
|
||||
json={"values": {"name": "바꿈"}},
|
||||
)
|
||||
assert res.status_code == 400 and "공종 축 뼈대" in res.json()["detail"]
|
||||
|
||||
Reference in New Issue
Block a user