Files
Aislo/B06_Section/logic/B06_Section_UI_Page_Patches.ts
T
eomsangdonandClaude Opus 5.5 857b2d89d4 fix(445 B06 저장 patch): 바탕 = 캐시 설계 사용자 칸 + 세션 얹음 · 되돌림 null · 옮긴 측점 입력 · 집수정 범위 · 버린 수
- H3 — 횡단 patch 바탕을 캐시 설계 사용자 칸(측점마다 통째)으로 · 세션 고침은 벽 · 단 · 쪽 열쇠마다 합침(mergeCrossPatches)
- M2 — 벽 ↺ · 연동 풂 = 세션 null(안쪽 열쇠 지움) · 추가 벽 0 · 빈 전체 폭 0 을 세션에 남김 · 새 탭 − 가 저장분 단 수에서 출발
- H2 — 옮긴 관 · 구조물(정본 행 없음) = 지반 등 입력(CARRY_INPUT_KEYS)까지 실음 · 화면도 들고 감 · 서버 기본 설계가 그 입력으로 섬
- H5 — 노선 바꾼 [저장] 본문에 카드 선택 · 암선 · 암 경사 · 표준 횡단면 · 풍선 자리
- M1 — 집수정 내공 화면 범위 = 서버 1.0~2.0
- M5 — /save · /confirm 응답에 버린 patch 측점 · 기본 설계 실패 · 측점 맞춤 실패 · 화면 알림
- M7 — kindreset 을 측점 옮김 · 노선 저장에 따라 옮김
- 시험 test_445_save_patch · 기존 시험 꼴 맞춤(318 · 384 · 428 · 429 · fills_empty)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-18 18:55:25 +09:00

