diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index ef78ff05..6577a6ae 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-15T21:46:51+09:00", + "generated_at": "2026-09-16T10:49:25+09:00", "note": "마스터 자료의 **사람이 읽을 이름표**. 값이 아니라 이름만 담는다 — 여기를 고쳐도 계산은 안 바뀐다. Z01 마스터 관리 화면이 표·열 이름을 여기서 읽는다.", "policy": { "labels_only": "값·수식·단가를 담지 않는다. 마스터 파일은 손대지 않는다.", @@ -66,16 +66,22 @@ "name_ko": "출처", "unit": "", "visible": true, - "editable": true, "from": "새 칸", "note": "건설업(대한건설협회) · 제조업(중소기업중앙회). ⚠ 이 칸이 없으면 두 코드 공간이 섞인다." }, + { + "key": "effective_date", + "name_ko": "판 기준일", + "unit": "", + "visible": true, + "from": "둘 다", + "note": "두 공표의 판이 달라 한 표에 두 날짜가 선다." + }, { "key": "occupation_code", "name_ko": "직종코드", "unit": "", "visible": true, - "editable": true, "from": "둘 다" }, { @@ -83,23 +89,13 @@ "name_ko": "직종명", "unit": "", "visible": true, - "editable": true, "from": "둘 다" }, - { - "key": "industry", - "name_ko": "업종", - "unit": "", - "visible": true, - "editable": true, - "from": "제조업만" - }, { "key": "unit", "name_ko": "단위", "unit": "", "visible": true, - "editable": true, "from": "둘 다", "note": "둘 다 KRW/day — 어긋남 없음." }, @@ -108,7 +104,6 @@ "name_ko": "1일 근로시간", "unit": "시간", "visible": true, - "editable": true, "from": "건설업만" }, { @@ -116,15 +111,14 @@ "name_ko": "일 노임", "unit": "원", "visible": true, - "editable": true, - "from": "둘 다" + "from": "둘 다", + "note": "⚠ 30 줄은 칸 자체가 없다 — 미조사라 **값 없음이 제값**이다." }, { "key": "status", "name_ko": "공표 상태", "unit": "", "visible": true, - "editable": true, "from": "둘 다" }, { @@ -132,16 +126,28 @@ "name_ko": "신뢰도 기호", "unit": "", "visible": true, - "editable": true, "from": "건설업만", - "note": "* 조사현장 5개 미만 · ** 미조사. 뜻풀이가 원본에 있다." + "note": "* 조사현장 5개 미만 · ** 미조사. 없으면 정상 공표." + }, + { + "key": "industry", + "name_ko": "업종", + "unit": "", + "visible": true, + "from": "제조업만" + }, + { + "key": "variation_coefficient", + "name_ko": "변동계수", + "unit": "%", + "visible": true, + "from": "제조업만" }, { "key": "source_flag", "name_ko": "원문 표시", "unit": "", "visible": true, - "editable": true, "from": "제조업만", "note": "`*` 16 줄. ⚠ 뜻풀이가 원본에 없어 신뢰도 기호와 한 칸으로 못 묶는다." } @@ -156,103 +162,97 @@ ], "rows_note": "6,999 줄 — 다섯 가운데 가장 크다", "columns": [ + { + "key": "effective_date", + "name_ko": "판 기준일", + "unit": "", + "visible": true + }, { "key": "item_code", "name_ko": "물품코드", "unit": "", "visible": true, - "editable": true + "note": "⭐ 줄 열쇠 — 6,999 줄 겹침 0." }, { "key": "classification_code", "name_ko": "물품분류번호", "unit": "", - "visible": true, - "editable": true + "visible": true }, { "key": "classification_name", "name_ko": "물품분류명", "unit": "", - "visible": true, - "editable": true + "visible": true }, { "key": "specification", "name_ko": "규격", "unit": "", "visible": true, - "editable": true + "note": "⚠ 2 줄은 빈 문자열이다(`null` 아님)." }, { "key": "unit", "name_ko": "단위", "unit": "", - "visible": true, - "editable": true + "visible": true }, { "key": "price_krw", "name_ko": "단가", "unit": "원", - "visible": true, - "editable": true - }, - { - "key": "vat_basis", - "name_ko": "부가세 기준", - "unit": "", - "visible": true, - "editable": true - }, - { - "key": "price_type", - "name_ko": "가격 종류", - "unit": "", - "visible": true, - "editable": true - }, - { - "key": "delivery_condition", - "name_ko": "납품 조건", - "unit": "", - "visible": true, - "editable": true - }, - { - "key": "business_division_name", - "name_ko": "사업구분", - "unit": "", - "visible": true, - "editable": true - }, - { - "key": "field", - "name_ko": "분야", - "unit": "", - "visible": true, - "editable": true + "visible": true }, { "key": "notice_datetime", "name_ko": "공고 일시", "unit": "", - "visible": true, - "editable": true + "visible": true }, { "key": "notice_number", "name_ko": "공고번호", "unit": "", - "visible": false, - "editable": true + "visible": false }, { "key": "business_division_code", "name_ko": "사업구분 코드", "unit": "", - "visible": false, - "editable": true + "visible": false + }, + { + "key": "business_division_name", + "name_ko": "사업구분", + "unit": "", + "visible": true + }, + { + "key": "vat_basis", + "name_ko": "부가세 기준", + "unit": "", + "visible": true + }, + { + "key": "price_type", + "name_ko": "가격 종류", + "unit": "", + "visible": true + }, + { + "key": "delivery_condition", + "name_ko": "납품 조건", + "unit": "", + "visible": true + }, + { + "key": "field", + "name_ko": "분야", + "unit": "", + "visible": true } ] }, @@ -267,224 +267,320 @@ ], "rows_note": "전국평균 2 + 시도별 34 = 36 줄", "columns": [ - { - "key": "fuel", - "name_ko": "유종", - "unit": "", - "visible": true, - "editable": true, - "from": "원본 키", - "note": "휘발유 · 경유." - }, { "key": "source", "name_ko": "출처", "unit": "", "visible": true, - "editable": true, "from": "새 칸", - "note": "전국평균 판 · 시도별 판. 기준일이 판마다 다르다." + "note": "전국평균 판 · 시도별 판." }, { - "key": "region_code", - "name_ko": "시도코드", + "key": "fuel", + "name_ko": "유종", "unit": "", "visible": true, - "editable": true, - "from": "시도판", - "note": "전국평균 판은 `00` 으로 맞춘다." + "from": "원본 키", + "note": "휘발유(`oil_gasoline`) · 경유(`oil_diesel`)." }, { - "key": "region_name", - "name_ko": "시도명", + "key": "effective_date", + "name_ko": "판 기준일", "unit": "", "visible": true, - "editable": true, - "from": "시도판" + "from": "둘 다", + "note": "전국 판과 지역 판의 판이 다르다." }, { - "key": "price_krw_per_l", - "name_ko": "단가", - "unit": "원/L", + "key": "unit", + "name_ko": "단위", + "unit": "", "visible": true, - "editable": true, "from": "둘 다" }, { "key": "date", - "name_ko": "기준일", + "name_ko": "원천 조사일", "unit": "", "visible": true, - "editable": true, "from": "둘 다", - "note": "⚠ 판마다 다르다 — 같은 표에 두 날짜가 선다." - }, - { - "key": "source_product_name", - "name_ko": "원천 제품명", - "unit": "", - "visible": false, - "editable": true, - "from": "둘 다" + "note": "⚠ 판 기준일과 다른 칸이다 — 원천이 값을 잰 날." }, { "key": "source_product_code", "name_ko": "원천 제품코드", "unit": "", "visible": false, - "editable": true, "from": "둘 다" + }, + { + "key": "source_product_name", + "name_ko": "원천 제품명", + "unit": "", + "visible": true, + "from": "둘 다" + }, + { + "key": "price_krw_per_l", + "name_ko": "단가", + "unit": "원/L", + "visible": true, + "from": "둘 다" + }, + { + "key": "region_code", + "name_ko": "시도코드", + "unit": "", + "visible": true, + "from": "시도판", + "note": "전국평균 판은 `00`." + }, + { + "key": "region_name", + "name_ko": "시도명", + "unit": "", + "visible": true, + "from": "시도판" } ] }, { - "key": "rates", + "key": "rate", "name_ko": "제비율", "summary": "법정경비·일반관리비·이윤·부가세 요율 한 표. 원본 19 벌을 한 줄씩 펴서 모은다.", "sources": [ "rates::variables/* (19 벌)" ], - "rows_note": "한 값짜리 8 + 구간표 11 벌을 펴서 약 190 줄", + "rows_note": "한 값짜리 8 + 구간표 14 개를 펴서 약 190 줄", + "axis_note": "⭐ **구간 축을 한 칸으로 합치지 않는다**(2026-09-15 브레인 ④). 서버가 원문 칸을 그대로 펴고 이름표가 그 틀에 맞춘다 — 합치면 ㉠ 어느 축의 구간인지 사라지고 ㉡ 축이 둘 이상인 줄(안전관리비 = 대상액 × 공사 종류 · 간접노무비 = 직접공사비 × 공사기간 × 공사 종류)을 못 읽으며 ㉢ 고친 값을 거는 열쇠(`@id`)가 축 이름=값으로 서 있어 축이 죽으면 열쇠도 흔들린다.", "columns": [ { - "key": "item_key", - "name_ko": "항목 키", - "unit": "", - "visible": false, - "editable": true, - "from": "원본 키" - }, - { - "key": "item_name", - "name_ko": "항목", + "key": "variable", + "name_ko": "요율 항목", "unit": "", "visible": true, - "editable": true, - "from": "이름표" + "note": "`rate_sanjae`·`rate_goyong` … 19 벌." }, { - "key": "variant", - "name_ko": "벌", + "key": "part", + "name_ko": "원문 목록", "unit": "", "visible": true, - "editable": true, - "from": "새 칸", - "note": "한 항목이 표를 둘 가진 경우 — 일반관리비(토목/전문) · 기계 지급보증(종합/전문) · 환경보전비(임도 후보/전 공종)." + "note": "한 항목이 표를 여럿 가질 때 어느 목록인지 — `brackets`·`annual_rates`·`all_work_types`·`civil_landscape_industrial` 따위. 한 값짜리 줄은 빈다." }, { - "key": "work_type", - "name_ko": "공사 종류", + "key": "effective_date", + "name_ko": "판 기준일", "unit": "", - "visible": true, - "editable": true, - "from": "6 벌" - }, - { - "key": "amount_bracket", - "name_ko": "금액 구간", - "unit": "", - "visible": true, - "editable": true, - "from": "9 벌", - "note": "⚠ **축이 넷인데 한 칸으로 접힌다** — 추정금액(고용) · 추정가격(하도급·일반관리비·이윤) · 직접공사비(간접노무·기타경비·이행보증) · 대상액(안전관리비). 어느 금액인지는 곁 칸이 말한다." - }, - { - "key": "bracket_basis", - "name_ko": "구간이 무슨 금액인가", - "unit": "", - "visible": true, - "editable": true, - "from": "새 칸", - "note": "위 칸을 접으면 사라지는 것 — 반드시 함께 둔다." - }, - { - "key": "duration_bracket", - "name_ko": "공사기간 구간", - "unit": "", - "visible": true, - "editable": true, - "from": "2 벌" - }, - { - "key": "grade", - "name_ko": "등급", - "unit": "", - "visible": true, - "editable": true, - "from": "고용보험만" - }, - { - "key": "year", - "name_ko": "연도", - "unit": "", - "visible": true, - "editable": true, - "from": "국민연금만" + "visible": true }, { "key": "rate_percent", "name_ko": "요율", "unit": "%", "visible": true, - "editable": true, - "from": "18 벌", - "note": "이행보증은 요율이 아니라 산식이라 이 칸이 빈다." - }, - { - "key": "base_amount_krw", - "name_ko": "기초액", - "unit": "원", - "visible": true, - "editable": true, - "from": "안전관리비만" - }, - { - "key": "formula", - "name_ko": "산식", - "unit": "", - "visible": true, - "editable": true, - "from": "이행보증만" + "note": "이행보증만 산식이라 이 칸이 빈다." }, { "key": "base", "name_ko": "밑수", "unit": "", "visible": true, - "editable": true, - "from": "19 벌 다", "note": "이 요율을 어디에 곱하는가." }, { - "key": "minimum_krw", - "name_ko": "적용 하한", - "unit": "원", - "visible": true, - "editable": true, - "from": "3 벌", - "note": "이 금액에 못 미치면 줄이 안 선다 — 안전관리비 2천만 · 환경보전비 1억 · 퇴직공제 1억." - }, - { - "key": "note", - "name_ko": "비고", + "key": "grade", + "name_ko": "등급", "unit": "", "visible": true, - "editable": true, - "from": "원본 곁글" + "from": "고용보험", + "note": "축." + }, + { + "key": "estimated_amount_bracket", + "name_ko": "추정금액 구간", + "unit": "", + "visible": true, + "from": "고용보험", + "note": "축." + }, + { + "key": "year", + "name_ko": "연도", + "unit": "", + "visible": true, + "from": "국민연금", + "note": "축." + }, + { + "key": "target_amount_bracket", + "name_ko": "대상액 구간", + "unit": "", + "visible": true, + "from": "안전관리비", + "note": "축." + }, + { + "key": "work_type", + "name_ko": "공사 종류", + "unit": "", + "visible": true, + "from": "6 벌", + "note": "축." + }, + { + "key": "direct_cost_bracket", + "name_ko": "직접공사비 구간", + "unit": "", + "visible": true, + "from": "간접노무비·기타경비·이행보증", + "note": "축." + }, + { + "key": "duration_bracket", + "name_ko": "공사기간 구간", + "unit": "", + "visible": true, + "from": "간접노무비·기타경비", + "note": "축." + }, + { + "key": "estimated_price_bracket", + "name_ko": "추정가격 구간", + "unit": "", + "visible": true, + "from": "하도급보증·일반관리비·이윤", + "note": "축." + }, + { + "key": "amount_bracket", + "name_ko": "금액 구간", + "unit": "", + "visible": true, + "from": "안전관리비 기초액", + "note": "기초액이 걸리는 구간 — 위 축들과 달리 한 줄에만 있다." + }, + { + "key": "base_amount_krw", + "name_ko": "기초액", + "unit": "원", + "visible": true, + "from": "안전관리비" + }, + { + "key": "rate_from_2033_percent", + "name_ko": "2033년 이후 요율", + "unit": "%", + "visible": true, + "from": "국민연금", + "note": "표가 끝나는 해부터 쓰는 값." + }, + { + "key": "base_without_owner_supplied_material", + "name_ko": "대상액 — 관급자재 없을 때", + "unit": "", + "visible": true, + "from": "안전관리비", + "note": "재료비 + 직접노무비." + }, + { + "key": "base_with_owner_supplied_material", + "name_ko": "대상액 — 관급자재 있을 때", + "unit": "", + "visible": true, + "from": "안전관리비", + "note": "고시가 준 두 식 가운데 작은 쪽." + }, + { + "key": "base_rule", + "name_ko": "밑수 규칙", + "unit": "", + "visible": true, + "from": "안전관리비" + }, + { + "key": "minimum_total_construction_amount_krw", + "name_ko": "적용 하한 — 총공사금액", + "unit": "원", + "visible": true, + "from": "안전관리비", + "note": "이 금액에 못 미치면 줄이 안 선다(2천만 원)." + }, + { + "key": "minimum_estimated_amount_krw", + "name_ko": "적용 하한 — 추정금액", + "unit": "원", + "visible": true, + "from": "환경보전비·퇴직공제", + "note": "1억 원." + }, + { + "key": "manager_thresholds.default_estimated_amount_krw", + "name_ko": "안전관리자 선임 기준액 — 일반", + "unit": "원", + "visible": true, + "from": "안전관리비" + }, + { + "key": "manager_thresholds.civil_main_work_estimated_amount_krw", + "name_ko": "안전관리자 선임 기준액 — 토목 주공사", + "unit": "원", + "visible": true, + "from": "안전관리비" + }, + { + "key": "embedded_in", + "name_ko": "실린 자리", + "unit": "", + "visible": true, + "from": "안전관리비 기초액", + "note": "기초액이 따로 안 서고 요율표 안에 들어 있다는 표시." + }, + { + "key": "forest_road_selection_status", + "name_ko": "임도 줄 고름 상태", + "unit": "", + "visible": true, + "from": "환경보전비", + "note": "임도에 어느 줄을 쓸지 고름이 끝났는지 — 금액에 바로 닿는다." + }, + { + "key": "typical_forest_road_applicability", + "name_ko": "임도에 걸리는지", + "unit": "", + "visible": true, + "from": "이행보증", + "note": "보통 임도 규모에서 이 항목이 서는지." + }, + { + "key": "base_note", + "name_ko": "밑수 비고", + "unit": "", + "visible": true, + "from": "기계 지급보증" + }, + { + "key": "formula", + "name_ko": "산식", + "unit": "", + "visible": true, + "from": "이행보증", + "note": "요율이 아니라 식으로 주는 유일한 항목." } ] }, { "key": "machine", "name_ko": "기계경비", - "summary": "기종 한 줄 + 시간당 단가. ⚠ **한 표로 안 모인다** — 시간당 단가가 다섯 표를 물어 나오는 계산값이다.", + "summary": "기종 한 줄 + 시간당 단가. ⚠ **한 표로 안 모인다** — 시간당 단가가 여러 표를 물어 나오는 계산값이다.", "sources": [ "mach_base::variables/mach_price/records", "mach_base::variables/mach_loss_coef/records", "mach_base::variables/mach_fuel_rate/parsed_records", "mach_base::variables/mach_operator_map/explicit_mappings", - "mach_base::variables/mach_rock_adj/rules", - "machine_operating::records" + "machine_operating::records", + "labor_const::variables/labor_rate/records (조종원 노임)", + "oil::variables (연료 단가)" ], "rows_note": "취득가 613 줄이 뼈대 · 손료계수 387 · 연료 214 · 조종원 121 이 붙는다(안 붙는 줄은 빈 칸)", "columns": [ @@ -493,7 +589,14 @@ "name_ko": "기계코드", "unit": "", "visible": true, - "editable": true, + "from": "취득가", + "note": "⭐ 줄 열쇠 — 네 표 다 겹침 0." + }, + { + "key": "effective_date", + "name_ko": "판 기준일", + "unit": "", + "visible": true, "from": "취득가" }, { @@ -501,7 +604,6 @@ "name_ko": "기계명", "unit": "", "visible": true, - "editable": true, "from": "취득가" }, { @@ -509,33 +611,43 @@ "name_ko": "규격", "unit": "", "visible": true, - "editable": true, - "from": "취득가" + "from": "취득가", + "note": "⚠ 230 줄은 칸이 없다(규격 없는 기종)." }, { "key": "price_thousand_krw", "name_ko": "취득가", "unit": "천원", "visible": true, - "editable": true, "from": "취득가", - "note": "⭐ 밑값." + "note": "⚠ 여기만 **천원** 단위다 — 나머지 금액은 원." }, { "key": "loss_coefficient_per_hour", "name_ko": "시간당 손료계수", "unit": "", "visible": true, - "editable": true, - "from": "손료계수", - "note": "⭐ 밑값." + "from": "손료계수" + }, + { + "key": "economic_life_hours", + "name_ko": "내용시간", + "unit": "시간", + "visible": true, + "from": "손료계수" + }, + { + "key": "annual_standard_hours", + "name_ko": "연간표준가동시간", + "unit": "시간", + "visible": true, + "from": "손료계수" }, { "key": "fuel_type", "name_ko": "연료 종류", "unit": "", "visible": true, - "editable": true, "from": "연료" }, { @@ -543,52 +655,73 @@ "name_ko": "시간당 연료량", "unit": "L", "visible": true, - "editable": true, - "from": "연료", - "note": "⭐ 밑값." + "from": "연료" }, { "key": "misc_material_percent", "name_ko": "잡재료비", "unit": "연료비의 %", "visible": true, - "editable": true, - "from": "연료", - "note": "⭐ 밑값." + "from": "연료" }, { "key": "operator_occupation_code", - "name_ko": "조종원 직종", + "name_ko": "조종원 직종코드", "unit": "", "visible": true, - "editable": true, "from": "조종원", - "note": "⭐ 밑값 — 노임 표를 가리킨다." + "note": "노임 표를 가리킨다." }, { "key": "operator_person_per_day", "name_ko": "조종원", "unit": "인/일", "visible": true, - "editable": true, - "from": "조종원", - "note": "⭐ 밑값." + "from": "연료" }, { - "key": "rock_adjust_pct", - "name_ko": "암 작업 할증", - "unit": "%", + "key": "operating_effective_date", + "name_ko": "운전경비 판 기준일", + "unit": "", "visible": true, - "editable": true, - "from": "암석보정", - "note": "⭐ 밑값 — 기계 묶음별." + "from": "운전경비 파생본", + "note": "연료·조종원이 온 벌의 판. 취득가 판과 다를 수 있어 따로 보인다." + }, + { + "key": "fuel_price_krw_per_l", + "name_ko": "연료 단가", + "unit": "원/L", + "visible": true, + "from": "유가 표", + "note": "⭐ **다른 표에서 온 값** — 유가를 고치면 모든 기종이 움직인다." + }, + { + "key": "fuel_price_date", + "name_ko": "연료 단가 기준일", + "unit": "", + "visible": true, + "from": "유가 표" + }, + { + "key": "operator_daily_wage_krw", + "name_ko": "조종원 일 노임", + "unit": "원", + "visible": true, + "from": "노임 표", + "note": "⭐ **다른 표에서 온 값** — 노임을 고치면 모든 기종이 움직인다." + }, + { + "key": "operator_wage_date", + "name_ko": "조종원 노임 기준일", + "unit": "", + "visible": true, + "from": "노임 표" }, { "key": "hourly_loss_krw", "name_ko": "시간당 손료", "unit": "원", "visible": true, - "editable": false, "from": "계산", "note": "취득가 × 1,000 × 손료계수." }, @@ -597,27 +730,32 @@ "name_ko": "시간당 연료·잡재료", "unit": "원", "visible": true, - "editable": false, "from": "계산", - "note": "연료량 × 유가 × (1 + 잡재료비율). 유가는 **유가 표**에서 온다." + "note": "연료량 × 연료 단가 × (1 + 잡재료비율)." }, { "key": "hourly_operator_krw", "name_ko": "시간당 조종원", "unit": "원", "visible": true, - "editable": false, "from": "계산", - "note": "일 노임 ÷ 8 × 제수당 계수. 일 노임은 **노임 표**에서 온다." + "note": "일 노임 ÷ 8 × 제수당 계수 × 조종원 인수." }, { "key": "hourly_total_krw", "name_ko": "시간당 기계경비", "unit": "원", "visible": true, - "editable": false, "from": "계산", "note": "손료 + 운전경비. 수송비는 여기 안 든다." + }, + { + "key": "hourly_note", + "name_ko": "시간당 단가 비고", + "unit": "", + "visible": true, + "from": "계산", + "note": "못 채운 성분이 있으면 그 사실을 적는 자리 — 0 으로 안 때운다." } ] } @@ -629,48 +767,53 @@ "step": "① 손료", "formula": "취득가(천원) × 1,000 × 시간당 손료계수", "inputs": [ - "취득가", - "시간당 손료계수" + "price_thousand_krw", + "loss_coefficient_per_hour" ], - "note": "손료계수는 상각·정비·관리 계수의 합이다. 그 셋과 내용시간·연간표준가동시간도 밑값이라 함께 보인다." + "output": "hourly_loss_krw", + "note": "손료계수는 상각·정비·관리 계수의 합이다. 내용시간·연간표준가동시간도 함께 보인다." }, { "step": "② 운전경비 — 연료", - "formula": "시간당 연료량(L) × 유가(원/L) × (1 + 잡재료비율)", + "formula": "시간당 연료량(L) × 연료 단가(원/L) × (1 + 잡재료비율)", "inputs": [ - "시간당 연료량", - "잡재료비", - "유가 표" + "fuel_rate_l_per_hour", + "fuel_price_krw_per_l", + "misc_material_percent" ], - "note": "유가는 이 표에 없다 — **유가 표**에서 온다. 그래서 유가를 고치면 모든 기종이 움직인다." + "output": "hourly_fuel_krw", + "note": "연료 단가는 이 표의 값이 아니라 **유가 표**에서 온다 — 유가를 고치면 모든 기종이 움직인다." }, { "step": "③ 운전경비 — 조종원", "formula": "일 노임 ÷ 8시간 × 제수당 계수 × 조종원(인/일)", "inputs": [ - "조종원 직종", - "조종원 인/일", - "노임 표" + "operator_daily_wage_krw", + "operator_person_per_day", + "operator_occupation_code" ], - "note": "일 노임은 이 표에 없다 — **노임 표**에서 온다. 8시간은 품셈 8-1-6 단서다." + "output": "hourly_operator_krw", + "note": "일 노임은 **노임 표**에서 온다. 8시간은 품셈 8-1-6 단서다." }, { "step": "④ 암 작업 할증", "formula": "위 값 × (1 + 암 할증률)", "inputs": [ - "암 작업 할증" + "mach_rock_adj (기계 묶음별)" ], - "note": "기계 묶음별로 붙는다(불도저·굴착기·덤프)." + "output": "", + "note": "불도저·굴착기·덤프에 붙는다. 기종 표에는 안 서고 공종 쪽에서 걸린다." }, { "step": "⑤ 시간당 기계경비", - "formula": "① + ② + ③ (④ 를 걸친 뒤)", + "formula": "① + ② + ③", "inputs": [], - "note": "⚠ **수송비는 안 든다** — 「회당」으로 서는 별개 공종(산림품셈 10-4)이다." + "output": "hourly_total_krw", + "note": "⚠ **수송비는 안 든다** — 「회당」으로 서는 별개 공종(산림품셈 10-4)이다. 못 채운 성분은 0 으로 안 때우고 `hourly_note` 에 적는다." } ], - "editable_rule": "밑값 = 취득가 · 손료계수(와 그 속 계수들) · 연료량 · 잡재료비율 · 조종원 직종과 인수 · 암 할증률. 계산값 = 시간당 손료 · 시간당 연료 · 시간당 조종원 · 시간당 기계경비. ⚠ 계산값을 손으로 고치면 밑값과 어긋나 되짚을 수 없게 된다 — 화면에서 막는다.", - "cross_table": "기계 표는 **유가 표와 노임 표를 문다**. 그 둘이 갱신되면 기계 시간당 단가가 다 바뀐다 — 갱신 차례는 노임·유가가 먼저, 기계가 나중이다." + "editable_rule": "밑값 = 취득가 · 손료계수(와 그 속 계수들) · 연료량 · 잡재료비율 · 조종원 직종과 인수. 계산값 = 시간당 손료 · 시간당 연료 · 시간당 조종원 · 시간당 기계경비. ⚠ 계산값을 손으로 고치면 밑값과 어긋나 되짚을 수 없게 된다. ⚠ **어느 열이 고쳐지는지는 서버 `spec()` 한 곳이 정한다** — 이름표는 까닭만 적는다(판정 두 벌 금지).", + "cross_table": "기계 표는 **유가 표와 노임 표를 문다**(`fuel_price_krw_per_l`·`operator_daily_wage_krw`). 그 둘이 갱신되면 기계 시간당 단가가 다 바뀐다 — 갱신 차례는 노임·유가가 먼저, 기계가 나중이다." }, "merge_findings": { "renamed_same_column": [ @@ -689,8 +832,17 @@ "oil.value", "oil_regional.value" ], - "verdict": "합침", - "why": "둘 다 원/L 단가다. 이름만 `value` 로 같고 뜻도 같다 — 「단가」 한 칸으로 묶는다." + "verdict": "합침 — `price_krw_per_l`", + "why": "둘 다 원/L 단가다. 이름만 `value` 로 같고 뜻도 같다." + }, + { + "where": "유가", + "columns": [ + "effective_date", + "date" + ], + "verdict": "합치지 않음", + "why": "`effective_date` 는 **우리 판**, `date` 는 **원천이 값을 잰 날**이다. 닮아 보여 묶기 쉬운데 다른 것이다." } ], "one_side_only": [ @@ -715,8 +867,8 @@ "where": "유가", "only_in": "시도판", "columns": [ - "sido_code", - "sido_name" + "region_code", + "region_name" ], "note": "전국평균 판은 시도코드 `00`·시도명 「전국」으로 맞춘다." }, @@ -728,16 +880,26 @@ "year", "base_amount_krw", "formula", - "duration_bracket" + "duration_bracket", + "embedded_in" ], - "note": "19 벌이 축을 제각각 쓴다 — 안 쓰는 벌에서는 빈 칸이다." + "note": "19 벌이 축을 제각각 쓴다 — 안 쓰는 벌에서는 빈 칸이다. **합치지 않고 그대로 둔다.**" + }, + { + "where": "기계", + "only_in": "다른 표에서 옴", + "columns": [ + "fuel_price_krw_per_l", + "operator_daily_wage_krw" + ], + "note": "유가 표·노임 표에서 온다. 이 표에서 고치는 값이 아니다." } ], "unit_or_axis_mismatch": [ { "where": "제비율", - "what": "금액 구간 축이 넷", - "detail": "추정금액(고용보험) · 추정가격(하도급보증·일반관리비·이윤) · 직접공사비(간접노무비·기타경비·이행보증) · 대상액(안전관리비). 한 칸으로 접으면 「어느 금액인가」가 사라지므로 `bracket_basis` 칸을 함께 둔다." + "what": "구간 축이 다섯인데 **합치지 않는다**", + "detail": "추정금액(고용보험) · 추정가격(하도급보증·일반관리비·이윤) · 직접공사비(간접노무비·기타경비·이행보증) · 대상액(안전관리비) · 금액 구간(안전관리비 기초액). 서버가 원문 칸을 그대로 펴므로 이름표도 축마다 한 칸으로 둔다 — 합치면 어느 축인지 사라지고 축이 둘 이상인 줄을 못 읽는다(2026-09-15 브레인 ④ 판정)." }, { "where": "제비율", @@ -746,9 +908,14 @@ }, { "where": "기계", - "what": "취득가는 천원, 나머지 금액은 원", + "what": "취득가만 천원, 나머지 금액은 원", "detail": "원본이 천원 단위다. 한 표에 섞이면 1,000배 틀린다 — 열 이름에 단위를 못박는다." }, + { + "where": "기계", + "what": "판 기준일이 둘", + "detail": "`effective_date`(취득가 판)와 `operating_effective_date`(운전경비 파생본 판)가 다를 수 있다. 연료 단가·조종원 노임은 또 제 기준일을 달고 온다." + }, { "where": "노임", "what": "단위는 어긋나지 않음", @@ -759,12 +926,12 @@ { "where": "노임", "what": "직종코드 공간이 다르다", - "detail": "건설업은 네 자리(1001~), 제조업은 한두 자리(1·2·3…). **지금은 겹치는 코드가 0** 이지만 같은 칸에 넣으면 나중에 부딪힌다 — `source` 칸을 반드시 함께 둔다." + "detail": "건설업은 네 자리(1001~), 제조업은 한두 자리(1·2·3…). **지금은 겹치는 코드가 0** 이지만 같은 칸에 넣으면 나중에 부딪힌다 — 줄 열쇠가 `{dataset_id}/{occupation_code}` 로 서 있다." }, { "where": "유가", "what": "「전국」이 두 줄 선다", - "detail": "전국평균 판(휘발유 1,863.25 · 경유 1,846.39 · 2026-08-14)과 시도판 코드 `00`(1,858.93 · 1,843.79 · 2026-09-09)이 **값도 기준일도 다르다**. ⚠ 어느 쪽을 쓸지는 판정이 필요하다 — 이름표는 두 줄을 그대로 보이고 `source` 로 가른다." + "detail": "전국평균 판(휘발유 1,863.25 · 2026-08-14)과 시도판 `00`(1,858.93 · 2026-09-09)이 **값도 기준일도 다르다**. ⚠ 어느 쪽을 쓸지는 판정이 필요하다 — 이름표는 두 줄을 그대로 보이고 `source` 로 가른다." } ] }, @@ -777,7 +944,7 @@ "source", "occupation_code" ], - "checked": "건설업 132 줄·제조업 129 줄 각각 직종코드 겹침 0(전수). 다만 코드 공간이 달라(건설 네 자리 1001~ · 제조 한두 자리 1·2·3) **출처 칸과 묶어야** 한 표에서 안 부딪힌다." + "checked": "건설업 132 줄·제조업 129 줄 각각 직종코드 겹침 0(전수). 다만 코드 공간이 달라(건설 네 자리 1001~ · 제조 한두 자리 1·2·3) **출처와 묶어야** 한 표에서 안 부딪힌다. 서버가 `{dataset_id}/{occupation_code}` 로 세웠다." }, { "table": "material", @@ -786,7 +953,7 @@ "key": [ "item_code" ], - "checked": "6,999 줄 물품코드 겹침 0(전수). 그대로 열쇠로 쓸 수 있다." + "checked": "6,999 줄 물품코드 겹침 0(전수). 그대로 열쇠로 쓴다." }, { "table": "oil", @@ -797,18 +964,18 @@ "source", "region_code" ], - "checked": "원본이 유종마다 따로 선 표라 줄에는 시도코드뿐이다(유종별 17 줄 겹침 0). 유종은 표 이름에, 출처는 판에 있어 **줄 안에 없다** — 셋을 묶어야 줄이 하나로 가려진다." + "checked": "원본이 유종마다 따로 선 표라 줄에는 시도코드뿐이다(유종별 17 줄 겹침 0). 유종은 표 이름에, 출처는 판에 있어 **줄 안에 없다** — 셋을 묶어야 줄이 하나로 가려진다. 서버가 `{scope}/{변수}[/{sido_code}]` 로 세웠다." }, { - "table": "rates", + "table": "rate", "name_ko": "제비율", - "verdict": "없음 — 조건으로만 가려짐", + "verdict": "없음 — 축으로만 가려짐", "key": [ - "item_key", - "variant", - "(조건 열들)" + "variable", + "part", + "(축 열들)" ], - "checked": "19 벌에 id 칸이 아예 없다. 벌마다 조건 묶음으로 줄이 가려지는 것은 확인했다(14 개 구간표 전수 겹침 0). 다만 **조건 축이 벌마다 다르다** — 등급·연도·공사 종류·공사기간·금액 구간 넷. 한 끈으로 쓰려면 항목키+벌+조건을 이어야 한다." + "checked": "19 벌에 id 칸이 아예 없다. 벌마다 축 묶음으로 줄이 가려지는 것은 확인했다(14 개 구간표 전수 겹침 0). 서버가 `{변수}[/{목록}/{축=값;…}]` 로 세웠다 — ⚠ **축이 열쇠의 몸이라 축을 한 칸으로 합치면 열쇠가 흔들린다.**" }, { "table": "machine", @@ -912,7 +1079,7 @@ }, "counts": { "merged_tables": 5, - "merged_columns": 62, + "merged_columns": 88, "files": 34, "tables": 91, "columns": 455, diff --git a/resources/tester/test_master_labels_base_prices.py b/resources/tester/test_master_labels_base_prices.py new file mode 100644 index 00000000..0e455f08 --- /dev/null +++ b/resources/tester/test_master_labels_base_prices.py @@ -0,0 +1,153 @@ +"""기초단가 다섯 표 — **열 이름이 영문으로 떨어지면 빨강**. + +이 시험이 있는 까닭(2026-09-15 크로스체크) — 세 창이 각자 초록이었는데도 +**26칸이 화면에 영문 key 로 뜨고 있었다.** 서버는 이름표에서 못 찾으면 영문 key 로 +조용히 떨어지게 짜여 있고, 이름표 쪽 시험은 서버가 실제로 보는 자리를 안 봤다. +**어긋나도 아무도 모르는 자리**였다. + +⚠ 그래서 이 시험은 이름표 파일만 읽지 않고 **서버가 내는 응답 그대로** 잰다. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from Z01_MasterData import Z01_MasterData_BasePrices as base_prices + +ROOT = Path(__file__).resolve().parents[2] +LABELS_PATH = ROOT / "resources" / "data_master_labels" / "labels_2026-01-01.json" + + +@pytest.fixture(scope="module") +def labels() -> dict: + return json.loads(LABELS_PATH.read_text(encoding="utf-8")) + + +@pytest.fixture(scope="module") +def shown() -> dict[str, list[dict]]: + """서버가 실제로 내보내는 열 — 이름표 파일이 아니라 **응답**을 잰다.""" + out = {} + for kind in base_prices.KINDS: + columns = base_prices._columns(kind, base_prices.rows(kind)) + out[kind] = [base_prices.column_meta(kind, c) for c in columns] + return out + + +def test_다섯_표가_다_선다(shown): + assert set(shown) == {"labor", "machine", "material", "oil", "rate"} + for kind, columns in shown.items(): + assert columns, f"{kind} 에 열이 하나도 없다" + + +def test_영문으로_떨어지는_열이_하나도_없다(shown): + """⭐ 이 시험이 이번 판의 값진 자리 — 이름표가 서버 틀과 어긋나면 여기서 빨강.""" + english = [ + f"{kind}/{column['key']}" + for kind, columns in shown.items() + for column in columns + if column["label"] == column["key"] + ] + assert not english, ( + f"한글 이름이 없어 영문 key 로 뜨는 열 {len(english)}개: {english}\n" + "→ 이름표 `merged_tables[].columns` 에 그 key 를 더할 것." + ) + + +def test_열_이름이_비지_않았다(shown): + blank = [ + f"{kind}/{column['key']}" + for kind, columns in shown.items() + for column in columns + if not str(column["label"]).strip() + ] + assert not blank, blank + + +def test_이름표에_묵은_열이_남아_있지_않다(labels, shown): + """서버가 안 내는 열을 이름표가 들고 있으면 옛 틀이 굳은 것이다.""" + stale = [] + for entry in labels["merged_tables"]: + kind = entry["key"] + live = {c["key"] for c in shown.get(kind, ())} + for column in entry["columns"]: + if column["key"] not in live: + stale.append(f"{kind}/{column['key']}") + assert not stale, f"서버가 안 내는 묵은 열 이름표: {stale}" + + +def test_요율_축이_한_칸으로_안_합쳐졌다(shown): + """⭐ 2026-09-15 브레인 ④ — 합치면 어느 축의 구간인지 사라지고 `@id` 도 흔들린다. + + 축은 서버가 원문 칸 그대로 펴고 이름표가 거기에 맞춘다. + """ + keys = {column["key"] for column in shown["rate"]} + for axis in ( + "grade", + "year", + "work_type", + "target_amount_bracket", + "estimated_amount_bracket", + "estimated_price_bracket", + "direct_cost_bracket", + "duration_bracket", + ): + assert axis in keys, f"요율 축 {axis} 가 사라졌다" + + +def test_요율_줄_열쇠에_축이_살아_있다(): + """`@id` 가 `{변수}[/{목록}/{축=값;…}]` 꼴 — 축이 죽으면 고친 값이 주인을 잃는다.""" + rows = base_prices.rows("rate") + multi = [r for r in rows if "/" in str(r["@id"])] + assert multi, "축이 든 줄 열쇠가 하나도 없다" + assert any("=" in str(r["@id"]) for r in multi) + assert len({r["@id"] for r in rows}) == len(rows), "줄 열쇠가 겹친다" + + +def test_기계_시간당_단가는_계산값으로_잠긴다(shown): + """밑값만 고칠 수 있다 — 판정은 서버 `spec()` 한 곳(이름표가 따로 안 정한다).""" + meta = base_prices.table("machine", page=1, size=1) + assert set(meta["formula"]) == { + "hourly_loss_krw", + "hourly_fuel_krw", + "hourly_operator_krw", + "hourly_total_krw", + } + for key in meta["formula"]: + assert key not in meta["editable"], f"{key} 는 계산값인데 고칠 수 있다" + + +def test_이름표는_고칠수있나를_안_정한다(labels): + """판정 두 벌 금지 — `editable` 은 서버만 정한다.""" + carried = [ + f"{entry['key']}/{column['key']}" + for entry in labels["merged_tables"] + for column in entry["columns"] + if "editable" in column + ] + assert not carried, f"이름표가 고칠 수 있나를 들고 있다: {carried}" + + +def test_단위가_붙어야_할_열에_단위가_있다(shown): + """「유가 1,858」만 떠서는 리터인지 드럼인지 모른다 — 금액·수량 열은 단위를 단다.""" + want = { + "labor": {"daily_wage_krw": "원", "hours_per_day": "시간"}, + "material": {"price_krw": "원"}, + "oil": {"price_krw_per_l": "원/L"}, + "machine": { + "price_thousand_krw": "천원", + "fuel_rate_l_per_hour": "L", + "fuel_price_krw_per_l": "원/L", + "hourly_total_krw": "원", + }, + "rate": {"rate_percent": "%", "base_amount_krw": "원"}, + } + bad = [] + for kind, expected in want.items(): + units = {c["key"]: c["unit"] for c in shown[kind]} + for key, unit in expected.items(): + if units.get(key) != unit: + bad.append(f"{kind}/{key}: {units.get(key)!r} ≠ {unit!r}") + assert not bad, bad diff --git a/resources/tester/test_master_labels_cover.py b/resources/tester/test_master_labels_cover.py index 671704e0..8e5504f9 100644 --- a/resources/tester/test_master_labels_cover.py +++ b/resources/tester/test_master_labels_cover.py @@ -449,7 +449,7 @@ def test_미결은_미판정으로_표시된다(labels): assert column["name_ko"] == "미판정" -MERGED_KEYS = ("labor", "material", "oil", "rates", "machine") +MERGED_KEYS = ("labor", "material", "oil", "rate", "machine") def test_기초단가_다섯이_다_있다(labels): @@ -462,31 +462,27 @@ def test_기초단가_다섯이_다_있다(labels): assert entry["sources"], entry["key"] -def test_합친_표의_열마다_이름과_고칠수있나가_있다(labels): - bad = [] - for entry in labels["merged_tables"]: - for column in entry["columns"]: - if not column["name_ko"].strip(): - bad.append(f"{entry['key']}/{column['key']} — 이름 없음") - if not isinstance(column.get("editable"), bool): - bad.append(f"{entry['key']}/{column['key']} — editable 없음") +def test_합친_표의_열마다_한글_이름이_있다(labels): + """⚠ 「고칠 수 있나」는 여기서 안 정한다 — 서버 `spec()` 한 곳(판정 두 벌 금지). + 영문으로 떨어지는 열이 있는지는 `test_master_labels_base_prices.py` 가 응답으로 잰다.""" + bad = [ + f"{entry['key']}/{column['key']}" + for entry in labels["merged_tables"] + for column in entry["columns"] + if not column["name_ko"].strip() + ] assert not bad, bad -def test_계산값은_기계에만_있고_못_고친다(labels): - """⭐ 밑값만 고칠 수 있다 — 계산값을 손으로 고치면 밑값과 어긋나 되짚을 수 없다.""" - derived = { - entry["key"]: [c["key"] for c in entry["columns"] if not c["editable"]] - for entry in labels["merged_tables"] - } - assert derived["machine"] == [ - "hourly_loss_krw", - "hourly_fuel_krw", - "hourly_operator_krw", - "hourly_total_krw", - ] - for key in ("labor", "material", "oil", "rates"): - assert derived[key] == [], f"{key} 에 계산값이 생겼다" +def test_기계만_다른_표의_값을_문다(labels): + """⭐ 기계 시간당 단가는 유가·노임 표를 물어 나온다 — 갱신 차례가 여기서 갈린다.""" + merged = {m["key"]: m for m in labels["merged_tables"]} + machine = {c["key"]: c for c in merged["machine"]["columns"]} + for key in ("fuel_price_krw_per_l", "operator_daily_wage_krw"): + assert key in machine, f"{key} 이름표가 없다" + assert "표" in machine[key].get("from", ""), f"{key} 가 어디서 오는지 안 적혔다" + for key in ("labor", "material", "oil", "rate"): + assert len(merged[key]["sources"]) <= 3, key def test_기계_계산_사슬이_다섯_걸음으로_적혀_있다(labels): @@ -521,7 +517,7 @@ def test_줄_열쇠가_다섯_다_판정돼_있다(labels): # 한 칸으로는 안 되는 둘 — 묶어야 한다. assert len(keys["labor"]["key"]) > 1 assert keys["oil"]["verdict"].startswith("없음") - assert keys["rates"]["verdict"].startswith("없음") + assert keys["rate"]["verdict"].startswith("없음") def test_null_이_제값인_열이_적혀_있다(labels):