diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 2bf89825..5305e28d 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -61,10 +61,15 @@ export interface CrossRefreshInput { shouldApply?: () => boolean; } -/** 다시 계산해도 **살려 두는 값** — 화면 조작으로만 생기거나 상태를 나르는 필드다. */ -const PRESERVED_KEYS = [ - "status", - "pavement_suggested", +/** + * 계산이 만들지 않는 **사용자 값** — 다시 계산해도 살려 두고, [저장]·[확정]에도 이 목록으로 + * 실어 보낸다(`B06_Section_Section_Store`). 서버 `B06_Section_Router_Design.USER_TOUCHED_KEYS` + * 와 **짝**이며 갈리면 시험이 깨진다(`tmp/tests/test_b06_user_touched_keys.py`). + * + * 손으로 나열하는 자리를 하나로 모은 것이다(2026-09-07) — 나열이 흩어져 있어 새 값을 + * 더할 때 한 곳만 빠지면 그 값이 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`). + */ +export const USER_TOUCHED_KEYS = [ "display_half_width_m", "inlet_structure", "basin_adjust", @@ -77,6 +82,9 @@ const PRESERVED_KEYS = [ "revet_follow_grade", ] as const; +/** 다시 계산해도 살려 두는 값 — 위 사용자 값에 **상태를 나르는 둘**을 더한 것. */ +const PRESERVED_KEYS = ["status", "pavement_suggested", ...USER_TOUCHED_KEYS] as const; + function preserveUserFields( next: NonNullable, previous: CrossSection["design"], diff --git a/B06_Section/B06_Section_Router_Confirm.py b/B06_Section/B06_Section_Router_Confirm.py index 1f66658c..e4755389 100644 --- a/B06_Section/B06_Section_Router_Confirm.py +++ b/B06_Section/B06_Section_Router_Confirm.py @@ -119,51 +119,16 @@ async def _apply_section_edits( if request and request.cross_patches: patches: list[tuple[float, dict[str, Any]]] = [] for patch_item in request.cross_patches: - patch: dict[str, Any] = {} - if patch_item.rock_boundary_offset_m is not None: - patch["rock_boundary_offset_m"] = patch_item.rock_boundary_offset_m - if patch_item.display_half_width_m is not None: - patch["display_half_width_m"] = patch_item.display_half_width_m - if patch_item.inlet_structure is not None: - patch["inlet_structure"] = patch_item.inlet_structure - if patch_item.basin_adjust is not None: - patch["basin_adjust"] = patch_item.basin_adjust.model_dump() - # 기슭막이 4축·다단 단 수 — 세션 전용이던 값을 정본에 남긴다(2026-08-24). - if patch_item.revet_adjust is not None: - patch["revet_adjust"] = { - role: adjust.model_dump() for role, adjust in patch_item.revet_adjust.items() - } - if patch_item.ford_adjust is not None: - patch["ford_adjust"] = patch_item.ford_adjust.model_dump() - if patch_item.box_adjust is not None: - patch["box_adjust"] = patch_item.box_adjust.model_dump() - if patch_item.extra_wall_counts is not None: - patch["extra_wall_counts"] = patch_item.extra_wall_counts.model_dump() - if patch_item.extra_spans is not None: - patch["extra_spans"] = { - wall: span.model_dump() for wall, span in patch_item.extra_spans.items() - } - if patch_item.revet_link_detached is not None: - patch["revet_link_detached"] = patch_item.revet_link_detached - if patch_item.revet_follow_grade is not None: - patch["revet_follow_grade"] = patch_item.revet_follow_grade - # 카드 버튼 선택 — 브라우저가 고른 값을 그대로 정본에 얹는다(2026-09-06). - for choice_key in ( - "ground_type", - "section_mode", - "ditch_side", - "ditch_type", - "paved", - "two_stage_slope", - ): - choice = getattr(patch_item, choice_key) - if choice is not None: - patch[choice_key] = choice - # 구조물 폐회로 면적 — 브라우저가 계산해 보낸 값을 그대로 정본에 얹는다. - for area_key in ("cut_area_m2", "fill_area_m2", "cut_soil_area_m2", "cut_rock_area_m2"): - value = getattr(patch_item, area_key) - if value is not None: - patch[area_key] = value + # 필드를 손으로 나열하지 않는다(2026-09-07) — 나열이 네 곳에 흩어져 있어 새 값을 + # 더할 때 한 곳만 빠지면 그 값이 **조용히 사라졌다**(`extra_spans` 실사고 `b6941bd2`). + # 스키마가 곧 목록이므로 통째로 덤프하고 **최상위 None 만** 걷는다. + # (중첩 None 은 남긴다 — 기슭막이 4축의 `d: null` 은 「자동」이라는 뜻이다.) + dumped = patch_item.model_dump() + patch: dict[str, Any] = { + key: value + for key, value in dumped.items() + if key != "chainage_m" and value is not None + } if patch: patches.append((patch_item.chainage_m, patch)) # 측점 patch 도 한 문장으로 — 전 측점을 보내는 저장에서 왕복이 측점 수만큼 났다. diff --git a/B06_Section/B06_Section_Section_Store.ts b/B06_Section/B06_Section_Section_Store.ts index 12368851..b82cef1a 100644 --- a/B06_Section/B06_Section_Section_Store.ts +++ b/B06_Section/B06_Section_Section_Store.ts @@ -30,6 +30,7 @@ import { import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts"; import type { CrossSectionPatch, SectionDetailResponse } from "./B06_Section_Api_Fetch"; import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch"; +import { USER_TOUCHED_KEYS } from "./B06_Section_Cross_Refresh"; const cache = new Map(); const pending = new Map>(); @@ -160,21 +161,17 @@ export function crossPatchesFromCache(detail: SectionDetailResponse): CrossSecti if (!design) continue; const patch: CrossSectionPatch = { chainage_m: section.chainage_m }; let touched = false; - const put = (key: K, value: CrossSectionPatch[K]): void => { - if (value === undefined || value === null) return; - patch[key] = value; + // 필드를 손으로 나열하지 않는다(2026-09-07) — 목록은 `USER_TOUCHED_KEYS` 한 벌뿐이고 + // 서버 목록과 짝이다. 나열이 흩어져 있던 탓에 새 값을 더할 때 한 곳이 빠져 그 값이 + // 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`). + const source = design as unknown as Record; + const target = patch as unknown as Record; + for (const key of USER_TOUCHED_KEYS) { + const value = source[key]; + if (value === undefined || value === null) continue; + target[key] = value; touched = true; - }; - put("display_half_width_m", design.display_half_width_m); - put("inlet_structure", design.inlet_structure); - put("basin_adjust", design.basin_adjust); - put("revet_adjust", design.revet_adjust); - put("ford_adjust", design.ford_adjust); - put("box_adjust", design.box_adjust); - put("extra_wall_counts", design.extra_wall_counts); - put("extra_spans", design.extra_spans); - put("revet_link_detached", design.revet_link_detached); - put("revet_follow_grade", design.revet_follow_grade); + } if (touched) patches.push(patch); } return patches;