/* ============================================================================= * map_layer_api.ts * 공용 지도 겹 받기(PLAN 569 · 581 · 584 · 603 · 605 · 606 · 624-3 · 624-4 · 요구 530) — D03 예정노선 · B05 평면이 같이 씀(D03_Api 에서 옮김). * 서버 길 = `/api/projects/{id}/plan/…`(624-1 · 임도 프로젝트도 · 띠 · 올린 임도망도 = 예정노선만). * 급경사 · 음영 · 0차곡 = 지형 원천 인자 `terrain`(지형 칩 값 · 624-4). * 지적도 · 도엽 겹 GeoJSON 경위도 · 도엽 조각 · 그림 겹(급경사 · 음영기복 · 산사태위험) · 보호구역 · 산림입지도 · * 선 겹(단층 · 지질구조선) · 0차곡 · 띠 · 올린 임도망도(올리기 · 지움) · 산사태취약지역. * D03 = `D03_PlanAnalysis_Api` 가 다시 내놓음(부르는 곳 그대로). * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import type { TerrainSource } from "./map_terrain_chip"; /** 경위도 GeoJSON 묶음 — B04 `GeoJsonCollection` 과 같은 꼴(공용 폴더는 페이지 파일을 안 부름). */ export type GeoJsonCollection = { features?: Array<{ geometry?: { type: string; coordinates: unknown } | null; properties?: Record | null; }>; }; async function request(path: string): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include", headers: { "Content-Type": "application/json" }, }); 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`; /** 지도 겹 그림(569-1) — 작업 좌표계 m 상자(칸 가장자리) · data URL(위 = 북). */ export interface PlanRaster { box_m: [number, number, number, number]; /** 경위도 네 귀퉁이(왼쪽 아래 · 오른쪽 아래 · 오른쪽 위 · 왼쪽 위) — GL 바탕(586-1). */ box?: Array<[number, number]>; image: string; /** 급경사만(603) — `counts[v]` = 기울기 % 내림 v(0~250) 칸 수 · 그림 = 회색 칸 값(255 = 빈 칸). */ counts?: number[]; /** 쓴 지형 원천(624-1 · 급경사 · 음영). */ source?: string; } /** 지형 원천 인자(624-1) — 안 넘김 = 서버 기본(D = 수치지형도 · B = 설계 지형). */ const terrainQuery = (terrain?: TerrainSource): string => (terrain ? `&terrain=${terrain}` : ""); 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)}`); /** 보이는 도엽(606-2) — 지도 보기 경위도 상자 → 수 · 번호(가운데부터 · 30 장 넘음 = 빈 목록). */ export const fetchPlanSheetTiles = ( projectId: string, bbox: [number, number, number, number], ): Promise<{ count: number; sheets: string[] }> => request(`${base(projectId)}/sheet-tiles?bbox=${bbox.map((v) => v.toFixed(5)).join(",")}`); /** 도엽 조각 하나(606-1) — 공용 창고 · 없으면 서버가 만듦. */ export const fetchPlanSheetTile = ( projectId: string, sheet: string, name: string, ): Promise => request(`${base(projectId)}/sheet-tiles/${sheet}/${encodeURIComponent(name)}`); /** 그림 겹 — 급경사 = PNG 바이트 + 머리 `X-Plan-Raster`(616-4 · `image` = blob 주소 · 다 읽으면 * `slopeValues` 가 놓음) · 그 밖 = JSON(base64 그림). */ export const fetchPlanRaster = async ( projectId: string, kind: "slope" | "hillshade" | "landslide", terrain?: TerrainSource, ): Promise => { const path = `${base(projectId)}/layers/raster?kind=${kind}${terrainQuery(terrain)}`; if (kind !== "slope") return request(path); const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" }); if (!response.ok) { const body = (await response.json().catch(() => ({}))) as { detail?: unknown }; throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`); } const meta = JSON.parse(response.headers.get("X-Plan-Raster") ?? "{}") as PlanRaster; return { ...meta, image: URL.createObjectURL(await response.blob()) }; }; /** 보호 · 규제 구역(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; /** 쓴 지형 원천(624-1). */ source?: string; } export const fetchPlanZeroOrder = ( projectId: string, terrain?: TerrainSource, ): Promise => request(`${base(projectId)}/layers/zero-order?${terrainQuery(terrain).slice(1)}`); export const fetchPlanBands = (projectId: string): Promise<{ stream: PlanBand; road: PlanBand }> => request(`${base(projectId)}/layers/bands`); /** 올린 묶음 한 줄(605-1) — 올릴 때마다 하나 · 자른 가닥 · 길이 m · 원본 선 수 · 잘린 상자 · 기준점 거리. */ export interface PlanRoadBundle { id: string; name: string; files: string[]; bytes: number | null; uploaded_at: string | null; count: number; length_m: number; total: number; bounds: [number, number, number, number] | null; distance_m: number | null; } /** 올린 임도망도(584-3 · 595-4 · 605-1) — 묶음 모두를 합친 값: 기준점 ±5 km ∪ 받은 지형으로 자른 선 * (선마다 `__bundle` 묶음 번호 · `__line` 묶음 안 차례) + 가닥 · 길이 m · 원본 선 수 · 좌표계 · * 잘린 상자([서, 남, 동, 북] 경위도) · 기준점 ~ 가장 가까운 임도 m · 마지막 올린 파일 · 묶음 줄. */ export interface PlanForestRoads { bundles: PlanRoadBundle[]; count: number; length_m: number; total: number; crs: string[]; bounds: [number, number, number, number] | null; distance_m: number | null; source: { files: string[]; bytes: number; uploaded_at: string } | null; collection: GeoJsonCollection; } /** 올린 적 없으면 오류(404 글 그대로). */ export const fetchPlanForestRoads = (projectId: string): Promise => request(`${base(projectId)}/layers/forest-roads`); /** 묶음 하나 지움(605-1) — 남은 묶음 합친 값 · 하나도 안 남으면 null. */ export async function deletePlanForestRoad( projectId: string, bundle: string, ): Promise { const response = await fetch( `${API_BASE_URL}${base(projectId)}/layers/forest-roads/${encodeURIComponent(bundle)}`, { method: "DELETE", credentials: "include" }, ); 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.bundles.length ? body : null; } /** 임도망도 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`);