feat(B08): 이름 표기 갈림·공식 기호 깃발 + 물빼기 파이프 이중계상 규칙

서브 창 제보 교차 확인. 값을 바꾸지 않고 깃발만 실음.

- `spaced_names` 20건 — 「굴 삭 기 (무한궤도)」처럼 자간 공백이 든 자원 이름.
  같은 표 묶음 안에서도 표기가 갈림(13-6-1 은 공백, 바로 옆 13-6-2 는 없음).
  받는 쪽이 이름으로 찾으면 그 줄이 통째로 빠짐.
  ⚠ 여기서 이름을 고치지 않음 — 정규화는 값을 살리지만 잘못된 줄도 함께 살림
  (서브 실례: 정규화 직후 버킷계수 K 를 소요량으로 읽어 사용료가 이중).
  ⚠ 처음 101건으로 넓게 잡혔던 것을 **단위 칸 + 수치가 함께 있는 자원 줄**로
  좁혀 20건. 짝 시험(「단 위」·「모 래」는 안 걸림)을 같이 둠.
- `formula_rows` 24건 — 값 자리에 K·f·E·Cm 가 온 줄. 자원으로 세면 이중계상.

㉥ 물빼기 파이프 이중계상 — 품셈 13-6-2·13-7-2 [주]③ 이 제잡비 **윗단** 값에
「물빼기 파이프 설치에 관계되는 노무비, 재료비를 포함한다」고 함. 우리 원단위가
물구멍을 자재로 내므로 그 쪽과 겹침.
- **우리 선택: 물구멍을 자재로 세우고 제잡비는 아랫단(미설치)** 을 씀. 까닭은
  자재 줄로 세우면 규격·수량이 눈에 보이고 되짚을 수 있기 때문. 데이터와 코드
  양쪽에 적음.
- ⚠ 지금 쓰는 13-4 계열에는 **제잡비 행 자체가 없어 겹치지 않음**(전수 확인).
  이 규칙은 13-6·13-7 을 쓰게 될 때 걸림.

관측값이 원문과 맞는 첫 사례 기록 — 채움콘크리트 0.2㎥/㎡ 가 품셈 13-6-2 [주]⑩
「뒤채움콘크리트량은 0.2㎥ 기준」과 일치.

갈래 키 정규화 — `code` 는 내부 공백만 제거(`FP-12-03#보통`), 원문 문구는
`kind_label` 로 함께 실음. 다른 글자는 안 건드림(괄호·기호 유지 짝 시험).

