From da2238505c6643c6142073c5af35242d85754b45 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 19:17:28 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(z01):=20=EC=9D=B4=EB=A6=84=ED=91=9C?= =?UTF-8?q?=EC=97=90=20=EC=A4=84=20=EC=9D=B4=EB=A6=84=20=EC=97=B4=20?= =?UTF-8?q?=EC=97=AC=EB=8D=9F=EA=B3=BC=20=E3=80=8C=EA=B0=92=C2=B7=EC=84=A4?= =?UTF-8?q?=EB=AA=85=E3=80=8D=20=EA=B0=80=EB=A6=84=20172=20=EC=B9=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사전 모양 표 8 에 **줄 이름 열 `@key`** 를 둠(브레인 ①) — 표마다 제 이름: fx 「통화」 · oil 「유종」 · masonry_slope 「쌓기 방식」 · material_surcharge 「구실」 · stone_kind·structure_unit 「출처 키」 · masonry_wet 「표 이름」·「칸 키」 · `is_row_key` 표시 붙임 - 값 묶음 172 를 **값 49 · 설명 123** 으로 가름(브레인 ②) — 잣대는 「고치면 금액이 움직이나」 · 이름표 `policy.value_role_rule` 에 적음 · 갈래 뜻은 `value_roles` 에 - 값으로 선 것 — 한 값짜리 요율 여덟(산재·건강·요양·부가세·퇴직공제·임금채권·석면·안전관리비 기초액) · 요율마다의 밑수 · 시세 · 계수·원단위표(돌쌓기·거푸집·철근) · 잇는 값(노임 별칭·타설·배수관) - 설명으로 숨는 것 — 방침·출처·비고·읽기 기록과 표 머리의 키·단위·기준일 - 시험 25건(7건 늘) — 줄 이름 열이 빠지거나 한 값짜리 요율이 설명으로 숨으면 빨강 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FFCnEYNH4tsS2MbHvzBhZk --- .../data_master_labels/labels_2026-01-01.json | 596 ++++++++++++------ resources/tester/test_master_labels_cover.py | 90 ++- 2 files changed, 509 insertions(+), 177 deletions(-) diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index 4e79b5b9..ae9fbe4b 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-15T18:59:13+09:00", + "generated_at": "2026-09-15T19:16:20+09:00", "note": "마스터 자료의 **사람이 읽을 이름표**. 값이 아니라 이름만 담는다 — 여기를 고쳐도 계산은 안 바뀐다. Z01 마스터 관리 화면이 표·열 이름을 여기서 읽는다.", "policy": { "labels_only": "값·수식·단가를 담지 않는다. 마스터 파일은 손대지 않는다.", @@ -11,7 +11,9 @@ "counting_rule": "표 = 화면이 표로 그릴 마디(줄이 여럿인 마디). 값 묶음 = 표가 아닌 마디(설명·방침·한 값짜리 요율 따위).", "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 파일)는 배수용이라 뺀다." + "excluded": "강우 IDF 캐시(resources/data_rainfall_idf_cache, 96 파일)는 배수용이라 뺀다.", + "row_key_column": "사전 모양(shape=map) 표는 **줄 이름(키) 자체가 한 열**이다. API 가 `@key` 로 내보내므로 열 목록 맨 앞에 `@key`(`is_row_key: true`)를 두고 표마다 제 이름을 붙인다 — 뜻이 표마다 달라 한 이름으로 뭉치지 않는다(2026-09-15 브레인 ①).", + "value_role_rule": "값 묶음은 갈래가 둘이다. **값** = 원가·수량에 그대로 드는 것(한 값짜리 요율·시세·계수·원단위표·잇는 값) — 낱값 마디(`@values`)에 **보인다**. **설명** = 읽으라고 적어 둔 글(방침·출처·비고·읽기 기록·표 머리의 키·단위·기준일) — 낱값 마디에 **안 보인다**. 가름은 「고치면 금액이 움직이나」로 한다. 움직이면 값, 안 움직이면 설명(2026-09-15 브레인 ②)." }, "lookup_order": { "column": [ @@ -47,12 +49,26 @@ "summary": "앞으로 늘려 갈 첫 벌. 모양을 잡아 두려고 먼저 놓은 것이다." } }, + "value_roles": { + "value": { + "name_ko": "값", + "summary": "원가·수량에 그대로 드는 값. 낱값 마디에 **보인다** — 여기서 사라지면 트리에서 값이 사라진다." + }, + "note": { + "name_ko": "설명", + "summary": "읽으라고 적어 둔 글(방침·출처·비고·기록). 낱값 마디에 **안 보인다** — 보이면 화면이 흐려진다." + } + }, "counts": { "files": 34, "tables": 91, - "columns": 447, + "columns": 455, "value_groups": 172, - "unknown": 0 + "unknown": 0, + "value_groups_by_role": { + "note": 123, + "value": 49 + } }, "files": [ { @@ -133,12 +149,14 @@ { "key": "variables/pum/normalization_status", "name_ko": "정돈 상태", - "summary": "자원 코드 정돈이 끝났는지." + "summary": "자원 코드 정돈이 끝났는지.", + "role": "note" }, { "key": "variables/pum/representation", "name_ko": "담은 꼴", - "summary": "원문 표를 어떤 꼴로 담았는지." + "summary": "원문 표를 어떤 꼴로 담았는지.", + "role": "note" } ] }, @@ -226,12 +244,14 @@ { "key": "variables/pum/normalization_status", "name_ko": "정돈 상태", - "summary": "자원 코드 정돈이 끝났는지." + "summary": "자원 코드 정돈이 끝났는지.", + "role": "note" }, { "key": "variables/pum/representation", "name_ko": "담은 꼴", - "summary": "원문 표를 어떤 꼴로 담았는지." + "summary": "원문 표를 어떤 꼴로 담았는지.", + "role": "note" } ] }, @@ -343,12 +363,14 @@ { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "stats", "name_ko": "집계", - "summary": "몇 줄이 섰고 몇 줄이 빠졌는지 센 것." + "summary": "몇 줄이 섰고 몇 줄이 빠졌는지 센 것.", + "role": "note" } ] }, @@ -553,57 +575,68 @@ { "key": "variables/coef_soil_C/duplicate_note", "name_ko": "원문 중복 비고", - "summary": "원문이 같은 줄을 두 번 실어 어느 줄을 쓸지 남은 자리." + "summary": "원문이 같은 줄을 두 번 실어 어느 줄을 쓸지 남은 자리.", + "role": "note" }, { "key": "variables/coef_soil_C/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" }, { "key": "variables/coef_soil_L/duplicate_note", "name_ko": "원문 중복 비고", - "summary": "원문이 같은 줄을 두 번 실어 어느 줄을 쓸지 남은 자리." + "summary": "원문이 같은 줄을 두 번 실어 어느 줄을 쓸지 남은 자리.", + "role": "note" }, { "key": "variables/coef_soil_L/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" }, { "key": "variables/rate_tool", "name_ko": "공구손료율", - "summary": "주요 재료비에 곱하는 공구손료 율(하한·상한). 어느 값을 쓸지는 고름이 필요하다." + "summary": "주요 재료비에 곱하는 공구손료 율(하한·상한). 어느 값을 쓸지는 고름이 필요하다.", + "role": "value" }, { "key": "variables/surcharge_labor/application_status", "name_ko": "적용 상태", - "summary": "쓰려면 무엇이 더 정해져야 하는지." + "summary": "쓰려면 무엇이 더 정해져야 하는지.", + "role": "note" }, { "key": "variables/surcharge_labor/representation", "name_ko": "담은 꼴", - "summary": "원문 표를 어떤 꼴로 담았는지." + "summary": "원문 표를 어떤 꼴로 담았는지.", + "role": "note" }, { "key": "variables/surcharge_labor/unit", "name_ko": "값 단위", - "summary": "할증은 %다." + "summary": "할증은 %다.", + "role": "note" }, { "key": "variables/surcharge_mat/duplicate_application_prohibited", "name_ko": "중복 적용 금지", - "summary": "두 자리에 걸면 두 번 세게 된다는 표시." + "summary": "두 자리에 걸면 두 번 세게 된다는 표시.", + "role": "note" }, { "key": "variables/surcharge_mat/representation", "name_ko": "담은 꼴", - "summary": "원문 표를 어떤 꼴로 담았는지." + "summary": "원문 표를 어떤 꼴로 담았는지.", + "role": "note" }, { "key": "variables/surcharge_mat/unit", "name_ko": "값 단위", - "summary": "할증은 %다." + "summary": "할증은 %다.", + "role": "note" } ] }, @@ -717,6 +750,13 @@ "shape": "map", "rows": 2, "columns": [ + { + "key": "@key", + "name_ko": "구실", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "doc", "name_ko": "문서", @@ -742,22 +782,26 @@ { "key": "candidates_pending_user/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "not_found", "name_ko": "못 찾은 것", - "summary": "두 품셈을 다 뒤졌으나 이름이 없는 것." + "summary": "두 품셈을 다 뒤졌으나 이름이 없는 것.", + "role": "note" }, { "key": "observed_practice", "name_ko": "실무 관측", - "summary": "실무 집계에서 본 할증률. 참고이지 기본값이 아니다." + "summary": "실무 집계에서 본 할증률. 참고이지 기본값이 아니다.", + "role": "value" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" } ] }, @@ -899,47 +943,56 @@ { "key": "euroform_type/material_unit_note", "name_ko": "자재 밑수 비고", - "summary": "자재 환산을 어느 창이 하는지." + "summary": "자재 환산을 어느 창이 하는지.", + "role": "note" }, { "key": "euroform_type/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "euroform_type/reuse_note", "name_ko": "사용횟수 비고", - "summary": "닮은 두 사용횟수를 하나로 잇지 않는 까닭." + "summary": "닮은 두 사용횟수를 하나로 잇지 않는 까닭.", + "role": "note" }, { "key": "euroform_type/source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "reuse_ratio_pct", "name_ko": "사용횟수별 기준수량 비율", - "summary": "품셈 12-4 — 일위대가 재료비에 걸린다. B08 이 곱하면 이중계상이다." + "summary": "품셈 12-4 — 일위대가 재료비에 걸린다. B08 이 곱하면 이중계상이다.", + "role": "value" }, { "key": "shoring", "name_ko": "동바리", - "summary": "강관동바리가 어느 구조물에 서는지." + "summary": "강관동바리가 어느 구조물에 서는지.", + "role": "value" }, { "key": "source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" } ] }, @@ -1027,22 +1080,26 @@ { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "price_hint_krw_per_ton", "name_ko": "단가 참고값", - "summary": "표시 전용 — 갈래 차이를 사람이 보라고 둔 값. 계산에 안 들어간다." + "summary": "표시 전용 — 갈래 차이를 사람이 보라고 둔 값. 계산에 안 들어간다.", + "role": "value" }, { "key": "source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" } ] }, @@ -1136,22 +1193,26 @@ { "key": "basis_unit", "name_ko": "밑수", - "summary": "값이 무엇 하나당인지." + "summary": "값이 무엇 하나당인지.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "pending_user", "name_ko": "사용자 확정 대기", - "summary": "아직 사람이 답해야 닫히는 물음." + "summary": "아직 사람이 답해야 닫히는 물음.", + "role": "note" }, { "key": "source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" } ] }, @@ -1188,37 +1249,44 @@ { "key": "back_length/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "back_length/option_key", "name_ko": "고르개 키", - "summary": "화면 고르개가 쓰는 저장 칸 이름." + "summary": "화면 고르개가 쓰는 저장 칸 이름.", + "role": "value" }, { "key": "back_length/source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" }, { "key": "bond", "name_ko": "쌓기 방식", - "summary": "메쌓기·찰쌓기로 공종코드가 갈린다." + "summary": "메쌓기·찰쌓기로 공종코드가 갈린다.", + "role": "value" }, { "key": "boulder_diameter", "name_ko": "돌 직경 갈래", - "summary": "큰돌쌓기 — 저장 제원 stone_cm 으로 갈린다." + "summary": "큰돌쌓기 — 저장 제원 stone_cm 으로 갈린다.", + "role": "value" }, { "key": "face_slope", "name_ko": "전면 기울기", - "summary": "형식마다 정해진 1:n. 코드 기본 0.3 이 여기서 왔다." + "summary": "형식마다 정해진 1:n. 코드 기본 0.3 이 여기서 왔다.", + "role": "value" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" } ] }, @@ -1236,6 +1304,13 @@ "shape": "map", "rows": 2, "columns": [ + { + "key": "@key", + "name_ko": "쌓기 방식", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "성토", "name_ko": "성토부 경사", @@ -1255,27 +1330,32 @@ { "key": "not_here", "name_ko": "이 표가 안 다루는 것", - "summary": "헷갈리기 쉬운 이웃 값을 여기 안 둔다고 적어 둔 자리." + "summary": "헷갈리기 쉬운 이웃 값을 여기 안 둔다고 적어 둔 자리.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" }, { "key": "steps_m", "name_ko": "직고 칸", - "summary": "표가 갈리는 높이 경계(m)." + "summary": "표가 갈리는 높이 경계(m).", + "role": "value" } ] }, @@ -1290,32 +1370,38 @@ { "key": "not_here", "name_ko": "이 표가 안 다루는 것", - "summary": "헷갈리기 쉬운 이웃 값을 여기 안 둔다고 적어 둔 자리." + "summary": "헷갈리기 쉬운 이웃 값을 여기 안 둔다고 적어 둔 자리.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" }, { "key": "steps_m", "name_ko": "직고 칸", - "summary": "표가 갈리는 높이 경계(m)." + "summary": "표가 갈리는 높이 경계(m).", + "role": "value" }, { "key": "table_cm", "name_ko": "뒷길이 범위표", - "summary": "메·찰마다 직고 칸별 뒷길이 하한·상한(㎝)." + "summary": "메·찰마다 직고 칸별 뒷길이 하한·상한(㎝).", + "role": "value" } ] }, @@ -1333,6 +1419,13 @@ "shape": "map", "rows": 4, "columns": [ + { + "key": "@key", + "name_ko": "출처 키", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "doc", "name_ko": "문서", @@ -1358,67 +1451,80 @@ { "key": "back_lengths_cm", "name_ko": "뒷길이 규격", - "summary": "표가 값을 주는 뒷길이 일곱 칸." + "summary": "표가 값을 주는 뒷길이 일곱 칸.", + "role": "value" }, { "key": "backfill_ratio_of_back_length", "name_ko": "뒤채움 몫", - "summary": "뒷길이 가운데 뒤채움이 차지하는 비. 나머지가 잡석이다." + "summary": "뒷길이 가운데 뒤채움이 차지하는 비. 나머지가 잡석이다.", + "role": "value" }, { "key": "fallback", "name_ko": "미지정일 때", - "summary": "사용자가 안 고른 경우 쓰는 한 벌." + "summary": "사용자가 안 고른 경우 쓰는 한 벌.", + "role": "value" }, { "key": "fill_concrete_m3_per_m2", "name_ko": "채움 콘크리트 원단위", - "summary": "돌 종류 × 뒷길이마다 ㎥/㎡. 원문이 두 줄뿐이다." + "summary": "돌 종류 × 뒷길이마다 ㎥/㎡. 원문이 두 줄뿐이다.", + "role": "value" }, { "key": "kinds", "name_ko": "돌 종류", - "summary": "야면석·호박돌 / 깬잡석 / 깬돌 / 견치돌 네 줄." + "summary": "야면석·호박돌 / 깬잡석 / 깬돌 / 견치돌 네 줄.", + "role": "value" }, { "key": "no_folding", "name_ko": "접지 않음 규칙", - "summary": "표에 없는 뒷길이를 가까운 칸으로 접지 않는다 — 접으면 값이 조용히 틀린다." + "summary": "표에 없는 뒷길이를 가까운 칸으로 접지 않는다 — 접으면 값이 조용히 틀린다.", + "role": "note" }, { "key": "not_here", "name_ko": "이 표가 안 다루는 것", - "summary": "헷갈리기 쉬운 이웃 값을 여기 안 둔다고 적어 둔 자리." + "summary": "헷갈리기 쉬운 이웃 값을 여기 안 둔다고 적어 둔 자리.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "option_key", "name_ko": "고르개 키", - "summary": "화면 고르개가 쓰는 저장 칸 이름." + "summary": "화면 고르개가 쓰는 저장 칸 이름.", + "role": "value" }, { "key": "option_note", "name_ko": "고르개 비고", - "summary": "고르개를 만들 때 챙길 것." + "summary": "고르개를 만들 때 챙길 것.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "wedge_stone_m3_per_m2", "name_ko": "고임돌 원단위", - "summary": "돌 종류 × 뒷길이마다 ㎥/㎡. null 은 원문 「-」다." + "summary": "돌 종류 × 뒷길이마다 ㎥/㎡. null 은 원문 「-」다.", + "role": "value" }, { "key": "why", "name_ko": "까닭", - "summary": "이 벌을 왜 따로 두는지." + "summary": "이 벌을 왜 따로 두는지.", + "role": "note" } ] }, @@ -1433,22 +1539,26 @@ { "key": "items", "name_ko": "항목", - "summary": "이 벌이 담는 항목 묶음." + "summary": "이 벌이 담는 항목 묶음.", + "role": "value" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "rank", "name_ko": "우선순위", - "summary": "원문끼리 어긋날 때 어느 쪽을 먼저 보는지." + "summary": "원문끼리 어긋날 때 어느 쪽을 먼저 보는지.", + "role": "note" }, { "key": "scope", "name_ko": "걸치는 범위", - "summary": "어디까지 쓰는 값인지." + "summary": "어디까지 쓰는 값인지.", + "role": "note" } ] }, @@ -1670,6 +1780,13 @@ "shape": "map", "rows": 2, "columns": [ + { + "key": "@key", + "name_ko": "출처 키", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "doc", "name_ko": "문서", @@ -1716,32 +1833,38 @@ { "key": "double_count_rules/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "not_found/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "pending_choices/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "原文_뒷받침/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" } ] }, @@ -2183,57 +2306,68 @@ { "key": "composite/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "composite/unit_conversion", "name_ko": "단위 환산", - "summary": "단가 단위와 원단위 단위가 달라 맞추는 값." + "summary": "단가 단위와 원단위 단위가 달라 맞추는 값.", + "role": "value" }, { "key": "concrete_placing", "name_ko": "콘크리트 타설 잇기", - "summary": "타설 방식 × 구조물 종류로 공종이 갈리는 자리." + "summary": "타설 방식 × 구조물 종류로 공종이 갈리는 자리.", + "role": "value" }, { "key": "ground_aliases_moved_to", "name_ko": "갈래 별칭 옮긴 자리", - "summary": "갈래 이름 별칭은 별칭표로 옮겼다. 여기 다시 두지 않는다." + "summary": "갈래 이름 별칭은 별칭표로 옮겼다. 여기 다시 두지 않는다.", + "role": "note" }, { "key": "masonry_class_reference", "name_ko": "돌쌓기 갈래 참고 자리", - "summary": "돌쌓기 갈래는 이제 참고용이고 어긋나면 그것이 신호다." + "summary": "돌쌓기 갈래는 이제 참고용이고 어긋나면 그것이 신호다.", + "role": "note" }, { "key": "master", "name_ko": "마스터 자리", - "summary": "공종 코드를 읽어 오는 마스터 파일." + "summary": "공종 코드를 읽어 오는 마스터 파일.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "pending_user/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "pipe", "name_ko": "배수관 잇기", - "summary": "관종으로 공종이 갈린다. 관 정본은 pipe_points 다." + "summary": "관종으로 공종이 갈린다. 관 정본은 pipe_points 다.", + "role": "value" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "variant_contract", "name_ko": "갈래 키 계약", - "summary": "갈래 키 문자열을 두 창이 각자 조립하지 않기로 한 약속." + "summary": "갈래 키 문자열을 두 창이 각자 조립하지 않기로 한 약속.", + "role": "note" } ] }, @@ -2294,7 +2428,8 @@ { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" } ] }, @@ -2355,12 +2490,14 @@ { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" } ] }, @@ -2454,17 +2591,20 @@ { "key": "variables/aliases", "name_ko": "노임 별칭", - "summary": "코드 대신 쓰는 별칭 이름표(labor_1002 = 보통인부)." + "summary": "코드 대신 쓰는 별칭 이름표(labor_1002 = 보통인부).", + "role": "value" }, { "key": "variables/labor_rate/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" }, { "key": "variables/labor_rate/reliability_note", "name_ko": "신뢰도 기호 뜻풀이", - "summary": "노임 표의 * · ** 가 무엇을 뜻하는지." + "summary": "노임 표의 * · ** 가 무엇을 뜻하는지.", + "role": "note" } ] }, @@ -2564,7 +2704,8 @@ { "key": "variables/labor_mfg/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" } ] }, @@ -2910,52 +3051,62 @@ { "key": "parse_audit", "name_ko": "읽기 점검", - "summary": "취득가 표에서 못 읽은 줄이 있는지 적어 둔 자리." + "summary": "취득가 표에서 못 읽은 줄이 있는지 적어 둔 자리.", + "role": "note" }, { "key": "variables/mach_fuel_rate/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" }, { "key": "variables/mach_fuel_rate/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "variables/mach_fuel_rate/unit", "name_ko": "값 단위", - "summary": "연료량은 L/시간이다." + "summary": "연료량은 L/시간이다.", + "role": "note" }, { "key": "variables/mach_loss_coef/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" }, { "key": "variables/mach_loss_coef/unit", "name_ko": "값 단위", - "summary": "손료계수는 1/시간이다." + "summary": "손료계수는 1/시간이다.", + "role": "note" }, { "key": "variables/mach_operator_map/note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "variables/mach_price/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" }, { "key": "variables/mach_price/unit", "name_ko": "값 단위", - "summary": "취득가는 천원 단위다." + "summary": "취득가는 천원 단위다.", + "role": "note" }, { "key": "variables/mach_rock_adj/unit", "name_ko": "값 단위", - "summary": "할증은 %다." + "summary": "할증은 %다.", + "role": "note" } ] }, @@ -3034,27 +3185,32 @@ { "key": "derived_from", "name_ko": "무엇에서 나왔나", - "summary": "어느 벌을 풀어 만든 파생본인지." + "summary": "어느 벌을 풀어 만든 파생본인지.", + "role": "note" }, { "key": "dropped_tables", "name_ko": "못 읽어 버린 표", - "summary": "칸 수가 안 맞아 못 읽고 버린 원문 표." + "summary": "칸 수가 안 맞아 못 읽고 버린 원문 표.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "stats", "name_ko": "집계", - "summary": "몇 줄이 섰고 몇 줄이 빠졌는지 센 것." + "summary": "몇 줄이 섰고 몇 줄이 빠졌는지 센 것.", + "role": "note" } ] }, @@ -3211,17 +3367,20 @@ { "key": "selection_policy", "name_ko": "고르는 방침", - "summary": "같은 물품이 여러 번 공고되면 어느 것을 쓰는지." + "summary": "같은 물품이 여러 번 공고되면 어느 것을 쓰는지.", + "role": "note" }, { "key": "source_row_count", "name_ko": "원천 줄 수", - "summary": "걸러 내기 전 원본 줄 수." + "summary": "걸러 내기 전 원본 줄 수.", + "role": "note" }, { "key": "variables/mat_price/key", "name_ko": "줄을 가르는 칸", - "summary": "이 표에서 줄 하나를 집는 칸 이름." + "summary": "이 표에서 줄 하나를 집는 칸 이름.", + "role": "note" } ] }, @@ -3266,6 +3425,13 @@ "shape": "map", "rows": 2, "columns": [ + { + "key": "@key", + "name_ko": "유종", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "value", "name_ko": "단가", @@ -3400,57 +3566,68 @@ { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "variables/oil_diesel/date", "name_ko": "기준일", - "summary": "값이 선 날." + "summary": "값이 선 날.", + "role": "note" }, { "key": "variables/oil_diesel/scope", "name_ko": "걸치는 범위", - "summary": "어디까지 쓰는 값인지." + "summary": "어디까지 쓰는 값인지.", + "role": "note" }, { "key": "variables/oil_diesel/source_product_code", "name_ko": "원천 제품코드", - "summary": "원천이 쓰는 제품 코드." + "summary": "원천이 쓰는 제품 코드.", + "role": "note" }, { "key": "variables/oil_diesel/source_product_name", "name_ko": "원천 제품명", - "summary": "원천이 쓰는 제품 이름." + "summary": "원천이 쓰는 제품 이름.", + "role": "note" }, { "key": "variables/oil_diesel/unit", "name_ko": "값 단위", - "summary": "단가는 원/L 이다." + "summary": "단가는 원/L 이다.", + "role": "note" }, { "key": "variables/oil_gasoline/date", "name_ko": "기준일", - "summary": "값이 선 날." + "summary": "값이 선 날.", + "role": "note" }, { "key": "variables/oil_gasoline/scope", "name_ko": "걸치는 범위", - "summary": "어디까지 쓰는 값인지." + "summary": "어디까지 쓰는 값인지.", + "role": "note" }, { "key": "variables/oil_gasoline/source_product_code", "name_ko": "원천 제품코드", - "summary": "원천이 쓰는 제품 코드." + "summary": "원천이 쓰는 제품 코드.", + "role": "note" }, { "key": "variables/oil_gasoline/source_product_name", "name_ko": "원천 제품명", - "summary": "원천이 쓰는 제품 이름." + "summary": "원천이 쓰는 제품 이름.", + "role": "note" }, { "key": "variables/oil_gasoline/unit", "name_ko": "값 단위", - "summary": "단가는 원/L 이다." + "summary": "단가는 원/L 이다.", + "role": "note" } ] }, @@ -3495,6 +3672,13 @@ "shape": "map", "rows": 5, "columns": [ + { + "key": "@key", + "name_ko": "통화", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "value", "name_ko": "환율", @@ -3899,147 +4083,176 @@ { "key": "processing_rules", "name_ko": "셈 규칙", - "summary": "요율만으로는 금액이 안 나온다 — 밑수를 어떻게 만들고 어디서 자르는지 적은 자리. 코드와 어긋나면 거울 시험이 잡는다." + "summary": "요율만으로는 금액이 안 나온다 — 밑수를 어떻게 만들고 어디서 자르는지 적은 자리. 코드와 어긋나면 거울 시험이 잡는다.", + "role": "note" }, { "key": "variables/rate_asbestos_contribution", "name_ko": "석면피해구제 분담금", - "summary": "노무비에 곱하는 율." + "summary": "노무비에 곱하는 율.", + "role": "value" }, { "key": "variables/rate_care", "name_ko": "노인장기요양보험료", - "summary": "건강보험료 금액에 곱하는 율." + "summary": "건강보험료 금액에 곱하는 율.", + "role": "value" }, { "key": "variables/rate_environment/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_environment/forest_road_selection_status", "name_ko": "임도 줄 고름 상태", - "summary": "임도에 어느 줄을 쓸지 고름이 끝났는지." + "summary": "임도에 어느 줄을 쓸지 고름이 끝났는지.", + "role": "note" }, { "key": "variables/rate_environment/minimum_estimated_amount_krw", "name_ko": "적용 하한 — 추정금액", - "summary": "이 금액에 못 미치면 줄이 안 선다(원)." + "summary": "이 금액에 못 미치면 줄이 안 선다(원).", + "role": "value" }, { "key": "variables/rate_equipment_payment_guarantee/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_equipment_payment_guarantee/base_note", "name_ko": "밑수 비고", - "summary": "밑수를 그렇게 적은 까닭." + "summary": "밑수를 그렇게 적은 까닭.", + "role": "note" }, { "key": "variables/rate_goyong/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_health", "name_ko": "건강보험료", - "summary": "직접노무비에 곱하는 율." + "summary": "직접노무비에 곱하는 율.", + "role": "value" }, { "key": "variables/rate_indirect_labor/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_other_expense/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_overhead/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_pension/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_pension/rate_from_2033_percent", "name_ko": "2033년 이후 요율", - "summary": "표 밖으로 나가는 해부터 쓰는 율(%)." + "summary": "표 밖으로 나가는 해부터 쓰는 율(%).", + "role": "value" }, { "key": "variables/rate_performance_guarantee_fee/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_performance_guarantee_fee/typical_forest_road_applicability", "name_ko": "임도에 걸리는지", - "summary": "보통 임도 규모에서 이 항목이 서는지." + "summary": "보통 임도 규모에서 이 항목이 서는지.", + "role": "note" }, { "key": "variables/rate_profit/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_retirement_mutual_aid", "name_ko": "퇴직공제부금비", - "summary": "직접노무비에 곱하는 율. 추정금액 하한이 있다." + "summary": "직접노무비에 곱하는 율. 추정금액 하한이 있다.", + "role": "value" }, { "key": "variables/rate_safety_base", "name_ko": "산업안전보건관리비 기초액", - "summary": "요율표 안에 함께 든 기초액이 걸리는 구간." + "summary": "요율표 안에 함께 든 기초액이 걸리는 구간.", + "role": "value" }, { "key": "variables/rate_safety_pct/base_rule", "name_ko": "밑수 규칙", - "summary": "밑수를 고르는 방식." + "summary": "밑수를 고르는 방식.", + "role": "value" }, { "key": "variables/rate_safety_pct/base_with_owner_supplied_material", "name_ko": "대상액 — 관급자재 있을 때", - "summary": "고시가 준 두 식 가운데 작은 쪽." + "summary": "고시가 준 두 식 가운데 작은 쪽.", + "role": "value" }, { "key": "variables/rate_safety_pct/base_without_owner_supplied_material", "name_ko": "대상액 — 관급자재 없을 때", - "summary": "재료비 + 직접노무비." + "summary": "재료비 + 직접노무비.", + "role": "value" }, { "key": "variables/rate_safety_pct/manager_thresholds", "name_ko": "안전관리자 선임 기준액", - "summary": "안전관리자를 둬야 하는 금액 문턱." + "summary": "안전관리자를 둬야 하는 금액 문턱.", + "role": "value" }, { "key": "variables/rate_safety_pct/minimum_total_construction_amount_krw", "name_ko": "적용 하한 — 총공사금액", - "summary": "이 금액에 못 미치면 줄이 안 선다(원)." + "summary": "이 금액에 못 미치면 줄이 안 선다(원).", + "role": "value" }, { "key": "variables/rate_sanjae", "name_ko": "산재보험료", - "summary": "노무비에 곱하는 율." + "summary": "노무비에 곱하는 율.", + "role": "value" }, { "key": "variables/rate_subcontract_payment_guarantee/base", "name_ko": "밑수", - "summary": "이 요율을 곱하는 대상." + "summary": "이 요율을 곱하는 대상.", + "role": "value" }, { "key": "variables/rate_vat", "name_ko": "부가가치세", - "summary": "공급가액에 곱하는 율." + "summary": "공급가액에 곱하는 율.", + "role": "value" }, { "key": "variables/rate_wage_claim_contribution", "name_ko": "임금채권보장 부담금", - "summary": "노무비에 곱하는 율." + "summary": "노무비에 곱하는 율.", + "role": "value" } ] }, @@ -4088,7 +4301,8 @@ { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" } ] }, @@ -4143,7 +4357,8 @@ { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" } ] }, @@ -4246,12 +4461,14 @@ { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" }, { "key": "stats", "name_ko": "집계", - "summary": "몇 줄이 섰고 몇 줄이 빠졌는지 센 것." + "summary": "몇 줄이 섰고 몇 줄이 빠졌는지 센 것.", + "role": "note" } ] }, @@ -4300,7 +4517,8 @@ { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" } ] }, @@ -4393,6 +4611,13 @@ "shape": "map", "rows": 3, "columns": [ + { + "key": "@key", + "name_ko": "표 이름", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "note", "name_ko": "비고", @@ -4459,6 +4684,13 @@ "shape": "map", "rows": 15, "columns": [ + { + "key": "@key", + "name_ko": "칸 키", + "unit": "", + "visible": true, + "is_row_key": true + }, { "key": "label", "name_ko": "칸 이름", @@ -4514,42 +4746,50 @@ { "key": "code", "name_ko": "항목 코드", - "summary": "라이브러리 항목 코드." + "summary": "라이브러리 항목 코드.", + "role": "note" }, { "key": "differs_from_engine", "name_ko": "전개와 다른 자리", - "summary": "지금 돌아가는 전개와 이 양식이 갈리는 곳을 적어 둔 자리." + "summary": "지금 돌아가는 전개와 이 양식이 갈리는 곳을 적어 둔 자리.", + "role": "note" }, { "key": "item_kind", "name_ko": "항목 갈래", - "summary": "양식형(form)인지 고정형인지." + "summary": "양식형(form)인지 고정형인지.", + "role": "note" }, { "key": "library_tier", "name_ko": "라이브러리 단", - "summary": "프로그램·회사·개인 가운데 어느 단의 항목인지." + "summary": "프로그램·회사·개인 가운데 어느 단의 항목인지.", + "role": "note" }, { "key": "name", "name_ko": "항목 이름", - "summary": "화면에 보이는 이름." + "summary": "화면에 보이는 이름.", + "role": "note" }, { "key": "note", "name_ko": "설명", - "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글." + "summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.", + "role": "note" }, { "key": "type_id", "name_ko": "구조물 종류 키", - "summary": "어느 구조물 종류에 붙는 양식인지." + "summary": "어느 구조물 종류에 붙는 양식인지.", + "role": "note" }, { "key": "unit_price/note", "name_ko": "일위대가 틀 설명", - "summary": "일위대가 줄을 어떻게 세우는지 적은 글." + "summary": "일위대가 줄을 어떻게 세우는지 적은 글.", + "role": "note" } ] }, @@ -4691,7 +4931,8 @@ { "key": "policy", "name_ko": "방침", - "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것." + "summary": "이 벌을 다룰 때 지킬 것 — 지어내지 않기·빈칸 안 두기 같은 것.", + "role": "note" } ] }, @@ -4734,7 +4975,8 @@ { "key": "built_by", "name_ko": "만든 자리", - "summary": "이 벌을 지은 코드나 사람." + "summary": "이 벌을 지은 코드나 사람.", + "role": "note" } ] }, @@ -4777,12 +5019,14 @@ { "key": "built_by", "name_ko": "만든 자리", - "summary": "이 벌을 지은 코드나 사람." + "summary": "이 벌을 지은 코드나 사람.", + "role": "note" }, { "key": "source", "name_ko": "출처", - "summary": "어느 원문에서 왔는지." + "summary": "어느 원문에서 왔는지.", + "role": "note" } ] } diff --git a/resources/tester/test_master_labels_cover.py b/resources/tester/test_master_labels_cover.py index 3e9ec49a..250a6672 100644 --- a/resources/tester/test_master_labels_cover.py +++ b/resources/tester/test_master_labels_cover.py @@ -95,9 +95,13 @@ def _scan(path: Path) -> tuple[list, list]: return tables, values +#: 사전 모양 표의 **줄 이름 열** — API 가 이 이름으로 내보낸다(2026-09-15 브레인 ①). +ROW_KEY_COLUMN = "@key" + + def _columns_of(node, shape: str) -> list[str]: records = node if shape == "list" else list(node.values()) - seen: list[str] = [] + seen: list[str] = [ROW_KEY_COLUMN] if shape == "map" else [] for record in records: if not isinstance(record, dict): continue @@ -281,3 +285,87 @@ def test_세어_둔_수가_실제와_같다(labels): def test_강우_IDF_캐시는_안_담았다(labels): assert not [f for f in labels["files"] if "rainfall" in f["path"]] + + +def test_사전_모양_표에는_줄_이름_열이_있다(labels): + """`fx`·`oil` 처럼 사전 모양인 표는 **줄 이름(키) 자체가 한 열**이다. + 그 열이 없으면 화면에 영문 `@key` 로 뜬다(2026-09-15 브레인 ①).""" + bad = [] + for entry in labels["files"]: + for table in entry["tables"]: + if table["shape"] != "map": + continue + first = table["columns"][0] if table["columns"] else None + if first is None or first["key"] != ROW_KEY_COLUMN: + bad.append(f"{entry['file_id']}::{table['key']} — 줄 이름 열 없음") + elif not first["name_ko"].strip(): + bad.append(f"{entry['file_id']}::{table['key']} — 줄 이름 열에 한글 이름 없음") + elif not first.get("is_row_key"): + bad.append(f"{entry['file_id']}::{table['key']} — is_row_key 표시 없음") + assert not bad, f"줄 이름 열 문제: {bad}" + + +def test_줄_이름_열은_사전_모양_표에만_있다(labels): + stray = [ + f"{entry['file_id']}::{table['key']}" + for entry in labels["files"] + for table in entry["tables"] + if table["shape"] != "map" and any(c["key"] == ROW_KEY_COLUMN for c in table["columns"]) + ] + assert not stray, f"사전 모양이 아닌데 줄 이름 열이 있다: {stray}" + + +def test_줄_이름_열의_이름이_표마다_제_것이다(labels): + """뜻이 표마다 다르므로 한 이름으로 뭉치면 안 된다 — 같은 이름이 겹치면 살펴볼 것.""" + names = [ + column["name_ko"] + for entry in labels["files"] + for table in entry["tables"] + for column in table["columns"] + if column["key"] == ROW_KEY_COLUMN + ] + assert names, "줄 이름 열이 하나도 없다" + # 「출처 키」만 두 표가 같은 뜻으로 쓴다(출처 사전 둘). 그 밖은 겹치지 않는다. + duplicated = {n for n in names if names.count(n) > 1} + assert duplicated <= {"출처 키"}, f"줄 이름이 겹친다: {sorted(duplicated)}" + + +def test_값_묶음마다_값이냐_설명이냐가_적혀_있다(labels): + known = set(labels["value_roles"]) + bad = [] + for entry in labels["files"]: + for group in entry["value_groups"]: + if group.get("role") not in known: + bad.append(f"{entry['file_id']}::{group['key']} — {group.get('role')!r}") + assert not bad, f"값·설명이 안 갈린 값 묶음: {bad}" + + +def test_값_갈래_잣대가_이름표에_적혀_있다(labels): + rule = labels["policy"]["value_role_rule"] + assert "금액이 움직이나" in rule + assert labels["policy"]["row_key_column"] + + +def test_한_값짜리_요율은_값으로_선다(labels): + """산재 3.56 % 처럼 원가에 그대로 드는 요율이 설명으로 숨으면 트리에서 사라진다.""" + rates = next(f for f in labels["files"] if f["file_id"] == "rates") + by_key = {g["key"]: g["role"] for g in rates["value_groups"]} + for key in ( + "variables/rate_sanjae", + "variables/rate_health", + "variables/rate_care", + "variables/rate_vat", + "variables/rate_retirement_mutual_aid", + "variables/rate_wage_claim_contribution", + "variables/rate_asbestos_contribution", + ): + assert by_key.get(key) == "value", f"{key} 가 값으로 안 섰다" + assert by_key.get("processing_rules") == "note" + + +def test_세어_둔_갈래_수가_실제와_같다(labels): + counted: dict[str, int] = {} + for entry in labels["files"]: + for group in entry["value_groups"]: + counted[group["role"]] = counted.get(group["role"], 0) + 1 + assert counted == labels["counts"]["value_groups_by_role"] From f2bfaf888d9d8fb902f7c561ab24d943bc79083b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 19:20:30 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(z01):=20=EB=82=B1=EA=B0=92=20=ED=91=9C?= =?UTF-8?q?=20`@values`=20=E2=80=94=20=ED=91=9C=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EB=A7=88=EB=94=94(=EC=82=B0=EC=9E=AC=203.56%=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EA=B0=92=20=EB=AC=B6=EC=9D=8C)=EB=A5=BC=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=A7=88=EB=8B=A4=20=ED=91=9C=20=ED=95=98?= =?UTF-8?q?=EB=82=98(=EC=9D=B4=EB=A6=84=20=C2=B7=20=EA=B0=92=20=C2=B7=20?= =?UTF-8?q?=EC=98=81=EB=AC=B8=20=EC=9E=90=EB=A6=AC=20=EC=88=A8=EA=B9=80)?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=99=EC=9D=80=20rows=20=EA=B8=B8=EC=97=90=20?= =?UTF-8?q?=ED=83=9C=EC=9B=80=20=C2=B7=20=EC=84=A4=EB=AA=85=C2=B7=EB=B0=A9?= =?UTF-8?q?=EC=B9=A8(note=C2=B7policy=C2=B7source=20=E2=80=A6)=EC=9D=80=20?= =?UTF-8?q?=EB=BA=8C(=EA=B0=80=EB=A6=84=EC=9D=80=20=ED=95=9C=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=C2=B7=20=EC=9D=B4=EB=A6=84=ED=91=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=98=A4=EB=A9=B4=20=EA=B0=88=EC=95=84=EB=81=BC?= =?UTF-8?q?=EC=9B=80)=20=C2=B7=20=EA=B0=92=20=EB=AC=B6=EC=9D=8C=20?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=C2=B7=EC=9D=B4=EB=A6=84=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=ED=91=9C=EC=99=80=20=EC=96=91=EC=AA=BD=20=EC=85=88=200(?= =?UTF-8?q?=EB=B8=8C=EB=A0=88=EC=9D=B8=20=E2=91=A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- Z01_MasterData/Z01_MasterData_Tables.py | 65 ++++++++++++++++++++---- resources/tester/test_z01_master_data.py | 55 ++++++++++++++++++-- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/Z01_MasterData/Z01_MasterData_Tables.py b/Z01_MasterData/Z01_MasterData_Tables.py index 1d544f90..852cbb95 100644 --- a/Z01_MasterData/Z01_MasterData_Tables.py +++ b/Z01_MasterData/Z01_MasterData_Tables.py @@ -6,6 +6,7 @@ 표 = **화면이 표로 그릴 마디** — 이름표 생성기와 같은 잣대(브레인 「91 이 정본」): · 딕셔너리 목록(비지 않음) → 줄 = 딕셔너리 그대로({열key: 값}) · 딕셔너리 둘 이상이 모인 딕셔너리(열이 비슷함) → 줄마다 `@key` 열(그 줄의 이름) + 값 딕셔너리 + · 표 아닌 마디(값 묶음 — 산재 3.56% 같은 낱값)는 파일마다 표 하나 `@values`(이름 · 값) — 설명·방침은 뺌(브레인 ④) ⚠ 표 id 가 이름표와 한 글자라도 다르면 이름이 통째로 안 붙음 — `test_z01_master_data.py` 가 양쪽으로 셈. """ @@ -22,6 +23,18 @@ LABEL_DIR = RESOURCES / "data_master_labels" DEFAULT_PAGE_SIZE = 50 MAX_PAGE_SIZE = 500 ROW_KEY = "@key" +VALUES_TABLE = "@values" +_CODE_HIDDEN = {"@path"} # 낱값 표의 영문 자리 — 이름표에 없을 때만 + +# ⚠ 잠정 — 이름표가 값 묶음마다 「값이냐 설명이냐」 표시를 주면 **이 자리만** 갈아끼움(브레인 ④) +_DESCRIPTION_KEYS = {"note", "notes", "policy", "source", "sources", "quote", "why"} + + +def is_description(group_key: str) -> bool: + """값 묶음이 설명·방침인가(값이 아님) — 끝 조각으로 가름.""" + last = group_key.rsplit("/", 1)[-1] + return last in _DESCRIPTION_KEYS or last.endswith("_note") + # 파일 머리 — 표도 값 묶음도 아님(이름표 생성기와 같은 목록) META_KEYS = { @@ -57,7 +70,9 @@ def _has_table(node: Any, depth: int = 0) -> bool: return any(_has_table(v, depth + 1) for v in node.values()) -def _collect(node: Any, path: list[str], out: dict[str, list], depth: int = 0) -> None: +def _collect( + node: Any, path: list[str], out: dict[str, list], values: dict, depth: int = 0 +) -> None: shape = _is_table(node) if shape == "list": out["/".join(path)] = node @@ -65,22 +80,45 @@ def _collect(node: Any, path: list[str], out: dict[str, list], depth: int = 0) - out["/".join(path)] = [{ROW_KEY: k, **v} for k, v in node.items()] elif depth <= 5 and isinstance(node, dict) and any(_has_table(v) for v in node.values()): for key, value in node.items(): - _collect(value, [*path, key], out, depth + 1) + _collect(value, [*path, key], out, values, depth + 1) + else: + values["/".join(path)] = node @lru_cache(maxsize=64) -def _tables_of(path: str, _mtime_ns: int) -> dict[str, list[dict[str, Any]]]: +def _scan(path: str, _mtime_ns: int) -> tuple[dict[str, list[dict[str, Any]]], dict[str, Any]]: doc = json.loads(Path(path).read_text(encoding="utf-8")) out: dict[str, list[dict[str, Any]]] = {} + values: dict[str, Any] = {} for key, value in doc.items(): if key not in META_KEYS: - _collect(value, [key], out) - return out + _collect(value, [key], out, values) + return out, values def tables_of(path: Path) -> dict[str, list[dict[str, Any]]]: """표 id(이름표의 표 key · `/` 로 이은 자리) → 줄.""" - return _tables_of(str(path), path.stat().st_mtime_ns) + return _scan(str(path), path.stat().st_mtime_ns)[0] + + +def values_of(path: Path) -> dict[str, Any]: + """값 묶음 자리(이름표의 값 묶음 key) → 값 — 설명·방침도 섞임.""" + return _scan(str(path), path.stat().st_mtime_ns)[1] + + +def _value_rows(fid: str, path: Path, labels: dict[str, Any]) -> list[dict[str, Any]]: + """낱값 표 줄 — 찾는 차례: 「파일id::key」 → 「끝 조각」 → 영문 key.""" + rows = [] + for key, value in values_of(path).items(): + if is_description(key): + continue + named = ( + (labels.get("value_overrides") or {}).get(f"{fid}::{key}") + or (labels.get("value_keys") or {}).get(key.rsplit("/", 1)[-1]) + or {} + ) + rows.append({"@name": named.get("name_ko") or key, "@value": value, "@path": key}) + return rows @lru_cache(maxsize=4) @@ -114,14 +152,18 @@ def tree() -> dict[str, Any]: named = {t["key"]: t.get("name_ko") or "" for t in entry.get("tables") or []} kind = entry.get("kind") or "" group = groups.setdefault(kind, {"key": kind, "label": kind, "files": []}) + counts = {tid: len(rows) for tid, rows in tables_of(path).items()} + value_count = len(_value_rows(entry["file_id"], path, labels)) + if value_count: + counts[VALUES_TABLE] = value_count group["files"].append( { "id": entry["file_id"], "label": entry.get("name_ko") or entry["file_id"], "key": path.name, "tables": [ - {"id": tid, "label": named.get(tid) or tid, "key": tid, "row_count": len(rows)} - for tid, rows in tables_of(path).items() + {"id": tid, "label": named.get(tid) or tid, "key": tid, "row_count": n} + for tid, n in counts.items() ], } ) @@ -135,7 +177,10 @@ def rows( labels = load_labels() entry = next((f for f in labels["files"] if f["file_id"] == fid), None) path = _file_path(entry) if entry else None - table = tables_of(path).get(tid) if path else None + if path and tid == VALUES_TABLE: + table = _value_rows(fid, path, labels) or None + else: + table = tables_of(path).get(tid) if path else None if table is None: return None keys: dict[str, None] = {} @@ -157,7 +202,7 @@ def rows( "key": key, "label": named.get("name_ko") or key, "unit": named.get("unit") or "", - "hidden": named.get("visible") is False, + "hidden": named.get("visible") is False if named else key in _CODE_HIDDEN, } ) return {"columns": columns, "rows": hits[start : start + size], "total": len(hits)} diff --git a/resources/tester/test_z01_master_data.py b/resources/tester/test_z01_master_data.py index 8be5f053..7b5d77ef 100644 --- a/resources/tester/test_z01_master_data.py +++ b/resources/tester/test_z01_master_data.py @@ -63,19 +63,22 @@ def test_갈래는_이름표_차례_파일_34_강우_캐시는_없음(client: Te def test_트리_표_id_와_이름표_표_id_는_양쪽으로_같음(client: TestClient) -> None: - files = _files(client) + files = { # 낱값 표 `@values` 는 예약어 — 이름표 표 목록 밖(아래 시험이 따로 셈) + fid: [t for t in f["tables"] if t["id"] != tables.VALUES_TABLE] + for fid, f in _files(client).items() + } not_labelled, label_only = [], [] for entry in LABELS["files"]: - tree_ids = {t["id"] for t in files[entry["file_id"]]["tables"]} + tree_ids = {t["id"] for t in files[entry["file_id"]]} label_ids = {t["key"] for t in entry["tables"]} not_labelled += [f"{entry['file_id']}::{i}" for i in sorted(tree_ids - label_ids)] label_only += [f"{entry['file_id']}::{i}" for i in sorted(label_ids - tree_ids)] assert not_labelled == [], not_labelled assert label_only == [], label_only - assert sum(len(f["tables"]) for f in files.values()) == LABELS["counts"]["tables"] == 91 + assert sum(len(t) for t in files.values()) == LABELS["counts"]["tables"] == 91 for entry in LABELS["files"]: # 이름이 실제로 붙음 · 줄 수도 이름표 셈과 같음 named = {t["key"]: t for t in entry["tables"]} - for t in files[entry["file_id"]]["tables"]: + for t in files[entry["file_id"]]: assert t["label"] == named[t["id"]]["name_ko"], (entry["file_id"], t["id"]) assert t["row_count"] == named[t["id"]]["rows"], (entry["file_id"], t["id"]) @@ -164,6 +167,50 @@ def test_찾는_차례와_없는_이름은_영문_key( ) +def test_낱값은_파일마다_표_하나_설명류는_뺌(client: TestClient) -> None: + """브레인 ④ — 표 아닌 마디(값 묶음)도 트리에 `@values` 한 마디로 · 설명·방침은 값이 아님.""" + files = _files(client) + rates = files["rates"]["tables"] + assert rates[-1]["id"] == "@values" + values = _rows(client, file="rates", table="@values", size=500) + assert values["total"] == rates[-1]["row_count"] + by_path = {r["@path"]: r for r in values["rows"]} + sanjae = by_path["variables/rate_sanjae"] + assert sanjae["@value"] == {"rate_percent": 3.56, "base": "total_labor_cost"} + assert sanjae["@name"] == "산재보험료" + assert {c["key"]: c["hidden"] for c in values["columns"]} == { + "@name": False, + "@value": False, + "@path": True, + } + master = {r["@path"] for r in _rows(client, file="work_item_master", table="@values")["rows"]} + assert master == {"stats"} # policy 는 방침 + assert "@values" not in {t["id"] for t in files["aliases"]["tables"]} # note 뿐 + assert "@values" not in {t["id"] for t in files["fx"]["tables"]} # 값 묶음 없음 + for f in files.values(): + if "@values" in {t["id"] for t in f["tables"]}: + paths = [ + r["@path"] for r in _rows(client, file=f["id"], table="@values", size=500)["rows"] + ] + assert not [p for p in paths if tables.is_description(p)], f["id"] + hit = _rows(client, file="rates", table="@values", q="산재") + assert [r["@path"] for r in hit["rows"]] == ["variables/rate_sanjae"] + + +def test_값_묶음_자리는_이름표와_양쪽으로_같음() -> None: + not_labelled, label_only = [], [] + for entry in LABELS["files"]: + ours = set(tables.values_of(tables.ROOT / entry["path"])) + theirs = {g["key"] for g in entry["value_groups"]} + not_labelled += [f"{entry['file_id']}::{k}" for k in sorted(ours - theirs)] + label_only += [f"{entry['file_id']}::{k}" for k in sorted(theirs - ours)] + assert not_labelled == [] and label_only == [], (not_labelled, label_only) + for entry in LABELS["files"]: # 이름도 이름표가 푼 그대로(찾는 차례 「파일id::key」 → 끝 조각) + named = {g["key"]: g["name_ko"] for g in entry["value_groups"]} + rows = tables._value_rows(entry["file_id"], tables.ROOT / entry["path"], LABELS) + assert [r["@name"] for r in rows] == [named[r["@path"]] for r in rows], entry["file_id"] + + def test_없는_파일_표는_404(client: TestClient) -> None: for params in ( {"file": "nope", "table": "rows"},