feat(B06): 측구 선택과 결과를 가른 칸 ditch_choice 추가

- 사용자가 누른 선택(`ditch_choice`)과 자동 판정 결과(`ditch_enabled`)를 다른 칸으로 나눔.
- 캐시(`CrossDesignChoice`)에 칸이 없어 토글이 화면에서 안 돌던 자리 이음 —
  선택이 있으면 그것을 따르고, 없으면 저장분 결과를 자동값과 다를 때만 선택으로 살림.
- 서버 patch 스키마에도 같은 칸을 둠. 최상위 `None` 은 병합에서 걷히므로
  「자동」으로 되돌리는 길은 아직 없음(카드 토글은 켬/끔 둘뿐이라 지금은 손해 없음).

화면 실측(카드에서 껐다 켜기)은 PLAN.md 3-16 에 미체크로 남아 있음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 20:32:50 +09:00
co-authored by Claude Opus 5
parent 404ec16e65
commit 5907de85c6
6 changed files with 18 additions and 1 deletions
+2
View File
@@ -560,6 +560,8 @@ export interface CrossSectionPatch {
ditch_type?: string | null;
paved?: boolean;
two_stage_slope?: boolean;
/** 측구를 둘지 **사용자 선택**(결과 `ditch_enabled` 와 다른 칸, 2026-09-09). */
ditch_choice?: boolean | null;
/** 절·성토 면적(㎡) — 브라우저가 계산해 보낸다(2026-09-06 사용자 확정). */
cut_area_m2?: number;
fill_area_m2?: number;
@@ -20,6 +20,9 @@ export interface CrossDesignChoice {
ditch_type?: string | null;
paved?: boolean;
two_stage_slope?: boolean;
/** 측구를 둘지 **사용자가 정한 선택**(`null` = 자동 판정). 결과(`ditch_enabled`)가 아니다.
* ⚠ 이 칸이 없으면 측구 토글이 눌려도 캐시에 안 담겨 **화면에서 안 돈다**(2026-09-09). */
ditch_choice?: boolean | null;
}
type ChoiceMap = Record<string, CrossDesignChoice>;
+5 -1
View File
@@ -289,7 +289,11 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
(design.two_stage_slope === undefined ? true : Boolean(design.two_stage_slope)),
// ⚠ **선택과 결과를 갈라 넘긴다**(2026-09-09). 선택이 있으면 그것을 따르고,
// 없으면 옛 저장분의 결과를 **자동값과 다를 때만** 선택으로 살린다.
ditchChoice: typeof design.ditch_choice === "boolean" ? design.ditch_choice : null,
ditchChoice: choice
? (choice.ditch_choice ?? null)
: typeof design.ditch_choice === "boolean"
? design.ditch_choice
: null,
ditchEnabled: typeof design.ditch_enabled === "boolean" ? design.ditch_enabled : null,
// 세월교 월류 하강은 계획선 편집으로 바뀌지 않는다 — 저장분 값을 그대로 잇는다.
surfaceDropM: typeof design.surface_drop_m === "number" ? design.surface_drop_m : 0,
+3
View File
@@ -170,6 +170,9 @@ class CrossSectionPatch(BaseModel):
ditch_type: str | None = None
paved: bool | None = None
two_stage_slope: bool | None = None
# 측구를 둘지 **사용자 선택**(결과 `ditch_enabled` 와 다른 칸, 2026-09-09). None 은
# 「안 보냄」이라 병합에서 걷힌다 — 「자동」을 patch 로 되돌릴 길은 아직 없다.
ditch_choice: bool | None = None
# 구조물이 선 측점의 폐회로 절·성토 면적(㎡) — 브라우저가 계산해 보낸다
# (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). 서버는 초기값을 만들 때만
# 같은 코드를 Node 로 돌린다(`B06_Section_Server_Calc_Node.ts`).
+2
View File
@@ -240,6 +240,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
ditch_type: change.ditch_type,
paved: change.paved,
two_stage_slope: change.two_stage_slope,
ditch_choice: change.ditch_choice,
};
sectionView.refreshCard(chainageM);
}
@@ -254,6 +255,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
ditch_type: change.ditch_type,
paved: change.paved,
two_stage_slope: change.two_stage_slope,
ditch_choice: change.ditch_choice,
});
// (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다.
await reconcileStaleDesigns({ force: true });
@@ -382,6 +382,9 @@ export function collectSectionEdits(ctx: SectionPersistContext): {
if (choice.ditch_type !== undefined) patch.ditch_type = choice.ditch_type;
if (choice.paved !== undefined) patch.paved = choice.paved;
if (choice.two_stage_slope !== undefined) patch.two_stage_slope = choice.two_stage_slope;
// ⚠ 「자동」(null)은 patch 로 못 보낸다 — 서버 병합이 최상위 null 을 걷는다. 카드 토글은
// 자동으로 되돌아가지 않으므로(켬/끔 둘 뿐) 지금은 손해가 없다(2026-09-09).
if (typeof choice.ditch_choice === "boolean") patch.ditch_choice = choice.ditch_choice;
});
if (detail) {
// 구조물 폐회로 면적 — **카드를 그리지 않은 측점까지** 여기서 다 계산한다