272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* 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<T>(path: string, init: RequestInit): Promise<T> {
|
|
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<SectionContextResponse> {
|
|
return requestJson<SectionContextResponse>(`/projects/${projectId}/sections/context`, {
|
|
method: "GET",
|
|
});
|
|
}
|
|
|
|
/** 경로의 종단면 요약을 조회한다. */
|
|
export async function getSections(
|
|
projectId: string,
|
|
routeId: number,
|
|
): Promise<SectionSummaryResponse> {
|
|
return requestJson<SectionSummaryResponse>(`/projects/${projectId}/sections/${routeId}`, {
|
|
method: "GET",
|
|
});
|
|
}
|
|
|
|
/** 경로의 SVG 렌더링용 종단·횡단 원시 샘플을 조회한다. */
|
|
/**
|
|
* 초안 자리(놓거나 옮긴 관 · 구조물)의 횡단을 미리 떠 받는다 — 서버는 아무것도 안 씀 · 캐시에만 얹음
|
|
* ([저장] 때 서버가 같은 계산으로 정본에 씀 · 273-2).
|
|
*/
|
|
export async function previewStructureSections(
|
|
projectId: string,
|
|
chainages: number[],
|
|
): Promise<CrossSection[]> {
|
|
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<SectionDetailResponse> {
|
|
return requestJson<SectionDetailResponse>(`/projects/${projectId}/sections/${routeId}/detail`, {
|
|
method: "GET",
|
|
});
|
|
}
|
|
|
|
/** 횡단 반폭을 반영해 종횡단을 재생성·저장하고 갱신된 상세를 반환한다. */
|
|
export async function regenerateSections(
|
|
projectId: string,
|
|
routeId: number,
|
|
crossHalfWidthM: number,
|
|
): Promise<SectionDetailResponse> {
|
|
return requestJson<SectionDetailResponse>(
|
|
`/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<string, unknown>,
|
|
): Promise<SectionConfirmResponse> {
|
|
const body = sectionConfirmBody(standardCrossSection, crossPatches, massHaul);
|
|
return warnUnrecomputed(
|
|
requestJson<SectionConfirmResponse>(`/projects/${projectId}/sections/${routeId}/confirm`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** 서버 정본 셈이 실패했으면 경고 한 줄(388-2) — 저장은 남음 · 면적 · 관 길이는 옛 서버 값.
|
|
* 서버가 버린 고침(행 없는 측점 · 기본 설계 못 냄 · 측점 맞춤 실패)도 한 줄(445 M5). */
|
|
async function warnUnrecomputed(
|
|
request: Promise<SectionConfirmResponse>,
|
|
): Promise<SectionConfirmResponse> {
|
|
const result = await request;
|
|
warnSectionSave(result);
|
|
return result;
|
|
}
|
|
|
|
/** 횡단 저장 본문(저장 · 확정 · 한 본문 [저장] 조각 공통). */
|
|
export function sectionConfirmBody(
|
|
standardCrossSection?: StandardCrossSection,
|
|
crossPatches?: CrossSectionPatch[],
|
|
massHaul?: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
const body: Record<string, unknown> = {};
|
|
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<SectionConfirmResponse>): 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<string, unknown>,
|
|
): Promise<SectionConfirmResponse> {
|
|
const body = sectionConfirmBody(standardCrossSection, crossPatches, massHaul);
|
|
return warnUnrecomputed(
|
|
requestJson<SectionConfirmResponse>(`/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<CompanyStandardListResponse> {
|
|
return requestJson<CompanyStandardListResponse>(
|
|
`/projects/${projectId}/sections/company-standards`,
|
|
{ method: "GET" },
|
|
);
|
|
}
|
|
|
|
/** 특정 프로젝트의 표준횡단 설계값을 미리보기용으로 조회한다(적용 전, 현재 값 불변). */
|
|
export async function getCompanyStandard(
|
|
projectId: string,
|
|
sourceProjectId: string,
|
|
): Promise<CompanyStandardResponse> {
|
|
return requestJson<CompanyStandardResponse>(
|
|
`/projects/${projectId}/sections/company-standards/${sourceProjectId}`,
|
|
{ method: "GET" },
|
|
);
|
|
}
|
|
|
|
/** B07 횡단 수량표 한 장(138-4 · 서버 `cross_quantity_sheet`) — 값 · 글 · 칸 배치. */
|
|
export interface CrossQuantitySheet {
|
|
quantity_table: Record<string, number | null>;
|
|
/** B07 표에 찍히는 글 그대로(값 없음 = 「-」). */
|
|
text: Record<string, string>;
|
|
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<CrossQuantitySheet> {
|
|
return requestJson<CrossQuantitySheet>(
|
|
`/projects/${projectId}/sections/${routeId}/cross-quantity-table`,
|
|
{ method: "POST", body: JSON.stringify({ chainage_m: chainageM, design: design ?? null }) },
|
|
);
|
|
}
|