feat(B09): 갈래 키를 이쪽에서 만듦 — 물결표 통일 + 구간 판정
두 창 합의 — B08 은 **의미**만 보내고(`work_item_code` + 저장 제원 원본값), 품셈 원문 표기를 키로 옮기는 것은 **원문을 읽는 이쪽 몫** - 품셈이 물결표를 두 종류로 섞어 씀(`∼` U+223C 26건 · `~` U+FF5E 8건) — 같은 뜻인데 키가 두 벌이었음. 키에서만 한 종류로 모으고 **원문 문구는 이름에 보존** - ⚠ 규칙은 둘뿐 — **내부 공백 제거 + 물결표 통일**. 키에 쓰인 글자를 세어 그 밖에는 소수점·괄호뿐임을 확인(하이픈·곱셈표 없음) - 「60~80」은 「직경60㎝이상~80㎝미만」 **안에 없어**(사이에 「㎝이상」이 낌) 글자 포함으로는 못 맞춤 → **수의 짝**으로 견줌 - 저장 제원이 한 값으로 오는 경우(뒷길이 45㎝)는 **그 값을 담는 가장 좁은 구간**을 고름 — 45 → `#55cm이하`, 25 → `#35cm이하` - ⚠ 담을 갈래가 없으면 `None` — **가까운 것을 임의로 고르지 않음**(95 → 없음) - 조판이 `variant_axis`·`variant_value` 를 받아 코드를 고르고, 못 고르면 종전대로 후보를 보임 검증: pytest 198 통과(신규 5 — 짝 시험 포함) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,11 @@ from B09_Estimation.B09_Estimation_Guards import (
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
|
||||
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import (
|
||||
UnitPriceBuild,
|
||||
cached_build,
|
||||
find_variant_code,
|
||||
)
|
||||
|
||||
_ZERO = Decimal(0)
|
||||
|
||||
@@ -84,6 +88,9 @@ class HandoffWorkItem:
|
||||
#: 막힘 갈래 — `input_missing`(사용자가 입력하면 풀림) /
|
||||
#: `unit_data_missing`·`formula_missing`(우리가 만들어야 함). 할 일이 다르므로 가른다.
|
||||
blocked_kind: str = ""
|
||||
#: 갈래 축·원본값 — 「stone_cm」·「60~80」. **키 문자열은 우리가 만든다**(두 창 합의).
|
||||
variant_axis: str = ""
|
||||
variant_value: str = ""
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
@@ -227,6 +234,8 @@ def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[
|
||||
structure_kind=row.get("structure_kind") or "",
|
||||
blocked_reason=row.get("blocked_reason") or "",
|
||||
blocked_kind=row.get("blocked_kind") or "",
|
||||
variant_axis=row.get("variant_axis") or "",
|
||||
variant_value=str(row.get("variant_value") or ""),
|
||||
)
|
||||
for row in payload["work_items"]
|
||||
]
|
||||
@@ -587,6 +596,14 @@ def _leaf_row(
|
||||
return row
|
||||
|
||||
price_code = f"B-{node.code}"
|
||||
if item.variant_value and price_code not in unit_prices.book.titles:
|
||||
# B08 은 **의미**(어느 공종·어느 제원)만 보내고 갈래 키는 우리가 만든다.
|
||||
# 못 맞추면 후보를 보이는 길로 내려간다 — 가까운 갈래를 임의로 고르지 않는다.
|
||||
picked = find_variant_code(node.code, item.variant_value, unit_prices)
|
||||
if picked is not None:
|
||||
price_code = picked
|
||||
row.spec = f"{row.spec} {item.variant_value}".strip()
|
||||
|
||||
if price_code not in unit_prices.book.titles:
|
||||
# 한 층 아래에 일위대가가 있으면 **후보로 보여준다** — 임의로 고르지 않는다
|
||||
# (CLAUDE.md 3장 「미결 항목 임의 확정 금지」, B08 `mapping_pending_user` 와 같은 태도).
|
||||
|
||||
@@ -224,6 +224,81 @@ def load_basis_missing(
|
||||
}
|
||||
|
||||
|
||||
#: 품셈 원문이 섞어 쓰는 물결표 — 「직경40㎝이상∼60㎝미만」(U+223C)과
|
||||
#: 「직경40㎝이상~60㎝미만」(U+FF5E)이 **같은 뜻인데 키가 두 벌**이었다(2026-09-08 실측:
|
||||
#: 13-6-1·2 는 ∼, 13-6-3 은 ~). 키에서만 한 종류로 모으고 **원문 문구는 이름에 보존**한다.
|
||||
#: ⚠ 규칙은 둘뿐이다 — **내부 공백 제거 + 물결표 통일.** 다른 글자는 손대지 않는다
|
||||
#: (키에 쓰인 글자를 세어 보니 그 밖에는 소수점·괄호뿐이었다).
|
||||
_TILDE_CHARS = "∼~〜~"
|
||||
|
||||
|
||||
def normalize_variant_key(text: str) -> str:
|
||||
"""갈래 키 정규화 — 공백을 지우고 물결표를 한 종류(`~`)로 모은다."""
|
||||
tight = "".join(str(text).split())
|
||||
return "".join("~" if ch in _TILDE_CHARS else ch for ch in tight)
|
||||
|
||||
|
||||
def find_variant_code(
|
||||
work_item_code: str,
|
||||
variant_value: str,
|
||||
build: UnitPriceBuild | None = None,
|
||||
) -> str | None:
|
||||
"""B08 이 보낸 **저장 제원 원본값**(「60~80」)을 내 갈래 코드로 옮긴다.
|
||||
|
||||
갈래 키는 품셈 원문에서 나오고 **그 원문을 읽는 쪽이 여기**다(2026-09-08 두 창 합의).
|
||||
못 맞추면 `None` — **가까운 갈래를 임의로 고르지 않는다.**
|
||||
"""
|
||||
prices = build or cached_build()
|
||||
wanted = normalize_variant_key(variant_value)
|
||||
if not wanted:
|
||||
return None
|
||||
|
||||
prefix = f"B-{work_item_code}#"
|
||||
candidates = [code for code in prices.book.titles if code.startswith(prefix)]
|
||||
for code in candidates:
|
||||
if normalize_variant_key(code[len(prefix) :]) == wanted:
|
||||
return code
|
||||
# ⚠ 글자 포함으로는 안 맞는다 — 「60~80」은 「직경60㎝이상~80㎝미만」 **안에 없다**
|
||||
# (사이에 「㎝이상」이 낀다). **수의 짝**으로 견준다: [60, 80] == [60, 80].
|
||||
numbers = _numbers_of(wanted)
|
||||
if not numbers:
|
||||
return None
|
||||
hits = [
|
||||
code
|
||||
for code in candidates
|
||||
if _numbers_of(normalize_variant_key(code[len(prefix) :])) == numbers
|
||||
]
|
||||
if len(hits) == 1:
|
||||
return hits[0]
|
||||
if len(numbers) == 1:
|
||||
# 저장 제원이 **한 값**으로 온다(뒷길이 45㎝). 갈래는 구간이므로 그 값을 담는
|
||||
# 구간을 고른다 — 「45」 → 「55cm이하」. **가장 좁은 구간**을 고른다.
|
||||
return _bracket_for(numbers[0], candidates, prefix)
|
||||
return None
|
||||
|
||||
|
||||
def _bracket_for(value: Decimal, candidates: list[str], prefix: str) -> str | None:
|
||||
"""그 값을 담는 갈래 — 「N 이하」는 상한, 「A 이상~B 미만」은 범위로 본다."""
|
||||
best: tuple[Decimal, str] | None = None
|
||||
for code in candidates:
|
||||
label = normalize_variant_key(code[len(prefix) :])
|
||||
bounds = _numbers_of(label)
|
||||
if len(bounds) == 1:
|
||||
if "이하" in label and value <= bounds[0]:
|
||||
if best is None or bounds[0] < best[0]:
|
||||
best = (bounds[0], code)
|
||||
elif len(bounds) == 2 and bounds[0] <= value <= bounds[1]:
|
||||
width = bounds[1] - bounds[0]
|
||||
if best is None or width < best[0]:
|
||||
best = (width, code)
|
||||
return best[1] if best else None
|
||||
|
||||
|
||||
def _numbers_of(text: str) -> list[Decimal]:
|
||||
"""그 문자열에 나오는 수들 — 「직경60㎝이상~80㎝미만」 → [60, 80]."""
|
||||
return [Decimal(token) for token in re.findall(r"\d+(?:\.\d+)?", text)]
|
||||
|
||||
|
||||
def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
||||
"""자원 축을 일위대가(`B`)로 조립한다.
|
||||
|
||||
@@ -254,7 +329,7 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
||||
# 갈래 키는 **내부 공백을 지운 것**, 화면 문구는 **원문 그대로**
|
||||
# (2026-09-08 두 창 합의). 원문이 「보 통」·「보 통」으로 들쭉날쭉해
|
||||
# 키에 공백을 남기면 한 칸 차이로 영영 안 맞는다. 공백 말고는 손대지 않는다.
|
||||
variant_key = "".join(variant.split())
|
||||
variant_key = normalize_variant_key(variant)
|
||||
title_code = f"B-{work_item_code}" + (f"#{variant_key}" if variant_key else "")
|
||||
if title_code in build.book.titles:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user