Files
Aislo/B06_Section/B06_Section_UI_Page_Span_Control.ts
T
eomsangdon 6809f8a148 refactor(B06): 측점 제어기 세션 맵 통합·구간값 제어 분리 (843→643줄)
- `createSessionMap` 을 `_Page_Link_Session.ts` 에 추가 — 반폭·기슭막이 4축·유입
  형식·집수정·추가 벽 수·단별 구간값 6쌍이 같은 모양(읽기 → 항목 검증 → 담기 /
  통째 직렬화)이라 제어기마다 두던 load/persist 짝을 하나로 모음. 항목 검증과
  구형식 호환(숫자 x·d=0)은 `accept` 로 넘어감.
- `B06_Section_UI_Page_Span_Control.ts`(130줄) 신설 — 다단 단별 구간값과 유입·유출·
  집수정 구간값 제어를 세션·캐시·정본 큐와 함께 한 덩어리로 이동.
- 화면 검증: B06 카드 렌더 후 콘솔 오류 0, 조정창 DOM(구간 69·단 23) 그대로.
  표시 반폭 − 조작으로 세션 `b06:crossw` 에 13 기록, ⤢ 로 12 복귀 후 키 제거.
- `tsc --noEmit` 0, prettier 적용, pytest 359 passed.
2026-09-02 16:43:57 +09:00

132 lines
6.0 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Page_Span_Control.ts
* 구조물 **구간값(길이·전/후)** 제어 — 다단 단별 구간값과 유입·유출·집수정 구간값.
*
* `B06_Section_UI_Page_Station_Controls` 에서 떼어냈다(700줄 제한, 2026-09-02).
* 세션 맵(`extraSpans`)과 캐시(design)·정본 큐(`culvertOptions`)를 함께 만지는
* 자리라 한 덩어리로 옮겼다. 판정 규칙은 종전대로 `_Cross_Culvert_Const` 몫이다.
* ========================================================================== */
import type { CrossDesign, CrossSection, StoredWallSpan } from "./B06_Section_Api_Fetch";
import {
tierSpanOf,
type SpanValues,
type StructureSpanControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
import * as CulvertConst from "./B06_Section_UI_Cross_Culvert_Const";
/** 구간값 제어가 페이지·상위 제어기에서 받아 쓰는 창구. */
export interface SpanControlDeps {
detail: () => { cross_sections: CrossSection[] } | null;
refreshCard: (chainageM: number) => void;
/** 연동 구조물의 소유 측점 — 값은 소유 측점 하나에만 담는다. */
ownerOf: (section: CrossSection) => CrossSection | null;
/** 세션 맵(누가거리:벽키 → 구간값)과 저장. */
extraSpans: Map<string, SpanValues>;
persistExtraSpans: () => void;
patchCachedDesign: (chainageM: number, patch: Partial<CrossDesign>) => void;
/** 정본 반영 큐(관 옵션). */
culvertOptions: { queue: (chainageM: number, values: Record<string, number>) => void };
}
/** 구간값 제어기와 단별 구간값 조회를 만든다. */
export function createSpanControl(deps: SpanControlDeps): {
control: StructureSpanControl;
/** 이 측점의 단별 구간값 전부(세션 우선) — 상위 제어기가 payload에 실을 때 쓴다. */
tierSpansOf: (owner: CrossSection) => Record<string, StoredWallSpan>;
spanKeyOf: (chainageM: number, wall: string) => string;
} {
const { extraSpans, persistExtraSpans, patchCachedDesign, ownerOf, culvertOptions } = deps;
const spanKeyOf = (chainageM: number, wall: string): string => `${chainageM.toFixed(2)}:${wall}`;
const storedTierSpan = (owner: CrossSection, wall: string): SpanValues => {
const span = tierSpanOf(owner, wall);
return {
lengthM: Math.round((span.beforeM + span.afterM) * 10) / 10,
beforeM: Math.round(span.beforeM * 10) / 10,
afterM: Math.round(span.afterM * 10) / 10,
};
};
/** 이 측점의 단별 구간값 전부(세션 우선) — 캐시·payload에 실을 모양으로. */
const tierSpansOf = (owner: CrossSection): Record<string, StoredWallSpan> => {
const prefix = `${owner.chainage_m.toFixed(2)}:`;
const result: Record<string, StoredWallSpan> = { ...(owner.design?.extra_spans ?? {}) };
extraSpans.forEach((value, key) => {
if (!key.startsWith(prefix)) return;
result[key.slice(prefix.length)] = {
length_m: value.lengthM,
before_m: value.beforeM,
after_m: value.afterM,
};
});
return result;
};
const control: StructureSpanControl = {
ownerOf,
tierValuesFor: (section, key) => {
const owner = ownerOf(section) ?? section;
return extraSpans.get(spanKeyOf(owner.chainage_m, key)) ?? storedTierSpan(owner, key);
},
updateTier: (section, key, patch) => {
const owner = ownerOf(section);
if (!owner) return;
const mapKey = spanKeyOf(owner.chainage_m, key);
const current = extraSpans.get(mapKey) ?? storedTierSpan(owner, key);
const next = CulvertConst.applySpanPatch(current, patch);
extraSpans.set(mapKey, next);
persistExtraSpans();
// 캐시(design)에도 얹는다 — 링크 판정·3D가 순수 함수로 이 값을 읽는다.
patchCachedDesign(owner.chainage_m, { extra_spans: tierSpansOf(owner) });
// 연장이 바뀌면 링크되는 옆 측점이 달라진다 — 옛 연장·새 연장을 합친 구간만.
const reach = Math.max(current.beforeM, current.afterM, next.beforeM, next.afterM);
for (const other of deps.detail()?.cross_sections ?? []) {
if (Math.abs(other.chainage_m - owner.chainage_m) <= reach + 1e-9) {
deps.refreshCard(other.chainage_m);
}
}
},
valuesFor: (section, role) => {
const owner = ownerOf(section);
return owner ? CulvertConst.spanValuesOf(owner, role) : null;
},
update: (section, role, patch) => {
const owner = ownerOf(section);
if (!owner?.culvert) return;
const current = CulvertConst.spanValuesOf(owner, role);
if (!current) return;
// 길이↔전/후 산식은 다단과 공용(`applySpanPatch`).
const { lengthM, beforeM, afterM } = CulvertConst.applySpanPatch(current, patch);
const spec = role === "outlet" ? owner.culvert.outlet : owner.culvert.inlet;
const keys = CulvertConst.SPAN_OPTION_KEYS[role];
if (role === "basin") {
spec.basin_length_m = lengthM;
spec.basin_before_m = beforeM;
spec.basin_after_m = afterM;
} else {
spec.revet_length_m = lengthM;
spec.revet_before_m = beforeM;
spec.revet_after_m = afterM;
}
culvertOptions.queue(owner.chainage_m, {
[keys.length]: lengthM,
[keys.before]: beforeM,
[keys.after]: afterM,
});
// 연장이 바뀌면 링크되는 옆 측점 목록이 달라진다. 다시 그릴 대상은 **옛 연장과
// 새 연장을 합친 구간**뿐이다 — 측점 23개를 통째로 다시 그리면 조정창이 매번
// 새로 만들어져 연타한 +/-가 중간에 삼켜진다(2026-08-24 화면 실측: 3번 눌러
// 2번만 반영).
const reach = Math.max(current.beforeM, current.afterM, beforeM, afterM);
for (const other of deps.detail()?.cross_sections ?? []) {
if (Math.abs(other.chainage_m - owner.chainage_m) <= reach + 1e-9) {
deps.refreshCard(other.chainage_m);
}
}
},
};
return { control, tierSpansOf, spanKeyOf };
}