diff --git a/Z01_MasterData/Z01_MasterData_Api_Fetch.ts b/Z01_MasterData/Z01_MasterData_Api_Fetch.ts index 7d881ee6..7a76b9e7 100644 --- a/Z01_MasterData/Z01_MasterData_Api_Fetch.ts +++ b/Z01_MasterData/Z01_MasterData_Api_Fetch.ts @@ -47,9 +47,26 @@ export interface RowsQuery { sort?: SortOrder | null; } -/** 기초단가 다섯 — 고친 값은 원본이 아니라 덮개 파일에 쌓임(품셈이 갱신돼도 안 날아감). */ +/** + * ① 기초단가 다섯 — 얼마인가. 시장이 정하고 주간~반기로 바뀜. + * 고친 값은 원본이 아니라 덮개 파일에 쌓임(품셈이 갱신돼도 안 날아감). + */ export const BASE_PRICE_KINDS = ["labor", "machine", "material", "oil", "rate"] as const; -export type BasePriceKind = (typeof BASE_PRICE_KINDS)[number]; + +/** ② 품셈 기준 아홉 — 얼마나 드나. 품셈이 정하고 해마다 한 번 개정(2026-09-16 사용자 ①②). */ +export const SPEC_KINDS = [ + "coef", + "material_surcharge", + "formwork_reuse", + "rebar_complexity", + "timber_structure_class", + "masonry_slope", + "masonry_back_length", + "stone_kind", + "masonry_class", +] as const; + +export type BasePriceKind = (typeof BASE_PRICE_KINDS)[number] | (typeof SPEC_KINDS)[number]; async function request(path: string, init: RequestInit = {}): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { diff --git a/Z01_MasterData/Z01_MasterData_UI_Side.ts b/Z01_MasterData/Z01_MasterData_UI_Side.ts index c50d6fed..a0fb7fee 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Side.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Side.ts @@ -15,9 +15,11 @@ import { fetchMasterRows, fetchOverrides, type MasterGroup, + type BasePriceKind, type MasterRows, type RowsQuery, saveBasePrice, + SPEC_KINDS, VALUES_TABLE_ID, } from "./Z01_MasterData_Api_Fetch"; import { valueWithUnit, type OverrideState } from "./Z01_MasterData_UI_Cells"; @@ -31,25 +33,32 @@ export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLEleme }; const baseTitle = L("Z01_MasterData_BasePrices"); - const baseGrid = el("div", { className: "z01-master__base-grid" }); - for (const kind of BASE_PRICE_KINDS) { - const label = L(`Z01_MasterData_Base_${kind}` as const); - const button = el("button", { - className: "z01-master__base", - attrs: { type: "button" }, - text: label, - }); - button.addEventListener("click", () => { - activate(button); - view.show({ - title: `${baseTitle} › ${label}`, - key: `base-prices/${kind}`, - load: (query) => fetchBasePrices(kind, query), - save: (rowId, values) => saveBasePrice(kind, rowId, values), + const specTitle = L("Z01_MasterData_SpecValues"); + /** 한 상자 몫 단추들 — ①얼마인가 ②얼마나 드나 로 갈라 세움(성격도 갱신 주기도 다름). */ + const kindGrid = (boxTitle: string, kinds: readonly BasePriceKind[]): HTMLElement => { + const grid = el("div", { className: "z01-master__base-grid" }); + for (const kind of kinds) { + const label = L(`Z01_MasterData_Base_${kind}` as const); + const button = el("button", { + className: "z01-master__base", + attrs: { type: "button" }, + text: label, }); - }); - baseGrid.append(button); - } + button.addEventListener("click", () => { + activate(button); + view.show({ + title: `${boxTitle} › ${label}`, + key: `base-prices/${kind}`, + load: (query) => fetchBasePrices(kind, query), + save: (rowId, values) => saveBasePrice(kind, rowId, values), + }); + }); + grid.append(button); + } + return grid; + }; + const baseGrid = kindGrid(baseTitle, BASE_PRICE_KINDS); + const specGrid = kindGrid(specTitle, SPEC_KINDS); const overridesButton = el("button", { className: "z01-master__base z01-master__base--wide", attrs: { type: "button" }, @@ -113,7 +122,11 @@ export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLEleme tree.append(groupBox); } - panel.append(section(baseTitle, baseGrid), section(L("Z01_MasterData_Tree"), tree)); + panel.append( + section(baseTitle, baseGrid), + section(specTitle, specGrid), + section(L("Z01_MasterData_Tree"), tree), + ); attachCollapsible(panel); return panel; } diff --git a/resources/data_cost_input_value/_manifest.json b/resources/data_cost_input_value/_manifest.json index 56945ecf..389c2456 100644 --- a/resources/data_cost_input_value/_manifest.json +++ b/resources/data_cost_input_value/_manifest.json @@ -13,8 +13,8 @@ "file": "coef_2026.json", "dataset_id": "coef", "effective_date": "2026-01-01", - "sha256": "0bc3fe981165e8d320b7c20f4535c545f2f54415427ac60205dac4a72dbb4f19", - "size_bytes": 29413 + "sha256": "daba170cd806374c92e086dec69f9559271c0317a7426e66e9dc841c2ca264bd", + "size_bytes": 31715 }, { "file": "fx_2026-08-14.json", diff --git a/resources/data_cost_input_value/coef_2026.json b/resources/data_cost_input_value/coef_2026.json index b2654f01..51cb6ef4 100644 --- a/resources/data_cost_input_value/coef_2026.json +++ b/resources/data_cost_input_value/coef_2026.json @@ -787,6 +787,7 @@ ] }, "surcharge_mat": { + "used_value_at": "resources/data_material_surcharge/material_surcharge_2026-01-01.json → rates_pct — ⚠ 코드가 읽어 쓰는 값은 저기다. 이 표는 원문 보관용이라 읽는 코드가 없다(두 벌이 아니다).", "unit": "percent", "representation": "full_source_tables", "duplicate_application_prohibited": true, @@ -1108,6 +1109,8 @@ "max": 1.45, "duplicate_group": "암괴나 호박돌이 섞인 점토", "duplicate_index": 1, + "duplicate_same_value": true, + "duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).", "selection": "pending" }, { @@ -1129,6 +1132,9 @@ "max": 1.45, "duplicate_group": "암괴나 호박돌이 섞인 점토", "duplicate_index": 2, + "duplicate_same_value": true, + "hidden_duplicate": true, + "duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).", "selection": "pending" } ] @@ -1210,6 +1216,8 @@ "max": 0.95, "duplicate_group": "암괴나 호박돌이 섞인 점토", "duplicate_index": 1, + "duplicate_same_value": true, + "duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).", "selection": "pending" }, { @@ -1231,6 +1239,9 @@ "max": 0.95, "duplicate_group": "암괴나 호박돌이 섞인 점토", "duplicate_index": 2, + "duplicate_same_value": true, + "hidden_duplicate": true, + "duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).", "selection": "pending" } ] diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index 4607da7e..c91f964b 100644 --- a/resources/data_master_labels/labels_2026-01-01.json +++ b/resources/data_master_labels/labels_2026-01-01.json @@ -2,14 +2,14 @@ "schema_version": "1.0", "dataset_id": "data_master_labels", "effective_date": "2026-01-01", - "generated_at": "2026-09-16T11:39:55+09:00", + "generated_at": "2026-09-16T13:11:45+09:00", "note": "마스터 자료의 **사람이 읽을 이름표**. 값이 아니라 이름만 담는다 — 여기를 고쳐도 계산은 안 바뀐다. Z01 마스터 관리 화면이 표·열 이름을 여기서 읽는다.", "policy": { "labels_only": "값·수식·단가를 담지 않는다. 마스터 파일은 손대지 않는다.", "no_invented_meaning": "뜻을 모르는 열은 name_ko 를 비우고 unknown 에 사유를 적는다. 지어내지 않는다.", "unit_is_label_only": "unit 은 이름표에서만 정한다. 자료에 없는 단위를 만들어 붙이지 않는다.", "counting_rule": "표 = 화면이 표로 그릴 마디(줄이 여럿인 마디). 값 묶음 = 표가 아닌 마디(설명·방침·한 값짜리 요율 따위).", - "brain_kind_table": "갈래는 2026-09-15 브레인 갈래 나눔표를 그대로 따른다(로직 17 · 기초값 9 · 부산물 4 · 씨앗 1).", + "brain_kind_table": "갈래는 2026-09-15 브레인 갈래 나눔표에서 출발한다(로직 17 · 기초값 9 · 부산물 4 · 씨앗 1). 2026-09-16 사용자 판정으로 `structure_unit_observed`·`revetment_sabang` 둘을 **참고 사례**로 옮겼다 — 한 현장 관찰값·교본 설명 섞임이라 기초데이터가 아니다(로직 15 · 참고 사례 2).", "out_of_31": "폴더 지문(_manifest.json) 셋은 브레인 31 밖이다. 부산물이 아니라 **지문** 갈래로 둔다 — 계산이 남긴 기록이 아니라 어느 판으로 셈했는지 못박는 표라 「정본 아님」 딱지가 안 맞는다(2026-09-15 브레인 ②).", "excluded": "강우 IDF 캐시(resources/data_rainfall_idf_cache, 96 파일)는 배수용이라 뺀다.", "merged_tables_rule": "`merged_tables` 는 **기초단가 다섯**(노임·자재·유가·제비율·기계)을 화면이 한 표씩으로 보이려고 여러 원본 표를 모은 자리다. ⚠ **값을 옮기지 않는다** — 열 이름·단위·숨김과 `editable`(고칠 수 있나)만 담는다. `editable: false` 는 다른 값에서 나온 계산값이라 사람이 못 고친다. 기계는 한 표로 안 모이므로 `machine_cost_chain` 이 사슬을 적는다 (2026-09-15 사용자 지시 · 브레인).", @@ -45,6 +45,10 @@ "name_ko": "지문", "summary": "어느 판으로 셈했는지 못박는 자리. 값도 규칙도 아니고 **판을 가리키는 표**다 — 고치지 않는다." }, + "reference": { + "name_ko": "참고 사례", + "summary": "한 현장·한 교본에서 본 값. ⚠ 표준이 아니다 — 근거로 보되 기초데이터처럼 갈아 끼우지 않는다." + }, "seed": { "name_ko": "씨앗", "summary": "앞으로 늘려 갈 첫 벌. 모양을 잡아 두려고 먼저 놓은 것이다." @@ -766,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": { @@ -993,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": { @@ -1086,15 +1610,15 @@ } }, "counts": { - "merged_tables": 5, - "merged_columns": 89, + "merged_tables": 14, + "merged_columns": 136, "files": 34, "tables": 91, - "columns": 456, - "value_groups": 173, + "columns": 462, + "value_groups": 175, "unknown": 0, "value_groups_by_kind": { - "doc": 107, + "doc": 109, "value": 66 } }, @@ -1484,6 +2008,24 @@ "name_ko": "채택 여부", "unit": "", "visible": true + }, + { + "key": "duplicate_same_value", + "name_ko": "값 같은 중복", + "unit": "", + "visible": true + }, + { + "key": "duplicate_reason", + "name_ko": "중복 사유", + "unit": "", + "visible": true + }, + { + "key": "hidden_duplicate", + "name_ko": "접힌 줄", + "unit": "", + "visible": false } ] }, @@ -1529,6 +2071,24 @@ "name_ko": "채택 여부", "unit": "", "visible": true + }, + { + "key": "duplicate_same_value", + "name_ko": "값 같은 중복", + "unit": "", + "visible": true + }, + { + "key": "duplicate_reason", + "name_ko": "중복 사유", + "unit": "", + "visible": true + }, + { + "key": "hidden_duplicate", + "name_ko": "접힌 줄", + "unit": "", + "visible": false } ] }, @@ -1665,6 +2225,12 @@ "name_ko": "값 단위", "summary": "할증은 %다.", "kind": "value" + }, + { + "key": "variables/surcharge_mat/used_value_at", + "name_ko": "쓰는 값이 있는 자리", + "summary": "이 표는 원문 보관용이라 읽는 코드가 없다. 값은 `material_surcharge` 의 `rates_pct` 를 읽는다.", + "kind": "doc" } ] }, @@ -1827,6 +2393,12 @@ "summary": "실무 집계에서 본 할증률. 참고이지 기본값이 아니다.", "kind": "value" }, + { + "key": "original_tables", + "name_ko": "원문 표가 실린 자리", + "summary": "같은 값이 `coef` 의 `surcharge_mat` 에도 있다. ⚠ 두 벌이 아니다 — 저쪽은 원문 보관이고 읽어 쓰는 정본은 여기다.", + "kind": "doc" + }, { "key": "policy", "name_ko": "방침", @@ -2562,9 +3134,9 @@ { "file_id": "revetment_sabang", "path": "resources/data_masonry/revetment_sabang_2026-01-01.json", - "name_ko": "기슭막이 치수(사방)", - "summary": "독립 기슭막이의 높이·계획비탈·둑마루 두께·뒷채움을 한 자리에 둔 벌.", - "kind": "logic", + "name_ko": "기슭막이 치수(사방) — 참고 사례", + "summary": "사방기술교본 값에 설명 글이 섞인 벌. ⚠ 기초데이터가 아니다 — 표준값처럼 쓰지 말고 근거로만 본다(2026-09-16 사용자 판정).", + "kind": "reference", "tables": [], "value_groups": [ { @@ -2596,9 +3168,9 @@ { "file_id": "structure_unit_observed", "path": "resources/data_structure_unit/structure_unit_observed_2026-01-01.json", - "name_ko": "구조물 관측 원단위", - "summary": "품셈에 표준 물량표가 없는 콘크리트 구조물을 실무 설계원본에서 뽑은 벌.", - "kind": "logic", + "name_ko": "구조물 관측 원단위 — 참고 사례", + "summary": "울진소광 **한 현장**을 관찰한 값이다. ⚠ 기초데이터가 아니다 — 규격이 다르면 안 맞고 표준도 아니다(2026-09-16 사용자 판정).", + "kind": "reference", "tables": [ { "key": "double_count_rules/rules", @@ -6320,6 +6892,16 @@ "unit": "", "visible": true }, + "duplicate_reason": { + "name_ko": "중복 사유", + "unit": "", + "visible": true + }, + "duplicate_same_value": { + "name_ko": "값 같은 중복", + "unit": "", + "visible": true + }, "duration_bracket": { "name_ko": "공사기간 구간", "unit": "", @@ -6470,6 +7052,11 @@ "unit": "", "visible": true }, + "hidden_duplicate": { + "name_ko": "접힌 줄", + "unit": "", + "visible": false + }, "hours_per_day": { "name_ko": "1일 근로시간", "unit": "시간", @@ -7479,6 +8066,10 @@ "name_ko": "값 단위", "summary": "할증은 %다." }, + "coef::variables/surcharge_mat/used_value_at": { + "name_ko": "쓰는 값이 있는 자리", + "summary": "이 표는 원문 보관용이라 읽는 코드가 없다. 값은 `material_surcharge` 의 `rates_pct` 를 읽는다." + }, "formwork_reuse::reuse_ratio_pct": { "name_ko": "사용횟수별 기준수량 비율", "summary": "품셈 12-4 — 일위대가 재료비에 걸린다. B08 이 곱하면 이중계상이다." @@ -7599,6 +8190,10 @@ "name_ko": "실무 관측", "summary": "실무 집계에서 본 할증률. 참고이지 기본값이 아니다." }, + "material_surcharge::original_tables": { + "name_ko": "원문 표가 실린 자리", + "summary": "같은 값이 `coef` 의 `surcharge_mat` 에도 있다. ⚠ 두 벌이 아니다 — 저쪽은 원문 보관이고 읽어 쓰는 정본은 여기다." + }, "oil::variables/oil_diesel": { "name_ko": "경유 단가", "summary": "전국평균 경유 값(원/L)." diff --git a/resources/data_master_sources/sources_2026-09-15.json b/resources/data_master_sources/sources_2026-09-15.json index ec955a7e..09287064 100644 --- a/resources/data_master_sources/sources_2026-09-15.json +++ b/resources/data_master_sources/sources_2026-09-15.json @@ -2,11 +2,12 @@ "schema_version": "1.0", "dataset_id": "data_master_sources", "effective_date": "2026-09-15", - "generated_at": "2026-09-15T21:59:23+09:00", + "generated_at": "2026-09-16T14:05:40+09:00", "note": "기초단가 자료가 **어디서 오고 얼마 만에 바뀌는지** 적은 표. 파일 안에는 제 판만 있고 「최신이 무엇인지·어디서 받는지」가 없어 건설 노임이 반 년 뒤처진 것을 아무도 몰랐다(2026-09-15). 그 자리를 메운다.", "policy": { "values_untouched": "값을 담지 않는다. 자료가 어디서 오는지만 담는다.", "our_edition_is_read": "`our_edition` 은 **실제 파일에서 읽는다**(`effective_date`, 파생본은 `derived_from.effective_date`). 손으로 적으면 갱신하고 안 고쳐 또 뒤처진다.", + "address_found_on": "받는 자리 일곱은 2026-09-16 에 안티그래비티가 찾아 채웠다(제조노임·기계·기계 운전경비·자재·유가 전국·유가 지역·요율). 건설노임 한 줄은 그 전(2026-09-15)에 브레인이 준 것이다.", "no_invented_address": "받는 자리는 **받은 것만** 적는다. 모르면 빈 칸으로 두고 사유를 단다 — 주소를 지어내지 않는다.", "status_rule": "`latest_published` 가 `our_edition` 보다 뒤면 **뒤처짐**, 같으면 최신, 둘 가운데 하나라도 없으면 **모름**. 날마다 바뀌는 유가는 날짜 차이로 보여야 한다." }, @@ -65,19 +66,18 @@ "kind_ko": "노임", "name_ko": "중소제조업 직종별 임금", "publisher": "중소기업중앙회", - "where_to_get": "", + "where_to_get": "중소기업중앙회 https://www.kbiz.or.kr | [정보마당] > [조사연구통계](게시판 mnSeq=325) > 「중소제조업 직종별 임금조사보고서」 PDF", "cycle": "반기", "latest_published": "", "latest_basis": "", "our_edition": "2026-07-01", "our_edition_from": "effective_date", "status": "unknown", - "checked_on": "2026-09-15", - "note": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다. 우리 판은 2026 상반기 조사(조사월 2026-03 · 공표 2026-06-30)다.", + "checked_on": "2026-09-16", + "note": "우리 판은 2026 상반기 조사(조사월 2026-03 · 공표 2026-06-30)다. ⚠ 게시판에 반기마다 새 보고서가 올라오므로 **최신 공표일은 게시판에서 봐야** 한다.", "files": [ "resources/data_cost_input_value/labor_mfg_2026-07-01.json" ], - "where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.", "latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다." }, { @@ -86,19 +86,18 @@ "kind_ko": "기계", "name_ko": "건설공사 표준품셈 제8장 건설기계", "publisher": "국토교통부", - "where_to_get": "", + "where_to_get": "공사비원가관리센터 https://cost.kict.re.kr | [표준품셈] (또는 CODIL https://www.codil.or.kr [건설기준정보] > [건설공사표준품셈]) > 건설공사 표준품셈 원문 PDF(공통부문 제8장 건설기계 8-3)", "cycle": "해마다", "latest_published": "2026-01-01", "latest_basis": "2026-09-15 브레인 쪽지", "our_edition": "2026-01-01", "our_edition_from": "effective_date", "status": "current", - "checked_on": "2026-09-15", + "checked_on": "2026-09-16", "note": "우리 판이 최신이다. 취득가·손료계수·연료소모량·조종원이 다 여기서 온다.", "files": [ "resources/data_cost_input_value/mach_base_2026.json" - ], - "where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다." + ] }, { "source_id": "machine_operating", @@ -106,19 +105,18 @@ "kind_ko": "기계", "name_ko": "건설기계 운전경비(품셈 8-4 파생본)", "publisher": "국토교통부", - "where_to_get": "", + "where_to_get": "공사비원가관리센터 https://cost.kict.re.kr | 별도 배포처 없음(위 건설공사 표준품셈 원문과 같음) > 공통부문 제8장 건설기계 「8-4 건설기계 운전경비 산정」 표", "cycle": "해마다 — 위 품셈을 따라간다", "latest_published": "2026-01-01", "latest_basis": "파생 원본(mach_base)의 판", "our_edition": "2026-01-01", "our_edition_from": "derived_from.effective_date", "status": "current", - "checked_on": "2026-09-15", + "checked_on": "2026-09-16", "note": "⚠ **판을 모르는 파일이 아니다** — 파일 안 `derived_from.effective_date` 에 2026-01-01 이 박혀 있다(건설품셈 8-4 운전경비 파생본 · 원본 지문까지 함께). `mach_fuel_rate`·`mach_operator_map` 이 다 차면 걷어낼 자리다.", "files": [ "resources/data_cost_machine_operating/machine_operating_2026.json" - ], - "where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다." + ] }, { "source_id": "mat_price_public", @@ -126,19 +124,18 @@ "kind_ko": "자재", "name_ko": "관급자재 단가(시설공통자재)", "publisher": "조달청", - "where_to_get": "", + "where_to_get": "나라장터 https://www.g2b.go.kr | [가격정보] > [시설공통자재] (또는 조달청 https://www.pps.go.kr [정보제공] > [업무별자료] > [시설공사]) > 시설공통자재 가격조사 엑셀", "cycle": "반기 + 수시", "latest_published": "", "latest_basis": "", "our_edition": "2026-08-14", "our_edition_from": "effective_date", "status": "unknown", - "checked_on": "2026-09-15", + "checked_on": "2026-09-16", "note": "수시 공고라 「최신 공표일」이 한 날로 안 정해진다 — 받은 날(우리 판)이 곧 기준이다. 철근·레미콘·아스콘은 이 벌에 없다(쇼핑몰 스냅숏 미확보).", "files": [ "resources/data_cost_input_value/mat_price_public_2026-08-14.json" ], - "where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.", "latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다." }, { @@ -147,19 +144,18 @@ "kind_ko": "유가", "name_ko": "유가 — 전국평균", "publisher": "한국석유공사(오피넷)", - "where_to_get": "", + "where_to_get": "오피넷 https://www.opinet.co.kr | [국내유가통계] > [주유소] > [평균판매가격] — 「전국」 탭 조회 뒤 [엑셀저장](API avgAllPrice.do)", "cycle": "주간 · 일별", "latest_published": "", "latest_basis": "", "our_edition": "2026-08-14", "our_edition_from": "effective_date", "status": "unknown", - "checked_on": "2026-09-15", + "checked_on": "2026-09-16", "note": "날마다 바뀌는 값이라 「최신」이 곧 오늘이다. **뒤처짐을 날짜 차이로 보여야 하는 자료**다. ⚠ 시도별 판과 「전국」 줄이 겹치는데 값도 기준일도 다르다(어느 쪽을 쓸지 판정 대기).", "files": [ "resources/data_cost_input_value/oil_2026-08-14.json" ], - "where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.", "latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다." }, { @@ -168,19 +164,18 @@ "kind_ko": "유가", "name_ko": "유가 — 시도별", "publisher": "한국석유공사(오피넷)", - "where_to_get": "", + "where_to_get": "오피넷 https://www.opinet.co.kr | [국내유가통계] > [주유소] > [평균판매가격] — 「지역별」 탭에서 시도를 고른 뒤 [엑셀저장](API avgSidoPrice.do)", "cycle": "주간 · 일별", "latest_published": "", "latest_basis": "", "our_edition": "2026-09-09", "our_edition_from": "effective_date", "status": "unknown", - "checked_on": "2026-09-15", + "checked_on": "2026-09-16", "note": "품셈 8-1-7 5호 「유류가격은 해당지역의 가격으로 한다」를 따르려고 둔 벌이다.", "files": [ "resources/data_cost_input_value/oil_regional_2026-09-09.json" ], - "where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.", "latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다." }, { @@ -189,19 +184,18 @@ "kind_ko": "요율", "name_ko": "토목공사 원가계산 제비율 적용기준", "publisher": "조달청", - "where_to_get": "", + "where_to_get": "조달청 https://www.pps.go.kr | [정보제공] > [업무별자료] > [시설공사] > 「시설공사 원가계산 간접공사비(제비율) 적용기준」 토목 엑셀(재게시 kseis https://www.kseis.co.kr 자료실)", "cycle": "해마다 + 수시(법이 바뀌면)", "latest_published": "2026-04-13", "latest_basis": "2026-09-15 브레인 쪽지", "our_edition": "2026-04-13", "our_edition_from": "effective_date", "status": "current", - "checked_on": "2026-09-15", + "checked_on": "2026-09-16", "note": "우리 판이 최신이다. 국민연금 요율은 국민연금법(현행 2026-06-17)을 따로 물었다. ⚠ 요율은 **법이 바뀌면 주기와 상관없이** 바뀐다 — 주기만 보고 안심하면 안 된다.", "files": [ "resources/data_cost_input_value/rates_2026.json" - ], - "where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다." + ] } ] } diff --git a/resources/data_material_surcharge/_manifest.json b/resources/data_material_surcharge/_manifest.json index 31b2bde7..a8192720 100644 --- a/resources/data_material_surcharge/_manifest.json +++ b/resources/data_material_surcharge/_manifest.json @@ -6,8 +6,8 @@ "files": [ { "file": "material_surcharge_2026-01-01.json", - "sha256": "30a1e11acb6f6bd52c72e0ff471c96f772917858306ea61f66ecbcb5df433c9f", - "size_bytes": 5087 + "sha256": "1394a39be22ac0eb7476e4bc2b81c1e23c365a4ad5b915befe85d3f0f99023d5", + "size_bytes": 5344 } ] } diff --git a/resources/data_material_surcharge/material_surcharge_2026-01-01.json b/resources/data_material_surcharge/material_surcharge_2026-01-01.json index 2e732b1e..03a971a1 100644 --- a/resources/data_material_surcharge/material_surcharge_2026-01-01.json +++ b/resources/data_material_surcharge/material_surcharge_2026-01-01.json @@ -1,6 +1,7 @@ { "schema_version": "1.0", "dataset_id": "material_surcharge", + "original_tables": "resources/data_cost_input_value/coef_2026.json → variables/surcharge_mat — 원문 표가 실린 자리. ⚠ 값이 겹치지만 이원화가 아니다: 저쪽은 원문 보관이고 읽어 쓰는 정본은 여기(rates_pct)다.", "effective_date": "2026-01-01", "source": { "primary": { 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 8e5504f9..9a9a9c4a 100644 --- a/resources/tester/test_master_labels_cover.py +++ b/resources/tester/test_master_labels_cover.py @@ -19,7 +19,9 @@ ROOT = Path(__file__).resolve().parents[2] LABELS_PATH = ROOT / "resources" / "data_master_labels" / "labels_2026-01-01.json" #: 브레인 갈래 나눔표(2026-09-15). 폴더 장부(_manifest)는 그 31 밖이라 따로 센다. -BRAIN_KIND_COUNTS = {"logic": 17, "base_value": 9, "byproduct": 4, "seed": 1} +#: 2026-09-16 사용자 판정 — `structure_unit_observed`(울진소광 한 현장 관찰값)와 +#: `revetment_sabang`(교본값에 설명 글 섞임)은 기초데이터가 아니라 **참고 사례**다. +BRAIN_KIND_COUNTS = {"logic": 15, "base_value": 9, "byproduct": 4, "seed": 1, "reference": 2} MANIFEST_PREFIX = "manifest_" META_KEYS = { @@ -449,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() diff --git a/resources/tester/test_z01_master_data.py b/resources/tester/test_z01_master_data.py index f7f00f68..3814a58e 100644 --- a/resources/tester/test_z01_master_data.py +++ b/resources/tester/test_z01_master_data.py @@ -49,7 +49,15 @@ def test_갈래는_이름표_차례_파일_34_강우_캐시는_없음(client: Te groups = _tree(client)["groups"] assert [g["key"] for g in groups] == [k for k in LABELS["kinds"]] counts = {g["key"]: len(g["files"]) for g in groups} - assert counts == {"logic": 17, "base_value": 9, "byproduct": 4, "fingerprint": 3, "seed": 1} + # 2026-09-16 사용자 판정 — 한 현장 관찰값·교본 설명 섞임 둘은 참고 사례로 옮겼다. + assert counts == { + "logic": 15, + "base_value": 9, + "byproduct": 4, + "reference": 2, + "fingerprint": 3, + "seed": 1, + } assert {g["key"]: g["label"] for g in groups}["base_value"] == "기초값" by_id = {f["file_id"]: f for f in LABELS["files"]} for f in (f for g in groups for f in g["files"]): diff --git a/ui_template/ui_template_locale_b3.ts b/ui_template/ui_template_locale_b3.ts index 662fc14c..866bf245 100644 --- a/ui_template/ui_template_locale_b3.ts +++ b/ui_template/ui_template_locale_b3.ts @@ -147,6 +147,17 @@ export const ui_locales_b3 = { Z01_MasterData_Base_material: ["자재단가", "Material"], Z01_MasterData_Base_oil: ["유가", "Fuel"], Z01_MasterData_Base_rate: ["요율", "Rates"], + /* ② 품셈 기준 아홉 — 얼마나 드나(품셈이 정하고 해마다 한 번 개정) */ + Z01_MasterData_SpecValues: ["품셈 기준", "Standard rates"], + Z01_MasterData_Base_coef: ["토량환산", "Soil conversion"], + Z01_MasterData_Base_material_surcharge: ["자재할증", "Material surcharge"], + Z01_MasterData_Base_formwork_reuse: ["거푸집", "Formwork reuse"], + Z01_MasterData_Base_rebar_complexity: ["철근", "Rebar"], + Z01_MasterData_Base_timber_structure_class: ["목구조", "Timber structure"], + Z01_MasterData_Base_masonry_slope: ["돌쌓기 경사", "Masonry slope"], + Z01_MasterData_Base_masonry_back_length: ["뒷길이", "Back length"], + Z01_MasterData_Base_stone_kind: ["돌종류", "Stone kind"], + Z01_MasterData_Base_masonry_class: ["갈래", "Masonry class"], Z01_MasterData_Overrides: ["고친 것 모아 보기", "Edited values"], Z01_MasterData_Edit_Can: [ "고칠 수 있는 칸 — 눌러 고치고 Enter 로 저장 (Esc 취소)",