From 2ee6a7ed886f1858ff07a095df93a9ae787d6973 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 17 Sep 2026 16:45:50 +0900 Subject: [PATCH 1/4] auto: 2026-09-17 16:45 (ESD_LAPTOP) --- Z01_MasterData/Z01_MasterData_WorkItems.py | 14 +++++++--- resources/tester/test_z01_work_items.py | 30 +++++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Z01_MasterData/Z01_MasterData_WorkItems.py b/Z01_MasterData/Z01_MasterData_WorkItems.py index 0d955e3f..4bacb5ef 100644 --- a/Z01_MasterData/Z01_MasterData_WorkItems.py +++ b/Z01_MasterData/Z01_MasterData_WorkItems.py @@ -148,6 +148,8 @@ def notice(kind: str) -> list[str]: #: ⚠ 「임도가 쓰는 것」 같은 쓰임 가름은 잣대가 아직 없어 안 냄(지어내지 않음). FILTER_KEYS = ("division", "axis_role") ALL = "" +#: 값을 안 보냈을 때 거는 값 — 총칙 줄은 기본 감춤 · 「전부」를 고르면 보임(2026-09-17 브레인) +DEFAULTS = {"axis_role": "work_item"} def _facets(kind: str) -> dict[str, dict[str, str]]: @@ -166,7 +168,7 @@ def _role_names() -> dict[str, str]: def filters(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: - """거르기 상자 — 값이 둘 이상 갈리는 가름만 · 기본은 「전부」(안 거름).""" + """거르기 상자 — 값이 둘 이상 갈리는 가름만 · 기본은 `DEFAULTS`(그 값이 표에 있을 때) 아니면 「전부」.""" facets = _facets(kind) labels = {"division": ("부문", {}), "axis_role": ("줄 구실", _role_names())} out = [] @@ -179,13 +181,14 @@ def filters(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: if len(values) < 2: continue label, names = labels[key] + default = DEFAULTS.get(key, ALL) out.append( { "key": key, "label": label, "options": [{"value": ALL, "label": "전부", "rows": len(rows)}] + [{"value": v, "label": names.get(v, v), "rows": counts[v]} for v in values], - "default": ALL, + "default": default if default in values else ALL, } ) return out @@ -195,8 +198,11 @@ def apply_filters( kind: str, rows: list[dict[str, Any]], picked: dict[str, str] ) -> list[dict[str, Any]]: facets = _facets(kind) - for key, value in picked.items(): - if key in FILTER_KEYS and value != ALL: + # 안 보낸 가름은 상자 기본값으로 — 상자에 보이는 값과 걸린 값이 같게(찾기 결과가 아니라 표 전체로 정함) + defaults = {f["key"]: f["default"] for f in filters(kind, base_rows(kind))} + for key in FILTER_KEYS: + value = picked.get(key, defaults.get(key, ALL)) + if value != ALL: rows = [r for r in rows if facets.get(r["@id"], {}).get(key) == value] return rows diff --git a/resources/tester/test_z01_work_items.py b/resources/tester/test_z01_work_items.py index 9e8b30a8..d32ec95a 100644 --- a/resources/tester/test_z01_work_items.py +++ b/resources/tester/test_z01_work_items.py @@ -40,7 +40,7 @@ def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> Non forest = next(i for i in listed if i["kind"] == "work_item_forest") assert forest["group"] == "work_item" and forest["rows"] == 365 - table = _get(client, "work_item_forest", size=500) + table = _get(client, "work_item_forest", size=500, axis_role="") # 「전부」 — 총칙까지 assert table["total"] == 365 and table["stats"] == {"work_items": 365, "units": 627} assert table["editable"] == [] and set(table["locked"]) == set(table["sortable"]) rows = table["rows"] @@ -67,7 +67,7 @@ def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> Non def test_건설_공종_축도_같은_길(client: TestClient) -> None: """건설(2026-09-17 뽑음) — 부문이 뿌리라 경로가 부문부터 · 열쇠 CW- · 계산 규칙은 전부 choose_one.""" - table = _get(client, "work_item_const", size=500) + table = _get(client, "work_item_const", size=500, axis_role="") assert table["total"] == table["stats"]["work_items"] > 900 rows = table["rows"] assert all(r["@id"].startswith("CW-") for r in rows) @@ -118,7 +118,7 @@ def test_불변_열쇠가_오면_id_로_씀_자료_파일은_판_id_로_가림( def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestClient) -> None: """화면(서브 b890fba0)이 `filters` 를 받으면 상자를 세우고 고른 값만 보냄 — 판정은 서버 한 곳. 건설 부문 다섯 · 줄 구실(이름은 axis_policy.json). ⚠ 「임도가 쓰는 것」은 잣대가 없어 안 냄.""" - const = _get(client, "work_item_const", size=1) + const = _get(client, "work_item_const", size=1, axis_role="") by_key = {f["key"]: f for f in const["filters"]} assert set(by_key) == {"division", "axis_role"} division = by_key["division"] @@ -136,7 +136,7 @@ def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestCl "general_provision" ] == "총칙" - civil = _get(client, "work_item_const", size=500, division="토목부문") + civil = _get(client, "work_item_const", size=500, division="토목부문", axis_role="") assert civil["total"] == next( o["rows"] for o in division["options"] if o["value"] == "토목부문" ) @@ -149,3 +149,25 @@ def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestCl assert [f["key"] for f in _get(client, "work_item_forest", size=1)["filters"]] == ["axis_role"] assert _get(client, "labor", size=1, division="토목부문")["total"] == 261 # 다른 표는 안 거름 + + +def test_총칙_줄은_기본_감춤_전부를_고르면_보임(client: TestClient) -> None: + """2026-09-17 브레인 — 값을 안 보내면 상자 기본값(공종)으로 거름 · 화면은 그 기본값을 상자에 보임.""" + for kind in ("work_item_forest", "work_item_const"): + shown = _get(client, kind, size=500) + role = next(f for f in shown["filters"] if f["key"] == "axis_role") + assert role["default"] == "work_item" + counts = {o["value"]: o["rows"] for o in role["options"]} + assert counts["general_provision"] > 0 + # 기본 = 공종만 · 찾기 결과로 기본이 흔들리지 않음(총칙만 걸리는 낱말도 기본은 감춤) + assert shown["total"] == counts["work_item"] + assert ( + _get(client, kind, size=500, q="적용기준")["total"] + < _get(client, kind, size=500, q="적용기준", axis_role="")["total"] + ) + # 「전부」(빈 값을 보냄) → 총칙까지 + assert _get(client, kind, size=1, axis_role="")["total"] == counts[""] + assert ( + _get(client, kind, size=1, axis_role="general_provision")["total"] + == (counts["general_provision"]) + ) From b4294e34831d151022c7c15976a051b5c17575c7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 17 Sep 2026 16:46:16 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(z01):=20=EA=B3=B5=EC=A2=85=20=EC=B6=95?= =?UTF-8?q?=20=EC=B4=9D=EC=B9=99=20=EC=A4=84=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=B0=90=EC=B6=A4=20=E2=80=94=20=EB=B0=94=EB=A1=9C=20=EC=95=9E?= =?UTF-8?q?=20auto:=20=EC=BB=A4=EB=B0=8B=EC=9D=98=20=EC=84=A4=EB=AA=85(?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B=20=EA=B8=80=20=EB=94=B0=EC=98=B4=ED=91=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=AA=85=EB=A0=B9=EC=9D=B4=20=EA=B9=A8=EC=A0=B8=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1=EC=9C=BC=EB=A1=9C=20=EC=98=AC=EB=9D=BC?= =?UTF-8?q?=EA=B0=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 줄 구실 거르기 기본값을 「공종」으로 · 「전부」를 고르면 총칙까지 - 값을 안 보내면 상자 기본값으로 거름(상자에 보이는 값 = 걸린 값) · 빈 값을 보내면 전부 - 기본값은 찾기 결과가 아니라 표 전체로 정함 — 총칙만 걸리는 낱말로 찾아도 기본은 감춤 - 그 값이 표에 없으면 기본 「전부」(총칙 가름이 없는 자료는 안 거름) - 화면 코드는 그대로(b890fba0 거르기 상자) — 산림 306(전부 365) · 건설 983(전부 997) - 시험: 기본 감춤·전부·총칙만 · 기본값을 끄면 빨강 확인 · 전체 목록을 보던 시험 넷은 axis_role 빈 값으로 전부를 명시 · Z01 시험 127 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq From 0ff186c1946d8fa584ef5c434f22f820b4942075 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 17 Sep 2026 16:52:56 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(z01):=20=EA=B3=B5=EC=A2=85=20=EC=B6=95?= =?UTF-8?q?=20=EA=B0=88=EB=9E=98=EB=A7=88=EB=8B=A4=20=ED=95=9C=20=EC=A4=84?= =?UTF-8?q?=20=E2=80=94=20=EA=B5=90=EB=AA=A9=EA=B5=B4=EC=B7=A8(=EB=82=98?= =?UTF-8?q?=EB=AC=B4=EB=86=92=EC=9D=B4)=209=EA=B0=88=EB=9E=98=EA=B0=80=20?= =?UTF-8?q?=ED=95=9C=20=EC=A4=84=EC=97=90=20=EB=AD=89=EC=B3=90=20=EC=96=B4?= =?UTF-8?q?=EB=8A=90=20=EB=86=92=EC=9D=B4=20=EA=B0=92=EC=9D=B8=EC=A7=80=20?= =?UTF-8?q?=EB=AA=BB=20=EC=A7=91=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 줄 = 갈래(일위대가가 갈래마다 다름) · 산림 365공종 → 627줄 · 건설은 갈래 키가 비어 997 그대로 - 줄 열쇠 = 공종 열쇠#갈래 키(FW-00105#1.1∼1.5 · B08·B09 코드#갈래 꼴) · 갈래 키 글자는 마스터 것 그대로(조립 두 벌 금지) - 공종 열쇠는 @key 로 함께 보냄 — 거르기(부문·줄 구실)와 stats.work_items 는 공종 단위 - 칸: 갈래 키 목록(variant_keys) → 갈래 하나(variant) · 이름표 공종 축 두 표의 열도 같이 바꿈(묵은 열 시험) - 화면 코드는 그대로 — 산림 단추 627 · 교목굴취 찾기 10줄(9갈래 + 근원직경) · 2.6∼3.0 칸 집힘 - 시험: 교목굴취 9갈래 차례·열쇠 · 갈래 없는 공종은 # 없는 한 줄 · 갈래 이름으로 찾힘 · 앞 시험 수(365 → 627 · 발파암 3 → 5 · 가짜 자료 두 줄) 맞춤 · Z01·이름표 시험 105 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- Z01_MasterData/Z01_MasterData_WorkItems.py | 43 +++++++++++-------- .../data_master_labels/labels_2026-01-01.json | 14 +++--- resources/tester/test_z01_work_items.py | 32 ++++++++++---- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/Z01_MasterData/Z01_MasterData_WorkItems.py b/Z01_MasterData/Z01_MasterData_WorkItems.py index 4bacb5ef..766efd62 100644 --- a/Z01_MasterData/Z01_MasterData_WorkItems.py +++ b/Z01_MasterData/Z01_MasterData_WorkItems.py @@ -1,7 +1,7 @@ """Z01 공종 축 — 산림·건설 두 kind 를 기초단가와 **같은 계약**으로(2026-09-17 브레인 · PLAN_공종축 8-4). 줄 = **품셈 표가 붙은 마디**(`axis_policy.json` `flat_list_filter: pum_tables > 0` — 잎으로 거르면 부모에 붙은 표가 안 뜸) · - 갈래(`variant_keys`)는 칸으로(편 수 = `stats.units`). + 갈래(`variant_keys`)마다 한 줄로 폄 — 줄 수 = 편 수(`stats.units`) · 공종 수 = `stats.work_items`. 이름 = 뿌리부터 전체 경로(「토공 › 토사깍기 › 기계」 · 건설은 부문부터) — 잎 이름만으론 겹침. 계산 규칙 = 표 수준 `rules`(부모 마디 choose_one · sum_steps) + 줄 칸 `rule`·`step_weight` — 빠지면 발파암(절취 0.1 + 깍기 0.9 + 집토 1) 금액이 틀어짐. @@ -86,20 +86,25 @@ def base_rows(kind: str) -> list[dict[str, Any]]: (s["weight"] for s in parent.get("steps") or [] if s["code"] == item["work_item_code"]), None, ) - rows.append( - { - "@id": _key(item), - "@source": name, - "work_item_code": item["work_item_code"], # 목차 코드 — 열쇠 아님, 칸으로만 - "number": item["number"], - "name": _path(item, by), - "variant_keys": item.get("variant_keys") or [], - "pum_tables": [t["pum_table_id"] for t in item["tables"]], - "rule": parent.get("parent_mode"), - "step_weight": weight, - "effective_date": d.get("pum_edition") or d.get("effective_date"), - } - ) + key = _key(item) + # 갈래마다 한 줄 — 일위대가가 갈래마다 달라 집는 단위가 갈래임(교목굴취 나무높이 9 · 2026-09-17 브레인 ③). + # 열쇠 = 공종 열쇠#갈래 키(B08·B09 `코드#갈래` 꼴) · 갈래 키 글자는 마스터 것 그대로(조립 두 벌 금지). + for variant in item.get("variant_keys") or [None]: + rows.append( + { + "@id": f"{key}#{variant}" if variant else key, + "@key": key, # 공종 열쇠 — 거르기·줄 수는 공종 단위로 + "@source": name, + "work_item_code": item["work_item_code"], # 목차 코드 — 열쇠 아님, 칸으로만 + "number": item["number"], + "name": _path(item, by), + "variant": variant, + "pum_tables": [t["pum_table_id"] for t in item["tables"]], + "rule": parent.get("parent_mode"), + "step_weight": weight, + "effective_date": d.get("pum_edition") or d.get("effective_date"), + } + ) return rows @@ -175,7 +180,7 @@ def filters(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: for key in FILTER_KEYS: counts: dict[str, int] = {} for row in rows: - value = facets.get(row["@id"], {}).get(key, ALL) + value = facets.get(row["@key"], {}).get(key, ALL) counts[value] = counts.get(value, 0) + 1 values = [v for v in counts if v != ALL] if len(values) < 2: @@ -203,7 +208,7 @@ def apply_filters( for key in FILTER_KEYS: value = picked.get(key, defaults.get(key, ALL)) if value != ALL: - rows = [r for r in rows if facets.get(r["@id"], {}).get(key) == value] + rows = [r for r in rows if facets.get(r["@key"], {}).get(key) == value] return rows @@ -214,7 +219,7 @@ def extra(kind: str) -> dict[str, Any]: "filters": filters(kind, rows), "rules": rules(kind), "stats": { - "work_items": len(rows), - "units": sum(max(1, len(r["variant_keys"])) for r in rows), + "work_items": len({r["@key"] for r in rows}), + "units": len(rows), }, } diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index 1a560ad0..ae00c042 100644 --- a/resources/data_master_labels/labels_2026-01-01.json +++ b/resources/data_master_labels/labels_2026-01-01.json @@ -1389,10 +1389,11 @@ "note": "뿌리부터 전체 경로(「토공 › 토사깍기 › 기계」) — 잎 이름만으론 겹침." }, { - "key": "variant_keys", - "name_ko": "갈래 키", + "key": "variant", + "name_ko": "갈래", "unit": "", - "visible": true + "visible": true, + "note": "갈래마다 한 줄(일위대가가 갈래마다 다름) — 줄 열쇠는 공종 열쇠#갈래." }, { "key": "pum_tables", @@ -1452,10 +1453,11 @@ "note": "부문부터 전체 경로(「공통부문 › 토공사 › 굴착 › 굴착(인력/토사)」) — 부문마다 장 번호가 1부터라 부문이 뿌리." }, { - "key": "variant_keys", - "name_ko": "갈래 키", + "key": "variant", + "name_ko": "갈래", "unit": "", - "visible": true + "visible": true, + "note": "갈래마다 한 줄(일위대가가 갈래마다 다름) — 줄 열쇠는 공종 열쇠#갈래." }, { "key": "pum_tables", diff --git a/resources/tester/test_z01_work_items.py b/resources/tester/test_z01_work_items.py index d32ec95a..ac7c4d2a 100644 --- a/resources/tester/test_z01_work_items.py +++ b/resources/tester/test_z01_work_items.py @@ -38,15 +38,15 @@ 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"] == 365 + assert forest["group"] == "work_item" and forest["rows"] == 627 # 갈래마다 한 줄 table = _get(client, "work_item_forest", size=500, axis_role="") # 「전부」 — 총칙까지 - assert table["total"] == 365 and table["stats"] == {"work_items": 365, "units": 627} + assert table["total"] == 627 and table["stats"] == {"work_items": 365, "units": 627} assert table["editable"] == [] and set(table["locked"]) == set(table["sortable"]) - rows = table["rows"] - assert len({r["@id"] for r in rows}) == 365 + rows = table["rows"] + _get(client, "work_item_forest", size=500, page=2, axis_role="")["rows"] + assert len({r["@id"] for r in rows}) == 627 assert ( - len({r["name"] for r in rows}) == 365 + len({r["@key"] for r in rows}) == len({r["name"] for r in rows}) == 365 ) # 잎 이름 겹침 — 경로로 풀림 · 목차 오기 바로잡아 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"] == "토공 › 토사깍기 › 기계" @@ -62,7 +62,7 @@ def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> Non assert not any("불변 열쇠" in line for line in table["notice"]) found = _get(client, "work_item_forest", q="발파암 ›") - assert found["total"] == 3 + assert found["total"] == 5 # 절취 · 깍기(연암·보통암·경암) · 집토 def test_건설_공종_축도_같은_길(client: TestClient) -> None: @@ -101,7 +101,8 @@ def test_불변_열쇠가_오면_id_로_씀_자료_파일은_판_id_로_가림( monkeypatch.setattr(work_items, "FOLDER", folder) table = _get(client, "work_item_const") - assert [r["@id"] for r in table["rows"]] == ["CW-00002"] + assert [r["@id"] for r in table["rows"]] == ["CW-00002#갑", "CW-00002#을"] # 갈래마다 한 줄 + assert {r["@key"] 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"] == {"work_items": 1, "units": 2} @@ -109,7 +110,7 @@ def test_불변_열쇠가_오면_id_로_씀_자료_파일은_판_id_로_가림( assert _get(client, "work_item_forest")["total"] == 0 # 산림 파일은 이 폴더에 없음 res = client.put( - "/api/master-data/base-prices/work_item_const/CW-00002", + "/api/master-data/base-prices/work_item_const/CW-00002%23갑", json={"values": {"name": "바꿈"}}, ) assert res.status_code == 400 and "공종 축 뼈대" in res.json()["detail"] @@ -151,6 +152,21 @@ def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestCl assert _get(client, "labor", size=1, division="토목부문")["total"] == 261 # 다른 표는 안 거름 +def test_갈래마다_한_줄이라_편_하나를_집는다(client: TestClient) -> None: + """2026-09-17 브레인 ③ — 일위대가는 갈래마다 다름. 교목굴취(나무높이) 한 줄이면 어느 높이 값인지 못 고름.""" + rows = _get(client, "work_item_forest", size=50, q="교목굴취(나무높이)")["rows"] + assert [r["variant"] for r in rows] == [ + "1.0 이하", "1.1∼1.5", "1.6∼2.0", "2.1∼2.5", "2.6∼3.0", + "3.1∼3.5", "3.6∼4.0", "4.1∼4.5", "4.6∼5.0", + ] # fmt: skip + assert rows[1]["@id"] == "FW-00105#1.1∼1.5" and rows[1]["@key"] == "FW-00105" + # 갈래 없는 공종은 한 줄 · 열쇠에 # 없음 + plain = _get(client, "work_item_forest", q="교목굴취(근원직경)")["rows"] + assert [(r["@id"], r["variant"]) for r in plain] == [("FW-00106", None)] + # 갈래 이름으로도 찾힘 + assert _get(client, "work_item_forest", q="2.1∼2.5")["total"] >= 1 + + def test_총칙_줄은_기본_감춤_전부를_고르면_보임(client: TestClient) -> None: """2026-09-17 브레인 — 값을 안 보내면 상자 기본값(공종)으로 거름 · 화면은 그 기본값을 상자에 보임.""" for kind in ("work_item_forest", "work_item_const"): From fd2ed651866536bfbecff66375c17f26ac512721 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 17 Sep 2026 16:54:48 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat(z01):=20=EC=83=81=EC=9E=90=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=C2=B7=EA=B3=84=EC=82=B0=20=EA=B7=9C=EC=B9=99=EC=9D=84?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=ED=91=9C=EC=97=90=EC=84=9C=20=EC=9D=BD?= =?UTF-8?q?=EC=96=B4=20=EB=B3=B4=EB=83=84=20=E2=80=94=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=ED=91=9C=EC=97=94=20=EC=9D=B4=EB=AF=B8=20=EC=9E=88=EC=97=88?= =?UTF-8?q?=EA=B3=A0=20=EC=84=9C=EB=B2=84=EA=B0=80=20=EC=95=88=20=EC=8B=A4?= =?UTF-8?q?=EC=96=B4=20=EC=98=81=EB=AC=B8=EC=9C=BC=EB=A1=9C=20=EB=9C=A8?= =?UTF-8?q?=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 목록 응답 kinds 에 group_label — 이름표 base_price_groups 이름(기초단가 · 품셈 기준 · 공종 축) · 화면은 7cb15af1 부터 이 칸을 읽음 - 공종 축 계산 규칙 칸 — 이름표 value_overrides 의 work_item_master 방식 이름(아래 하나를 고름 · 아래를 단계로 다 더함) · 없으면 영문 그대로 - 표 수준 rules 의 mode 는 영문 그대로(기계가 읽는 자리) - 화면 확인: 상자 넷 한글 · 발파암 다섯 줄 「아래를 단계로 다 더함 | 0.1 · 0.9 · 1」 - 시험: 상자 이름 셋 · 발파암 깍기 합산 · 토사깍기 하나 고름 · Z01·이름표 시험 105 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- Z01_MasterData/Z01_MasterData_BasePrices.py | 9 ++++++++- Z01_MasterData/Z01_MasterData_WorkItems.py | 7 ++++++- resources/tester/test_z01_work_items.py | 10 +++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Z01_MasterData/Z01_MasterData_BasePrices.py b/Z01_MasterData/Z01_MasterData_BasePrices.py index 4a17fb8d..9db2ba37 100644 --- a/Z01_MasterData/Z01_MasterData_BasePrices.py +++ b/Z01_MasterData/Z01_MasterData_BasePrices.py @@ -427,14 +427,21 @@ def kind_label(kind: str) -> str: return named.get("name_ko") or kind +def group_label(group: str) -> str: + """상자 이름 — 이름표 `base_price_groups[갈래].name_ko` 에서만(없으면 영문 그대로 · 화면은 `group_label` 을 읽음).""" + named = (tables.load_labels().get("base_price_groups") or {}).get(group) or {} + return named.get("name_ko") or group + + def kinds() -> list[dict[str, Any]]: """화면이 상자를 세울 목록 — **서버가 냄**(화면이 제 코드에 들면 새 kind 가 조용히 안 뜸). - 갈래 · kind · 한글 이름(이름표) · 줄 수. + 갈래 · 갈래 한글 이름 · kind · 한글 이름(이름표) · 줄 수. """ return [ { "group": kind_group(kind), + "group_label": group_label(kind_group(kind)), "kind": kind, "label": kind_label(kind), "rows": len(rows(kind)), diff --git a/Z01_MasterData/Z01_MasterData_WorkItems.py b/Z01_MasterData/Z01_MasterData_WorkItems.py index 766efd62..37b271eb 100644 --- a/Z01_MasterData/Z01_MasterData_WorkItems.py +++ b/Z01_MasterData/Z01_MasterData_WorkItems.py @@ -77,6 +77,8 @@ def base_rows(kind: str) -> list[dict[str, Any]]: return [] d, name = found items, by, children = _tree(d) + # 계산 규칙 이름 — 이름표 `value_overrides["work_item_master::<방식>"]`(「아래를 단계로 다 더함」) · 없으면 영문 + named = tables.load_labels().get("value_overrides") or {} rows = [] for item in items: if not item.get("tables"): @@ -100,7 +102,10 @@ def base_rows(kind: str) -> list[dict[str, Any]]: "name": _path(item, by), "variant": variant, "pum_tables": [t["pum_table_id"] for t in item["tables"]], - "rule": parent.get("parent_mode"), + "rule": (named.get(f"work_item_master::{parent.get('parent_mode')}") or {}).get( + "name_ko" + ) + or parent.get("parent_mode"), "step_weight": weight, "effective_date": d.get("pum_edition") or d.get("effective_date"), } diff --git a/resources/tester/test_z01_work_items.py b/resources/tester/test_z01_work_items.py index ac7c4d2a..64828a8d 100644 --- a/resources/tester/test_z01_work_items.py +++ b/resources/tester/test_z01_work_items.py @@ -39,6 +39,12 @@ def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> Non 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"] == 627 # 갈래마다 한 줄 + # 상자 이름은 이름표 것 — 영문 키(work_item)로 뜨던 자리(2026-09-17 브레인 ①) + assert {i["group"]: i["group_label"] for i in listed} == { + "base_price": "기초단가", + "pumsem_basis": "품셈 기준", + "work_item": "공종 축", + } table = _get(client, "work_item_forest", size=500, axis_role="") # 「전부」 — 총칙까지 assert table["total"] == 627 and table["stats"] == {"work_items": 365, "units": 627} @@ -55,7 +61,9 @@ def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> Non assert modes.count("choose_one") == 91 and modes.count("sum_steps") == 3 blast = next(r for r in table["rules"] if r["work_item_code"] == "FP-09-05") assert [s["weight"] for s in blast["steps"]] == ["0.1", "0.9", "1"] - assert by_code["FP-09-05-02"]["rule"] == "sum_steps" + # 계산 규칙 칸은 이름표 이름 — 「sum_steps」 영문으로는 더해야 하는 줄로 안 읽힘(브레인 ②) + assert by_code["FP-09-05-02"]["rule"] == "아래를 단계로 다 더함" + assert by_code["FP-09-03-02"]["rule"] == "아래 하나를 고름" 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)