Files
Aislo/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts
T
eomsangdonandClaude Opus 5 ca00ac278d feat(B07): 표준도를 「보여주기」에서 「고치기」로 — 제원 네 칸 입력
`phase: "detail"` 칸(돌 종류·조달·뒷길이·전면 기울기)을 **그리는 화면이 없었음**
(2026-09-09 실측: B06 구조물 폼은 b05 phase 열 칸만 그림. 화면 글자 전수에서 「조달」·
「뒷길이」·「돌 종류」 0건). 그 자리를 표준도가 맡음 — PLAN 4-5b 「표준도는 입력 화면이기도 함」.
B06 은 「어디에·몇 m」(배치), 표준도는 「어떤 제원」.

**장 하나 = 제원 조합 하나**라 한 번 고치면 그 조합의 개소 전부에 걸림.

지킨 것
  · **자동값을 저장에 박지 않음** — 빈 칸이 「정한 적 없음」의 뜻(확정 ⑨). 비우면 그 키를
    지움. 판정된 기울기는 칸이 아니라 **회색 도움말**로만 비춤.
  · **막지 않음** — 실무 도면에 1:0.7·1:0.8 이 실재(구조물도 53장). 품셈 범위(0.20~0.50)
    밖이어도 값은 받고 안내만.
  · **값을 여기서 셈하지 않음** — 정본에 적기만 하고 표·그림은 다음 조회에서 그 정본으로
    다시 섬. 표준도가 두 번째 정본이 되면 안 됨.

⚠ 만들다 잡은 것 셋
  ① `face_slope_ratio` 가 **구조물 등록부에 없어** 저장이 통째로 거절됐음
     (`ValueError: 돌쌓기(찰)에 정의되지 않은 옵션입니다`). 한 칸 때문에 전부 못 저장되는
     것은 나쁨 — 등록부에 없는 칸은 **빼고 이름으로 알림**(조용히 버리면 저장된 줄 앎).
     그 칸 신설은 랩탑 메인 몫.
  ② 저장 뒤 표·그림을 다시 받으면서 **폼이 새로 그려져 안내가 지워졌음.** 밖에서 들고
     있다가 다시 넣음.
  ③ **장 제목에 제원이 들어 있는데 좌측 단추 글자가 안 따라왔음.** 목록을 통째로 다시
     받지 않고 표준도 줄만 갈아 끼움.

실화면 확인 (프로젝트 936be972, 개발 우회로로 열어서)
  「표준도 2장」에서 돌 종류 견치돌·뒷길이 55 로 저장 →
    단추 글자가 「…뒷길이 35㎝ 야면석·호박돌」 → 「…뒷길이 55㎝ 견치돌」로 **즉시 바뀜**
  기울기 0.7 저장 → 안내 셋: 「1개소에 반영했습니다」 · 「1:0.7 는 품셈 표준경사 표
    범위(1:0.2~1:0.5) 밖입니다 — 값은 그대로 씁니다」 · 「「전면 기울기」 칸이 masonry_dry
    등록부에 아직 없어 저장하지 못했습니다」
  조달 「구입」 저장 → 그 장의 **2개소에만** 들어가고 다른 장은 그대로(정본 실측)
  확인 뒤 **지정받은 제원으로 되돌리고 우회로도 닫음**(목록 409 복귀).

700줄 제한으로 표준도 라우터를 `B07_DesignDetail_Router_Standard.py` 로 뗌(579 + 153).

자체검증 — 새 시험 7건 + 회귀 580 통과 · 0 실패, `tsc --noEmit` 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 20:41:43 +09:00

290 lines
8.9 KiB
TypeScript

/* 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<string, unknown>[];
/** 횡단도 측점별 실좌표(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<string, number | null>;
/** 측구 형식별 규격 (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<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" },
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<DesignDrawingListResponse> {
return requestJson(`/projects/${projectId}/design-drawings`);
}
export function fetchDesignDrawing(
projectId: string,
drawingId: string,
): Promise<DesignDrawingResponse> {
return requestJson(`/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}`);
}
export function confirmDesignDrawing(
projectId: string,
drawingId: string,
drawing: CadDrawing,
quantityTable?: QuantityTable | null,
): Promise<DesignDrawingConfirmResponse> {
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<void> {
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<string, string>;
}
export function fetchFrameTemplate(projectId: string): Promise<FrameTemplateResponse> {
return requestJson(`/projects/${projectId}/frame-template`);
}
export function saveFrameTemplate(projectId: string, drawing: CadDrawing): Promise<void> {
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<void> {
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<string, unknown>;
}[];
}
export function fetchStandardSheets(projectId: string): Promise<StandardSheetsResponse> {
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;
},
): 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`);
}