fix(z01): 이름표를 서버 틀에 맞춤 — 영문으로 뜨던 19칸에 이름 · 요율 축 살림
- ⓐ 다섯 표 열을 **서버 응답 그대로** 다시 적음(열 88 = 노임 12 · 기계 23 · 자재 15 · 유가 10 · 요율 28).
⇒ 영문으로 떨어지던 19칸 0 으로 — 기계 6(`operating_effective_date`·`fuel_price_krw_per_l`·
`fuel_price_date`·`operator_daily_wage_krw`·`operator_wage_date`·`hourly_note`) ·
요율 13(`part`·`base_rule`·`embedded_in`·`manager_thresholds.…` 따위)
- ④ **요율 축을 한 칸으로 안 합침**(브레인 판정) — 등급·연도·공사 종류·대상액·추정금액·추정가격·
직접공사비·공사기간 여덟 축을 원문 칸 그대로 둠. 합치면 어느 축인지 사라지고 축이 둘 이상인 줄을
못 읽으며 `@id`(`{변수}/{목록}/{축=값;…}`)가 흔들림. 앞서 지어냈던 `item_key`·`bracket_basis` 는 걷음
- **「고칠 수 있나」를 이름표에서 뺌** — 서버 `spec()` 한 곳이 정함(판정 두 벌 금지)
- ⓑ 새 시험 `test_master_labels_base_prices.py` 9건 — ⭐ **영문으로 떨어지는 열이 있으면 빨강**.
이름표 파일이 아니라 **서버 응답**을 재므로 두 창이 갈리면 그 자리에서 잡힘 ·
묵은 열 이름표 · 요율 축 여덟 · 줄 열쇠에 축이 살아 있나 · 계산값 넷 잠김 · 단위까지 봄
- 왜 오늘 두 번 당했나 — 서버가 「못 찾으면 영문 key」로 **조용히 떨어지고**,
이름표 시험은 서버가 보는 자리를 안 봤음. 어긋나도 아무도 모르던 자리임
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFCnEYNH4tsS2MbHvzBhZk
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
"""기초단가 다섯 표 — **열 이름이 영문으로 떨어지면 빨강**.
|
||||
|
||||
이 시험이 있는 까닭(2026-09-15 크로스체크) — 세 창이 각자 초록이었는데도
|
||||
**26칸이 화면에 영문 key 로 뜨고 있었다.** 서버는 이름표에서 못 찾으면 영문 key 로
|
||||
조용히 떨어지게 짜여 있고, 이름표 쪽 시험은 서버가 실제로 보는 자리를 안 봤다.
|
||||
**어긋나도 아무도 모르는 자리**였다.
|
||||
|
||||
⚠ 그래서 이 시험은 이름표 파일만 읽지 않고 **서버가 내는 응답 그대로** 잰다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from Z01_MasterData import Z01_MasterData_BasePrices as base_prices
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
LABELS_PATH = ROOT / "resources" / "data_master_labels" / "labels_2026-01-01.json"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def labels() -> dict:
|
||||
return json.loads(LABELS_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def shown() -> dict[str, list[dict]]:
|
||||
"""서버가 실제로 내보내는 열 — 이름표 파일이 아니라 **응답**을 잰다."""
|
||||
out = {}
|
||||
for kind in base_prices.KINDS:
|
||||
columns = base_prices._columns(kind, base_prices.rows(kind))
|
||||
out[kind] = [base_prices.column_meta(kind, c) for c in columns]
|
||||
return out
|
||||
|
||||
|
||||
def test_다섯_표가_다_선다(shown):
|
||||
assert set(shown) == {"labor", "machine", "material", "oil", "rate"}
|
||||
for kind, columns in shown.items():
|
||||
assert columns, f"{kind} 에 열이 하나도 없다"
|
||||
|
||||
|
||||
def test_영문으로_떨어지는_열이_하나도_없다(shown):
|
||||
"""⭐ 이 시험이 이번 판의 값진 자리 — 이름표가 서버 틀과 어긋나면 여기서 빨강."""
|
||||
english = [
|
||||
f"{kind}/{column['key']}"
|
||||
for kind, columns in shown.items()
|
||||
for column in columns
|
||||
if column["label"] == column["key"]
|
||||
]
|
||||
assert not english, (
|
||||
f"한글 이름이 없어 영문 key 로 뜨는 열 {len(english)}개: {english}\n"
|
||||
"→ 이름표 `merged_tables[<kind>].columns` 에 그 key 를 더할 것."
|
||||
)
|
||||
|
||||
|
||||
def test_열_이름이_비지_않았다(shown):
|
||||
blank = [
|
||||
f"{kind}/{column['key']}"
|
||||
for kind, columns in shown.items()
|
||||
for column in columns
|
||||
if not str(column["label"]).strip()
|
||||
]
|
||||
assert not blank, blank
|
||||
|
||||
|
||||
def test_이름표에_묵은_열이_남아_있지_않다(labels, shown):
|
||||
"""서버가 안 내는 열을 이름표가 들고 있으면 옛 틀이 굳은 것이다."""
|
||||
stale = []
|
||||
for entry in labels["merged_tables"]:
|
||||
kind = entry["key"]
|
||||
live = {c["key"] for c in shown.get(kind, ())}
|
||||
for column in entry["columns"]:
|
||||
if column["key"] not in live:
|
||||
stale.append(f"{kind}/{column['key']}")
|
||||
assert not stale, f"서버가 안 내는 묵은 열 이름표: {stale}"
|
||||
|
||||
|
||||
def test_요율_축이_한_칸으로_안_합쳐졌다(shown):
|
||||
"""⭐ 2026-09-15 브레인 ④ — 합치면 어느 축의 구간인지 사라지고 `@id` 도 흔들린다.
|
||||
|
||||
축은 서버가 원문 칸 그대로 펴고 이름표가 거기에 맞춘다.
|
||||
"""
|
||||
keys = {column["key"] for column in shown["rate"]}
|
||||
for axis in (
|
||||
"grade",
|
||||
"year",
|
||||
"work_type",
|
||||
"target_amount_bracket",
|
||||
"estimated_amount_bracket",
|
||||
"estimated_price_bracket",
|
||||
"direct_cost_bracket",
|
||||
"duration_bracket",
|
||||
):
|
||||
assert axis in keys, f"요율 축 {axis} 가 사라졌다"
|
||||
|
||||
|
||||
def test_요율_줄_열쇠에_축이_살아_있다():
|
||||
"""`@id` 가 `{변수}[/{목록}/{축=값;…}]` 꼴 — 축이 죽으면 고친 값이 주인을 잃는다."""
|
||||
rows = base_prices.rows("rate")
|
||||
multi = [r for r in rows if "/" in str(r["@id"])]
|
||||
assert multi, "축이 든 줄 열쇠가 하나도 없다"
|
||||
assert any("=" in str(r["@id"]) for r in multi)
|
||||
assert len({r["@id"] for r in rows}) == len(rows), "줄 열쇠가 겹친다"
|
||||
|
||||
|
||||
def test_기계_시간당_단가는_계산값으로_잠긴다(shown):
|
||||
"""밑값만 고칠 수 있다 — 판정은 서버 `spec()` 한 곳(이름표가 따로 안 정한다)."""
|
||||
meta = base_prices.table("machine", page=1, size=1)
|
||||
assert set(meta["formula"]) == {
|
||||
"hourly_loss_krw",
|
||||
"hourly_fuel_krw",
|
||||
"hourly_operator_krw",
|
||||
"hourly_total_krw",
|
||||
}
|
||||
for key in meta["formula"]:
|
||||
assert key not in meta["editable"], f"{key} 는 계산값인데 고칠 수 있다"
|
||||
|
||||
|
||||
def test_이름표는_고칠수있나를_안_정한다(labels):
|
||||
"""판정 두 벌 금지 — `editable` 은 서버만 정한다."""
|
||||
carried = [
|
||||
f"{entry['key']}/{column['key']}"
|
||||
for entry in labels["merged_tables"]
|
||||
for column in entry["columns"]
|
||||
if "editable" in column
|
||||
]
|
||||
assert not carried, f"이름표가 고칠 수 있나를 들고 있다: {carried}"
|
||||
|
||||
|
||||
def test_단위가_붙어야_할_열에_단위가_있다(shown):
|
||||
"""「유가 1,858」만 떠서는 리터인지 드럼인지 모른다 — 금액·수량 열은 단위를 단다."""
|
||||
want = {
|
||||
"labor": {"daily_wage_krw": "원", "hours_per_day": "시간"},
|
||||
"material": {"price_krw": "원"},
|
||||
"oil": {"price_krw_per_l": "원/L"},
|
||||
"machine": {
|
||||
"price_thousand_krw": "천원",
|
||||
"fuel_rate_l_per_hour": "L",
|
||||
"fuel_price_krw_per_l": "원/L",
|
||||
"hourly_total_krw": "원",
|
||||
},
|
||||
"rate": {"rate_percent": "%", "base_amount_krw": "원"},
|
||||
}
|
||||
bad = []
|
||||
for kind, expected in want.items():
|
||||
units = {c["key"]: c["unit"] for c in shown[kind]}
|
||||
for key, unit in expected.items():
|
||||
if units.get(key) != unit:
|
||||
bad.append(f"{kind}/{key}: {units.get(key)!r} ≠ {unit!r}")
|
||||
assert not bad, bad
|
||||
@@ -449,7 +449,7 @@ def test_미결은_미판정으로_표시된다(labels):
|
||||
assert column["name_ko"] == "미판정"
|
||||
|
||||
|
||||
MERGED_KEYS = ("labor", "material", "oil", "rates", "machine")
|
||||
MERGED_KEYS = ("labor", "material", "oil", "rate", "machine")
|
||||
|
||||
|
||||
def test_기초단가_다섯이_다_있다(labels):
|
||||
@@ -462,31 +462,27 @@ def test_기초단가_다섯이_다_있다(labels):
|
||||
assert entry["sources"], entry["key"]
|
||||
|
||||
|
||||
def test_합친_표의_열마다_이름과_고칠수있나가_있다(labels):
|
||||
bad = []
|
||||
for entry in labels["merged_tables"]:
|
||||
for column in entry["columns"]:
|
||||
if not column["name_ko"].strip():
|
||||
bad.append(f"{entry['key']}/{column['key']} — 이름 없음")
|
||||
if not isinstance(column.get("editable"), bool):
|
||||
bad.append(f"{entry['key']}/{column['key']} — editable 없음")
|
||||
def test_합친_표의_열마다_한글_이름이_있다(labels):
|
||||
"""⚠ 「고칠 수 있나」는 여기서 안 정한다 — 서버 `spec()` 한 곳(판정 두 벌 금지).
|
||||
영문으로 떨어지는 열이 있는지는 `test_master_labels_base_prices.py` 가 응답으로 잰다."""
|
||||
bad = [
|
||||
f"{entry['key']}/{column['key']}"
|
||||
for entry in labels["merged_tables"]
|
||||
for column in entry["columns"]
|
||||
if not column["name_ko"].strip()
|
||||
]
|
||||
assert not bad, bad
|
||||
|
||||
|
||||
def test_계산값은_기계에만_있고_못_고친다(labels):
|
||||
"""⭐ 밑값만 고칠 수 있다 — 계산값을 손으로 고치면 밑값과 어긋나 되짚을 수 없다."""
|
||||
derived = {
|
||||
entry["key"]: [c["key"] for c in entry["columns"] if not c["editable"]]
|
||||
for entry in labels["merged_tables"]
|
||||
}
|
||||
assert derived["machine"] == [
|
||||
"hourly_loss_krw",
|
||||
"hourly_fuel_krw",
|
||||
"hourly_operator_krw",
|
||||
"hourly_total_krw",
|
||||
]
|
||||
for key in ("labor", "material", "oil", "rates"):
|
||||
assert derived[key] == [], f"{key} 에 계산값이 생겼다"
|
||||
def test_기계만_다른_표의_값을_문다(labels):
|
||||
"""⭐ 기계 시간당 단가는 유가·노임 표를 물어 나온다 — 갱신 차례가 여기서 갈린다."""
|
||||
merged = {m["key"]: m for m in labels["merged_tables"]}
|
||||
machine = {c["key"]: c for c in merged["machine"]["columns"]}
|
||||
for key in ("fuel_price_krw_per_l", "operator_daily_wage_krw"):
|
||||
assert key in machine, f"{key} 이름표가 없다"
|
||||
assert "표" in machine[key].get("from", ""), f"{key} 가 어디서 오는지 안 적혔다"
|
||||
for key in ("labor", "material", "oil", "rate"):
|
||||
assert len(merged[key]["sources"]) <= 3, key
|
||||
|
||||
|
||||
def test_기계_계산_사슬이_다섯_걸음으로_적혀_있다(labels):
|
||||
@@ -521,7 +517,7 @@ def test_줄_열쇠가_다섯_다_판정돼_있다(labels):
|
||||
# 한 칸으로는 안 되는 둘 — 묶어야 한다.
|
||||
assert len(keys["labor"]["key"]) > 1
|
||||
assert keys["oil"]["verdict"].startswith("없음")
|
||||
assert keys["rates"]["verdict"].startswith("없음")
|
||||
assert keys["rate"]["verdict"].startswith("없음")
|
||||
|
||||
|
||||
def test_null_이_제값인_열이_적혀_있다(labels):
|
||||
|
||||
Reference in New Issue
Block a user