From 4b6d4f141cec0a9453b2f65d637571525b4c6b74 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 17 Sep 2026 18:55:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(z01,b08):=20=EC=9D=B4=EB=A6=84=20=EC=9E=90?= =?UTF-8?q?=EA=B0=84=20=EB=8B=A4=EB=93=AC=EA=B8=B0=C2=B7=ED=92=88=EC=85=88?= =?UTF-8?q?=20=EB=B2=88=ED=98=B8=20=EC=88=AB=EC=9E=90=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=C2=B7=EA=B1=B4=EC=84=A4=20=EB=B9=88=20=EC=B9=B8=20=EA=B0=90?= =?UTF-8?q?=EC=B6=A4=20=E2=80=94=20=ED=95=9C=20=EA=B8=80=EC=9E=90=20?= =?UTF-8?q?=EB=82=B1=EB=A7=90=EB=BF=90=EC=9D=B8=20=EC=A1=B0=EA=B0=81?= =?UTF-8?q?=EC=9D=B4=EB=82=98=20=EC=85=8B=20=EC=9D=B4=EC=83=81=20=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EC=A7=84=20=ED=95=9C=20=EA=B8=80=EC=9E=90=20=EB=82=B1?= =?UTF-8?q?=EB=A7=90=EB=A7=8C=20=EB=B6=99=EC=9E=84(=E3=80=8C=EC=B8=A1=20?= =?UTF-8?q?=EB=9F=89=E3=80=8D=E2=86=92=E3=80=8C=EC=B8=A1=EB=9F=89=E3=80=8D?= =?UTF-8?q?=C2=B7=E3=80=8C=EB=A9=94=20=EB=B6=99=20=EC=9E=84=20=C2=B7=20?= =?UTF-8?q?=EA=B9=AC=20=EB=8F=8C=E3=80=8D=E2=86=92=E3=80=8C=EB=A9=94?= =?UTF-8?q?=EB=B6=99=EC=9E=84=20=C2=B7=20=EA=B9=AC=EB=8F=8C=E3=80=8D=20?= =?UTF-8?q?=C2=B7=20=E3=80=8C=EA=B7=B8=20=EC=99=B8=20=EC=9E=90=EC=9E=AC?= =?UTF-8?q?=E3=80=8D=EB=8A=94=20=EA=B7=B8=EB=8C=80=EB=A1=9C)=20=C2=B7=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EA=B2=BD=EB=A1=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EA=B3=BC=20=EA=B0=88=EB=9E=98=20=EB=8B=A4=EB=93=AC=EC=9D=80=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84(name=5Fclean)=EC=97=90=EB=A7=8C=20=C2=B7=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8=20=EC=9D=B4=EB=A6=84=EC=9D=80=20=EB=A7=88?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EC=97=90=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=C2=B7=20=EC=97=B4=EC=87=A0=EB=8A=94=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EB=9D=BC=20=EC=95=88=20=ED=9D=94=EB=93=A4=EB=A6=BC=EC=9D=84=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=EC=9C=BC=EB=A1=9C=20=EB=AA=BB=EB=B0=95?= =?UTF-8?q?=EC=9D=8C(=EB=8B=A4=EB=93=AC=EA=B8=B0=20=EB=81=88=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EA=B3=BC=20=EC=97=B4=EC=87=A0=20=EB=A7=9E=EB=8C=90=20?= =?UTF-8?q?=C2=B7=20=EC=9E=A5=EB=B6=80=20=EC=83=88=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?0)=20=C2=B7=20=ED=92=88=EC=85=88=20=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=B9=B8=EC=9D=80=20=EB=A7=88=EB=94=94=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EC=84=B8=EC=9B=80(=E3=80=8C10-1=E3=80=8D?= =?UTF-8?q?=EC=9D=B4=20=E3=80=8C2-1=E3=80=8D=20=EB=92=A4)=20=C2=B7=20?= =?UTF-8?q?=EA=B1=B4=EC=84=A4=20=E3=80=8C=EB=8B=A8=EA=B3=84=20=EB=AC=B4?= =?UTF-8?q?=EA=B2=8C=E3=80=8D=EB=8A=94=20=EB=8A=98=20=EB=B9=88=EC=B9=B8?= =?UTF-8?q?=EC=9D=B4=EB=9D=BC=20=EA=B0=90=EC=B6=A4(=EB=B8=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B8=20=E2=91=A5=20=C2=B7=20=EC=84=9C=EB=B8=8C=20=ED=9B=91?= =?UTF-8?q?=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- .../B08_Quantity_Build_WorkItemMaster_Keys.py | 38 +++++- Z01_MasterData/Z01_MasterData_BasePrices.py | 15 ++- Z01_MasterData/Z01_MasterData_WorkItems.py | 11 +- .../data_master_labels/labels_2026-01-01.json | 4 +- .../data_work_item_master/_manifest.json | 6 +- .../_manifest_const.json | 6 +- .../const_work_item_master_2026-01-01.json | 36 ++--- .../work_item_master_2026-01-01.json | 124 +++++++++--------- .../tester/test_work_item_key_registry.py | 3 +- .../tester/test_work_item_master_const.py | 14 ++ resources/tester/test_z01_work_items.py | 36 +++++ 11 files changed, 195 insertions(+), 98 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py index 91fb37ba..d7a709b2 100644 --- a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py @@ -180,10 +180,42 @@ def variant_match(text: str) -> str: return "".join(_TILDES.get(ch, ch) for ch in squeezed) +_SYLLABLE = re.compile(r"^[가-힣]$") +_PIECE_GAP = re.compile(r"( [·ㆍ・›] )") + + +def tidy_spacing(text: str) -> str: + """**자간 공백** 다듬기 — 「측 량」→「측량」 · 「평 규 준 틀」→「평규준틀」 · 「굴 삭 기 (무한궤도)」→「굴삭기 (무한궤도)」. + + 한 글자 낱말뿐인 조각이거나, 한 글자 낱말이 **셋 이상** 이어질 때만 붙임 — + 「그 외 자재」·「호박돌 및 야면석」 같은 진짜 띄어쓰기는 그대로(2026-09-17 서브 훑기 175줄 · 브레인). + ⚠ 보일 이름만 — 열쇠는 번호라(공종 `FW-…` · 갈래 `#01`) 다듬어도 안 흔들림. + """ + # 가운뎃점·경로 표시 사이가 한 조각 — 「메 붙 임 · 깬 돌 · 뒷길이 25㎝」 의 「깬 돌」 + pieces = _PIECE_GAP.split(" ".join(str(text or "").split())) + return "".join(piece if n % 2 else _tidy_piece(piece) for n, piece in enumerate(pieces)) + + +def _tidy_piece(text: str) -> str: + tokens = text.split(" ") + if len(tokens) >= 2 and all(_SYLLABLE.match(t) for t in tokens): + return "".join(tokens) + out: list[str] = [] + run: list[str] = [] + for token in [*tokens, ""]: + if token and _SYLLABLE.match(token): + run.append(token) + continue + out.extend(["".join(run)] if len(run) >= 3 else run) + run = [] + if token: + out.append(token) + return " ".join(out) + + def variant_display(text: str) -> str: - """다듬은 갈래 이름 — 자간 공백을 한 칸으로, 물결표를 하나로. 원문은 따로 남긴다.""" - spaced = " ".join(str(text or "").split()) - return "".join(_TILDES.get(ch, ch) for ch in spaced) + """다듬은 갈래 이름 — 자간 공백을 붙이고 물결표를 하나로. 원문은 따로 남긴다.""" + return "".join(_TILDES.get(ch, ch) for ch in tidy_spacing(text)) def format_variant(serial: int) -> str: diff --git a/Z01_MasterData/Z01_MasterData_BasePrices.py b/Z01_MasterData/Z01_MasterData_BasePrices.py index 8eb68f64..9a3d7342 100644 --- a/Z01_MasterData/Z01_MasterData_BasePrices.py +++ b/Z01_MasterData/Z01_MasterData_BasePrices.py @@ -10,6 +10,7 @@ from __future__ import annotations import json +import re from datetime import date from decimal import Decimal from functools import lru_cache @@ -329,15 +330,21 @@ class SortError(ValueError): """모르는 열로 세우라 함 — 조용히 원본 차례로 돌아가지 않음(브레인 ④).""" +#: 품셈 번호 꼴(「9-3-2」·「12-17」) — 글자로 세우면 「10-1」이 「2-1」 앞에 섬(서브 훑기 · 브레인). +_NUMBER_PATH = re.compile(r"^\d+(?:-\d+)+$") + + def _order_key(value: Any) -> tuple: - """섞인 칸도 세움 — 수 먼저, 그 뒤 글자.""" + """섞인 칸도 세움 — 수 먼저, 품셈 번호 꼴은 **마디마다 수로**, 그 뒤 글자.""" if isinstance(value, bool): - return (1, str(value)) + return (2, str(value)) if isinstance(value, (int, float)): return (0, float(value)) if isinstance(value, (dict, list)): - return (1, json.dumps(value, ensure_ascii=False, default=str)) - return (1, str(value)) + return (2, json.dumps(value, ensure_ascii=False, default=str)) + if isinstance(value, str) and _NUMBER_PATH.match(value): + return (1, tuple(int(part) for part in value.split("-"))) + return (2, str(value)) def _sorted(rows: list[dict[str, Any]], column: str, desc: bool) -> list[dict[str, Any]]: diff --git a/Z01_MasterData/Z01_MasterData_WorkItems.py b/Z01_MasterData/Z01_MasterData_WorkItems.py index 5031ff67..23c8d85b 100644 --- a/Z01_MasterData/Z01_MasterData_WorkItems.py +++ b/Z01_MasterData/Z01_MasterData_WorkItems.py @@ -71,6 +71,13 @@ def _path(item: dict[str, Any] | None, by: dict[str, dict]) -> str: return PATH_JOIN.join(reversed(names)) +def _shown_path(item: dict[str, Any], by: dict[str, dict]) -> str: + """목록에 보일 경로 — 조각마다 자간 공백 다듬기(「토목부문 › 측 량」→「측량」). 열쇠와 무관.""" + from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import tidy_spacing + + return PATH_JOIN.join(tidy_spacing(part) for part in _path(item, by).split(PATH_JOIN)) + + def base_rows(kind: str) -> list[dict[str, Any]]: found = doc(kind) if found is None: @@ -101,7 +108,7 @@ def base_rows(kind: str) -> list[dict[str, Any]]: "@source": name, "work_item_code": item["work_item_code"], # 목차 코드 — 열쇠 아님, 칸으로만 "number": item["number"], - "name": _path(item, by), + "name": _shown_path(item, by), # 자간 공백만 다듬음 — 원문 이름은 마스터에 "variant": variant["name"] if variant else None, # 원문 그대로 "variant_clean": variant["name_clean"] if variant else None, # 다듬은 것 "pum_tables": [t["pum_table_id"] for t in item["tables"]], @@ -156,7 +163,7 @@ def rules(kind: str) -> list[dict[str, Any]]: rule = { "@id": _key(item), "work_item_code": item["work_item_code"], - "name": _path(item, by), + "name": _shown_path(item, by), "mode": mode, "children": [_key(c) for c in children.get(item["work_item_code"], [])], } diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index 0cee9a99..f31e3e44 100644 --- a/resources/data_master_labels/labels_2026-01-01.json +++ b/resources/data_master_labels/labels_2026-01-01.json @@ -1504,8 +1504,8 @@ "key": "step_weight", "name_ko": "단계 무게", "unit": "", - "visible": true, - "note": "`sum_steps` 일 때만 — 건설은 아직 합산형 선언 없음." + "visible": false, + "note": "`sum_steps` 일 때만 — 건설은 아직 합산형 선언이 없어 늘 빈칸이라 감춤(선언이 서면 보이게 할 것)." }, { "key": "link", diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 73bc0cab..50628247 100644 --- a/resources/data_work_item_master/_manifest.json +++ b/resources/data_work_item_master/_manifest.json @@ -1,7 +1,7 @@ { "schema_version": "1.0", "dataset_id": "data_work_item_master_manifest", - "generated_at": "2026-09-17T18:25:05+09:00", + "generated_at": "2026-09-17T18:52:20+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": "97d4c0a12d5cb811d10d3c4108bc1a20acf97748b572db026847b603804f7598", - "size_bytes": 925084 + "sha256": "546624601feaf9e9c7d757d6013b7c9bc3bc973817fb42ded70f3b2306b6778d", + "size_bytes": 924964 }, { "file": "form_undetermined_2026-01-01.json", diff --git a/resources/data_work_item_master/_manifest_const.json b/resources/data_work_item_master/_manifest_const.json index f3b15dd4..065190e5 100644 --- a/resources/data_work_item_master/_manifest_const.json +++ b/resources/data_work_item_master/_manifest_const.json @@ -1,7 +1,7 @@ { "schema_version": "1.0", "dataset_id": "data_work_item_master_manifest", - "generated_at": "2026-09-17T18:25:40+09:00", + "generated_at": "2026-09-17T18:52:53+09:00", "built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py", "source": { "dataset_id": "pum_const", @@ -12,8 +12,8 @@ "files": [ { "file": "const_work_item_master_2026-01-01.json", - "sha256": "f01928541cd24f8cfa6f46036d8d988479d8649796d6630d5b70179c485001bd", - "size_bytes": 3839561 + "sha256": "e8e3bc48c2563246b7a6fd4b5bc5129fe00c5f112fa190d78119912beddf40ff", + "size_bytes": 3839500 }, { "file": "const_form_undetermined_2026-01-01.json", diff --git a/resources/data_work_item_master/const_work_item_master_2026-01-01.json b/resources/data_work_item_master/const_work_item_master_2026-01-01.json index 613cc89e..691bf18c 100644 --- a/resources/data_work_item_master/const_work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/const_work_item_master_2026-01-01.json @@ -4,7 +4,7 @@ "effective_date": "2026-01-01", "pum_edition": "2026-01-01", "toc_edition": "2026-01-01", - "generated_at": "2026-09-17T18:25:40+09:00", + "generated_at": "2026-09-17T18:52:53+09:00", "dataset_version": { "dataset_id": "pum_const", "effective_date": "2026-01-01", @@ -49,7 +49,7 @@ "empty": 74 }, "keys_newly_issued": 0, - "variant_keys_newly_issued": 407, + "variant_keys_newly_issued": 0, "variant_keys": 407, "toc_duplicate_rows": 0, "toc_corrections": 0 @@ -5712,12 +5712,12 @@ { "variant_key": "01", "name": "평 규 준 틀", - "name_clean": "평 규 준 틀" + "name_clean": "평규준틀" }, { "variant_key": "02", "name": "귀 규 준 틀", - "name_clean": "귀 규 준 틀" + "name_clean": "귀규준틀" } ], "toc_code": "CP-01-02-05-04", @@ -7050,7 +7050,7 @@ { "variant_key": "01", "name": "벽 용", - "name_clean": "벽 용" + "name_clean": "벽용" }, { "variant_key": "02", @@ -126550,7 +126550,7 @@ { "variant_key": "01", "name": "주 철 재 바 닥 설 치 20절 이하", - "name_clean": "주 철 재 바 닥 설 치 20절 이하" + "name_clean": "주철재바닥설치 20절 이하" }, { "variant_key": "02", @@ -126560,12 +126560,12 @@ { "variant_key": "03", "name": "벽 걸 이 3절", - "name_clean": "벽 걸 이 3절" + "name_clean": "벽걸이 3절" }, { "variant_key": "04", "name": "천 장 달 기 3절", - "name_clean": "천 장 달 기 3절" + "name_clean": "천장달기 3절" }, { "variant_key": "05", @@ -131030,32 +131030,32 @@ { "variant_key": "01", "name": "분 전 반", - "name_clean": "분 전 반" + "name_clean": "분전반" }, { "variant_key": "02", "name": "조 작 반", - "name_clean": "조 작 반" + "name_clean": "조작반" }, { "variant_key": "03", "name": "계 기 반 ( 자 립 개 방 )", - "name_clean": "계 기 반 ( 자 립 개 방 )" + "name_clean": "계기반 ( 자립개방 )" }, { "variant_key": "04", "name": "계 기 반 ( 자 립 밀 폐 )", - "name_clean": "계 기 반 ( 자 립 밀 폐 )" + "name_clean": "계기반 ( 자립밀폐 )" }, { "variant_key": "05", "name": "계 기 반 ( 현 장 )", - "name_clean": "계 기 반 ( 현 장 )" + "name_clean": "계기반 ( 현 장 )" }, { "variant_key": "06", "name": "〃 ( 발 신 기 수 납 상 )", - "name_clean": "〃 ( 발 신 기 수 납 상 )" + "name_clean": "〃 ( 발신기수납상 )" } ], "toc_code": "CP-04-12-01-01", @@ -131910,17 +131910,17 @@ { "variant_key": "01", "name": "유 니 온 엘 보", - "name_clean": "유 니 온 엘 보" + "name_clean": "유니온엘보" }, { "variant_key": "02", "name": "공 기 압 축 기 시 험", - "name_clean": "공 기 압 축 기 시 험" + "name_clean": "공기압축기시험" }, { "variant_key": "03", "name": "조 작 함 ( 설 비 물 )", - "name_clean": "조 작 함 ( 설 비 물 )" + "name_clean": "조작함 ( 설비물 )" } ], "toc_code": "CP-04-12-02-05", @@ -154239,7 +154239,7 @@ { "variant_key": "01", "name": "산 기 분 기 관 제 작", - "name_clean": "산 기 분 기 관 제 작" + "name_clean": "산기분기관제작" }, { "variant_key": "02", diff --git a/resources/data_work_item_master/work_item_master_2026-01-01.json b/resources/data_work_item_master/work_item_master_2026-01-01.json index 9b7f3a82..abe1d0ad 100644 --- a/resources/data_work_item_master/work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/work_item_master_2026-01-01.json @@ -4,7 +4,7 @@ "effective_date": "2026-01-01", "pum_edition": "2026-01-01", "toc_edition": "산림청고시제2025-82호", - "generated_at": "2026-09-17T18:25:05+09:00", + "generated_at": "2026-09-17T18:52:20+09:00", "dataset_version": { "dataset_id": "pum_forest", "effective_date": "2026-01-01", @@ -10618,12 +10618,12 @@ { "variant_key": "01", "name": "채 취", - "name_clean": "채 취" + "name_clean": "채취" }, { "variant_key": "02", "name": "운 반", - "name_clean": "운 반" + "name_clean": "운반" }, { "variant_key": "03", @@ -28953,17 +28953,17 @@ { "variant_key": "01", "name": "간 단", - "name_clean": "간 단" + "name_clean": "간단" }, { "variant_key": "02", "name": "보 통", - "name_clean": "보 통" + "name_clean": "보통" }, { "variant_key": "03", "name": "복 잡", - "name_clean": "복 잡" + "name_clean": "복잡" }, { "variant_key": "04", @@ -31445,12 +31445,12 @@ { "variant_key": "03", "name": "설 치", - "name_clean": "설 치" + "name_clean": "설치" }, { "variant_key": "04", "name": "철 거", - "name_clean": "철 거" + "name_clean": "철거" } ], "toc_code": "FP-12-17-01", @@ -33774,17 +33774,17 @@ { "variant_key": "01", "name": "간 단", - "name_clean": "간 단" + "name_clean": "간단" }, { "variant_key": "02", "name": "보 통", - "name_clean": "보 통" + "name_clean": "보통" }, { "variant_key": "03", "name": "복 잡", - "name_clean": "복 잡" + "name_clean": "복잡" } ], "toc_code": "FP-12-38", @@ -33952,17 +33952,17 @@ { "variant_key": "01", "name": "간 단", - "name_clean": "간 단" + "name_clean": "간단" }, { "variant_key": "02", "name": "보 통", - "name_clean": "보 통" + "name_clean": "보통" }, { "variant_key": "03", "name": "복 잡", - "name_clean": "복 잡" + "name_clean": "복잡" } ], "toc_code": "FP-12-38-02", @@ -34082,17 +34082,17 @@ { "variant_key": "01", "name": "복 잡", - "name_clean": "복 잡" + "name_clean": "복잡" }, { "variant_key": "02", "name": "보 통", - "name_clean": "보 통" + "name_clean": "보통" }, { "variant_key": "03", "name": "간 단", - "name_clean": "간 단" + "name_clean": "간단" } ], "toc_code": "FP-12-38-03", @@ -36558,202 +36558,202 @@ { "variant_key": "01", "name": "메 붙 임 · 깬 돌 · 뒷길이 25㎝", - "name_clean": "메 붙 임 · 깬 돌 · 뒷길이 25㎝" + "name_clean": "메붙임 · 깬돌 · 뒷길이 25㎝" }, { "variant_key": "02", "name": "메 붙 임 · 깬 돌 · 뒷길이 30㎝", - "name_clean": "메 붙 임 · 깬 돌 · 뒷길이 30㎝" + "name_clean": "메붙임 · 깬돌 · 뒷길이 30㎝" }, { "variant_key": "03", "name": "메 붙 임 · 깬 돌 · 뒷길이 35㎝", - "name_clean": "메 붙 임 · 깬 돌 · 뒷길이 35㎝" + "name_clean": "메붙임 · 깬돌 · 뒷길이 35㎝" }, { "variant_key": "04", "name": "메 붙 임 · 깬 돌 · 뒷길이 45㎝", - "name_clean": "메 붙 임 · 깬 돌 · 뒷길이 45㎝" + "name_clean": "메붙임 · 깬돌 · 뒷길이 45㎝" }, { "variant_key": "05", "name": "메 붙 임 · 깬 돌 · 뒷길이 55㎝", - "name_clean": "메 붙 임 · 깬 돌 · 뒷길이 55㎝" + "name_clean": "메붙임 · 깬돌 · 뒷길이 55㎝" }, { "variant_key": "06", "name": "메 붙 임 · 깬 돌 · 뒷길이 60㎝", - "name_clean": "메 붙 임 · 깬 돌 · 뒷길이 60㎝" + "name_clean": "메붙임 · 깬돌 · 뒷길이 60㎝" }, { "variant_key": "07", "name": "메 붙 임 · 깬 돌 · 뒷길이 70㎝", - "name_clean": "메 붙 임 · 깬 돌 · 뒷길이 70㎝" + "name_clean": "메붙임 · 깬돌 · 뒷길이 70㎝" }, { "variant_key": "08", "name": "메 붙 임 · 깬잡석 · 뒷길이 25㎝", - "name_clean": "메 붙 임 · 깬잡석 · 뒷길이 25㎝" + "name_clean": "메붙임 · 깬잡석 · 뒷길이 25㎝" }, { "variant_key": "09", "name": "메 붙 임 · 깬잡석 · 뒷길이 30㎝", - "name_clean": "메 붙 임 · 깬잡석 · 뒷길이 30㎝" + "name_clean": "메붙임 · 깬잡석 · 뒷길이 30㎝" }, { "variant_key": "10", "name": "메 붙 임 · 깬잡석 · 뒷길이 35㎝", - "name_clean": "메 붙 임 · 깬잡석 · 뒷길이 35㎝" + "name_clean": "메붙임 · 깬잡석 · 뒷길이 35㎝" }, { "variant_key": "11", "name": "메 붙 임 · 깬잡석 · 뒷길이 45㎝", - "name_clean": "메 붙 임 · 깬잡석 · 뒷길이 45㎝" + "name_clean": "메붙임 · 깬잡석 · 뒷길이 45㎝" }, { "variant_key": "12", "name": "메 붙 임 · 깬잡석 · 뒷길이 55㎝", - "name_clean": "메 붙 임 · 깬잡석 · 뒷길이 55㎝" + "name_clean": "메붙임 · 깬잡석 · 뒷길이 55㎝" }, { "variant_key": "13", "name": "메 붙 임 · 깬잡석 · 뒷길이 60㎝", - "name_clean": "메 붙 임 · 깬잡석 · 뒷길이 60㎝" + "name_clean": "메붙임 · 깬잡석 · 뒷길이 60㎝" }, { "variant_key": "14", "name": "메 붙 임 · 깬잡석 · 뒷길이 70㎝", - "name_clean": "메 붙 임 · 깬잡석 · 뒷길이 70㎝" + "name_clean": "메붙임 · 깬잡석 · 뒷길이 70㎝" }, { "variant_key": "15", "name": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 25㎝", - "name_clean": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 25㎝" + "name_clean": "메붙임 · 조약돌 및 야면석 · 뒷길이 25㎝" }, { "variant_key": "16", "name": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 30㎝", - "name_clean": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 30㎝" + "name_clean": "메붙임 · 조약돌 및 야면석 · 뒷길이 30㎝" }, { "variant_key": "17", "name": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 35㎝", - "name_clean": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 35㎝" + "name_clean": "메붙임 · 조약돌 및 야면석 · 뒷길이 35㎝" }, { "variant_key": "18", "name": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 45㎝", - "name_clean": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 45㎝" + "name_clean": "메붙임 · 조약돌 및 야면석 · 뒷길이 45㎝" }, { "variant_key": "19", "name": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 55㎝", - "name_clean": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 55㎝" + "name_clean": "메붙임 · 조약돌 및 야면석 · 뒷길이 55㎝" }, { "variant_key": "20", "name": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 60㎝", - "name_clean": "메 붙 임 · 조약돌 및 야면석 · 뒷길이 60㎝" + "name_clean": "메붙임 · 조약돌 및 야면석 · 뒷길이 60㎝" }, { "variant_key": "21", "name": "찰 붙 임 · 깬 돌 · 뒷길이 25㎝", - "name_clean": "찰 붙 임 · 깬 돌 · 뒷길이 25㎝" + "name_clean": "찰붙임 · 깬돌 · 뒷길이 25㎝" }, { "variant_key": "22", "name": "찰 붙 임 · 깬 돌 · 뒷길이 30㎝", - "name_clean": "찰 붙 임 · 깬 돌 · 뒷길이 30㎝" + "name_clean": "찰붙임 · 깬돌 · 뒷길이 30㎝" }, { "variant_key": "23", "name": "찰 붙 임 · 깬 돌 · 뒷길이 35㎝", - "name_clean": "찰 붙 임 · 깬 돌 · 뒷길이 35㎝" + "name_clean": "찰붙임 · 깬돌 · 뒷길이 35㎝" }, { "variant_key": "24", "name": "찰 붙 임 · 깬 돌 · 뒷길이 45㎝", - "name_clean": "찰 붙 임 · 깬 돌 · 뒷길이 45㎝" + "name_clean": "찰붙임 · 깬돌 · 뒷길이 45㎝" }, { "variant_key": "25", "name": "찰 붙 임 · 깬 돌 · 뒷길이 55㎝", - "name_clean": "찰 붙 임 · 깬 돌 · 뒷길이 55㎝" + "name_clean": "찰붙임 · 깬돌 · 뒷길이 55㎝" }, { "variant_key": "26", "name": "찰 붙 임 · 깬 돌 · 뒷길이 60㎝", - "name_clean": "찰 붙 임 · 깬 돌 · 뒷길이 60㎝" + "name_clean": "찰붙임 · 깬돌 · 뒷길이 60㎝" }, { "variant_key": "27", "name": "찰 붙 임 · 깬 돌 · 뒷길이 70㎝", - "name_clean": "찰 붙 임 · 깬 돌 · 뒷길이 70㎝" + "name_clean": "찰붙임 · 깬돌 · 뒷길이 70㎝" }, { "variant_key": "28", "name": "찰 붙 임 · 깬잡석 · 뒷길이 25㎝", - "name_clean": "찰 붙 임 · 깬잡석 · 뒷길이 25㎝" + "name_clean": "찰붙임 · 깬잡석 · 뒷길이 25㎝" }, { "variant_key": "29", "name": "찰 붙 임 · 깬잡석 · 뒷길이 30㎝", - "name_clean": "찰 붙 임 · 깬잡석 · 뒷길이 30㎝" + "name_clean": "찰붙임 · 깬잡석 · 뒷길이 30㎝" }, { "variant_key": "30", "name": "찰 붙 임 · 깬잡석 · 뒷길이 35㎝", - "name_clean": "찰 붙 임 · 깬잡석 · 뒷길이 35㎝" + "name_clean": "찰붙임 · 깬잡석 · 뒷길이 35㎝" }, { "variant_key": "31", "name": "찰 붙 임 · 깬잡석 · 뒷길이 45㎝", - "name_clean": "찰 붙 임 · 깬잡석 · 뒷길이 45㎝" + "name_clean": "찰붙임 · 깬잡석 · 뒷길이 45㎝" }, { "variant_key": "32", "name": "찰 붙 임 · 깬잡석 · 뒷길이 55㎝", - "name_clean": "찰 붙 임 · 깬잡석 · 뒷길이 55㎝" + "name_clean": "찰붙임 · 깬잡석 · 뒷길이 55㎝" }, { "variant_key": "33", "name": "찰 붙 임 · 깬잡석 · 뒷길이 60㎝", - "name_clean": "찰 붙 임 · 깬잡석 · 뒷길이 60㎝" + "name_clean": "찰붙임 · 깬잡석 · 뒷길이 60㎝" }, { "variant_key": "34", "name": "찰 붙 임 · 깬잡석 · 뒷길이 70㎝", - "name_clean": "찰 붙 임 · 깬잡석 · 뒷길이 70㎝" + "name_clean": "찰붙임 · 깬잡석 · 뒷길이 70㎝" }, { "variant_key": "35", "name": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 25㎝", - "name_clean": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 25㎝" + "name_clean": "찰붙임 · 호박돌 및 야면석 · 뒷길이 25㎝" }, { "variant_key": "36", "name": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 30㎝", - "name_clean": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 30㎝" + "name_clean": "찰붙임 · 호박돌 및 야면석 · 뒷길이 30㎝" }, { "variant_key": "37", "name": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 35㎝", - "name_clean": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 35㎝" + "name_clean": "찰붙임 · 호박돌 및 야면석 · 뒷길이 35㎝" }, { "variant_key": "38", "name": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 45㎝", - "name_clean": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 45㎝" + "name_clean": "찰붙임 · 호박돌 및 야면석 · 뒷길이 45㎝" }, { "variant_key": "39", "name": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 55㎝", - "name_clean": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 55㎝" + "name_clean": "찰붙임 · 호박돌 및 야면석 · 뒷길이 55㎝" }, { "variant_key": "40", "name": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 60㎝", - "name_clean": "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 60㎝" + "name_clean": "찰붙임 · 호박돌 및 야면석 · 뒷길이 60㎝" } ], "toc_code": "FP-13-05-01", @@ -39049,17 +39049,17 @@ { "variant_key": "02", "name": "복 토", - "name_clean": "복 토" + "name_clean": "복토" }, { "variant_key": "03", "name": "간 단 구 조", - "name_clean": "간 단 구 조" + "name_clean": "간단구조" }, { "variant_key": "04", "name": "보 통 구 조", - "name_clean": "보 통 구 조" + "name_clean": "보통구조" } ], "toc_code": "FP-13-13-02", @@ -39132,7 +39132,7 @@ { "variant_key": "01", "name": "단 끊 기", - "name_clean": "단 끊 기" + "name_clean": "단끊기" }, { "variant_key": "02", @@ -39475,7 +39475,7 @@ { "variant_key": "01", "name": "사 면", - "name_clean": "사 면" + "name_clean": "사면" }, { "variant_key": "02", diff --git a/resources/tester/test_work_item_key_registry.py b/resources/tester/test_work_item_key_registry.py index 219c987d..acd55406 100644 --- a/resources/tester/test_work_item_key_registry.py +++ b/resources/tester/test_work_item_key_registry.py @@ -285,7 +285,8 @@ def test_갈래에도_불변_열쇠가_있다(master: dict) -> None: # 이름 칸 둘 — 원문은 손대지 않고, 다듬은 것이 따로 raw = [it.get("variant_keys") or [] for it in items] assert [n for it in items for n in (it.get("variant_keys") or [])] == [p[0] for p in pairs] - assert sum(1 for a, b in pairs if a != b) == 14 # 자간·물결표가 다듬어진 줄 + # 자간·물결표가 다듬어진 줄 — 14 → 70(2026-09-17 한 글자 낱말 자간 붙이기 「메 붙 임」→「메붙임」 · 열쇠는 그대로) + assert sum(1 for a, b in pairs if a != b) == 70 assert raw # 원문 목록은 그대로 남아 있다 diff --git a/resources/tester/test_work_item_master_const.py b/resources/tester/test_work_item_master_const.py index 7f054cb1..7c79a06c 100644 --- a/resources/tester/test_work_item_master_const.py +++ b/resources/tester/test_work_item_master_const.py @@ -175,3 +175,17 @@ def test_건설_갈래도_이름을_다듬어도_같은_열쇠(master: dict) -> assert [v["variant_key"] for v in after["variants"]] == [ v["variant_key"] for v in before["variants"] ] + + +def test_자간_다듬기는_진짜_띄어쓰기를_안_붙임() -> None: + from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import tidy_spacing + + assert tidy_spacing("측 량") == "측량" + assert tidy_spacing("평 규 준 틀") == "평규준틀" + assert tidy_spacing("굴 삭 기 (무한궤도)") == "굴삭기 (무한궤도)" + assert tidy_spacing("메 붙 임 · 깬 돌 · 뒷길이 25㎝") == "메붙임 · 깬돌 · 뒷길이 25㎝" + assert tidy_spacing("토목부문 › 측 량 › 기준점 측량") == "토목부문 › 측량 › 기준점 측량" + assert ( + tidy_spacing("그 외 자재의 운반품셈") == "그 외 자재의 운반품셈" + ) # 한 글자 낱말 둘은 진짜 띄어쓰기 + assert tidy_spacing("호박돌 및 야면석 · 뒷길이 25㎝") == "호박돌 및 야면석 · 뒷길이 25㎝" diff --git a/resources/tester/test_z01_work_items.py b/resources/tester/test_z01_work_items.py index 7b9e4b0b..7a5c525f 100644 --- a/resources/tester/test_z01_work_items.py +++ b/resources/tester/test_z01_work_items.py @@ -271,3 +271,39 @@ def test_찾기는_물결표_가운뎃점_띄어쓰기를_같게_봄(client: Tes assert ( spaced == _get(client, "work_item_forest", size=50, q="토사깍기", axis_role="")["total"] > 0 ) + + +def test_이름_다듬기는_보일_이름만_열쇠는_그대로( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """⭐ 갈래 불변 열쇠를 세운 까닭(브레인 2026-09-17) — 자간 공백(「측 량」·「평 규 준 틀」)을 다듬어도 + 줄 열쇠(`FW-…#01`)가 한 글자도 안 흔들림. 다듬기를 끈 목록과 켠 목록의 열쇠를 맞댐.""" + from B08_Quantity import B08_Quantity_Build_WorkItemMaster_Keys as keys + from Z01_MasterData import Z01_MasterData_WorkItems as work_items + + for kind in work_items.KINDS: + tidy = work_items.base_rows(kind) + with monkeypatch.context() as patch: + patch.setattr(keys, "tidy_spacing", lambda text: " ".join(str(text or "").split())) + raw = work_items.base_rows(kind) + assert [r["@id"] for r in tidy] == [r["@id"] for r in raw] + if kind == "work_item_const": + assert ( + sum(a["name"] != b["name"] for a, b in zip(tidy, raw)) > 0 + ) # 실제로 다듬어진 줄이 있음 + shown = _get(client, "work_item_const", size=50, q="기준점 측량", axis_role="")["rows"] + assert shown and all("측 량" not in r["name"] and "› 측량 ›" in r["name"] for r in shown) + + +def test_품셈_번호는_마디마다_수로_세움(client: TestClient) -> None: + """글자로 세우면 「10-1」이 「2-1」 앞에 섬(서브 훑기) — 마디마다 수로.""" + rows = _get(client, "work_item_forest", size=500, sort="number", axis_role="")["rows"] + numbers = [tuple(int(x) for x in r["number"].split("-")) for r in rows if "-" in r["number"]] + assert len(numbers) > 300 and numbers == sorted(numbers) + + +def test_건설_늘_빈_칸은_감춤(client: TestClient) -> None: + """건설은 합산형 선언이 없어 「단계 무게」가 늘 빈칸 — 감춤(산림은 보임).""" + hidden = lambda kind: {c["key"]: c["hidden"] for c in _get(client, kind, size=1)["columns"]} # noqa: E731 + assert hidden("work_item_const")["step_weight"] is True + assert hidden("work_item_forest")["step_weight"] is False