Merge remote-tracking branch 'origin/sub_desktop_1' into main_laptop_1

This commit is contained in:
2026-09-15 22:00:33 +09:00
2 changed files with 335 additions and 0 deletions
@@ -0,0 +1,207 @@
{
"schema_version": "1.0",
"dataset_id": "data_master_sources",
"effective_date": "2026-09-15",
"generated_at": "2026-09-15T21:59:23+09:00",
"note": "기초단가 자료가 **어디서 오고 얼마 만에 바뀌는지** 적은 표. 파일 안에는 제 판만 있고 「최신이 무엇인지·어디서 받는지」가 없어 건설 노임이 반 년 뒤처진 것을 아무도 몰랐다(2026-09-15). 그 자리를 메운다.",
"policy": {
"values_untouched": "값을 담지 않는다. 자료가 어디서 오는지만 담는다.",
"our_edition_is_read": "`our_edition` 은 **실제 파일에서 읽는다**(`effective_date`, 파생본은 `derived_from.effective_date`). 손으로 적으면 갱신하고 안 고쳐 또 뒤처진다.",
"no_invented_address": "받는 자리는 **받은 것만** 적는다. 모르면 빈 칸으로 두고 사유를 단다 — 주소를 지어내지 않는다.",
"status_rule": "`latest_published` 가 `our_edition` 보다 뒤면 **뒤처짐**, 같으면 최신, 둘 가운데 하나라도 없으면 **모름**. 날마다 바뀌는 유가는 날짜 차이로 보여야 한다."
},
"kinds": {
"labor": "노임",
"machine": "기계",
"material": "자재",
"oil": "유가",
"rate": "요율"
},
"status": {
"behind": {
"name_ko": "뒤처짐",
"summary": "최신 공표일이 우리 판보다 뒤다 — 받아야 한다."
},
"current": {
"name_ko": "최신",
"summary": "최신 공표일과 우리 판이 같다."
},
"unknown": {
"name_ko": "모름",
"summary": "최신 공표일을 아직 못 잡았다 — 수시·일별이라 한 날로 안 정해지거나, 확인을 아직 안 했다."
}
},
"counts": {
"sources": 8,
"by_status": {
"behind": 1,
"unknown": 4,
"current": 3
}
},
"sources": [
{
"source_id": "labor_const",
"kind": "labor",
"kind_ko": "노임",
"name_ko": "건설업 시중노임단가",
"publisher": "대한건설협회",
"where_to_get": "cak.or.kr [지원·사업] > [건설적산기준] > [건설임금]",
"cycle": "해마다 두 번 — **1월 1일 · 9월 1일**",
"latest_published": "2026-09-01",
"latest_basis": "2026-09-15 브레인 쪽지",
"our_edition": "2026-01-01",
"our_edition_from": "effective_date",
"status": "behind",
"checked_on": "2026-09-15",
"note": "⚠ **뒤처졌다.** 우리 판이 2026-01-01 이라 9월 공표를 반 년 가까이 못 받았다. ⚠ 주기를 **7월 1일로 알고 있던 것이 틀렸다** — 1.1 / 9.1 이다. 노임은 일위대가 품값의 밑값이라 여기가 뒤처지면 **모든 호표가 옛값으로 선다**.",
"files": [
"resources/data_cost_input_value/labor_const_2026-01-01.json"
]
},
{
"source_id": "labor_mfg",
"kind": "labor",
"kind_ko": "노임",
"name_ko": "중소제조업 직종별 임금",
"publisher": "중소기업중앙회",
"where_to_get": "",
"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)다.",
"files": [
"resources/data_cost_input_value/labor_mfg_2026-07-01.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.",
"latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
"source_id": "mach_base",
"kind": "machine",
"kind_ko": "기계",
"name_ko": "건설공사 표준품셈 제8장 건설기계",
"publisher": "국토교통부",
"where_to_get": "",
"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",
"note": "우리 판이 최신이다. 취득가·손료계수·연료소모량·조종원이 다 여기서 온다.",
"files": [
"resources/data_cost_input_value/mach_base_2026.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다."
},
{
"source_id": "machine_operating",
"kind": "machine",
"kind_ko": "기계",
"name_ko": "건설기계 운전경비(품셈 8-4 파생본)",
"publisher": "국토교통부",
"where_to_get": "",
"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",
"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",
"kind": "material",
"kind_ko": "자재",
"name_ko": "관급자재 단가(시설공통자재)",
"publisher": "조달청",
"where_to_get": "",
"cycle": "반기 + 수시",
"latest_published": "",
"latest_basis": "",
"our_edition": "2026-08-14",
"our_edition_from": "effective_date",
"status": "unknown",
"checked_on": "2026-09-15",
"note": "수시 공고라 「최신 공표일」이 한 날로 안 정해진다 — 받은 날(우리 판)이 곧 기준이다. 철근·레미콘·아스콘은 이 벌에 없다(쇼핑몰 스냅숏 미확보).",
"files": [
"resources/data_cost_input_value/mat_price_public_2026-08-14.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.",
"latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
"source_id": "oil",
"kind": "oil",
"kind_ko": "유가",
"name_ko": "유가 — 전국평균",
"publisher": "한국석유공사(오피넷)",
"where_to_get": "",
"cycle": "주간 · 일별",
"latest_published": "",
"latest_basis": "",
"our_edition": "2026-08-14",
"our_edition_from": "effective_date",
"status": "unknown",
"checked_on": "2026-09-15",
"note": "날마다 바뀌는 값이라 「최신」이 곧 오늘이다. **뒤처짐을 날짜 차이로 보여야 하는 자료**다. ⚠ 시도별 판과 「전국」 줄이 겹치는데 값도 기준일도 다르다(어느 쪽을 쓸지 판정 대기).",
"files": [
"resources/data_cost_input_value/oil_2026-08-14.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다.",
"latest_missing_reason": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
"source_id": "oil_regional",
"kind": "oil",
"kind_ko": "유가",
"name_ko": "유가 — 시도별",
"publisher": "한국석유공사(오피넷)",
"where_to_get": "",
"cycle": "주간 · 일별",
"latest_published": "",
"latest_basis": "",
"our_edition": "2026-09-09",
"our_edition_from": "effective_date",
"status": "unknown",
"checked_on": "2026-09-15",
"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": "수시·일별이라 최신 공표일이 한 날로 안 정해지거나, 아직 확인을 안 했다."
},
{
"source_id": "rates",
"kind": "rate",
"kind_ko": "요율",
"name_ko": "토목공사 원가계산 제비율 적용기준",
"publisher": "조달청",
"where_to_get": "",
"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",
"note": "우리 판이 최신이다. 국민연금 요율은 국민연금법(현행 2026-06-17)을 따로 물었다. ⚠ 요율은 **법이 바뀌면 주기와 상관없이** 바뀐다 — 주기만 보고 안심하면 안 된다.",
"files": [
"resources/data_cost_input_value/rates_2026.json"
],
"where_to_get_missing_reason": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다."
}
]
}
+128
View File
@@ -0,0 +1,128 @@
"""자료 출처표(`resources/data_master_sources/sources_2026-09-15.json`) 시험.
이 표가 있는 까닭 — 마스터 파일 안에는 **제 판만** 있고 「최신이 무엇인지·어디서 받는지」가
없어서 건설 노임이 반 년 뒤처진 것을 아무도 몰랐다(2026-09-15).
- `our_edition` 이 **실제 파일과 어긋나면 빨강** — 손으로 적힌 판이 굳는 것을 막는다
- 뒤처짐 판정이 뒤집히면 빨강
- 받는 자리를 모르는 줄에 **사유가 없으면 빨강**(주소를 지어내지 않기로 한 자리)
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
SOURCES_PATH = ROOT / "resources" / "data_master_sources" / "sources_2026-09-15.json"
@pytest.fixture(scope="module")
def sources() -> dict:
return json.loads(SOURCES_PATH.read_text(encoding="utf-8"))
def _edition_of(path: Path) -> tuple[str, str]:
doc = json.loads(path.read_text(encoding="utf-8"))
if doc.get("effective_date"):
return doc["effective_date"], "effective_date"
derived = doc.get("derived_from")
if isinstance(derived, dict) and derived.get("effective_date"):
return derived["effective_date"], "derived_from.effective_date"
return "", ""
def test_출처표가_읽힌다(sources):
assert sources["dataset_id"] == "data_master_sources"
assert sources["sources"], "출처가 하나도 없다"
def test_가리키는_파일이_다_있다(sources):
missing = [
path for row in sources["sources"] for path in row["files"] if not (ROOT / path).is_file()
]
assert not missing, f"없는 파일을 가리킨다: {missing}"
def test_우리_판이_실제_파일과_같다(sources):
"""⭐ 손으로 적힌 판이 굳으면 또 뒤처진다 — 파일에서 읽은 값과 대 본다."""
wrong = []
for row in sources["sources"]:
edition, where = _edition_of(ROOT / row["files"][0])
if row["our_edition"] != edition:
wrong.append(f"{row['source_id']}: 표 {row['our_edition']} ≠ 파일 {edition}")
elif row["our_edition_from"] != where:
wrong.append(f"{row['source_id']}: 읽은 자리 {row['our_edition_from']}{where}")
assert not wrong, wrong
def test_뒤처짐_판정이_맞다(sources):
for row in sources["sources"]:
latest, ours = row["latest_published"], row["our_edition"]
if not latest or not ours:
assert row["status"] == "unknown", row["source_id"]
elif latest > ours:
assert row["status"] == "behind", row["source_id"]
else:
assert row["status"] == "current", row["source_id"]
def test_건설_노임이_뒤처진_것으로_선다(sources):
"""2026-09-15 에 드러난 자리 — 우리 2026-01-01, 최신 2026-09-01."""
row = next(r for r in sources["sources"] if r["source_id"] == "labor_const")
assert row["status"] == "behind"
assert row["our_edition"] == "2026-01-01"
assert row["latest_published"] == "2026-09-01"
# 주기를 7월 1일로 알던 것이 틀렸다 — 1.1 / 9.1 이다.
assert "9월 1일" in row["cycle"]
assert row["where_to_get"], "받는 자리를 받았는데 비어 있다"
def test_운전경비는_판_모름이_아니다(sources):
"""파일 안 `derived_from.effective_date` 에 판이 박혀 있다 — 「모름」으로 적으면 안 된다."""
row = next(r for r in sources["sources"] if r["source_id"] == "machine_operating")
assert row["our_edition"] == "2026-01-01"
assert row["our_edition_from"] == "derived_from.effective_date"
assert row["status"] != "unknown"
def test_모르는_칸에는_사유가_있다(sources):
"""주소·최신 공표일을 지어내지 않기로 한 자리 — 빈 칸이면 사유가 있어야 한다."""
bad = []
for row in sources["sources"]:
if not row["where_to_get"] and not row.get("where_to_get_missing_reason"):
bad.append(f"{row['source_id']} — 받는 자리 사유 없음")
if not row["latest_published"] and not row.get("latest_missing_reason"):
bad.append(f"{row['source_id']} — 최신 공표일 사유 없음")
assert not bad, bad
def test_줄마다_갈래와_주기가_있다(sources):
known = set(sources["kinds"])
for row in sources["sources"]:
assert row["kind"] in known, row["source_id"]
assert row["kind_ko"] == sources["kinds"][row["kind"]]
assert row["cycle"].strip(), row["source_id"]
assert row["publisher"].strip(), row["source_id"]
assert row["checked_on"].strip(), row["source_id"]
def test_기초단가_다섯_갈래를_다_덮는다(sources):
covered = {row["kind"] for row in sources["sources"]}
assert covered == {"labor", "machine", "material", "oil", "rate"}
def test_세어_둔_수가_실제와_같다(sources):
counts = sources["counts"]
assert counts["sources"] == len(sources["sources"])
tallied: dict[str, int] = {}
for row in sources["sources"]:
tallied[row["status"]] = tallied.get(row["status"], 0) + 1
assert counts["by_status"] == tallied
def test_상태_낱말이_다_풀려_있다(sources):
known = set(sources["status"])
assert {row["status"] for row in sources["sources"]} <= known