From d8f4ac024005e8e494e4d4fbef141650b1db28cb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 21:47:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(z01):=20=EA=B8=B0=EC=B4=88=EB=8B=A8?= =?UTF-8?q?=EA=B0=80=20=EB=8B=A4=EC=84=AF=EC=9D=84=20=ED=95=9C=20=ED=91=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=AA=A8=EC=9C=BC=EB=8A=94=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=ED=91=9C=20=C2=B7=20=EC=A4=84=20=EC=97=B4=EC=87=A0=C2=B7null?= =?UTF-8?q?=20=ED=8C=90=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `merged_tables` 다섯(노임·자재·유가·제비율·기계) — 열 62. **값은 안 옮김**, 열 이름·단위·숨김과 `editable`(고칠 수 있나)만 담음. 계산값 넷은 기계에만 있고 `editable: false` - `machine_cost_chain` 다섯 걸음 — 손료(취득가×1,000×손료계수) · 연료(연료량×유가×(1+잡재료)) · 조종원(일 노임÷8×제수당) · 암 할증 · 시간당 기계경비. **밑값 11 / 계산값 4** 를 가름 · 기계가 유가·노임 표를 물어 갱신 차례가 갈림을 적음 - `merge_findings` 네 갈래 — 이름 다른 같은 열 2 · 한쪽에만 있는 열 4 · 단위·축 어긋남 4 · 부딪힘 2 - `row_keys` 다섯 판정(브레인 ①) — 자재 `item_code`·기계 `machine_code` 는 전수 겹침 0 으로 **있음** · 노임은 두 코드 공간이라 `출처+직종코드` 로 묶어야 함 · 유가·제비율은 **줄 열쇠 없음**(지어내지 않음) - `null_is_real` 여섯(브레인 ②) — ⭐ 노임 일 노임이 **30 줄에 아예 없음**(미조사가 제값) · 기계 운전경비에 **명시 null 12** · 되돌리기 null 과 뜻이 겹치는 자리를 규칙과 함께 적음 - 시험 36건(8건 늘) — 계산값이 기계 밖으로 새거나 줄 열쇠·null 판정이 빠지면 빨강 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FFCnEYNH4tsS2MbHvzBhZk --- .../data_master_labels/labels_2026-01-01.json | 822 +++++++++++++++++- resources/tester/test_master_labels_cover.py | 91 ++ 2 files changed, 912 insertions(+), 1 deletion(-) diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index e59ae89f..ef78ff05 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-15T19:33:14+09:00", + "generated_at": "2026-09-15T21:46:51+09:00", "note": "마스터 자료의 **사람이 읽을 이름표**. 값이 아니라 이름만 담는다 — 여기를 고쳐도 계산은 안 바뀐다. Z01 마스터 관리 화면이 표·열 이름을 여기서 읽는다.", "policy": { "labels_only": "값·수식·단가를 담지 않는다. 마스터 파일은 손대지 않는다.", @@ -12,6 +12,7 @@ "brain_kind_table": "갈래는 2026-09-15 브레인 갈래 나눔표를 그대로 따른다(로직 17 · 기초값 9 · 부산물 4 · 씨앗 1).", "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 사용자 지시 · 브레인).", "row_key_column": "사전 모양(shape=map) 표는 **줄 이름(키) 자체가 한 열**이다. API 가 `@key` 로 내보내므로 열 목록 맨 앞에 `@key`(`is_row_key: true`)를 두고 표마다 제 이름을 붙인다 — 뜻이 표마다 달라 한 이름으로 뭉치지 않는다(2026-09-15 브레인 ①).", "value_kind_rule": "값 묶음마다 `kind` 를 낱말로 적는다. `value`(값) = 원가·수량에 그대로 드는 것(한 값짜리 요율·시세·계수·원단위표·잇는 값) — 낱값 마디(`@values`)에 **보인다**. `doc`(설명) = 읽으라고 적어 둔 글(방침·출처·비고·읽기 기록·집계·표 머리의 줄 키·담은 꼴) — 낱값 마디에 **안 보인다**. 참·거짓이 아니라 낱말이라 수식 같은 갈래가 늘면 낱말만 더하면 된다. 가름은 「고치면 금액이 움직이나」로 한다. 움직이면 값, 안 움직이면 설명. 곁값(값 단위·기준일·걸치는 범위)은 **값 쪽**이다 — 없으면 값의 뜻이 안 선다. `undecided: true` 는 값도 설명도 아닌 미결이라 판정을 기다린다(2026-09-15 브레인 ②)." }, @@ -49,6 +50,823 @@ "summary": "앞으로 늘려 갈 첫 벌. 모양을 잡아 두려고 먼저 놓은 것이다." } }, + "merged_tables": [ + { + "key": "labor", + "name_ko": "노임", + "summary": "직종마다 하루 노임 한 표. 건설업·제조업 두 공표를 한 자리에 모은다.", + "sources": [ + "labor_const::variables/labor_rate/records", + "labor_mfg::variables/labor_mfg/records" + ], + "rows_note": "건설업 132 + 제조업 129 = 261 줄", + "columns": [ + { + "key": "source", + "name_ko": "출처", + "unit": "", + "visible": true, + "editable": true, + "from": "새 칸", + "note": "건설업(대한건설협회) · 제조업(중소기업중앙회). ⚠ 이 칸이 없으면 두 코드 공간이 섞인다." + }, + { + "key": "occupation_code", + "name_ko": "직종코드", + "unit": "", + "visible": true, + "editable": true, + "from": "둘 다" + }, + { + "key": "occupation_name", + "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 — 어긋남 없음." + }, + { + "key": "hours_per_day", + "name_ko": "1일 근로시간", + "unit": "시간", + "visible": true, + "editable": true, + "from": "건설업만" + }, + { + "key": "daily_wage_krw", + "name_ko": "일 노임", + "unit": "원", + "visible": true, + "editable": true, + "from": "둘 다" + }, + { + "key": "status", + "name_ko": "공표 상태", + "unit": "", + "visible": true, + "editable": true, + "from": "둘 다" + }, + { + "key": "reliability", + "name_ko": "신뢰도 기호", + "unit": "", + "visible": true, + "editable": true, + "from": "건설업만", + "note": "* 조사현장 5개 미만 · ** 미조사. 뜻풀이가 원본에 있다." + }, + { + "key": "source_flag", + "name_ko": "원문 표시", + "unit": "", + "visible": true, + "editable": true, + "from": "제조업만", + "note": "`*` 16 줄. ⚠ 뜻풀이가 원본에 없어 신뢰도 기호와 한 칸으로 못 묶는다." + } + ] + }, + { + "key": "material", + "name_ko": "자재 단가", + "summary": "물품마다 규격·단위·단가 한 표. 원본이 하나라 합칠 것이 없다.", + "sources": [ + "mat_price_public::variables/mat_price/records" + ], + "rows_note": "6,999 줄 — 다섯 가운데 가장 크다", + "columns": [ + { + "key": "item_code", + "name_ko": "물품코드", + "unit": "", + "visible": true, + "editable": true + }, + { + "key": "classification_code", + "name_ko": "물품분류번호", + "unit": "", + "visible": true, + "editable": true + }, + { + "key": "classification_name", + "name_ko": "물품분류명", + "unit": "", + "visible": true, + "editable": true + }, + { + "key": "specification", + "name_ko": "규격", + "unit": "", + "visible": true, + "editable": true + }, + { + "key": "unit", + "name_ko": "단위", + "unit": "", + "visible": true, + "editable": 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 + }, + { + "key": "notice_datetime", + "name_ko": "공고 일시", + "unit": "", + "visible": true, + "editable": true + }, + { + "key": "notice_number", + "name_ko": "공고번호", + "unit": "", + "visible": false, + "editable": true + }, + { + "key": "business_division_code", + "name_ko": "사업구분 코드", + "unit": "", + "visible": false, + "editable": true + } + ] + }, + { + "key": "oil", + "name_ko": "유가", + "summary": "유종 × 지역 한 표. 전국평균 판과 시도별 판을 한 자리에 모은다.", + "sources": [ + "oil::variables", + "oil_regional::variables/oil_gasoline/records", + "oil_regional::variables/oil_diesel/records" + ], + "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": "전국평균 판 · 시도별 판. 기준일이 판마다 다르다." + }, + { + "key": "region_code", + "name_ko": "시도코드", + "unit": "", + "visible": true, + "editable": true, + "from": "시도판", + "note": "전국평균 판은 `00` 으로 맞춘다." + }, + { + "key": "region_name", + "name_ko": "시도명", + "unit": "", + "visible": true, + "editable": true, + "from": "시도판" + }, + { + "key": "price_krw_per_l", + "name_ko": "단가", + "unit": "원/L", + "visible": true, + "editable": true, + "from": "둘 다" + }, + { + "key": "date", + "name_ko": "기준일", + "unit": "", + "visible": true, + "editable": true, + "from": "둘 다", + "note": "⚠ 판마다 다르다 — 같은 표에 두 날짜가 선다." + }, + { + "key": "source_product_name", + "name_ko": "원천 제품명", + "unit": "", + "visible": false, + "editable": true, + "from": "둘 다" + }, + { + "key": "source_product_code", + "name_ko": "원천 제품코드", + "unit": "", + "visible": false, + "editable": true, + "from": "둘 다" + } + ] + }, + { + "key": "rates", + "name_ko": "제비율", + "summary": "법정경비·일반관리비·이윤·부가세 요율 한 표. 원본 19 벌을 한 줄씩 펴서 모은다.", + "sources": [ + "rates::variables/* (19 벌)" + ], + "rows_note": "한 값짜리 8 + 구간표 11 벌을 펴서 약 190 줄", + "columns": [ + { + "key": "item_key", + "name_ko": "항목 키", + "unit": "", + "visible": false, + "editable": true, + "from": "원본 키" + }, + { + "key": "item_name", + "name_ko": "항목", + "unit": "", + "visible": true, + "editable": true, + "from": "이름표" + }, + { + "key": "variant", + "name_ko": "벌", + "unit": "", + "visible": true, + "editable": true, + "from": "새 칸", + "note": "한 항목이 표를 둘 가진 경우 — 일반관리비(토목/전문) · 기계 지급보증(종합/전문) · 환경보전비(임도 후보/전 공종)." + }, + { + "key": "work_type", + "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": "국민연금만" + }, + { + "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": "이행보증만" + }, + { + "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": "비고", + "unit": "", + "visible": true, + "editable": true, + "from": "원본 곁글" + } + ] + }, + { + "key": "machine", + "name_ko": "기계경비", + "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" + ], + "rows_note": "취득가 613 줄이 뼈대 · 손료계수 387 · 연료 214 · 조종원 121 이 붙는다(안 붙는 줄은 빈 칸)", + "columns": [ + { + "key": "machine_code", + "name_ko": "기계코드", + "unit": "", + "visible": true, + "editable": true, + "from": "취득가" + }, + { + "key": "machine_name", + "name_ko": "기계명", + "unit": "", + "visible": true, + "editable": true, + "from": "취득가" + }, + { + "key": "specification", + "name_ko": "규격", + "unit": "", + "visible": true, + "editable": true, + "from": "취득가" + }, + { + "key": "price_thousand_krw", + "name_ko": "취득가", + "unit": "천원", + "visible": true, + "editable": true, + "from": "취득가", + "note": "⭐ 밑값." + }, + { + "key": "loss_coefficient_per_hour", + "name_ko": "시간당 손료계수", + "unit": "", + "visible": true, + "editable": true, + "from": "손료계수", + "note": "⭐ 밑값." + }, + { + "key": "fuel_type", + "name_ko": "연료 종류", + "unit": "", + "visible": true, + "editable": true, + "from": "연료" + }, + { + "key": "fuel_rate_l_per_hour", + "name_ko": "시간당 연료량", + "unit": "L", + "visible": true, + "editable": true, + "from": "연료", + "note": "⭐ 밑값." + }, + { + "key": "misc_material_percent", + "name_ko": "잡재료비", + "unit": "연료비의 %", + "visible": true, + "editable": true, + "from": "연료", + "note": "⭐ 밑값." + }, + { + "key": "operator_occupation_code", + "name_ko": "조종원 직종", + "unit": "", + "visible": true, + "editable": true, + "from": "조종원", + "note": "⭐ 밑값 — 노임 표를 가리킨다." + }, + { + "key": "operator_person_per_day", + "name_ko": "조종원", + "unit": "인/일", + "visible": true, + "editable": true, + "from": "조종원", + "note": "⭐ 밑값." + }, + { + "key": "rock_adjust_pct", + "name_ko": "암 작업 할증", + "unit": "%", + "visible": true, + "editable": true, + "from": "암석보정", + "note": "⭐ 밑값 — 기계 묶음별." + }, + { + "key": "hourly_loss_krw", + "name_ko": "시간당 손료", + "unit": "원", + "visible": true, + "editable": false, + "from": "계산", + "note": "취득가 × 1,000 × 손료계수." + }, + { + "key": "hourly_fuel_krw", + "name_ko": "시간당 연료·잡재료", + "unit": "원", + "visible": true, + "editable": false, + "from": "계산", + "note": "연료량 × 유가 × (1 + 잡재료비율). 유가는 **유가 표**에서 온다." + }, + { + "key": "hourly_operator_krw", + "name_ko": "시간당 조종원", + "unit": "원", + "visible": true, + "editable": false, + "from": "계산", + "note": "일 노임 ÷ 8 × 제수당 계수. 일 노임은 **노임 표**에서 온다." + }, + { + "key": "hourly_total_krw", + "name_ko": "시간당 기계경비", + "unit": "원", + "visible": true, + "editable": false, + "from": "계산", + "note": "손료 + 운전경비. 수송비는 여기 안 든다." + } + ] + } + ], + "machine_cost_chain": { + "note": "기계경비 = 기계손료 + 운전경비 + 수송비(품셈 8-1-6). ⭐ **밑값만 고칠 수 있다** — 계산값은 밑값이 바뀌면 따라 움직인다.", + "steps": [ + { + "step": "① 손료", + "formula": "취득가(천원) × 1,000 × 시간당 손료계수", + "inputs": [ + "취득가", + "시간당 손료계수" + ], + "note": "손료계수는 상각·정비·관리 계수의 합이다. 그 셋과 내용시간·연간표준가동시간도 밑값이라 함께 보인다." + }, + { + "step": "② 운전경비 — 연료", + "formula": "시간당 연료량(L) × 유가(원/L) × (1 + 잡재료비율)", + "inputs": [ + "시간당 연료량", + "잡재료비", + "유가 표" + ], + "note": "유가는 이 표에 없다 — **유가 표**에서 온다. 그래서 유가를 고치면 모든 기종이 움직인다." + }, + { + "step": "③ 운전경비 — 조종원", + "formula": "일 노임 ÷ 8시간 × 제수당 계수 × 조종원(인/일)", + "inputs": [ + "조종원 직종", + "조종원 인/일", + "노임 표" + ], + "note": "일 노임은 이 표에 없다 — **노임 표**에서 온다. 8시간은 품셈 8-1-6 단서다." + }, + { + "step": "④ 암 작업 할증", + "formula": "위 값 × (1 + 암 할증률)", + "inputs": [ + "암 작업 할증" + ], + "note": "기계 묶음별로 붙는다(불도저·굴착기·덤프)." + }, + { + "step": "⑤ 시간당 기계경비", + "formula": "① + ② + ③ (④ 를 걸친 뒤)", + "inputs": [], + "note": "⚠ **수송비는 안 든다** — 「회당」으로 서는 별개 공종(산림품셈 10-4)이다." + } + ], + "editable_rule": "밑값 = 취득가 · 손료계수(와 그 속 계수들) · 연료량 · 잡재료비율 · 조종원 직종과 인수 · 암 할증률. 계산값 = 시간당 손료 · 시간당 연료 · 시간당 조종원 · 시간당 기계경비. ⚠ 계산값을 손으로 고치면 밑값과 어긋나 되짚을 수 없게 된다 — 화면에서 막는다.", + "cross_table": "기계 표는 **유가 표와 노임 표를 문다**. 그 둘이 갱신되면 기계 시간당 단가가 다 바뀐다 — 갱신 차례는 노임·유가가 먼저, 기계가 나중이다." + }, + "merge_findings": { + "renamed_same_column": [ + { + "where": "노임", + "columns": [ + "labor_const.reliability", + "labor_mfg.source_flag" + ], + "verdict": "합치지 않음", + "why": "둘 다 원문 별표지만 뜻이 다르다 — 건설업은 「조사현장 5개 미만(*)·미조사(**)」로 뜻풀이가 있고, 제조업 `*`(16 줄)는 원본에 뜻풀이가 없다. 한 칸으로 묶으면 뜻이 섞인다." + }, + { + "where": "유가", + "columns": [ + "oil.value", + "oil_regional.value" + ], + "verdict": "합침", + "why": "둘 다 원/L 단가다. 이름만 `value` 로 같고 뜻도 같다 — 「단가」 한 칸으로 묶는다." + } + ], + "one_side_only": [ + { + "where": "노임", + "only_in": "건설업", + "columns": [ + "hours_per_day", + "reliability" + ] + }, + { + "where": "노임", + "only_in": "제조업", + "columns": [ + "industry", + "variation_coefficient", + "source_flag" + ] + }, + { + "where": "유가", + "only_in": "시도판", + "columns": [ + "sido_code", + "sido_name" + ], + "note": "전국평균 판은 시도코드 `00`·시도명 「전국」으로 맞춘다." + }, + { + "where": "제비율", + "only_in": "일부 벌", + "columns": [ + "grade", + "year", + "base_amount_krw", + "formula", + "duration_bracket" + ], + "note": "19 벌이 축을 제각각 쓴다 — 안 쓰는 벌에서는 빈 칸이다." + } + ], + "unit_or_axis_mismatch": [ + { + "where": "제비율", + "what": "금액 구간 축이 넷", + "detail": "추정금액(고용보험) · 추정가격(하도급보증·일반관리비·이윤) · 직접공사비(간접노무비·기타경비·이행보증) · 대상액(안전관리비). 한 칸으로 접으면 「어느 금액인가」가 사라지므로 `bracket_basis` 칸을 함께 둔다." + }, + { + "where": "제비율", + "what": "이행보증만 요율이 아니라 산식", + "detail": "`(직접공사비 × 0.0108%) × 공사연수` 꼴이라 `rate_percent` 가 빈다. `formula` 칸으로 받는다." + }, + { + "where": "기계", + "what": "취득가는 천원, 나머지 금액은 원", + "detail": "원본이 천원 단위다. 한 표에 섞이면 1,000배 틀린다 — 열 이름에 단위를 못박는다." + }, + { + "where": "노임", + "what": "단위는 어긋나지 않음", + "detail": "둘 다 `KRW/day` 다(건설 132 · 제조 129 전수 확인)." + } + ], + "collision": [ + { + "where": "노임", + "what": "직종코드 공간이 다르다", + "detail": "건설업은 네 자리(1001~), 제조업은 한두 자리(1·2·3…). **지금은 겹치는 코드가 0** 이지만 같은 칸에 넣으면 나중에 부딪힌다 — `source` 칸을 반드시 함께 둔다." + }, + { + "where": "유가", + "what": "「전국」이 두 줄 선다", + "detail": "전국평균 판(휘발유 1,863.25 · 경유 1,846.39 · 2026-08-14)과 시도판 코드 `00`(1,858.93 · 1,843.79 · 2026-09-09)이 **값도 기준일도 다르다**. ⚠ 어느 쪽을 쓸지는 판정이 필요하다 — 이름표는 두 줄을 그대로 보이고 `source` 로 가른다." + } + ] + }, + "row_keys": [ + { + "table": "labor", + "name_ko": "노임", + "verdict": "있음(묶어야 함)", + "key": [ + "source", + "occupation_code" + ], + "checked": "건설업 132 줄·제조업 129 줄 각각 직종코드 겹침 0(전수). 다만 코드 공간이 달라(건설 네 자리 1001~ · 제조 한두 자리 1·2·3) **출처 칸과 묶어야** 한 표에서 안 부딪힌다." + }, + { + "table": "material", + "name_ko": "자재 단가", + "verdict": "있음", + "key": [ + "item_code" + ], + "checked": "6,999 줄 물품코드 겹침 0(전수). 그대로 열쇠로 쓸 수 있다." + }, + { + "table": "oil", + "name_ko": "유가", + "verdict": "없음 — 묶어야 함", + "key": [ + "fuel", + "source", + "region_code" + ], + "checked": "원본이 유종마다 따로 선 표라 줄에는 시도코드뿐이다(유종별 17 줄 겹침 0). 유종은 표 이름에, 출처는 판에 있어 **줄 안에 없다** — 셋을 묶어야 줄이 하나로 가려진다." + }, + { + "table": "rates", + "name_ko": "제비율", + "verdict": "없음 — 조건으로만 가려짐", + "key": [ + "item_key", + "variant", + "(조건 열들)" + ], + "checked": "19 벌에 id 칸이 아예 없다. 벌마다 조건 묶음으로 줄이 가려지는 것은 확인했다(14 개 구간표 전수 겹침 0). 다만 **조건 축이 벌마다 다르다** — 등급·연도·공사 종류·공사기간·금액 구간 넷. 한 끈으로 쓰려면 항목키+벌+조건을 이어야 한다." + }, + { + "table": "machine", + "name_ko": "기계경비", + "verdict": "있음", + "key": [ + "machine_code" + ], + "checked": "취득가 613 · 손료계수 387 · 연료 214 · 운전경비 92 **네 표 모두 기계코드 겹침 0**(전수). 취득가가 가장 넓어 뼈대가 되고 나머지가 붙는다." + } + ], + "null_is_real": { + "columns": [ + { + "table": "labor", + "column": "daily_wage_krw", + "rows": 30, + "shape": "칸이 아예 없음", + "why": "⭐ **가장 위험한 자리** — 건설업 14 줄·제조업 16 줄에 일 노임 칸이 없다. 건설업 쪽은 `reliability` 가 `**`(미조사)인 줄이라 **값이 없는 것이 제값**이다. 되돌리기 null 과 구별이 안 된다." + }, + { + "table": "labor", + "column": "reliability · source_flag · variation_coefficient", + "rows": 221, + "shape": "칸이 아예 없음", + "why": "기호가 없는 줄 = 정상 공표라는 뜻이다. 빈 칸 자체가 값이다." + }, + { + "table": "machine", + "column": "fuel_liters_per_hour · misc_material_percent · operator_person_days", + "rows": 12, + "shape": "**명시 null**", + "why": "운전경비 파생본이 원문에서 못 읽은 칸을 `null` 로 적어 두었다 — 0 으로 안 때운 자리다. 다섯 가운데 **진짜 `null` 이 든 유일한 자료**다." + }, + { + "table": "machine", + "column": "economic_life_hours 외 손료계수 8 열", + "rows": 22, + "shape": "칸이 아예 없음", + "why": "손료계수는 있는데 그 속을 이루는 계수·내용시간이 원문에 없는 줄이다." + }, + { + "table": "machine", + "column": "specification", + "rows": 255, + "shape": "칸이 아예 없음", + "why": "규격이 없는 기종(취득가 230 · 연료 25). 규격 없음이 제값이라 빈 칸으로 둔다." + }, + { + "table": "material", + "column": "specification", + "rows": 2, + "shape": "빈 문자열", + "why": "null 이 아니라 `\"\"` 다. 되돌리기 null 과는 안 겹치나 **빈 칸 판정이 두 가지**가 된다." + } + ], + "rule": "⚠ 되돌리기를 `null` 로 보내면 위 자리에서 뜻이 겹친다. 겹침을 푸는 길은 둘 — ㉠ 되돌리기를 값이 아니라 **짓**으로 보낸다(덮개에서 그 열을 지움) · ㉡ 되돌리기 낱말을 따로 둔다(`\"@revert\"` 같은). 어느 쪽이든 **원본의 `null`·빈 칸은 그대로 살려야** 한다 — 「미조사」가 「값 지움」으로 바뀌면 안 된다. ⚠ 자재 `specification` 은 `null` 이 아니라 빈 문자열이라 빈 칸 판정이 두 가지다." + }, "value_kinds": { "value": { "name_ko": "값", @@ -93,6 +911,8 @@ } }, "counts": { + "merged_tables": 5, + "merged_columns": 62, "files": 34, "tables": 91, "columns": 455, diff --git a/resources/tester/test_master_labels_cover.py b/resources/tester/test_master_labels_cover.py index 26ed63b0..671704e0 100644 --- a/resources/tester/test_master_labels_cover.py +++ b/resources/tester/test_master_labels_cover.py @@ -447,3 +447,94 @@ def test_미결은_미판정으로_표시된다(labels): column = labels["synthetic_tables"]["@values"]["columns"][3] assert column["key"] == "@undecided" assert column["name_ko"] == "미판정" + + +MERGED_KEYS = ("labor", "material", "oil", "rates", "machine") + + +def test_기초단가_다섯이_다_있다(labels): + merged = {m["key"]: m for m in labels["merged_tables"]} + assert tuple(merged) == MERGED_KEYS + for entry in merged.values(): + assert entry["name_ko"].strip() + assert entry["summary"].strip() + assert entry["columns"], entry["key"] + 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 없음") + 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): + chain = labels["machine_cost_chain"] + assert len(chain["steps"]) == 5 + for step in chain["steps"]: + assert step["step"].strip() and step["formula"].strip() + assert "밑값" in chain["editable_rule"] and "계산값" in chain["editable_rule"] + # 기계는 유가·노임 표를 문다 — 갱신 차례가 여기서 갈린다. + assert "유가" in chain["cross_table"] and "노임" in chain["cross_table"] + + +def test_열_맞대기_어긋남이_네_갈래로_적혀_있다(labels): + findings = labels["merge_findings"] + assert set(findings) == { + "renamed_same_column", + "one_side_only", + "unit_or_axis_mismatch", + "collision", + } + assert all(findings[k] for k in findings) + + +def test_줄_열쇠가_다섯_다_판정돼_있다(labels): + """덮개가 원본 줄을 붙드는 끈 — 없는 자료는 「없음」으로 내고 지어내지 않는다.""" + keys = {r["table"]: r for r in labels["row_keys"]} + assert set(keys) == set(MERGED_KEYS) + for entry in keys.values(): + assert entry["verdict"].strip() and entry["key"] and entry["checked"].strip() + assert keys["material"]["key"] == ["item_code"] + assert keys["machine"]["key"] == ["machine_code"] + # 한 칸으로는 안 되는 둘 — 묶어야 한다. + assert len(keys["labor"]["key"]) > 1 + assert keys["oil"]["verdict"].startswith("없음") + assert keys["rates"]["verdict"].startswith("없음") + + +def test_null_이_제값인_열이_적혀_있다(labels): + """되돌리기 null 과 원본의 「값 없음」이 겹치는 자리 — 겹치면 미조사가 값 지움으로 바뀐다.""" + block = labels["null_is_real"] + assert block["columns"] + for row in block["columns"]: + assert row["table"] in MERGED_KEYS + assert row["column"].strip() and row["why"].strip() + assert isinstance(row["rows"], int) + # 가장 위험한 자리 — 노임 일 노임이 없는 줄(미조사) + wage = [ + r for r in block["columns"] if r["table"] == "labor" and "daily_wage_krw" in r["column"] + ] + assert wage and wage[0]["rows"] == 30 + assert "되돌리기" in block["rule"]