/* ============================================================================= * B06_Section_Api_Fetch.ts * 3차 워크플로우(종·횡단 생성) API 클라이언트 * * 백엔드 계약 (B06_Section_Router.py): * GET /api/projects/{project_id}/sections/context → 확정 경로 + 기본 옵션 * GET /api/projects/{project_id}/sections/{route_id} → 종단 요약 조회 * GET /api/projects/{project_id}/sections/{route_id}/detail → 종횡단 원시 샘플 조회 * POST /api/projects/{project_id}/sections/{route_id}/regenerate → 횡단 반폭 재생성 * POST /api/projects/{project_id}/sections/{route_id}/confirm → 종횡단 확정 * * 규칙: * - 모든 제어 상수는 config_frontend에서 참조 (하드코딩 금지). * - 오류 응답 형식 {status:"error", message:"..."}을 Error로 변환. * - **타입 선언은 `B06_Section_Api_Types.ts`에 있다**(700줄 제한, 2026-09-02). * 기존 호출 코드가 깨지지 않도록 여기서 그대로 다시 내보낸다. * ========================================================================== */ import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; import { t as L } from "@ui/ui_template_locale"; import type { CrossDesign, CrossSection, CrossSectionPatch, SectionConfirmResponse, SectionContextResponse, SectionDetailResponse, SectionSummaryResponse, StandardCrossSection, } from "./B06_Section_Api_Types"; // 타입 정본은 `B06_Section_Api_Types.ts`. 기존 호출부가 여기서 가져오던 것을 // 그대로 쓰도록 다시 내보낸다 — 이동은 파일만 나눈 것이고 계약은 그대로다. export type * from "./B06_Section_Api_Types"; /** 공통 fetch 헬퍼: 타임아웃 + 인증 헤더 + 오류 응답 변환. */ 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", ...(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(timeoutId); } } /** 종횡단 설정값 한 번 묻기(캐시 없음) — 세션 캐시 창구는 `B06_Section_Api_Context.ts`(472-2). */ export function requestSectionContext(projectId: string): Promise { return requestJson(`/projects/${projectId}/sections/context`, { method: "GET", }); } /** 경로의 종단면 요약을 조회한다. */ export async function getSections( projectId: string, routeId: number, ): Promise { return requestJson(`/projects/${projectId}/sections/${routeId}`, { method: "GET", }); } /** 경로의 SVG 렌더링용 종단·횡단 원시 샘플을 조회한다. */ /** * 초안 자리(놓거나 옮긴 관 · 구조물)의 횡단을 미리 떠 받는다 — 서버는 아무것도 안 씀 · 캐시에만 얹음 * ([저장] 때 서버가 같은 계산으로 정본에 씀 · 273-2). */ export async function previewStructureSections( projectId: string, chainages: number[], ): Promise { const result = await requestJson<{ cross_sections?: CrossSection[] }>( `/projects/${projectId}/section/preview-stations`, { method: "POST", body: JSON.stringify({ chainages }) }, ); return result.cross_sections ?? []; } export async function fetchSectionDetail( projectId: string, routeId: number, ): Promise { return requestJson(`/projects/${projectId}/sections/${routeId}/detail`, { method: "GET", }); } /** 횡단 반폭을 반영해 종횡단을 재생성·저장하고 갱신된 상세를 반환한다. */ export async function regenerateSections( projectId: string, routeId: number, crossHalfWidthM: number, ): Promise { return requestJson( `/projects/${projectId}/sections/${routeId}/regenerate`, { method: "POST", body: JSON.stringify({ cross_half_width_m: crossHalfWidthM }) }, ); } /** 경로의 종·횡단면을 확정한다. 표준 횡단면 설정값·측점별 세션 보관값을 함께 저장할 수 있다. */ export async function confirmSections( projectId: string, routeId: number, standardCrossSection?: StandardCrossSection, crossPatches?: CrossSectionPatch[], /** 프론트에서 계산한 유토곡선 결과. 확정 시점에만 영구 저장한다. */ massHaul?: Record, ): Promise { const body = sectionConfirmBody(standardCrossSection, crossPatches, massHaul); return warnUnrecomputed( requestJson(`/projects/${projectId}/sections/${routeId}/confirm`, { method: "POST", body: JSON.stringify(body), }), ); } /** 서버 정본 셈이 실패했으면 경고 한 줄(388-2) — 저장은 남음 · 면적 · 관 길이는 옛 서버 값. * 서버가 버린 고침(행 없는 측점 · 기본 설계 못 냄 · 측점 맞춤 실패)도 한 줄(445 M5). */ async function warnUnrecomputed( request: Promise, ): Promise { const result = await request; warnSectionSave(result); return result; } /** 횡단 저장 본문(저장 · 확정 · 한 본문 [저장] 조각 공통). */ export function sectionConfirmBody( standardCrossSection?: StandardCrossSection, crossPatches?: CrossSectionPatch[], massHaul?: Record, ): Record { const body: Record = {}; if (standardCrossSection) body.standard_cross_section = standardCrossSection; if (crossPatches?.length) body.cross_patches = crossPatches; if (massHaul) body.mass_haul = massHaul; return body; } /** 횡단 저장 응답의 경고 — 재계산 실패 · 서버가 버린 고침 · 측점 맞춤 실패(한 본문 [저장]도 부름). */ export function warnSectionSave(result: Partial): void { const dropped = [ ...new Set([ ...(result.dropped_patch_chainages ?? []), ...(result.default_failed_chainages ?? []), ]), ]; const where = dropped .slice(0, 5) .map((at) => `${at.toFixed(2)}m`) .join(" · "); const lost = [ ...(dropped.length ? [ `횡단 고침 ${dropped.length}곳을 못 남겼습니다 — 측점 행이 없음: ${where}` + (dropped.length > 5 ? ` 외 ${dropped.length - 5}곳` : ""), ] : []), ...(result.stations_synced === false ? ["구조물 측점 맞춤 실패 — 옛 측점이 남았습니다."] : []), ]; if (result.recomputed === false || lost.length) void import("@ui/ui_template_elements").then(({ showToast }) => { if (result.recomputed === false) showToast(L("B06_Recompute_Failed"), "warning", 8000); if (lost.length) showToast(lost.join(" "), "warning", 8000); }); } /** * 편집 중인 종·횡단을 **확정하지 않고** 영구저장소에만 남긴다(임시 저장). * 저장 내용은 확정과 같지만 경로 상태·워크플로 단계를 건드리지 않아 페이지 이동도 없다. */ export async function saveSections( projectId: string, routeId: number, standardCrossSection?: StandardCrossSection, crossPatches?: CrossSectionPatch[], massHaul?: Record, ): Promise { const body = sectionConfirmBody(standardCrossSection, crossPatches, massHaul); return warnUnrecomputed( requestJson(`/projects/${projectId}/sections/${routeId}/save`, { method: "POST", body: JSON.stringify(body), }), ); } /** 같은 회사에서 표준횡단 설계값을 불러올 수 있는 프로젝트 항목. */ export interface CompanyStandardProject { project_id: string; name: string; } export interface CompanyStandardListResponse { status: string; projects: CompanyStandardProject[]; } export interface CompanyStandardResponse { status: string; project_id: string; standard_cross_section: StandardCrossSection; } /** 같은 회사에서 설계값을 불러올 수 있는 프로젝트 목록을 조회한다(회사 스코프). */ export async function listCompanyStandards( projectId: string, ): Promise { return requestJson( `/projects/${projectId}/sections/company-standards`, { method: "GET" }, ); } /** 특정 프로젝트의 표준횡단 설계값을 미리보기용으로 조회한다(적용 전, 현재 값 불변). */ export async function getCompanyStandard( projectId: string, sourceProjectId: string, ): Promise { return requestJson( `/projects/${projectId}/sections/company-standards/${sourceProjectId}`, { method: "GET" }, ); } /** B07 횡단 수량표 한 장(138-4 · 서버 `cross_quantity_sheet`) — 값 · 글 · 칸 배치. */ export interface CrossQuantitySheet { quantity_table: Record; /** B07 표에 찍히는 글 그대로(값 없음 = 「-」). */ text: Record; layout: { header: Array<[string, string]>; left: Array<[string | null, string | null, string | null]>; /** 더한 홀로 선 줄(179-4)은 하위 이름 없음 */ middle: Array<[string | null, string | null, string]>; right: Array<[string | null, string | null]>; /** 본문 줄 id(양식 b0~ · 더한 행 r…) · 더한 열(키 = 본문 줄마다) — 179-4 */ rows: string[]; columns: Array<{ id: string; label: string; keys: string[] }>; }; /** 길이 출처가 설계선을 벗어난 칸 — 값 비움 · 「다시 고름」(179-4). */ repick: string[]; } /** 캐시 설계로 B07 횡단 수량표를 받는다 — 표시 전용(정본을 안 씀). */ export async function fetchCrossQuantityTable( projectId: string, routeId: number, chainageM: number, design: CrossDesign | null | undefined, ): Promise { return requestJson( `/projects/${projectId}/sections/${routeId}/cross-quantity-table`, { method: "POST", body: JSON.stringify({ chainage_m: chainageM, design: design ?? null }) }, ); }