feat(z01): 기초단가 다섯을 한 표로 모으는 이름표 · 줄 열쇠·null 판정

- `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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFCnEYNH4tsS2MbHvzBhZk
This commit is contained in:
2026-09-15 21:47:43 +09:00
co-authored by Claude Opus 5
parent 5ae246146c
commit d8f4ac0240
2 changed files with 912 additions and 1 deletions
@@ -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,
@@ -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"]