refactor(472-2 2-A B05 걷어내기): 안 쓰는 길 지움 — Ford 시설 칸 파일 · 노선 초기화 엔드포인트(replan/reset) · TS solveRoute · 바깥 부름 0 export 열
- B05_Profile_UI_Drainage_Facility_Ford.ts 통째 · 글 검사 시험 목록 한 줄 - POST /route/replan/reset(reset_route_plan) + TS resetRoutePlan · 머리 설명 줄 - TS solveRoute + 딸린 RouteSolveRequest · RouteSolveResponse 타입(엔드포인트 /route/solve 는 남김) - defaultOptions · cutInnerTo · inletKindOf(+ 딸려 죽은 INLET_KINDS) · createBasinSplitFields · COLLAPSED_KEY · pointInRing · minCoverWarningText · designHalfWidths · segmentsIntersect · parseStationText — 파일 안 부름도 0 = 몸통째 - 위키 B05_Profile_Api_Fetch 의 solveRoute 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
* 2차 워크플로우(경로 설계) API 클라이언트
|
||||
*
|
||||
* 백엔드 계약 (B05_Profile_Router.py):
|
||||
* POST /api/projects/{project_id}/route/solve → 경로 탐색 + DB 기록
|
||||
* POST /api/projects/{project_id}/route/confirm → 최신 경로 확정
|
||||
*
|
||||
* 규칙:
|
||||
@@ -28,44 +27,6 @@ export interface CirclePoint extends RoutePoint {
|
||||
radius_m: number;
|
||||
}
|
||||
|
||||
/** 경로 탐색 실행 요청 (RouteSolveRequest) */
|
||||
export interface RouteSolveRequest {
|
||||
filter_key: string;
|
||||
method?: string;
|
||||
smooth?: boolean;
|
||||
surface_model_id?: number | null;
|
||||
algorithm?: string;
|
||||
bp: RoutePoint;
|
||||
ep: RoutePoint;
|
||||
cp?: RoutePoint[];
|
||||
ap?: CirclePoint[];
|
||||
fp?: CirclePoint[];
|
||||
grade_class?: string;
|
||||
paved?: boolean;
|
||||
min_curve_radius_m?: number | null;
|
||||
max_uphill_grade?: number | null;
|
||||
max_downhill_grade?: number | null;
|
||||
min_uphill_grade?: number | null;
|
||||
min_downhill_grade?: number | null;
|
||||
allow_avoid_pass_through?: boolean;
|
||||
station_interval_m?: number | null;
|
||||
cross_half_width_m?: number | null;
|
||||
cross_sample_interval_m?: number | null;
|
||||
long_sample_interval_m?: number | null;
|
||||
terrain_type?: string;
|
||||
/** 설계속도(km/h) — 종단 법정 기준 축. 임도 기본 20(2026-08-19). */
|
||||
design_speed_kph?: number | null;
|
||||
main_direction?: string;
|
||||
max_grade_pct?: number | null;
|
||||
min_vertical_radius_m?: number | null;
|
||||
min_tangent_length_m?: number | null;
|
||||
balance_segment_length_m?: number | null;
|
||||
start_elevation_offset_m?: number | null;
|
||||
end_elevation_offset_m?: number | null;
|
||||
/** 횡단배수 최소 계획고 강제 — 기본 해제(2026-09-01 사용자 지시). */
|
||||
enforce_pipe_clearance?: boolean | null;
|
||||
}
|
||||
|
||||
/** 계획선 산출 요약 (실패 시 null) */
|
||||
export interface RouteGradeSummary {
|
||||
id: string;
|
||||
@@ -82,20 +43,6 @@ export interface RouteGradeSummary {
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/** 경로 탐색 실행 결과 (RouteSolveResponse) */
|
||||
export interface RouteSolveResponse {
|
||||
status: string;
|
||||
project_id: string;
|
||||
route_id: number;
|
||||
total_length_m: number;
|
||||
metrics: Record<string, unknown>;
|
||||
required_points_ok: boolean;
|
||||
route_data_path: string;
|
||||
longitudinal_length_m: number | null;
|
||||
cross_section_count: number | null;
|
||||
grade_summary: RouteGradeSummary | null;
|
||||
}
|
||||
|
||||
/** 경로 확정 결과 (RouteConfirmResponse) */
|
||||
export interface RouteConfirmResponse {
|
||||
status: string;
|
||||
@@ -190,17 +137,6 @@ async function requestJson<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/** 경로 탐색을 실행한다 (비용면 생성 → Dijkstra/ridge-valley → GeoJSON 저장). */
|
||||
export async function solveRoute(
|
||||
projectId: string,
|
||||
request: RouteSolveRequest,
|
||||
): Promise<RouteSolveResponse> {
|
||||
return requestJson<RouteSolveResponse>(`/projects/${projectId}/route/solve`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
/** 등고선 간격 재적용 값을 서버(stage 1 params)에 영속화한다. */
|
||||
export async function updateContourInterval(
|
||||
projectId: string,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*
|
||||
* GET /projects/{id}/route/plan → 예상노선·계획노선 정점(사업지 좌표계 m)
|
||||
* POST /projects/{id}/route/replan → 노선 초안 저장(346-3 · draft = 계약 RouteDraft · as_initial)
|
||||
* POST /projects/{id}/route/replan/reset → 계획노선을 예상노선으로 되돌리고 재계산
|
||||
* GET /projects/{id}/route/terrain-sources → 고를 수 있는 분석 지형(라이다 · 수치지형도)
|
||||
*
|
||||
* 편집 창 분석(지반고 · 횡단 미리보기 · 추천)은 **고른 분석 지형**(`terrain`)을 실어
|
||||
@@ -255,12 +254,3 @@ export interface CrossPreviewResponse {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/** 계획노선을 예상노선으로 되돌리고 같은 재계산을 돈다(노선 초기화). */
|
||||
export async function resetRoutePlan(projectId: string): Promise<RouteReplanResponse> {
|
||||
return requestJson<RouteReplanResponse>(
|
||||
`/projects/${projectId}/route/replan/reset`,
|
||||
{ method: "POST" },
|
||||
REPLAN_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -367,17 +367,6 @@ export function structureAnchorM(structure: StructureInstance): number {
|
||||
return structure.chainage_m ?? structure.start_m ?? 0;
|
||||
}
|
||||
|
||||
/** 타입 정의의 기본값으로 옵션을 채운다(신규 추가·타입 변경 시). */
|
||||
export function defaultOptions(type: StructureType): Record<string, string | number> {
|
||||
const options: Record<string, string | number> = {};
|
||||
type.options.forEach((field) => {
|
||||
// 기본값이 있는 항목만 채운다. 필수 선택지의 첫 항목을 대신 넣어 주면 사용자가
|
||||
// 고르지도 않은 재료·형식이 확정값으로 저장된다(2026-08-16 크로스체크 지적 2).
|
||||
if (field.default !== null && field.default !== undefined) options[field.key] = field.default;
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
/* ── 미저장 구조물 조작분(세션) ────────────────────────────────────────────
|
||||
* 조작은 세션에만 쌓고 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장,
|
||||
* 2026-08-29 사용자 확정). B05에서 만지고 B06으로 넘어가 확정하는 경로가 있어
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
- **계획노선**(수정본) — 초기본에서 시작해 사용자가 **노드를 잡아** 고친 것.
|
||||
`planned_route.csv`. 설계 계통은 이것이 있으면 이것을 읽는다(`load_design_route`).
|
||||
|
||||
노선 초기화는 수정본 파일을 지우는 것이다 — 그러면 초기 폴리라인을 읽으므로 「초기본을
|
||||
수정본으로 복사」와 결과가 같다.
|
||||
|
||||
노선 저장(346-3) — 여기서 노선을 갈아 끼운 뒤 `B05_Profile_Route_Save.run_route_save_chain` 이
|
||||
풀기 → 관 · 구조물 좌표 따라감 → 종횡단 한 번 → 계획고 옮김 → 횡단 이월(측점 열쇠) → B06 확정까지
|
||||
돈다. 배수유역은 다시 안 셈(낡음) · 관 정본은 안 지움 · 초기값은 추천 [적용](`as_initial`)일 때만.
|
||||
@@ -21,7 +18,6 @@
|
||||
|
||||
GET /api/projects/{id}/route/plan → 예상노선·계획노선 정점(사업지 좌표계)
|
||||
POST /api/projects/{id}/route/replan → 노선 초안 저장(노선 저장 체인)
|
||||
POST /api/projects/{id}/route/replan/reset → 계획노선을 예상노선으로 되돌리고 재계산
|
||||
POST /api/projects/{id}/route/initial → 초기 계획노선 갈래 하나를 만들어 돌려줌(추천 저장본만 씀)
|
||||
"""
|
||||
|
||||
@@ -669,30 +665,5 @@ async def replan_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, An
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{project_id}/route/replan/reset", response_model=None)
|
||||
async def reset_route_plan(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
"""계획노선을 예상노선으로 되돌리고 다시 계산한다(노선 초기화).
|
||||
|
||||
수정본 파일을 지우면 설계 계통이 원본을 읽으므로 「원본을 수정본으로 복사」와 같다.
|
||||
"""
|
||||
paths = await _project_paths(project_id)
|
||||
if paths is None:
|
||||
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
|
||||
project_root, stored_path = paths
|
||||
await asyncio.to_thread(_ensure_expected_route, project_root)
|
||||
radius_m, radius_limit_m, arc_limit_m = await _plan_criteria(project_id)
|
||||
await asyncio.to_thread(_ensure_planned_initial, project_root, radius_m)
|
||||
working_path = planned_route_working_path(project_root)
|
||||
if working_path.is_file():
|
||||
working_path.unlink()
|
||||
logger.info("계획노선 초기화(초기 폴리라인으로): project_id=%s", project_id)
|
||||
result = await _recompute(project_id, project_root, stored_path)
|
||||
if "error" in result:
|
||||
return JSONResponse(
|
||||
status_code=409, content={"status": "error", "message": result["error"]}
|
||||
)
|
||||
return {"status": "success", "project_id": str(project_id), **result}
|
||||
|
||||
|
||||
# `POST /route/initial` 은 나눈 파일에서 같은 `router` 에 붙는다 — 끝에서 불러 등록.
|
||||
from B05_Profile import B05_Profile_Router_RouteInitial # noqa: E402, F401
|
||||
|
||||
@@ -360,40 +360,6 @@ export function classifyStation(section: CrossSection): StationPieces | null {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 단면(도로측 → 바깥)에서 **경계 편거리 안쪽을 잘라낸다**(2026-08-25 구조물 절단).
|
||||
* 경계 바깥 조각만 남기고, 경계 지점을 보간해 첫 점으로 세운다 — 이 점이 행마다
|
||||
* 이어져 절단선(메시 모서리)이 된다. 전부 안쪽이면 null(조각 드롭).
|
||||
*/
|
||||
export function cutInnerTo(
|
||||
inner2outer: OffsetPoint[],
|
||||
boundaryOffset: number,
|
||||
side: "left" | "right",
|
||||
): OffsetPoint[] | null {
|
||||
const sign = side === "left" ? 1 : -1;
|
||||
const beyond = (point: OffsetPoint): number => (point.offset_m - boundaryOffset) * sign;
|
||||
const kept: OffsetPoint[] = [];
|
||||
for (let i = 0; i < inner2outer.length; i += 1) {
|
||||
const point = inner2outer[i];
|
||||
if (beyond(point) >= -1e-9) {
|
||||
if (!kept.length && i > 0) {
|
||||
// 경계 통과 지점을 보간해 첫 점으로.
|
||||
const prev = inner2outer[i - 1];
|
||||
const span = point.offset_m - prev.offset_m;
|
||||
const t = Math.abs(span) < 1e-12 ? 0 : (boundaryOffset - prev.offset_m) / span;
|
||||
kept.push({
|
||||
offset_m: boundaryOffset,
|
||||
elevation_m: prev.elevation_m + (point.elevation_m - prev.elevation_m) * t,
|
||||
});
|
||||
}
|
||||
kept.push({ ...point });
|
||||
}
|
||||
}
|
||||
if (kept.length < 2) return null;
|
||||
const width = Math.abs(kept[kept.length - 1].offset_m - kept[0].offset_m);
|
||||
return width < SLOPE_MIN_WIDTH_M ? null : kept;
|
||||
}
|
||||
|
||||
/** 비탈 폭 하한(m) — 트림 후 이보다 좁으면 그 행에서 비탈이 없는 것으로 본다. */
|
||||
export const SLOPE_MIN_WIDTH_M = 0.02;
|
||||
/** 폴리라인 안에서 지반을 못 만날 때 마지막 구배로 연장하는 한계(m)·걸음(m). */
|
||||
|
||||
@@ -16,26 +16,6 @@ import { FORD_MANNING_N, FORD_SLOPE } from "@config/config_frontend";
|
||||
* 문자열 리터럴로 두어 B05가 B06 모듈에 기대지 않게 한다. */
|
||||
export type InletStructureKind = "auto" | "revet" | "I" | "L" | "U";
|
||||
|
||||
/** 유입구 "구조" 목록 — 조정창의 구조물 형식(자동/기슭막이/집수정 I·ㄴ·ㄷ)을 이 한
|
||||
* 드롭다운으로 합쳤다(2026-08-29 사용자 지시 5: 일단 리스트를 합치고 중복은 뒤에 뺀다).
|
||||
* 고른 값 하나가 정본 둘을 정한다 — 관 옵션 `inlet_type`(기슭막이/집수정)과 B06
|
||||
* 유입측 형식(`inlet_structure`). */
|
||||
export const INLET_KINDS: ReadonlyArray<{
|
||||
label: string;
|
||||
type: "기슭막이" | "집수정";
|
||||
structure: InletStructureKind;
|
||||
}> = [
|
||||
{ label: "흙막이", type: "기슭막이", structure: "revet" },
|
||||
{ label: "집수정", type: "집수정", structure: "auto" },
|
||||
{ label: "자동(규칙)", type: "기슭막이", structure: "auto" },
|
||||
{ label: "집수정 I형", type: "집수정", structure: "I" },
|
||||
{ label: "집수정 ㄴ형", type: "집수정", structure: "L" },
|
||||
{ label: "집수정 ㄷ형", type: "집수정", structure: "U" },
|
||||
];
|
||||
|
||||
export const inletKindOf = (label: string): (typeof INLET_KINDS)[number] =>
|
||||
INLET_KINDS.find((kind) => kind.label === label) ?? INLET_KINDS[0];
|
||||
|
||||
/* ── 물넘이·세월교 개략 단면 ────────────────────────────────────────────── */
|
||||
|
||||
/** 월류 단면 한 벌 — 설계유량을 흘리는 데 **필요한** 수심·단면적과 그때의 유속. */
|
||||
@@ -363,61 +343,6 @@ export function createRevetmentFields(keys: RevetmentKeys): RevetmentFields {
|
||||
};
|
||||
}
|
||||
|
||||
/** 집수정 기준측점 전 · 후 한 줄 — 옛 조정창에만 있던 칸(계획서 24-4a). 길이 칸과 짝:
|
||||
* 전 + 후 = 길이 · 나중에 고친 쪽이 살고 반대쪽이 나머지(기슭막이 칸과 같은 규칙). */
|
||||
export function createBasinSplitFields(length: HTMLInputElement): {
|
||||
row: HTMLDivElement;
|
||||
write: (options: Record<string, string | number>) => void;
|
||||
read: (target: Record<string, string | number>) => void;
|
||||
onChange: (handler: () => void) => void;
|
||||
} {
|
||||
const before = numberInput("0.1", "0");
|
||||
const after = numberInput("0.1", "0");
|
||||
let last: HTMLInputElement = before;
|
||||
const sync = (): void => {
|
||||
const total = Number.parseFloat(length.value);
|
||||
if (!Number.isFinite(total)) return;
|
||||
const other = last === before ? after : before;
|
||||
const kept = Math.min(Math.max(Number.parseFloat(last.value) || 0, 0), total);
|
||||
last.value = kept.toFixed(1);
|
||||
other.value = (total - kept).toFixed(1);
|
||||
};
|
||||
return {
|
||||
row: grid(
|
||||
labeled("기준측점 전 (m)", stepper(before, 0.5), "inlet_basin_before_m"),
|
||||
labeled("기준측점 후 (m)", stepper(after, 0.5), "inlet_basin_after_m"),
|
||||
),
|
||||
write(options) {
|
||||
before.value =
|
||||
options.inlet_basin_before_m !== undefined ? String(options.inlet_basin_before_m) : "";
|
||||
after.value =
|
||||
options.inlet_basin_after_m !== undefined ? String(options.inlet_basin_after_m) : "";
|
||||
},
|
||||
read(target) {
|
||||
// 전/후는 0도 값(기준이 곧 끝) — 빈칸만 뺀다.
|
||||
for (const [key, input] of [
|
||||
["inlet_basin_before_m", before],
|
||||
["inlet_basin_after_m", after],
|
||||
] as const) {
|
||||
const value = Number.parseFloat(input.value);
|
||||
if (Number.isFinite(value) && value >= 0) target[key] = Number(value.toFixed(1));
|
||||
}
|
||||
},
|
||||
onChange(handler) {
|
||||
for (const input of [before, after]) {
|
||||
input.addEventListener("change", () => {
|
||||
last = input;
|
||||
sync();
|
||||
handler();
|
||||
});
|
||||
}
|
||||
length.addEventListener("change", () => {
|
||||
if (before.value || after.value) sync();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 날개벽 옵션 키 묶음 — 유입·유출이 같은 조각을 쓰되 키만 다르다. */
|
||||
interface WingKeys {
|
||||
install: string;
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Facility_Ford.ts
|
||||
* 물넘이포장·세월교 칸 묶음 — 월류 폭·높이·바닥 경사·포장 두께·길이·련 수 + 개략 단면 줄.
|
||||
*
|
||||
* 서브폼 본체(`_Drainage_Facility.ts`)가 700줄에 닿아 떼어 냄(2026-09-14).
|
||||
* 월류 폭 기본값(세월교 10m · 물넘이 5m · 등록부)은 시설이 생길 때 칸에 실제 값으로 들어옴
|
||||
* (PLAN 46-7 · 제안값 단추 없음). 빈 칸 안내 = 엔진이 비운 칸을 읽는 값(폭 기본 · 필요 수심).
|
||||
* 적은 높이가 필요 수심보다 낮으면 되돌림(계산값 미만은 안 받음).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
FORD_BRIDGE_DEFAULT_WIDTH_M,
|
||||
FORD_PAVEMENT_DEFAULT_WIDTH_M,
|
||||
} from "@config/config_frontend";
|
||||
import {
|
||||
fordSection,
|
||||
grid,
|
||||
labeled,
|
||||
numberInput,
|
||||
putNumber,
|
||||
stepper,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
|
||||
type FordKind = "ford_pavement" | "ford_bridge";
|
||||
|
||||
export interface FordFields {
|
||||
widthRow: HTMLElement;
|
||||
slopeRow: HTMLElement;
|
||||
countRow: HTMLElement;
|
||||
summary: HTMLElement;
|
||||
/** 바뀌면 저장 흐름을 울릴 칸(높이는 자기 검사 뒤 스스로 울림). */
|
||||
inputs: HTMLInputElement[];
|
||||
setDesignFlow: (designFlow: number | null) => void;
|
||||
sync: () => void;
|
||||
write: (kind: FordKind, options: Record<string, string | number>) => void;
|
||||
read: (kind: FordKind, target: Record<string, string | number>) => void;
|
||||
}
|
||||
|
||||
export function createFordFields(emit: () => void): FordFields {
|
||||
// 세월교 — 구체 내 배관 수량(련). 련은 정수라 소수 자릿수를 두지 않는다.
|
||||
const count = numberInput("1", "1", "련");
|
||||
const countRow = grid(labeled("수량 (련)", stepper(count, 1, 0), "pipe_count"));
|
||||
const width = numberInput("0.1");
|
||||
const height = numberInput("0.01");
|
||||
const widthRow = grid(
|
||||
labeled("월류 폭 (m)", stepper(width, 0.1), "ford_width_m"),
|
||||
labeled("월류 높이 (m)", stepper(height, 0.1), "ford_height_m"),
|
||||
);
|
||||
// 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정).
|
||||
// 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다.
|
||||
const slope = numberInput("0.1");
|
||||
slope.placeholder = "노면 기울기";
|
||||
// 포장 두께·노폭 방향 길이 — 수량(㎡ = 월류 폭 × 길이 · 두께로 원단위 고름)이 읽는 칸(A3).
|
||||
const thickness = numberInput("1");
|
||||
const length = numberInput("0.1");
|
||||
const slopeRow = grid(
|
||||
labeled("바닥 경사 유입→유출 (%)", stepper(slope, 0.1), "ford_slope_pct"),
|
||||
labeled("포장 두께 (㎝)", stepper(thickness, 1, 0), "thickness_cm"),
|
||||
labeled("포장 길이 노폭 방향 (m)", stepper(length, 0.1), "length_m"),
|
||||
);
|
||||
const summary = document.createElement("p");
|
||||
summary.className = "b05-drainage__facility-note";
|
||||
let designFlowM3s: number | null = null;
|
||||
/** 현재 조건(설계유량·월류 폭)의 필요 최소 수심(m). 계산 불가면 null. */
|
||||
let minDepthM: number | null = null;
|
||||
|
||||
/** 폭을 비우면 엔진이 쓰는 폭(`B06_Section_Engine_Culvert.FORD_*_DEFAULT_WIDTH_M` 짝). */
|
||||
const emptyWidth = (kind: FordKind): number =>
|
||||
kind === "ford_bridge" ? FORD_BRIDGE_DEFAULT_WIDTH_M : FORD_PAVEMENT_DEFAULT_WIDTH_M;
|
||||
|
||||
function sync(): void {
|
||||
const widthM = Number.parseFloat(width.value || width.dataset.empty || "");
|
||||
const section = designFlowM3s !== null ? fordSection(designFlowM3s, widthM) : null;
|
||||
minDepthM = section ? Number(section.depthM.toFixed(2)) : null;
|
||||
height.placeholder = minDepthM !== null ? `필요 수심 ${minDepthM.toFixed(2)} 이상` : "";
|
||||
if (designFlowM3s === null) {
|
||||
summary.textContent =
|
||||
"담당 유역의 설계유량이 아직 없습니다 — [유역 분석] 후 개략 단면이 나옵니다.";
|
||||
return;
|
||||
}
|
||||
const head = `설계유량 ${designFlowM3s.toFixed(3)} ㎥/s`;
|
||||
if (!section) {
|
||||
summary.textContent = `${head} · 월류 폭을 넣으면 필요 수심·단면을 계산합니다.`;
|
||||
return;
|
||||
}
|
||||
// 필요 최소 수심이다 — 월류 높이는 이 값 아래로 내릴 수 없다.
|
||||
summary.textContent =
|
||||
`${head} · 필요 수심 ${section.depthM.toFixed(2)} m · ` +
|
||||
`필요 단면 ${section.areaM2.toFixed(2)} ㎡ · 유속 ${section.velocityMs.toFixed(2)} m/s — ` +
|
||||
`월류 높이는 필요 수심 이상만 입력됩니다.`;
|
||||
}
|
||||
|
||||
// 적은 값이 계산값 미만이면 받지 않는다 — 계산값으로 되돌리고 잠깐 붉힌다(빈 칸은 그대로 빈 칸).
|
||||
height.addEventListener("change", () => {
|
||||
const value = Number.parseFloat(height.value);
|
||||
if (
|
||||
minDepthM !== null &&
|
||||
height.value !== "" &&
|
||||
(!Number.isFinite(value) || value < minDepthM)
|
||||
) {
|
||||
height.value = minDepthM.toFixed(2);
|
||||
height.classList.add("is-invalid");
|
||||
window.setTimeout(() => height.classList.remove("is-invalid"), 900);
|
||||
}
|
||||
emit();
|
||||
});
|
||||
|
||||
return {
|
||||
widthRow,
|
||||
slopeRow,
|
||||
countRow,
|
||||
summary,
|
||||
inputs: [count, width],
|
||||
setDesignFlow(designFlow) {
|
||||
designFlowM3s = designFlow ?? null;
|
||||
},
|
||||
sync,
|
||||
write(kind, options) {
|
||||
const text = (key: string): string =>
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
count.value = kind === "ford_bridge" ? text("pipe_count") : "";
|
||||
width.value = text("ford_width_m");
|
||||
width.dataset.empty = String(emptyWidth(kind));
|
||||
width.placeholder = `비우면 ${emptyWidth(kind)}`;
|
||||
height.value = text("ford_height_m");
|
||||
slope.value = text("ford_slope_pct");
|
||||
thickness.value = text("thickness_cm");
|
||||
length.value = text("length_m");
|
||||
},
|
||||
read(kind, target) {
|
||||
putNumber(target, "ford_width_m", width.value);
|
||||
// 월류 높이는 cm 단위 수심이라 0.01m 정밀도로 싣는다(putNumber는 0.1m 반올림).
|
||||
const value = Number.parseFloat(height.value);
|
||||
if (Number.isFinite(value) && value > 0) target.ford_height_m = Number(value.toFixed(2));
|
||||
if (kind === "ford_pavement") {
|
||||
putNumber(target, "ford_slope_pct", slope.value);
|
||||
putNumber(target, "thickness_cm", thickness.value);
|
||||
putNumber(target, "length_m", length.value);
|
||||
return;
|
||||
}
|
||||
const pipes = Number.parseInt(count.value, 10);
|
||||
if (Number.isFinite(pipes) && pipes > 0) target.pipe_count = pipes;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -58,7 +58,6 @@ export const hotspotToggleColor = (): string => themeColor("--map-flow-ramp-4",
|
||||
export const satelliteToggleColor = (): string => themeColor("--map-satellite-toggle", "#64748b");
|
||||
|
||||
/** 접힘·폭은 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
||||
export const COLLAPSED_KEY = stateKey("drainage-collapsed") ?? "";
|
||||
/** 드래그로 조절한 패널 폭(px) 보관 키 — 브라우저 세션 동안만 유지한다. */
|
||||
export const WIDTH_KEY = stateKey("drainage-width") ?? "";
|
||||
/** 지도가 담기는 최소 폭(px). CSS의 min-width와 같은 값. */
|
||||
@@ -257,17 +256,6 @@ export function drawHotspots(
|
||||
context.restore();
|
||||
}
|
||||
|
||||
/** 화면 좌표 폴리곤 안에 점이 있는지(홀짝 규칙). 유역을 눌러 고를 때 쓴다. */
|
||||
export function pointInRing(ring: ReadonlyArray<[number, number]>, x: number, y: number): boolean {
|
||||
let inside = false;
|
||||
for (let index = 0, previous = ring.length - 1; index < ring.length; previous = index++) {
|
||||
const [xi, yi] = ring[index];
|
||||
const [xj, yj] = ring[previous];
|
||||
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/** 표시 토글 한 줄을 통째로 만든다 — 위성/도엽/화살표/강도/집중점/상류세류 6종.
|
||||
* 패널이 700줄 한계에 닿아 옮겼다. 상태는 갖지 않고 켜고 끌 때 넘겨받은 함수만 부른다. */
|
||||
export function mountDrainageToggles(
|
||||
|
||||
@@ -124,14 +124,6 @@ export function findMinCoverViolations(
|
||||
return violations;
|
||||
}
|
||||
|
||||
/** 경고 문구 — 가장 크게 모자란 곳부터 알려 준다. */
|
||||
export function minCoverWarningText(violations: MinCoverViolation[]): string | null {
|
||||
if (!violations.length) return null;
|
||||
const worst = [...violations].sort((a, b) => b.shortfall_m - a.shortfall_m)[0];
|
||||
const more = violations.length > 1 ? ` 외 ${violations.length - 1}곳` : "";
|
||||
return `${worst.label} 최소고 ${worst.shortfall_m.toFixed(2)}m 부족${more}`;
|
||||
}
|
||||
|
||||
/** 종단 격자에서 그 자리 원지반고 — 가드 판정용 선형 보간. */
|
||||
function groundAt(base: AlignmentBase, chainageM: number): number | null {
|
||||
const { chainage: xs, ground: ys } = base;
|
||||
|
||||
@@ -53,18 +53,6 @@ export function summarizeCross(preview: CrossPreviewResponse): string {
|
||||
return `${slope} · 절토 ${design.cut_area_m2.toFixed(2)}㎡ · 성토 ${design.fill_area_m2.toFixed(2)}㎡`;
|
||||
}
|
||||
|
||||
/** 좌·우 설계 반폭(m) — 사면이 원지반과 만나는 자리까지. 못 만난 측은 기본 반폭(샘플 끝). */
|
||||
export function designHalfWidths(preview: CrossPreviewResponse): { leftM: number; rightM: number } {
|
||||
const offsets = preview.samples
|
||||
.map((sample) => Number(sample.offset_m))
|
||||
.filter((offset) => Number.isFinite(offset));
|
||||
const toes = preview.design ? toeOffsets(asSection(preview)) : { left: null, right: null };
|
||||
return {
|
||||
leftM: toes.left ?? Math.max(0, ...offsets), // +offset = 좌
|
||||
rightM: Math.abs(toes.right ?? Math.min(0, ...offsets)), // −offset = 우
|
||||
};
|
||||
}
|
||||
|
||||
/** B06 셈 함수가 보는 꼴로 맞춘다 — 그 둘은 `samples` 와 `design` 만 읽는다. */
|
||||
function asSection(preview: CrossPreviewResponse): CrossSection {
|
||||
return { samples: preview.samples, design: preview.design } as unknown as CrossSection;
|
||||
|
||||
@@ -29,14 +29,6 @@ export function pointInTri2(p: P2, a: P2, b: P2, c: P2): boolean {
|
||||
return !(hasNeg && hasPos);
|
||||
}
|
||||
|
||||
export function segmentsIntersect(a: P2, b: P2, c: P2, d: P2): boolean {
|
||||
const d1 = cross2(b.x - a.x, b.z - a.z, c.x - a.x, c.z - a.z);
|
||||
const d2 = cross2(b.x - a.x, b.z - a.z, d.x - a.x, d.z - a.z);
|
||||
const d3 = cross2(d.x - c.x, d.z - c.z, a.x - c.x, a.z - c.z);
|
||||
const d4 = cross2(d.x - c.x, d.z - c.z, b.x - c.x, b.z - c.z);
|
||||
return d1 * d2 < -EPS && d3 * d4 < -EPS;
|
||||
}
|
||||
|
||||
/** 경계 선분 하나 — AABB를 함께 들고 있어 격자 후보 판정이 싸다. */
|
||||
export interface RegionSegment {
|
||||
p: P2;
|
||||
|
||||
@@ -34,20 +34,6 @@ export function chainageToStation(
|
||||
return { station, remainder };
|
||||
}
|
||||
|
||||
/** "3+18.0" · "3 + 18" · "76.5"(누가거리 직접) 입력을 누가거리로 해석한다.
|
||||
* 해석 불가하면 null — 호출부가 칸을 붉히고 진행을 멈춘다. */
|
||||
export function parseStationText(text: string, intervalM: number): number | null {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return null;
|
||||
const match = /^(\d+)\s*\+\s*(\d+(?:\.\d+)?)$/.exec(trimmed);
|
||||
if (match) {
|
||||
const chainage = stationToChainage(Number(match[1]), Number(match[2]), intervalM);
|
||||
return Number.isFinite(chainage) ? chainage : null;
|
||||
}
|
||||
const direct = Number(trimmed);
|
||||
return Number.isFinite(direct) && direct >= 0 ? direct : null;
|
||||
}
|
||||
|
||||
/** 구조물 측점과 겹쳤다고 볼 거리(m) — 이보다 가까우면 규칙 측점을 지운다. */
|
||||
export const STATION_MERGE_TOLERANCE_M = 0.1;
|
||||
|
||||
|
||||
@@ -9,13 +9,12 @@ last_updated: 2026-07-24
|
||||
|
||||
# B05_Profile_Api_Fetch.ts
|
||||
|
||||
FastAPI 백엔드 B05 라우터 API 통신(solve, confirm, latest, profile-alignment, contour-interval)을 담당하는 클라이언트 HTTP 클라이언트 모듈입니다.
|
||||
FastAPI 백엔드 B05 라우터 API 통신(confirm, latest, profile-alignment, contour-interval)을 담당하는 클라이언트 HTTP 클라이언트 모듈입니다.
|
||||
|
||||
## 🛠️ 주요 API 함수 목록
|
||||
|
||||
| 항목 | 위치(상대 경로) | 역할 |
|
||||
|---|---|---|
|
||||
| `solveRoute()` | `B05_Profile/B05_Profile_Api_Fetch.ts` | `POST /api/projects/{id}/route/solve` 최적 탐색 요청 호출 |
|
||||
| `confirmRoute()` | `B05_Profile/B05_Profile_Api_Fetch.ts` | `POST /api/projects/{id}/route/confirm` 경로 확정 및 비정규 측점 생성 요청 호출 |
|
||||
| `fetchLatestRoute()` | `B05_Profile/B05_Profile_Api_Fetch.ts` | `GET /api/projects/{id}/route/latest` 최신 경로 및 제어점 데이터 복원 호출 |
|
||||
| `saveProfileAlignment()` | `B05_Profile/B05_Profile_Api_Fetch.ts` | `PUT /api/projects/{id}/route/profile-alignment` 수동 표고/반경 델타 영속화 호출 |
|
||||
|
||||
@@ -80,7 +80,6 @@ def test_화면에_제안값_단추_제안_글이_없음():
|
||||
files = [
|
||||
"B05_Profile/B05_Profile_UI_Drainage_Facility.ts",
|
||||
"B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts",
|
||||
"B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts",
|
||||
"B05_Profile/B05_Profile_UI_Structures_Fields.ts",
|
||||
"B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user