fix(B08): 「단 위」 표지가 공종 41건을 참조로 버리던 것 + 작업조 표 깃발

⚠ 서브 창 제보(유로폼 12-38-3 이 참조로 찍혀 읽히지도 않던 것)를 파고 들어가
더 넓은 결함을 찾음. `REFERENCE_MARKS` 의 `"단 위"` 하나로 **41건이 참조로
버려지고 있었고**, 그 안에 **돌쌓기(장비) 13-4-5·13-4-2**(우리 매핑이 실제로
쓰는 코드) · 12-2 표면 마무리 · 9-19-1 면고르기 · 9-20 뿌리다듬기 ·
13-8 막돌쌓기 · 11-2 규준틀 같은 명백한 공종이 섞여 있었음.

- 원래 겨냥은 품셈 1-2-2 「단위 표준」 표였는데 **1장은 chapter 규칙이 이미
  거르므로** 그 표지는 넓기만 했음. 「단위 열이 있다」는 소요량표의 흔한
  모양이지 참조표의 표지가 아님. 제거.
- reference 98 → 58, requirement 246 → 287. 돌쌓기(장비) 둘이 공종으로 섬.

작업조 표 깃발(`crew_table`) 16건 — 「형틀목공 4인 + 보통인부 1인 / 시공량 35㎡」
의 「4」는 소요량이 아니라 **인원**이라 그냥 읽으면 35배 부픔. 값을 바꾸지 않고
표시만 함(`expression_cells`·`partial_ratio` 와 같은 취급). 평떼 시비도 잡힘 —
서브가 실물에서 트럭 한 대를 빠뜨린 그 표.

