- design_save: 노선 초안이 없으면 saveOneBody — 차례마다 조각(본문 · 저장 뒤 정리) · 서버가 다 저장한 뒤에만 정리(초안 비움 · 화면 맞춤) · 실패 = 차례 이름째 「아무것도 저장 안 함」 · 되돌림 실패 = 「복구 확인 필요」 · 기다림 알림 - 조각 함수: 페이지 설정 · 관 · 구조물(보기 목록) · 추가 측점 · 계획고(한 벌 종단도) · 관 옵션 · 상단측 · 횡단(횡단 보기) — 옛 flush 함수는 노선 바뀐 저장 길에 그대로(조각을 써서 같은 본문) - 보기 몫 steps · cross → parts · crossPart · 노선 바뀐 저장 길은 옛 차례 그대로(6-B) - 옛 9 차례를 단언하던 시험 10 개를 한 본문 꼴로(옛 PUT 줄 → SAVE 줄 · 조각 정리 차례) · 위키 [저장] 표 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
334 lines
13 KiB
TypeScript
334 lines
13 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 { CrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
|
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" },
|
|
);
|
|
}
|
|
|
|
/** 계획선 편집 델타(자동 선형 대비 계획고 델타 + 종단곡선 반경). B05 편집 스토어와 같은 모양. */
|
|
export interface ProfileAlignmentEdits {
|
|
station_offsets: Record<string, number>;
|
|
curve_radii: Record<string, number>;
|
|
}
|
|
|
|
/**
|
|
* 계획선 편집 프리뷰 결과 — **바뀌는 것만** 온다.
|
|
* 횡단 지반선 원시 샘플은 계획고와 무관해 그대로이므로 싣지 않는다(응답 1MB → 수십 KB).
|
|
*/
|
|
export interface CrossDesignPreviewResponse {
|
|
status: string;
|
|
/** 유토곡선이 읽는 설계 필드만 담긴 부분 갱신값(설계선 좌표 등은 오지 않는다). */
|
|
designs: Array<{ chainage_m: number; design: Partial<CrossDesign> }>;
|
|
}
|
|
|
|
/**
|
|
* 계획선 편집을 반영해 **계획선 + 전 측점 횡단 설계**를 다시 계산해 받는다(서버 저장 없음).
|
|
*
|
|
* B05에서 계획고를 끄는 동안 횡단 단면적이 함께 움직여야 횡단 기준 유토곡선이 따라온다.
|
|
* 측점마다 따로 부르면 수십 번 왕복하므로 한 번에 계산한다. 영속화는 각 페이지의
|
|
* 임시저장·확정이 맡는다.
|
|
*/
|
|
export async function previewCrossDesigns(
|
|
projectId: string,
|
|
routeId: number,
|
|
edits: ProfileAlignmentEdits,
|
|
standardCrossSection?: StandardCrossSection,
|
|
options?: {
|
|
/** true면 설계 전체(설계선 좌표 포함)를 받는다 — B06 진입 시 stale 일괄 재계산용. */
|
|
fullDesigns?: boolean;
|
|
/** 측점별 암 경계 오프셋 세션값(chainage 키 → m). DB 저장분보다 우선한다. */
|
|
rockBoundaryOffsets?: Record<string, number>;
|
|
/** 측점별 소단 제원(chainage 키 → 폭·간격·기울기). 값이 없는 측점은 소단 없음. */
|
|
berms?: Record<
|
|
string,
|
|
{ width_m: number; interval_m: number; slope_deg: number; depth_m?: number }
|
|
>;
|
|
/** 측점별 암 절토 경사비(chainage 키 → 1:n 의 n). 0 은 「표준값을 씀」이다. */
|
|
cutSlopeRatios?: Record<string, number>;
|
|
/** 측점별 카드 선택(chainage 키 → 지반 · 측구 · 복합경사 …) 세션값 — 저장분 위에 얹음(439). */
|
|
designChoices?: Record<string, CrossDesignChoice>;
|
|
},
|
|
): Promise<CrossDesignPreviewResponse> {
|
|
return requestJson<CrossDesignPreviewResponse>(
|
|
`/projects/${projectId}/sections/${routeId}/cross-design/preview`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
...edits,
|
|
standard_cross_section: standardCrossSection ?? null,
|
|
full_designs: options?.fullDesigns ?? false,
|
|
rock_boundary_offsets: options?.rockBoundaryOffsets ?? null,
|
|
berms: options?.berms ?? null,
|
|
cut_slope_ratios: options?.cutSlopeRatios ?? null,
|
|
design_choices: options?.designChoices ?? null,
|
|
}),
|
|
},
|
|
);
|
|
}
|
|
|
|
/** 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 }) },
|
|
);
|
|
}
|