/* ============================================================================= * B05_Profile_Api_PlusStations.ts * 추가 측점(368-2 · 요구 240-21) — 종단 원지반선에서 넣은 일반 측점(정수 m · 이름 없음). * * 초안 = 설계 가게 `plusStations`(세션 · 되돌리기 사진첩에 듦) · 저장분 = 서버 `plus_stations.json` * (`GET/PUT /section/plus-stations`) · [저장] 차례 = 구조물 뒤 · 계획고 앞(`design_save`). * 측점 계획(`planStations`)이 읽는 곳 = 초안 ?? 저장분(`setPlusStations` · 부를 때마다 읽음). * ========================================================================== */ import { API_BASE_URL, API_TIMEOUT_MS } from "../config/config_frontend"; import { draftProjectId, useDesignStore } from "../A00_Common/design/design_store"; import { onPageHistory } from "./B05_Profile_UI_Profile_History"; import type { SavePart } from "../A00_Common/design/design_save_api"; import type { FollowingStructure } from "../common_util/common_util_route_follow"; const saved = new Map(); /** 저장분 땅 위 자리(누가거리 · x · y) — 노선 초안 동안 이 좌표에서 따라감(493-3). */ const savedRows = new Map(); const loaded = new Map>(); async function request(projectId: string, init: RequestInit = {}): Promise { const controller = new AbortController(); const timer = window.setTimeout(() => controller.abort(), API_TIMEOUT_MS); try { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/section/plus-stations`, { credentials: "include", headers: { "Content-Type": "application/json" }, signal: controller.signal, ...init, }); const payload = await response.json().catch(() => null); if (!response.ok) throw new Error(payload?.message ?? `요청이 실패했습니다 (${response.status}).`); if (Array.isArray(payload?.stations)) savedRows.set(projectId, payload.stations); return Array.isArray(payload?.chainages) ? payload.chainages.map(Number) : []; } finally { window.clearTimeout(timer); } } /** 저장분 추가 측점 땅 위 자리 — 좌표를 못 받았으면 누가거리만(노선 위 자리로 놓음). */ export const savedPlusRows = (projectId: string): FollowingStructure[] => savedRows.get(projectId) ?? (saved.get(projectId) ?? []).map((chainage_m) => ({ chainage_m })); /** 서버 저장분을 지금 받음(초안 · 받아 둔 값 안 봄) — 추천 [적용] 확인 창 지워질 수(493-2). */ export const fetchSavedPlusStations = (projectId: string): Promise => request(projectId); export const readPlusDraft = (projectId: string): number[] | null => useDesignStore(projectId, null).get("plusStations"); /** 지금 추가 측점 — 초안이 있으면 초안(빈 목록 = 다 지움) · 없으면 저장분. */ export const plusStationsOf = (projectId: string): number[] => readPlusDraft(projectId) ?? saved.get(projectId) ?? []; /** 초안을 씀(정렬 · 같은 m 하나) — 바뀜 신호로 B05 종단 · B06 목록이 다시 맞춤. */ export function writePlusDraft(projectId: string, next: number[] | null): void { const list = next && [...new Set(next.map(Math.round))].sort((a, b) => a - b); useDesignStore(projectId, null).set("plusStations", list); } /** 저장분이 바뀜 — 듣는 쪽을 부름(초안 칸 값 그대로 다시 씀). */ function notify(projectId: string): void { useDesignStore(projectId, null).set("plusStations", readPlusDraft(projectId)); } // 되돌리기 · 다시하기는 세션을 바로 되씀 — 바뀜 신호를 다시 냄(종단 측점선 · B06 목록) · 다른 되살림이 // 측점 목록을 다시 세운 뒤(같은 차례 안에서 덮이지 않게) onPageHistory({ restore: () => void setTimeout(() => notify(draftProjectId()), 0) }); /** 저장분을 한 번 받음(못 받으면 빈 목록) — 읽는 곳 두기(`setPlusStations`)는 횡단 상세 가게 몫. */ export function loadSavedPlusStations(projectId: string): Promise { if (!loaded.has(projectId)) loaded.set( projectId, request(projectId) .then((list) => { saved.set(projectId, list); if (list.length) notify(projectId); }) .catch(() => undefined), ); return loaded.get(projectId)!; } /** 노선 초안 전 추가 측점 초안(없으면 null) — 초안을 버리면 이것으로(459). */ const beforeRoute = new Map(); /** follow 만 쓴 초안(사용자 초안 없던 때) — 그대로면 [저장]이 PUT 안 함(서버 따라감이 정본 · 459). */ const followedOnly = new Map(); /** 노선 밖 알림 글(459 · 458 「노선 밖 N 곳」과 같은 말). */ export const outsideNotice = (count: number): string => `노선 밖 ${count} 곳 — 측점 · 알약에서 뺌(좌표는 그대로 · 노선을 다시 늘이면 돌아옴)`; /** 프로젝트마다 마지막으로 알린 노선 밖 수 — 같으면 다시 안 알림. */ const warnedOutside = new Map(); /** follow 출력 꼴(묶음 `FollowPatch` 중 쓰는 칸만). */ interface FollowedDraft { plusStations?: ReadonlyArray | null; pipes?: unknown; structures?: unknown; } /** * 노선 초안 동안 따라감 뒤처리(459) — 묶음 follow 출력의 추가 측점(새 노선 정수 m)을 초안에 씀 · 노선 밖 관 · * 구조물 수를 알림(바뀐 때만). `next = null` = 노선 초안을 버림(초안 전 값으로). 부르는 곳 = * design_pipeline_page follow 구독(관 · 구조물 `writeFacilities` 곁 · 초안 버림 갈래도). */ export function followPlusDraft(projectId: string, next: FollowedDraft | null | undefined): void { if (next === null) { warnedOutside.delete(projectId); if (!beforeRoute.has(projectId)) return; writePlusDraft(projectId, beforeRoute.get(projectId)!); beforeRoute.delete(projectId); followedOnly.delete(projectId); return; } if (!next) return; const outside = [next.pipes, next.structures] .flatMap((list) => (Array.isArray(list) ? list : [])) .filter((item) => (item as { outside?: boolean })?.outside).length; if (outside !== (warnedOutside.get(projectId) ?? 0)) { warnedOutside.set(projectId, outside); // 알림 틀은 늦게 불러옴(이 모듈을 쓰는 Node 시험이 화면 모듈 없이 돎) if (outside) void import("@ui/ui_template_elements").then(({ showToast }) => showToast(outsideNotice(outside), "warning", 6000), ); } const plus = next.plusStations; if (!plus) return; if (!beforeRoute.has(projectId)) beforeRoute.set(projectId, readPlusDraft(projectId)); writePlusDraft(projectId, [...plus]); if (beforeRoute.get(projectId)) followedOnly.delete(projectId); else followedOnly.set(projectId, JSON.stringify(readPlusDraft(projectId))); } /** 한 본문 [저장] 조각(472-6) — 사용자 초안이 있으면 `{chainages}` · 없으면(follow 만 쓴 초안 포함 · 459) null · * 저장 뒤 저장분을 받은 목록(없으면 다시 받음)으로 · 초안 비움. */ export function plusStationsPart(projectId: string): SavePart { const auto = followedOnly.get(projectId); followedOnly.delete(projectId); beforeRoute.delete(projectId); const kept = readPlusDraft(projectId); if (kept && auto === JSON.stringify(kept)) useDesignStore(projectId, null).set("plusStations", null); const draft = readPlusDraft(projectId); return { body: draft ? { chainages: draft } : null, after: async (response) => { const given = response?.chainages; if (Array.isArray(given)) savedRows.delete(projectId); // 고쳐 쓴 저장분 = 노선 위 자리(누가거리로 놓음) const list = Array.isArray(given) ? given.map(Number) : await request(projectId); saved.set(projectId, list); loaded.set(projectId, Promise.resolve()); if (draft) useDesignStore(projectId, null).set("plusStations", null); else notify(projectId); }, }; } /** 저장분을 다시 받음 — 추천 [적용] 뒤(서버가 추가 측점을 지움) 옛 저장분 · 따라감 기억이 남지 않게(493-5). */ export async function reloadSavedPlusStations(projectId: string): Promise { [saved, savedRows, loaded, beforeRoute, followedOnly, warnedOutside].forEach((map) => map.delete(projectId), ); await loadSavedPlusStations(projectId); notify(projectId); }