fix(M02): 프로젝트 표 저장은 양식 + 손 값만 — 설계값 · 펼친 열은 서버가 걸러 버림 · 없어진 측점 손 값은 맨 아래 비고 줄 (PLAN 10-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015LuapLYqN1GGFD8Y1PStD5
This commit is contained in:
@@ -252,13 +252,17 @@ async def save_layer_template(
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
folder = await _layer_dir(session, layer, project_id=project_id, write=True)
|
||||
document = body.문서
|
||||
if kind == "table" and fill.is_fillable(document):
|
||||
# 채운 표가 와도 설계값은 안 받아 적음 — 양식 + 손 값만(5장 · 브라우저 값을 믿지 않음)
|
||||
document = fill.strip_design(document)
|
||||
try:
|
||||
version = await asyncio.to_thread(
|
||||
layers.write_template,
|
||||
folder,
|
||||
kind,
|
||||
name,
|
||||
body.문서,
|
||||
document,
|
||||
version=body.판,
|
||||
check_version=True,
|
||||
)
|
||||
|
||||
@@ -36,6 +36,9 @@ FACILITY_TYPES = {
|
||||
"revetment": "revetment",
|
||||
}
|
||||
EMPTY_LABEL = "-"
|
||||
ORPHAN_NOTE = "설계에서 측점 없어짐"
|
||||
#: 채운 표에 실어 보내는 펼침 전 열 — 저장 때 빠진 규칙 열을 되살리는 데만 씀.
|
||||
RULE_COLUMNS_KEY = "양식열"
|
||||
_REF = re.compile(r"\[([^\]$@][^\]@]*)\]")
|
||||
|
||||
|
||||
@@ -354,7 +357,8 @@ def fill_document(
|
||||
doc = copy.deepcopy(document)
|
||||
variables = dict(doc.get("변수") or {})
|
||||
interval = _number(variables.get("측점간격_m")) or 20.0
|
||||
columns, made = _expand_columns(doc.get("열") or [], items, variables, _choices(types))
|
||||
rule_columns = _unexpand(doc.get("열") or [])
|
||||
columns, made = _expand_columns(rule_columns, items, variables, _choices(types))
|
||||
notes: list[str] = []
|
||||
|
||||
# 줄 자리 — 관은 한 줄에 하나(둘째 관부터 줄을 나눔)
|
||||
@@ -411,7 +415,7 @@ def fill_document(
|
||||
notes.append(f"가드레일·경계석·위험표지 종류 미정 {loose}건 — 표에 안 셈")
|
||||
|
||||
# 손 값 지키기 — 같은 줄 id 의 손 열 · 전구간 줄 · 사용자가 더한 줄
|
||||
hand = {column["id"] for column in columns if column.get("손")}
|
||||
hand = _hand_ids(columns)
|
||||
old_rows = {row.get("id"): row for row in doc.get("줄") or []}
|
||||
fixed = [row for row in doc.get("줄") or [] if row.get("고정")]
|
||||
added = [row for row in doc.get("줄") or [] if row.get("손") and not row.get("고정")]
|
||||
@@ -438,13 +442,78 @@ def fill_document(
|
||||
}
|
||||
if values:
|
||||
sta = (row.get("값") or {}).get("sta")
|
||||
added.append({"id": row["id"], "값": {"sta": sta, **values}, "손": True})
|
||||
memo = " · ".join(filter(None, [str(values.get("memo") or ""), ORPHAN_NOTE]))
|
||||
added.append({"id": row["id"], "값": {"sta": sta, **values, "memo": memo}, "손": True})
|
||||
doc["열"] = columns
|
||||
doc["줄"] = [*fixed, *design_rows, *added]
|
||||
doc["알림"] = notes
|
||||
doc[RULE_COLUMNS_KEY] = copy.deepcopy(rule_columns) # 저장 때 설계에 안 쓰인 펼침 열도 되살림
|
||||
return doc
|
||||
|
||||
|
||||
def _hand_ids(columns: list[dict[str, Any]]) -> set[str]:
|
||||
"""사람이 적는 열 — 바인딩 없는 열(손 열 · 비고 · 사용자가 더한 열) · 차례 · 측점 빼고."""
|
||||
return {
|
||||
column["id"]
|
||||
for column in columns
|
||||
if not column.get("바인딩") and column["id"] not in ("no", "sta")
|
||||
}
|
||||
|
||||
|
||||
def _restore_rules(
|
||||
columns: list[dict[str, Any]], rules: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""채울 때 설계 값이 없어 빠졌던 펼침 열을 원래 자리(앞 열 뒤)에 되살림."""
|
||||
out = list(columns)
|
||||
for index, rule in enumerate(rules):
|
||||
if not rule.get("펼침") or any(c["id"] == rule["id"] for c in out):
|
||||
continue
|
||||
before = [r["id"] for r in rules[:index]]
|
||||
at = max((i + 1 for i, c in enumerate(out) if c["id"] in before), default=0)
|
||||
out.insert(at, copy.deepcopy(rule))
|
||||
return out
|
||||
|
||||
|
||||
def strip_design(document: dict[str, Any]) -> dict[str, Any]:
|
||||
"""작업본에 둘 것만 — 양식(열 · 식 · 변수 · 머리) + 손 값 + 전구간 줄 · 사용자 줄.
|
||||
|
||||
채운 표가 [저장]으로 와도 설계값 · 펼친 열은 버림(펼침 규칙 열 하나로 되돌림) ·
|
||||
측점 줄은 손 값이 있을 때만 `{id(측점 키), sta, 손 값}` 으로 남김 —
|
||||
다시 채우면 같은 측점에 붙음.
|
||||
"""
|
||||
doc = copy.deepcopy(document)
|
||||
for key in ("알림", "결과"):
|
||||
doc.pop(key, None)
|
||||
columns = _restore_rules(_unexpand(doc.get("열") or []), doc.pop(RULE_COLUMNS_KEY, None) or [])
|
||||
hand = _hand_ids(columns)
|
||||
rows = []
|
||||
for row in doc.get("줄") or []:
|
||||
if row.get("고정"):
|
||||
rows.append(row)
|
||||
continue
|
||||
values = row.get("값") or {}
|
||||
kept = {k: v for k, v in values.items() if k in hand and v not in (None, "")}
|
||||
if row.get("손"):
|
||||
formulas = {k: v for k, v in (row.get("식") or {}).items() if k in hand}
|
||||
new = {**row, "값": {"sta": values.get("sta"), **kept}}
|
||||
new.pop("식", None)
|
||||
if formulas:
|
||||
new["식"] = formulas
|
||||
rows.append(new)
|
||||
elif kept:
|
||||
rows.append({"id": row["id"], "값": {"sta": values.get("sta"), **kept}})
|
||||
doc["열"] = columns
|
||||
doc["줄"] = rows
|
||||
return doc
|
||||
|
||||
|
||||
def is_fillable(document: Any) -> bool:
|
||||
"""설계값을 받는 표인가 — 바인딩 열이 하나라도 있으면."""
|
||||
return isinstance(document, dict) and any(
|
||||
isinstance(column, dict) and column.get("바인딩") for column in document.get("열") or []
|
||||
)
|
||||
|
||||
|
||||
def fill_table(
|
||||
project_root: str | Path,
|
||||
document: dict[str, Any],
|
||||
|
||||
@@ -148,3 +148,38 @@ def test_측점_표기() -> None:
|
||||
assert fill.station_label(38) == "NO.1+18"
|
||||
assert fill.station_label(59.999) == "NO.3"
|
||||
assert fill.station_label(2413.5) == "NO.120+13.5"
|
||||
|
||||
|
||||
def test_저장은_양식과_손_값만_다시_채우면_제자리(project: Path) -> None:
|
||||
master = _master()
|
||||
doc = _filled(project)
|
||||
rows = {row["id"]: row for row in doc["줄"]}
|
||||
rows["s300.00"]["값"]["h_intake"] = 2
|
||||
rows["s85.05#1"]["값"]["memo"] = "흄관 확인"
|
||||
doc["줄"][0]["값"]["ps"] = 3
|
||||
saved = fill.strip_design({**doc, "결과": {"계산": {}}})
|
||||
assert [c["id"] for c in saved["열"]] == [
|
||||
c["id"] for c in master["열"]
|
||||
] # 펼침 규칙 열로 되돌림
|
||||
assert "알림" not in saved and "결과" not in saved
|
||||
assert saved["줄"] == [
|
||||
{"id": "all", "값": {"sta": "전구간", "ps": 3}, "고정": "전구간"},
|
||||
{"id": "s85.05#1", "값": {"sta": "NO.4+5.05", "memo": "흄관 확인"}},
|
||||
{"id": "s300.00", "값": {"sta": "NO.15", "h_intake": 2}},
|
||||
]
|
||||
again = {row["id"]: row for row in _filled(project, saved)["줄"]}
|
||||
assert again["s300.00"]["값"]["h_intake"] == 2 and again["s300.00"]["값"]["ps"] == 1
|
||||
assert again["s85.05#1"]["값"]["memo"] == "흄관 확인"
|
||||
assert again["s85.05#1"]["값"]["pp_len|흄관|1000"] == 14.0
|
||||
|
||||
|
||||
def test_측점이_없어진_손_값은_맨_아래_비고_줄로(project: Path) -> None:
|
||||
saved = fill.strip_design(_filled(project))
|
||||
saved["줄"].append({"id": "s300.00", "값": {"sta": "NO.15", "h_intake": 2}})
|
||||
saved["줄"].append({"id": "s555.00", "값": {"sta": "NO.27+15", "h_intake": 1}})
|
||||
doc = _filled(project, saved)
|
||||
last = doc["줄"][-1]
|
||||
assert last["id"] == "s555.00" and last["손"] is True
|
||||
assert last["값"] == {"sta": "NO.27+15", "h_intake": 1, "memo": "설계에서 측점 없어짐"}
|
||||
kept = fill.strip_design(doc)
|
||||
assert kept["줄"][-1]["값"]["h_intake"] == 1 # 다음 저장에도 남음
|
||||
|
||||
@@ -265,3 +265,33 @@ def test_옛_프로젝트_넣기_도구는_더하기만_폴더_없으면_건너
|
||||
assert not (world["storage"] / "7/42/gone").exists()
|
||||
assert migrate.migrate(projects)[0]["상태"] == "이미 있음"
|
||||
assert "| 7 | 42 | 옛 |" in migrate.render(list(report.values()), [])
|
||||
|
||||
|
||||
def test_프로젝트_표_저장은_서버가_설계값을_걸러_씀(world: dict[str, Any]) -> None:
|
||||
from M02_MasterTemplete import M02_Table_Fill as fill
|
||||
|
||||
client = world["client"]
|
||||
master = json.loads(
|
||||
(
|
||||
config_system.PROJECT_ROOT / "resources/master_template/table/구조물집계표.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
filled = fill.fill_document(
|
||||
master,
|
||||
[
|
||||
{
|
||||
"type_id": "position_sign",
|
||||
"placement": "point",
|
||||
"chainage_m": 40.0,
|
||||
"start_m": None,
|
||||
"end_m": None,
|
||||
"options": {},
|
||||
}
|
||||
],
|
||||
)
|
||||
filled["줄"][1]["값"]["h_intake"] = 2
|
||||
version = client.get(_url()).json()["판"]
|
||||
assert client.put(_url(), json={"판": version, "문서": filled}).status_code == 200
|
||||
stored = layers.read_template(_root(world, P1) / "templates", "table", "구조물집계표")["문서"]
|
||||
assert stored["줄"][1] == {"id": "s40.00", "값": {"sta": "NO.2", "h_intake": 2}}
|
||||
assert "알림" not in stored and all("|" not in c["id"] for c in stored["열"])
|
||||
|
||||
Reference in New Issue
Block a user