/* ============================================================================= * B05_Profile_Api_Replan.ts * 계획노선 두 벌(예상노선·계획노선) 읽기와 노선 갈아 끼우기 요청. * * GET /projects/{id}/route/plan → 예상노선·계획노선 정점(사업지 좌표계 m) * POST /projects/{id}/route/replan → 고친 계획노선으로 갈아 끼우고 재계산 * POST /projects/{id}/route/replan/reset → 계획노선을 예상노선으로 되돌리고 재계산 * * 재계산은 배수유역부터 전 단계를 다시 도는 무거운 작업이라(용화 67측점 기준 3분대) * 타임아웃을 길게 잡는다 — 기본값으로 두면 중간에 끊긴다. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; /** 노선 재계산 대기 상한 — 배수유역 분석(90초대)까지 포함해 넉넉히 잡는다. */ const REPLAN_TIMEOUT_MS = 15 * 60 * 1000; /** 사용자가 잡아 옮기는 제어점 하나 — 서버가 이 노드로 폴리라인을 만든다. */ export interface RoutePlanNode { x: number; y: number; /** 직전·직후 구간이 이루는 내각(도). 끝점은 null. */ inner_angle_deg: number | null; /** 이 자리에 끼운 원호 반지름(m). 곡선을 지운 자리는 null. */ radius_m: number | null; tangent_m: number | null; /** 법정 기준 위반 표시 — 값은 내되 막지 않는다. */ violations: string[]; } /** 직선 사이에 놓인 **곡선 성분 하나** — 화면이 손잡이와 R 칸을 그리는 재료. */ export interface RoutePlanCurve { /** 앞뒤 직선을 늘려 만나는 자리(교각점). **반지름을 바꿔도 여기는 안 움직인다.** */ apex: [number, number]; radius_m: number; tangent_m: number; inner_angle_deg: number; /** 곡선 시작점 — 직선이 곡선에 닿는 자리. 사용자가 잡는 손잡이다. */ start: [number, number]; /** 곡선 끝점. */ end: [number, number]; /** 이 곡선이 대신하는 꺾임점 구간(첫·끝) — 편집이 어느 노드를 건드리는지 알려 준다. */ node_first: number; node_last: number; violations: string[]; } export interface RoutePlanResponse { status: string; project_id: string; /** 예상노선(원본) **점 묶음** [[x, y], …] — 사업지 좌표계(m). 폴리라인이 아니다. */ expected: Array<[number, number]>; /** 계획노선 폴리라인(원호 포함) — 그려 보이는 선. 잡는 대상이 아니다. */ planned: Array<[number, number]>; /** 잡아 옮기는 노드(꺾임점). 편집은 이것으로 한다(2026-09-06 사용자 지시). */ nodes: RoutePlanNode[]; /** 직선·곡선 성분 — 곡선 시작·끝점과 반지름. 화면이 이것으로 손잡이를 그린다. */ curves: RoutePlanCurve[]; /** 이 프로젝트에 적용한 법정 최소곡선반지름(m). */ min_radius_m: number; curve_count: number; violation_count: number; /** 사용자가 고친 계획노선이 저장돼 있으면 true. */ edited: boolean; } export interface RouteReplanResponse { status: string; project_id: string; route_id: number | null; total_length_m: number | null; vertex_count: number; } async function requestJson(path: string, init: RequestInit, timeoutMs: number): Promise { const controller = new AbortController(); const timer = window.setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(`${API_BASE_URL}${path}`, { ...init, credentials: "include", headers: { "Content-Type": "application/json", ...(init.headers ?? {}) }, signal: controller.signal, }); const payload = (await response.json()) as T & { message?: string }; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); return payload; } finally { window.clearTimeout(timer); } } /** 예상노선·계획노선을 함께 읽는다(편집 모달이 점선·실선으로 그린다). */ export async function fetchRoutePlan(projectId: string): Promise { return requestJson( `/projects/${projectId}/route/plan`, { method: "GET" }, 60000, ); } /** 꺾임점 하나에 실어 보내는 편집값 — 곡선을 둘지, 반지름을 못박을지. */ export interface RouteReplanVertex { x: number; y: number; /** 이 자리에 곡선을 둘지. 끄면 직선이 그대로 꺾인다(곡선 삭제). */ curve?: boolean; /** 못박을 반지름(m). 없으면 서버가 고른다. */ radius_m?: number | null; } /** 고친 계획노선으로 갈아 끼우고 배수유역부터 다시 계산한다. * * 편집 세 가지가 모두 이 한 목록으로 나간다(2026-09-07 사용자 지시) — * **직선 삭제·추가**는 점을 빼거나 더하는 것, **곡선 삭제·추가**는 `curve` 를 끄고 켜는 것, * **반지름 변경**은 `radius_m` 을 주는 것. */ export async function replanRoute( projectId: string, vertices: Array<[number, number]> | RouteReplanVertex[], ): Promise { const payload = (vertices as Array<[number, number] | RouteReplanVertex>).map((vertex) => Array.isArray(vertex) ? { x: vertex[0], y: vertex[1] } : vertex, ); return requestJson( `/projects/${projectId}/route/replan`, { method: "POST", body: JSON.stringify({ vertices: payload }) }, REPLAN_TIMEOUT_MS, ); } /** 계획노선을 예상노선으로 되돌리고 같은 재계산을 돈다(노선 초기화). */ export async function resetRoutePlan(projectId: string): Promise { return requestJson( `/projects/${projectId}/route/replan/reset`, { method: "POST" }, REPLAN_TIMEOUT_MS, ); }