feat(z01,b08): 이름 자간 다듬기·품셈 번호 숫자 정렬·건설 빈 칸 감춤 — 한 글자 낱말뿐인 조각이나 셋 이상 이어진 한 글자 낱말만 붙임(「측 량」→「측량」·「메 붙 임 · 깬 돌」→「메붙임 · 깬돌」 · 「그 외 자재」는 그대로) · 목록 경로 이름과 갈래 다듬은 이름(name_clean)에만 · 원문 이름은 마스터에 그대로 · 열쇠는 번호라 안 흔들림을 시험으로 못박음(다듬기 끈 목록과 열쇠 맞댐 · 장부 새 발급 0) · 품셈 번호 칸은 마디마다 수로 세움(「10-1」이 「2-1」 뒤) · 건설 「단계 무게」는 늘 빈칸이라 감춤(브레인 ⑥ · 서브 훑기)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-17 18:55:29 +09:00
co-authored by Claude Opus 5
parent 7a1f6a01ef
commit 4b6d4f141c
11 changed files with 195 additions and 98 deletions
@@ -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:
+11 -4
View File
@@ -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]]:
+9 -2
View File
@@ -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"], [])],
}
@@ -1504,8 +1504,8 @@
"key": "step_weight",
"name_ko": "단계 무게",
"unit": "",
"visible": true,
"note": "`sum_steps` 일 때만 — 건설은 아직 합산형 선언 없."
"visible": false,
"note": "`sum_steps` 일 때만 — 건설은 아직 합산형 선언어 늘 빈칸이라 감춤(선언이 서면 보이게 할 것)."
},
{
"key": "link",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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 # 원문 목록은 그대로 남아 있다
@@ -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㎝"
+36
View File
@@ -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