Merge remote-tracking branch 'origin/sub_desktop_1' into main_laptop_1 — 총칙 기본 감춤 기준을 잣대 list_default 로(데스크탑 서브) 받음 · 줄 구실 표시 칸 이름도 잣대 list_label 을 따르게 시험 맞춤

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-17 17:19:08 +09:00
co-authored by Claude Opus 5
5 changed files with 92 additions and 20 deletions
+25 -10
View File
@@ -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,
@@ -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"
}
]
},
@@ -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.11.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 브레인). ⚠ 지운 것이 아니라 감춘 것이다."
}
}
@@ -252,6 +252,25 @@ def test_건설_총칙_뿌리가_실제로_붙음() -> None:
assert 기계["axis_role"] != "general_provision"
def test_갈래_문자열이_열쇠라_흔들림을_못박음(master: dict) -> None:
"""⚠ 갈래가 줄 열쇠에 그대로 들어간다(`@id` = `FW-00105#1.11.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
+6 -2
View File
@@ -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",