Files
Aislo/common_util/common_util_aliases.py
T
eomsangdonandClaude Opus 5 a08938ea1a refactor(aliases): 별칭표를 한 벌로 — 범위(scope)·판을 붙여 B08·B09 가 같은 파일만 읽음
- 정본 resources/data_aliases/aliases_2026-01-01.json · 로더 common_util/common_util_aliases.py
  (칸 빠짐·모르는 축·겹친 범위의 두 대상은 읽을 때 오류 · 판이 다르면 안 씀 · 양방향 조회)
- 흩어진 세 벌을 옮김: 매핑표 ground_aliases(리핑암→파쇄암·암절취) 삭제 ·
  B09 UnitPrice 전역 VARIANT_ALIASES 삭제 · B08 인계본 ground_class_aliases 는 별칭표에서 만든 보기
- 범위: 리핑암→파쇄암 @FP-10-11 · 리핑암→암절취 @FP-10-12 · 화약공→1016 @FP-09-05 (근거 문구 줄마다)
- 자원 축 별칭 검사도 같은 로더로 합침(자원 목록 폴더에서 별칭 파일을 옮김)
- 시험 test_common_aliases.py 6건 · 전체 1509 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-13 17:22:17 +09:00

119 lines
5.1 KiB
Python

"""별칭표 한 벌 — 우리 이름 ↔ 품셈·실무 이름 (2026-09-13 축 C 명세 5장·17장).
정본 파일 `resources/data_aliases/aliases_<판>.json`. **B08·B09 는 이 파일만 읽는다** — 매핑표·
단가 코드·인계본에 따로 두었더니 세 벌이 서로 달라졌다(리핑암 → 파쇄암·암절취 / 파쇄암만 / 안 읽음).
한 줄 = `{axis, from, to, scope, pum_edition, basis}`
axis `resource`(자원 이름 → 카탈로그 코드) · `variant`(우리 갈래 이름 → 품셈 갈래 이름)
scope 이 별칭이 **참인 `FP-*` 범위** — 없으면 오류. 별칭은 표 범위에서만 참이다
(「리핑암 = 파쇄암」은 10-11 f 표 안에서만 — 발파암도 캐고 나면 파쇄암 상태다)
pum_edition scope 가 묶인 품셈 판 — 판이 다르면 쓰지 않는다(`FP-*` 는 판 안에서만 유일)
⚠ **별칭 ≠ 대체** — 대응이 아예 없어 갈음하는 것은 여기 넣지 않는다(설계가 고른다).
⚠ 같은 이름이 **겹친 범위**에서 두 곳으로 가면 **읽을 때 오류** — 조용히 첫째를 고르지 않는다.
"""
from __future__ import annotations
import json
import re
from functools import lru_cache
from pathlib import Path
from typing import Any
ALIAS_FILE = (
Path(__file__).resolve().parents[1] / "resources" / "data_aliases" / "aliases_2026-01-01.json"
)
AXES = frozenset({"resource", "variant"})
_FIELDS = ("axis", "from", "to", "scope", "pum_edition")
class AliasError(ValueError):
"""별칭표가 규약을 어겼다 — 조용히 넘기지 않는다."""
def _key(text: str) -> str:
"""이름 비교 키 — 내부 공백만 지운다(「화 약 공」 = 「화약공」). 다른 글자는 안 건드린다."""
return re.sub(r"\s+", "", str(text or ""))
def in_scope(work_item_code: str, scope: str) -> bool:
"""`FP-*` 범위 — 그 코드 자신이거나 그 아래. ⚠ `AX`·`AR` 코드에는 계층이 없어 쓰지 않는다."""
return work_item_code == scope or work_item_code.startswith(scope + "-")
def parse_aliases(rows: list[dict[str, Any]]) -> list[dict[str, str]]:
"""줄을 검사해 돌려준다. 칸 빠짐 · 모르는 축 · 겹친 범위의 두 대상은 오류."""
aliases: list[dict[str, str]] = []
for row in rows:
picked = {key: str(row.get(key) or "").strip() for key in _FIELDS}
if not all(picked.values()):
raise AliasError(f"별칭 줄에 {'·'.join(_FIELDS)} 가 다 있어야 합니다: {row}")
if picked["axis"] not in AXES:
raise AliasError(f"모르는 별칭 축입니다: {picked['axis']}")
picked["basis"] = str(row.get("basis") or "")
aliases.append(picked)
for index, left in enumerate(aliases):
for right in aliases[index + 1 :]:
if (
left["axis"] == right["axis"]
and _key(left["from"]) == _key(right["from"])
and left["to"] != right["to"]
and left["pum_edition"] == right["pum_edition"]
and (
in_scope(left["scope"], right["scope"])
or in_scope(right["scope"], left["scope"])
)
):
raise AliasError(
f"같은 이름 「{left['from']}」이 겹친 범위에서 두 곳으로 갑니다: "
f"{left['to']}({left['scope']}) · {right['to']}({right['scope']})"
)
return aliases
@lru_cache(maxsize=4)
def _load(path: str) -> tuple[dict[str, str], ...]:
target = Path(path)
if not target.is_file():
return () # 파일이 없으면 별칭 없이 돈다 — 못 맞춘 줄로 드러난다
payload = json.loads(target.read_text(encoding="utf-8"))
return tuple(parse_aliases(list(payload.get("aliases") or [])))
def load_aliases(axis: str, path: Path | None = None) -> list[dict[str, str]]:
"""그 축의 별칭 줄. 파일은 한 번만 읽는다."""
return [dict(row) for row in _load(str(path or ALIAS_FILE)) if row["axis"] == axis]
def alias_target(
rows: list[dict[str, str]],
name: str,
work_item_code: str,
edition: str,
*,
reverse: bool = False,
) -> str | None:
"""범위·판이 맞는 줄에서 이름을 옮긴다. `reverse` 면 남의 이름 → 우리 이름(양방향 조회)."""
source, target = ("to", "from") if reverse else ("from", "to")
wanted = _key(name)
for row in rows:
if (
row["pum_edition"] == edition
and _key(row[source]) == wanted
and in_scope(work_item_code, row["scope"])
):
return row[target]
return None
def names_view(rows: list[dict[str, str]]) -> dict[str, dict[str, Any]]:
"""인계본 모양 `{우리 이름: {names, scopes, basis}}` — B08 `ground_class_aliases` 가 싣는다."""
view: dict[str, dict[str, Any]] = {}
for row in rows:
entry = view.setdefault(row["from"], {"names": [], "scopes": {}, "basis": ""})
entry["names"].append(row["to"])
entry["scopes"][row["to"]] = row["scope"]
entry["basis"] = " · ".join(part for part in (entry["basis"], row["basis"]) if part)
return view