refactor(B06): 사용자 값 목록을 한 벌로 — 손나열 두 곳 제거
새 사용자 값을 더할 때 **네 곳을 다 고쳐야** 했고, 하나만 빠지면 그 값이 저장에서
조용히 사라졌음(`extra_spans` 실사고 `b6941bd2`). 나열을 걷어 목록 한 벌로 모음.
- 확정·저장 payload 조립(`_Router_Confirm.py`) — `if patch_item.X is not None:` 11개를
스키마 통째 덤프 + 최상위 None 걷기로 바꿈. 새 필드가 저절로 실림.
중첩 None 은 남김(기슭막이 4축 `d: null` 은 「자동」이라는 뜻).
- 브라우저 payload(`_Section_Store.ts`) — `put("X", design.X)` 10줄을 목록 순회로.
- `_Cross_Refresh.ts` 에 `USER_TOUCHED_KEYS` 를 내보내고 `PRESERVED_KEYS` 는
거기에 상태 둘을 더해 만들게 함. 서버 목록과 짝임을 머리에 적음.
전수 조사 결과 `design` 키는 세 갈래였음 — ① 재계산 인자로 되먹여 사는 것 8개
② 목록으로 베껴야 사는 것 12개 ③ 순수 계산값 24개. 위험한 칸은 ② 하나뿐임.
시험 3건 추가:
- `test_b06_design_key_split.py` — 사용자 값이 ①·② 어디에도 안 걸리면 깨짐(불변식).
지금 코드는 통과 = 알려지지 않은 구멍 없음.
- `test_b06_recompute_keeps_user_values.py` — 포장 재계산 뒤 ② 값 보존·① 인자 전달.
- `test_b06_user_touched_keys.py` — 손나열이 되살아나면 깨지는 검사 둘 추가.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<CrossSection["design"]>,
|
||||
previous: CrossSection["design"],
|
||||
|
||||
@@ -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 도 한 문장으로 — 전 측점을 보내는 저장에서 왕복이 측점 수만큼 났다.
|
||||
|
||||
@@ -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<string, SectionDetailResponse>();
|
||||
const pending = new Map<string, Promise<SectionDetailResponse>>();
|
||||
@@ -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 = <K extends keyof CrossSectionPatch>(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<string, unknown>;
|
||||
const target = patch as unknown as Record<string, unknown>;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user