Merge remote-tracking branch 'origin/sub_desktop_1' into main_laptop_1
This commit is contained in:
@@ -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": "받는 자리를 아직 못 받았다 — 주소를 지어내지 않는다."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user