검증 — 품셈 43건 통과, 전체 611 passed, tsc 오류 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 01:55:04 +09:00
co-authored by Claude Opus 5
parent bc4b6837e6
commit a4c7c8c8cd
6 changed files with 1396 additions and 42 deletions
@@ -359,6 +359,54 @@ def expression_cells(table: dict[str, Any]) -> list[str]:
CREW_QUANTITY_MARKS = ("시공량", "작업량", "1일작업량")
# ⚠ **이름에 자간 공백이 든 기종·직종** — 같은 표 묶음 안에서도 표기가 갈린다
# (`13-6-1` 은 「굴 삭 기 (무한궤도)」, 바로 옆 `13-6-2` 는 「굴착기 (무한궤도)」).
# 받는 쪽이 이름으로 자원을 찾으므로 **표기가 갈리면 그 줄이 통째로 빠진다.**
# ⚠ **여기서 이름을 고치지 않는다** — 정규화는 값을 살리지만 **잘못된 줄도 함께 살린다**
# (서브 창 실례: 이름 정규화 직후 버킷계수 `K` 를 소요량으로 읽어 시간당 사용료가 이중).
# 깃발만 실어 받는 쪽이 대조하게 한다.
_SPACED_NAME_RE = re.compile(r"^(?=.*\S\s\S)[가-힣](?:\s+[가-힣])+")
#: 시공능력 공식 파라미터가 값 자리에 온 줄 — **소요량이 아니다.** 그냥 읽으면 이중계상.
_FORMULA_KEYS = frozenset({"K", "k", "f", "E", "Cm", "q", "qo", "Q", "㎝(sec)"})
#: 자원 줄임을 알리는 단위 칸. 이것과 수치가 함께 있어야 자원으로 본다.
_RESOURCE_UNITS = frozenset(
{"", "h", "hr", "시간", "", "", "", "", "kg", "", "", "", "m", "", "", "ton"}
)
def spaced_names(table: dict[str, Any]) -> list[str]:
"""자간 공백이 든 **자원 이름**. 표기가 갈리는 자리를 드러낸다.
⚠ **좁게 잡는다.** 「단 위」·「모 래」 같은 머리글·재료명까지 걸면 101건이 되어
목록이 잡음이 되고, 잡음이 되면 아무도 안 본다(오늘 아홉 번 겪은 병).
**그 줄에 단위 칸과 수치가 함께 있는 것**만 자원 줄로 본다.
"""
found: list[str] = []
for row in table.get("rows", []):
if not row:
continue
name = norm(row[0])
if not name or not _SPACED_NAME_RE.match(name):
continue
rest = [norm(cell) for cell in row[1:]]
has_unit = any(cell in _RESOURCE_UNITS for cell in rest)
has_number = any(_PLAIN_NUMBER_RE.match(cell) for cell in rest if cell)
if has_unit and has_number and name not in found:
found.append(name)
return found
def formula_rows(table: dict[str, Any]) -> list[str]:
"""값 자리에 시공능력 공식 기호가 온 줄. 자원으로 세면 이중계상이다."""
found: list[str] = []
for row in table.get("rows", []):
for cell in row:
text = norm(cell)
if text in _FORMULA_KEYS and text not in found:
found.append(text)
return found
def crew_table(table: dict[str, Any]) -> bool:
"""작업조 + 시공량으로 적힌 표인가."""
hay = " ".join(norm(h) for h in table.get("headers", []))
@@ -480,6 +528,10 @@ def build() -> dict[str, Any]:
"expression_cells": expression_cells(table),
# ⚠ 작업조 표 — 「4」가 소요량이 아니라 인원이다. 그냥 읽으면 35배 부푼다.
"crew_table": crew_table(table),
# ⚠ 이름 표기가 갈리는 줄 — 받는 쪽이 이름으로 찾으면 통째로 빠진다.
"spaced_names": spaced_names(table),
# ⚠ 공식 기호 줄 — 소요량이 아니다. 자원으로 세면 이중계상.
"formula_rows": formula_rows(table),
# 공식 기호가 이 표에 직접 있는가 — 없으면 앞 표에서 물려받는 모양이다.
"capacity_formula_here": capacity_formula_pending(table),
"variant_key": variant_axis(table),
+17 -4
View File
@@ -55,6 +55,17 @@ DATASET_PREFIX = "work_item_mapping_"
REBAR_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_rebar"
REBAR_PREFIX = "rebar_complexity_"
def normalize_kind_key(label: str) -> str:
"""갈래 키 — **내부 공백만** 지운다 (2026-09-07 두 창 확정).
원문 표는 「보 통」처럼 자간 공백이 들어 있어 그대로 쓰면 양쪽이 안 맞는다.
⚠ **다른 글자는 손대지 않는다** — 정규화를 넓히면 오늘 아홉 번 겪은 그 병을
여기서 새로 만든다. 원문 문구는 버리지 않고 `label` 로 함께 싣는다.
"""
return "".join(str(label).split())
#: 줄이 어디서 왔나 — 되짚을 때 쓴다.
ORIGIN_EARTHWORK = "earthwork"
ORIGIN_STRUCTURE = "structure"
@@ -209,10 +220,11 @@ def composite_quantities(
entry["incomplete_note"] = spec["incomplete_note"]
if suffix == "euroform_type":
kind, why = euroform_type(str(structure.get("type_id") or ""))
entry["kind"] = kind
entry["kind"] = normalize_kind_key(kind) if kind else None
entry["kind_label"] = kind # 원문 문구 그대로
entry["kind_basis"] = why
if kind:
entry["code"] = f"{spec.get('code')}#{kind}"
entry["code"] = f"{spec.get('code')}#{normalize_kind_key(kind)}"
else:
entry["not_ready"] = True
entry["why"] = why
@@ -222,10 +234,11 @@ def composite_quantities(
complexity, why = rebar_complexity(
str(structure.get("type_id") or ""), structure.get("options") or {}
)
entry["kind"] = complexity
entry["kind"] = normalize_kind_key(complexity) if complexity else None
entry["kind_label"] = complexity # 원문 문구 그대로(자간 공백 포함)
entry["kind_basis"] = why
if complexity:
entry["code"] = f"{spec.get('code')}#{complexity}"
entry["code"] = f"{spec.get('code')}#{normalize_kind_key(complexity)}"
else:
entry["not_ready"] = True
entry["why"] = why
@@ -233,6 +233,11 @@ def stone_masonry(
# 물구멍 — 벽면적 2㎡당 1개소, 개소당 0.5m.
# ⚠ 이것은 **관(파이프) 자재**이지 공제 대상이 아니다. 품셈 1-2-1 이 「공제하지 않는다」고
# 말하는 물구멍은 **콘크리트 체적에서 뺄 구멍**이고, 여기 값은 그 구멍에 넣는 **관 길이**다.
# ⚠⚠ **㉥ 이중계상** — 품셈 13-6-2·13-7-2 [주]③ 은 제잡비 **윗단** 값에
# 「물빼기 파이프 설치에 관계되는 노무비, 재료비를 포함한다」고 한다. 그 쪽을 쓰면
# 이 줄과 겹친다. **우리 선택은 이 줄을 세우고 제잡비는 아랫단(미설치)** 이다
# (`structure_unit_observed` 의 `double_count_rules`).
# 지금 쓰는 13-4 계열에는 제잡비 행 자체가 없어 겹치지 않는다(전수 확인).
# ⚠ 관종·지름은 미확정 — 법은 「지름 3~6㎝ 파이프」, 실무 관측은 Ø50. 규격이 정해지면
# 이름에 붙인다(`물구멍 Ø50`). 지어내지 않고 규격 없는 이름으로 둔다.
components.append(
@@ -26,75 +26,235 @@
"entries": [
{
"type_id": "retaining_wall",
"spec": { "form": "반중력식", "height_m": 2.0 },
"spec": {
"form": "반중력식",
"height_m": 2.0
},
"unit": "m",
"source": "uljin_library",
"source_note": "§7 옹벽류 — 반중력식옹벽 H=2.0",
"components": [
{ "name": "콘크리트", "unit": "㎥", "amount": 1.35, "destination": "unit_price", "basis_note": "기초 0.75 + 벽체 0.60" },
{ "name": "버림콘크리트", "unit": "㎥", "amount": 0.15, "destination": "unit_price" },
{ "name": "유로폼", "unit": "㎡", "amount": 3.2, "destination": "unit_price", "basis_note": "배면+전면" },
{ "name": "합판거푸집", "unit": "㎡", "amount": 0.6, "destination": "unit_price", "basis_note": "기초" },
{ "name": "물구멍", "unit": "m", "amount": 0.32, "destination": "material", "basis_note": "Ø50" },
{ "name": "이형철근 D13", "unit": "kg", "amount": 13.45, "destination": "material" },
{ "name": "이형철근 D16", "unit": "kg", "amount": 30.42, "destination": "material" }
{
"name": "콘크리트",
"unit": "㎥",
"amount": 1.35,
"destination": "unit_price",
"basis_note": "기초 0.75 + 벽체 0.60"
},
{
"name": "버림콘크리트",
"unit": "㎥",
"amount": 0.15,
"destination": "unit_price"
},
{
"name": "유로폼",
"unit": "㎡",
"amount": 3.2,
"destination": "unit_price",
"basis_note": "배면+전면"
},
{
"name": "합판거푸집",
"unit": "㎡",
"amount": 0.6,
"destination": "unit_price",
"basis_note": "기초"
},
{
"name": "물구멍",
"unit": "m",
"amount": 0.32,
"destination": "material",
"basis_note": "Ø50"
},
{
"name": "이형철근 D13",
"unit": "kg",
"amount": 13.45,
"destination": "material"
},
{
"name": "이형철근 D16",
"unit": "kg",
"amount": 30.42,
"destination": "material"
}
]
},
{
"type_id": "pipe_inlet_basin",
"spec": { "inlet_basin_form": "돌집수정 ㄷ형" },
"spec": {
"inlet_basin_form": "돌집수정 ㄷ형"
},
"unit": "개소",
"source": "uljin_compare",
"source_note": "관보호공 돌집수정 ㄷ형 /개소",
"components": [
{ "name": "콘크리트", "unit": "㎥", "amount": 4.03, "destination": "unit_price" },
{ "name": "모르터", "unit": "㎥", "amount": 0.157, "destination": "unit_price" },
{ "name": "터파기", "unit": "㎥", "amount": 21.1, "destination": "earthwork", "basis_note": "토사 14.8 + 암 6.3 — 지반 구분은 토공집계가 다시 가름" },
{ "name": "되메우기", "unit": "㎥", "amount": 2.6, "destination": "earthwork" },
{ "name": "잔토처리", "unit": "㎥", "amount": 18.5, "destination": "earthwork" }
{
"name": "콘크리트",
"unit": "㎥",
"amount": 4.03,
"destination": "unit_price"
},
{
"name": "모르터",
"unit": "㎥",
"amount": 0.157,
"destination": "unit_price"
},
{
"name": "터파기",
"unit": "㎥",
"amount": 21.1,
"destination": "earthwork",
"basis_note": "토사 14.8 + 암 6.3 — 지반 구분은 토공집계가 다시 가름"
},
{
"name": "되메우기",
"unit": "㎥",
"amount": 2.6,
"destination": "earthwork"
},
{
"name": "잔토처리",
"unit": "㎥",
"amount": 18.5,
"destination": "earthwork"
}
]
},
{
"type_id": "pipe_inlet_basin",
"spec": { "inlet_basin_form": "돌집수정 ㄴ형" },
"spec": {
"inlet_basin_form": "돌집수정 ㄴ형"
},
"unit": "개소",
"source": "uljin_compare",
"source_note": "관보호공 돌집수정 ㄴ형 /개소",
"components": [
{ "name": "콘크리트", "unit": "㎥", "amount": 2.69, "destination": "unit_price" },
{ "name": "모르터", "unit": "㎥", "amount": 0.096, "destination": "unit_price" },
{ "name": "터파기", "unit": "㎥", "amount": 16.4, "destination": "earthwork", "basis_note": "토사 4.9 + 암 11.5" },
{ "name": "되메우기", "unit": "㎥", "amount": 1.2, "destination": "earthwork" },
{ "name": "잔토처리", "unit": "㎥", "amount": 15.2, "destination": "earthwork" }
{
"name": "콘크리트",
"unit": "㎥",
"amount": 2.69,
"destination": "unit_price"
},
{
"name": "모르터",
"unit": "㎥",
"amount": 0.096,
"destination": "unit_price"
},
{
"name": "터파기",
"unit": "㎥",
"amount": 16.4,
"destination": "earthwork",
"basis_note": "토사 4.9 + 암 11.5"
},
{
"name": "되메우기",
"unit": "㎥",
"amount": 1.2,
"destination": "earthwork"
},
{
"name": "잔토처리",
"unit": "㎥",
"amount": 15.2,
"destination": "earthwork"
}
]
},
{
"type_id": "pipe_inlet_basin",
"spec": { "inlet_basin_form": "□형(기본형)", "inlet_basin_material": "콘크리트", "pipe_diameter_mm": "800" },
"spec": {
"inlet_basin_form": "□형(기본형)",
"inlet_basin_material": "콘크리트",
"pipe_diameter_mm": "800"
},
"unit": "개소",
"source": "uljin_library",
"source_note": "§2 집수정 Ø800 — 내부 3.0×1.0×1.2, 벽 0.2, 바닥기초 3.4×1.4×0.2",
"components": [
{ "name": "콘크리트", "unit": "㎥", "amount": 2.84, "destination": "unit_price" },
{ "name": "합판거푸집", "unit": "㎡", "amount": 21.28, "destination": "unit_price" },
{ "name": "이형철근 D13", "unit": "kg", "amount": 4.78, "destination": "material" },
{ "name": "면목", "unit": "m", "amount": 12.67, "destination": "material", "basis_note": "A25" },
{ "name": "터파기", "unit": "㎥", "amount": 10.64, "destination": "earthwork" },
{ "name": "되메우기", "unit": "㎥", "amount": 6.44, "destination": "earthwork" },
{ "name": "잔토처리", "unit": "㎥", "amount": 4.2, "destination": "earthwork" }
{
"name": "콘크리트",
"unit": "㎥",
"amount": 2.84,
"destination": "unit_price"
},
{
"name": "합판거푸집",
"unit": "㎡",
"amount": 21.28,
"destination": "unit_price"
},
{
"name": "이형철근 D13",
"unit": "kg",
"amount": 4.78,
"destination": "material"
},
{
"name": "면목",
"unit": "m",
"amount": 12.67,
"destination": "material",
"basis_note": "A25"
},
{
"name": "터파기",
"unit": "㎥",
"amount": 10.64,
"destination": "earthwork"
},
{
"name": "되메우기",
"unit": "㎥",
"amount": 6.44,
"destination": "earthwork"
},
{
"name": "잔토처리",
"unit": "㎥",
"amount": 4.2,
"destination": "earthwork"
}
]
},
{
"type_id": "ford_pavement",
"spec": { "thickness_cm": 20 },
"spec": {
"thickness_cm": 20
},
"unit": "㎡",
"source": "uljin_compare",
"source_note": "콘크리트포장 T=20cm /㎡",
"components": [
{ "name": "레미콘", "unit": "㎥", "amount": 0.2, "destination": "unit_price" },
{ "name": "와이어메쉬", "unit": "㎡", "amount": 1.16, "destination": "material" },
{ "name": "터파기", "unit": "㎥", "amount": 0.2, "destination": "earthwork" },
{ "name": "잔토처리", "unit": "㎥", "amount": 0.2, "destination": "earthwork" }
{
"name": "레미콘",
"unit": "㎥",
"amount": 0.2,
"destination": "unit_price"
},
{
"name": "와이어메쉬",
"unit": "㎡",
"amount": 1.16,
"destination": "material"
},
{
"name": "터파기",
"unit": "㎥",
"amount": 0.2,
"destination": "earthwork"
},
{
"name": "잔토처리",
"unit": "㎥",
"amount": 0.2,
"destination": "earthwork"
}
]
}
],
@@ -113,9 +273,36 @@
},
{
"type_id": "retaining_wall",
"spec": { "form": "반중력식", "height_m": 1.6 },
"spec": {
"form": "반중력식",
"height_m": 1.6
},
"why": "울진 2공구에 H=1.6 이 실재하나 수치가 라이브러리에 없음. H=2.0 값을 비례로 줄이지 않음 — 기초·벽체는 높이에 비례하지 않음."
}
]
},
"double_count_rules": {
"note": "이 표의 값이 품셈 다른 자리와 겹치는 곳. 겹치면 한쪽만 쓴다.",
"rules": [
{
"key": "물빼기 파이프 ↔ 제잡비 윗단",
"where": "품셈 13-6-2·13-6-3·13-7-2 [주]③",
"quote": "물빼기 파이프를 설치한 경우는 윗단의 값, 설치하지 않는 경우는 아랫단의 값으로 하며, 상단에는 물빼기 파이프 설치에 관계되는 노무비, 재료비를 포함한다.",
"our_choice": "물구멍(관)을 **자재로 명시해 세고**, 큰돌쌓기·큰돌붙이기를 쓸 때는 **제잡비 아랫단(미설치)** 을 쓴다.",
"why": "물구멍을 자재 줄로 세우면 규격·수량이 눈에 보이고 되짚을 수 있다. 제잡비 윗단은 같은 것을 품 안에 녹이는 다른 방식이라 어느 쪽이든 하나만 골라야 한다.",
"scope": "⚠ 지금 쓰는 돌쌓기(13-4 계열)에는 **제잡비 행 자체가 없어** 겹치지 않는다(전수 확인). 이 규칙은 13-6·13-7 을 쓰게 될 때 걸린다.",
"guard": "제잡비 윗단과 물구멍 줄이 함께 서면 멈출 것 — B09 ㉥ 가드와 짝."
}
]
},
"原文_뒷받침": {
"note": "관측값이 **원문과 맞는 것이 확인된** 항목. 지금까지 관측값은 근거가 약한 참조였는데 이 줄은 원문 뒷받침이 있다.",
"items": [
{
"item": "채움콘크리트 0.2 ㎥/㎡",
"observed": "울진 라이브러리 돌쌓기(찰) 채움 0.2 ㎥/㎡",
"source": "품셈 13-6-2 [주]⑩ 「큰돌쌓기(찰쌓기)의 뒤채움콘크리트량은 0.2㎥기준으로 하고 현지여건에 따라 0.3㎥까지 적용할 수 있다」"
}
]
}
}
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-08T01:45:21+09:00",
"generated_at": "2026-09-08T01:52:34+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_forest",
@@ -12,8 +12,8 @@
"files": [
{
"file": "work_item_master_2026-01-01.json",
"sha256": "f80d3ad4bf3ee242d282d8d54d18305e1b89fbbb9fc414c31b95cecc8b84d47f",
"size_bytes": 813044
"sha256": "08b0c7c26c4aa569ace3b13490b8cbd00606254abb1439894eac750d387bfb03",
"size_bytes": 839464
},
{
"file": "form_undetermined_2026-01-01.json",
File diff suppressed because it is too large Load Diff