diff --git a/resources/data_cost_input_value/_build_mach.py b/resources/data_cost_input_value/_build_mach.py index 01847b15..74926de0 100644 --- a/resources/data_cost_input_value/_build_mach.py +++ b/resources/data_cost_input_value/_build_mach.py @@ -123,7 +123,10 @@ def names_in(lines: list[str]) -> dict[str, str]: continue for m in _NAME_AT.finditer(line): name = m.group(2).strip(" .·") - if name and not name[0].isdigit(): + # ⚠ 숫자로 시작하는 이름도 있다 — `(8201) 3D GNSS 머신 가이던스(굴착기용)`. + # 첫 낱자가 숫자면 버리던 잣대가 그것을 쪽번호로 보고 내버려, 옛 벌의 뭉개진 글을 + # 이어받고 있었다(2026-09-18). 낱자가 하나도 없는 토막만 버린다. + if name and re.search(r"[가-힣A-Za-z]", name): found.setdefault(m.group(1), re.sub(r"\s+", " ", name)) return found diff --git a/resources/data_cost_input_value/mach_base_2026.json b/resources/data_cost_input_value/mach_base_2026.json index e979cbe7..f219ed1c 100644 --- a/resources/data_cost_input_value/mach_base_2026.json +++ b/resources/data_cost_input_value/mach_base_2026.json @@ -2,7 +2,7 @@ "schema_version": "1.0", "dataset_id": "mach_base", "effective_date": "2026-01-01", - "generated_at": "2026-09-18T14:37:18+09:00", + "generated_at": "2026-09-18T17:02:43+09:00", "sources": [ { "path": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제8장_건설기계.md", @@ -3177,7 +3177,7 @@ }, { "machine_code": "8201-0100", - "machine_name": "", + "machine_name": "3D GNSS 머신 가이던스(굴착기용)", "price_thousand_krw": 55000, "specification": "3D GNSS MG" }, @@ -11565,7 +11565,7 @@ }, { "machine_code": "8201-0100", - "machine_name": "배관파이프시 간 당(10-7)분류번호규 격7995-0050ø50-2.6m5,0008-3-9 [80]스마트 건설장비(8201) 3D GNSS 머신 가이던스(굴착기용)", + "machine_name": "3D GNSS 머신 가이던스(굴착기용)", "specification": "3D GNSS MG", "loss_coefficient_per_hour": 0.00039299999999999996, "depreciation_coefficient_1e_minus_7": 1800, diff --git a/resources/tester/fixtures/base_prices_source_known.json b/resources/tester/fixtures/base_prices_source_known.json index 145d5bd3..170238c1 100644 --- a/resources/tester/fixtures/base_prices_source_known.json +++ b/resources/tester/fixtures/base_prices_source_known.json @@ -11,16 +11,6 @@ ], "labor": {}, "machine": { - "가격 8201-0100 machine_name": { - "source": "3D GNSS 머신 가이던스(굴착기용)", - "data": "", - "why": "자료 이름이 빔 — 원문 손료표 머리 `(코드) 이름` 엔 이름 있음", - "class": "㉯ 남이 고쳐야 풀림", - "owner": "랩탑 메인", - "owner_ask": "기계 원천 벌(mach_base_2026.json) 재추출 — 그 추출기가 이 저장소에 없어 우리가 못 돌림", - "갈래": "잣대 못 알아봄", - "class_why": "제8장 8-3 (8201) 손료표 머리가 글로 뭉개져 기종 이름이 표 밖 글줄에 섞였음 — PDF 대조로 표 머리를 되살려 줄 것. 되살리면 추출기·시험이 저절로 읽음." - }, "셈 연료 원문에 없는 자료 줄": { "source": 0, "data": 1, @@ -41,46 +31,6 @@ "갈래": "잣대 못 알아봄", "class_why": "위 7993-0020 과 한 자리 — 그 줄이 고쳐지면 셈도 맞음." }, - "손료 8201-0100 machine_name": { - "source": "3D GNSS 머신 가이던스(굴착기용)", - "data": "배관파이프시 간 당(10-7)분류번호규 격7995-0050ø50-2.6m5,0008-3-9 [80]스마트 건설장비(8201) 3D GNSS 머신 가이던스(굴착기용)", - "why": "자료 이름 칸에 원문 뭉개진 손료 글이 통째로 들어감 — 자료 이름 틀림", - "class": "㉯ 남이 고쳐야 풀림", - "owner": "랩탑 메인", - "owner_ask": "기계 원천 벌(mach_base_2026.json) 재추출 — 그 추출기가 이 저장소에 없어 우리가 못 돌림", - "갈래": "잣대 못 알아봄", - "class_why": "위와 한 자리 — 뭉개진 글이 자료 이름 칸에 통째로 들어갔음." - }, - "손료보정 bulldozer_under_19_ton": { - "source": "불도저(19톤이상제외)", - "data": "2510", - "why": "원문 8-1-7 가산비율 표가 글로 뭉개져 이름 칸에 수(`2510`)가 들어앉음 — 25·10 인지 2·510 인지 못 가름(같은 자리를 「손료보정 수 자리 가름」으로 이미 적어 둠). md 가 풀리면 저절로 맞음", - "class": "㉯ 남이 고쳐야 풀림", - "갈래": "잣대 못 알아봄", - "class_why": "원문 8-1-7 가산비율 표가 **글로 뭉개져** 기종 이름 칸에 수(`2510`)가 들어앉음 — 25·10 인지 2·510 인지 가를 수 없음. 값은 원문에 있으나 꼴이 뭉개져 못 읽는 자리(같은 뿌리를 「손료보정 수 자리 가름」 으로도 셈).", - "owner": "랩탑 메인", - "owner_ask": "8-1-7 가산비율 표가 글로 뭉개진 자리 — 기종 이름과 두 수(암석작업·전석섞인토사)를 갈라 줄 것. `손료보정 수 자리 가름` 과 한 자리라 함께 풀림." - }, - "손료보정 crawler_excavator_or_loader": { - "source": "굴착기(무한궤도)및로더(무한궤도)", - "data": "2010", - "why": "원문 8-1-7 가산비율 표가 글로 뭉개져 이름 칸에 수(`2510`)가 들어앉음 — 25·10 인지 2·510 인지 못 가름(같은 자리를 「손료보정 수 자리 가름」으로 이미 적어 둠). md 가 풀리면 저절로 맞음", - "class": "㉯ 남이 고쳐야 풀림", - "갈래": "잣대 못 알아봄", - "class_why": "원문 8-1-7 가산비율 표가 **글로 뭉개져** 기종 이름 칸에 수(`2510`)가 들어앉음 — 25·10 인지 2·510 인지 가를 수 없음. 값은 원문에 있으나 꼴이 뭉개져 못 읽는 자리(같은 뿌리를 「손료보정 수 자리 가름」 으로도 셈).", - "owner": "랩탑 메인", - "owner_ask": "8-1-7 가산비율 표가 글로 뭉개진 자리 — 기종 이름과 두 수(암석작업·전석섞인토사)를 갈라 줄 것. `손료보정 수 자리 가름` 과 한 자리라 함께 풀림." - }, - "손료보정 dump_truck": { - "source": "덤프트럭", - "data": "2510", - "why": "원문 8-1-7 가산비율 표가 글로 뭉개져 이름 칸에 수(`2510`)가 들어앉음 — 25·10 인지 2·510 인지 못 가름(같은 자리를 「손료보정 수 자리 가름」으로 이미 적어 둠). md 가 풀리면 저절로 맞음", - "class": "㉯ 남이 고쳐야 풀림", - "갈래": "잣대 못 알아봄", - "class_why": "원문 8-1-7 가산비율 표가 **글로 뭉개져** 기종 이름 칸에 수(`2510`)가 들어앉음 — 25·10 인지 2·510 인지 가를 수 없음. 값은 원문에 있으나 꼴이 뭉개져 못 읽는 자리(같은 뿌리를 「손료보정 수 자리 가름」 으로도 셈).", - "owner": "랩탑 메인", - "owner_ask": "8-1-7 가산비율 표가 글로 뭉개진 자리 — 기종 이름과 두 수(암석작업·전석섞인토사)를 갈라 줄 것. `손료보정 수 자리 가름` 과 한 자리라 함께 풀림." - }, "연료 7993-0020": { "source": null, "data": { @@ -167,15 +117,6 @@ "owner": "랩탑 서브", "owner_ask": "제8장 8-3 (7430)·(7431) 윈치 · (7811)·(7812) 엔진 · (9030) 예선 표 — PDF 에서 「기종」 칸이 한 칸에 어떻게 적혀 있는지 짚어 줄 것(「수동 싱글드럼」 처럼 한 칸인지, 줄마다 다른 이름인지). 그것이 갈리면 자료의 이어받기가 맞는지 곧 판가름남." }, - "손료보정 수 자리 가름": { - "what": "뭉개진 글의 이어 붙은 수(2510)를 통째로 맞댐", - "why": "8-1-7 가산비율 표가 글로 뭉개져 `2510` 처럼 붙음 — 25·10 인지 2·510 인지 못 가름", - "class": "㉯ 남이 고쳐야 풀림", - "owner": "랩탑 메인", - "owner_ask": "기계 원천 벌(mach_base_2026.json) 재추출 — 그 추출기가 이 저장소에 없어 우리가 못 돌림", - "갈래": "잣대 못 알아봄", - "class_why": "8-1-7 가산비율 표가 글로 뭉개져 `2510` 처럼 붙었음 — 25·10 인지 2·510 인지 PDF 로 갈라 줄 것." - }, "운전사 기종별 판단": { "what": 121, "why": "기종→운전사 구분(explicit_mappings)은 사람 판단 — 원문 8-1-3 5호는 기종 이름 글만 있음", @@ -554,7 +495,7 @@ "센 날": "2026-09-18", "셈": { "원문 부재 / ㉰": 7, - "잣대 못 알아봄 / ㉯": 16 + "잣대 못 알아봄 / ㉯": 10 }, "잣대": "두 갈래로 가름 — **원문 부재**: 값이 원문·원천에 없거나 원문 밖 말이라 채울 것이 없음 · **잣대 못 알아봄**: 값은 원문에 있는데 전사·읽기가 못 가름 ⇒ 고치면 풀림. ㉮ 영영 할 일 아님 · ㉯ 남이 고쳐야 풀림 · ㉰ 사용자·브레인 판단.", "할 일 아님으로 뺀 줄": 34 diff --git a/resources/tester/test_base_prices_source_match.py b/resources/tester/test_base_prices_source_match.py index 788a4185..e4669890 100644 --- a/resources/tester/test_base_prices_source_match.py +++ b/resources/tester/test_base_prices_source_match.py @@ -343,7 +343,8 @@ def read_machine(path: Path) -> dict: return out -#: 손료보정 표는 글로 뭉개짐 — 기종 글 바로 뒤에 `암석작업` `전석섞인토사` 두 수가 붙어 있음(`덤프트럭2510`). +#: 8-1-7 손료보정 표의 기종 글 — 원문이 낱자 사이에 빈칸을 두므로(`불도저( 1 9 톤이상 제외)`) **빈칸을 지워** 맞댄다. +#: 2026-09-18 md 가 제 표로 고쳐져 두 수(암석작업·전석섞인토사)를 **칸에서 바로** 읽는다(전엔 `덤프트럭2510` 처럼 글로 뭉개져 있었다). ROCK = {"bulldozer_under_19_ton": "불도저(19톤이상제외)", "crawler_excavator_or_loader": "굴착기(무한궤도)및로더(무한궤도)", "dump_truck": "덤프트럭"} # fmt: skip @@ -379,6 +380,11 @@ def check_machine(src: dict, bad: dict, blind: dict) -> dict: counts[f"{label} 코드 수"] = (len(rows), len(by_code)) counts[f"{label} 원문에 없는 자료 줄"] = (0, len(set(by_code) - set(rows) - unread)) _unchecked(blind, var, records, {"machine_code", "machine_name", *fields}) + # 원문 8-3 에 `(코드) 이름` 머리글이 아예 없어 **이름이 빈** 기종 — 양쪽이 다 비어 조용히 지나간다. + # 표는 있는데 이름만 없는 자리라 세어 둔다(md 에 머리글이 붙으면 저절로 빠짐). + for code, rec in sorted({r["machine_code"]: r for r in v["mach_loss_coef"]["records"]}.items()): + if not rec.get("machine_name") and source["names"].get(code[:4]) is None: + blind[f"기종 이름 없는 머리글 {code[:4]}"] = {"what": code} # 원문이 규격 칸을 비워 둔 자리에 **자료가 앞 줄 값을 이어받아 채운** 줄 — 원문에 없는 글이라 사람이 볼 자리. loss_by_code = {r["machine_code"]: r for r in v["mach_loss_coef"]["records"]} for code in sorted(source.get("spec_blank", ())): @@ -412,15 +418,24 @@ def check_machine(src: dict, bad: dict, blind: dict) -> dict: ) text = _t("".join(source["lines"])) + rock_rows = {} # 기종 글(빈칸 지움) → (암석작업, 전석섞인토사) + for t in md_tables(source["lines"]): + if [_t(h) for h in t["headers"]][:2] != ["기종", "가산비율"]: + continue + for r in t["rows"]: + try: + rock_rows[_t(r[0])] = (_num(r[1]), _num(r[2])) + except ValueError: # 머리 곁줄(`암석작업(연암…)` · `전석섞인토사`) + continue for rule in v["mach_rock_adj"]["rules"]: - group, pair = rule["machine_group"], f"{rule['rock_work']}{rule['boulder_mixed_soil']}" + group = rule["machine_group"] + pair = (rule["rock_work"], rule["boulder_mixed_soil"]) if group in ROCK: - ok = f"{ROCK[group]}{pair}" in text - else: - ok = pair == "00" and "불도저(19톤이상)의경우는보정하지않는다" in text - if not ok: - bad[f"손료보정 {group}"] = {"source": ROCK.get(group), "data": pair} - blind["손료보정 수 자리 가름"] = {"what": "뭉개진 글의 이어 붙은 수(2510)를 통째로 맞댐"} + got = rock_rows.get(ROCK[group]) + else: # 「불도저(19톤이상)은 보정하지 않는다」 — 표에 줄이 없고 [주]①이 0 을 말한다 + got = (0, 0) if "불도저(19톤이상)의경우는보정하지않는다" in text else None + if got != pair: + bad[f"손료보정 {group}"] = {"source": got, "data": list(pair)} _unchecked(blind, "mach_rock_adj.rules", v["mach_rock_adj"]["rules"], {"machine_group", "rock_work", "boulder_mixed_soil"}) # fmt: skip return counts