From f8fafd2300f359c3952af8b1f04d271945eef339 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 11:31:07 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(B06):=20=EC=95=94=20=EA=B2=BD=EA=B3=84?= =?UTF-8?q?=EC=84=A0=20=EC=98=A4=ED=94=84=EC=85=8B=EC=9D=84=20=EC=9D=BD?= =?UTF-8?q?=EB=8A=94=20=EC=AA=BD=EC=9D=B4=20=EC=98=9B=20=ED=82=A4=EB=A5=BC?= =?UTF-8?q?=20=EB=B3=B4=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 쓰는 쪽은 `stateKey("rockb", …)` = `aislo:draft:rockb:{p}:{r}` 에 쓰는데, 읽는 쪽 `readRockBoundarySession` 이 옛 키 `b06:rockb:{p}:{r}` 를 날문자열로 읽고 있었음. `writeStateRaw` 는 옛 키를 안 쓰므로 **읽는 쪽이 늘 빈 값**을 받았음. 그래서 계획선을 고쳐 횡단을 다시 계산할 때(`B06_Section_Cross_Refresh`) 사용자가 옮긴 암 경계선이 안 실려 나가 **토사/암 나눔이 기본값으로 되돌아갔음** — 수량이 갈리는 자리. [저장] payload 는 메모리 Map 을 써서 저장 자체는 무사했음. - 읽는 쪽을 등록표(`readState("rockb", …)`)로 바꿈. 옛 키 만드는 함수는 지움. - 실화면 실측(용화 5601e828 · route 169) — 경계선 -0.5 → -0.8m 로 옮기니 새 키에 `{"0.00":-0.8}` 가 쌓이고 **옛 키는 아예 없었음**(고치기 전 근거). 고친 뒤 새로고침해도 값·표기 그대로. 시험 `tmp/tests/test_session_keys_registered.py` 3건 — 등록표 밖 날문자열 세션 키를 쓰면 깨지는 그물 + 이 자리 못박기. **훑어 보니 다른 날문자열 키는 0개**로, `rockb` 가 유일한 구멍이었음. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Page_Persist.ts | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index 267761d1..5c90c6fa 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -21,6 +21,7 @@ import { flushPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures"; import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch"; import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches"; +import { readState } from "../A00_Common/b_page_state"; import { applyStructureAreaRows, STRUCTURE_AREA_KEYS, @@ -33,25 +34,22 @@ import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view" import { L } from "./B06_Section_UI_Page_Common"; /** - * 암 경계선 오프셋 세션 키 — 저장소와 재계산 창구가 **같은 자리**를 보게 정의처를 하나로 둔다. - * (`B06_Section_Cross_Refresh` 가 패널 없는 B05에서도 같은 값을 읽어 서버로 보낸다.) + * 세션에 쌓인 암 경계선 오프셋(측점키 → m). 없거나 손상되면 빈 객체. + * + * **등록표를 거쳐 읽는다**(2026-09-07 고침). 예전에는 옛 키 `b06:rockb:{p}:{r}` 를 날문자열로 + * 읽었는데, 쓰는 쪽(`B06_Section_UI_Page.ts` 의 `stateKey("rockb", …)`)은 새 키 + * `aislo:draft:rockb:{p}:{r}` 에 쓰고 있어 **읽는 쪽이 늘 빈 값**을 받았다. 그래서 + * 계획선을 고쳐 횡단을 다시 계산할 때(`B06_Section_Cross_Refresh`) 사용자가 옮긴 암 경계선이 + * 안 실려 나가 토사/암 나눔이 기본값으로 돌아갔다 — 수량이 갈리는 자리다. + * 실측(용화 5601e828, route 169): 경계선을 -0.5 → -0.8m 로 옮기니 새 키에만 `{"0.00":-0.8}` + * 가 쌓이고 옛 키는 아예 없었다. */ -export function rockBoundarySessionKey(projectId: string, routeId: number): string { - return `b06:rockb:${projectId}:${routeId}`; -} - -/** 세션에 쌓인 암 경계선 오프셋(측점키 → m). 없거나 손상되면 빈 객체. */ export function readRockBoundarySession( projectId: string, routeId: number, ): Record { - try { - const raw = window.sessionStorage.getItem(rockBoundarySessionKey(projectId, routeId)); - const parsed = raw ? (JSON.parse(raw) as unknown) : null; - return parsed && typeof parsed === "object" ? (parsed as Record) : {}; - } catch { - return {}; - } + const stored = readState>("rockb", projectId, routeId); + return stored && typeof stored === "object" ? stored : {}; } /** 암 경계선 오프셋 저장소 — 값(Map)과 조정창 제어기를 함께 낸다. */ From 647ec367d03c2125d1e92c7c2f98491b82a75fe6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 11:37:49 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs(A00):=20=ED=91=9C=EC=A4=80=EB=8B=A8?= =?UTF-8?q?=EB=A9=B4=20=EC=B4=88=EC=95=88=EC=9D=80=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=92=A4=EC=97=90=EB=8F=84=20=EC=9C=A0=EC=A7=80=EC=9E=84?= =?UTF-8?q?=EC=9D=84=20=EB=93=B1=EB=A1=9D=ED=91=9C=EC=97=90=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `std-cross` 가 [저장] 뒤에도 안 비는 것이 실수인지 의도인지 갈렸음 — **의도로 확정.** 다른 초안과 달리 이 값은 브라우저가 가진 **유일한** 사용자 표준단면임: `sections/context` 가 주는 `standard_cross_section` 은 저장분이 아니라 config 기본값이고 (`B06_Section_Router.py:132`), 저장분은 서버 안에서만 쓰임 (`stored_standard_cross_section`). 비우면 브라우저 횡단 계산이 그 순간 config 기본값으로 되돌아감. [초기화]에서만 버림. Co-Authored-By: Claude Opus 5 (1M context) --- A00_Common/b_page_state.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 57a6c426..7cfab979 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -109,7 +109,14 @@ export const STATE_REGISTRY = { culvertmove: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertmove:${p}:${r}` }, /** 암 경계선 오프셋(측점별). */ rockb: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:rockb:${p}:${r}` }, - /** 규격 횡단 측점별 지정. */ + /** 표준 횡단면 설정 패널의 편집값. + * + * ⚠ **[저장]·[확정] 뒤에도 지우지 않는다**(2026-09-07 확인). 다른 초안과 달리 이 값은 + * 브라우저가 가진 **유일한** 사용자 표준단면이다 — `sections/context` 가 주는 + * `standard_cross_section` 은 저장분이 아니라 **config 기본값**이고(`B06_Section_Router.py:132`), + * 저장분(`stored_standard_cross_section`)은 서버 안에서만 쓰인다. 그래서 비우면 브라우저 + * 횡단 계산이 그 순간 config 기본값으로 되돌아간다. [초기화]에서만 버린다 + * (`clearStandardCrossSession`). */ "std-cross": { bucket: "draft", scope: "project", legacy: (p) => `b06:std-cross:${p}` }, /** 표시 반폭 — 사용자가 고른 값이라 초안이되, 바꾸면 횡단 재생성(③)을 함께 부른다. */ "cross-display": { From 72e50b3eddfb786a058feb8a1645b8398da21256 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 11:47:27 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(B06):=20=EC=A0=80=EC=9E=A5=EB=90=9C=20?= =?UTF-8?q?=ED=91=9C=EC=A4=80=20=ED=9A=A1=EB=8B=A8=EB=A9=B4=EC=9D=B4=20?= =?UTF-8?q?=EC=83=88=20=ED=83=AD=EC=97=90=EC=84=9C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=20=EC=95=88=20=EC=84=9C=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 표준단면 편집값은 sessionStorage 에만 살아 **탭을 새로 열면** 사라졌음. 그때 브라우저는 config 기본값으로, 서버는 저장분으로 계산해 **같은 측점이 갈렸음**. 표준단면은 모든 측점의 횡단 모양을 정하므로 면적·유토곡선·수량까지 그대로 흐름. - `sections/context` 가 저장분을 **한 칸 더** 실어 보냄(`stored_standard_cross_section`). 기존 `standard_cross_section`(config 기본값)은 그대로 둠 — 옛 화면 안 깨짐, 마이그레이션 없음. - 브라우저는 두 화면이 함께 지나는 한 자리(`fetchSectionContext`)에서 **세션이 비었을 때만** 그 값으로 세움. 그 탭에서 고친 값이 있으면 안 건드림(초안 우선). 실측(용화 5601e828 · route 169): - 고치기 전 — 저장분 암반 횡단경사 **5%** · 측구 상단폭 **0.9m** 인데 화면이 받는 값은 **3%** · **0.69m**(config 기본값)였음. 응답에 저장분 칸 자체가 없었음. - 고친 뒤 — 응답에 0.9·5 가 실리고, **빈 새 탭**에서 B06 을 열면 세션이 `rock.ditch.top_width_m=0.9` · `rock.cross_slope_pct.max=5` 로 섬(검증 탭은 닫음). 시험 `test_b06_stored_standard_reaches_browser.py` 3건 — 칸이 따로 있고 기본은 None · 라우터가 저장분을 실음 · 브라우저가 세션이 빈 경우에만 세움(두 갈래 모두). Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_Api_Fetch.ts | 26 ++++++++++++++++++++++++-- B06_Section/B06_Section_Api_Types.ts | 3 +++ B06_Section/B06_Section_Router.py | 10 ++++++++++ B06_Section/B06_Section_Schema.py | 5 +++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/B06_Section/B06_Section_Api_Fetch.ts b/B06_Section/B06_Section_Api_Fetch.ts index 59d21970..626813ba 100644 --- a/B06_Section/B06_Section_Api_Fetch.ts +++ b/B06_Section/B06_Section_Api_Fetch.ts @@ -63,13 +63,35 @@ async function requestJson(path: string, init: RequestInit): Promise { * (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */ export async function fetchSectionContext(projectId: string): Promise { const cached = readState("section-context", projectId); - if (cached) return cached; + if (cached) return seedStandardCross(projectId, cached); const fresh = await requestJson( `/projects/${projectId}/sections/context`, { method: "GET" }, ); writeState("section-context", fresh, projectId); - return fresh; + return seedStandardCross(projectId, fresh); +} + +/** + * 저장된 표준 횡단면을 **세션이 비어 있을 때만** 채운다(2026-09-07). + * + * 브라우저 횡단 계산은 `세션 ?? config 기본값` 으로 서는데, 표준단면 세션값은 그 탭에서만 + * 산다. 그래서 **탭을 새로 열면** 화면은 config 기본값으로, 서버는 저장분으로 계산해 + * 같은 측점이 갈렸다(실측 — 용화 route 169 저장분은 암반 횡단경사 **5%**·측구 상단폭 + * **0.9m**, config 기본값은 **3%**·**0.69m**). + * + * 여기가 두 화면(B05·B06)이 함께 지나는 유일한 자리라 이 한 곳에서 채운다. 사용자가 + * 그 탭에서 고친 값이 있으면 **건드리지 않는다** — 초안이 언제나 우선이다. + */ +function seedStandardCross( + projectId: string, + context: SectionContextResponse, +): SectionContextResponse { + const stored = context.stored_standard_cross_section; + if (stored && readState("std-cross", projectId) === null) { + writeState("std-cross", stored, projectId); + } + return context; } export function clearSectionContextCache(projectId: string): void { diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index f0900921..42dacd1b 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -74,6 +74,9 @@ export interface SectionContextResponse { defaults: SectionOptionDefaults; /** 표준 횡단면 설정 패널(토사/암/포장) 기본값. */ standard_cross_section: StandardCrossSection; + /** 이 프로젝트에 **저장된** 표준 횡단면(사용자가 고쳐 확정한 값). 없으면 null. + * 위 `standard_cross_section` 은 config 기본값이라 둘은 다른 것이다(2026-09-07). */ + stored_standard_cross_section?: StandardCrossSection | null; /** 암 경계선 기본 오프셋(m)과 상/하 제어 스텝(m). */ rock_boundary_default_offset_m: number; rock_boundary_step_m: number; diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 2dfe6718..c497a156 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -113,6 +113,15 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON run_with_connection(_road_type), ) + # 저장된 표준 횡단면 — 브라우저 계산이 서버와 같은 값을 쓰게 함께 내려보낸다 + # (2026-09-07). 노선이 없으면 저장분도 없다. + stored_standard: dict[str, Any] | None = None + if route_context and route_context.get("route_id") is not None: + longitudinal_row = await run_with_connection( + get_longitudinal_section, project_id, int(route_context["route_id"]) + ) + stored_standard = _stored_standard_cross_section(longitudinal_row) + defaults = SectionGenerationOptions() return SectionContextResponse( project_id=str(project_id), @@ -130,6 +139,7 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON vertical_exaggeration=SECTION_VERTICAL_EXAGGERATION, ), standard_cross_section=STANDARD_CROSS_SECTION, + stored_standard_cross_section=stored_standard, rock_boundary_default_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M, rock_boundary_step_m=STANDARD_ROCK_BOUNDARY_STEP_M, earthwork_conversion=EARTHWORK_CONVERSION_FACTORS, diff --git a/B06_Section/B06_Section_Schema.py b/B06_Section/B06_Section_Schema.py index 9a24481b..db92a93d 100644 --- a/B06_Section/B06_Section_Schema.py +++ b/B06_Section/B06_Section_Schema.py @@ -238,6 +238,11 @@ class SectionContextResponse(BaseModel): defaults: SectionOptionDefaults # 표준 횡단면 설정 패널(토사/암/포장) 기본값. config STANDARD_CROSS_SECTION 사본. standard_cross_section: dict[str, Any] = Field(default_factory=dict) + # 이 프로젝트에 **저장된** 표준 횡단면(사용자가 고쳐 [확정]한 값). 없으면 None(=기본값 그대로). + # 2026-09-07 추가 — 예전에는 브라우저가 이 값을 받을 길이 없어, 세션이 빈 새 탭에서 + # **화면은 config 기본값으로, 서버는 저장분으로** 계산해 같은 측점이 갈렸다. + # 기존 `standard_cross_section`(기본값)은 그대로 두고 **한 칸만 더한다**. + stored_standard_cross_section: dict[str, Any] | None = None # 암 경계선 기본 오프셋(m)과 상/하 제어 스텝(m). rock_boundary_default_offset_m: float = -0.5 rock_boundary_step_m: float = 0.1