diff --git a/Z01_MasterData/Z01_MasterData_Tables.py b/Z01_MasterData/Z01_MasterData_Tables.py index 852cbb95..55b94e2e 100644 --- a/Z01_MasterData/Z01_MasterData_Tables.py +++ b/Z01_MasterData/Z01_MasterData_Tables.py @@ -26,14 +26,13 @@ ROW_KEY = "@key" VALUES_TABLE = "@values" _CODE_HIDDEN = {"@path"} # 낱값 표의 영문 자리 — 이름표에 없을 때만 -# ⚠ 잠정 — 이름표가 값 묶음마다 「값이냐 설명이냐」 표시를 주면 **이 자리만** 갈아끼움(브레인 ④) -_DESCRIPTION_KEYS = {"note", "notes", "policy", "source", "sources", "quote", "why"} - - -def is_description(group_key: str) -> bool: - """값 묶음이 설명·방침인가(값이 아님) — 끝 조각으로 가름.""" - last = group_key.rsplit("/", 1)[-1] - return last in _DESCRIPTION_KEYS or last.endswith("_note") +# 값 묶음 갈래 — 이름표 `value_groups[].role`(데스크탑 서브) · 낱말이 바뀌면 이 자리만 갈아끼움 +ROLE_FIELD = "role" +SHOWN_ROLE = "value" # 원가·수량에 드는 값 — 낱값 표에 보임 +HIDDEN_ROLE = "note" # 읽으라고 적은 글 — 안 보임 +# 이름표가 안 가른(또는 모르는 낱말) 마디 — 조용히 빼지도 섞지도 않고 값으로 보이되 드러냄(브레인) +UNDECIDED_COLUMN = "@undecided" +UNDECIDED = "미판정" # 파일 머리 — 표도 값 묶음도 아님(이름표 생성기와 같은 목록) @@ -106,18 +105,24 @@ def values_of(path: Path) -> dict[str, Any]: return _scan(str(path), path.stat().st_mtime_ns)[1] -def _value_rows(fid: str, path: Path, labels: dict[str, Any]) -> list[dict[str, Any]]: - """낱값 표 줄 — 찾는 차례: 「파일id::key」 → 「끝 조각」 → 영문 key.""" +def _value_rows(entry: dict[str, Any], path: Path, labels: dict[str, Any]) -> list[dict[str, Any]]: + """낱값 표 줄 — 이름 찾는 차례: 「파일id::key」 → 「끝 조각」 → 영문 key.""" + fid = entry["file_id"] + roles = {g.get("key"): g.get(ROLE_FIELD) for g in entry.get("value_groups") or []} rows = [] for key, value in values_of(path).items(): - if is_description(key): + role = roles.get(key) + if role == HIDDEN_ROLE: continue named = ( (labels.get("value_overrides") or {}).get(f"{fid}::{key}") or (labels.get("value_keys") or {}).get(key.rsplit("/", 1)[-1]) or {} ) - rows.append({"@name": named.get("name_ko") or key, "@value": value, "@path": key}) + row = {"@name": named.get("name_ko") or key, "@value": value, "@path": key} + if role != SHOWN_ROLE: + row[UNDECIDED_COLUMN] = UNDECIDED + rows.append(row) return rows @@ -153,7 +158,7 @@ def tree() -> dict[str, Any]: kind = entry.get("kind") or "" group = groups.setdefault(kind, {"key": kind, "label": kind, "files": []}) counts = {tid: len(rows) for tid, rows in tables_of(path).items()} - value_count = len(_value_rows(entry["file_id"], path, labels)) + value_count = len(_value_rows(entry, path, labels)) if value_count: counts[VALUES_TABLE] = value_count group["files"].append( @@ -178,11 +183,13 @@ def rows( entry = next((f for f in labels["files"] if f["file_id"] == fid), None) path = _file_path(entry) if entry else None if path and tid == VALUES_TABLE: - table = _value_rows(fid, path, labels) or None + table = _value_rows(entry, path, labels) or None else: table = tables_of(path).get(tid) if path else None if table is None: return None + own = next((t for t in entry.get("tables") or [] if t.get("key") == tid), {}) + own_columns = {c.get("key"): c for c in own.get("columns") or []} keys: dict[str, None] = {} for r in table: keys.update(dict.fromkeys(r)) @@ -195,8 +202,13 @@ def rows( size = max(1, min(size, MAX_PAGE_SIZE)) start = (max(page, 1) - 1) * size columns = [] - for key in keys: # 찾는 차례: 「파일id/열key」 → 「열key」 → 영문 key - named = labels["column_overrides"].get(f"{fid}/{key}") or labels["columns"].get(key) or {} + for key in keys: # 찾는 차례: 그 표 열 목록(줄 이름 열 `@key` 는 표마다 제 이름) → 「파일id/열key」 → 「열key」 → 영문 key + named = ( + own_columns.get(key) + or labels["column_overrides"].get(f"{fid}/{key}") + or labels["columns"].get(key) + or {} + ) columns.append( { "key": key, diff --git a/resources/tester/test_z01_master_data.py b/resources/tester/test_z01_master_data.py index 7b5d77ef..73f28243 100644 --- a/resources/tester/test_z01_master_data.py +++ b/resources/tester/test_z01_master_data.py @@ -107,8 +107,10 @@ def test_묶음_표는_줄마다_이름_열(client: TestClient) -> None: assert slope["rows"][0]["성토"] == [0.3, 0.35, 0.4, 0.45, 0.5] cols = {c["key"]: c for c in slope["columns"]} assert cols["성토"]["label"] == "성토부 경사" and cols["성토"]["unit"] == "1:n" + assert cols["@key"]["label"] == "쌓기 방식" # 줄 이름 열은 표마다 제 이름(이름표 표 열 목록) fx = {c["key"]: c for c in _rows(client, file="fx", table="variables")["columns"]} assert fx["value"]["label"] == "환율" and fx["value"]["unit"] == "원" # 「fx/value」 덮어쓰기 + assert fx["@key"]["label"] == "통화" def test_숨김은_이름표의_보임에서(client: TestClient) -> None: @@ -167,8 +169,8 @@ def test_찾는_차례와_없는_이름은_영문_key( ) -def test_낱값은_파일마다_표_하나_설명류는_뺌(client: TestClient) -> None: - """브레인 ④ — 표 아닌 마디(값 묶음)도 트리에 `@values` 한 마디로 · 설명·방침은 값이 아님.""" +def test_낱값은_파일마다_표_하나_설명은_뺌(client: TestClient) -> None: + """브레인 ④ — 표 아닌 마디(값 묶음)도 트리에 `@values` 한 마디로 · 값/설명 가름은 이름표 `role`.""" files = _files(client) rates = files["rates"]["tables"] assert rates[-1]["id"] == "@values" @@ -183,20 +185,57 @@ def test_낱값은_파일마다_표_하나_설명류는_뺌(client: TestClient) "@value": False, "@path": True, } - master = {r["@path"] for r in _rows(client, file="work_item_master", table="@values")["rows"]} - assert master == {"stats"} # policy 는 방침 - assert "@values" not in {t["id"] for t in files["aliases"]["tables"]} # note 뿐 - assert "@values" not in {t["id"] for t in files["fx"]["tables"]} # 값 묶음 없음 - for f in files.values(): - if "@values" in {t["id"] for t in f["tables"]}: - paths = [ - r["@path"] for r in _rows(client, file=f["id"], table="@values", size=500)["rows"] - ] - assert not [p for p in paths if tables.is_description(p)], f["id"] + assert "@values" not in { + t["id"] for t in files["work_item_master"]["tables"] + } # 방침·집계 = 설명 + assert "@values" not in {t["id"] for t in files["fx"]["tables"]} # 값 묶음 없음 — 줄이 곧 값 + for entry in LABELS["files"]: # 보이는 것 = 이름표가 「값」 이라 한 것 그대로 + want = [g["key"] for g in entry["value_groups"] if g["role"] == "value"] + got = ( + _rows(client, file=entry["file_id"], table="@values", size=500)["rows"] + if "@values" in {t["id"] for t in files[entry["file_id"]]["tables"]} + else [] + ) + assert sorted(r["@path"] for r in got) == sorted(want), entry["file_id"] hit = _rows(client, file="rates", table="@values", q="산재") assert [r["@path"] for r in hit["rows"]] == ["variables/rate_sanjae"] +def test_이름표가_값_설명을_안_가른_마디는_미판정으로_보임( + client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """브레인 — 조용히 값으로 보이면 설명이 섞이고, 조용히 빼면 원가에 드는 값이 사라짐 → 값으로 보이되 드러나게.""" + entry = next(f for f in LABELS["files"] if f["file_id"] == "rates") + lagging = [dict(g) for g in entry["value_groups"]] + for g in lagging: + if g["key"] in ("variables/rate_vat", "variables/rate_goyong/base"): + del g["role"] # 이름표가 뒤처짐 + elif g["key"] == "variables/rate_care": + g["role"] = "formula" # 코드가 모르는 낱말도 미판정 + (tmp_path / "labels_2026-01-01.json").write_text( + json.dumps({**LABELS, "files": [{**entry, "value_groups": lagging}]}, ensure_ascii=False), + encoding="utf-8", + ) + monkeypatch.setattr(tables, "LABEL_DIR", tmp_path) + values = _rows(client, file="rates", table="@values", size=500) + marks = {r["@path"]: r.get(tables.UNDECIDED_COLUMN) for r in values["rows"]} + undecided = {"variables/rate_vat", "variables/rate_goyong/base", "variables/rate_care"} + assert {p for p, m in marks.items() if m} == undecided + assert {marks[p] for p in undecided} == {tables.UNDECIDED} + assert tables.UNDECIDED_COLUMN in {c["key"] for c in values["columns"]} + assert not next(c for c in values["columns"] if c["key"] == tables.UNDECIDED_COLUMN)["hidden"] + + +def test_진짜_이름표엔_미판정이_없음() -> None: + undecided = [ + f"{entry['file_id']}::{r['@path']}" + for entry in LABELS["files"] + for r in tables._value_rows(entry, tables.ROOT / entry["path"], LABELS) + if r.get(tables.UNDECIDED_COLUMN) + ] + assert undecided == [], undecided + + def test_값_묶음_자리는_이름표와_양쪽으로_같음() -> None: not_labelled, label_only = [], [] for entry in LABELS["files"]: @@ -207,7 +246,7 @@ def test_값_묶음_자리는_이름표와_양쪽으로_같음() -> None: assert not_labelled == [] and label_only == [], (not_labelled, label_only) for entry in LABELS["files"]: # 이름도 이름표가 푼 그대로(찾는 차례 「파일id::key」 → 끝 조각) named = {g["key"]: g["name_ko"] for g in entry["value_groups"]} - rows = tables._value_rows(entry["file_id"], tables.ROOT / entry["path"], LABELS) + rows = tables._value_rows(entry, tables.ROOT / entry["path"], LABELS) assert [r["@name"] for r in rows] == [named[r["@path"]] for r in rows], entry["file_id"]