From f67bd52781cbed72153ccb2f68b3ca82449f51b4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 08:55:32 +0900 Subject: [PATCH] =?UTF-8?q?refactor(472-2=202-A=20B05=20=EA=B1=B7=EC=96=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0):=20=EC=95=88=20=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B8=20=EC=A7=80=EC=9B=80=20=E2=80=94=20Ford=20=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=EC=B9=B8=20=ED=8C=8C=EC=9D=BC=20=C2=B7=20=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8(replan/reset)=20=C2=B7=20TS=20solve?= =?UTF-8?q?Route=20=C2=B7=20=EB=B0=94=EA=B9=A5=20=EB=B6=80=EB=A6=84=200=20?= =?UTF-8?q?export=20=EC=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- B05_Profile/B05_Profile_Api_Fetch.ts | 64 -------- B05_Profile/B05_Profile_Api_Replan.ts | 10 -- B05_Profile/B05_Profile_Api_Structures.ts | 11 -- B05_Profile/B05_Profile_Router_Replan.py | 29 ---- .../B05_Profile_UI_Corridor_Station.ts | 34 ---- ...B05_Profile_UI_Drainage_Facility_Fields.ts | 75 --------- .../B05_Profile_UI_Drainage_Facility_Ford.ts | 145 ------------------ B05_Profile/B05_Profile_UI_Drainage_Parts.ts | 12 -- .../B05_Profile_UI_Profile_MinCover.ts | 8 - .../B05_Profile_UI_RouteEdit_Cross_Draw.ts | 12 -- .../logic/B05_Profile_UI_Corridor_Region.ts | 8 - B05_Profile/logic/B05_Profile_Util_Station.ts | 14 -- .../frontend/B05_Profile_Api_Fetch.md | 3 +- .../tester/test_b05_new_structure_defaults.py | 1 - 14 files changed, 1 insertion(+), 425 deletions(-) delete mode 100644 B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts diff --git a/B05_Profile/B05_Profile_Api_Fetch.ts b/B05_Profile/B05_Profile_Api_Fetch.ts index 653b11c7e..e87d9169d 100644 --- a/B05_Profile/B05_Profile_Api_Fetch.ts +++ b/B05_Profile/B05_Profile_Api_Fetch.ts @@ -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; - 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( } } -/** 경로 탐색을 실행한다 (비용면 생성 → Dijkstra/ridge-valley → GeoJSON 저장). */ -export async function solveRoute( - projectId: string, - request: RouteSolveRequest, -): Promise { - return requestJson(`/projects/${projectId}/route/solve`, { - method: "POST", - body: JSON.stringify(request), - }); -} - /** 등고선 간격 재적용 값을 서버(stage 1 params)에 영속화한다. */ export async function updateContourInterval( projectId: string, diff --git a/B05_Profile/B05_Profile_Api_Replan.ts b/B05_Profile/B05_Profile_Api_Replan.ts index 02530740f..2b1d11a84 100644 --- a/B05_Profile/B05_Profile_Api_Replan.ts +++ b/B05_Profile/B05_Profile_Api_Replan.ts @@ -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 { - return requestJson( - `/projects/${projectId}/route/replan/reset`, - { method: "POST" }, - REPLAN_TIMEOUT_MS, - ); -} diff --git a/B05_Profile/B05_Profile_Api_Structures.ts b/B05_Profile/B05_Profile_Api_Structures.ts index a7588a8cc..af5c22d92 100644 --- a/B05_Profile/B05_Profile_Api_Structures.ts +++ b/B05_Profile/B05_Profile_Api_Structures.ts @@ -367,17 +367,6 @@ export function structureAnchorM(structure: StructureInstance): number { return structure.chainage_m ?? structure.start_m ?? 0; } -/** 타입 정의의 기본값으로 옵션을 채운다(신규 추가·타입 변경 시). */ -export function defaultOptions(type: StructureType): Record { - const options: Record = {}; - 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으로 넘어가 확정하는 경로가 있어 diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 75a018f5f..6111af307 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -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 diff --git a/B05_Profile/B05_Profile_UI_Corridor_Station.ts b/B05_Profile/B05_Profile_UI_Corridor_Station.ts index 400218fa7..611f952b2 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Station.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Station.ts @@ -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). */ diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index a8d14e9e3..76f3b12a8 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -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) => void; - read: (target: Record) => 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; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts deleted file mode 100644 index c1a5a8ae2..000000000 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts +++ /dev/null @@ -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) => void; - read: (kind: FordKind, target: Record) => 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; - }, - }; -} diff --git a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts index e39ee5ee1..acd8afb0b 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts @@ -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( diff --git a/B05_Profile/B05_Profile_UI_Profile_MinCover.ts b/B05_Profile/B05_Profile_UI_Profile_MinCover.ts index f69067f2b..5f8ac8b8d 100644 --- a/B05_Profile/B05_Profile_UI_Profile_MinCover.ts +++ b/B05_Profile/B05_Profile_UI_Profile_MinCover.ts @@ -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; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw.ts index 37c0a30e2..a0c2a7fc1 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw.ts @@ -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; diff --git a/B05_Profile/logic/B05_Profile_UI_Corridor_Region.ts b/B05_Profile/logic/B05_Profile_UI_Corridor_Region.ts index ddf8658ce..7587b3909 100644 --- a/B05_Profile/logic/B05_Profile_UI_Corridor_Region.ts +++ b/B05_Profile/logic/B05_Profile_UI_Corridor_Region.ts @@ -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; diff --git a/B05_Profile/logic/B05_Profile_Util_Station.ts b/B05_Profile/logic/B05_Profile_Util_Station.ts index 60a862d21..26dc58588 100644 --- a/B05_Profile/logic/B05_Profile_Util_Station.ts +++ b/B05_Profile/logic/B05_Profile_Util_Station.ts @@ -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; diff --git a/docs/wiki/pages/B05_Profile/frontend/B05_Profile_Api_Fetch.md b/docs/wiki/pages/B05_Profile/frontend/B05_Profile_Api_Fetch.md index 3e094249e..385b6d472 100644 --- a/docs/wiki/pages/B05_Profile/frontend/B05_Profile_Api_Fetch.md +++ b/docs/wiki/pages/B05_Profile/frontend/B05_Profile_Api_Fetch.md @@ -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` 수동 표고/반경 델타 영속화 호출 | diff --git a/resources/tester/test_b05_new_structure_defaults.py b/resources/tester/test_b05_new_structure_defaults.py index d6a60d202..b6177eddf 100644 --- a/resources/tester/test_b05_new_structure_defaults.py +++ b/resources/tester/test_b05_new_structure_defaults.py @@ -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", ]