feat(z01): 덮개에 넷째 낱말 meaning_changed — 고칠 때 그 줄의 값 아닌 글자 칸(이름·규격·단위·인도조건)을 함께 적고 갱신 뒤 달라지면 「줄의 뜻이 바뀜」(같은 코드에 다른 기준 — 노임 국가유산직종 개편 · 자재 KS 개정) · was/now 는 서버가 한 줄로 합쳐 냄(이름 칸이 여럿인 자료가 있어 화면이 합치면 판정 두 벌) · 모아 보기 차례 맨 위 · 판·공표 날짜는 뜻 칸에서 뺌(브레인)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -366,6 +366,24 @@ def column_meta(kind: str, key: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
#: 뜻 칸에서 뺄 자리 — 판·공표 날짜는 갱신 때마다 바뀌어 「뜻 바뀜」과 구별이 안 됨
|
||||
_NOT_MEANING = ("effective_date", "date", "notice_datetime")
|
||||
|
||||
|
||||
def meaning_of(kind: str, row: dict[str, Any], editable: list[str]) -> dict[str, Any]:
|
||||
"""그 줄의 **값이 아닌 글자 칸**(이름·규격·단위·인도조건 …) — 이름표가 숨김 아니라 한 열만.
|
||||
|
||||
갱신으로 같은 코드에 다른 기준이 붙으면(노임 국가유산직종 개편 · 자재 KS 개정) 값만으로는 못 잡음.
|
||||
"""
|
||||
out = {}
|
||||
for key, value in row.items():
|
||||
if key.startswith("@") or key in editable or key in _NOT_MEANING or key.endswith("_date"):
|
||||
continue
|
||||
if isinstance(value, str) and value.strip() and not column_meta(kind, key)["hidden"]:
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def public(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"""내보낼 줄 — 안쪽 칸(`@source`)은 뺌."""
|
||||
return {k: v for k, v in row.items() if k != "@source"}
|
||||
@@ -422,7 +440,7 @@ def edit(kind: str, row_id: str, values: dict[str, Any], by: Any) -> tuple[int,
|
||||
problem = overrides.check_value(value)
|
||||
if problem:
|
||||
return 400, {"message": f"{column}: {problem}"}
|
||||
overrides.write(kind, base, values, by)
|
||||
overrides.write(kind, base, values, by, meaning_of(kind, base, info["editable"]))
|
||||
return 200, public(next(r for r in rows(kind) if r["@id"] == row_id))
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ from common_util.common_util_json import atomic_write_json
|
||||
|
||||
OVERRIDE_DIR = Path(__file__).resolve().parent.parent / "resources" / "data_master_override"
|
||||
EDITED, SOURCE_CHANGED, ORPHAN = "edited", "source_changed", "orphan"
|
||||
STATE_ORDER = (SOURCE_CHANGED, ORPHAN, EDITED) # 모아 보기 차례 — 확인할 것이 맨 위
|
||||
#: ⚠ 넷째 — 값이 아니라 **줄의 뜻**이 바뀜(같은 코드에 다른 기준: 노임 문화재→국가유산 · 자재 KS 개정 규격).
|
||||
#: 값이 틀린 것보다 **다른 물건에 값이 붙은 것**이 더 나쁨 → 맨 위(브레인 2026-09-16).
|
||||
MEANING_CHANGED = "meaning_changed"
|
||||
STATE_ORDER = (MEANING_CHANGED, SOURCE_CHANGED, ORPHAN, EDITED) # 모아 보기 차례 — 센 것이 맨 위
|
||||
_LOCK = threading.Lock() # ponytail: 한 프로세스 잠금 — 서버를 여럿 띄우면 파일 잠금이 필요
|
||||
|
||||
|
||||
@@ -61,9 +64,33 @@ def check_value(value: Any) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def mark(original: Any, value: Any, current: Any) -> dict[str, Any]:
|
||||
state = EDITED if same(original, current) else SOURCE_CHANGED
|
||||
return {"original": original, "value": value, "state": state, "current": current}
|
||||
#: 이름 칸이 여럿인 자료가 있어(자재 품명·규격·단위·인도조건) **서버가 한 줄로 합침** — 화면이 합치면 판정이 두 벌.
|
||||
_JOIN = " · "
|
||||
|
||||
|
||||
def meaning_line(entry: dict[str, Any], row: dict[str, Any] | None) -> tuple[str, str]:
|
||||
"""(고칠 때 그 줄 · 지금 그 줄) 한 줄씩 — 달라진 데가 없으면 빈 둘."""
|
||||
stored = entry.get("meaning") or {}
|
||||
keys = [k for k in stored if row is not None and k in row]
|
||||
if not keys or all(row[k] == stored[k] for k in keys):
|
||||
return "", ""
|
||||
return (
|
||||
_JOIN.join(str(stored[k]) for k in keys),
|
||||
_JOIN.join(str(row[k]) for k in keys),
|
||||
)
|
||||
|
||||
|
||||
def mark(entry: dict[str, Any], current: Any, row: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
original, value = entry.get("original"), entry.get("value")
|
||||
was, now = meaning_line(entry, row)
|
||||
if was:
|
||||
state = MEANING_CHANGED
|
||||
else:
|
||||
state = EDITED if same(original, current) else SOURCE_CHANGED
|
||||
out = {"original": original, "value": value, "state": state, "current": current}
|
||||
if was: # ⚠ meaning_changed 인데 비어 오면 서버 버그 — 늘 함께 나감
|
||||
out["was"], out["now"] = was, now
|
||||
return out
|
||||
|
||||
|
||||
def overlay(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
@@ -80,7 +107,7 @@ def overlay(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
for column, entry in entries.items():
|
||||
if column not in row:
|
||||
continue # 주인 없는 칸 — 모아 보기에만
|
||||
marks[column] = mark(entry.get("original"), entry.get("value"), row[column])
|
||||
marks[column] = mark(entry, row[column], row)
|
||||
row[column] = entry.get("value")
|
||||
if marks:
|
||||
row["@overrides"] = marks
|
||||
@@ -88,7 +115,13 @@ def overlay(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
def write(kind: str, base_row: dict[str, Any], values: dict[str, Any], by: Any) -> None:
|
||||
def write(
|
||||
kind: str,
|
||||
base_row: dict[str, Any],
|
||||
values: dict[str, Any],
|
||||
by: Any,
|
||||
meaning: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""고친 값을 덮개에 씀 — null 이거나 지금 원본과 같으면 그 칸을 뺌. 원본은 안 건드림."""
|
||||
row_id = base_row["@id"]
|
||||
with _LOCK:
|
||||
@@ -103,6 +136,8 @@ def write(kind: str, base_row: dict[str, Any], values: dict[str, Any], by: Any)
|
||||
"value": value,
|
||||
"original": current, # 이 값을 보고 고침 — 갱신 뒤 원본과 달라지면 source_changed
|
||||
"source": base_row.get("@source", ""),
|
||||
# 값이 아닌 글자 칸 — 갱신으로 **줄의 뜻**이 바뀌면 이것으로 잡음
|
||||
"meaning": meaning or {},
|
||||
"at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"by": by,
|
||||
}
|
||||
@@ -148,10 +183,10 @@ def items(kind: str, base_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
row = by_id.get(row_id)
|
||||
for column, entry in entries.items():
|
||||
if row is None or column not in row:
|
||||
current, state = None, ORPHAN
|
||||
current, marked = None, {"state": ORPHAN}
|
||||
else:
|
||||
current = row[column]
|
||||
state = mark(entry.get("original"), entry.get("value"), current)["state"]
|
||||
marked = mark(entry, current, row)
|
||||
out.append(
|
||||
{
|
||||
"kind": kind,
|
||||
@@ -160,7 +195,8 @@ def items(kind: str, base_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"original": entry.get("original"),
|
||||
"current": current,
|
||||
"value": entry.get("value"),
|
||||
"state": state,
|
||||
"state": marked["state"],
|
||||
**({"was": marked["was"], "now": marked["now"]} if "was" in marked else {}),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
@@ -548,3 +548,56 @@ def test_덮개_얹은_값으로_세움(client: TestClient) -> None:
|
||||
def test_sort_가_없으면_원본_차례(client: TestClient) -> None:
|
||||
assert _get(client, "rate", size=1)["rows"][0]["@id"] == "rate_sanjae"
|
||||
assert _get(client, "machine", size=1)["rows"][0]["@id"] == "0101-0007"
|
||||
|
||||
|
||||
def test_줄의_뜻이_바뀌면_넷째_낱말(client: TestClient, store: Path) -> None:
|
||||
"""브레인 — 같은 코드에 다른 기준이 붙음(노임 3001~ 문화재→국가유산 · 자재 KS 개정 규격).
|
||||
|
||||
값만 보면 안 잡힘 → 덮개를 쓸 때 **값이 아닌 글자 칸**(이름·규격·단위…)을 함께 적어 두고 견줌.
|
||||
"""
|
||||
saved = _put(client, "labor", "labor_const/1001", {"daily_wage_krw": 230000}).json()
|
||||
assert saved["@overrides"]["daily_wage_krw"]["state"] == overrides.EDITED
|
||||
entry = json.loads((store / "labor.json").read_text(encoding="utf-8"))["overrides"]
|
||||
meaning = entry["labor_const/1001"]["daily_wage_krw"]["meaning"]
|
||||
assert meaning["occupation_name"] == "작업반장" # 이름·규격 같은 글자 칸만
|
||||
assert "daily_wage_krw" not in meaning and "hours_per_day" not in meaning # 값 칸은 안 적음
|
||||
assert "effective_date" not in meaning # 판 날짜는 갱신마다 바뀜 — 뜻 바뀜과 구별이 안 됨
|
||||
_put(client, "oil", "national_average/oil_diesel", {"price_krw_per_l": 1900})
|
||||
oil = json.loads((store / "oil.json").read_text(encoding="utf-8"))["overrides"]
|
||||
assert "date" not in oil["national_average/oil_diesel"]["price_krw_per_l"]["meaning"]
|
||||
entry["labor_const/1001"]["daily_wage_krw"]["meaning"]["occupation_name"] = "옛 직종 이름"
|
||||
(store / "labor.json").write_text(
|
||||
json.dumps({"schema_version": "1.0", "kind": "labor", "overrides": entry}), encoding="utf-8"
|
||||
)
|
||||
mark = _row(client, "labor", "labor_const/1001")["@overrides"]["daily_wage_krw"]
|
||||
assert mark["state"] == overrides.MEANING_CHANGED # 값은 그대로인데 줄의 뜻이 바뀜
|
||||
# 이름 칸이 여럿인 자료가 있어 **서버가 한 줄로 합쳐** 냄(화면이 합치면 판정이 두 벌)
|
||||
assert mark["was"] == "labor_const · 1001 · 옛 직종 이름 · KRW/day · published"
|
||||
assert mark["now"] == "labor_const · 1001 · 작업반장 · KRW/day · published"
|
||||
listed = client.get("/api/master-data/overrides").json()
|
||||
top = listed["items"][0]
|
||||
assert top["state"] == overrides.MEANING_CHANGED # 값 틀림보다 더 세게
|
||||
assert (top["was"], top["now"]) == (mark["was"], mark["now"]) # 모아 보기에도 같은 둘
|
||||
assert overrides.STATE_ORDER[0] == overrides.MEANING_CHANGED
|
||||
assert all(i.get("was") for i in listed["items"] if i["state"] == overrides.MEANING_CHANGED)
|
||||
|
||||
|
||||
def test_뜻_칸은_이름표가_가림_옛_덮개는_그대로(client: TestClient, store: Path) -> None:
|
||||
_put(client, "machine", "0101-0007", {"price_thousand_krw": 80000})
|
||||
meaning = json.loads((store / "machine.json").read_text(encoding="utf-8"))["overrides"][
|
||||
"0101-0007"
|
||||
]["price_thousand_krw"]["meaning"]
|
||||
assert meaning["machine_name"] == "불도저(무한궤도)" and meaning["specification"] == "7"
|
||||
assert not [k for k in meaning if k in ("price_thousand_krw", "loss_coefficient_per_hour")]
|
||||
store.mkdir(
|
||||
exist_ok=True
|
||||
) # 뜻 칸이 없던 옛 덮개 — 그대로 edited 로 둠(없는 것을 지어내지 않음)
|
||||
(store / "material.json").write_text(
|
||||
json.dumps(
|
||||
{"overrides": {"10023392": {"price_krw": {"value": 30, "original": 22, "source": "x"}}}}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert (
|
||||
_row(client, "material", "10023392")["@overrides"]["price_krw"]["state"] == overrides.EDITED
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user