Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_desktop_1' into sub_laptop_1

This commit is contained in:
2026-09-15 19:28:13 +09:00
4 changed files with 309 additions and 217 deletions
+28 -16
View File
@@ -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,
File diff suppressed because it is too large Load Diff
+24 -10
View File
@@ -330,26 +330,40 @@ def test_줄_이름_열의_이름이_표마다_제_것이다(labels):
assert duplicated <= {"출처 키"}, f"줄 이름이 겹친다: {sorted(duplicated)}"
def test_값_묶음마다_값이냐_설명이냐가_적혀_있다(labels):
known = set(labels["value_roles"])
def test_값_묶음마다_kind_가_낱말로_적혀_있다(labels):
known = set(labels["value_kinds"])
bad = []
for entry in labels["files"]:
for group in entry["value_groups"]:
if group.get("role") not in known:
if group.get("kind") not in known:
bad.append(f"{entry['file_id']}::{group['key']}{group.get('role')!r}")
assert not bad, f"값·설명이 안 갈린 값 묶음: {bad}"
def test_값_갈래_잣대가_이름표에_적혀_있다(labels):
rule = labels["policy"]["value_role_rule"]
def test_값_갈래_잣대와_낱값_마디가_적혀_있다(labels):
rule = labels["policy"]["value_kind_rule"]
assert "금액이 움직이나" in rule
assert labels["policy"]["row_key_column"]
node = labels["synthetic_tables"]["@values"]
assert node["name_ko"] == "낱값"
assert node["summary"] == "표가 아닌 한 값들"
assert [c["key"] for c in node["columns"]] == ["@name", "@value", "@path"]
assert [c["name_ko"] for c in node["columns"]] == ["항목", "", "원문 자리"]
assert node["columns"][2]["visible"] is False
def test_값_갈래는_참거짓이_아니라_낱말이다(labels):
"""수식 같은 갈래가 늘면 낱말만 더하면 된다 — 참·거짓이면 그때 뜻이 뒤집힌다."""
for entry in labels["files"]:
for group in entry["value_groups"]:
assert isinstance(group["kind"], str), f"{entry['file_id']}::{group['key']}"
assert set(labels["value_kinds"]) == {"value", "doc"}
def test_한_값짜리_요율은_값으로_선다(labels):
"""산재 3.56 % 처럼 원가에 그대로 드는 요율이 설명으로 숨으면 트리에서 사라진다."""
rates = next(f for f in labels["files"] if f["file_id"] == "rates")
by_key = {g["key"]: g["role"] for g in rates["value_groups"]}
by_key = {g["key"]: g["kind"] for g in rates["value_groups"]}
for key in (
"variables/rate_sanjae",
"variables/rate_health",
@@ -360,12 +374,12 @@ def test_한_값짜리_요율은_값으로_선다(labels):
"variables/rate_asbestos_contribution",
):
assert by_key.get(key) == "value", f"{key} 가 값으로 안 섰다"
assert by_key.get("processing_rules") == "note"
assert by_key.get("processing_rules") == "doc"
def test_세어_둔_갈래_수가_실제와_같다(labels):
def test_세어_둔_값_갈래_수가_실제와_같다(labels):
counted: dict[str, int] = {}
for entry in labels["files"]:
for group in entry["value_groups"]:
counted[group["role"]] = counted.get(group["role"], 0) + 1
assert counted == labels["counts"]["value_groups_by_role"]
counted[group["kind"]] = counted.get(group["kind"], 0) + 1
assert counted == labels["counts"]["value_groups_by_kind"]
+52 -13
View File
@@ -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"]