유로폼 조각에 `incomplete_note` — 물량은 섰으나 **자재 몫(패널 0.89매/10㎡ ·
부자재 24/52/79 % · 소모자재 5 %)이 자재 단가가 없어 안 붙음**. 「못 채움」과
달리 값이 있으므로 「부분」으로 갈라 보임 — 부분 성공이 완전 실패보다 위험함.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 01:47:33 +09:00
co-authored by Claude Opus 5
parent 8b0f7e8baf
commit bc4b6837e6
7 changed files with 682 additions and 91 deletions
@@ -72,7 +72,13 @@ COEFFICIENT_MARKS = ("손료계수", "시간당손료계수", "기계손료")
# 기계 시공능력 공식(Q = 3600·qo·K·f·E / Cm)의 파라미터 기호. 이 기호만 있는 표는 계수표다.
COEFFICIENT_ROW_KEYS = {"K", "f", "E", "Cm", "㎝(sec)", "q", "qo", "Q", "V", "L"}
# 품셈 1장(적용기준)·할증·공제·단위 표준 — 공종이 아니라 **기준표**다.
REFERENCE_MARKS = ("할증률", "할인", "공제율", "적재량", "단 위", "지위")
# ⚠ **「단 위」를 뺐다** (2026-09-07). 원래 겨냥은 품셈 1-2-2 「단위 표준」 표였는데,
# **1장은 chapter 규칙이 이미 참조로 거르므로** 이 표지는 넓기만 했다. 그 탓에
# **41건이 「헤더 '단 위'」 하나로 참조가 되어 버려지고 있었고**, 그 안에
# **돌쌓기(장비) 13-4-5·13-4-2**(우리 매핑이 실제로 쓰는 코드) · 12-2 표면 마무리 ·
# 9-19-1 면고르기 · 9-20 뿌리다듬기 · 13-8 막돌쌓기 같은 **명백한 공종**이 섞여 있었다.
# 「단위 열이 있다」는 소요량표의 흔한 모양이지 참조표의 표지가 아니다.
REFERENCE_MARKS = ("할증률", "할인", "공제율", "적재량", "지위")
# 값 대신 「어디를 따르라」고만 적은 표. 품이 아니므로 공종으로 세우지 않는다.
REFERENCE_CELL_MARKS = ("별도계상", "구역화물", "적용한다", "따른다", "준용")
# 직종이 값의 주인인 표 = 소요량형. `보통인부(인)`·`콘크리트공(인)` 처럼 `(인)` 이 붙는다.
@@ -346,6 +352,23 @@ def expression_cells(table: dict[str, Any]) -> list[str]:
return seen
# ⚠ **작업조 표** — 「형틀목공 4인 + 보통인부 1인 / 시공량 25·35·40㎡」처럼
# **인원과 시공량**으로 적힌 표다. 그 「4」는 소요량이 아니라 **작업조 인원**이라
# 행-자원으로 그냥 읽으면 **35배 부푼다**(유로폼 12-38-3 이 그 표다).
# ⚠ 값을 바꾸지 않고 **표시만** 한다 — 나누는 것은 받는 쪽 몫이다.
CREW_QUANTITY_MARKS = ("시공량", "작업량", "1일작업량")
def crew_table(table: dict[str, Any]) -> bool:
"""작업조 + 시공량으로 적힌 표인가."""
hay = " ".join(norm(h) for h in table.get("headers", []))
body = " ".join(norm(c) for row in table.get("rows", []) for c in row)
squeezed = (hay + " " + body).replace(" ", "")
if not any(mark in squeezed for mark in CREW_QUANTITY_MARKS):
return False
return bool(OCCUPATION_RE.search(squeezed))
def resource_shares(table: dict[str, Any]) -> dict[str, float]:
"""`{인력: 10.0, 장비: 90.0}` — 딱지에 붙은 몫. 없으면 빈 칸."""
shares: dict[str, float] = {}
@@ -455,6 +478,8 @@ def build() -> dict[str, Any]:
"partial_ratio": partial,
# ⚠ 값 자리에 식이 적힌 칸 — 그 성분은 숫자로 안 읽히므로 **금액을 만들면 안 된다**.
"expression_cells": expression_cells(table),
# ⚠ 작업조 표 — 「4」가 소요량이 아니라 인원이다. 그냥 읽으면 35배 부푼다.
"crew_table": crew_table(table),
# 공식 기호가 이 표에 직접 있는가 — 없으면 앞 표에서 물려받는 모양이다.
"capacity_formula_here": capacity_formula_pending(table),
"variant_key": variant_axis(table),
@@ -203,6 +203,10 @@ def composite_quantities(
"basis_kind": next(iter(kinds)) if len(kinds) == 1 else (sorted(kinds) or None),
"from_components": sources,
}
if spec.get("incomplete_note"):
# ⚠ 물량은 섰으나 **일부 몫이 빠진** 조각 — 「못 채움」과 달리 값은 있다.
# 화면·인계 둘 다 그 사실을 알아야 「다 섰다」로 오해하지 않는다.
entry["incomplete_note"] = spec["incomplete_note"]
if suffix == "euroform_type":
kind, why = euroform_type(str(structure.get("type_id") or ""))
entry["kind"] = kind
+10 -1
View File
@@ -73,6 +73,8 @@ export interface CompositePart {
kind_basis?: string;
not_ready?: boolean;
why?: string;
/** 물량은 섰으나 일부 몫이 빠진 조각 — 「다 섰다」로 오해하지 않게 함께 보인다. */
incomplete_note?: string;
}
export interface MaterialResponse {
@@ -364,12 +366,19 @@ export function renderUnitQuantityGrid(response: MaterialResponse): HTMLElement
part.quantity === null || part.quantity === undefined
? ""
: `${num(part.quantity, 3)}${part.unit ?? ""}`;
return `${part.name ?? part.code}${kind} ${amount}${part.not_ready ? " ⚠" : ""}`;
const flag = part.not_ready ? " ⚠" : part.incomplete_note ? " ⚠부분" : "";
return `${part.name ?? part.code}${kind} ${amount}${flag}`;
});
box.textContent = `${group.name}: 묶음 공종 — ${parts.join(" · ")}`;
wrap.append(box);
const blocked = warning("⚠ 물량을 못 채운 조각", reasons(group.not_ready));
if (blocked) wrap.append(blocked);
// ⚠ 값이 있는데 일부만 선 조각 — **부분 성공이 완전 실패보다 위험하다**.
const partial = warning(
"⚠ 일부 몫이 빠진 조각",
group.parts.filter((p) => p.incomplete_note).map((p) => `${p.name}: ${p.incomplete_note}`),
);
if (partial) wrap.append(partial);
}
if (!unit.structures.length) {
@@ -190,7 +190,8 @@
"from_components": [
"유로폼"
],
"kind_suffix": "euroform_type"
"kind_suffix": "euroform_type",
"incomplete_note": "⚠ 자재 몫이 빠져 있음 — 12-38-2 의 패널 0.89매/10㎡·부자재(주자재비의 24/52/79 %)·소모자재 5 % 는 **자재 단가가 서야** 붙는다. 지금 서는 것은 설치·해체 품뿐이다."
},
{
"code": "FP-12-03",
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-08T01:18:06+09:00",
"generated_at": "2026-09-08T01:45:21+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": "086441e1380441259e860620d1e81b886019f11838dea601c0a3797f6f1a3d3e",
"size_bytes": 800211
"sha256": "f80d3ad4bf3ee242d282d8d54d18305e1b89fbbb9fc414c31b95cecc8b84d47f",
"size_bytes": 813044
},
{
"file": "form_undetermined_2026-01-01.json",
@@ -22,8 +22,8 @@
},
{
"file": "basis_missing_2026-01-01.json",
"sha256": "27f8d80d10b791df8502e633c1d279ce2acce63f871eae6b0c4eb26ca8290e2e",
"size_bytes": 20674
"sha256": "b69091149adb942ce507d902b7e6ac400777674301b9200b1f1c60cfbd709839",
"size_bytes": 22832
}
]
}
@@ -34,12 +34,24 @@
"pum_form": "requirement",
"line": 1444
},
{
"pum_table_id": "F0057",
"section": "2-1-7. 소각, 매몰, 훈증, 박피",
"pum_form": "requirement",
"line": 1518
},
{
"pum_table_id": "F0061",
"section": "2-1-11. 지상 약제살포",
"pum_form": "requirement",
"line": 1570
},
{
"pum_table_id": "F0062",
"section": "2-1-12. 그물망 피복",
"pum_form": "requirement",
"line": 1579
},
{
"pum_table_id": "F0075",
"section": "3-1. 경계표시",
@@ -292,6 +304,12 @@
"pum_form": "requirement",
"line": 2919
},
{
"pum_table_id": "F0145",
"section": "5-28-2. 방초매트 및 야자섬유매트 포장",
"pum_form": "requirement",
"line": 2929
},
{
"pum_table_id": "F0153",
"section": "6-1. 비료주기",
@@ -322,6 +340,12 @@
"pum_form": "requirement",
"line": 3122
},
{
"pum_table_id": "F0158",
"section": "6-4-1. 덩굴걷기",
"pum_form": "requirement",
"line": 3143
},
{
"pum_table_id": "F0159",
"section": "6-4-2. 덩굴 약제 살포처리",
@@ -610,6 +634,12 @@
"pum_form": "requirement",
"line": 4860
},
{
"pum_table_id": "F0256",
"section": "9-11-1. 콘크리트(기계)",
"pum_form": "requirement",
"line": 4946
},
{
"pum_table_id": "F0294",
"section": "9-21. 제근",
@@ -622,6 +652,18 @@
"pum_form": "requirement",
"line": 5770
},
{
"pum_table_id": "F0311",
"section": "10-7-4. 모노레일 운반",
"pum_form": "requirement",
"line": 5779
},
{
"pum_table_id": "F0312",
"section": "10-7-4. 모노레일 운반",
"pum_form": "requirement",
"line": 5795
},
{
"pum_table_id": "F0313",
"section": "10-8-1. 짐내리기",
@@ -634,6 +676,12 @@
"pum_form": "requirement",
"line": 5829
},
{
"pum_table_id": "F0315",
"section": "10-8-3. 케이블 크레인 운전",
"pum_form": "requirement",
"line": 5840
},
{
"pum_table_id": "F0317",
"section": "10-10-1. 콘크리트 및 골재운반(지상)",
@@ -646,6 +694,18 @@
"pum_form": "requirement",
"line": 5894
},
{
"pum_table_id": "F0326",
"section": "11-2. 토공의 비탈 규준틀",
"pum_form": "requirement",
"line": 6043
},
{
"pum_table_id": "F0327",
"section": "11-3. 수평규준틀",
"pum_form": "requirement",
"line": 6056
},
{
"pum_table_id": "F0337",
"section": "12-5. 문양거푸집(07m)",
@@ -676,12 +736,36 @@
"pum_form": "requirement",
"line": 6421
},
{
"pum_table_id": "F0355",
"section": "12-17-1. 펌프카 타설",
"pum_form": "requirement",
"line": 6499
},
{
"pum_table_id": "F0362",
"section": "12-2 표면 마무리를 따른다.",
"pum_form": "requirement",
"line": 6569
},
{
"pum_table_id": "F0366",
"section": "12-19. 강관비계",
"pum_form": "requirement",
"line": 6614
},
{
"pum_table_id": "F0379",
"section": "12-29. 스페이셔 설치(몰탈 블록)",
"pum_form": "requirement",
"line": 6775
},
{
"pum_table_id": "F0395",
"section": "12-38-3. 설치 및 해체",
"pum_form": "requirement",
"line": 6943
},
{
"pum_table_id": "F0405",
"section": "13-3. 기초다짐 및 뒤채움’ 항을 적용한다.",
@@ -700,6 +784,12 @@
"pum_form": "requirement",
"line": 7185
},
{
"pum_table_id": "F0424",
"section": "13-8. 막돌쌓기",
"pum_form": "requirement",
"line": 7415
},
{
"pum_table_id": "F0430",
"section": "13-10-2. 나무 말뚝박기",
@@ -730,6 +820,12 @@
"pum_form": "requirement",
"line": 7745
},
{
"pum_table_id": "F0449",
"section": "13-16-2. 통기성매트",
"pum_form": "requirement",
"line": 7788
},
{
"pum_table_id": "F0454",
"section": "8-2-3 굴착기(2025)」를 참조하여 적용계수를 달리 적용하도록 한다.",
File diff suppressed because it is too large Load Diff