diff --git a/Z01_MasterData/Z01_MasterData_WorkItems.py b/Z01_MasterData/Z01_MasterData_WorkItems.py index 142f1505..77945cfe 100644 --- a/Z01_MasterData/Z01_MasterData_WorkItems.py +++ b/Z01_MasterData/Z01_MasterData_WorkItems.py @@ -186,8 +186,10 @@ def notice(kind: str) -> list[str]: #: 줄 구실만 기본 「공종」(총칙 기본 감춤 · 서브 2ee6a7ed · 브레인). FILTER_KEYS = ("link", "division", "axis_role") ALL = "" -#: 값을 안 보냈을 때 거는 값 — 총칙 줄은 기본 감춤 · 「전부」를 고르면 보임(2026-09-17 브레인) -DEFAULTS = {"axis_role": "work_item"} +#: 값을 안 보냈을 때 거는 값 — **잣대가 정한다**(`axis_policy.json` 의 `list_default`). +#: ⚠ 코드에 박지 않는다 — 총칙 범위는 기관이 정하는 것이라 관리자가 고칠 수 있어야 한다. +#: 잣대를 못 읽을 때만 쓰는 대비값(총칙 줄은 기본 감춤 · 「전부」를 고르면 보임 · 2026-09-17 브레인). +FALLBACK_DEFAULTS = {"axis_role": "work_item"} def _facets(kind: str) -> dict[str, dict[str, str]]: @@ -204,17 +206,30 @@ def _facets(kind: str) -> dict[str, dict[str, str]]: } -def _role_names() -> dict[str, str]: - """줄 구실 이름 — `axis_policy.json`(데스크탑 서브 잣대) 한 곳에서.""" +def _policy() -> dict[str, Any]: + """줄 구실 잣대 — `axis_policy.json`(데스크탑 서브) 한 곳에서.""" path = FOLDER / "axis_policy.json" - if not path.is_file(): - return {} - roles = _read(str(path), path.stat().st_mtime_ns).get("roles") or {} - return {key: str(role.get("name_ko") or key) for key, role in roles.items()} + return _read(str(path), path.stat().st_mtime_ns) if path.is_file() else {} + + +def _role_names() -> dict[str, str]: + """거르기 상자에 뜰 이름. 목록에서는 「총칙」이 아니라 **공종이 아님**을 드러내는 이름으로 적는다.""" + roles = _policy().get("roles") or {} + return { + key: str(role.get("list_label") or role.get("name_ko") or key) + for key, role in roles.items() + } + + +def _defaults() -> dict[str, str]: + """안 고른 가름에 걸 값 — 잣대의 `list_default`. 못 읽으면 대비값.""" + wanted = _policy().get("list_default") or {} + key = str(wanted.get("filter") or "") + return {key: str(wanted.get("value") or ALL)} if key else dict(FALLBACK_DEFAULTS) def filters(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: - """거르기 상자 — 줄이 둘 이상으로 갈리는 가름만 · 기본은 `DEFAULTS`(그 값이 표에 있을 때) 아니면 「전부」.""" + """거르기 상자 — 값이 둘 이상 갈리는 가름만 · 기본은 **잣대**(`list_default`)가 정한다 — 그 값이 표에 있을 때만.""" facets = _facets(kind) labels = { "link": ("연결", {LINKED: "연결된 것만"}), @@ -231,7 +246,7 @@ def filters(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: if len(counts) < 2: # 다 같은 값이면 거를 것이 없음(연결은 「연결됨」 · 빈칸 둘로 갈림) continue label, names = labels[key] - default = DEFAULTS.get(key, ALL) + default = _defaults().get(key, ALL) out.append( { "key": key, diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index 2ef02421..2571ce98 100644 --- a/resources/data_master_labels/labels_2026-01-01.json +++ b/resources/data_master_labels/labels_2026-01-01.json @@ -1963,11 +1963,11 @@ "merged_columns": 176, "files": 41, "tables": 100, - "columns": 532, - "value_groups": 202, + "columns": 534, + "value_groups": 203, "unknown": 0, "value_groups_by_kind": { - "doc": 136, + "doc": 137, "value": 66 } }, @@ -7563,6 +7563,18 @@ "name_ko": "까닭", "unit": "", "visible": true + }, + { + "key": "list_label", + "name_ko": "목록에 뜰 이름", + "unit": "", + "visible": true + }, + { + "key": "hidden_by_default", + "name_ko": "기본으로 감추나", + "unit": "", + "visible": true } ] }, @@ -7685,6 +7697,12 @@ "name_ko": "가르는 차례", "summary": "총칙 → 공종 → 묶는 마디 → 빈 잎 차례로 본다.", "kind": "doc" + }, + { + "key": "list_default", + "name_ko": "목록 기본 거르기", + "summary": "목록을 열면 무엇만 보일지 — 기본은 공종만. 총칙은 감춘 것이지 지운 것이 아니다.", + "kind": "doc" } ] }, diff --git a/resources/data_work_item_master/axis_policy.json b/resources/data_work_item_master/axis_policy.json index 96a9f4b0..fda25dde 100644 --- a/resources/data_work_item_master/axis_policy.json +++ b/resources/data_work_item_master/axis_policy.json @@ -13,25 +13,33 @@ "work_item": { "name_ko": "공종", "rule": "품셈 표가 붙은 줄", - "keep": true + "keep": true, + "list_label": "공종", + "hidden_by_default": false }, "group": { "name_ko": "묶는 마디", "rule": "표는 없고 아래를 묶는 줄", "keep": true, - "why": "`parent_mode`(choose_one·sum_steps)가 여기 붙는다 — 계산 규칙을 지닌 줄이다." + "why": "`parent_mode`(choose_one·sum_steps)가 여기 붙는다 — 계산 규칙을 지닌 줄이다.", + "list_label": "묶는 마디", + "hidden_by_default": true }, "general_provision": { "name_ko": "총칙", "rule": "`general_provision` 뿌리 아래의 줄", "keep": true, - "why": "문서 구역이나 값표가 붙어 원가가 읽는다." + "why": "문서 구역이나 값표가 붙어 원가가 읽는다.", + "list_label": "공종 아님 · 규정·기준", + "hidden_by_default": true }, "empty": { "name_ko": "빈 잎", "rule": "표도 아래도 없는 잎", "keep": true, - "why": "원문에 표가 안 붙은 자리. 지우면 「있었는지 없었는지」가 안 남는다." + "why": "원문에 표가 안 붙은 자리. 지우면 「있었는지 없었는지」가 안 남는다.", + "list_label": "빈 잎", + "hidden_by_default": true } }, "decision_order": [ @@ -59,7 +67,10 @@ "group": 82, "general_provision": 78, "empty": 13 - } + }, + "known_defects": [ + "갈래 문자열이 그대로 줄 열쇠가 됨(`@id` = `FW-00105#1.1∼1.5`) — 품셈 원문이 물결표를 두 가지로 쓰고(∼ U+223C · ~ U+FF5E) 자간 공백만 다른 짝도 있어, 문자열이 손질되면 열쇠가 갈린다. ⬜ 갈래도 장부에 올릴지 판정 대기(2026-09-17 데스크탑 서브)." + ] }, "const": { "name_ko": "건설 공종", @@ -106,5 +117,10 @@ "note": "총칙 표시 전 셈(랩탑 메인 첫 산출). 뿌리를 붙이면 `general_provision` 으로 옮겨 간다 — 줄이 줄지는 않는다." } } + }, + "list_default": { + "filter": "axis_role", + "value": "work_item", + "why": "목록 기본은 **공종만**(산림 306 · 건설 983). 총칙 줄은 표가 붙어 있어 목록에 섞여 뜨는데, 공종이 아니라 규정·기준이다. 상자를 따로 만들지 않고 **기본 감춤 + 고르면 보임**으로 간다 (2026-09-17 브레인). ⚠ 지운 것이 아니라 감춘 것이다." } } diff --git a/resources/tester/test_work_item_key_registry.py b/resources/tester/test_work_item_key_registry.py index 42c85893..1ff904e4 100644 --- a/resources/tester/test_work_item_key_registry.py +++ b/resources/tester/test_work_item_key_registry.py @@ -252,6 +252,25 @@ def test_건설_총칙_뿌리가_실제로_붙음() -> None: assert 기계["axis_role"] != "general_provision" +def test_갈래_문자열이_열쇠라_흔들림을_못박음(master: dict) -> None: + """⚠ 갈래가 줄 열쇠에 그대로 들어간다(`@id` = `FW-00105#1.1∼1.5`). + + 품셈 원문이 물결표를 두 가지로 쓰고 자간 공백도 들쭉날쭉이라, 문자열을 손질하면 **열쇠가 갈린다.** + 지금 몇 개가 그런지 못박아 둔다 — 늘면 손질이 들어간 것이고, 줄면 열쇠가 바뀐 것이다. + """ + import collections + + keys = [k for it in master["work_items"] for k in (it.get("variant_keys") or [])] + assert len(keys) == 335 + tilde = collections.Counter(ch for k in keys for ch in k if ch in "∼~") + assert tilde == {"∼": 26, "~": 8}, "물결표 쓰임이 바뀜 — 열쇠가 갈렸는지 볼 것" + squeezed = collections.defaultdict(set) + for k in keys: + squeezed["".join(k.split()).replace("~", "∼")].add(k) + messy = {n: v for n, v in squeezed.items() if len(v) > 1} + assert len(messy) == 6, f"공백·물결표만 다른 짝이 {len(messy)} — 6 이던 것" + + def test_어느_구실도_지우지_않음() -> None: """브레인 승인(2026-09-17) — 「덜어내기」가 아니라 「가름만」.""" import json as _json diff --git a/resources/tester/test_z01_work_items.py b/resources/tester/test_z01_work_items.py index 5713d692..78b71d65 100644 --- a/resources/tester/test_z01_work_items.py +++ b/resources/tester/test_z01_work_items.py @@ -141,9 +141,11 @@ def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestCl "유지관리부문", ] assert sum(o["rows"] for o in division["options"][1:]) == const["total"] + # 2026-09-17 목록에서는 「총칙」이 아니라 **공종이 아님**을 드러내는 이름(잣대 `list_label`). assert {o["value"]: o["label"] for o in by_key["axis_role"]["options"]}[ "general_provision" - ] == "총칙" + ] == "공종 아님 · 규정·기준" + assert by_key["axis_role"]["default"] == "work_item" # 감춘 것이지 지운 것이 아님 civil = _get(client, "work_item_const", size=500, division="토목부문", axis_role="") assert civil["total"] == next( @@ -155,7 +157,9 @@ def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestCl ) assert rules["total"] > 0 assert all(r["name"].startswith("공통부문 › 적용기준") for r in rules["rows"]) - assert {r["axis_role"] for r in rules["rows"]} == {"총칙"} # 표시 칸 — 감추지 않고 보임 + role_names = {o["value"]: o["label"] for o in by_key["axis_role"]["options"]} + # 표시 칸 — 이름은 잣대(`list_label` · 「공종 아님 · 규정·기준」) 한 곳 + assert {r["axis_role"] for r in rules["rows"]} == {role_names["general_provision"]} assert [f["key"] for f in _get(client, "work_item_forest", size=1)["filters"]] == [ "link",