diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index d43c8ff1..c91f964b 100644 --- a/resources/data_master_labels/labels_2026-01-01.json +++ b/resources/data_master_labels/labels_2026-01-01.json @@ -2,7 +2,7 @@ "schema_version": "1.0", "dataset_id": "data_master_labels", "effective_date": "2026-01-01", - "generated_at": "2026-09-16T13:00:09+09:00", + "generated_at": "2026-09-16T13:11:45+09:00", "note": "마스터 자료의 **사람이 읽을 이름표**. 값이 아니라 이름만 담는다 — 여기를 고쳐도 계산은 안 바뀐다. Z01 마스터 관리 화면이 표·열 이름을 여기서 읽는다.", "policy": { "labels_only": "값·수식·단가를 담지 않는다. 마스터 파일은 손대지 않는다.", @@ -770,6 +770,433 @@ "note": "못 채운 성분이 있으면 그 사실을 적는 자리 — 0 으로 안 때운다." } ] + }, + { + "key": "coef", + "name_ko": "토량환산계수", + "summary": "판 흙이 부풀고(L) 다진 흙이 줄어드는(C) 비. 토질마다 하한·상한.", + "sources": [ + "coef::variables/coef_soil_L/records", + "coef::variables/coef_soil_C/records" + ], + "rows_note": "L 16 + C 16 = 32 줄(암괴 중복 두 줄은 한 줄로 합침)", + "columns": [ + { + "key": "table", + "name_ko": "어느 표", + "unit": "", + "visible": true, + "note": "`coef_soil_L`(부풂) · `coef_soil_C`(다짐). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "soil_type", + "name_ko": "토질", + "unit": "", + "visible": true, + "note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "min", + "name_ko": "하한", + "unit": "", + "visible": true + }, + { + "key": "max", + "name_ko": "상한", + "unit": "", + "visible": true + }, + { + "key": "rule", + "name_ko": "별도 규정", + "unit": "", + "visible": true, + "note": "원문이 값 대신 「별도 설계」라 적은 자리." + }, + { + "key": "selection", + "name_ko": "채택 여부", + "unit": "", + "visible": true, + "note": "원문이 같은 토질을 두 줄로 실어 어느 줄을 쓸지 아직 안 고른 자리(`pending`)." + }, + { + "key": "source_rows", + "name_ko": "원문 줄 수", + "unit": "줄", + "visible": true, + "note": "⭐ 한 줄로 보이지만 원문에는 몇 줄이었나 — 암괴 중복 쌍이 `2` 다." + }, + { + "key": "duplicate_note", + "name_ko": "합친 사유", + "unit": "", + "visible": true, + "note": "⭐ 「원문 두 줄 · 값 같음 — 한 줄로 보임」. 원문은 그대로 두고 화면만 접었다는 뜻." + } + ] + }, + { + "key": "material_surcharge", + "name_ko": "재료 할증률", + "summary": "자재마다 몇 %를 더 사는지. 조건이 갈리면 다른 조건 칸에 함께 둔다.", + "sources": [ + "material_surcharge::rates_pct" + ], + "rows_note": "19 줄", + "columns": [ + { + "key": "material", + "name_ko": "자재명", + "unit": "", + "visible": true, + "note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "rate", + "name_ko": "할증률", + "unit": "%", + "visible": true + }, + { + "key": "condition", + "name_ko": "조건", + "unit": "", + "visible": true + }, + { + "key": "alt_rate", + "name_ko": "다른 조건 할증률", + "unit": "%", + "visible": true + }, + { + "key": "alt_condition", + "name_ko": "다른 조건", + "unit": "", + "visible": true + }, + { + "key": "pumsem", + "name_ko": "어느 품셈", + "unit": "", + "visible": true, + "note": "`forest`(산림 · 1차) · `construction`(건설 · 보완)." + } + ] + }, + { + "key": "formwork_reuse", + "name_ko": "거푸집 사용횟수", + "summary": "구조물 갈래마다 거푸집을 몇 번 쓰는지, 횟수마다 기준수량의 몇 %인지.", + "sources": [ + "formwork_reuse::reuse_by_class", + "formwork_reuse::reuse_ratio_pct", + "formwork_reuse::euroform_type/classes" + ], + "rows_note": "구조 갈래 4 + 사용횟수 비율 12(합판·각재 × 6) + 유로폼 갈래 3 = 19 줄", + "columns": [ + { + "key": "table", + "name_ko": "어느 표", + "unit": "", + "visible": true, + "note": "`reuse_by_class`(갈래별 횟수) · `reuse_ratio_pct`(횟수별 비율) · `euroform_type/classes`(유로폼 갈래). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "class", + "name_ko": "구조 갈래", + "unit": "", + "visible": true, + "note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "reuse_count", + "name_ko": "사용횟수", + "unit": "회", + "visible": true, + "note": "비율표에서는 ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "examples", + "name_ko": "원문 예시", + "unit": "", + "visible": true + }, + { + "key": "material", + "name_ko": "거푸집 재료", + "unit": "", + "visible": true, + "note": "`plywood`(합판) · `timber`(각재). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "ratio_pct", + "name_ko": "기준수량 비율", + "unit": "%", + "visible": true, + "note": "⭐ 품셈 12-4 — 그 횟수로 쓸 때 기준수량의 몇 %인지. 일위대가 재료비에 걸린다." + }, + { + "key": "daily_area_m2", + "name_ko": "1일 시공량", + "unit": "㎡", + "visible": true, + "note": "유로폼 설치·해체 품이 갈리는 값." + } + ] + }, + { + "key": "rebar_complexity", + "name_ko": "철근 가공·조립 갈래", + "summary": "간단·보통·복잡·매우복잡 갈래와 갈래별 단가 참고값.", + "sources": [ + "rebar_complexity::classes", + "rebar_complexity::price_hint_krw_per_ton" + ], + "rows_note": "갈래 4 + 단가 참고값 4 = 8 줄", + "columns": [ + { + "key": "table", + "name_ko": "어느 표", + "unit": "", + "visible": true, + "note": "`classes`(갈래) · `price_hint_krw_per_ton`(단가 참고값). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "class", + "name_ko": "갈래", + "unit": "", + "visible": true, + "note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "examples", + "name_ko": "원문 예시", + "unit": "", + "visible": true, + "note": "품셈 12-3 [주]① 이 든 구조물 보기." + }, + { + "key": "price_krw_per_ton", + "name_ko": "단가 참고값", + "unit": "원/ton", + "visible": true, + "note": "⚠ **표시 전용** — 갈래 차이를 사람이 보라고 둔 값이다. 계산에 안 들어간다." + } + ] + }, + { + "key": "timber_structure_class", + "name_ko": "목재공작물 구조 갈래", + "summary": "갈래마다 목재 채적 1㎥ 에 드는 건축목공·보통인부.", + "sources": [ + "timber_structure_class::classes" + ], + "rows_note": "6 줄", + "columns": [ + { + "key": "class", + "name_ko": "갈래", + "unit": "", + "visible": true, + "note": "보통구조 하·중·상 · 상등구조 하·중·상. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "carpenter", + "name_ko": "건축목공", + "unit": "인/㎥", + "visible": true + }, + { + "key": "laborer", + "name_ko": "보통인부", + "unit": "인/㎥", + "visible": true + }, + { + "key": "examples", + "name_ko": "원문 예시", + "unit": "", + "visible": true + } + ] + }, + { + "key": "masonry_slope", + "name_ko": "돌쌓기 표준경사", + "summary": "쌓기 방식 × 성절토 × 직고 구간마다 전면 기울기(1:n).", + "sources": [ + "masonry_slope::table" + ], + "rows_note": "메찰 2 × 성절토 2 × 직고 5 = 20 줄", + "columns": [ + { + "key": "bond", + "name_ko": "쌓기 방식", + "unit": "", + "visible": true, + "note": "메쌓기 · 찰쌓기. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "face", + "name_ko": "성토·절토", + "unit": "", + "visible": true, + "note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "height_bracket", + "name_ko": "직고 구간", + "unit": "m", + "visible": true, + "note": "⚠ 원문 표기 그대로(`~1.5`·`~3`·`~5`·`~7`·`7이상`) — 예쁘게 고치면 열쇠가 바뀐다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "face_slope_ratio", + "name_ko": "전면 기울기", + "unit": "1:n", + "visible": true, + "note": "코드가 늘 쓰던 0.3 은 이 표의 「메쌓기·성토·~1.5」 한 칸일 뿐이었다." + } + ] + }, + { + "key": "masonry_back_length", + "name_ko": "돌쌓기 뒷길이 표준", + "summary": "쌓기 방식 × 직고 구간마다 뒷길이 하한·상한(㎝).", + "sources": [ + "masonry_back_length::table_cm" + ], + "rows_note": "메찰 2 × 직고 5 = 10 줄", + "columns": [ + { + "key": "bond", + "name_ko": "쌓기 방식", + "unit": "", + "visible": true, + "note": "메쌓기 · 찰쌓기. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "height_bracket", + "name_ko": "직고 구간", + "unit": "m", + "visible": true, + "note": "⚠ 원문 표기 그대로(`~1.5`·`~3`·`~5`·`~7`·`7이상`) — 예쁘게 고치면 열쇠가 바뀐다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "min_cm", + "name_ko": "뒷길이 하한", + "unit": "㎝", + "visible": true, + "note": "⭐ 안 고르면 이 값이 제안된다(범위 안에 드는 품셈 규격 중 가장 작은 것)." + }, + { + "key": "max_cm", + "name_ko": "뒷길이 상한", + "unit": "㎝", + "visible": true, + "note": "원문이 상한을 안 준 칸은 빈다." + } + ] + }, + { + "key": "stone_kind", + "name_ko": "돌 종류별 소요량", + "summary": "돌 종류 × 뒷길이마다 고임돌·채움 콘크리트 원단위와 뒤채움 몫.", + "sources": [ + "stone_kind::wedge_stone_m3_per_m2", + "stone_kind::fill_concrete_m3_per_m2", + "stone_kind::backfill_ratio_of_back_length", + "stone_kind::fallback" + ], + "rows_note": "고임돌 28 + 채움 28 + 뒤채움 몫 4 + 미지정일 때 15 = 75 줄", + "columns": [ + { + "key": "table", + "name_ko": "어느 표", + "unit": "", + "visible": true, + "note": "`wedge_stone_m3_per_m2`(고임돌) · `fill_concrete_m3_per_m2`(채움 콘크리트) · `backfill_ratio_of_back_length`(뒤채움 몫) · `fallback/…`(돌 종류 미지정일 때). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "stone_kind", + "name_ko": "돌 종류", + "unit": "", + "visible": true, + "note": "야면석·호박돌 / 깬잡석 / 깬돌 / 견치돌. 미지정 벌에는 이 칸이 없다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "back_length_cm", + "name_ko": "뒷길이", + "unit": "㎝", + "visible": true, + "note": "25·30·35·45·55·60·75 일곱 규격. ⚠ 표에 없는 뒷길이를 가까운 칸으로 접지 않는다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "m3_per_m2", + "name_ko": "원단위", + "unit": "㎥/㎡", + "visible": true, + "note": "고임돌·채움 콘크리트 표의 값. 원문 「-」 칸은 빈다 — 0 이 아니다." + }, + { + "key": "ratio", + "name_ko": "뒤채움 몫", + "unit": "", + "visible": true, + "note": "뒷길이 가운데 뒤채움이 차지하는 비. 나머지가 잡석이 된다." + } + ] + }, + { + "key": "masonry_class", + "name_ko": "돌쌓기 규격 갈래", + "summary": "저장 제원으로 자동 판정하는 축 — 뒷길이 갈래·돌 직경 갈래·형식별 전면 기울기.", + "sources": [ + "masonry_class::back_length/classes", + "masonry_class::boulder_diameter", + "masonry_class::face_slope" + ], + "rows_note": "뒷길이 갈래 3 + 돌 직경 갈래 3 + 형식별 기울기 3 = 9 줄", + "columns": [ + { + "key": "table", + "name_ko": "어느 표", + "unit": "", + "visible": true, + "note": "`back_length/classes`(뒷길이 갈래) · `boulder_diameter`(돌 직경 갈래) · `face_slope`(형식별 기울기). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "class", + "name_ko": "갈래", + "unit": "", + "visible": true, + "note": "뒷길이는 `35cm이하` 꼴, 돌 직경은 원문 표기 `40~60` 꼴. ⚠ 원문 표기 그대로(`~1.5`·`~3`·`~5`·`~7`·`7이상`) — 예쁘게 고치면 열쇠가 바뀐다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "max_cm", + "name_ko": "상한", + "unit": "㎝", + "visible": true, + "note": "뒷길이 갈래의 상한." + }, + { + "key": "type_id", + "name_ko": "구조물 종류 키", + "unit": "", + "visible": true, + "note": "`masonry_wet`·`masonry_dry`·`boulder_masonry`. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다." + }, + { + "key": "face_slope_ratio", + "name_ko": "전면 기울기", + "unit": "1:n", + "visible": true, + "note": "형식마다 정해진 값. 코드 기본 0.3 이 여기서 왔다." + } + ] } ], "machine_cost_chain": { @@ -997,6 +1424,99 @@ "machine_code" ], "checked": "취득가 613 · 손료계수 387 · 연료 214 · 운전경비 92 **네 표 모두 기계코드 겹침 0**(전수). 취득가가 가장 넓어 뼈대가 되고 나머지가 붙는다." + }, + { + "table": "coef", + "name_ko": "토량환산계수", + "verdict": "있음(묶어야 함)", + "key": [ + "table", + "soil_type" + ], + "checked": "L·C 두 표가 한 자리에 서므로 `table` 이 함께 있어야 한다. 토질 이름은 원문 그대로. 암괴 중복 두 줄은 값이 같아 한 줄로 합쳤고 `source_rows`·`duplicate_note` 가 그 사유를 든다." + }, + { + "table": "material_surcharge", + "name_ko": "재료 할증률", + "verdict": "있음", + "key": [ + "material" + ], + "checked": "자재명 19 줄 겹침 0(전수). 그대로 열쇠로 쓴다." + }, + { + "table": "formwork_reuse", + "name_ko": "거푸집 사용횟수", + "verdict": "있음(표마다 다름)", + "key": [ + "table", + "class", + "material", + "reuse_count" + ], + "checked": "갈래 표는 `table+class`, 비율표는 `table+material+reuse_count` 로 가려진다. 한 표 안에서 축이 갈리므로 열쇠도 줄마다 다르다." + }, + { + "table": "rebar_complexity", + "name_ko": "철근 갈래", + "verdict": "있음(묶어야 함)", + "key": [ + "table", + "class" + ], + "checked": "갈래 표와 단가 참고값 표가 같은 갈래 이름을 쓰므로 `table` 이 함께 있어야 한다." + }, + { + "table": "timber_structure_class", + "name_ko": "목재 구조 갈래", + "verdict": "있음", + "key": [ + "class" + ], + "checked": "갈래 6 줄 겹침 0(전수)." + }, + { + "table": "masonry_slope", + "name_ko": "돌쌓기 표준경사", + "verdict": "없음 — 축으로만 가려짐", + "key": [ + "bond", + "face", + "height_bracket" + ], + "checked": "id 칸이 없다. 쌓기 방식 × 성절토 × 직고 구간 셋으로만 줄이 가려진다. ⚠ 직고 구간은 원문 표기 `~1.5`·`~3`·`~5`·`~7`·`7이상` 그대로 — 열쇠의 몸이라 못 바꾼다." + }, + { + "table": "masonry_back_length", + "name_ko": "돌쌓기 뒷길이 표준", + "verdict": "없음 — 축으로만 가려짐", + "key": [ + "bond", + "height_bracket" + ], + "checked": "id 칸이 없다. 쌓기 방식 × 직고 구간 둘로만 가려진다. 구간 표기는 위와 같다." + }, + { + "table": "stone_kind", + "name_ko": "돌 종류별 소요량", + "verdict": "없음 — 축으로만 가려짐", + "key": [ + "table", + "stone_kind", + "back_length_cm" + ], + "checked": "id 칸이 없다. 표 × 돌 종류 × 뒷길이로 가려지되 **표마다 축 수가 다르다** — 뒤채움 몫은 돌 종류까지, 미지정 벌은 뒷길이까지만 쓴다." + }, + { + "table": "masonry_class", + "name_ko": "돌쌓기 규격 갈래", + "verdict": "없음 — 축으로만 가려짐", + "key": [ + "table", + "class", + "type_id" + ], + "checked": "id 칸이 없다. 뒷길이·돌 직경 갈래는 `table+class`, 형식별 기울기는 `table+type_id`. ⚠ 돌 직경 갈래 이름은 원문 표기 `40~60` 꼴 그대로다." } ], "null_is_real": { @@ -1090,8 +1610,8 @@ } }, "counts": { - "merged_tables": 5, - "merged_columns": 89, + "merged_tables": 14, + "merged_columns": 136, "files": 34, "tables": 91, "columns": 462, diff --git a/resources/tester/test_master_labels_base_prices.py b/resources/tester/test_master_labels_base_prices.py index 801bd9b0..92a36288 100644 --- a/resources/tester/test_master_labels_base_prices.py +++ b/resources/tester/test_master_labels_base_prices.py @@ -152,3 +152,60 @@ def test_단위가_붙어야_할_열에_단위가_있다(shown): if units.get(key) != unit: bad.append(f"{kind}/{key}: {units.get(key)!r} ≠ {unit!r}") assert not bad, bad + + +def test_줄_열쇠가_서버_축과_같다(labels): + """⭐ 덮개가 줄을 붙드는 끈 — 서버가 줄마다 `@axis` 로 내는 축이 곧 열쇠다. + + 이름표가 축을 빠뜨리면 고친 값이 갱신 뒤 주인을 잃는다. + """ + declared = {r["table"]: set(r["key"]) for r in labels["row_keys"]} + bad = [] + for kind in base_prices.KINDS: + axes = set() + for row in base_prices.rows(kind): + axes |= set(row.get("@axis") or ()) + if not axes: # 다섯 표는 `@id` 한 칸이라 축을 안 낸다 + continue + missing = axes - declared.get(kind, set()) + if missing: + bad.append(f"{kind}: 이름표에 없는 축 {sorted(missing)}") + assert not bad, bad + + +def test_구간_이름은_원문_표기_그대로다(): + """⚠ `~1.5`·`7이상` 을 예쁘게 고치면 줄 열쇠가 바뀌어 덮개가 주인을 잃는다.""" + for kind in ("masonry_slope", "masonry_back_length"): + brackets = {str(r.get("height_bracket")) for r in base_prices.rows(kind)} + assert brackets == {"~1.5", "~3", "~5", "~7", "7이상"}, (kind, sorted(brackets)) + classes = { + str(r.get("class")) + for r in base_prices.rows("masonry_class") + if r.get("table") == "boulder_diameter" + } + assert classes == {"40~60", "60~80", "80~100"}, sorted(classes) + + +#: ⚠ **줄마다 축이기도 값이기도 한 칸** — `formwork_reuse.reuse_count` 는 +#: `reuse_by_class` 줄에서는 **고치는 값**(갈래별 사용횟수)이고 +#: `reuse_ratio_pct` 줄에서는 **축**(몇 회째인가)이다. 서버는 열 하나에 한 판정만 내므로 +#: 지금은 고칠 수 있는 칸으로 서 있다. 비율표 줄에서 이 칸을 고치면 **다른 줄로 옮겨 간다**. +#: 2026-09-16 크로스체크에서 드러났고 판정 대기 — 고치기 전까지 여기 예외로 적어 둔다. +AXIS_ALSO_VALUE = {"formwork_reuse/reuse_count"} + + +def test_축_칸은_못_고친다(): + """축을 고치면 줄이 다른 줄이 된다 — 서버가 잠가야 한다.""" + bad = [] + for kind in base_prices.KINDS: + rows = base_prices.rows(kind) + axes = set() + for row in rows: + axes |= set(row.get("@axis") or ()) + if not axes: + continue + meta = base_prices.table(kind, page=1, size=1) + for axis in axes: + if axis in meta["editable"] and f"{kind}/{axis}" not in AXIS_ALSO_VALUE: + bad.append(f"{kind}/{axis} 가 고칠 수 있는 칸으로 섰다") + assert not bad, bad diff --git a/resources/tester/test_master_labels_cover.py b/resources/tester/test_master_labels_cover.py index 8f08bcd7..9a9a9c4a 100644 --- a/resources/tester/test_master_labels_cover.py +++ b/resources/tester/test_master_labels_cover.py @@ -451,12 +451,29 @@ def test_미결은_미판정으로_표시된다(labels): assert column["name_ko"] == "미판정" -MERGED_KEYS = ("labor", "material", "oil", "rate", "machine") +#: 2026-09-16 사용자 판정 — 기초데이터 아홉이 더해져 열넷. +#: 잣대 = 계산되는 값이 아니라 **그냥 값만으로 정의되는 데이터셋**. +MERGED_KEYS = ( + "labor", + "material", + "oil", + "rate", + "machine", + "coef", + "material_surcharge", + "formwork_reuse", + "rebar_complexity", + "timber_structure_class", + "masonry_slope", + "masonry_back_length", + "stone_kind", + "masonry_class", +) -def test_기초단가_다섯이_다_있다(labels): +def test_기초데이터_열넷이_다_있다(labels): merged = {m["key"]: m for m in labels["merged_tables"]} - assert tuple(merged) == MERGED_KEYS + assert set(merged) == set(MERGED_KEYS) for entry in merged.values(): assert entry["name_ko"].strip() assert entry["summary"].strip()