130 lines
6.6 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Page_Patches.ts
* 확정·임시저장이 보내는 **측점별 편집분(cross_patches) 조립** — 페이지
* (`_UI_Page.ts`)에서 700줄 제한으로 분리했다(2026-08-25).
*
* 세션·제어기에 흩어져 있던 조작값을 누가거리 하나로 합친다. 값은 만들지 않고
* 모으기만 한다 — 판정·한계는 각 제어기가 이미 끝냈다.
* ========================================================================== */
import type { CrossSectionPatch, StoredWallAdjust, StoredWallSpan } from "../B06_Section_Api_Fetch";
import type { BasinAdjust } from "../B06_Section_UI_Cross_Culvert_Types";
import type { FordAdjust } from "../B06_Section_UI_Cross_Ford";
import type { BoxAdjust } from "../B06_Section_UI_Cross_Box";
import type { InletStructureChoice } from "../B06_Section_UI_Cross_Culvert";
export interface CrossPatchSources {
/** 암 경계선 오프셋(누가거리 문자열 키). */
rockOffsets: Map<string, number>;
/** 측점별 암 절토 경사비(1:n 의 n). **0 은 「표준값을 씀」**(되돌리기)이다. */
cutSlopeRatios: Map<string, number>;
/** 측점 개별 표시 반폭(m). */
stationWidths: Map<string, number>;
inletStructures: Map<string, InletStructureChoice>;
basinAdjustments: Map<string, BasinAdjust>;
/** 벽 역할 → 값 · null = 되돌림(바탕 dict 에서 그 벽을 뺌 · 445 M2). */
revetAdjusts: Map<number, Record<string, StoredWallAdjust | null>>;
/** 세션에 있는 쪽만(445 H3 — 없는 쪽은 바탕 값 그대로). */
extraCounts: Map<number, Partial<{ outlet: number; basin: number }>>;
/** 다단 단별 구간값(2026-08-29) — 키는 벽 키("extra0"…/"bextra0"…). */
extraSpans: Map<number, Record<string, StoredWallSpan>>;
fordAdjusts: Map<number, FordAdjust>;
boxAdjusts: Map<number, BoxAdjust>;
linkFlags: Map<number, { detached?: boolean; followGrade?: boolean }>;
}
/** 누가거리별 패치를 합쳐 한 배열로 돌려준다. */
export function buildCrossPatches(sources: CrossPatchSources): CrossSectionPatch[] {
const patchByChainage = new Map<number, CrossSectionPatch>();
const patchFor = (chainageM: number): CrossSectionPatch => {
const existing = patchByChainage.get(chainageM);
if (existing) return existing;
const created: CrossSectionPatch = { chainage_m: chainageM };
patchByChainage.set(chainageM, created);
return created;
};
sources.rockOffsets.forEach((offset, chainage) => {
patchFor(Number(chainage)).rock_boundary_offset_m = offset;
});
// 측점별 암 절토 경사(2026-09-07) — 0 이면 「표준값을 씀」이라 정본의 옛 값을 덮어 지운다.
sources.cutSlopeRatios.forEach((ratio, chainage) => {
patchFor(Number(chainage)).cut_slope_ratio_user = ratio;
});
// 개별 표시 반폭(2026-08-06) — design에 병합돼 재접근 시 유지된다.
sources.stationWidths.forEach((width, chainage) => {
patchFor(Number(chainage)).display_half_width_m = width;
});
sources.inletStructures.forEach((structure, chainage) => {
patchFor(Number(chainage)).inlet_structure = structure;
});
sources.basinAdjustments.forEach((adjust, chainage) => {
patchFor(Number(chainage)).basin_adjust = adjust;
});
// 기슭막이 4축·다단 단 수 — 세션 전용이던 값을 정본에 실어 확정한다(2026-08-24).
// 되돌림(null) · 한쪽 단 수는 바탕과 합칠 때 풀림(`mergeCrossPatches`) — 혼자 나가면 null 걷고 빈 쪽 0
sources.revetAdjusts.forEach((adjusts, chainage) => {
patchFor(chainage).revet_adjust = adjusts as Record<string, StoredWallAdjust>;
});
sources.extraCounts.forEach((counts, chainage) => {
patchFor(chainage).extra_wall_counts = counts as { outlet: number; basin: number };
});
// 다단 단별 구간값 — 세션에서 만진 값을 정본에 싣는다(2026-08-29 사용자).
sources.extraSpans.forEach((spans, chainage) => {
patchFor(chainage).extra_spans = spans;
});
// 세월교 측벽 조작값(2026-08-25) — 배수관 값과 같은 자리에 실어 3D·재계산이 잇는다.
sources.fordAdjusts.forEach((adjust, chainage) => {
patchFor(chainage).ford_adjust = adjust;
});
sources.boxAdjusts.forEach((adjust, chainage) => {
patchFor(chainage).box_adjust = adjust;
});
// 연동 해제(측점별)·종단경사 반영(전체 공통) — 같은 체계로 정본에 싣는다.
sources.linkFlags.forEach((flags, chainage) => {
if (flags.detached !== undefined) patchFor(chainage).revet_link_detached = flags.detached;
if (flags.followGrade !== undefined) patchFor(chainage).revet_follow_grade = flags.followGrade;
});
return [...patchByChainage.values()];
}
/** 벽 · 단 · 쪽 열쇠로 담는 칸 — 세션은 만진 열쇠만 들고 있어 바탕(캐시 설계)과 **열쇠마다** 합침(445 H3). */
const DICT_KEYS = new Set(["revet_adjust", "extra_spans", "extra_wall_counts"]);
type Loose = Record<string, unknown>;
const isDict = (value: unknown): value is Loose =>
!!value && typeof value === "object" && !Array.isArray(value);
/** 안쪽 null = 되돌림 — 그 열쇠를 뺌(서버는 dict 를 통째 갈아 끼움). */
const withoutNulls = (dict: Loose): Loose =>
Object.fromEntries(Object.entries(dict).filter(([, value]) => value !== null));
/**
* 저장 patch = 바탕(캐시 설계의 사용자 칸 · 측점마다 통째) 위에 세션 고침을 얹음(445 H2 · H3 · M2).
* 같은 측점 = 누가거리 0.01 m 글. 벽 · 단 · 쪽 dict 는 열쇠마다 합치고 안쪽 null 은 뺌 · 나머지 칸은 세션이 이김.
* 바탕에 없는 측점의 세션 고침도 그대로 나감(안쪽 null 걷고 · 단 수 빈 쪽 0).
*/
export function mergeCrossPatches(
base: readonly CrossSectionPatch[],
overlay: readonly CrossSectionPatch[],
): CrossSectionPatch[] {
const out = new Map<string, Loose>();
const join = (patch: CrossSectionPatch): void => {
const at = patch.chainage_m.toFixed(2);
const into = out.get(at) ?? { chainage_m: patch.chainage_m };
for (const [key, value] of Object.entries(patch)) {
if (key === "chainage_m" || value === undefined) continue;
into[key] =
DICT_KEYS.has(key) && isDict(value)
? withoutNulls({ ...(isDict(into[key]) ? into[key] : {}), ...value })
: value;
}
out.set(at, into);
};
base.forEach(join);
overlay.forEach(join);
for (const patch of out.values()) {
const counts = patch.extra_wall_counts;
if (isDict(counts)) patch.extra_wall_counts = { outlet: 0, basin: 0, ...counts };
}
return [...out.values()] as unknown as CrossSectionPatch[];
}