From 23c6645839eeb4e885e691f256eabd6b95ae4502 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 17:21:38 +0900 Subject: [PATCH] =?UTF-8?q?fix(388-2=20=EB=B3=B4=EC=95=88):=20[=ED=99=95?= =?UTF-8?q?=EC=A0=95]=20=EB=A9=B4=EC=A0=81=20=C2=B7=20=EA=B4=80=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=3D=20=EC=84=9C=EB=B2=84=20=EC=85=88?= =?UTF-8?q?=EB=A7=8C=20=C2=B7=20=EC=9E=AC=EA=B3=84=EC=82=B0=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EA=B2=BD=EA=B3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 [저장] · [확정]이 면적 · 관 길이 9칸을 안 보냄 - 서버가 받아도 _AREA_KEYS 걷음(옛 화면 · 위조 값 정본에 안 남음) - 서버 재계산 실패(상세 못 받음 · Node 실패)를 삼키지 않음 → 응답 recomputed:false - 화면 경고 한 줄(저장은 됨 · 옛 서버 값 남음 · 다시 [저장]) - 새 test_384_s_area_keys · 옛 시험 셋 새 동작에 맞춤 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- B06_Section/B06_Section_Api_Fetch.ts | 33 +++-- B06_Section/B06_Section_Api_Types.ts | 2 + B06_Section/B06_Section_Router_Confirm.py | 21 ++- B06_Section/B06_Section_Schema.py | 7 +- .../B06_Section_Server_Calc_Prebuild.py | 11 +- B06_Section/B06_Section_UI_Page_Persist.ts | 28 +--- resources/tester/test_384_s_area_keys.py | 125 ++++++++++++++++++ resources/tester/test_76_stale_route_save.py | 3 +- .../tester/test_b05_b06_save_roundtrip.py | 2 +- resources/tester/test_b06_pipe_length.py | 4 +- .../test_b06_save_fills_empty_design.py | 2 +- ui_template/ui_template_locale_b4.ts | 8 ++ 12 files changed, 191 insertions(+), 55 deletions(-) create mode 100644 resources/tester/test_384_s_area_keys.py diff --git a/B06_Section/B06_Section_Api_Fetch.ts b/B06_Section/B06_Section_Api_Fetch.ts index 181ec122a..d40878897 100644 --- a/B06_Section/B06_Section_Api_Fetch.ts +++ b/B06_Section/B06_Section_Api_Fetch.ts @@ -19,6 +19,7 @@ import { clearState, readState, stateKey, writeState } from "../A00_Common/b_page_state"; import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History"; import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; +import { t as L } from "@ui/ui_template_locale"; import type { CrossDesign, CrossDesignRequest, @@ -184,10 +185,24 @@ export async function confirmSections( if (standardCrossSection) body.standard_cross_section = standardCrossSection; if (crossPatches?.length) body.cross_patches = crossPatches; if (massHaul) body.mass_haul = massHaul; - return requestJson(`/projects/${projectId}/sections/${routeId}/confirm`, { - method: "POST", - body: JSON.stringify(body), - }); + return warnUnrecomputed( + requestJson(`/projects/${projectId}/sections/${routeId}/confirm`, { + method: "POST", + body: JSON.stringify(body), + }), + ); +} + +/** 서버 정본 셈이 실패했으면 경고 한 줄(388-2) — 저장은 남음 · 면적 · 관 길이는 옛 서버 값. */ +async function warnUnrecomputed( + request: Promise, +): Promise { + const result = await request; + if (result.recomputed === false) + void import("@ui/ui_template_elements").then(({ showToast }) => + showToast(L("B06_Recompute_Failed"), "warning", 8000), + ); + return result; } /** @@ -205,10 +220,12 @@ export async function saveSections( if (standardCrossSection) body.standard_cross_section = standardCrossSection; if (crossPatches?.length) body.cross_patches = crossPatches; if (massHaul) body.mass_haul = massHaul; - return requestJson(`/projects/${projectId}/sections/${routeId}/save`, { - method: "POST", - body: JSON.stringify(body), - }); + return warnUnrecomputed( + requestJson(`/projects/${projectId}/sections/${routeId}/save`, { + method: "POST", + body: JSON.stringify(body), + }), + ); } /** 같은 회사에서 표준횡단 설계값을 불러올 수 있는 프로젝트 항목. */ diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 6711fed51..927d12fcd 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -358,6 +358,8 @@ export interface SectionConfirmResponse { project_id: string; route_id: number; confirmed: boolean; + /** 서버 정본 셈(면적 · 관 길이 · 유토곡선)이 됐나 — false 면 경고(388-2). */ + recomputed?: boolean; } /** 측점 표준횡단 설계 지정값 (버튼 상태). */ diff --git a/B06_Section/B06_Section_Router_Confirm.py b/B06_Section/B06_Section_Router_Confirm.py index e535d552f..7a446cd82 100644 --- a/B06_Section/B06_Section_Router_Confirm.py +++ b/B06_Section/B06_Section_Router_Confirm.py @@ -46,6 +46,7 @@ from B06_Section.B06_Section_Schema import ( SectionConfirmRequest, SectionConfirmResponse, ) +from B06_Section.B06_Section_Server_Calc_Prebuild import _AREA_KEYS from common_util.common_util_storage import resolve_stored_project_path from common_util.common_util_workflow_state import complete_stage, start_stage from config.config_db import get_db_pool @@ -147,10 +148,11 @@ async def _apply_section_edits( dumped = patch_item.model_dump() if dumped.pop("structure_reset", None): resets.append((patch_item.chainage_m, dict.fromkeys(STRUCTURE_ADJUST_KEYS))) + # 면적 · 관 길이(`_AREA_KEYS`)는 서버 셈만 — 브라우저가 보내도 안 받음(388-2) patch: dict[str, Any] = { key: value for key, value in dumped.items() - if key != "chainage_m" and value is not None + if key != "chainage_m" and key not in _AREA_KEYS and value is not None } if patch: patches.append((patch_item.chainage_m, patch)) @@ -160,7 +162,7 @@ async def _apply_section_edits( ) -async def _recompute_stored_designs(project_id: UUID, route_id: int) -> None: +async def _recompute_stored_designs(project_id: UUID, route_id: int) -> bool: """정본을 **서버가 다시 계산**한다 — 사용자 편집이 들어간 **뒤**에 돈다. 포장 구간·세월교 노면 하강 보정에 더해 구조물 면적·유토곡선(배분·운반거리 포함)까지 @@ -172,18 +174,21 @@ async def _recompute_stored_designs(project_id: UUID, route_id: int) -> None: 850ms → 약 980ms 인데(Node 131ms) 기다리는 조작이라 허용한다. 사용자가 끌어 옮긴 balloon 위치는 서버가 만들지 않으므로 저장분에서 떼어 도로 붙인다 - (`B06_Section_Server_Calc_Prebuild`). 실패는 비치명적이다 — 저장은 그대로 남는다. + (`B06_Section_Server_Calc_Prebuild`). 실패해도 저장은 남음 — False 를 돌려 + 응답 `recomputed: false` 로 화면에 알림(388-2). """ try: from B06_Section.B06_Section_Server_Calc_Prebuild import recompute_server_side await recompute_server_side(project_id, route_id) + return True except Exception: logger.exception( "저장분 서버 재계산 실패 (저장은 유지): project_id=%s route_id=%s", project_id, route_id, ) + return False def _prune_cross_files(project_root: Path, longitudinal_file_path: str) -> None: @@ -304,10 +309,10 @@ async def save_sections( await connection.rollback() raise # 편집이 들어간 **뒤** 서버가 정본을 다시 낸다 — 바뀐 벽·측점이 면적·유토곡선에 실린다. - await _recompute_stored_designs(project_id, route_id) + recomputed = await _recompute_stored_designs(project_id, route_id) await _prebuild_corridor(project_id, route_id, project_root) return SectionConfirmResponse( - project_id=str(project_id), route_id=route_id, confirmed=False + project_id=str(project_id), route_id=route_id, confirmed=False, recomputed=recomputed ) except Exception: logger.exception( @@ -407,7 +412,7 @@ async def confirm_sections( marks.append(("확정 저장(트랜잭션)", time.perf_counter())) # 편집이 들어간 **뒤** 서버가 정본을 다시 낸다(임시저장과 같은 자리). - await _recompute_stored_designs(project_id, route_id) + recomputed = await _recompute_stored_designs(project_id, route_id) marks.append(("서버 재계산", time.perf_counter())) # 측구 방향(design.ditch_side) 변경을 B05 종단 정본 stations.uphill_side에 역반영한다(E-7). @@ -439,7 +444,9 @@ async def confirm_sections( await _prebuild_corridor(project_id, route_id, project_root) marks.append(("3D 길 모형 서버 짓기", time.perf_counter())) _log_steps("B06 확정", marks) - return SectionConfirmResponse(project_id=str(project_id), route_id=route_id) + return SectionConfirmResponse( + project_id=str(project_id), route_id=route_id, recomputed=recomputed + ) except Exception: logger.exception("B06 종횡단 확정 실패: project_id=%s", project_id) return JSONResponse( diff --git a/B06_Section/B06_Section_Schema.py b/B06_Section/B06_Section_Schema.py index e9c31c6a8..30d923cf8 100644 --- a/B06_Section/B06_Section_Schema.py +++ b/B06_Section/B06_Section_Schema.py @@ -234,9 +234,8 @@ class CrossSectionPatch(BaseModel): # 측구를 둘지 **사용자 선택**(결과 `ditch_enabled` 와 다른 칸, 2026-09-09). None 은 # 「안 보냄」이라 병합에서 걷힌다 — 「자동」을 patch 로 되돌릴 길은 아직 없다. ditch_choice: bool | None = None - # 구조물이 선 측점의 폐회로 절·성토 면적(㎡) — 브라우저가 계산해 보낸다 - # (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). 서버는 초기값을 만들 때만 - # 같은 코드를 Node 로 돌린다(`B06_Section_Server_Calc_Node.ts`). + # 구조물이 선 측점의 폐회로 절·성토 면적(㎡) · 아래 관 길이 묶음 — 옛 화면 호환으로 받기만 하고 + # 저장에서 걷음(388-2 · `_AREA_KEYS`) · 정본은 서버가 Node 로 셈(`B06_Section_Server_Calc_Node.ts`). cut_area_m2: float | None = Field(default=None, ge=0) fill_area_m2: float | None = Field(default=None, ge=0) cut_soil_area_m2: float | None = Field(default=None, ge=0) @@ -283,6 +282,8 @@ class SectionConfirmResponse(BaseModel): project_id: str route_id: int confirmed: bool = True + # 서버 정본 셈(면적 · 관 길이 · 유토곡선)이 됐나 — False 면 화면이 경고 한 줄(388-2) + recomputed: bool = True class SectionOptionDefaults(BaseModel): diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 9532a11c1..fae86cfec 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -17,7 +17,7 @@ 으로 감싸 그대로 돌린다. 파이썬으로 포팅하면 같은 기하가 두 벌이 되어 「그림은 이런데 수량은 저렇다」가 생긴다. -실패는 비치명적이다 — 보정 전(표준) 값이 그대로 남고 화면은 예전처럼 스스로 고친다. +실패는 던진다 — 부르는 쪽([저장] · [확정])이 `recomputed: false` 로 화면에 알린다(388-2). """ from __future__ import annotations @@ -258,9 +258,8 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: ) marks.append(("종횡단 상세+DB 조회(병렬)", time.perf_counter())) payload = getattr(response, "model_dump", None) - if payload is None: # JSONResponse = 실패 - logger.warning("서버 재계산: 종횡단 상세를 못 받음 (route_id=%s)", route_id) - return 0 + if payload is None: # JSONResponse = 실패 — 삼키지 않음(388-2) + raise RuntimeError(f"서버 재계산: 종횡단 상세를 못 받음 (route_id={route_id})") detail = payload(mode="json") sections = detail.get("cross_sections") or [] project_root = Path(resolve_stored_project_path(stored_path)) @@ -307,8 +306,8 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: }, ) marks.append(("Node 번들(면적·유토곡선)", time.perf_counter())) - if not isinstance(output, dict): - output = {} + if not isinstance(output, dict): # Node 실패 — 삼키지 않음(388-2) + raise RuntimeError(f"서버 재계산: Node 면적 · 유토곡선 실패 (route_id={route_id})") rows = output.get("areas") mass_haul = output.get("mass_haul") # 선 다단 벽 목록(④) — 목록째 얹음(수가 아니라 `_AREA_KEYS` 로는 못 거름). 주인 측점엔 빈 목록도. diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index 4dfbd6730..fc804489e 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -32,11 +32,6 @@ import { BERM_DEFAULT_SLOPE_DEG, BERM_DEFAULT_WIDTH_M, } from "@util/common_util_cross_berm"; -import { - applyStructureAreaRows, - STRUCTURE_ROW_KEYS, - structureAreaRows, -} from "./B06_Section_Structure_Layouts"; import { crossDesignChoices, readBermDepths, @@ -429,10 +424,7 @@ export function collectSectionEdits(ctx: SectionPersistContext): { massHaul: Record | undefined; } { const crossPatches = buildCrossPatches(ctx.patchSources()); - const detail = ctx.detail(); - // 구조물 폐회로 면적 — **카드를 그리지 않은 측점까지** 여기서 다 계산해 싣는다 - // (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). 카드 그리기가 고쳐 둔 값에 - // 기대면 화면에 안 뜬 측점이 표준값으로 남는다. + // 측점별 patch — 없는 측점은 새로 만듦 const byChainage = new Map(crossPatches.map((patch) => [patch.chainage_m, patch])); const patchFor = (chainageM: number): CrossSectionPatch => { const existing = byChainage.get(chainageM); @@ -486,23 +478,7 @@ export function collectSectionEdits(ctx: SectionPersistContext): { }); // 관 종류를 바꾼 측점(318-4) — 서버가 옛 종류 조정값을 먼저 비우고 이 patch 값만 얹음 for (const at of kindChangedChainages(ctx.projectId)) patchFor(at).structure_reset = true; - if (detail) { - // 구조물 폐회로 면적 — **카드를 그리지 않은 측점까지** 여기서 다 계산한다 - // (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). - applyStructureAreaRows(detail.cross_sections, structureAreaRows(detail.cross_sections)); - // 면적은 **전 측점**을 싣는다 — 브라우저가 만든 값이 곧 작업본이다. 카드 버튼을 - // 바꾸면 구조물이 없는 측점의 면적도 달라지므로 구조물 측점만 보내면 수량이 어긋난다. - for (const section of detail.cross_sections) { - const design = section.design as Record | undefined; - if (!design) continue; - let patch: CrossSectionPatch | null = null; - for (const key of STRUCTURE_ROW_KEYS) { - if (typeof design[key] !== "number") continue; - patch = patch ?? patchFor(section.chainage_m); - patch[key] = design[key] as number; - } - } - } + // 면적 · 관 길이는 안 보냄 — 서버가 [저장] · [확정] 때 정본으로 다시 셈(388-2) // 유토곡선 **정본은 서버가 낸다**(2026-09-06 사용자 확정) — 저장 뒤 `recompute_server_side` // 가 Node 로 다시 계산해 덮어쓴다. 그래서 여기서는 곡선도 배분도 만들지 않는다. // 서버가 만들 수 없는 것 하나만 보낸다: 사용자가 끌어 옮긴 balloon 위치(화면값). diff --git a/resources/tester/test_384_s_area_keys.py b/resources/tester/test_384_s_area_keys.py new file mode 100644 index 000000000..916b22ecc --- /dev/null +++ b/resources/tester/test_384_s_area_keys.py @@ -0,0 +1,125 @@ +"""388-2 [저장] · [확정] 면적 · 관 길이 = 서버 셈만(요구 242 · 분석 1장 S3). + +- 브라우저가 면적 · 관 길이 9칸(`_AREA_KEYS`)을 보내도 정본에 안 적음 · 다른 칸은 그대로 +- 서버 재계산 실패를 삼키지 않음 — 상세 못 받음 · Node 실패 = 던짐 → 응답 `recomputed: false` +- 화면은 면적 · 관 길이를 안 보냄 · `recomputed: false` 면 경고 한 줄 +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +from types import SimpleNamespace +from uuid import uuid4 + +import pytest + +import B06_Section.B06_Section_Router_Confirm as confirm +import B06_Section.B06_Section_Server_Calc_Prebuild as prebuild +from B06_Section.B06_Section_Schema import SectionConfirmRequest, SectionConfirmResponse + +ROOT = Path(__file__).resolve().parents[2] + + +def test_면적_보내도_정본에_안_적음(monkeypatch): + written: list = [] + + async def merge(connection, *, route_id, entries, replace, project_id=None): + written.append((replace, entries)) + return 0 + + monkeypatch.setattr(confirm, "merge_cross_section_designs", merge) + forged = {key: 999.0 for key in prebuild._AREA_KEYS} + request = SectionConfirmRequest( + cross_patches=[{"chainage_m": 20.0, "ground_type": "rock", **forged}] + ) + asyncio.run(confirm._apply_section_edits(None, 3, request, [])) + patches = written[-1][1] + assert patches == [(20.0, {"ground_type": "rock"})] + + +def test_면적만_보낸_측점은_patch_없음(monkeypatch): + written: list = [] + + async def merge(connection, *, route_id, entries, replace, project_id=None): + written.append(entries) + return 0 + + monkeypatch.setattr(confirm, "merge_cross_section_designs", merge) + request = SectionConfirmRequest(cross_patches=[{"chainage_m": 40.0, "cut_area_m2": 12.5}]) + asyncio.run(confirm._apply_section_edits(None, 3, request, [])) + assert written[-1] == [] + + +def test_재계산_실패는_False(monkeypatch): + async def boom(project_id, route_id): + raise RuntimeError("Node 실패") + + async def ok(project_id, route_id): + return 1 + + monkeypatch.setattr(prebuild, "recompute_server_side", boom) + assert asyncio.run(confirm._recompute_stored_designs(uuid4(), 3)) is False + monkeypatch.setattr(prebuild, "recompute_server_side", ok) + assert asyncio.run(confirm._recompute_stored_designs(uuid4(), 3)) is True + + +def test_상세_못_받으면_던짐(monkeypatch): + import B06_Section.B06_Section_Router as b06_router + + async def no_detail(project_id, route_id): + return SimpleNamespace() # model_dump 없음 = JSONResponse 실패 + + async def run_with(fn, *args): + return None + + async def nothing(project_id): + return {} + + monkeypatch.setattr(b06_router, "get_section_detail", no_detail) + monkeypatch.setattr(prebuild, "run_with_connection", run_with) + monkeypatch.setattr(prebuild, "haul_inputs_for", nothing) + with pytest.raises(RuntimeError): + asyncio.run(prebuild.recompute_server_side(uuid4(), 3)) + + +def test_Node_실패면_던짐(monkeypatch, tmp_path): + import B06_Section.B06_Section_Router as b06_router + import B06_Section.B06_Section_Router_Design as b06_design + + detail = {"cross_sections": [{"chainage_m": 0.0, "design": {}}], "longitudinal": {}} + + async def got(project_id, route_id): + return SimpleNamespace(model_dump=lambda mode: detail) + + async def run_with(fn, *args): + return "p" if fn is prebuild.get_project_storage_relative_path else {} + + async def nothing(project_id): + return {} + + monkeypatch.setattr(b06_router, "get_section_detail", got) + monkeypatch.setattr(prebuild, "run_with_connection", run_with) + monkeypatch.setattr(prebuild, "haul_inputs_for", nothing) + monkeypatch.setattr(prebuild, "resolve_stored_project_path", lambda path: str(tmp_path)) + monkeypatch.setattr(b06_design, "enforce_design_lines", lambda *a: None) + monkeypatch.setattr(prebuild, "_enforce_stored_designs", lambda *a: None) + monkeypatch.setattr(prebuild, "quantity_settings", lambda root: {}) + monkeypatch.setattr(prebuild, "mixed_conversion_factors", lambda settings: {}) + monkeypatch.setattr(prebuild, "haul_equipment_limits", lambda settings: []) + monkeypatch.setattr(prebuild, "_node_output", lambda node_input: None) + with pytest.raises(RuntimeError): + asyncio.run(prebuild.recompute_server_side(uuid4(), 3)) + + +def test_응답에_recomputed(): + assert SectionConfirmResponse(project_id="p", route_id=1).recomputed is True + assert SectionConfirmResponse(project_id="p", route_id=1, recomputed=False).recomputed is False + + +def test_화면은_면적_안_보내고_경고(): + persist = (ROOT / "B06_Section/B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8") + assert "STRUCTURE_ROW_KEYS" not in persist + api = (ROOT / "B06_Section/B06_Section_Api_Fetch.ts").read_text(encoding="utf-8") + assert api.count("return warnUnrecomputed(") == 2 + assert "result.recomputed === false" in api diff --git a/resources/tester/test_76_stale_route_save.py b/resources/tester/test_76_stale_route_save.py index 7b20e4da8..eeb036805 100644 --- a/resources/tester/test_76_stale_route_save.py +++ b/resources/tester/test_76_stale_route_save.py @@ -63,8 +63,9 @@ def _wire(monkeypatch: pytest.MonkeyPatch, latest: int) -> list[str]: async def _edits(*_: Any, **__: Any) -> None: called.append("edits") - async def _recompute(*_: Any, **__: Any) -> None: + async def _recompute(*_: Any, **__: Any) -> bool: called.append("recompute") + return True # 388-2 — 재계산 됐나(응답 recomputed) monkeypatch.setattr(confirm, "get_db_pool", lambda: _Pool()) monkeypatch.setattr( diff --git a/resources/tester/test_b05_b06_save_roundtrip.py b/resources/tester/test_b05_b06_save_roundtrip.py index 1ead3975f..05b8a6ea9 100644 --- a/resources/tester/test_b05_b06_save_roundtrip.py +++ b/resources/tester/test_b05_b06_save_roundtrip.py @@ -99,7 +99,7 @@ def api(tmp_path, monkeypatch): monkeypatch.setattr(sections, "confirm_route_status", rec.fake("confirm_route")) monkeypatch.setattr(sections, "complete_stage", rec.fake("complete_stage")) monkeypatch.setattr(sections, "start_stage", rec.fake("start_stage")) - monkeypatch.setattr(sections, "_recompute_stored_designs", rec.fake("recompute")) + monkeypatch.setattr(sections, "_recompute_stored_designs", rec.fake("recompute", True)) # 272 — 측점을 다시 맞춘 뒤 정본에 없는 측점의 횡단 행 지우기 monkeypatch.setattr(route_confirm, "sync_cross_section_rows", rec.fake("sync_rows", 0)) monkeypatch.setattr( diff --git a/resources/tester/test_b06_pipe_length.py b/resources/tester/test_b06_pipe_length.py index 6ad4cb70b..4cab50137 100644 --- a/resources/tester/test_b06_pipe_length.py +++ b/resources/tester/test_b06_pipe_length.py @@ -45,7 +45,8 @@ def test_같은_길로_정본에_실린다(): block = source[source.index("STRUCTURE_ROW_KEYS = [") :][:300] assert '"pipe_length_m"' in block, "관 길이가 정본에 얹는 키 목록에 없다" assert '"cut_area_m2"' in block, "면적과 같은 목록이어야 한다(길이 하나만 따로 나르지 않는다)" - assert "STRUCTURE_ROW_KEYS" in _PERSIST.read_text(encoding="utf-8") + # 388-2 — 화면 [저장]은 면적 · 관 길이를 안 보냄(서버 셈만) + assert "STRUCTURE_ROW_KEYS" not in _PERSIST.read_text(encoding="utf-8") def test_면적을_못_내는_측점에서도_길이는_낸다(): @@ -65,7 +66,6 @@ def test_이름이_한_벌이다(): assert "pipe_length_m" in path.read_text(encoding="utf-8"), path.name persist = _PERSIST.read_text(encoding="utf-8") assert "pipe_length_m" not in persist, "이름을 또 적으면 갈릴 자리가 하나 더 생긴다" - assert "STRUCTURE_ROW_KEYS" in persist def test_서버_화이트리스트가_TS_목록과_짝이다(): diff --git a/resources/tester/test_b06_save_fills_empty_design.py b/resources/tester/test_b06_save_fills_empty_design.py index 822bd0d9b..900407141 100644 --- a/resources/tester/test_b06_save_fills_empty_design.py +++ b/resources/tester/test_b06_save_fills_empty_design.py @@ -78,7 +78,7 @@ def _run_save(monkeypatch: pytest.MonkeyPatch, missing: list[float]) -> list[flo monkeypatch.setattr(confirm, "_compute_default_designs", _spy) monkeypatch.setattr(confirm, "_apply_section_edits", _async(None)) - monkeypatch.setattr(confirm, "_recompute_stored_designs", _async(None)) + monkeypatch.setattr(confirm, "_recompute_stored_designs", _async(True)) # 388-2 recomputed from uuid import UUID diff --git a/ui_template/ui_template_locale_b4.ts b/ui_template/ui_template_locale_b4.ts index 4488597ca..8758bc4ef 100644 --- a/ui_template/ui_template_locale_b4.ts +++ b/ui_template/ui_template_locale_b4.ts @@ -114,4 +114,12 @@ export const ui_locales_b4 = { "Unsaved edits in B05 · B06 — save there first, then link.", ], B05_Design_Leave: ["B05 · B06 에 저장 안 한 조작이 있음", "Unsaved edits in B05 · B06"], + B06_Recompute_Failed: [ + "저장은 됨 — 서버 면적 · 관 길이 다시 셈 실패(옛 값 남음) · 다시 [저장]", + "Saved — server area · pipe length recompute failed (old values kept) · save again", + ], + Session_Storage_Full: [ + "저장 공간 부족 — [저장]을 눌러 고친 것을 서버에 남기세요", + "Browser storage full — press [Save] to keep your edits on the server", + ], } as const;