Merge remote-tracking branch 'origin/dev' into sub_laptop_1

This commit is contained in:
2026-09-15 19:28:12 +09:00
4 changed files with 615 additions and 191 deletions
+55 -10
View File
@@ -6,6 +6,7 @@
표 = **화면이 표로 그릴 마디** — 이름표 생성기와 같은 잣대(브레인 「91 이 정본」):
· 딕셔너리 목록(비지 않음) → 줄 = 딕셔너리 그대로({열key: 값})
· 딕셔너리 둘 이상이 모인 딕셔너리(열이 비슷함) → 줄마다 `@key` 열(그 줄의 이름) + 값 딕셔너리
· 표 아닌 마디(값 묶음 — 산재 3.56% 같은 낱값)는 파일마다 표 하나 `@values`(이름 · 값) — 설명·방침은 뺌(브레인 ④)
⚠ 표 id 가 이름표와 한 글자라도 다르면 이름이 통째로 안 붙음 — `test_z01_master_data.py` 가 양쪽으로 셈.
"""
@@ -22,6 +23,18 @@ LABEL_DIR = RESOURCES / "data_master_labels"
DEFAULT_PAGE_SIZE = 50
MAX_PAGE_SIZE = 500
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")
# 파일 머리 — 표도 값 묶음도 아님(이름표 생성기와 같은 목록)
META_KEYS = {
@@ -57,7 +70,9 @@ def _has_table(node: Any, depth: int = 0) -> bool:
return any(_has_table(v, depth + 1) for v in node.values())
def _collect(node: Any, path: list[str], out: dict[str, list], depth: int = 0) -> None:
def _collect(
node: Any, path: list[str], out: dict[str, list], values: dict, depth: int = 0
) -> None:
shape = _is_table(node)
if shape == "list":
out["/".join(path)] = node
@@ -65,22 +80,45 @@ def _collect(node: Any, path: list[str], out: dict[str, list], depth: int = 0) -
out["/".join(path)] = [{ROW_KEY: k, **v} for k, v in node.items()]
elif depth <= 5 and isinstance(node, dict) and any(_has_table(v) for v in node.values()):
for key, value in node.items():
_collect(value, [*path, key], out, depth + 1)
_collect(value, [*path, key], out, values, depth + 1)
else:
values["/".join(path)] = node
@lru_cache(maxsize=64)
def _tables_of(path: str, _mtime_ns: int) -> dict[str, list[dict[str, Any]]]:
def _scan(path: str, _mtime_ns: int) -> tuple[dict[str, list[dict[str, Any]]], dict[str, Any]]:
doc = json.loads(Path(path).read_text(encoding="utf-8"))
out: dict[str, list[dict[str, Any]]] = {}
values: dict[str, Any] = {}
for key, value in doc.items():
if key not in META_KEYS:
_collect(value, [key], out)
return out
_collect(value, [key], out, values)
return out, values
def tables_of(path: Path) -> dict[str, list[dict[str, Any]]]:
"""표 id(이름표의 표 key · `/` 로 이은 자리) → 줄."""
return _tables_of(str(path), path.stat().st_mtime_ns)
return _scan(str(path), path.stat().st_mtime_ns)[0]
def values_of(path: Path) -> dict[str, Any]:
"""값 묶음 자리(이름표의 값 묶음 key) → 값 — 설명·방침도 섞임."""
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."""
rows = []
for key, value in values_of(path).items():
if is_description(key):
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})
return rows
@lru_cache(maxsize=4)
@@ -114,14 +152,18 @@ def tree() -> dict[str, Any]:
named = {t["key"]: t.get("name_ko") or "" for t in entry.get("tables") or []}
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))
if value_count:
counts[VALUES_TABLE] = value_count
group["files"].append(
{
"id": entry["file_id"],
"label": entry.get("name_ko") or entry["file_id"],
"key": path.name,
"tables": [
{"id": tid, "label": named.get(tid) or tid, "key": tid, "row_count": len(rows)}
for tid, rows in tables_of(path).items()
{"id": tid, "label": named.get(tid) or tid, "key": tid, "row_count": n}
for tid, n in counts.items()
],
}
)
@@ -135,7 +177,10 @@ def rows(
labels = load_labels()
entry = next((f for f in labels["files"] if f["file_id"] == fid), None)
path = _file_path(entry) if entry else None
table = tables_of(path).get(tid) if path else None
if path and tid == VALUES_TABLE:
table = _value_rows(fid, path, labels) or None
else:
table = tables_of(path).get(tid) if path else None
if table is None:
return None
keys: dict[str, None] = {}
@@ -157,7 +202,7 @@ def rows(
"key": key,
"label": named.get("name_ko") or key,
"unit": named.get("unit") or "",
"hidden": named.get("visible") is False,
"hidden": named.get("visible") is False if named else key in _CODE_HIDDEN,
}
)
return {"columns": columns, "rows": hits[start : start + size], "total": len(hits)}