/* ============================================================================= * D03_PlanAnalysis_Api.ts * D03 분석 화면 서버 요청(PLAN 563-1) — 지형 받기 범위 · 받기 · 진행 · * 사업 전체 지형면 한 벌(`models_plan/` · 격자) · 그은 선 정본(562-2 GET · PUT network · 응답에 D02 시점 · 종점) · * 지도 겹(569 — 지적도 · 도엽 겹 GeoJSON 경위도 · 급경사 그림 · 581 — 음영기복 · 보호구역 · 띠 · 산림입지도). * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { fetchCachedBytes } from "../A00_Common/b_asset_cache"; import { listSurfaceModels } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_sampler"; import type { PlanLine, PlanPiece, Vertex } from "./logic/D03_PlanAnalysis_Plan"; export interface BasePoint { lon: number; lat: number; x: number; y: number; } /** D02 정본 — 지점 목록 하나(작업 좌표계 m + 경위도 · 585-1). */ export interface PlanBaseView { epsg: number | null; points: BasePoint[]; } export interface PlanJob { running: boolean; percent: number; message: string | null; error: string | null; } /** 받기 전 범위(561-2) — 상자(작업 좌표계 m · 경위도 네 귀) · 받은 범위 · 도엽 · 칸 수 어림. */ export interface TerrainExtent { epsg: number; margin_m: number; box_m: [number, number, number, number]; box: Array<[number, number]>; have_m: [number, number, number, number] | null; have: Array<[number, number]> | null; covered: boolean; sheets: Array<{ no: string; ring: Array<[number, number]>; stored: boolean }>; cells: number; } /** 그은 선 서버 꼴(562-2) — 마크 = 조각 경계 · 조각 없는 구역 = 연도 없음. */ export interface ServerNetwork { epsg: number; lines: PlanLine[]; marks: Array<{ line: string; at_m: number }>; pieces: PlanPiece[]; } export interface PlanNetworkView { /** D02 정본 전체 시점 · 종점. */ base: PlanBaseView; network: ServerNetwork | null; derived: { lines: Array<{ id: string; length_m: number; stations: number[] }>; pieces: Array; } | null; /** 옛 연도 노선을 선으로 바꾼 것인가. */ legacy: boolean; /** 사업연도 시작(있으면). */ year_start?: number | null; } export interface PlanSurface { modelId: number; grid: DtmGrid; } async function request(path: string, init: RequestInit = {}): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { ...init, credentials: "include", headers: { "Content-Type": "application/json", ...(init.headers ?? {}) }, }); const body = (await response.json().catch(() => ({}))) as T & { detail?: unknown }; if (!response.ok) { const detail = typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`; throw new Error(detail); } return body; } const base = (projectId: string): string => `/projects/${projectId}/plan`; /** 받기 전 범위 — `extra` = 더 덮을 점(그은 꺾임점). */ export const fetchTerrainExtent = ( projectId: string, marginM: number | null, extra: Vertex[], ): Promise => { const query = new URLSearchParams(); if (marginM !== null) query.set("margin_m", String(marginM)); if (extra.length) query.set("extra", JSON.stringify(extra)); return request(`${base(projectId)}/terrain/extent?${query}`); }; export const startTerrain = ( projectId: string, marginM: number | null, extra: Vertex[], ): Promise => request(`${base(projectId)}/terrain`, { method: "POST", body: JSON.stringify({ margin_m: marginM, extra }), }); export const fetchPlanProgress = (projectId: string): Promise<{ terrain: PlanJob }> => request(`${base(projectId)}/progress`); export const fetchPlanNetwork = (projectId: string): Promise => request(`${base(projectId)}/network`); export const savePlanNetwork = ( projectId: string, network: ServerNetwork, ): Promise => request(`${base(projectId)}/network`, { method: "PUT", body: JSON.stringify(network) }); /** 사업 전체 지형면 — 모델 행 경로 `…/models_plan/…` · 격자 = 평활본 · 없으면 null. */ export async function fetchPlanSurface(projectId: string): Promise { const { models } = await listSurfaceModels(projectId); const model = models.find((entry) => /models_plan\//.test(entry.model_file_path ?? "")); if (!model) return null; const stamp = String(model.generation_params?.file_stamp ?? ""); const url = `${API_BASE_URL}/projects/${projectId}/surface/models/${model.id}/grid` + `?smooth=true&sig=${encodeURIComponent(stamp)}`; return { modelId: model.id, grid: parseDtmGrid(await fetchCachedBytes(projectId, url)) }; } /** 지도 겹 그림(569-1) — 작업 좌표계 m 상자(칸 가장자리) · data URL(위 = 북) · 단계 색 · 넓이 비율. */ export interface PlanRaster { box_m: [number, number, number, number]; /** 경위도 네 귀퉁이(왼쪽 아래 · 오른쪽 아래 · 오른쪽 위 · 왼쪽 위) — GL 바탕(586-1). */ box?: Array<[number, number]>; image: string; bands: Array<{ min_deg: number; max_deg: number; color: string; ratio: number }>; } export const fetchPlanCadastral = (projectId: string): Promise => request(`${base(projectId)}/layers/cadastral`); /** 도엽 겹(`도로중심선` 등) — 없으면 서버가 받은 도엽에서 풀어 냄. */ export const fetchPlanSheetLayer = (projectId: string, key: string): Promise => request(`${base(projectId)}/layers/sheet?key=${encodeURIComponent(key)}`); export const fetchPlanRaster = ( projectId: string, kind: "slope" | "hillshade" | "landslide", ): Promise => request(`${base(projectId)}/layers/raster?kind=${kind}`); /** 보호 · 규제 구역(581-1) — 지물 `kind` · `name` · `area_m2` + 종류별 개수 · 넓이(실패 = `failed`). */ export interface PlanZones extends GeoJsonCollection { summary: Record; } export const fetchPlanZones = (projectId: string): Promise => request(`${base(projectId)}/layers/zones`); /** 산림입지도(581-2) — 지물 `code` · `name` · `area_m2` + 토양형별 개수 · 넓이(코드 차례). */ export interface PlanSoils extends GeoJsonCollection { summary: Array<{ code: string; name: string; count: number; area_m2: number }>; } export const fetchPlanSoils = (projectId: string): Promise => request(`${base(projectId)}/layers/soils`); /** 계류 100 m · 도로 300 m 띠(581-1) — 받은 범위 안 넓이 · 비율. */ export interface PlanBand { dist_m: number; lines: number; area_m2: number; ratio: number; collection: GeoJsonCollection; } /** 선 겹(584-1) — 받은 범위 안 선 + 가닥 · 길이 m. */ export interface PlanLines extends GeoJsonCollection { summary: { count: number; length_m: number }; } /** 단층 · 지질구조선(브이월드 WFS · 서버 `LINE_KINDS` 열쇠). */ export const fetchPlanLines = (projectId: string, kind: string): Promise => request(`${base(projectId)}/layers/lines?kind=${encodeURIComponent(kind)}`); /** 0차곡(584-1 · 지형 추정) — 위 집수 넓이 [min_m2, max_m2) 선 + 가닥 · 길이 m. */ export interface PlanZeroOrder { min_m2: number; max_m2: number; cell_m: number; count: number; length_m: number; collection: GeoJsonCollection; } export const fetchPlanZeroOrder = (projectId: string): Promise => request(`${base(projectId)}/layers/zero-order`); export const fetchPlanBands = (projectId: string): Promise<{ stream: PlanBand; road: PlanBand }> => request(`${base(projectId)}/layers/bands`); /** 올린 임도망도(584-3) — 받은 범위로 자른 선 + 가닥 · 길이 m · 원본 선 수 · 좌표계 · 올린 파일. */ export interface PlanForestRoads { count: number; length_m: number; total: number; crs: string[]; source: { files: string[]; bytes: number; uploaded_at: string } | null; collection: GeoJsonCollection; } /** 올린 적 없으면 오류(404 글 그대로). */ export const fetchPlanForestRoads = (projectId: string): Promise => request(`${base(projectId)}/layers/forest-roads`); /** 임도망도 SHP 올리기(zip 하나 · shp · shx · dbf · prj 묶음) — 못 읽으면 서버 글 그대로 오류. */ export async function uploadPlanForestRoads( projectId: string, files: File[], ): Promise { const form = new FormData(); for (const file of files) form.append("files", file); const response = await fetch(`${API_BASE_URL}${base(projectId)}/layers/forest-roads`, { method: "POST", credentials: "include", body: form, }); const body = (await response.json().catch(() => ({}))) as PlanForestRoads & { detail?: unknown; }; if (!response.ok) throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`); return body; } /** 산사태취약지역(584-3) — 받은 범위 안 점 · 지번 맞는 필지 테두리 · 시도별 표 · 표 없는 시도. */ export interface PlanLandslideZones { points: Array<{ lon: number; lat: number; kind: string | null; area_m2: number | null; place: string; }>; parcels: GeoJsonCollection; sidos: Array<{ code: string; name: string; rows: number; located: number; basis?: string }>; missing: string[]; /** 지적도를 받았나(못 받으면 점만). */ cadastral: boolean; } export const fetchPlanLandslideZones = (projectId: string): Promise => request(`${base(projectId)}/layers/landslide-zones`);