Merge remote-tracking branch 'origin/main_laptop_1' into sub_desktop_1

충돌은 **뽑아서 생기는 파일 넷**(공종 마스터 둘 · 이름표 둘)뿐 — 손으로 쓴 자리가 없어
랩탑 쪽을 취함. 랩탑이 판정 코드를 고쳐 뽑은 것이라 그쪽이 새 것임(형태 275 → 196).
곧바로 다시 뽑아 덮음.
This commit is contained in:
2026-09-18 13:22:34 +09:00
13 changed files with 1379 additions and 3283 deletions
@@ -212,13 +212,33 @@ def build() -> tuple[
require_code_edition(data["effective_date"], "공종 마스터 빌드 — 표 형태·부모 모양 판정")
tables = data["variables"]["pum"]["tables"]
# 원문을 함께 연다 — 밑수가 표 밖(본문)에 있기 때문이다. 못 열면 밑수 없이 간다.
source_lines: list[str] = []
for entry in data.get("sources") or []:
candidate = ROOT / str(entry.get("path") or "")
if candidate.is_file():
source_lines = candidate.read_text(encoding="utf-8").splitlines()
break
# 원문을 함께 연다 — 밑수·[주]가 표 밖(본문)에 있기 때문이다. 못 열면 밑수 없이 간다.
# ⚠ **표마다 제 원문 파일을 본다** — 건설은 원문이 장별로 갈려 있고 `line` 도 그 파일 기준이다.
# 첫 파일만 열어 모든 표의 줄 번호를 맞대면 **남의 장 글을 근거로 읽는다**
# (2026-09-18: 그 탓에 건설 밑수 미판독 79 가 헛으로 세어졌다).
_line_cache: dict[str, list[str]] = {}
def lines_of(table: dict[str, Any]) -> list[str]:
name = str(table.get("source_file") or "")
if name not in _line_cache:
candidate = ROOT / name
if not candidate.is_file():
candidate = candidate.with_suffix(".md")
if not candidate.is_file(): # 산림처럼 표에 파일이 안 적힌 판 — 첫 원천을 쓴다
first = next(
(
ROOT / str(e.get("path") or "")
for e in data.get("sources") or []
if (ROOT / str(e.get("path") or "")).is_file()
),
None,
)
candidate = first or candidate
_line_cache[name] = (
candidate.read_text(encoding="utf-8").splitlines() if candidate.is_file() else []
)
return _line_cache[name]
toc_table = next(t for t in tables if t["table_id"] == "F0001")
nodes = parse_toc(toc_table["rows"])
toc_corrections = renumber_from_body(nodes, tables)
@@ -247,7 +267,7 @@ def build() -> tuple[
if node is not None and not title_names_node(section, node):
node = None # 번호만 겹친 딴 문서 표(부록 사례) — 못 붙인 표 목록으로
entry = table_entry(
table, section, number, chapter, source_lines, node and node["name"], report
table, section, number, chapter, lines_of(table), node and node["name"], report
)
if node is None:
orphans.append({"pum_table_id": table["table_id"], "section": section})
@@ -45,6 +45,13 @@ _ASIDE_HEAD_RE = re.compile(r"비고|적용기준|산출근거|산출식|참고"
COST_ITEMS = {"재료비", "설치비", "노무비", "경비", "제작비", "운송비", "자재비", "주재료비"}
#: 공식 기호 한 글자(아래첨자·숫자 꼬리 포함) — `β` · `E` · `t 1` · `q₀`.
_SYMBOL_RE = re.compile(r"[A-Za-zα-ωΑ-Ω][₀-₉0-9]?")
#: 할증·보정 낱말 — 값이 품이 아니라 **다른 값에 곱하는 율**임을 원문이 이름으로 말한 자리.
_RATE_WORD_RE = re.compile(r"할증|할인|가산|감액|경비의|노무비의|재료비의|기계경비의|효\s*율")
#: 능력(작업량·시공량·소요시간) 낱말 — 「한 시간·하루에 얼마」라 품 표가 아니라 능력 표다.
_CAPACITY_RE = re.compile(
r"작업량|시공량|시공능력|작업능력|처리량|산출량|소요시간|작업시간|"
r"㎥/hr|㎡/hr|본/hr|톤/hr|ton/hr|hr/ton|min/|/hr|/일\)"
)
def _squeeze(text: Any) -> str:
@@ -118,9 +125,78 @@ def judged_const_form(table: dict[str, Any]) -> tuple[str, str] | None:
return "coefficient", f"줄 이름이 공식 기호뿐 {sorted(set(named))[:4]}"
# ⑧ 값이 모두 백분율 — 품·자재 수량이 아니라 요율(층별 할증 `5% 8% 12%`)
# ⚠ 「0」은 백분율 줄의 첫 칸으로 자주 온다(지하층 0 · 4층 5% …) — 셈에서 빼고 본다.
percents = [_squeeze(c) for c in _value_cells(table) if _squeeze(c)]
figures = [x for x in percents if _NUMBERS_RE.match(x) or x.endswith("%")]
if figures and all(x.endswith("%") for x in figures) and len(figures) >= 3:
marks = [x for x in figures if x not in ("0", "0.0")]
if marks and all(x.endswith("%") for x in marks) and len(marks) >= 3:
return "coefficient", "값이 모두 백분율 — 요율표"
# ⑨ 할증·보정 낱말이 줄 이름·머리에 있고 값이 있음 — 율표(수직고별 할증률 · 기계경비의 %)
if found := next((x for x in headers + first_col if _RATE_WORD_RE.search(x)), None):
return "coefficient", f"'{found}' — 할증·보정 율표"
# ⑩ 작업량·시공능력·소요시간 표 — 품이 아니라 **능력**이다(한 시간·하루에 얼마)
if found := next((x for x in headers + first_col if _CAPACITY_RE.search(x)), None):
return "productivity", f"'{found}' — 작업량·시공능력 표"
# ⑪ 기계 기초표 — 가격·손료는 **기계 벌(mach_base_2026.json)이 정본**이라 공종 품이 아님
if any("가격" in h for h in headers) and any("분류번호" in h for h in headers):
return "reference", "기계 가격표 — 기계 벌이 정본"
if any("시간당" in h for h in headers) and any("분류번호" in h for h in headers):
return "reference", "기계 손료표 — 기계 벌이 정본"
# ⑫ 머리 칸이 시공능력 공식 기호 — `K`·`E`·`f`·`t(분)` (건설 8-2 기계 시공능력 밑값)
if found := next((h for h in headers if _FORMULA_HEAD_RE.fullmatch(h)), None):
return "coefficient", f"머리가 공식 기호 '{found}' — 시공능력 밑값"
# ⑬ 줄 이름이 시간 — 「시간(분)」·「사이클타임」은 품이 아니라 **걸리는 시간**이다
if found := next((x for x in first_col if _TIME_ROW_RE.search(x)), None):
return "productivity", f"줄 이름 '{found}' — 걸리는 시간"
# ⑭ 줄 이름이 사용량·소요량 — 자재 소요량 표(`사용량(kg) | 6.5 | 8.0`)
if found := next((x for x in first_col if _AMOUNT_ROW_RE.search(x)), None):
return "requirement", f"줄 이름 '{found}' — 자재 소요량"
# ⑮ 다짐 기준표 — 두께·횟수·규격을 정한 **시방 기준**이지 품이 아님(건설 8-2-9 롤러)
if sum(1 for h in headers if _COMPACT_RE.search(h)) >= 2:
return "reference", "다짐 두께·횟수 기준표 — 시방 기준"
return None
#: 머리 한 칸이 공식 기호뿐 — 건설 8-2 시공능력 식의 밑값(`K`·`E`·`f`·`t(분)`·`C(분)`).
_FORMULA_HEAD_RE = re.compile(r"[A-Za-zα-ωΑ-Ω][₀-₉0-9]?(?:\([^)]{1,4}\))?")
#: 줄 이름이 「걸리는 시간」 — 품(사람·날)이 아니라 능력 쪽 값.
_TIME_ROW_RE = re.compile(r"시간\(|\(분\)|사이클|주기\(")
#: 줄 이름이 자재 쓰는 양.
_AMOUNT_ROW_RE = re.compile(r"사용량|소요량|사용수량")
#: 다짐 기준표 머리 — 두께·횟수·규격을 정한 시방 기준.
_COMPACT_RE = re.compile(r"다짐두께|다짐횟수|다짐기계|다짐")
#: 머리에 구간이 적힌 표 — 「0∼10m」·「10m미만」처럼 **조건으로 갈리는** 표.
_RANGE_RE = re.compile(r"[~]|미만|이상|이하|초과")
#: 규격·치수 단위 — 값이 이것뿐이면 품이 아니라 **규격 배정·기준**이다.
_SPEC_UNIT_RE = re.compile(r"^[\d,.]+\s*(?:㎜|mm|㎝|m|㎡|㎥|inch|인치|kg|㎏|톤|ton|개|주|본)$")
def const_undetermined_kind(table: dict[str, Any]) -> str:
"""못 가른 까닭을 **갈래로** 적는다(2026-09-18 브레인 — 남은 것을 눈으로 가르기 위해).
⚠ 갈래는 「어디를 더 봐야 하나」를 가리킬 뿐, 형태를 짐작해 채우지 않는다.
"""
headers = [_squeeze(h) for h in table.get("headers") or []]
rows = table.get("rows") or []
first_row = [_squeeze(c) for c in (rows[0] if rows else [])]
values = [_squeeze(c) for c in _value_cells(table) if _squeeze(c)]
if not _has_number(values):
return "값이 없음 — 설명·서식·이름 목록"
if first_row and not any(_NUMBER_RE.match(c) for c in first_row) and sum(1 for c in first_row if c) >= 2: # fmt: skip
return "머리가 두 단 — 아래 단이 하위 구분(잣대가 못 폄)"
if any(_RANGE_RE.search(h) for h in headers):
return "머리가 구간 — 조건으로 갈리는 표"
figures = [c for c in values if _NUMBERS_RE.match(c) or _SPEC_UNIT_RE.match(c)]
if figures and all(_SPEC_UNIT_RE.match(c) for c in figures):
return "값이 규격·치수뿐 — 기준표(원문이 품을 안 적음)"
return "그 밖 — 사람이 봐야 함"
@@ -15,7 +15,10 @@ from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Forms import (
judged_basis,
judged_form,
)
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_FormsConst import judged_const_form
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_FormsConst import (
const_undetermined_kind,
judged_const_form,
)
# ── 형태 판정 ────────────────────────────────────────────────────────────
# 헤더·비고 문자열에서 찾는 표지. 앞의 것이 먼저 걸린다(생산량형이 더 좁은 표현이라 우선).
@@ -198,7 +201,8 @@ def detect_form(table: dict[str, Any], chapter: str | None) -> tuple[str, str]:
# ⚠ 여기까지 왔다는 것은 산림 판정이 이미 못 가렸다는 뜻이라, 산림 결과는 흔들리지 않는다.
if hit := judged_const_form(table):
return hit[0], f"건설 꼴 — {hit[1]}"
return "undetermined", "헤더·첫 행에 단위·밑수·직종 표지 없음"
# 못 가른 것은 **갈래를 적어** 남긴다 — 「어디를 더 봐야 하나」가 보여야 줄일 수 있다.
return "undetermined", f"단위·밑수·직종 표지 없음 · {const_undetermined_kind(table)}"
BASIS_RE = re.compile(r"(\d[\d,.]*)\s*(㎥|m3|㎡|m2|㏊|ha|km|㎞|m|인|본|개|kg|㎏|톤|ton)\s*당")
@@ -218,8 +222,12 @@ SOURCE_BASIS_RATIO_RE = re.compile(
#: 밑수 단위 — 건설은 「일·기·조·명·틀」도 씀(2026-09-18 브레인 · 건설 밑수 미판독 162).
#: ⚠ 「회」는 넣지 않는다 — 2026-09-17 데스크탑 서브가 원문을 열어 **밑수 단위가 아니라고** 판정(10-4 중기운반).
#: 2026-09-18 건설 미판독 101 을 보고 넓힘 — 원문이 세는 물건이 그대로 밑수인 자리.
#: `(1대당)` · `(층당)` · `(set당)` · `(Joint당)` · `(1발파당)`.
#: ⚠ 「단위당」은 안 넣는다 — 무엇의 단위인지 원문이 안 적었다.
_BASIS_UNIT = (
"공㎥|㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대|일|기|조|명|틀"
"|층|set|SET|Joint|joint|발파|구멍|孔|㎢|km2|도엽|ea|EA"
)
#: ⚠ **괄호 앞뒤에 글이 붙은 건설 꼴**도 읽는다 — `(용접개소당)` · `(세면기 개당)` · `(절단길이 10m당)` ·
#: `(m당 : 관길이기준)`. 산림은 괄호 안이 밑수뿐이라 이 넓히기로 값이 달라지지 않는다.
@@ -231,6 +239,8 @@ SOURCE_BASIS_RE = re.compile(
)
#: 한 괄호 안의 밑수 토막 — 둘 이상이면 못 고른다(`(1ha당, 100본당)`).
_BASIS_TOKEN_RE = re.compile(rf"[\d,]*\.?\d*\s*(?:{_BASIS_UNIT})\s*당")
#: 「당」이 안 붙은 단위까지 센다 — `1일 1대당` 처럼 **단위가 둘인 칸**을 가려내는 데 쓴다.
_BASIS_UNIT_TOKEN_RE = re.compile(rf"[\d,]+\s*(?:{_BASIS_UNIT})")
#: 표 위로 몇 줄까지 거슬러 볼 것인가. 더 올라가면 **앞 표의 밑수**를 잘못 물어 온다.
SOURCE_LOOKBACK = 6
@@ -749,6 +759,21 @@ def table_entry(
if not absent and basis_qty is None and basis_unit is None:
basis_qty, basis_unit = detect_basis(table)
basis_source = "표 안" if basis_unit else None
# ⚠ 원문이 **표 머리 칸에** 밑수를 적어 둔 자리도 읽는다 — 「(단위 : ㎢)」·「(도엽당)」
# (2026-09-18 건설 미판독 97 을 갈라 보니 이 꼴이 열넷).
if not absent and basis_unit is None:
cells = [*(table.get("headers") or [])]
cells += [c for row in (table.get("rows") or [])[:2] for c in row]
for cell in cells:
text = norm(cell)
# ⚠ 한 칸에 단위가 둘이면(`손료계수 (1일 1대당)`) 어느 쪽이 밑수인지 못 가린다 —
# 한쪽만 집으면 조용히 틀리므로 **안 읽고 넘긴다**.
if len(_BASIS_UNIT_TOKEN_RE.findall(text)) > 1:
continue
qty, unit = basis_from_source([text, "", "|"], 3)
if unit:
basis_qty, basis_unit, basis_source = qty, unit, "표 머리"
break
elif basis_source is None:
basis_source = "본문"
# 본문·표 안에 없을 때만 **절 이름**을 본다(4-2-2 「1,000㎡당」) — 이름 전체가 밑수일 때만.
@@ -776,9 +801,14 @@ def table_entry(
"line": table.get("line"),
# ⚠ 「원문 부재」는 **채울 것이 아니다** — 없는 값을 지어내면 안 된다.
# 「미판독」만 진짜 할 일이다(2026-09-18 브레인 · 안티그래비티 3바퀴).
# ⚠ **작업조 표는 밑수가 애초에 표 밖이다** — 하루 편성 인원·장비를 적고
# 시공량은 [주]·다른 표에 있다. 「미판독」으로 세면 없는 할 일이 생긴다
# (2026-09-18 건설 미판독 101 가운데 79 가 이 꼴이었다).
"why": absent
or (
"미판독 — 원문이 적었는데 우리가 못 읽음"
"원문 부재 — 작업조 표(하루 편성) · 밑수는 표 밖의 시공량"
if crew_table(table)
else "미판독 — 원문이 적었는데 우리가 못 읽음"
if source_declares_basis(source_lines, int(table.get("line") or 0), table)
else "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
),
@@ -6363,7 +6363,7 @@
"name_ko": "밑수 못 찾은 표",
"summary": "기준 수량을 못 읽은 품셈 표. 1 단위당으로 단정하면 곱셈이 틀린다.",
"shape": "list",
"rows": 81,
"rows": 79,
"columns": [
{
"key": "pum_table_id",
@@ -7267,7 +7267,7 @@
"name_ko": "형태 미판정 표",
"summary": "생산량형인지 소요량형인지 못 정한 표.",
"shape": "list",
"rows": 275,
"rows": 196,
"columns": [
{
"key": "pum_table_id",
@@ -7323,7 +7323,7 @@
"name_ko": "밑수 미확보 표",
"summary": "기준 수량을 못 찾은 표.",
"shape": "list",
"rows": 439,
"rows": 431,
"columns": [
{
"key": "pum_table_id",
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-18T12:38:26+09:00",
"generated_at": "2026-09-18T12:53:57+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": "00bffb68105d4be348c92aa6d7c436eedb85170394d4f35ea68a814ae8fb46c1",
"size_bytes": 1095109
"sha256": "41a9ebd574ed425ca9cc180f3b9b5cbc3fa603d3e9a94e4731f2db275fb101fe",
"size_bytes": 1095122
},
{
"file": "form_undetermined_2026-01-01.json",
@@ -22,8 +22,8 @@
},
{
"file": "basis_missing_2026-01-01.json",
"sha256": "ab1fef3d606fefb142d1d4c2af6bba82e3ab3a6b03d05f9ab358f67d490a86df",
"size_bytes": 18830
"sha256": "57c58458ef2a95aa67a5dd2e15989f2613c628e7c697c84d4a944349fa102422",
"size_bytes": 18434
}
]
}
@@ -1,29 +1,29 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-18T12:39:43+09:00",
"generated_at": "2026-09-18T12:54:47+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_const",
"effective_date": "2026-01-01",
"sha256": "207129f59d95baed23a3a360c262a7250fb17fc44eada0c321d07825c410ee20",
"sha256": "b8c85e6ab5b242d5d82a42ff761ec4e144309a1389bc267ea2f1ceabc3d4b8f1",
"file": "pum_const_2026.json"
},
"files": [
{
"file": "const_work_item_master_2026-01-01.json",
"sha256": "31cf0d289351d45a804d2d2cf0b6c489786eabc69e685b8e413d3a00ec4e1fcc",
"size_bytes": 4708119
"sha256": "a3862986f252c2c395d765a879cd2e60899f011559268d27539bb051930738d1",
"size_bytes": 4712576
},
{
"file": "const_form_undetermined_2026-01-01.json",
"sha256": "5a526986ac85619a3dd0b8b377a39a36ce18ed272641215089ab47cc45834570",
"size_bytes": 152983
"sha256": "b1b4619532c561bb17e53881e5732548a0f8e666dee7f841d9507f1ba760dddf",
"size_bytes": 115516
},
{
"file": "const_basis_missing_2026-01-01.json",
"sha256": "90ef86c25df7e1f7e7b120aeeaec6813bf5b1f690d46e4fd2178d1e22f4497b5",
"size_bytes": 111824
"sha256": "bdc0720276122659f931f7144cce751aa18ea09d706ca7eb34a438b5e969bf7e",
"size_bytes": 110609
}
]
}
@@ -417,20 +417,6 @@
"line": 4373,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "F0228",
"section": "8-6-2. 드론방제",
"pum_form": "requirement",
"line": 4541,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "F0229",
"section": "8-6-2. 드론방제",
"pum_form": "requirement",
"line": 4561,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "F0230",
"section": "8-6-3. 지상방제",
@@ -88,13 +88,6 @@
"line": 1109,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0148",
"section": "2-12-2 마스트 설치 및 해체('23년 신설)(층당)",
"pum_form": "requirement",
"line": 1162,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0150",
"section": "2-12-3 축중계 설치 및 해체('09년 신설, '10년 보완)(회당)",
@@ -207,6 +200,13 @@
"line": 118,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0258",
"section": "2-2-2 주요자재 / 강재류’를 적용한다.5-1-7 어스앵커 공법('20, '26년 보완)",
"pum_form": "productivity",
"line": 142,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0263",
"section": "5-1-9 주열식 현장벽체공법(CIP,Cast In Placed Pile)('26년 신설)",
@@ -235,6 +235,13 @@
"line": 271,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0269",
"section": "5-1-9 주열식 현장벽체공법(CIP,Cast In Placed Pile)('26년 신설)",
"pum_form": "productivity",
"line": 291,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0271",
"section": "5-1-10 차수그라우팅('26년 신설)",
@@ -340,6 +347,13 @@
"line": 670,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0303",
"section": "85-3 말뚝5-3-1 기성말뚝 기초('99년 신설, '15, '16, '20, '26년 보완)",
"pum_form": "productivity",
"line": 698,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0306",
"section": "5-3-2 말뚝박기용 천공('08, '15, '16, '20, '25, '26년 보완)",
@@ -389,6 +403,13 @@
"line": 869,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0322",
"section": "5-3-5 현장타설말뚝('15, '21, '26년 보완)",
"pum_form": "productivity",
"line": 927,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0324",
"section": "5-3-5 현장타설말뚝('15, '21, '26년 보완)",
@@ -424,13 +445,6 @@
"line": 487,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0376",
"section": "6-3-10 신축이음(Expansion Joint) 설치('18년 신설)",
"pum_form": "requirement",
"line": 561,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0421",
"section": "8-1-2 공사규모별 표준건설기계('04, '17, '25년 보완)",
@@ -459,6 +473,20 @@
"line": 272,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0447",
"section": "8-2-6 모터 스크레이퍼Q = ⋅q⋅f⋅E cm여기서Q: 시간당 작업량(㎥/hr)q: 적재함용적×적재계수(k)f: 체적환산계수E: 작업효율cm : 1회 싸이클 시간",
"pum_form": "productivity",
"line": 440,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0449",
"section": "8-2-6 모터 스크레이퍼Q = ⋅q⋅f⋅E cm여기서Q: 시간당 작업량(㎥/hr)q: 적재함용적×적재계수(k)f: 체적환산계수E: 작업효율cm : 1회 싸이클 시간",
"pum_form": "productivity",
"line": 479,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0450",
"section": "8-2-6 모터 스크레이퍼Q = ⋅q⋅f⋅E cm여기서Q: 시간당 작업량(㎥/hr)q: 적재함용적×적재계수(k)f: 체적환산계수E: 작업효율cm : 1회 싸이클 시간",
@@ -480,6 +508,27 @@
"line": 861,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0475",
"section": "8-2-15 법면다짐기",
"pum_form": "productivity",
"line": 894,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0484",
"section": "8-2-19 콘크리트 운반",
"pum_form": "productivity",
"line": 1006,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0491",
"section": "8-2-19 콘크리트 운반",
"pum_form": "productivity",
"line": 1092,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0493",
"section": "8-2-19 콘크리트 운반",
@@ -1033,20 +1082,6 @@
"line": 426,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0804",
"section": "1-8-2 도로 표지판 설치('08년 신설, '20년 보완)(일당)",
"pum_form": "requirement",
"line": 606,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0855",
"section": "3-2-5 터널굴착 1발파당 작업인원('07, '20년 보완)(1발파당)",
"pum_form": "requirement",
"line": 149,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0856",
"section": "3-2-5 터널굴착 1발파당 작업인원('07, '20년 보완)(1발파당)",
@@ -1110,13 +1145,6 @@
"line": 168,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C0979",
"section": "7-4-3 배송관 진수(set당)",
"pum_form": "requirement",
"line": 303,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C0980",
"section": "7-4-3 배송관 진수(set당)",
@@ -1544,6 +1572,13 @@
"line": 1844,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1128",
"section": "9-5-4 수치지도 작성('21, '22, '24, '26년 보완)",
"pum_form": "productivity",
"line": 1851,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1131",
"section": "9-5-4 수치지도 작성('21, '22, '24, '26년 보완)",
@@ -1558,6 +1593,13 @@
"line": 1937,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1138",
"section": "9-5-4 수치지도 작성('21, '22, '24, '26년 보완)",
"pum_form": "productivity",
"line": 1952,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1139",
"section": "9-5-4/2. 수동입력'의 품을 적용한다.",
@@ -1705,13 +1747,6 @@
"line": 2889,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1233",
"section": "9-5-4/2. 수동입력’을적용한다.",
"pum_form": "requirement",
"line": 2922,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1246",
"section": "9-5-4 9. 수치표고모형구축’의 ‘가. 항공레이저측량에 의한 방법(레이저지형자료취득, 자료처리, 수치표고모형제작, 정리 및 점검)’ 공정에 한하여 적용한다.",
@@ -1747,48 +1782,6 @@
"line": 3171,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1260",
"section": "9-5-6 지도제작(기본도)",
"pum_form": "requirement",
"line": 3191,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1263",
"section": "9-5-6 지도제작(기본도)",
"pum_form": "requirement",
"line": 3216,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1266",
"section": "9-5-6 지도제작(기본도)",
"pum_form": "requirement",
"line": 3245,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1267",
"section": "9-5-6 지도제작(기본도)",
"pum_form": "requirement",
"line": 3253,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1270",
"section": "9-5-7 토지이용 현황도 제작",
"pum_form": "requirement",
"line": 3283,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1272",
"section": "9-5-7 토지이용 현황도 제작",
"pum_form": "requirement",
"line": 3300,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1274",
"section": "9-5-9 정밀도로지도 구축('19년 신설, '20년 보완)",
@@ -2132,6 +2125,13 @@
"line": 4600,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1412",
"section": "1-1 철골 가공 조립(공장생산)1-1-1 기본철골공수('08, '13년 보완)",
"pum_form": "requirement",
"line": 26,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1415",
"section": "1-1-3 기본용접공수",
@@ -2433,13 +2433,6 @@
"line": 541,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1671",
"section": "13-1-7 장거리 배관('93, '26년 보완)(Joint당)",
"pum_form": "requirement",
"line": 643,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1674",
"section": "13-1-8 이중보온관 설치('26년 보완)",
@@ -2468,41 +2461,6 @@
"line": 1149,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1703",
"section": "13-3 배관 및 기기보온13-3-1 pipe보온('04년 보완)",
"pum_form": "requirement",
"line": 1288,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1704",
"section": "13-3 배관 및 기기보온13-3-1 pipe보온('04년 보완)",
"pum_form": "requirement",
"line": 1319,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1705",
"section": "13-3 배관 및 기기보온13-3-1 pipe보온('04년 보완)",
"pum_form": "requirement",
"line": 1345,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1706",
"section": "13-3 배관 및 기기보온13-3-1 pipe보온('04년 보완)",
"pum_form": "requirement",
"line": 1376,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1707",
"section": "13-3 배관 및 기기보온13-3-1 pipe보온('04년 보완)",
"pum_form": "requirement",
"line": 1402,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1720",
"section": "13-4-3 STORAGE TANK",
@@ -2552,13 +2510,6 @@
"line": 1726,
"why": "원문 부재 — 원문에 밑수가 아예 없음(채우면 지어내는 꼴)"
},
{
"pum_table_id": "C1731",
"section": "13-5-2 보일러 드럼 설치(대당)",
"pum_form": "requirement",
"line": 1739,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C1732",
"section": "13-5-2 보일러 드럼 설치(대당)",
@@ -3063,13 +3014,6 @@
"line": 162,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C2130",
"section": "2-2-13 도상갱환('11년 신설)",
"pum_form": "requirement",
"line": 1088,
"why": "미판독 — 원문이 적었는데 우리가 못 읽음"
},
{
"pum_table_id": "C2268",
"section": "3-1-5 석축 헐기(인력)('22년 보완)",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
"effective_date": "2026-01-01",
"pum_edition": "2026-01-01",
"toc_edition": "산림청고시제2025-82호",
"generated_at": "2026-09-18T12:38:26+09:00",
"generated_at": "2026-09-18T12:53:57+09:00",
"dataset_version": {
"dataset_id": "pum_forest",
"effective_date": "2026-01-01",
@@ -25,10 +25,10 @@
"tables_attached": 453,
"tables_orphan": 22,
"form_undetermined": 0,
"basis_found": 251,
"basis_missing": 81,
"basis_found": 253,
"basis_missing": 79,
"basis_absent_in_source": 71,
"basis_unparsed": 10,
"basis_unparsed": 8,
"basis_grouped": 59,
"axis_roles": {
"general_provision": 78,
@@ -21171,9 +21171,9 @@
"source_line": 4541,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 머리",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -21262,9 +21262,9 @@
"source_line": 4561,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 머리",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -56,26 +56,6 @@
"class": "㉰ 사용자 판단 필요",
"class_why": "⚠ 마스터 「1km」는 반쪽 — 원문은 「인/1일 1km당」으로 분모가 둘(일·km)"
},
"F0228": {
"section": "8-6-2",
"md": [
1.0,
"ha"
],
"md_evidence": "표·머리 무인헬기 (ha당)",
"master": null,
"why": "원문 줄 칸 「무인헬기 (ha당)」 — 마스터가 아직 안 읽음(2026-09-09 「인」 오독 판정과 같은 자리, 밑수는 ha)"
},
"F0229": {
"section": "8-6-2",
"md": [
1.0,
"ha"
],
"md_evidence": "표·머리 멀티콥터 (ha당)",
"master": null,
"why": "위와 같음(멀티콥터)"
},
"F0322": {
"section": "10-13-1",
"md": null,
@@ -68,7 +68,7 @@ MASTER = OUT_DIR / "work_item_master_2026-01-01.json"
#: 2026-09-18 합침(랩탑 메인 · 데스크탑 서브 9f27d619 와 한 점) — 위 넓힘 + 데스크탑 서브의
#: F0046·F0068 밑수 되돌림·F0076 형태 판정·원천 벌 재추출을 한 벌로 다시 뽑아 지문이 또 뜸
#: (옛 837eb8aa… · 옛 6f223c69…). 양쪽 다 **원문을 더 읽은 것**이고 금액은 그대로다.
BASELINE_WORK_ITEMS_SHA = "645798a58bcd8d283653192d4da8b5be40e0b88474b8e8c3bbab77174b83eb7d"
BASELINE_WORK_ITEMS_SHA = "7919a44eab4ecf9a09edb39f0c4b48f5576caa8c5d491fe6d3e5fb89ba655436"
#: 열쇠 작업이 새로 얹은 칸. 지문을 잴 때만 떼어낸다.
ADDED_FIELDS = (
@@ -482,14 +482,15 @@ def test_밑수_미확보를_둘로_가른다(master: dict) -> None:
from pathlib import Path as _Path
stats = master["stats"]
assert stats["basis_missing"] == 81
assert stats["basis_missing"] == 79
# 2026-09-18 합침 70 → 71 — 원문 md 가 풀리며 밑수를 안 적은 표가 하나 더 드러남
assert stats["basis_absent_in_source"] == 71
# 2026-09-18 「당」 없이 몫으로만 적힌 자리(「(인/100㎡)」·「(대/ton)」)를 가리게 해 다섯을 더 채움.
# 미판독 12 → 9 → 10(가림이 넓어져 새로 드러난 것 포함) · 확보 243 → 251.
# ⚠ 남은 아홉은 **거절 자리**다 — 「(단위 : 인)」 셋(2026-09-09 오독 판정) ·
# 「㎥/1인/1일」·「1ha당, 100본당」·「1대 1조, 시간당」·「인 당」은 분모가 둘 이상이다.
assert stats["basis_unparsed"] == 10
# 2026-09-18 10 → 8 — **표 머리 칸까지 읽게 하자** 8-6-2·8-6-3 드론방제 「ha당」 둘이 채워짐
assert stats["basis_unparsed"] == 8
assert stats["basis_absent_in_source"] + stats["basis_unparsed"] == stats["basis_missing"]
items = _json.loads(
_Path("resources/data_work_item_master/basis_missing_2026-01-01.json").read_text(