Merge remote-tracking branches 'origin/main_laptop_1', 'origin/sub_desktop_1' and 'origin/sub_laptop_1' into CODEX

This commit is contained in:
2026-09-16 14:06:47 +09:00
12 changed files with 795 additions and 69 deletions
+19 -2
View File
@@ -47,9 +47,26 @@ export interface RowsQuery {
sort?: SortOrder | null;
}
/** 기초단가 다섯 — 고친 값은 원본이 아니라 덮개 파일에 쌓임(품셈이 갱신돼도 안 날아감). */
/**
* ① 기초단가 다섯 — 얼마인가. 시장이 정하고 주간~반기로 바뀜.
* 고친 값은 원본이 아니라 덮개 파일에 쌓임(품셈이 갱신돼도 안 날아감).
*/
export const BASE_PRICE_KINDS = ["labor", "machine", "material", "oil", "rate"] as const;
export type BasePriceKind = (typeof BASE_PRICE_KINDS)[number];
/** ② 품셈 기준 아홉 — 얼마나 드나. 품셈이 정하고 해마다 한 번 개정(2026-09-16 사용자 ①②). */
export const SPEC_KINDS = [
"coef",
"material_surcharge",
"formwork_reuse",
"rebar_complexity",
"timber_structure_class",
"masonry_slope",
"masonry_back_length",
"stone_kind",
"masonry_class",
] as const;
export type BasePriceKind = (typeof BASE_PRICE_KINDS)[number] | (typeof SPEC_KINDS)[number];
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
+32 -19
View File
@@ -15,9 +15,11 @@ import {
fetchMasterRows,
fetchOverrides,
type MasterGroup,
type BasePriceKind,
type MasterRows,
type RowsQuery,
saveBasePrice,
SPEC_KINDS,
VALUES_TABLE_ID,
} from "./Z01_MasterData_Api_Fetch";
import { valueWithUnit, type OverrideState } from "./Z01_MasterData_UI_Cells";
@@ -31,25 +33,32 @@ export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLEleme
};
const baseTitle = L("Z01_MasterData_BasePrices");
const baseGrid = el("div", { className: "z01-master__base-grid" });
for (const kind of BASE_PRICE_KINDS) {
const label = L(`Z01_MasterData_Base_${kind}` as const);
const button = el("button", {
className: "z01-master__base",
attrs: { type: "button" },
text: label,
});
button.addEventListener("click", () => {
activate(button);
view.show({
title: `${baseTitle} ${label}`,
key: `base-prices/${kind}`,
load: (query) => fetchBasePrices(kind, query),
save: (rowId, values) => saveBasePrice(kind, rowId, values),
const specTitle = L("Z01_MasterData_SpecValues");
/** 한 상자 몫 단추들 — ①얼마인가 ②얼마나 드나 로 갈라 세움(성격도 갱신 주기도 다름). */
const kindGrid = (boxTitle: string, kinds: readonly BasePriceKind[]): HTMLElement => {
const grid = el("div", { className: "z01-master__base-grid" });
for (const kind of kinds) {
const label = L(`Z01_MasterData_Base_${kind}` as const);
const button = el("button", {
className: "z01-master__base",
attrs: { type: "button" },
text: label,
});
});
baseGrid.append(button);
}
button.addEventListener("click", () => {
activate(button);
view.show({
title: `${boxTitle} ${label}`,
key: `base-prices/${kind}`,
load: (query) => fetchBasePrices(kind, query),
save: (rowId, values) => saveBasePrice(kind, rowId, values),
});
});
grid.append(button);
}
return grid;
};
const baseGrid = kindGrid(baseTitle, BASE_PRICE_KINDS);
const specGrid = kindGrid(specTitle, SPEC_KINDS);
const overridesButton = el("button", {
className: "z01-master__base z01-master__base--wide",
attrs: { type: "button" },
@@ -113,7 +122,11 @@ export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLEleme
tree.append(groupBox);
}
panel.append(section(baseTitle, baseGrid), section(L("Z01_MasterData_Tree"), tree));
panel.append(
section(baseTitle, baseGrid),
section(specTitle, specGrid),
section(L("Z01_MasterData_Tree"), tree),
);
attachCollapsible(panel);
return panel;
}
@@ -13,8 +13,8 @@
"file": "coef_2026.json",
"dataset_id": "coef",
"effective_date": "2026-01-01",
"sha256": "0bc3fe981165e8d320b7c20f4535c545f2f54415427ac60205dac4a72dbb4f19",
"size_bytes": 29413
"sha256": "daba170cd806374c92e086dec69f9559271c0317a7426e66e9dc841c2ca264bd",
"size_bytes": 31715
},
{
"file": "fx_2026-08-14.json",
@@ -787,6 +787,7 @@
]
},
"surcharge_mat": {
"used_value_at": "resources/data_material_surcharge/material_surcharge_2026-01-01.json → rates_pct — ⚠ 코드가 읽어 쓰는 값은 저기다. 이 표는 원문 보관용이라 읽는 코드가 없다(두 벌이 아니다).",
"unit": "percent",
"representation": "full_source_tables",
"duplicate_application_prohibited": true,
@@ -1108,6 +1109,8 @@
"max": 1.45,
"duplicate_group": "암괴나 호박돌이 섞인 점토",
"duplicate_index": 1,
"duplicate_same_value": true,
"duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).",
"selection": "pending"
},
{
@@ -1129,6 +1132,9 @@
"max": 1.45,
"duplicate_group": "암괴나 호박돌이 섞인 점토",
"duplicate_index": 2,
"duplicate_same_value": true,
"hidden_duplicate": true,
"duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).",
"selection": "pending"
}
]
@@ -1210,6 +1216,8 @@
"max": 0.95,
"duplicate_group": "암괴나 호박돌이 섞인 점토",
"duplicate_index": 1,
"duplicate_same_value": true,
"duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).",
"selection": "pending"
},
{
@@ -1231,6 +1239,9 @@
"max": 0.95,
"duplicate_group": "암괴나 호박돌이 섞인 점토",
"duplicate_index": 2,
"duplicate_same_value": true,
"hidden_duplicate": true,
"duplicate_reason": "원문 두 줄 · 값 같음 — 화면에는 한 줄로 보임. 원문은 그대로 둠(임의 보정 없음).",
"selection": "pending"
}
]
@@ -2,14 +2,14 @@
"schema_version": "1.0",
"dataset_id": "data_master_labels",
"effective_date": "2026-01-01",
"generated_at": "2026-09-16T11:39:55+09:00",
"generated_at": "2026-09-16T13:11:45+09:00",
"note": "마스터 자료의 **사람이 읽을 이름표**. 값이 아니라 이름만 담는다 — 여기를 고쳐도 계산은 안 바뀐다. Z01 마스터 관리 화면이 표·열 이름을 여기서 읽는다.",
"policy": {
"labels_only": "값·수식·단가를 담지 않는다. 마스터 파일은 손대지 않는다.",
"no_invented_meaning": "뜻을 모르는 열은 name_ko 를 비우고 unknown 에 사유를 적는다. 지어내지 않는다.",
"unit_is_label_only": "unit 은 이름표에서만 정한다. 자료에 없는 단위를 만들어 붙이지 않는다.",
"counting_rule": "표 = 화면이 표로 그릴 마디(줄이 여럿인 마디). 값 묶음 = 표가 아닌 마디(설명·방침·한 값짜리 요율 따위).",
"brain_kind_table": "갈래는 2026-09-15 브레인 갈래 나눔표를 그대로 따른다(로직 17 · 기초값 9 · 부산물 4 · 씨앗 1).",
"brain_kind_table": "갈래는 2026-09-15 브레인 갈래 나눔표에서 출발한다(로직 17 · 기초값 9 · 부산물 4 · 씨앗 1). 2026-09-16 사용자 판정으로 `structure_unit_observed`·`revetment_sabang` 둘을 **참고 사례**로 옮겼다 — 한 현장 관찰값·교본 설명 섞임이라 기초데이터가 아니다(로직 15 · 참고 사례 2).",
"out_of_31": "폴더 지문(_manifest.json) 셋은 브레인 31 밖이다. 부산물이 아니라 **지문** 갈래로 둔다 — 계산이 남긴 기록이 아니라 어느 판으로 셈했는지 못박는 표라 「정본 아님」 딱지가 안 맞는다(2026-09-15 브레인 ②).",
"excluded": "강우 IDF 캐시(resources/data_rainfall_idf_cache, 96 파일)는 배수용이라 뺀다.",
"merged_tables_rule": "`merged_tables` 는 **기초단가 다섯**(노임·자재·유가·제비율·기계)을 화면이 한 표씩으로 보이려고 여러 원본 표를 모은 자리다. ⚠ **값을 옮기지 않는다** — 열 이름·단위·숨김과 `editable`(고칠 수 있나)만 담는다. `editable: false` 는 다른 값에서 나온 계산값이라 사람이 못 고친다. 기계는 한 표로 안 모이므로 `machine_cost_chain` 이 사슬을 적는다 (2026-09-15 사용자 지시 · 브레인).",
@@ -45,6 +45,10 @@
"name_ko": "지문",
"summary": "어느 판으로 셈했는지 못박는 자리. 값도 규칙도 아니고 **판을 가리키는 표**다 — 고치지 않는다."
},
"reference": {
"name_ko": "참고 사례",
"summary": "한 현장·한 교본에서 본 값. ⚠ 표준이 아니다 — 근거로 보되 기초데이터처럼 갈아 끼우지 않는다."
},
"seed": {
"name_ko": "씨앗",
"summary": "앞으로 늘려 갈 첫 벌. 모양을 잡아 두려고 먼저 놓은 것이다."
@@ -766,6 +770,433 @@
"note": "못 채운 성분이 있으면 그 사실을 적는 자리 — 0 으로 안 때운다."
}
]
},
{
"key": "coef",
"name_ko": "토량환산계수",
"summary": "판 흙이 부풀고(L) 다진 흙이 줄어드는(C) 비. 토질마다 하한·상한.",
"sources": [
"coef::variables/coef_soil_L/records",
"coef::variables/coef_soil_C/records"
],
"rows_note": "L 16 + C 16 = 32 줄(암괴 중복 두 줄은 한 줄로 합침)",
"columns": [
{
"key": "table",
"name_ko": "어느 표",
"unit": "",
"visible": true,
"note": "`coef_soil_L`(부풂) · `coef_soil_C`(다짐). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "soil_type",
"name_ko": "토질",
"unit": "",
"visible": true,
"note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "min",
"name_ko": "하한",
"unit": "",
"visible": true
},
{
"key": "max",
"name_ko": "상한",
"unit": "",
"visible": true
},
{
"key": "rule",
"name_ko": "별도 규정",
"unit": "",
"visible": true,
"note": "원문이 값 대신 「별도 설계」라 적은 자리."
},
{
"key": "selection",
"name_ko": "채택 여부",
"unit": "",
"visible": true,
"note": "원문이 같은 토질을 두 줄로 실어 어느 줄을 쓸지 아직 안 고른 자리(`pending`)."
},
{
"key": "source_rows",
"name_ko": "원문 줄 수",
"unit": "줄",
"visible": true,
"note": "⭐ 한 줄로 보이지만 원문에는 몇 줄이었나 — 암괴 중복 쌍이 `2` 다."
},
{
"key": "duplicate_note",
"name_ko": "합친 사유",
"unit": "",
"visible": true,
"note": "⭐ 「원문 두 줄 · 값 같음 — 한 줄로 보임」. 원문은 그대로 두고 화면만 접었다는 뜻."
}
]
},
{
"key": "material_surcharge",
"name_ko": "재료 할증률",
"summary": "자재마다 몇 %를 더 사는지. 조건이 갈리면 다른 조건 칸에 함께 둔다.",
"sources": [
"material_surcharge::rates_pct"
],
"rows_note": "19 줄",
"columns": [
{
"key": "material",
"name_ko": "자재명",
"unit": "",
"visible": true,
"note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "rate",
"name_ko": "할증률",
"unit": "%",
"visible": true
},
{
"key": "condition",
"name_ko": "조건",
"unit": "",
"visible": true
},
{
"key": "alt_rate",
"name_ko": "다른 조건 할증률",
"unit": "%",
"visible": true
},
{
"key": "alt_condition",
"name_ko": "다른 조건",
"unit": "",
"visible": true
},
{
"key": "pumsem",
"name_ko": "어느 품셈",
"unit": "",
"visible": true,
"note": "`forest`(산림 · 1차) · `construction`(건설 · 보완)."
}
]
},
{
"key": "formwork_reuse",
"name_ko": "거푸집 사용횟수",
"summary": "구조물 갈래마다 거푸집을 몇 번 쓰는지, 횟수마다 기준수량의 몇 %인지.",
"sources": [
"formwork_reuse::reuse_by_class",
"formwork_reuse::reuse_ratio_pct",
"formwork_reuse::euroform_type/classes"
],
"rows_note": "구조 갈래 4 + 사용횟수 비율 12(합판·각재 × 6) + 유로폼 갈래 3 = 19 줄",
"columns": [
{
"key": "table",
"name_ko": "어느 표",
"unit": "",
"visible": true,
"note": "`reuse_by_class`(갈래별 횟수) · `reuse_ratio_pct`(횟수별 비율) · `euroform_type/classes`(유로폼 갈래). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "class",
"name_ko": "구조 갈래",
"unit": "",
"visible": true,
"note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "reuse_count",
"name_ko": "사용횟수",
"unit": "회",
"visible": true,
"note": "비율표에서는 ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "examples",
"name_ko": "원문 예시",
"unit": "",
"visible": true
},
{
"key": "material",
"name_ko": "거푸집 재료",
"unit": "",
"visible": true,
"note": "`plywood`(합판) · `timber`(각재). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "ratio_pct",
"name_ko": "기준수량 비율",
"unit": "%",
"visible": true,
"note": "⭐ 품셈 12-4 — 그 횟수로 쓸 때 기준수량의 몇 %인지. 일위대가 재료비에 걸린다."
},
{
"key": "daily_area_m2",
"name_ko": "1일 시공량",
"unit": "㎡",
"visible": true,
"note": "유로폼 설치·해체 품이 갈리는 값."
}
]
},
{
"key": "rebar_complexity",
"name_ko": "철근 가공·조립 갈래",
"summary": "간단·보통·복잡·매우복잡 갈래와 갈래별 단가 참고값.",
"sources": [
"rebar_complexity::classes",
"rebar_complexity::price_hint_krw_per_ton"
],
"rows_note": "갈래 4 + 단가 참고값 4 = 8 줄",
"columns": [
{
"key": "table",
"name_ko": "어느 표",
"unit": "",
"visible": true,
"note": "`classes`(갈래) · `price_hint_krw_per_ton`(단가 참고값). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "class",
"name_ko": "갈래",
"unit": "",
"visible": true,
"note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "examples",
"name_ko": "원문 예시",
"unit": "",
"visible": true,
"note": "품셈 12-3 [주]① 이 든 구조물 보기."
},
{
"key": "price_krw_per_ton",
"name_ko": "단가 참고값",
"unit": "원/ton",
"visible": true,
"note": "⚠ **표시 전용** — 갈래 차이를 사람이 보라고 둔 값이다. 계산에 안 들어간다."
}
]
},
{
"key": "timber_structure_class",
"name_ko": "목재공작물 구조 갈래",
"summary": "갈래마다 목재 채적 1㎥ 에 드는 건축목공·보통인부.",
"sources": [
"timber_structure_class::classes"
],
"rows_note": "6 줄",
"columns": [
{
"key": "class",
"name_ko": "갈래",
"unit": "",
"visible": true,
"note": "보통구조 하·중·상 · 상등구조 하·중·상. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "carpenter",
"name_ko": "건축목공",
"unit": "인/㎥",
"visible": true
},
{
"key": "laborer",
"name_ko": "보통인부",
"unit": "인/㎥",
"visible": true
},
{
"key": "examples",
"name_ko": "원문 예시",
"unit": "",
"visible": true
}
]
},
{
"key": "masonry_slope",
"name_ko": "돌쌓기 표준경사",
"summary": "쌓기 방식 × 성절토 × 직고 구간마다 전면 기울기(1:n).",
"sources": [
"masonry_slope::table"
],
"rows_note": "메찰 2 × 성절토 2 × 직고 5 = 20 줄",
"columns": [
{
"key": "bond",
"name_ko": "쌓기 방식",
"unit": "",
"visible": true,
"note": "메쌓기 · 찰쌓기. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "face",
"name_ko": "성토·절토",
"unit": "",
"visible": true,
"note": "⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "height_bracket",
"name_ko": "직고 구간",
"unit": "m",
"visible": true,
"note": "⚠ 원문 표기 그대로(`~1.5`·`~3`·`~5`·`~7`·`7이상`) — 예쁘게 고치면 열쇠가 바뀐다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "face_slope_ratio",
"name_ko": "전면 기울기",
"unit": "1:n",
"visible": true,
"note": "코드가 늘 쓰던 0.3 은 이 표의 「메쌓기·성토·~1.5」 한 칸일 뿐이었다."
}
]
},
{
"key": "masonry_back_length",
"name_ko": "돌쌓기 뒷길이 표준",
"summary": "쌓기 방식 × 직고 구간마다 뒷길이 하한·상한(㎝).",
"sources": [
"masonry_back_length::table_cm"
],
"rows_note": "메찰 2 × 직고 5 = 10 줄",
"columns": [
{
"key": "bond",
"name_ko": "쌓기 방식",
"unit": "",
"visible": true,
"note": "메쌓기 · 찰쌓기. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "height_bracket",
"name_ko": "직고 구간",
"unit": "m",
"visible": true,
"note": "⚠ 원문 표기 그대로(`~1.5`·`~3`·`~5`·`~7`·`7이상`) — 예쁘게 고치면 열쇠가 바뀐다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "min_cm",
"name_ko": "뒷길이 하한",
"unit": "㎝",
"visible": true,
"note": "⭐ 안 고르면 이 값이 제안된다(범위 안에 드는 품셈 규격 중 가장 작은 것)."
},
{
"key": "max_cm",
"name_ko": "뒷길이 상한",
"unit": "㎝",
"visible": true,
"note": "원문이 상한을 안 준 칸은 빈다."
}
]
},
{
"key": "stone_kind",
"name_ko": "돌 종류별 소요량",
"summary": "돌 종류 × 뒷길이마다 고임돌·채움 콘크리트 원단위와 뒤채움 몫.",
"sources": [
"stone_kind::wedge_stone_m3_per_m2",
"stone_kind::fill_concrete_m3_per_m2",
"stone_kind::backfill_ratio_of_back_length",
"stone_kind::fallback"
],
"rows_note": "고임돌 28 + 채움 28 + 뒤채움 몫 4 + 미지정일 때 15 = 75 줄",
"columns": [
{
"key": "table",
"name_ko": "어느 표",
"unit": "",
"visible": true,
"note": "`wedge_stone_m3_per_m2`(고임돌) · `fill_concrete_m3_per_m2`(채움 콘크리트) · `backfill_ratio_of_back_length`(뒤채움 몫) · `fallback/…`(돌 종류 미지정일 때). ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "stone_kind",
"name_ko": "돌 종류",
"unit": "",
"visible": true,
"note": "야면석·호박돌 / 깬잡석 / 깬돌 / 견치돌. 미지정 벌에는 이 칸이 없다. ⚠ 줄을 가리는 축 — 못 고침. 바꾸면 고친 값이 주인을 잃는다."
},
{
"key": "back_length_cm",
"name_ko": "뒷길이",
"unit": "㎝",
"visible": true,
"note": "25·30·35·45·55·60·75 . . . ."
},
{
"key": "m3_per_m2",
"name_ko": "",
"unit": "/",
"visible": true,
"note": "· . - 0 ."
},
{
"key": "ratio",
"name_ko": " ",
"unit": "",
"visible": true,
"note": " . ."
}
]
},
{
"key": "masonry_class",
"name_ko": " ",
"summary": " · · .",
"sources": [
"masonry_class::back_length/classes",
"masonry_class::boulder_diameter",
"masonry_class::face_slope"
],
"rows_note": " 3 + 3 + 3 = 9 ",
"columns": [
{
"key": "table",
"name_ko": " ",
"unit": "",
"visible": true,
"note": "`back_length/classes`( ) · `boulder_diameter`( ) · `face_slope`( ). . ."
},
{
"key": "class",
"name_ko": "",
"unit": "",
"visible": true,
"note": " `35cm` , `40~60` . (`~1.5`·`~3`·`~5`·`~7`·`7`) . . ."
},
{
"key": "max_cm",
"name_ko": "",
"unit": "",
"visible": true,
"note": " ."
},
{
"key": "type_id",
"name_ko": " ",
"unit": "",
"visible": true,
"note": "`masonry_wet`·`masonry_dry`·`boulder_masonry`. . ."
},
{
"key": "face_slope_ratio",
"name_ko": " ",
"unit": "1:n",
"visible": true,
"note": " . 0.3 ."
}
]
}
],
"machine_cost_chain": {
@@ -993,6 +1424,99 @@
"machine_code"
],
"checked": " 613 · 387 · 214 · 92 ** 0**(). ."
},
{
"table": "coef",
"name_ko": "",
"verdict": "( )",
"key": [
"table",
"soil_type"
],
"checked": "L·C `table` . . `source_rows`·`duplicate_note` ."
},
{
"table": "material_surcharge",
"name_ko": " ",
"verdict": "",
"key": [
"material"
],
"checked": " 19 0(). ."
},
{
"table": "formwork_reuse",
"name_ko": " ",
"verdict": "( )",
"key": [
"table",
"class",
"material",
"reuse_count"
],
"checked": " `table+class`, `table+material+reuse_count` . ."
},
{
"table": "rebar_complexity",
"name_ko": " ",
"verdict": "( )",
"key": [
"table",
"class"
],
"checked": " `table` ."
},
{
"table": "timber_structure_class",
"name_ko": " ",
"verdict": "",
"key": [
"class"
],
"checked": " 6 0()."
},
{
"table": "masonry_slope",
"name_ko": " ",
"verdict": " ",
"key": [
"bond",
"face",
"height_bracket"
],
"checked": "id . × × . `~1.5`·`~3`·`~5`·`~7`·`7` ."
},
{
"table": "masonry_back_length",
"name_ko": " ",
"verdict": " ",
"key": [
"bond",
"height_bracket"
],
"checked": "id . × . ."
},
{
"table": "stone_kind",
"name_ko": " ",
"verdict": " ",
"key": [
"table",
"stone_kind",
"back_length_cm"
],
"checked": "id . × × ** ** , ."
},
{
"table": "masonry_class",
"name_ko": " ",
"verdict": " ",
"key": [
"table",
"class",
"type_id"
],
"checked": "id . · `table+class`, `table+type_id`. `40~60` ."
}
],
"null_is_real": {
@@ -1086,15 +1610,15 @@
}
},
"counts": {
"merged_tables": 5,
"merged_columns": 89,
"merged_tables": 14,
"merged_columns": 136,
"files": 34,
"tables": 91,
"columns": 456,
"value_groups": 173,
"columns": 462,
"value_groups": 175,
"unknown": 0,
"value_groups_by_kind": {
"doc": 107,
"doc": 109,
"value": 66
}
},
@@ -1484,6 +2008,24 @@
"name_ko": "채택 여부",
"unit": "",
"visible": true
},
{
"key": "duplicate_same_value",
"name_ko": "값 같은 중복",
"unit": "",
"visible": true
},
{
"key": "duplicate_reason",
"name_ko": "중복 사유",
"unit": "",
"visible": true
},
{
"key": "hidden_duplicate",
"name_ko": "접힌 줄",
"unit": "",
"visible": false
}
]
},
@@ -1529,6 +2071,24 @@
"name_ko": "채택 여부",
"unit": "",
"visible": true
},
{
"key": "duplicate_same_value",
"name_ko": "값 같은 중복",
"unit": "",
"visible": true
},
{
"key": "duplicate_reason",
"name_ko": "중복 사유",
"unit": "",
"visible": true
},
{
"key": "hidden_duplicate",
"name_ko": "접힌 줄",
"unit": "",
"visible": false
}
]
},
@@ -1665,6 +2225,12 @@
"name_ko": "값 단위",
"summary": "할증은 %다.",
"kind": "value"
},
{
"key": "variables/surcharge_mat/used_value_at",
"name_ko": "쓰는 값이 있는 자리",
"summary": "이 표는 원문 보관용이라 읽는 코드가 없다. 값은 `material_surcharge` 의 `rates_pct` 를 읽는다.",
"kind": "doc"
}
]
},
@@ -1827,6 +2393,12 @@
"summary": "실무 집계에서 본 할증률. 참고이지 기본값이 아니다.",
"kind": "value"
},
{
"key": "original_tables",
"name_ko": "원문 표가 실린 자리",
"summary": "같은 값이 `coef` 의 `surcharge_mat` 에도 있다. ⚠ 두 벌이 아니다 — 저쪽은 원문 보관이고 읽어 쓰는 정본은 여기다.",
"kind": "doc"
},
{
"key": "policy",
"name_ko": "방침",
@@ -2562,9 +3134,9 @@
{
"file_id": "revetment_sabang",
"path": "resources/data_masonry/revetment_sabang_2026-01-01.json",
"name_ko": "기슭막이 치수(사방)",
"summary": "독립 기슭막이의 높이·계획비탈·둑마루 두께·뒷채움을 한 자리에 둔 벌.",
"kind": "logic",
"name_ko": "기슭막이 치수(사방) — 참고 사례",
"summary": "사방기술교본 값에 설명 글이 섞인 벌. ⚠ 기초데이터가 아니다 — 표준값처럼 쓰지 말고 근거로만 본다(2026-09-16 사용자 판정).",
"kind": "reference",
"tables": [],
"value_groups": [
{
@@ -2596,9 +3168,9 @@
{
"file_id": "structure_unit_observed",
"path": "resources/data_structure_unit/structure_unit_observed_2026-01-01.json",
"name_ko": "구조물 관측 원단위",
"summary": "품셈에 표준 물량표가 없는 콘크리트 구조물을 실무 설계원본에서 뽑은 벌.",
"kind": "logic",
"name_ko": "구조물 관측 원단위 — 참고 사례",
"summary": "울진소광 **한 현장**을 관찰한 값이다. ⚠ 기초데이터가 아니다 — 규격이 다르면 안 맞고 표준도 아니다(2026-09-16 사용자 판정).",
"kind": "reference",
"tables": [
{
"key": "double_count_rules/rules",
@@ -6320,6 +6892,16 @@
"unit": "",
"visible": true
},
"duplicate_reason": {
"name_ko": "중복 사유",
"unit": "",
"visible": true
},
"duplicate_same_value": {
"name_ko": "값 같은 중복",
"unit": "",
"visible": true
},
"duration_bracket": {
"name_ko": "공사기간 구간",
"unit": "",
@@ -6470,6 +7052,11 @@
"unit": "",
"visible": true
},
"hidden_duplicate": {
"name_ko": "접힌 줄",
"unit": "",
"visible": false
},
"hours_per_day": {
"name_ko": "1일 근로시간",
"unit": "시간",
@@ -7479,6 +8066,10 @@
"name_ko": "값 단위",
"summary": "할증은 %다."
},
"coef::variables/surcharge_mat/used_value_at": {
"name_ko": "쓰는 값이 있는 자리",
"summary": "이 표는 원문 보관용이라 읽는 코드가 없다. 값은 `material_surcharge` 의 `rates_pct` 를 읽는다."
},
"formwork_reuse::reuse_ratio_pct": {
"name_ko": "사용횟수별 기준수량 비율",
"summary": "품셈 12-4 — 일위대가 재료비에 걸린다. B08 이 곱하면 이중계상이다."
@@ -7599,6 +8190,10 @@
"name_ko": "실무 관측",
"summary": "실무 집계에서 본 할증률. 참고이지 기본값이 아니다."
},
"material_surcharge::original_tables": {
"name_ko": "원문 표가 실린 자리",
"summary": "같은 값이 `coef` 의 `surcharge_mat` 에도 있다. ⚠ 두 벌이 아니다 — 저쪽은 원문 보관이고 읽어 쓰는 정본은 여기다."
},
"oil::variables/oil_diesel": {
"name_ko": "경유 단가",
"summary": "전국평균 경유 값(원/L)."
@@ -2,11 +2,12 @@
"schema_version": "1.0",
"dataset_id": "data_master_sources",
"effective_date": "2026-09-15",
"generated_at": "2026-09-15T21:59:23+09:00",
"generated_at": "2026-09-16T14:05:40+09:00",
"note": "기초단가 자료가 **어디서 오고 얼마 만에 바뀌는지** 적은 표. 파일 안에는 제 판만 있고 「최신이 무엇인지·어디서 받는지」가 없어 건설 노임이 반 년 뒤처진 것을 아무도 몰랐다(2026-09-15). 그 자리를 메운다.",
"policy": {
"values_untouched": "값을 담지 않는다. 자료가 어디서 오는지만 담는다.",
"our_edition_is_read": "`our_edition` 은 **실제 파일에서 읽는다**(`effective_date`, 파생본은 `derived_from.effective_date`). 손으로 적으면 갱신하고 안 고쳐 또 뒤처진다.",
"address_found_on": "받는 자리 일곱은 2026-09-16 에 안티그래비티가 찾아 채웠다(제조노임·기계·기계 운전경비·자재·유가 전국·유가 지역·요율). 건설노임 한 줄은 그 전(2026-09-15)에 브레인이 준 것이다.",
"no_invented_address": "받는 자리는 **받은 것만** 적는다. 모르면 빈 칸으로 두고 사유를 단다 — 주소를 지어내지 않는다.",
"status_rule": "`latest_published` 가 `our_edition` 보다 뒤면 **뒤처짐**, 같으면 최신, 둘 가운데 하나라도 없으면 **모름**. 날마다 바뀌는 유가는 날짜 차이로 보여야 한다."
},
@@ -65,19 +66,18 @@
"kind_ko": "노임",
"name_ko": "중소제조업 직종별 임금",
"publisher": "중소기업중앙회",
"where_to_get": "",
"where_to_get": "중소기업중앙회 https://www.kbiz.or.kr | [정보마당] > [조사연구통계](게시판 mnSeq=325) > 「중소제조업 직종별 임금조사보고서」 PDF",
"cycle": "반기",
"latest_published": "",
"latest_basis": "",
"our_edition": "2026-07-01",
"our_edition_from": "effective_date",
"status": "unknown",
"checked_on": "2026-09-15",
"note": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다. 우리 판은 2026 상반기 조사(조사월 2026-03 · 공표 2026-06-30)다.",
"checked_on": "2026-09-16",
"note": "우리 판은 2026 상반기 조사(조사월 2026-03 · 공표 2026-06-30)다. ⚠ 게시판에 반기마다 새 보고서가 올라오므로 **최신 공표일은 게시판에서 봐야** 한다.",
"files": [
"resources/data_cost_input_value/labor_mfg_2026-07-01.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.",
"latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
@@ -86,19 +86,18 @@
"kind_ko": "기계",
"name_ko": "건설공사 표준품셈 제8장 건설기계",
"publisher": "국토교통부",
"where_to_get": "",
"where_to_get": "공사비원가관리센터 https://cost.kict.re.kr | [표준품셈] (또는 CODIL https://www.codil.or.kr [건설기준정보] > [건설공사표준품셈]) > 건설공사 표준품셈 원문 PDF(공통부문 제8장 건설기계 8-3)",
"cycle": "해마다",
"latest_published": "2026-01-01",
"latest_basis": "2026-09-15 브레인 쪽지",
"our_edition": "2026-01-01",
"our_edition_from": "effective_date",
"status": "current",
"checked_on": "2026-09-15",
"checked_on": "2026-09-16",
"note": "우리 판이 최신이다. 취득가·손료계수·연료소모량·조종원이 다 여기서 온다.",
"files": [
"resources/data_cost_input_value/mach_base_2026.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다."
]
},
{
"source_id": "machine_operating",
@@ -106,19 +105,18 @@
"kind_ko": "기계",
"name_ko": "건설기계 운전경비(품셈 8-4 파생본)",
"publisher": "국토교통부",
"where_to_get": "",
"where_to_get": "공사비원가관리센터 https://cost.kict.re.kr | 별도 배포처 없음(위 건설공사 표준품셈 원문과 같음) > 공통부문 제8장 건설기계 「8-4 건설기계 운전경비 산정」 표",
"cycle": "해마다 — 위 품셈을 따라간다",
"latest_published": "2026-01-01",
"latest_basis": "파생 원본(mach_base)의 판",
"our_edition": "2026-01-01",
"our_edition_from": "derived_from.effective_date",
"status": "current",
"checked_on": "2026-09-15",
"checked_on": "2026-09-16",
"note": "⚠ **판을 모르는 파일이 아니다** — 파일 안 `derived_from.effective_date` 에 2026-01-01 이 박혀 있다(건설품셈 8-4 운전경비 파생본 · 원본 지문까지 함께). `mach_fuel_rate`·`mach_operator_map` 이 다 차면 걷어낼 자리다.",
"files": [
"resources/data_cost_machine_operating/machine_operating_2026.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다."
]
},
{
"source_id": "mat_price_public",
@@ -126,19 +124,18 @@
"kind_ko": "자재",
"name_ko": "관급자재 단가(시설공통자재)",
"publisher": "조달청",
"where_to_get": "",
"where_to_get": "나라장터 https://www.g2b.go.kr | [가격정보] > [시설공통자재] (또는 조달청 https://www.pps.go.kr [정보제공] > [업무별자료] > [시설공사]) > 시설공통자재 가격조사 엑셀",
"cycle": "반기 + 수시",
"latest_published": "",
"latest_basis": "",
"our_edition": "2026-08-14",
"our_edition_from": "effective_date",
"status": "unknown",
"checked_on": "2026-09-15",
"checked_on": "2026-09-16",
"note": "수시 공고라 「최신 공표일」이 한 날로 안 정해진다 — 받은 날(우리 판)이 곧 기준이다. 철근·레미콘·아스콘은 이 벌에 없다(쇼핑몰 스냅숏 미확보).",
"files": [
"resources/data_cost_input_value/mat_price_public_2026-08-14.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.",
"latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
@@ -147,19 +144,18 @@
"kind_ko": "유가",
"name_ko": "유가 — 전국평균",
"publisher": "한국석유공사(오피넷)",
"where_to_get": "",
"where_to_get": "오피넷 https://www.opinet.co.kr | [국내유가통계] > [주유소] > [평균판매가격] — 「전국」 탭 조회 뒤 [엑셀저장](API avgAllPrice.do)",
"cycle": "주간 · 일별",
"latest_published": "",
"latest_basis": "",
"our_edition": "2026-08-14",
"our_edition_from": "effective_date",
"status": "unknown",
"checked_on": "2026-09-15",
"checked_on": "2026-09-16",
"note": "날마다 바뀌는 값이라 「최신」이 곧 오늘이다. **뒤처짐을 날짜 차이로 보여야 하는 자료**다. ⚠ 시도별 판과 「전국」 줄이 겹치는데 값도 기준일도 다르다(어느 쪽을 쓸지 판정 대기).",
"files": [
"resources/data_cost_input_value/oil_2026-08-14.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.",
"latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
@@ -168,19 +164,18 @@
"kind_ko": "유가",
"name_ko": "유가 — 시도별",
"publisher": "한국석유공사(오피넷)",
"where_to_get": "",
"where_to_get": "오피넷 https://www.opinet.co.kr | [국내유가통계] > [주유소] > [평균판매가격] — 「지역별」 탭에서 시도를 고른 뒤 [엑셀저장](API avgSidoPrice.do)",
"cycle": "주간 · 일별",
"latest_published": "",
"latest_basis": "",
"our_edition": "2026-09-09",
"our_edition_from": "effective_date",
"status": "unknown",
"checked_on": "2026-09-15",
"checked_on": "2026-09-16",
"note": "품셈 8-1-7 5호 「유류가격은 해당지역의 가격으로 한다」를 따르려고 둔 벌이다.",
"files": [
"resources/data_cost_input_value/oil_regional_2026-09-09.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.",
"latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
@@ -189,19 +184,18 @@
"kind_ko": "요율",
"name_ko": "토목공사 원가계산 제비율 적용기준",
"publisher": "조달청",
"where_to_get": "",
"where_to_get": "조달청 https://www.pps.go.kr | [정보제공] > [업무별자료] > [시설공사] > 「시설공사 원가계산 간접공사비(제비율) 적용기준」 토목 엑셀(재게시 kseis https://www.kseis.co.kr 자료실)",
"cycle": "해마다 + 수시(법이 바뀌면)",
"latest_published": "2026-04-13",
"latest_basis": "2026-09-15 브레인 쪽지",
"our_edition": "2026-04-13",
"our_edition_from": "effective_date",
"status": "current",
"checked_on": "2026-09-15",
"checked_on": "2026-09-16",
"note": "우리 판이 최신이다. 국민연금 요율은 국민연금법(현행 2026-06-17)을 따로 물었다. ⚠ 요율은 **법이 바뀌면 주기와 상관없이** 바뀐다 — 주기만 보고 안심하면 안 된다.",
"files": [
"resources/data_cost_input_value/rates_2026.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다."
]
}
]
}
@@ -6,8 +6,8 @@
"files": [
{
"file": "material_surcharge_2026-01-01.json",
"sha256": "30a1e11acb6f6bd52c72e0ff471c96f772917858306ea61f66ecbcb5df433c9f",
"size_bytes": 5087
"sha256": "1394a39be22ac0eb7476e4bc2b81c1e23c365a4ad5b915befe85d3f0f99023d5",
"size_bytes": 5344
}
]
}
@@ -1,6 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "material_surcharge",
"original_tables": "resources/data_cost_input_value/coef_2026.json → variables/surcharge_mat — 원문 표가 실린 자리. ⚠ 값이 겹치지만 이원화가 아니다: 저쪽은 원문 보관이고 읽어 쓰는 정본은 여기(rates_pct)다.",
"effective_date": "2026-01-01",
"source": {
"primary": {
@@ -152,3 +152,60 @@ def test_단위가_붙어야_할_열에_단위가_있다(shown):
if units.get(key) != unit:
bad.append(f"{kind}/{key}: {units.get(key)!r}{unit!r}")
assert not bad, bad
def test_줄_열쇠가_서버_축과_같다(labels):
"""⭐ 덮개가 줄을 붙드는 끈 — 서버가 줄마다 `@axis` 로 내는 축이 곧 열쇠다.
이름표가 축을 빠뜨리면 고친 값이 갱신 주인을 잃는다.
"""
declared = {r["table"]: set(r["key"]) for r in labels["row_keys"]}
bad = []
for kind in base_prices.KINDS:
axes = set()
for row in base_prices.rows(kind):
axes |= set(row.get("@axis") or ())
if not axes: # 다섯 표는 `@id` 한 칸이라 축을 안 낸다
continue
missing = axes - declared.get(kind, set())
if missing:
bad.append(f"{kind}: 이름표에 없는 축 {sorted(missing)}")
assert not bad, bad
def test_구간_이름은_원문_표기_그대로다():
"""⚠ `~1.5`·`7이상` 을 예쁘게 고치면 줄 열쇠가 바뀌어 덮개가 주인을 잃는다."""
for kind in ("masonry_slope", "masonry_back_length"):
brackets = {str(r.get("height_bracket")) for r in base_prices.rows(kind)}
assert brackets == {"~1.5", "~3", "~5", "~7", "7이상"}, (kind, sorted(brackets))
classes = {
str(r.get("class"))
for r in base_prices.rows("masonry_class")
if r.get("table") == "boulder_diameter"
}
assert classes == {"40~60", "60~80", "80~100"}, sorted(classes)
#: ⚠ **줄마다 축이기도 값이기도 한 칸** — `formwork_reuse.reuse_count` 는
#: `reuse_by_class` 줄에서는 **고치는 값**(갈래별 사용횟수)이고
#: `reuse_ratio_pct` 줄에서는 **축**(몇 회째인가)이다. 서버는 열 하나에 한 판정만 내므로
#: 지금은 고칠 수 있는 칸으로 서 있다. 비율표 줄에서 이 칸을 고치면 **다른 줄로 옮겨 간다**.
#: 2026-09-16 크로스체크에서 드러났고 판정 대기 — 고치기 전까지 여기 예외로 적어 둔다.
AXIS_ALSO_VALUE = {"formwork_reuse/reuse_count"}
def test_축_칸은_못_고친다():
"""축을 고치면 줄이 다른 줄이 된다 — 서버가 잠가야 한다."""
bad = []
for kind in base_prices.KINDS:
rows = base_prices.rows(kind)
axes = set()
for row in rows:
axes |= set(row.get("@axis") or ())
if not axes:
continue
meta = base_prices.table(kind, page=1, size=1)
for axis in axes:
if axis in meta["editable"] and f"{kind}/{axis}" not in AXIS_ALSO_VALUE:
bad.append(f"{kind}/{axis} 가 고칠 수 있는 칸으로 섰다")
assert not bad, bad
+23 -4
View File
@@ -19,7 +19,9 @@ ROOT = Path(__file__).resolve().parents[2]
LABELS_PATH = ROOT / "resources" / "data_master_labels" / "labels_2026-01-01.json"
#: 브레인 갈래 나눔표(2026-09-15). 폴더 장부(_manifest)는 그 31 밖이라 따로 센다.
BRAIN_KIND_COUNTS = {"logic": 17, "base_value": 9, "byproduct": 4, "seed": 1}
#: 2026-09-16 사용자 판정 — `structure_unit_observed`(울진소광 한 현장 관찰값)와
#: `revetment_sabang`(교본값에 설명 글 섞임)은 기초데이터가 아니라 **참고 사례**다.
BRAIN_KIND_COUNTS = {"logic": 15, "base_value": 9, "byproduct": 4, "seed": 1, "reference": 2}
MANIFEST_PREFIX = "manifest_"
META_KEYS = {
@@ -449,12 +451,29 @@ def test_미결은_미판정으로_표시된다(labels):
assert column["name_ko"] == "미판정"
MERGED_KEYS = ("labor", "material", "oil", "rate", "machine")
#: 2026-09-16 사용자 판정 — 기초데이터 아홉이 더해져 열넷.
#: 잣대 = 계산되는 값이 아니라 **그냥 값만으로 정의되는 데이터셋**.
MERGED_KEYS = (
"labor",
"material",
"oil",
"rate",
"machine",
"coef",
"material_surcharge",
"formwork_reuse",
"rebar_complexity",
"timber_structure_class",
"masonry_slope",
"masonry_back_length",
"stone_kind",
"masonry_class",
)
def test_기초단가_다섯이_다_있다(labels):
def test_기초데이터_열넷이_다_있다(labels):
merged = {m["key"]: m for m in labels["merged_tables"]}
assert tuple(merged) == MERGED_KEYS
assert set(merged) == set(MERGED_KEYS)
for entry in merged.values():
assert entry["name_ko"].strip()
assert entry["summary"].strip()
+9 -1
View File
@@ -49,7 +49,15 @@ def test_갈래는_이름표_차례_파일_34_강우_캐시는_없음(client: Te
groups = _tree(client)["groups"]
assert [g["key"] for g in groups] == [k for k in LABELS["kinds"]]
counts = {g["key"]: len(g["files"]) for g in groups}
assert counts == {"logic": 17, "base_value": 9, "byproduct": 4, "fingerprint": 3, "seed": 1}
# 2026-09-16 사용자 판정 — 한 현장 관찰값·교본 설명 섞임 둘은 참고 사례로 옮겼다.
assert counts == {
"logic": 15,
"base_value": 9,
"byproduct": 4,
"reference": 2,
"fingerprint": 3,
"seed": 1,
}
assert {g["key"]: g["label"] for g in groups}["base_value"] == "기초값"
by_id = {f["file_id"]: f for f in LABELS["files"]}
for f in (f for g in groups for f in g["files"]):
+11
View File
@@ -147,6 +147,17 @@ export const ui_locales_b3 = {
Z01_MasterData_Base_material: ["자재단가", "Material"],
Z01_MasterData_Base_oil: ["유가", "Fuel"],
Z01_MasterData_Base_rate: ["요율", "Rates"],
/* ② 품셈 기준 아홉 — 얼마나 드나(품셈이 정하고 해마다 한 번 개정) */
Z01_MasterData_SpecValues: ["품셈 기준", "Standard rates"],
Z01_MasterData_Base_coef: ["토량환산", "Soil conversion"],
Z01_MasterData_Base_material_surcharge: ["자재할증", "Material surcharge"],
Z01_MasterData_Base_formwork_reuse: ["거푸집", "Formwork reuse"],
Z01_MasterData_Base_rebar_complexity: ["철근", "Rebar"],
Z01_MasterData_Base_timber_structure_class: ["목구조", "Timber structure"],
Z01_MasterData_Base_masonry_slope: ["돌쌓기 경사", "Masonry slope"],
Z01_MasterData_Base_masonry_back_length: ["뒷길이", "Back length"],
Z01_MasterData_Base_stone_kind: ["돌종류", "Stone kind"],
Z01_MasterData_Base_masonry_class: ["갈래", "Masonry class"],
Z01_MasterData_Overrides: ["고친 것 모아 보기", "Edited values"],
Z01_MasterData_Edit_Can: [
"고칠 수 있는 칸 — 눌러 고치고 Enter 로 저장 (Esc 취소)",