/* B07 상세 설계 도면 목록·단건 API 클라이언트. */ import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; export interface DesignDrawingItem { id: string; // blank: 아직 내용을 만들지 않은 도면 — 도각만 실려 온다. kind: | "cover" | "longitudinal" | "cross" | "mass_haul" | "watershed" | "plan" | "landuse" | "plan_lidar" | "cross_standard" | "standard" | "blank"; label: string; chainage_m: number | null; confirmed: boolean; } export interface CadDrawing { entities: Record[]; /** 횡단도 측점별 실좌표(m) → 종이(mm) 변환값. 구조물을 같은 자리에 얹는 데 쓴다. */ cross_placements?: { chainage_m: number; ox: number; oy: number; dy: number; mm_per_m: number; x0: number; x1: number; }[]; layers: { id: string; name: string; isVisible: boolean; isLocked: boolean; }[]; } export interface DesignDrawingListResponse { status: string; project_id: string; route_id: number; drawings: DesignDrawingItem[]; /** 확정을 건너뛰고 개발 우회로로 열렸나 — 화면이 그 사실을 알려야 한다. */ dev_bypass?: boolean; } /** 수량 산출표 값 (미산정 항목은 null). 백엔드 `_quantity_table`의 키와 대응. */ export type QuantityTable = Record; /** 측구 형식별 규격 (B06 엔진 ditch_spec 신구조와 1:1). */ export type DitchSpec = | { type: "none" } | { type: "standard"; top_width_m: number; bottom_width_m: number; depth_m: number; } | { type: "l_type"; width_m: number; depth_m: number }; /** B06에서 지정한 설계(지반정보·계획정보). 횡단도에만 존재. status로 잠정/확정 구분. */ export interface CrossDesignInfo { ground_type: "soil" | "ripping_rock" | "blasting_rock"; geometry_preset: "soil" | "rock"; section_mode: "left_cut" | "right_cut" | "both_cut" | "both_fill"; ditch_side: "left" | "right"; ditch_type?: "standard" | "l_type" | null; ditch_enabled?: boolean; cut_slope_ratio: number; fill_slope_ratio: number; roadbed_width_m: number; carriageway_width_m?: number; cross_slope_pct?: number; paved?: boolean; ditch: DitchSpec; road_edges?: Record<"left" | "right", { offset_m: number; elevation_m: number }>; design_elevation_m: number; cut_area_m2: number; fill_area_m2: number; status?: "provisional" | "confirmed"; } export interface DesignDrawingResponse { status: string; project_id: string; route_id: number; id: string; // blank: 아직 내용을 만들지 않은 도면 — 도각만 실려 온다. kind: | "cover" | "longitudinal" | "cross" | "mass_haul" | "watershed" | "plan" | "landuse" | "plan_lidar" | "cross_standard" | "standard" | "blank"; label: string; drawing: CadDrawing; confirmed: boolean; quantity_table?: QuantityTable | null; design?: CrossDesignInfo | null; } export interface DesignDrawingConfirmResponse { status: string; project_id: string; id: string; confirmed: boolean; all_confirmed: boolean; design?: CrossDesignInfo | null; } async function requestJson(path: string, init: RequestInit = {}): Promise { const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), API_TIMEOUT_MS); try { const response = await fetch(`${API_BASE_URL}${path}`, { ...init, credentials: "include", headers: { "Content-Type": "application/json" }, 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(timeoutId); } } export function fetchDesignDrawingList(projectId: string): Promise { return requestJson(`/projects/${projectId}/design-drawings`); } export function fetchDesignDrawing( projectId: string, drawingId: string, ): Promise { return requestJson(`/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}`); } export function confirmDesignDrawing( projectId: string, drawingId: string, drawing: CadDrawing, quantityTable?: QuantityTable | null, ): Promise { return requestJson( `/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}/confirm`, { method: "PUT", body: JSON.stringify({ drawing, quantity_table: quantityTable ?? null }), }, ); } export function invalidateDesignDrawing(projectId: string, drawingId: string): Promise { return requestJson( `/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}/invalidate`, { method: "POST" }, ); } /** 도각 편집 화면이 싣는 도각 한 장 (실치수 1:1, 잠금 없는 도면층). */ export interface FrameTemplateResponse { status: string; project_id: string; drawing: CadDrawing; /** 회사가 고친 도각을 쓰고 있으면 true, 프로그램 기본 도각이면 false. */ customized: boolean; /** 자리표에 보여 줄 실제 값 — 편집 화면 전용이고 저장값은 토큰 그대로다. */ fields?: Record; } export function fetchFrameTemplate(projectId: string): Promise { return requestJson(`/projects/${projectId}/frame-template`); } export function saveFrameTemplate(projectId: string, drawing: CadDrawing): Promise { return requestJson(`/projects/${projectId}/frame-template`, { method: "PUT", body: JSON.stringify({ drawing }), }); } /** 외부 도각 파일(DXF·DWG)을 읽어 편집 화면에 실을 도면으로 받는다 — 아직 저장하지 않는다. */ export async function importFrameTemplate( projectId: string, file: File, ): Promise<{ drawing: CadDrawing; entity_count: number }> { const form = new FormData(); form.append("file", file); // 파일 전송이라 requestJson(JSON 헤더·짧은 시한)을 쓰지 않는다. const response = await fetch(`${API_BASE_URL}/projects/${projectId}/frame-template/import`, { method: "POST", credentials: "include", body: form, }); const payload = (await response.json()) as { drawing: CadDrawing; entity_count: number; message?: string; }; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); return payload; } /** 지금 도면을 DXF·DWG 파일로 받는다 (2026-09-06 사용자 지시) — 파일은 서버가 만든다. */ export async function exportDrawing( projectId: string, drawing: CadDrawing, fileFormat: "dxf" | "dwg", name: string, ): Promise<{ blob: Blob; skipped: number }> { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/drawing-export`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ drawing, file_format: fileFormat, name }), }); if (!response.ok) { const detail = (await response.json().catch(() => ({}))) as { message?: string }; throw new Error(detail.message ?? `HTTP ${response.status}`); } return { blob: await response.blob(), skipped: Number(response.headers.get("X-Aislo-Skipped") ?? 0), }; } /** 회사 도각을 지우고 프로그램 기본 도각으로 되돌린다. */ export function resetFrameTemplate(projectId: string): Promise { return requestJson(`/projects/${projectId}/frame-template`, { method: "DELETE", }); } /** 표준도 장 목록 — 제원 입력 칸이 쓰는 것만 추린 꼴. */ export interface StandardSheetsResponse { status: string; sheet_count: number; structure_count: number; sheets: { key: string; title: string; type_id: string; member_count: number; options: Record; }[]; } export function fetchStandardSheets(projectId: string): Promise { return requestJson(`/projects/${projectId}/standard-sheets`); } /** 장 하나의 제원 저장 — 빈 값(null)은 **그 칸을 지우라**는 뜻이다. */ export function putStandardSheetSpec( projectId: string, body: { sheet_key: string; base_revision: number; stone_kind: string | null; stone_supply: string | null; back_len_cm: string | null; face_slope_ratio: string | null; foundation: string | null; stone_coeff_basis: string | null; fill_concrete_mpa: string | null; }, ): Promise<{ status: string; revision: number; changed: number; notes: string[] }> { return requestJson(`/projects/${projectId}/standard-sheets/spec`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); } /** 구조물 정본 판번호 — 제원을 저장할 때 함께 보내야 다른 창 덮어쓰기를 막는다. */ export function fetchStructureRevision(projectId: string): Promise<{ revision: number }> { return requestJson(`/projects/${projectId}/route/structures`); }