From a08938ea1abeca3f157012c1f32a264dd2395ad0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 13 Sep 2026 17:22:17 +0900 Subject: [PATCH] =?UTF-8?q?refactor(aliases):=20=EB=B3=84=EC=B9=AD?= =?UTF-8?q?=ED=91=9C=EB=A5=BC=20=ED=95=9C=20=EB=B2=8C=EB=A1=9C=20=E2=80=94?= =?UTF-8?q?=20=EB=B2=94=EC=9C=84(scope)=C2=B7=ED=8C=90=EC=9D=84=20?= =?UTF-8?q?=EB=B6=99=EC=97=AC=20B08=C2=B7B09=20=EA=B0=80=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=ED=8C=8C=EC=9D=BC=EB=A7=8C=20=EC=9D=BD=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정본 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) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- .../B08_Quantity_Engine_Handoff_Mapping.py | 8 +- .../B09_Estimation_ResourceAxis_Join.py | 48 ++----- B09_Estimation/B09_Estimation_UnitPrice.py | 32 +++-- common_util/common_util_aliases.py | 118 ++++++++++++++++++ .../data_aliases/aliases_2026-01-01.json | 31 +++++ .../aliases_2026-01-01.json | 15 --- .../work_item_mapping_2026-01-01.json | 13 +- resources/tester/test_common_aliases.py | 77 ++++++++++++ 8 files changed, 258 insertions(+), 84 deletions(-) create mode 100644 common_util/common_util_aliases.py create mode 100644 resources/data_aliases/aliases_2026-01-01.json delete mode 100644 resources/data_resource_catalog/aliases_2026-01-01.json create mode 100644 resources/tester/test_common_aliases.py diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py index dfadd8eb..085d21f6 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py @@ -205,6 +205,7 @@ class WorkItemMapping: pipe: dict[str, Any] = field(default_factory=dict) #: 갈래 이름 별칭 — 우리 「리핑암」 ↔ 일위대가 「파쇄암」처럼 **같은 것을 다른 이름**으로 #: 부르는 자리. ⚠ 갈래 이름 자체를 갈지 않는다(흙깎기 매핑이 그 이름으로 서 있다). + #: ⚠ 정본은 **별칭표 한 벌**(`common_util_aliases`)이다 — 여기는 인계본에 싣는 보기일 뿐. ground_aliases: dict[str, Any] = field(default_factory=dict) def declared_units(self) -> dict[str, str]: @@ -263,6 +264,11 @@ def load_mapping(path: Path | None = None) -> WorkItemMapping: if target is None or not target.is_file(): return WorkItemMapping() payload = json.loads(target.read_text(encoding="utf-8")) + from common_util.common_util_aliases import load_aliases, names_view + + # 별칭은 매핑표가 아니라 별칭표 한 벌에서 온다 — **매핑이 기대는 공종 마스터 판**의 줄만 싣는다. + edition = str((payload.get("master") or {}).get("effective_date") or "") + variant_aliases = [row for row in load_aliases("variant") if row["pum_edition"] == edition] return WorkItemMapping( effective_date=str(payload.get("effective_date") or ""), earthwork=list(payload.get("earthwork") or []), @@ -274,7 +280,7 @@ def load_mapping(path: Path | None = None) -> WorkItemMapping: concrete_placing=payload.get("concrete_placing") or {}, pipe=payload.get("pipe") or {}, unit_conversion=(payload.get("composite") or {}).get("unit_conversion") or {}, - ground_aliases=payload.get("ground_aliases") or {}, + ground_aliases={"aliases": names_view(variant_aliases)} if variant_aliases else {}, ) diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_Join.py b/B09_Estimation/B09_Estimation_ResourceAxis_Join.py index 443f17ab..778506f4 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_Join.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_Join.py @@ -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--<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: diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 237792c6..3e2780ce 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -461,23 +461,16 @@ def normalize_variant_key(text: str) -> str: return "".join("~" if ch in _TILDE_CHARS else ch for ch in tight) -#: 갈래 이름이 **축이 달라 다르게 불리는** 자리 — 이름을 갈지 않고 여기서 잇는다. -#: -#: ⚠ **우리 축과 품셈 축이 다르다.** 우리 「리핑암」은 B05·B06 의 **지반유형**(캘 수 있는가)이 -#: 낳은 이름이고, 품셈 운반표의 「파쇄암」은 **운반할 때의 상태**(부서졌는가)를 가리킨다. -#: 그래서 일반적으로는 「리핑암 = 파쇄암」이 아니다 — **발파암도 캐고 나면 파쇄암 상태**다 -#: (건설품셈 8장 「발파 또는 리퍼작업 등에 의하여 얻어진 암과 파쇄암…」). -#: -#: ⭐ **다만 이 표 안에서는 성립한다.** 산림품셈 10-11 f 표가 **토사·파쇄암·발파암을 따로** -#: 두었으므로 그 표의 「파쇄암」은 **발파를 뺀 나머지 = 리퍼로 얻은 것**이다. 결정적 증거는 -#: 10-12 [주]③ 이다 — 「적재 재료의 토량환산계수(L)는 토사 1.3, **암절취 1.35**, 발파암 -#: 1.625 적용한다」. 10-11 f 표의 **파쇄암이 1/1.35** 라 **암절취(리핑)과 같은 값**이다. -#: -#: ⚠ **어느 쪽 이름도 갈지 않는다** — 우리 이름을 갈면 B05·B06 이 깨지고, 품셈 이름을 갈면 -#: 원문과 어긋난다. 잇는 자리는 여기 한 곳뿐이다. -VARIANT_ALIASES: dict[str, str] = { - "리핑암": "파쇄암", -} +#: 갈래 이름이 **축이 달라 다르게 불리는** 자리(우리 「리핑암」 ↔ 품셈 10-11 f 「파쇄암」)는 +#: **별칭표 한 벌**(`common_util_aliases`, axis `variant`)에서 **범위째** 읽는다(2026-09-13 이관). +#: ⚠ 「리핑암 = 파쇄암」은 10-11 f 표 안에서만 참이다 — 발파암도 캐고 나면 파쇄암 상태라 전역으로 +#: 잇으면 틀린다(근거 문구는 별칭표 줄의 `basis` 에 옮겨 둠). 어느 쪽 이름도 갈지 않는다. + + +@lru_cache(maxsize=1) +def _master_edition() -> str: + """별칭 범위가 묶인 품셈 판 — 지금 공종 마스터의 판(명세 17장).""" + return str(load_work_item_master().get("effective_date", "")) def find_variant_code( @@ -494,7 +487,10 @@ def find_variant_code( wanted = normalize_variant_key(variant_value) if not wanted: return None - wanted = VARIANT_ALIASES.get(wanted, wanted) + from common_util.common_util_aliases import alias_target, load_aliases + + alias = alias_target(load_aliases("variant"), wanted, work_item_code, _master_edition()) + wanted = normalize_variant_key(alias) if alias else wanted prefix = f"B-{work_item_code}#" candidates = [code for code in prices.book.titles if code.startswith(prefix)] diff --git a/common_util/common_util_aliases.py b/common_util/common_util_aliases.py new file mode 100644 index 00000000..71484ea5 --- /dev/null +++ b/common_util/common_util_aliases.py @@ -0,0 +1,118 @@ +"""별칭표 한 벌 — 우리 이름 ↔ 품셈·실무 이름 (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 diff --git a/resources/data_aliases/aliases_2026-01-01.json b/resources/data_aliases/aliases_2026-01-01.json new file mode 100644 index 00000000..22ba4693 --- /dev/null +++ b/resources/data_aliases/aliases_2026-01-01.json @@ -0,0 +1,31 @@ +{ + "schema_version": "1.0", + "dataset_id": "aliases", + "note": "별칭표 한 벌(명세 5장) — B08·B09 가 이 파일만 읽는다(`common_util/common_util_aliases.py`). 한 줄 = {axis, from, to, scope, pum_edition, basis}. axis: resource(자원 이름 → 카탈로그 코드) · variant(우리 갈래 이름 → 품셈 갈래 이름). scope 없는 줄은 오류 · 같은 이름이 겹친 범위에서 두 곳으로 가면 오류. 대체(대응이 아예 없어 갈음)는 넣지 않음. scope 는 품셈 판에 묶임 — 판이 다르면 적용 안 함(명세 17장).", + "aliases": [ + { + "axis": "resource", + "from": "화약공", + "to": "1016", + "scope": "FP-09-05", + "pum_edition": "2026-01-01", + "basis": "노임 표준 직종명은 「화약취급공」(노임 카탈로그 1016) — 품셈 9-5 표가 줄여 적음. 2026-09-13 사용자 위임 판정(브레인)" + }, + { + "axis": "variant", + "from": "리핑암", + "to": "파쇄암", + "scope": "FP-10-11", + "pum_edition": "2026-01-01", + "basis": "품셈 10-11 f 표가 토사·파쇄암·발파암을 따로 둠 — 그 표 안에서만 파쇄암 = 발파를 뺀 리퍼 암. 결정적 근거 10-12 [주]③ 환산계수 「암절취 1.35」 = 10-11 f 「파쇄암 1/1.35」. ⚠ 일반적으로는 발파암도 캐면 파쇄암 상태라 전역 별칭 금지. 도자 운반 일위대가가 파쇄암 이름으로 서 있어 이름만 못 이어 금액이 안 붙던 자리(2026-09-08 데스크탑 보조 원문 대조)" + }, + { + "axis": "variant", + "from": "리핑암", + "to": "암절취", + "scope": "FP-10-12", + "pum_edition": "2026-01-01", + "basis": "품셈 10-12 덤프 운반 잎이 토사·암절취·발파암으로 갈리고 [주]③ 환산계수가 「암절취 1.35」(10-11 f 파쇄암 1/1.35 와 같은 값) — 우리 리핑암(리퍼 절취) = 품셈 암절취. 옛 매핑표 ground_aliases 에서 범위를 붙여 옮김(2026-09-13). ⚠ 쓰는 곳: 덤프 운반 잎 고르기(아직 없음 — 갈래 고르기형 부모 판정 대기)" + } + ] +} diff --git a/resources/data_resource_catalog/aliases_2026-01-01.json b/resources/data_resource_catalog/aliases_2026-01-01.json deleted file mode 100644 index a17b202b..00000000 --- a/resources/data_resource_catalog/aliases_2026-01-01.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "schema_version": "1.0", - "dataset_id": "aliases", - "note": "별칭 한 벌(명세 5장). 한 줄 = {axis, from, to, scope, pum_edition, basis}. scope 없는 줄은 오류 · 같은 이름이 겹친 범위에서 두 코드로 가면 오류. 대체(대응이 아예 없어 갈음)는 여기 넣지 않음. scope 는 품셈 판에 묶임 — 판이 다르면 적용 안 함(명세 17장).", - "aliases": [ - { - "axis": "resource", - "from": "화약공", - "to": "1016", - "scope": "FP-09-05", - "pum_edition": "2026-01-01", - "basis": "노임 표준 직종명은 「화약취급공」(노임 카탈로그 1016) — 품셈 9-5 표가 줄여 적음. 2026-09-13 사용자 위임 판정(브레인)" - } - ] -} diff --git a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json index d8076881..c81f89d6 100644 --- a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json +++ b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json @@ -393,16 +393,5 @@ "터파기·되메우기": "⚠ 관 부설과 터파기·되메우기가 각각 오면 **같은 굴착을 두 번 셀 수 있다**(B09 ㉡ 가드). 관 줄에는 지금 터파기를 붙이지 않는다." } }, - "ground_aliases": { - "note": "우리 갈래 이름 ↔ 일위대가·품셈이 쓰는 다른 이름. **값을 바꾸지 않고 이름만 잇는다** — 갈래 이름을 갈아 버리면 흙깎기(FP-09-04 리핑암) 매핑이 어긋난다.", - "aliases": { - "리핑암": { - "names": [ - "파쇄암", - "암절취" - ], - "basis": "품셈 10-11 f 표가 「파쇄암 1/1.35」이고 10-12 [주]③ 환산계수가 「암절취 1.35」로 **같은 값**이다 ⇒ 암절취(리핑) = 파쇄암. 우리 일위대가에는 도자 운반이 「토사 1,711.9 · 파쇄암 3,911.3 · 발파암 4,708.1원/㎥」로 파쇄암 이름으로 서 있어, 이름만 못 이어 금액이 안 붙던 자리다(2026-09-08 데스크탑 보조 원문 대조)." - } - } - } + "ground_aliases_moved_to": "resources/data_aliases/aliases_2026-01-01.json — 갈래 이름 별칭은 범위(scope)·판을 붙여 별칭표 한 벌로 옮김(2026-09-13 축 C 명세 5장). 여기 다시 두지 말 것." } diff --git a/resources/tester/test_common_aliases.py b/resources/tester/test_common_aliases.py new file mode 100644 index 00000000..9d88c226 --- /dev/null +++ b/resources/tester/test_common_aliases.py @@ -0,0 +1,77 @@ +"""별칭표 한 벌 (2026-09-13 축 C 명세 5장·17장 · `common_util_aliases`). + +지키는 것 + ① 정본 파일의 모든 줄에 scope·pum_edition 이 있다 + ② 별칭은 **범위 안·같은 판**에서만 옮긴다 — 「리핑암 → 파쇄암」은 10-11 에서만 + ③ 양방향 조회 — 품셈 이름으로도 우리 이름을 찾는다 + ④ scope 없음 · 모르는 축 · 겹친 범위의 두 대상은 읽을 때 오류 + ⑤ B09 갈래 고르기와 B08 인계본이 **같은 파일**을 읽는다 +""" + +from __future__ import annotations + +import json + +import pytest + +from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping +from common_util.common_util_aliases import ( + ALIAS_FILE, + AliasError, + alias_target, + load_aliases, + parse_aliases, +) + +ROW = { + "axis": "variant", + "from": "리핑암", + "to": "파쇄암", + "scope": "FP-10-11", + "pum_edition": "2026-01-01", +} + + +def test_정본_파일_모든_줄에_범위와_판(): + rows = parse_aliases(json.loads(ALIAS_FILE.read_text(encoding="utf-8"))["aliases"]) + assert rows + assert all(row["scope"].startswith("FP-") and row["pum_edition"] for row in rows) + + +def test_범위_안_같은_판에서만_옮긴다(): + rows = load_aliases("variant") + assert alias_target(rows, "리핑암", "FP-10-11", "2026-01-01") == "파쇄암" + assert alias_target(rows, "리 핑 암", "FP-10-12-02", "2026-01-01") == "암절취" + assert alias_target(rows, "리핑암", "FP-09-19-02", "2026-01-01") is None + assert alias_target(rows, "리핑암", "FP-10-11", "2027-01-01") is None + assert alias_target(rows, "발파암", "FP-10-11", "2026-01-01") is None + + +def test_양방향_조회(): + rows = load_aliases("variant") + assert alias_target(rows, "파쇄암", "FP-10-11", "2026-01-01", reverse=True) == "리핑암" + assert alias_target(rows, "파쇄암", "FP-10-12", "2026-01-01", reverse=True) is None + + +def test_규약_어기면_읽을_때_오류(): + with pytest.raises(AliasError): + parse_aliases([{**ROW, "scope": ""}]) + with pytest.raises(AliasError): + parse_aliases([{**ROW, "axis": "ground"}]) + with pytest.raises(AliasError): + parse_aliases([ROW, {**ROW, "to": "암절취", "scope": "FP-10"}]) + # 범위가 안 겹치거나 판이 다르면 같은 이름이 다른 곳으로 가도 된다 + assert len(parse_aliases([ROW, {**ROW, "to": "암절취", "scope": "FP-10-12"}])) == 2 + assert len(parse_aliases([ROW, {**ROW, "to": "암절취", "pum_edition": "2027-01-01"}])) == 2 + + +def test_B08_인계본은_범위째_싣는다(): + view = load_mapping().ground_aliases["aliases"]["리핑암"] + assert view["scopes"] == {"파쇄암": "FP-10-11", "암절취": "FP-10-12"} + + +def test_B09_갈래_고르기가_별칭표를_읽는다(): + from B09_Estimation.B09_Estimation_UnitPrice import find_variant_code + + assert find_variant_code("FP-10-11", "리핑암") == "B-FP-10-11#파쇄암" + assert find_variant_code("FP-10-11", "발파암") == "B-FP-10-11#발파암"