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
This commit is contained in:
2026-09-13 17:22:17 +09:00
co-authored by Claude Opus 5
parent 2ebcf89f04
commit a08938ea1a
8 changed files with 258 additions and 84 deletions
@@ -7,7 +7,7 @@
품셈 칸이 규격을 안 주면 「규격 미정 — 후보 N」으로 드러낸다.
② **형식이 하나뿐인 계열 이름** — 「공기압축기(3.5㎥/min)」 → 「공기압축기(이동식)」 3.5.
형식이 둘 이상이면 고르지 않는다(공압식·전기식은 다른 장비 · 판정 Ⓒ).
③ **범위 별칭** `{from, to, scope, pum_edition}` — 공종 코드 범위 안에서만 바꾼다(명세 5장).
③ **범위 별칭** — 별칭표 한 벌(`common_util_aliases`)의 `resource` 줄을 공종 범위 안에서만 쓴다.
scope 없는 줄 · 겹친 범위에서 두 코드로 가는 이름은 **읽을 때 오류**로 세운다.
⚠ 대체(대응이 아예 없어 갈음)는 별칭이 아니다 — 여기 넣지 않는다.
@@ -34,10 +34,10 @@ from B09_Estimation.B09_Estimation_ResourceAxis_Sources import (
parse_machine_cell,
split_name_and_spec,
)
from common_util.common_util_aliases import AliasError, in_scope, load_aliases, parse_aliases
_EXT_SUBPATH = ("resources", "data_resource_catalog")
EXT_CATALOG_FILE = "resource_catalog_ext_2026-01-01.json"
ALIASES_FILE = "aliases_2026-01-01.json"
#: 명세 §2 ③ — `AR-<M|L|X>-<8자리 소문자 16진>`. 종류 글자는 PriceKind 글자 그대로.
_RE_AR_CODE = re.compile(r"^AR-([MLX])-[0-9a-f]{8}$")
@@ -94,45 +94,17 @@ def load_ext_entries(file_name: str = EXT_CATALOG_FILE) -> list[CatalogEntry]:
return parse_ext_entries(list(_read_optional(file_name).get("entries") 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_scoped_aliases(rows: list[dict[str, Any]]) -> list[dict[str, str]]:
"""자원 축 별칭만 골라 검사한다. **scope 없는 줄 · 겹친 범위의 두 코드는 오류.**"""
aliases: list[dict[str, str]] = []
for row in rows:
if row.get("axis") != "resource":
continue
picked = {
key: str(row.get(key) or "").strip() for key in ("from", "to", "scope", "pum_edition")
}
if not all(picked.values()):
raise ResourceAxisError(
f"별칭 줄에 from·to·scope·pum_edition 이 다 있어야 합니다: {row}"
)
aliases.append(picked)
for index, left in enumerate(aliases):
for right in aliases[index + 1 :]:
if (
_normalize(left["from"]) == _normalize(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 ResourceAxisError(
f"같은 이름 「{left['from']}」이 겹친 범위에서 두 코드로 갑니다: "
f"{left['to']} · {right['to']}"
)
return aliases
"""자원 축 별칭만 — 검사(scope 없음 · 겹친 범위의 두 코드는 오류)는 별칭표 한 벌이 한다."""
try:
return [row for row in parse_aliases(rows) if row["axis"] == "resource"]
except AliasError as error:
raise ResourceAxisError(str(error)) from error
def load_scoped_aliases(file_name: str = ALIASES_FILE) -> list[dict[str, str]]:
return parse_scoped_aliases(list(_read_optional(file_name).get("aliases") or []))
def load_scoped_aliases() -> list[dict[str, str]]:
"""자원 축 별칭 — 정본은 `resources/data_aliases/`(B08·B09 한 벌)."""
return load_aliases("resource")
def apply_scoped_alias(catalog: ResourceCatalog, name_cell: str, work_item_code: str) -> str: