기슭막이 범위(길이·기준측점 전/후)를 B06 횡단도 조정창에서 지정하게 하고, 그 범위가 덮는 옆 측점의 기슭막이를 "연동" 파생물로 다루도록 체계를 세웠다. 값의 원천은 배수관 옵션(pipe_points) 하나다 — 같은 숫자를 B06 정본에 따로 담으면 B05 배수관 카드와 갈려 어느 쪽이 참인지 알 수 없다. ① 집수정 전/후 필드 신설 — inlet_basin_before_m·after_m(기본 각 1m), 길이 2m 유지. 종방향 구간 계산을 revetSpanOfSpec·basinSpanOfSpec 한 곳으로 모아 2D 링크 판정과 3D 스윕이 같은 값을 쓰게 했다. ② B06 조정창에 길이·전·후 행 — 기슭막이(유입·유출)·집수정 모두. 캐시를 고쳐 즉시 보여주고 저장은 조작이 멎으면 한 번만 나간다(세부유역 재계산이 매번 돌지 않게). 길이를 바꾸면 늘어난 몫만 지금 비율대로 나눠 담는다 — 총길이에서 비율로 다시 계산하면 0.1m 반올림이 쌓여 5.0/5.0이 5.6/5.4로 어긋난다. ③ 링크 카드 선택 허용 — "소유 측점 통째 복사"에서 "위치 4축만 물려받고 원지반 접합은 이 측점 지형으로 재계산"으로 바꿨다. 구조물 형식은 언제나 소유 측점 값이다. 손대기 전에도 어긋나지 않게 소유 측점이 실제로 적용한 4축(revetShift)을 명시값으로 물려준다. ④ 연동(측점별)·경사(전체 공통) 토글 — 연동 조작은 소유 측점 키에 실리고 그 연장이 덮는 측점을 함께 다시 그린다. 경사는 3D 전용 스위치다(끄면 스윕 프레임 dz=0). 횡단도까지 -dz로 밀면 노견에서 출발하는 성토선까지 내려가 노견과 벌어진다. ⑤ 가림 구간 선 정리 — 유입부 접속선과 유출부 접지 후 절토선을 2D·3D 모두에서 빼되 기하는 남긴다. 면적·수량 계산은 건드리지 않았다(전면 개편 대상). ⑥ 집수정은 소유 측점 횡단도 하나에만 — 링크 카드에서 집수정·계류측 다단을 뺀다. ⑦ 3D 구간 겹침 정리 — 측점 간격이 구조물 연장보다 좁으면 두 구간 제어점이 번갈아 서서 짧은 쪽(집수정 2m)이 잘려 보였다. 두 소유 측점의 중간에서 끊는다. ⑧ 조정창 정리 — 십자·9키를 맨 아래로, 창을 위쪽 기준 오버레이로(낮은 횡단도에서 제목이 잘리던 문제), 연동·경사를 1행 토글 버튼으로, 스크롤 자리 유지. 파일 분리: B06_Section_UI_Cross_View_Structure.ts(조정창 배선), B06_Section_Api_Culvert_Options.ts(구간값 저장) — 700줄 제한. BUILD_VERSION 14 → 17(저장 코리도 만료). tsc/ruff/prettier 통과, pytest 196 passed(기존 실패 1건 유지). 작업이력·자체검증 기록: docs/raw/PLAN.md §4-9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
3.8 KiB
TypeScript
98 lines
3.8 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_Api_Culvert_Options.ts
|
|
* B06 조정창에서 만진 **배수관 세트 구간값**(기슭막이·집수정 길이·전/후)을
|
|
* 원천인 `pipe_points.json`에 되돌려 쓴다(2026-08-24 사용자).
|
|
*
|
|
* 값을 B06 정본(design)에 따로 담지 않는 이유: 같은 숫자가 B05 배수관 카드에도
|
|
* 있어서 두 곳이 갈리면 어느 쪽이 참인지 알 수 없다. 원천은 하나여야 한다.
|
|
*
|
|
* 저장은 **묶어서 늦게** 한다 — +/- 한 번마다 PUT하면 세부유역 재계산이 매번
|
|
* 돌아간다. 화면은 캐시를 고쳐 즉시 반영하고, 저장은 마지막 조작 뒤 한 번만 간다.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
fetchDetailPipePoints,
|
|
saveDetailPipePoints,
|
|
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
|
|
/** 마지막 조작 뒤 이만큼 조용하면 저장한다(ms). */
|
|
const SAVE_DELAY_MS = 800;
|
|
|
|
/** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로. */
|
|
export type CulvertOptionPatch = Record<string, number>;
|
|
|
|
export interface CulvertOptionWriter {
|
|
/** 측점 옵션을 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */
|
|
queue: (chainageM: number, patch: CulvertOptionPatch) => void;
|
|
/** 예약분을 즉시 내보낸다(확정·임시저장 직전에 쓴다). */
|
|
flush: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* 저장기 하나를 만든다. `projectId`가 없으면 아무것도 하지 않는다(조회 전용 상태).
|
|
* `onError`는 저장 실패를 화면에 알리는 쪽이 구현한다.
|
|
*/
|
|
export function createCulvertOptionWriter(
|
|
projectId: () => string | null,
|
|
onError?: (message: string) => void,
|
|
): CulvertOptionWriter {
|
|
/** 측점(누가거리 문자열) → 아직 안 보낸 옵션 조각. */
|
|
const pending = new Map<string, CulvertOptionPatch>();
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
let inFlight: Promise<void> = Promise.resolve();
|
|
|
|
const keyOf = (chainageM: number): string => chainageM.toFixed(2);
|
|
|
|
async function send(): Promise<void> {
|
|
const id = projectId();
|
|
if (!id || pending.size === 0) {
|
|
pending.clear();
|
|
return;
|
|
}
|
|
const batch = new Map(pending);
|
|
pending.clear();
|
|
try {
|
|
const current = await fetchDetailPipePoints(id);
|
|
let touched = false;
|
|
const points: DetailPipeInput[] = current.pipe_points.map((point) => {
|
|
// 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약).
|
|
const { lonlat: _lonlat, ...input } = point;
|
|
const patch = batch.get(keyOf(point.chainage_m));
|
|
if (!patch) return input;
|
|
touched = true;
|
|
return { ...input, options: { ...(input.options ?? {}), ...patch } };
|
|
});
|
|
if (!touched) return;
|
|
await saveDetailPipePoints(id, points);
|
|
} catch (error) {
|
|
// 실패한 조각은 되살려 다음 기회에 다시 시도한다 — 조용히 잃으면 화면과
|
|
// 저장분이 갈린다.
|
|
batch.forEach((patch, key) => {
|
|
pending.set(key, { ...patch, ...(pending.get(key) ?? {}) });
|
|
});
|
|
onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패");
|
|
}
|
|
}
|
|
|
|
return {
|
|
queue(chainageM, patch) {
|
|
const key = keyOf(chainageM);
|
|
pending.set(key, { ...(pending.get(key) ?? {}), ...patch });
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
timer = null;
|
|
inFlight = inFlight.then(send);
|
|
}, SAVE_DELAY_MS);
|
|
},
|
|
async flush() {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
inFlight = inFlight.then(send);
|
|
await inFlight;
|
|
},
|
|
};
|
|
}
|