feat(B09): 뭉친 이름+갈래 열 표 읽기 — 돌쌓기 계열 성립

메인이 `reference` 오판 41건을 되살려(bc4b6837) 돌쌓기(장비)가 마스터에
들어왔으나, 그 표는 **이름도 값도 뭉쳐 오고 열이 규격 갈래**인 또 다른 모양이라
여전히 안 섰음 (「석공 보통인부 | 0.09 0.05 | 0.08 0.04 | 0.07 0.03」)

- 뭉친 이름·값을 갈래 열에 짝지어 읽음. 개수가 어긋나면 표째 버림
- 「굴착기+부착용 집게」는 **두 기종 조합**으로 보아 같은 시간을 둘 다 붙임
  (TODO(미결) 해석 잠정 — 사용자 확인 대기)
- 규격이 **옆 칸**에 있고 갈래(무한궤도)가 안 적힌 기종은 규격 일치 + 무한궤도
  우선으로 잠정 채택. 카탈로그 규격이 범위(0.6∼0.8)면 그 안에 드는지로 판정
- ⚠ 「1.04(1.17)」 괄호값은 **조건 시공 시 대안값**임이 원문에서 확인됨
  (13-6-1 [주]② 흡출방지재 시공 시 ( ) 값). 기본은 괄호 밖, 대안값은
  `alternative_amount` 로 남겨 화면에 「시공 시 다름」으로 보이게 함
- 라벨 줄 판정을 **모든 칸**으로 — 첫 칸만 보면 기초잡석 12-25 를 라벨 줄로
  오해해 표째 가로챘음(그 탓에 기초잡석이 한때 다시 막혔음)
- 갈래 키를 **공백 없는 것**으로 통일(`#보통`), 원문 문구는 이름에 보존
  (두 창 합의)

결과: 자원 축 259 → 354, 일위대가 145 → 176
  돌쌓기(찰) 35cm이하 58,166.7 / 55cm이하 52,938.7 / 75cm이하 46,892.7 원/㎡
  메쌓기·찰쌓기·붙이기 계열도 직경 3갈래로 섬

검증: pytest 184 통과(신규 1). 화면 실측 — 지금 인계 자료에 구조물 줄이 없어
표에는 아직 안 뜸(B08 이 갈래를 골라 보내면 그대로 금액이 섬)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 02:04:42 +09:00
co-authored by Claude Opus 5
parent 17e24e2789
commit 15bdf6bd64
5 changed files with 6912 additions and 784 deletions
+39 -3
View File
@@ -350,6 +350,24 @@ def parse_amount_expression(cell: str) -> Decimal | None:
return Decimal(found.group(1)) * Decimal(found.group(2)) / Decimal(100)
#: 「1.04(1.17)」 — 괄호 밖이 기본, 괄호 안이 **조건 시공 시** 값.
#: 근거: 품셈 13-6-1 [주]② 「흡출방지재를 시공하는 경우는 ( )의 값을 적용한다」
#: (13-6-2·13-6-3·13-7-1·13-7-2 도 같은 [주]).
#: ⚠ **기본은 괄호 밖** — 방지재 시공 여부가 설계 조건에 아직 없다(사용자 확정 대기).
#: 괄호 값을 쓰려면 그 조건이 들어와야 하므로, 지금은 값과 함께 **대안값을 남겨** 둔다.
_RE_ALTERNATIVE = re.compile(r"^(\d+(?:\.\d+)?)\s*[((](\d+(?:\.\d+)?)[))]$")
def parse_amount_pair(cell: str) -> tuple[Decimal, Decimal | None] | None:
"""「1.04(1.17)」 → `(1.04, 1.17)`. 괄호가 없으면 `(값, None)`."""
text = _normalize(cell)
found = _RE_ALTERNATIVE.match(text)
if found:
return Decimal(found.group(1)), Decimal(found.group(2))
plain = parse_amount(text)
return None if plain is None else (plain, None)
def convert_amount(raw: Decimal, *, pum_form: str, basis_quantity: Decimal | None) -> Decimal:
"""표 형태에 맞춰 소요량으로 환산한다.
@@ -383,6 +401,9 @@ class ResourceRow:
amount: Decimal
amount_unit: str
raw_row_index: int
#: 조건 시공 시 쓰는 대안값 — 「1.04(1.17)」의 1.17 (품셈 13-6-1 [주]②).
#: **기본값은 `amount`(괄호 밖)** 이고 이 값은 화면에 「시공 시 다름」으로 보인다.
alternative_amount: Decimal | None = None
#: 규격 갈래 — 열이 자원인 표에서 행 이름(「무근구조물」). 없으면 빈 문자열.
#: **갈래마다 품이 다르므로 한 일위대가로 뭉치지 않는다.**
variant: str = ""
@@ -404,6 +425,9 @@ class ResourceRow:
"amount_unit": self.amount_unit,
"raw_row_index": self.raw_row_index,
"variant": self.variant,
"alternative_amount": (
None if self.alternative_amount is None else str(self.alternative_amount)
),
"group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct),
}
@@ -523,6 +547,13 @@ def match_table(
if match_transposed_table(node, table, catalog, result, basis_quantity, unit):
return
# 「석공 보통인부 | 0.09 0.05 | …」처럼 **이름도 값도 뭉쳐 오고 열이 갈래**인 표
# (돌쌓기 13-4 계열). 행-자원으로는 첫 이름조차 안 풀린다.
from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import match_packed_rows
if match_packed_rows(node, table, catalog, result, basis_quantity, unit):
return
for index, row in enumerate(table.get("raw_row", [])):
cells = [str(c) for c in row]
if not cells:
@@ -556,9 +587,13 @@ def match_table(
continue
# 숫자 셀이 없는 행은 자원 줄이 아니다(제목·설명 행) — 목록에 안 올린다.
amount_cell = next(
(parse_amount(c) for c in value_cells if parse_amount(c) is not None), None
)
alternative: Decimal | None = None
amount_cell = None
for cell in value_cells:
pair = parse_amount_pair(cell)
if pair is not None:
amount_cell, alternative = pair
break
if amount_cell is None:
# 「0.2 × 30%」 꼴은 값과 배분율이 한 칸에 있다 — 읽었으면 딱지 배분율은 버린다.
expression = next(
@@ -638,6 +673,7 @@ def match_table(
amount_unit=unit,
raw_row_index=index,
group_ratio_pct=group_ratio,
alternative_amount=alternative,
)
)