From 2eb2c97293fa04ee8f45ad59c641fe0e314391ff Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 21:58:07 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EC=A0=80=EC=9E=A5=EC=9D=B4=20?= =?UTF-8?q?=EC=98=9B=20=EB=A9=B4=EC=A0=81=EC=9D=84=20=EC=8B=A3=EB=8D=98=20?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=E2=80=94=20=EC=A0=80=EC=9E=A5=20=EC=A7=81?= =?UTF-8?q?=EC=A0=84=20=EC=A0=84=20=EC=B8=A1=EC=A0=90=20=EC=9E=AC=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20+=20=E3=80=8C=EC=95=88=20=EC=8B=A4=EB=A6=BC?= =?UTF-8?q?=E3=80=8D=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠ 잡은 자리(2026-09-09 실측) — 표준단면 「측구 상폭」을 0.69 → 0.9 로 고쳐 저장했더니 정본 한 줄에 **새 측구 기하(상폭 0.9 · 측구 0.18㎡)** 와 **옛 절토 면적(3.33㎡)** 이 섞여 남았음. 브라우저는 만진 측점만 다시 계산하는데 저장은 전 측점 면적을 실어 보내므로, 안 만진 측점이 옛 값 그대로 정본이 됨. 수량이 갈리는 자리임. - [저장]·[확정]이 면적을 모으기 **전에** 전 측점을 다시 계산함(`reconcileDesigns`). - 표준단면 패널에 「고친 값이 측점에 아직 안 실렸음」 알림 — 언제 따라오는지 함께 적음. ⭐ 거울(파이썬·TS) 문제가 아님을 먼저 가름 — 같은 입력을 주면 두 쪽이 같은 값을 냄 (측점 20.0m: 0.69 기준 절토 3.328 · 0.9 기준 3.899, 저장분은 3.328 + 측구 0.180 으로 섞여 있었음). 시험 둘 추가(저장·확정이 재계산을 먼저 부르는지), 전체 1205 통과·실패 0. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Page.ts | 3 ++ B06_Section/B06_Section_UI_Page_Persist.ts | 9 ++++ B06_Section/B06_Section_UI_Standard_Panel.ts | 32 +++++++++++++- B06_Section/B06_Section_UI_Style.css | 10 +++++ resources/tester/test_b06_save_recompute.py | 44 ++++++++++++++++++++ 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 resources/tester/test_b06_save_recompute.py diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 567e9119..178b74b8 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -621,6 +621,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { context: () => context, standardValues: () => standardPanel?.getValues(), flushCulvertOptions: () => stationControls.flushCulvertOptions(), + // 저장 직전 전 측점 재계산 — 표준단면·계획선을 고친 뒤 안 만진 측점이 옛 면적으로 + // 실려 나가던 자리(2026-09-09 실측). + reconcileDesigns: () => reconcileStaleDesigns({ force: true }), patchSources: () => ({ rockOffsets, cutSlopeRatios, diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index e69a3d1d..41299ad5 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -351,6 +351,11 @@ export interface SectionPersistContext { flushCulvertOptions: () => Promise; /** 측점별 편집분의 출처 묶음 — 세션·제어기에 흩어진 값을 페이지가 모아 준다. */ patchSources: () => CrossPatchSources; + /** 저장 직전 **전 측점을 다시 계산**한다(표준단면·계획선 최신값으로). + * ⚠ 없으면 옛 면적이 그대로 실려 나간다 — 2026-09-09 실측: 표준단면 암 측구 상폭을 + * 0.69 → 0.9 로 고친 뒤 저장했더니 정본 한 줄에 **새 측구 기하(0.9) + 옛 절토 면적 + * (0.69 기준 3.33㎡)** 이 섞여 남았다. 만진 측점만 새 값으로 서던 자리다. */ + reconcileDesigns: () => Promise; } /** 확정과 임시 저장이 함께 보내는 편집분(암 경계선 오프셋 + 유토곡선 + balloon 위치). */ @@ -444,6 +449,9 @@ export async function saveCurrentSections(ctx: SectionPersistContext): Promise writeSession(projectId, state); + // ⚠ **패널 값이 측점에 실렸는지**를 보인다(2026-09-09 실측). 패널을 고쳐도 측점은 그대로라 + // [전체 반영]을 눌러야 따라오는데, **카드를 하나 만지면 그 측점만** 새 값으로 다시 선다 + // (`handleDesignChange` 가 패널 값을 실어 보냄). 알려 주지 않으면 한 화면에 옛 값과 새 값이 + // 섞여 서고, 사용자는 왜 그 측점만 값이 달라졌는지 알 길이 없다. + // ⓘ 실측 예 — 측점 20.0m 절토 계 3.33㎡(저장분) ↔ 3.90㎡(패널 초안: 암 측구 상폭 0.69→0.9). + let applied = JSON.stringify(defaults); + const pending = document.createElement("p"); + pending.className = "b06-std__pending"; + pending.textContent = + "고친 값이 측점에 아직 안 실렸습니다 — [전체 측점 반영]을 누르면 바로 따라오고, " + + "[저장]·[확정] 때도 전 측점이 이 값으로 다시 섭니다. 그전에 카드를 만지면 그 측점만 먼저 바뀝니다."; + const showPending = (): void => { + const now = JSON.stringify(state); + // 값이 실렸는지 눈으로도 캘 수 있게 남긴다 — 화면 실측에서 판정 근거가 된다. + pending.dataset.now = String(now.length); + pending.dataset.applied = String(applied.length); + pending.hidden = now === applied; + }; + + const persist = (): void => { + writeSession(projectId, state); + showPending(); + }; /** 필드 한 칸. 공통은 토사 값을 보이고, 고치면 세 그룹에 함께 펼친다. */ const buildField = (spec: NumberFieldSpec, grid: HTMLElement): void => { @@ -411,17 +433,23 @@ export function createStandardPanel( actions.append(resetButton); // 횡단 반폭은 사이드의 **별도 컨테이너**로 뺐다(2026-08-23) — 여기는 표준단면 설정만. - root.append(body, loader, actions); + showPending(); + root.append(body, loader, pending, actions); return { root, getValues: () => state, applyStored: (stored) => { + // 측점에 실려 있는 값이 곧 정본이다 — 「안 실린 값」 판정 기준을 여기서 잡는다. + applied = JSON.stringify(stored); + showPending(); if (hadSession) return; // 진행 중 세션 편집값이 우선. (Object.keys(stored) as StandardCrossKey[]).forEach((key) => { if (stored[key]) state[key] = JSON.parse(JSON.stringify(stored[key])) as StandardCrossGroup; }); unifyCommon(state); + applied = JSON.stringify(state); + showPending(); renderBody(); }, }; diff --git a/B06_Section/B06_Section_UI_Style.css b/B06_Section/B06_Section_UI_Style.css index fb072053..89fed600 100644 --- a/B06_Section/B06_Section_UI_Style.css +++ b/B06_Section/B06_Section_UI_Style.css @@ -318,6 +318,16 @@ gap: var(--spacing-8); } +/* 고친 값이 아직 측점에 안 실렸다는 알림 — [전체 측점 반영] 바로 위. */ +.b06-std__pending { + margin: var(--spacing-8) 0 0; + padding: var(--spacing-8); + border-left: 3px solid var(--color-warning, #d98324); + background: var(--color-surface-muted, rgb(217 131 36 / 8%)); + font-size: 0.85em; + line-height: 1.45; +} + /* --- 우측 결과 --- */ .b06-profile__result { display: flex; diff --git a/resources/tester/test_b06_save_recompute.py b/resources/tester/test_b06_save_recompute.py new file mode 100644 index 00000000..162bceeb --- /dev/null +++ b/resources/tester/test_b06_save_recompute.py @@ -0,0 +1,44 @@ +"""[저장]·[확정]은 **면적을 모으기 전에** 전 측점을 다시 계산한다 (2026-09-09 실측). + +⚠ 잡은 자리 — 표준단면 「암 측구 상폭」을 0.69 → 0.9 로 고쳐 저장했더니 정본 한 줄에 + **새 측구 기하(상폭 0.9 · 측구 0.18㎡)** 와 **옛 절토 면적(0.69 기준 3.33㎡)** 이 섞여 + 남았다. 브라우저는 만진 측점만 다시 계산하는데 저장은 **전 측점 면적**을 실어 보내므로, + 안 만진 측점이 옛 값 그대로 정본이 된다. + (같은 입력을 주면 파이썬·TS 는 같은 값을 낸다 — 거울 문제가 아니라 **입력이 갈린 것**이다.) + +⇒ `saveCurrentSections`·`confirmCurrentSections` 가 `collectSectionEdits` **앞에서** + `ctx.reconcileDesigns()` 를 부른다. 순서가 뒤집히면 옛 면적이 그대로 실린다. +""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +PERSIST = ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts" +PAGE = ROOT / "B06_Section" / "B06_Section_UI_Page.ts" + + +def _body(source: str, marker: str) -> str: + start = source.index(marker) + return source[start : start + 1200] + + +def test_저장이_면적을_모으기_전에_다시_계산한다() -> None: + text = PERSIST.read_text(encoding="utf-8") + for marker in ( + "export async function saveCurrentSections", + "export async function confirmCurrentSections", + ): + body = _body(text, marker) + assert "ctx.reconcileDesigns()" in body, f"{marker} 가 재계산을 안 부른다" + assert body.index("ctx.reconcileDesigns()") < body.index("collectSectionEdits(ctx)"), ( + f"{marker}: 재계산이 면적 수집보다 뒤에 있다 — 옛 면적이 실린다" + ) + + +def test_페이지가_재계산_통로를_이어_준다() -> None: + text = PAGE.read_text(encoding="utf-8") + assert "reconcileDesigns: () => reconcileStaleDesigns({ force: true })" in text, ( + "페이지가 통로를 안 이어 주면 저장이 옛 값 그대로 나간다" + )