diff --git a/A00_Common/design/design_page_data.ts b/A00_Common/design/design_page_data.ts index f674b95de..1426b964d 100644 --- a/A00_Common/design/design_page_data.ts +++ b/A00_Common/design/design_page_data.ts @@ -55,7 +55,7 @@ export function forgetPageData(projectId: string, leaving = false): void { /** 횡단 context(세션 캐시 · 새 요청 없음) — 노선 id 칸을 채움 · 두 보기가 이것으로만 받음. 서버 창구는 부를 때 * 불러옴(묶음 · 초안 측점이 끄는 이 파일이 화면 모듈을 안 끌게). */ export async function pageSectionContext(projectId: string): Promise { - const { fetchSectionContext } = await import("../../B06_Section/B06_Section_Api_Fetch"); + const { fetchSectionContext } = await import("../../B06_Section/B06_Section_Api_Context"); const context = await fetchSectionContext(projectId); setPageRouteId(projectId, context.route_id); // 노선 id 한 칸(design_page_route) return context; diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index d6ec1a770..57581dac0 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -19,7 +19,7 @@ import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../b_workflow_nav"; import { readState, writeState } from "../b_page_state"; import { currentRoute, navigateTo, remountRoute } from "../router"; import { clearRouteLatestCache } from "../../B05_Profile/B05_Profile_Api_Fetch"; -import { fetchCurrentRouteId } from "../../B06_Section/B06_Section_Api_Fetch"; +import { fetchCurrentRouteId } from "../../B06_Section/B06_Section_Api_Context"; import { createStructuresSection, type StructuresSection } from "../b_structures_section"; import type { StructuresCallbacks } from "../../B05_Profile/B05_Profile_UI_Structures_Panel_Types"; import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types"; 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_Engine_Grade_Alignment.py b/B05_Profile/B05_Profile_Engine_Grade_Alignment.py index 5bd1d5a71..3744807b0 100644 --- a/B05_Profile/B05_Profile_Engine_Grade_Alignment.py +++ b/B05_Profile/B05_Profile_Engine_Grade_Alignment.py @@ -16,6 +16,8 @@ scipy에 의존하지 않는 순수 기하 계산만 두어 프론트엔드 구 직선 판정 짝: `is_straight_bend` · `STRAIGHT_HEIGHT_M` ↔ TS `logic/B05_Profile_Profile_Straight.ts` (거울 시험 `test_426_profile_straight.py`). +경고 글 짝: `build_alignment` warnings ↔ TS `buildAlignment` warnings — 같은 목록 +(거울 시험 `test_472_save_warnings.py`). """ from dataclasses import dataclass @@ -369,7 +371,7 @@ def build_curves( continue if length / 2.0 - half > 1e-6: warnings.append( - f"chainage {chainage:.1f}m: 인접 직선이 짧아 종단곡선 길이를 " + f"{chainage:.1f}m: 인접 직선이 짧아 종단곡선 길이를 " f"{half * 2.0:.1f}m(반경 {half * 2.0 / abs(delta):.0f}m)로 줄였습니다." ) length = half * 2.0 @@ -592,18 +594,9 @@ def build_alignment( for segment in segments if abs(segment["grade_percent"]) > policy.max_grade_pct + 1e-6 ] - if violations: - worst = max(abs(item["value"]) for item in violations) - warnings.append( - f"종단기울기 {worst:.2f}%가 기준 {policy.max_grade_pct:.2f}%를 초과하는 구간이 " - f"{len(violations)}개 있습니다." - ) + # 경고 글 = TS `buildAlignment` 와 한 벌(472 덧 · 거울 `test_472_save_warnings`) — 기울기 초과 · + # 불균형은 글로 안 남김(`violations` · `balance` 칸이 정본 · 화면 칩이 그 칸으로 셈) within_tolerance = imbalance <= policy.balance_tolerance_percent + 1e-9 - if not within_tolerance: - warnings.append( - f"절·성토 불균형 {imbalance:.1f}%가 허용치 " - f"{policy.balance_tolerance_percent:.1f}%를 초과합니다." - ) return { "schema_version": ALIGNMENT_SCHEMA_VERSION, diff --git a/B05_Profile/B05_Profile_Engine_Sections.py b/B05_Profile/B05_Profile_Engine_Sections.py index 2a6509a62..c4c5f6871 100644 --- a/B05_Profile/B05_Profile_Engine_Sections.py +++ b/B05_Profile/B05_Profile_Engine_Sections.py @@ -163,6 +163,18 @@ def valid_cross_names(cross_dir: Path, stations: list[Any]) -> set[str]: } +def cross_files_in_order(cross_dir: Path, stations: list[Any]) -> list[Path]: + """잔재를 걸러 읽을 cross_*.json — **측점 차례**(이름 차례면 `01080.16` 이 `01080` 앞).""" + valid = valid_cross_names(cross_dir, stations) + + def chainage(path: Path) -> tuple[float, str]: + match = re.fullmatch(rf"cross_{CROSS_KEY_PATTERN}m", path.stem) + return (float(match.group(1)) if match else float("inf"), path.name) + + files = [path for path in cross_dir.glob("cross_*.json") if not valid or path.name in valid] + return sorted(files, key=chainage) + + def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]: """stations에 없는 잔재 cross_*.json을 삭제하고 유효 파일명 집합을 반환한다. 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_IrregularStations.ts b/B05_Profile/B05_Profile_UI_IrregularStations.ts index 07e7b3e12..a425b540d 100644 --- a/B05_Profile/B05_Profile_UI_IrregularStations.ts +++ b/B05_Profile/B05_Profile_UI_IrregularStations.ts @@ -11,6 +11,7 @@ * ========================================================================== */ import { stationPartsLabel } from "../common_util/common_util_station"; +import { PIPE_DISPLAY_NAME } from "../common_util/common_util_structure_catalog"; import { ESCAPE_ROUTE_DEFAULT_WIDTH_M, ESCAPE_ROUTE_WIDTHS_M, @@ -54,8 +55,6 @@ export interface IrregularStation { /** 관 매설 지점이 구조물 목록에 실체화될 때 쓰는 종류 값(구 데이터 판정 키라 유지 — * 화면 표시 이름은 "배수관"이다, 2026-08-17 사용자 지시). */ export const PIPE_STRUCTURE_NAME = "배관"; -/** 화면에 보이는 이름. */ -export const PIPE_DISPLAY_NAME = "배수관"; /** 배수관으로 볼 항목인가. 드롭다운 종류가 "배관"이거나 손으로 이름을 "배관"·"배수관"이라 * 적은 것도 관 지점 정본을 따르게 한다 — 아니면 [초기화]로 지워지지 않고 배수유역도와 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/B06_Section/B06_Section_Api_Context.ts b/B06_Section/B06_Section_Api_Context.ts new file mode 100644 index 000000000..03b74160a --- /dev/null +++ b/B06_Section/B06_Section_Api_Context.ts @@ -0,0 +1,58 @@ +/* ============================================================================= + * B06_Section_Api_Context.ts + * 종횡단 설정값(`GET …/sections/context`)의 **세션 캐시 창구** — 화면 쪽. + * + * `B06_Section_Api_Fetch.ts` 에서 갈라냄(472-2) — 서버 Node 묶음이 Api_Fetch 를 끌어도 + * 세션(`b_page_state`) · 되돌리기 사진첩(`Profile_History`)을 함께 끌지 않게. + * ========================================================================== */ + +import { clearState, readState, stateKey, writeState } from "../A00_Common/b_page_state"; +import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History"; +import { requestSectionContext } from "./B06_Section_Api_Fetch"; +import type { SectionContextResponse } from "./B06_Section_Api_Types"; + +/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */ +/** 종횡단 설정값 — ④ 계산 결과라 세션에 담아 두고 화면을 오갈 때마다 다시 묻지 않는다 + * (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */ +export async function fetchSectionContext(projectId: string): Promise { + const cached = readState("section-context", projectId); + if (cached) return seedStandardCross(projectId, cached); + const fresh = await requestSectionContext(projectId); + writeState("section-context", fresh, projectId); + return seedStandardCross(projectId, fresh); +} + +/** 서버가 지금 보는 노선 id — 세션 캐시를 건너뛰고 묻는다. [저장] · [확정] 앞에서 화면이 쥔 + * 노선과 대조한다(PLAN 76-1). 받은 설정값으로 캐시도 새로 둔다. */ +export async function fetchCurrentRouteId(projectId: string): Promise { + const fresh = await requestSectionContext(projectId); + writeState("section-context", fresh, projectId); + return fresh.route_id; +} + +/** + * 저장된 표준 횡단면을 **세션이 비어 있을 때만** 채운다(2026-09-07). + * + * 브라우저 횡단 계산은 `세션 ?? config 기본값` 으로 서는데, 표준단면 세션값은 그 탭에서만 + * 산다. 그래서 **탭을 새로 열면** 화면은 config 기본값으로, 서버는 저장분으로 계산해 + * 같은 측점이 갈렸다(실측 — 용화 route 169 저장분은 암반 횡단경사 **5%**·측구 상단폭 + * **0.9m**, config 기본값은 **3%**·**0.69m**). + * + * 여기가 두 화면(B05·B06)이 함께 지나는 유일한 자리라 이 한 곳에서 채운다. 사용자가 + * 그 탭에서 고친 값이 있으면 **건드리지 않는다** — 초안이 언제나 우선이다. + */ +function seedStandardCross( + projectId: string, + context: SectionContextResponse, +): SectionContextResponse { + const stored = context.stored_standard_cross_section; + if (stored && readState("std-cross", projectId) === null) { + writeState("std-cross", stored, projectId); + seedPageHistory(stateKey("std-cross", projectId)); // 씨앗 — 되돌리기 걸음 아님(364-1) + } + return context; +} + +export function clearSectionContextCache(projectId: string): void { + clearState("section-context", projectId); +} diff --git a/B06_Section/B06_Section_Api_Fetch.ts b/B06_Section/B06_Section_Api_Fetch.ts index 89618f11e..d5e78e9b1 100644 --- a/B06_Section/B06_Section_Api_Fetch.ts +++ b/B06_Section/B06_Section_Api_Fetch.ts @@ -16,15 +16,11 @@ * 기존 호출 코드가 깨지지 않도록 여기서 그대로 다시 내보낸다. * ========================================================================== */ -import { clearState, readState, stateKey, writeState } from "../A00_Common/b_page_state"; -import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History"; 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, - CrossDesignRequest, - CrossDesignResponse, CrossSection, CrossSectionPatch, SectionConfirmResponse, @@ -62,56 +58,11 @@ async function requestJson(path: string, init: RequestInit): Promise { } } -/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */ -/** 종횡단 설정값 — ④ 계산 결과라 세션에 담아 두고 화면을 오갈 때마다 다시 묻지 않는다 - * (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */ -export async function fetchSectionContext(projectId: string): Promise { - const cached = readState("section-context", projectId); - if (cached) return seedStandardCross(projectId, cached); - const fresh = await requestJson( - `/projects/${projectId}/sections/context`, - { method: "GET" }, - ); - writeState("section-context", fresh, projectId); - return seedStandardCross(projectId, fresh); -} - -/** 서버가 지금 보는 노선 id — 세션 캐시를 건너뛰고 묻는다. [저장] · [확정] 앞에서 화면이 쥔 - * 노선과 대조한다(PLAN 76-1). 받은 설정값으로 캐시도 새로 둔다. */ -export async function fetchCurrentRouteId(projectId: string): Promise { - const fresh = await requestJson( - `/projects/${projectId}/sections/context`, - { method: "GET" }, - ); - writeState("section-context", fresh, projectId); - return fresh.route_id; -} - -/** - * 저장된 표준 횡단면을 **세션이 비어 있을 때만** 채운다(2026-09-07). - * - * 브라우저 횡단 계산은 `세션 ?? config 기본값` 으로 서는데, 표준단면 세션값은 그 탭에서만 - * 산다. 그래서 **탭을 새로 열면** 화면은 config 기본값으로, 서버는 저장분으로 계산해 - * 같은 측점이 갈렸다(실측 — 용화 route 169 저장분은 암반 횡단경사 **5%**·측구 상단폭 - * **0.9m**, config 기본값은 **3%**·**0.69m**). - * - * 여기가 두 화면(B05·B06)이 함께 지나는 유일한 자리라 이 한 곳에서 채운다. 사용자가 - * 그 탭에서 고친 값이 있으면 **건드리지 않는다** — 초안이 언제나 우선이다. - */ -function seedStandardCross( - projectId: string, - context: SectionContextResponse, -): SectionContextResponse { - const stored = context.stored_standard_cross_section; - if (stored && readState("std-cross", projectId) === null) { - writeState("std-cross", stored, projectId); - seedPageHistory(stateKey("std-cross", projectId)); // 씨앗 — 되돌리기 걸음 아님(364-1) - } - return context; -} - -export function clearSectionContextCache(projectId: string): void { - clearState("section-context", projectId); +/** 종횡단 설정값 한 번 묻기(캐시 없음) — 세션 캐시 창구는 `B06_Section_Api_Context.ts`(472-2). */ +export function requestSectionContext(projectId: string): Promise { + return requestJson(`/projects/${projectId}/sections/context`, { + method: "GET", + }); } /** 경로의 종단면 요약을 조회한다. */ @@ -161,18 +112,6 @@ export async function regenerateSections( ); } -/** 측점 표준횡단 설계(지반유형·단면유형)를 즉시 계산·저장하고 잠정 결과를 반환한다. */ -export async function computeCrossDesign( - projectId: string, - routeId: number, - request: CrossDesignRequest, -): Promise { - return requestJson( - `/projects/${projectId}/sections/${routeId}/cross-design`, - { method: "POST", body: JSON.stringify(request) }, - ); -} - /** 경로의 종·횡단면을 확정한다. 표준 횡단면 설정값·측점별 세션 보관값을 함께 저장할 수 있다. */ export async function confirmSections( projectId: string, diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 092918d85..dde05d7ba 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -550,35 +550,6 @@ export interface CrossDesign { pipe_length_m?: number; } -export interface CrossDesignResponse { - status: string; - chainage_m: number; - design: CrossDesign; -} - -export interface CrossDesignRequest { - chainage_m: number; - ground_type: GroundType; - section_mode: SectionMode; - ditch_side?: DitchSide | null; - /** 측구 형식(일반/L형). L형은 암 지반에서만 허용된다. */ - ditch_type?: DitchType; - /** 포장 중첩 여부 — 횡단경사·포장층만 포장 그룹 값으로 계산된다. */ - paved?: boolean; - /** 암 경계선 오프셋(m, 지면선 기준 하향 음수). 암 지반 2단계 절토 무릎 계산용. */ - rock_boundary_offset_m?: number | null; - /** 암 지반 2단계 경사 적용 여부(기본 true, 토글로 해제). */ - two_stage_slope?: boolean; - /** 이 측점만 쓰는 암 절토 경사비(1:n 의 n). 없으면 표준값(2026-09-07). */ - cut_slope_ratio?: number | null; - /** 옛 이름 — 결과값이 그대로 실려 오던 자리(호환). 새 요청은 `ditch_choice` 를 쓴다. */ - ditch_enabled?: boolean | null; - /** 측구를 둘지 **사용자가 정한 선택**. `null`/미지정 = 자동 판정(2026-09-09 정리). */ - ditch_choice?: boolean | null; - /** 설정 패널 편집값. 요청값 → config 기본값 순으로 우선한다. */ - standard_cross_section?: StandardCrossSection; -} - /** 확정 시 측점별 data.design에 병합할 프론트 세션 보관값. */ export interface CrossSectionPatch { chainage_m: number; diff --git a/B06_Section/B06_Section_Cross_Design_Session.ts b/B06_Section/B06_Section_Cross_Design_Session.ts index 451456bf7..8ba435be4 100644 --- a/B06_Section/B06_Section_Cross_Design_Session.ts +++ b/B06_Section/B06_Section_Cross_Design_Session.ts @@ -31,15 +31,6 @@ export interface CrossDesignChoice { const readAll = (projectId: string, routeId: number) => readKeyed("crossdesign", projectId, routeId); -/** 이 측점의 선택값(없으면 null). */ -export function readCrossDesignChoice( - projectId: string, - routeId: number, - chainageM: number, -): CrossDesignChoice | null { - return readAll(projectId, routeId).get(chainageKey(chainageM)) ?? null; -} - /** 선택값을 세션에 담는다. 같은 측점의 앞선 값은 덮어쓴다. */ export function writeCrossDesignChoice( projectId: string, diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 4ee4c3fcf..03dd566c3 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -16,18 +16,12 @@ from fastapi.responses import JSONResponse, Response from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile from B05_Profile.B05_Profile_Engine_Sections import ( + cross_files_in_order, relabel_pipe_stations, run_section_generation, - valid_cross_names, ) from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes -from B06_Section.B06_Section_Engine_Design import ( - compute_cross_design, - curve_widening_args, - stored_shoulder_extend, -) -from B06_Section.B06_Section_Engine_Tilt import tilt_args, tilt_ranges from B06_Section.B06_Section_Repository import ( count_cross_sections, create_longitudinal_section, @@ -40,26 +34,13 @@ from B06_Section.B06_Section_Repository import ( get_workflow_route_context, insert_cross_sections, list_recent_company_projects, - update_cross_section_design, ) from B06_Section.B06_Section_Router_Design import ( PREVIEW_DESIGN_FIELDS as _PREVIEW_DESIGN_FIELDS, ) -from B06_Section.B06_Section_Router_Design import ( - USER_TOUCHED_KEYS, - ford_drop_at, - ford_surface_drops, - paved_at, - pavement_ranges, - stored_berm, - stored_cut_slope, -) from B06_Section.B06_Section_Router_Design import ( attach_default_designs as _attach_default_designs, ) -from B06_Section.B06_Section_Router_Design import ( - read_cross_design_inputs as _read_cross_design_inputs, -) from B06_Section.B06_Section_Router_Design import ( recompute_designs_for_alignment as _recompute_designs_for_alignment, ) @@ -76,8 +57,6 @@ from B06_Section.B06_Section_Schema import ( CompanyStandardResponse, CrossDesignPreviewRequest, CrossDesignPreviewResponse, - CrossDesignRequest, - CrossDesignResponse, HaulEquipmentLimit, SectionContextResponse, SectionDetailResponse, @@ -269,12 +248,10 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8")) stations = longitudinal.get("stations") if isinstance(longitudinal, dict) else None - # 조회는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫) - valid_names = valid_cross_names(cross_dir, stations if isinstance(stations, list) else []) + # 조회는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫) · 측점 차례(472-2) cross_sections = [ json.loads(path.read_text(encoding="utf-8")) - for path in sorted(cross_dir.glob("cross_*.json")) - if not valid_names or path.name in valid_names + for path in cross_files_in_order(cross_dir, stations if isinstance(stations, list) else []) ] if not isinstance(longitudinal, dict) or not all( isinstance(section, dict) for section in cross_sections @@ -597,102 +574,3 @@ async def preview_cross_designs( status_code=500, content={"status": "error", "message": "횡단 설계 프리뷰 계산 중 오류가 발생했습니다."}, ) - - -@router.post("/{project_id}/sections/{route_id}/cross-design", response_model=CrossDesignResponse) -async def compute_cross_section_design( - project_id: UUID, route_id: int, request: CrossDesignRequest -) -> CrossDesignResponse | JSONResponse: - """측점 표준횡단 설계(지반유형·단면유형)를 즉시 계산해 잠정치로 저장한다.""" - pool = get_db_pool() - try: - async with pool.acquire() as connection: - longitudinal = await get_longitudinal_section(connection, project_id, route_id) - if not longitudinal: - return JSONResponse( - status_code=404, - content={"status": "error", "message": "종횡단 상세 결과가 없습니다."}, - ) - stored_path = await get_project_storage_relative_path(connection, project_id) - # 저장분을 **계산 전에** 읽는다 — 소단은 사용자 조작값이면서 **기하 입력**이라, - # 나중에 키만 베껴 붙이면 설계선·면적은 계단 없이 나오고 `berm` 값만 남아 - # 서로 어긋난다(2026-09-07). 값을 나르는 다른 사용자 키와 다른 점이다. - stored_designs = await get_cross_section_designs(connection, route_id) - stored_design: dict[str, Any] | None = None - for record in stored_designs: - if abs(float(record["chainage_m"]) - request.chainage_m) < 0.01: - candidate = record.get("design") - stored_design = candidate if isinstance(candidate, dict) else None - break - project_root = Path(resolve_stored_project_path(stored_path)) - samples, design_elevation, pavement_suggested, cross_record = await asyncio.to_thread( - _read_cross_design_inputs, - project_root, - str(longitudinal["longitudinal_file_path"]), - request.chainage_m, - ) - design = compute_cross_design( - samples, - design_elevation, - ground_type=request.ground_type, - section_mode=request.section_mode, - ditch_side=request.ditch_side, - ditch_type=request.ditch_type, - paved=paved_at(request.chainage_m, pavement_ranges(project_root)), - standard=request.standard_cross_section, - rock_boundary_offset_m=request.rock_boundary_offset_m, - two_stage_slope=request.two_stage_slope, - # 측점별 암 절토 경사 — 요청값이 없으면 저장분에서 잇는다(2026-09-07). - cut_slope_ratio=request.cut_slope_ratio or stored_cut_slope(stored_design or {}), - ditch_enabled=request.ditch_enabled, - ditch_choice=request.ditch_choice, - surface_drop_m=ford_drop_at(request.chainage_m, ford_surface_drops(project_root)), - berm=stored_berm(stored_design or {}), - **curve_widening_args(cross_record), - **stored_shoulder_extend(stored_design or {}), # 노견 확장(139-6) - **tilt_args(request.chainage_m, tilt_ranges(project_root)), - ) - # B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다. - design["status"] = "provisional" - # 법정 근거 문구 표기용 — 사용자가 포장을 바꿔도 제안 여부는 그대로 남긴다. - design["pavement_suggested"] = pavement_suggested - # 표시 설정(측점 개별 반폭)은 계산 입력이 아니다 — 저장분에서 이월해 재계산이 - # 지우지 않게 한다(2026-08-06). 목록을 여기 다시 적지 않는다 — 서버의 한 벌은 - # `B06_Section_Router_Design.USER_TOUCHED_KEYS` 다(2026-09-07). 예전에는 여기에 - # 따로 적어 두어 `extra_spans` 가 빠져 있었다. - if stored_design is not None: - for key in USER_TOUCHED_KEYS: - if stored_design.get(key) is not None: - design[key] = stored_design[key] - async with pool.acquire() as connection: - await connection.begin() - try: - updated = await update_cross_section_design( - connection, - route_id=route_id, - chainage_m=request.chainage_m, - design=design, - project_id=project_id, - ) - await connection.commit() - except Exception: - await connection.rollback() - raise - if not updated: - return JSONResponse( - status_code=404, - content={"status": "error", "message": "해당 측점 횡단 레코드가 없습니다."}, - ) - return CrossDesignResponse(chainage_m=request.chainage_m, design=design) - except FileNotFoundError as exc: - return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)}) - except ValueError as exc: - return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) - except Exception: - logger.exception( - "B06 측점 횡단 설계 계산 실패: project_id=%s route_id=%s", project_id, route_id - ) - return JSONResponse( - status_code=500, - content={"status": "error", "message": "횡단 설계 계산 중 오류가 발생했습니다."}, - ) diff --git a/B06_Section/B06_Section_Schema.py b/B06_Section/B06_Section_Schema.py index 20d81a71b..6587430e6 100644 --- a/B06_Section/B06_Section_Schema.py +++ b/B06_Section/B06_Section_Schema.py @@ -63,14 +63,6 @@ class CrossDesignRequest(BaseModel): standard_cross_section: dict[str, Any] | None = None -class CrossDesignResponse(BaseModel): - """측점 표준횡단 설계 계산 결과(잠정치).""" - - status: str = "success" - chainage_m: float - design: dict[str, Any] - - class BasinAdjustPatch(BaseModel): """집수정 내부 치수와 기본 위치 대비 이동량.""" diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index fae86cfec..0b8d11742 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -229,6 +229,42 @@ def _enforce_stored_designs( enforce_ford_surface_drops(longitudinal, sections, project_root, standard) +def refresh_alignment(longitudinal: dict[str, Any]) -> dict[str, Any] | None: + """종단 경고를 저장 입력(base_pvi · 편집 · 정책 · 기준)으로 지금 코드가 다시 셈 — 옛 코드가 + 쓴 경고가 남지 않게(472 덧 · 요구 272). 기하(계획고 · 곡선 · 표본 · 위반)는 저장값 그대로 — + 저장값이 6 자리 반올림이라 통째로 다시 쓰면 끝자리가 흔들림(계획고 1e-6 m → 면적 1e-4 ㎡). + 반환 = 요약(`grade_summary`) · 선형이 없는 옛 종단이면 None(그대로).""" + from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile + + stored = longitudinal.get("profile_alignment") + if not isinstance(stored, dict) or not stored.get("base_pvi"): + return None + fresh, profile = rebuild_alignment_profile(longitudinal, stored.get("edits")) + stored["warnings"] = fresh["warnings"] + profiles = longitudinal.get("design_profiles") + if isinstance(profiles, list) and profiles and isinstance(profiles[0], dict): + profile = profiles[0] + profile.setdefault("summary", {})["warnings"] = fresh["warnings"] + else: + longitudinal["design_profiles"] = [profile] + return {"id": profile.get("id"), **profile["summary"]} + + +def _refresh_alignment_file(project_root: Path, relative: str) -> dict[str, Any] | None: + """종단 정본 파일의 선형을 다시 셈 · 바뀐 때만 씀. 파일 · 선형이 없으면 None.""" + from common_util.common_util_json import atomic_write_json + + path = project_root / relative + if not path.is_file(): + return None + longitudinal = json.loads(path.read_text(encoding="utf-8")) + before = json.dumps(longitudinal, sort_keys=True, default=str) + summary = refresh_alignment(longitudinal) + if summary is not None and json.dumps(longitudinal, sort_keys=True, default=str) != before: + atomic_write_json(path, longitudinal) + return summary + + async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: """포장 구간·세월교 노면 하강 보정에 더해 구조물 면적·유토곡선까지 Node 로 만들어 저장한다. @@ -240,19 +276,40 @@ async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: async def _recompute(project_id: UUID | str, route_id: int) -> int: + from B05_Profile.B05_Profile_Repository import update_longitudinal_grade_summary from B06_Section.B06_Section_Router import get_section_detail project_uuid = UUID(str(project_id)) marks = [("시작", time.perf_counter())] - # 상세 만들기(파일 읽기 위주)와 DB 두 건은 서로 기다릴 이유가 없다 — 같이 보낸다. - # 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측). + # 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측) — 같이 보냄. pool = get_db_pool() - # 구조물 몫(채집석 공제·구조물 잔토)도 함께 받아 온다 — **B08 이 낸 값**이고, 안 넘기면 - # 통로만 있고 값이 안 흐른다(2026-09-09 실측: 공제가 늘 `None` 이라 사토가 안 줄었다). - response, stored_path, longitudinal_row, haul_inputs, surface = await asyncio.gather( - get_section_detail(project_uuid, route_id), + stored_path, longitudinal_row = await asyncio.gather( run_with_connection(get_project_storage_relative_path, project_uuid), run_with_connection(get_longitudinal_section, project_uuid, route_id), + ) + project_root = Path(resolve_stored_project_path(stored_path)) + # 종단 경고 먼저 — 저장 입력으로 다시 셈(472 덧) · 아래 상세가 새 경고를 읽음 + summary = await asyncio.to_thread( + _refresh_alignment_file, + project_root, + str((longitudinal_row or {}).get("longitudinal_file_path") or ""), + ) + if summary is not None: + async with pool.acquire() as connection: + await connection.begin() + try: + await update_longitudinal_grade_summary( + connection, route_id=route_id, grade_summary=summary + ) + await connection.commit() + except Exception: + await connection.rollback() + raise + marks.append(("종단 선형 다시 셈", time.perf_counter())) + # 구조물 몫(채집석 공제·구조물 잔토)도 함께 받아 온다 — **B08 이 낸 값**이고, 안 넘기면 + # 통로만 있고 값이 안 흐른다(2026-09-09 실측: 공제가 늘 `None` 이라 사토가 안 줄었다). + response, haul_inputs, surface = await asyncio.gather( + get_section_detail(project_uuid, route_id), haul_inputs_for(project_uuid), run_with_connection(get_surface_confirmation_params, str(project_uuid)), ) @@ -262,7 +319,6 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: raise RuntimeError(f"서버 재계산: 종횡단 상세를 못 받음 (route_id={route_id})") detail = payload(mode="json") sections = detail.get("cross_sections") or [] - project_root = Path(resolve_stored_project_path(stored_path)) from B06_Section.B06_Section_Router_Design import stored_standard_cross_section standard = stored_standard_cross_section(longitudinal_row) diff --git a/B06_Section/B06_Section_Station_Keys.ts b/B06_Section/B06_Section_Station_Keys.ts index 46f82a157..5ab6fa521 100644 --- a/B06_Section/B06_Section_Station_Keys.ts +++ b/B06_Section/B06_Section_Station_Keys.ts @@ -232,9 +232,6 @@ export function registeredKeys( return sections.map((s, i) => (list && nearest(list, s.chainage_m)?.key) ?? computed[i].key); } -/** 지금 노선(마지막으로 세운 목록) — 세션 열쇠만 아는 저장소(암 경계 · 암 경사)가 씀. */ -export const currentStationScope = () => current; - function nearest(list: Keyed[], chainageM: number): Keyed | undefined { let best: Keyed | undefined; for (const entry of list) { diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts index 8d21faa62..0b9bc258c 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts @@ -22,11 +22,6 @@ import { import { EXTRA_SPAN_DEFAULT } from "./logic/B06_Section_UI_Cross_Culvert_Const"; import type { StructureSpan } from "./logic/B06_Section_UI_Cross_Culvert_Const"; import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types"; -import { - designInterpolator, - groundInterpolator, - slopeToeOffset, -} from "./logic/B06_Section_UI_Cross_Culvert_Solve"; /** * 기슭막이 4축 조작 제어(2026-08-22 확정) — 벽을 눌러 고르고 조정창에서 * 좌우(x)·상하(d, 성토선 대각)·높이(h)·재질(m)을 만진다. 고른 벽은 여기 담아 두어 @@ -648,26 +643,3 @@ function adjustsInput( ), }; } - -/** - * **구조물 없는 상태**의 설계 절·성토선이 원지반과 만나는 지점이 들어오는 데 - * 필요한 표시 반폭(m, 여유 0.5 포함) — 2026-08-22 사용자 확정 줌 기준. - * 구조물(기슭막이·집수정)이 어디로 가든 화면 기준은 지형·설계선이 정한다. - */ -export function culvertRequiredHalfWidth(section: CrossSection): number | null { - const design = section.design; - if (!design) return null; - const groundAt = groundInterpolator(section.samples); - const designAt = designInterpolator(design.design_line); - const edges = design.road_edges; - if (!groundAt || !designAt || !edges) return null; - const offsets = section.samples.map((sample) => sample.offset_m ?? 0); - const limits = { left: Math.max(...offsets), right: Math.min(...offsets) }; - let extent = 0; - for (const side of ["left", "right"] as const) { - const edge = side === "left" ? edges.left : edges.right; - const toe = slopeToeOffset(designAt, groundAt, edge.offset_m, limits[side]); - extent = Math.max(extent, Math.abs(toe)); - } - return extent > 0 ? extent + 0.5 : null; -} diff --git a/B06_Section/B06_Section_UI_Section_Common.ts b/B06_Section/B06_Section_UI_Section_Common.ts index 529736949..d85a2ee28 100644 --- a/B06_Section/B06_Section_UI_Section_Common.ts +++ b/B06_Section/B06_Section_UI_Section_Common.ts @@ -8,15 +8,9 @@ * ========================================================================== */ import { PIPE_TYPES } from "@config/config_frontend"; -import { PIPE_DISPLAY_NAME } from "../B05_Profile/B05_Profile_UI_IrregularStations"; -import { pipeTitleKinds } from "../common_util/common_util_structure_catalog"; +import { PIPE_DISPLAY_NAME, pipeTitleKinds } from "../common_util/common_util_structure_catalog"; import type { CrossDesignChange } from "./B06_Section_UI_Cross_Design"; -import type { - DesignProfile, - LongitudinalSection, - SectionDetailResponse, - SectionSample, -} from "./B06_Section_Api_Fetch"; +import type { DesignProfile, LongitudinalSection, SectionSample } from "./B06_Section_Api_Fetch"; export type { CrossDesignChange }; export type DesignChangeHandler = (chainageM: number, change: CrossDesignChange) => void; @@ -274,34 +268,6 @@ export function longitudinalMaxChainage(data: LongitudinalSection): number { return Math.max(data.length_m, samples[samples.length - 1]?.chainage_m ?? 1, 1); } -/** - * 종·횡단 공통 Y스케일. `longHeightPx`는 종단면도의 실제 렌더 높이로, 패널 리사이즈로 - * 종단도가 줄면 그 높이를 넘겨야 표고 범위가 잘리지 않는다(기본은 고정 높이). - */ -export function calculateYScale( - detail: SectionDetailResponse, - longHeightPx: number = LONG_HEIGHT, -): YScaleOptions | undefined { - const elevations = [ - ...detail.longitudinal.samples.map((sample) => sample.elevation_m), - ...detail.cross_sections.flatMap((section) => - section.samples.map((sample) => sample.elevation_m), - ), - ...(detail.longitudinal.design_profiles ?? []).flatMap((profile) => - profile.samples.map((sample) => sample.elevation_m), - ), - ].filter((value): value is number => typeof value === "number" && Number.isFinite(value)); - if (!elevations.length) return undefined; - const globalMinElevation = Math.min(...elevations); - const globalMaxElevation = Math.max(...elevations); - const plotHeight = Math.max(longHeightPx - LONG_PAD.top - LONG_PAD.bottom, 1); - return { - pixelsPerMeter: plotHeight / Math.max(globalMaxElevation - globalMinElevation, 1), - globalMinElevation, - globalMaxElevation, - }; -} - /** * **보이는 구간의 표고 최저·최고**(종단 그래프 세로 자동 맞춤, 2026-09-04 사용자 확정). * diff --git a/B06_Section/B06_Section_UI_Section_View_Menu.ts b/B06_Section/B06_Section_UI_Section_View_Menu.ts index 08235c5ad..8faef4aba 100644 --- a/B06_Section/B06_Section_UI_Section_View_Menu.ts +++ b/B06_Section/B06_Section_UI_Section_View_Menu.ts @@ -1,21 +1,14 @@ /* ============================================================================= * B06_Section_UI_Section_View_Menu.ts - * B06 종단 그래프 위 **우클릭 메뉴** — B05 와 같은 부품(`mountStructureMenu`)을 그대로 쓴다. - * - * 왜 여기서도 여나(2026-09-12 사용자) — B05·B06 은 한 페이지다. 같은 그래프를 보면서 - * 한쪽에서만 넣고 뺄 수 있으면 사용자가 화면을 오가야 하고 직관도 깨진다. + * B06 종단 그래프 구조물 조작 길(넣기 · 빼기 · 옮기기) — 페이지가 물려 준다. * * 정본에 바로 쓰지 않는다 — 추가·삭제·이동은 **세션에 예약**하고 [저장]·[확정]에서 * 관 정본(`pipe_points.json`)으로 나간다(CLAUDE.md 5장). 세부 배수유역 재분할은 그때 * 서버가 관 목록을 다시 받아 처리한다. * ========================================================================== */ -import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures"; +import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures"; import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures"; -import type { IrregularStation } from "../B05_Profile/B05_Profile_UI_IrregularStations"; -import { mountStructureMenu } from "../B05_Profile/B05_Profile_UI_Profile_Structures"; -import { pickGroups } from "../B05_Profile/B05_Profile_UI_Structures_Panel_Types"; -import { listPick } from "../common_util/common_util_structure_catalog"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; /** 종단 그래프에서 구조물을 넣고 빼는 길 — 페이지가 물려 준다. 없으면 메뉴를 안 붙인다. */ @@ -87,49 +80,3 @@ function moveStructureMark( export function isPipeMark(structure: StructureInstance): boolean { return String(structure.structure_id ?? "").startsWith("pipe-"); } - -/** - * 종단 그래프 칸에 우클릭 메뉴를 붙인다. 그래프는 그릴 때마다 새로 만들어지므로 이 - * 함수도 그때마다 다시 부른다 — 상태를 밖에 남기지 않는다(B05 와 같은 규칙). - */ -export function mountSectionStructureMenu( - host: HTMLElement, - input: { - structures: ReadonlyArray; - types: ReadonlyArray; - x: (chainageM: number) => number; - chainageAt: (px: number) => number; - maxChainageM: number; - edit: SectionStructureEdit; - }, -): void { - // 메뉴가 쓰는 최소 정보만 옮겨 담는다 — B06 에는 B05 의 비정규 측점 목록이 없다. - const stations: IrregularStation[] = input.structures.map((structure) => { - const chainage = structureAnchorM(structure); - const pipe = isPipeMark(structure); - return { - id: String(structure.structure_id), - station: 0, - remainder: 0, - chainage_m: chainage, - structure: - input.types.find((type) => type.type_id === structure.type_id)?.name ?? - String(structure.type_id), - origin: pipe ? "pipe" : "user", - } as IrregularStation; - }); - - mountStructureMenu(host, { - stations, - x: input.x, - chainageAt: input.chainageAt, - maxChainageM: input.maxChainageM, - onRemove: (station) => { - if (station.origin === "pipe") input.edit.removePipe(station.chainage_m); - else input.edit.removeStructure(station.id); - }, - onAddPipe: (chainageM) => input.edit.addPipe(chainageM), - structureGroups: () => pickGroups(input.types, listPick("b06")), // B06 패널 거르기와 같음(129-1 · 322-1) - onAddStructureType: (chainageM, typeId) => input.edit.addStructureType(chainageM, typeId), - }); -} diff --git a/B06_Section/logic/B06_Section_UI_Cross_Culvert_Const.ts b/B06_Section/logic/B06_Section_UI_Cross_Culvert_Const.ts index dcc50e510..1c04150e9 100644 --- a/B06_Section/logic/B06_Section_UI_Cross_Culvert_Const.ts +++ b/B06_Section/logic/B06_Section_UI_Cross_Culvert_Const.ts @@ -28,9 +28,6 @@ export const REVET_THICKNESS_M = 0.45; * 못 가른 자리만 이 값. 돌쌓기 벽은 `B06_Section_UI_Cross_Lean.leanOf`(품셈 13-4-4 [주]⑪ 표준경사). */ export const REVET_LEAN_RATIO = 0.3; -/** 사다리꼴(배면부) 상단 폭(m) — 띠 두께의 절반(2026-08-20 사용자 확정). */ -export const REVET_TRAP_TOP_M = REVET_THICKNESS_M / 2; - /** 성토 위 이음 = 벽 윗면 사면 쪽 끝에서 이만큼 안(m · 371-1). */ export const WALL_FILL_TOP_INSET_M = 0.1; @@ -122,8 +119,6 @@ export function revetFormOf(m: string | null | undefined, specForm?: string | nu return m || specForm || REVET_FORM_DEFAULT; } -export const REVET_MATERIALS: RevetMaterial[] = ["dry", "wet", "concrete"]; - export function materialLabel(material: RevetMaterial): string { return material === "dry" ? "메쌓기" : material === "wet" ? "찰쌓기" : "콘크리트"; } @@ -195,7 +190,6 @@ export const EXTRA_TIER_SLOPE_M = 1.0; * (2026-08-21 사용자 지시). */ export const FILL_SLOPE_RATIO_MIN = 1.2; -export const FILL_SLOPE_RATIO_MAX = 2.0; /** 성토사면 길이 한계(m) — `성토_비탈면.md` §2 "5m 이내". 초과 시 옹벽·석축 등 의무. */ export const FILL_SLOPE_MAX_LENGTH_M = 5.0; @@ -221,13 +215,6 @@ export const PIPE_WALL_DEFAULT_RUN_M = export const pipeWallDefaultD = (adjust: { x: number }): number => Math.abs(adjust.x) > 1e-9 ? PIPE_WALL_DEFAULT_RUN_M : 0; -/** - * 성토 전체고(노견 − 원지반) 기준(m) — 이 높이를 넘으면 1:1.2로도 사면이 5m를 넘어 - * 구조물이 필요해진다(3m × √(1+1.2²) = 4.69m, 3.2m에서 5.0m). 기슭막이를 자연 사면 - * 끝이 아니라 **사면길이 5m 지점**으로 끌어올리는 판정에 쓴다(2026-08-21 사용자 지시). - */ -export const FILL_STRUCTURE_HEIGHT_M = 3.0; - /** * 성토 사면이 성립하는 **최소 성토고**(m) — 노견 − 벽 이음선 상단. * @@ -237,13 +224,6 @@ export const FILL_STRUCTURE_HEIGHT_M = 3.0; */ export const FILL_MIN_RISE_M = 0.3; -/** - * 집수정 바닥 여유(m) — **관 하단 아래**로 두는 깊이. 상부 여유는 기슭막이와 같은 - * `REVET_FREEBOARD_M`(0.5)을 쓴다. 집수정 깊이 = 바닥 여유 + 관경 + 상부 여유 - * (2026-08-21 사용자 확정 — 종전에는 성토 경사선과의 교점으로 높이를 잡았다). - */ -export const BASIN_BOTTOM_CLEARANCE_M = 0.5; - /** * 집수정 내부 높이(m) — 바닥판이 있는 ㄴ(L)·ㄷ형의 **바닥 윗면~벽 상단** 깊이. * 프로젝트 기본값 1.2m(2026-08-22 사용자 확정). 외부 기준은 확인되지 않았다. diff --git a/B06_Section/logic/B06_Section_UI_Cross_Culvert_Solve.ts b/B06_Section/logic/B06_Section_UI_Cross_Culvert_Solve.ts index 28bb3f116..1827b9473 100644 --- a/B06_Section/logic/B06_Section_UI_Cross_Culvert_Solve.ts +++ b/B06_Section/logic/B06_Section_UI_Cross_Culvert_Solve.ts @@ -196,26 +196,6 @@ export function cutLength(corners: { inlet: PipeEnd; outlet: PipeEnd }): number ); } -/** - * 유출 벽을 그 자리에 세웠을 때의 성토 사면(물매 1:n·사면길이)을 재는 함수를 만든다. - * 관 길이 정수 맞춤이 벽을 옮겨도 되는 자리인지 판정하는 데 쓴다. - */ -export function outletSlopeFactory( - info: { edge: { offset_m: number; elevation_m: number }; outward: number }, - invertAt: (offset: number) => number, - wallHeight: number, -): (offset: number, thickness: number) => { ratio: number; lengthM: number } { - return (offset, thickness) => { - const joint = offset + info.outward * (thickness / 2); - const rise = info.edge.elevation_m - (invertAt(offset) + wallHeight); - const run = Math.abs(joint - info.edge.offset_m); - return { - ratio: rise > 1e-6 ? run / rise : Number.POSITIVE_INFINITY, - lengthM: Math.hypot(run, Math.max(rise, 0)), - }; - }; -} - /** * 기슭막이 **자동 자리 = 노견(노폭 연장)이 최소가 되는 지점**(2026-08-22 사용자 확정). * 노견 끝에서 바깥으로 훑어, 표준 높이 벽으로 ① 성토고 하한(FILL_MIN_RISE_M)과 diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index d961ffe21..676d21211 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -286,7 +286,7 @@ def _cross_sheet_plan(project_root: Path, longitudinal_path: Path) -> list[dict[ """ longitudinal = _read_json(longitudinal_path) chainages: list[int | float] = [] - for path in sorted(_cross_files(longitudinal_path, longitudinal)): + for path in _cross_files(longitudinal_path, longitudinal): match = _CROSS_ID.fullmatch(path.stem) if not match: continue diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py b/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py index bbf43700b..c7b847d3f 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py @@ -9,7 +9,7 @@ import re from pathlib import Path from typing import Any -from B05_Profile.B05_Profile_Engine_Sections import cross_station, valid_cross_names +from B05_Profile.B05_Profile_Engine_Sections import cross_files_in_order, cross_station logger = logging.getLogger(__name__) @@ -39,12 +39,8 @@ def _cross_files(longitudinal_path: Path, longitudinal: dict[str, Any]) -> list[ if not cross_dir.is_dir(): raise FileNotFoundError("B06 횡단면 파일을 찾을 수 없습니다.") stations = longitudinal.get("stations") - # 읽기는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫 · R26) - valid_names = valid_cross_names(cross_dir, stations if isinstance(stations, list) else []) - files = sorted(cross_dir.glob("cross_*.json")) - if valid_names: - files = [path for path in files if path.name in valid_names] - return files + # 읽기는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫 · R26) · 측점 차례(472-2) + return cross_files_in_order(cross_dir, stations if isinstance(stations, list) else []) def _station_map(longitudinal: dict[str, Any]) -> dict[int | float, dict[str, Any]]: diff --git a/common_util/common_util_structure_catalog.ts b/common_util/common_util_structure_catalog.ts index f1dfb15c2..4e8f3679b 100644 --- a/common_util/common_util_structure_catalog.ts +++ b/common_util/common_util_structure_catalog.ts @@ -204,3 +204,6 @@ export function shapeNeeds(typeId: string): readonly string[] { /** 관 공식 이름의 관종들(정의 선택지) — 표시 이름이 「배수관」으로 줄일 이름을 알아봄. */ export const pipeTitleKinds = (): readonly string[] => pipeKinds; + +/** 배수관의 화면 표시 이름(2026-08-17 사용자 지시) — 목록 · 종 · 횡단 표기가 같이 씀. */ +export const PIPE_DISPLAY_NAME = "배수관"; diff --git a/docs/wiki/concepts/design_page/calculations.md b/docs/wiki/concepts/design_page/calculations.md index 435b5a144..2fbf731b3 100644 --- a/docs/wiki/concepts/design_page/calculations.md +++ b/docs/wiki/concepts/design_page/calculations.md @@ -33,7 +33,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md | P5 | 노선 바뀜 계획고 옮김 | `logic/B05_Profile_Grade_Carry.ts:188` | `B05_Profile_Engine_Grade_Carry.py:149` | — | 짝 · `test_400_profile_carry` · `test_349_0v_route_save_span` | | P6 | 옮긴 시설 꼭짓점 세움 | `logic/B05_Profile_Grade_Seat.ts:32` · `common_util_structure_station.ts:356` | `B05_Profile_Router_Confirm.py:220 · 306` | — | 짝 · `test_427_1_follow_seat` · `test_459_route_seat_mirror` | | P7 | 직선화 · 자유지정 · 쉬프트 · 시종점 | `logic/B05_Profile_UI_Profile_Straighten.ts` · `logic/design_profile_free.ts` · `logic/B05_Profile_UI_Profile_Pivot.ts` · `logic/B05_Profile_EndOffsets.ts` | — | — | 브라우저(편집값만 저장 → P2) | -| P8 | 종단 경고(기울기 · 법정 L · 최소 R · 피복) | `B05_Profile_UI_Panel_Fields.ts` · `logic/B05_Profile_UI_Profile_Guard.ts` · `B05_Profile_UI_Profile_MinCover.ts` | `B05_Profile_Engine_Grade.py` · `B05_Profile_PageSettings.py` | — | 짝 · `test_442_2_grade_limit_mirror` | +| P8 | 종단 경고(기울기 · 법정 L · 최소 R · 피복) | `B05_Profile_UI_Panel_Fields.ts` · `logic/B05_Profile_UI_Profile_Guard.ts` · `B05_Profile_UI_Profile_MinCover.ts` · 경고 글 `logic/B05_Profile_UI_Profile_Alignment.ts` buildAlignment | `B05_Profile_Engine_Grade.py` · `B05_Profile_PageSettings.py` · 경고 글 `B05_Profile_Engine_Grade_Alignment.py` build_alignment | — | 짝 · [저장] · [확정] = `B06_Section_Server_Calc_Prebuild.py` refresh_alignment 가 경고만 다시 씀(기하 그대로) · `test_442_2_grade_limit_mirror` · `test_472_save_warnings` | | C1 | 횡단 설계선 | `common_util_cross_design.ts:267` · `logic/B06_Section_Cross_Stage.ts:93` · `B06_Section_Cross_Refresh.ts:330` | `B06_Section_Engine_Design.py:195` | ex | 짝 · `test_b06_cross_design_mirror` | | C2 | 단면적(절 · 성 · 측구 · 층따기) | `common_util_cross_design_areas.ts` | `B06_Section_Engine_Areas.py` | sc · ex | 짝 · 정본 = 표준 측점 파이썬 · 구조물 측점 Node | | C3 | 확정 보정(포장 · 세월교 · 기울임) | `logic/B06_Section_Cross_Enforce.ts:40` | `B06_Section_Router_Design.py` · `B06_Section_Engine_Tilt.py` | — | 짝 · `test_349_0n_cross_enforce_mirror` | diff --git a/docs/wiki/concepts/design_page/operations_b06.md b/docs/wiki/concepts/design_page/operations_b06.md index f5e6a449b..285838845 100644 --- a/docs/wiki/concepts/design_page/operations_b06.md +++ b/docs/wiki/concepts/design_page/operations_b06.md @@ -32,5 +32,5 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/ | B06 폼 관 측점 · 옵션 고침 | `pipeFacilities` → 세션 `pipes` · 옮긴 자리(정수 m)로 옵션 · 캐시 얹기 · 집계 찾음 | B05 관 옮김과 같음 | 배수유역 다시 나눔 | 관 차례 `PUT /drainage/pipe-points` | 14 | `B06_Section_UI_Page_Structures_Panel.ts:300-345 · 536-560` | | B06 구조물 패널 | 보기 `structures` → 시설 줄 `set` → 세션 `structures` | B05 구조물과 같음 | — | `PUT /route/structures` | 11 | `B06_Section_UI_Page_Structures_Panel.ts:211-227 · 582-598` | | B06 평면 패널 끌기 | B05 교점 끌기와 같음(같은 편집기를 옮겨 붙임) | 같음 · B06 쪽 갈아끼움이 먼저 | 같음 | `/route/replan` | 11 | `design_plan_peek.ts:61-77` · `B05_Profile_UI_RouteEdit.ts:290-293 · 599-601` · `B06_Section_UI_Page_Station_Swap.ts:35` | -| 넓힘(표본 폭 넘음) | 없음 — 확인창 뒤 바로 서버 | 응답 뒤 상세 강제 다시 받음 | `/sections/{노선}/regenerate` | **저장 밖** — DB 바로 씀(이미 정본) | 3 | `B06_Section_UI_Page_Common.ts:32-63` · `B06_Section_Router.py:401-411` · `B06_Section_Router_Widen.py:80` | +| 넓힘(표본 폭 넘음) | 없음 — 확인창 뒤 바로 서버 | 응답 뒤 상세 강제 다시 받음 | `/sections/{노선}/regenerate` | **저장 밖** — DB 바로 씀(이미 정본) | 3 | `B06_Section_UI_Page_Common.ts:32-63` · `B06_Section_Router.py:378-388` · `B06_Section_Router_Widen.py:80` | | [측점 만들기] | 없음 — 바로 서버 | 응답 뒤 상세 비움 · 강제 다시 받음 · 알림 | `/section/missing-stations` | **저장 밖** — 종단 · 횡단 파일 바로 씀 | 3 | `B06_Section_UI_Missing_Stations.ts:86-138` · `B06_Section_Router_Stations.py:236-296 · 365-407` | diff --git a/docs/wiki/concepts/design_page/operations_common.md b/docs/wiki/concepts/design_page/operations_common.md index 53f0dce06..7517c1ae5 100644 --- a/docs/wiki/concepts/design_page/operations_common.md +++ b/docs/wiki/concepts/design_page/operations_common.md @@ -35,7 +35,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/ | alignment | `flushAlignmentDraft` | `PUT /route/profile-alignment` | 초안 노선 0 편집을 새 노선에 | `design_save.ts:185-186` | | culvertOptions | `flushCulvertOptions`(`culvertopt` · 옛 노선 열쇠) | `pipe_points.json` 한 번 더 PUT | 옛 노선 열쇠로 읽음 | `design_save.ts:187-188` · `B06_Section_Api_Culvert_Options.ts:67-95` | | uphill | `flushRouteParts` | 상단측 | 건너뜀 — 횡단 차례가 옮긴 뒤 보냄 | `design_save.ts:189-190 · 285` · `B05_Profile_Api_Fetch.ts:321-342` | -| cross | `ctx.cross(confirm)` = 횡단 패치 한 요청 | `/sections/{노선}/save` · `confirm` | `carryAfterRouteSave`(새 측점으로 옮김) | `design_save.ts:275-283` | +| cross | `ctx.cross(confirm)` = 횡단 패치 한 요청 | `/sections/{노선}/save` · `confirm` · 재계산 맨 앞 종단 경고 다시 씀(`B06_Section_Server_Calc_Prebuild.py` refresh_alignment) | `carryAfterRouteSave`(새 측점으로 옮김) | `design_save.ts:275-283` | - 차례마다 따로 커밋 — 실패 = 그 차례에서 멈춤 · 이름째 알림 · 앞 차례는 저장된 채(`design_save.ts:297-299`). - 저장 중 = `saving` 표시 · 묶음 맞춤은 저장 뒤로 미룸(`design_pipeline_page.ts:567`). diff --git a/docs/wiki/concepts/design_page/rules.md b/docs/wiki/concepts/design_page/rules.md index ebf9c4631..b28eefb62 100644 --- a/docs/wiki/concepts/design_page/rules.md +++ b/docs/wiki/concepts/design_page/rules.md @@ -45,7 +45,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함 | 내용 | 자리(파일:줄) | 상태 | |---|---|---| -| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용] | `B06_Section_Router.py:401-411` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` | 사용자 확정 · 확인창 · 472-6 협의 | +| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용] | `B06_Section_Router.py:378-388` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` | 사용자 확정 · 확인창 · 472-6 협의 | | [저장] 9 차례 · 차례마다 따로 커밋 — 실패 = 앞 차례만 저장 | `design_save.ts:253-299` | 472-6 | | `pipe_points.json` 한 [저장]에 두 번 PUT(관 · 관 옵션) | `B06_Section_Api_Culvert_Options.ts:67-95` | 코드 확인 | | 노선 바뀐 [저장]의 관 옵션 차례 = 옛 노선 열쇠 | `design_save.ts:187-188` | 추정 | 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/docs/wiki/pages/B06_Section/B06_api.md b/docs/wiki/pages/B06_Section/B06_api.md index 64cc105f9..89c11937b 100644 --- a/docs/wiki/pages/B06_Section/B06_api.md +++ b/docs/wiki/pages/B06_Section/B06_api.md @@ -20,7 +20,6 @@ source: ["docs/raw/verification/2026-07-19_verify_B05_B06_UI_Station_Improvement | `GET /{project_id}/sections/detail` | `B06_Section_Router.py` | 종단 결과와 다수 횡단면을 일괄 조회하고 배수관 측점에는 `culvert` 세트 제원을 결합 | | `GET /{project_id}/sections/road-widths` | `B06_Section_Router.py` | 임도 등급별 법정 최소 도로폭 정보(`FOREST_ROAD_MIN_WIDTH_M`) 조회 (B05 프론트엔드가 측점 가로선 반폭 계산 시 호출) | | `POST /{project_id}/sections/{route_id}/regenerate` | `B06_Section_Router.py:85` | 횡단 반폭 재생성 요청 (`SectionRegenerateRequest` 매핑) | -| `POST /{project_id}/sections/{route_id}/cross-design` | `B06_Section_Router.py` | 측점별 지반·단면·측구와 집수정 `basin_adjust`(폭·높이·좌우·상하)를 `data.design`에 저장 (`CrossDesignRequest` → `CrossDesignResponse`) | | `GET /{project_id}/recent-company-projects` | `B06_Section_Router.py` | 동일 회사 최근 5개 프로젝트 목록 반환 API | | `GET /{project_id}/standard-cross-section-from/{target_id}` | `B06_Section_Router.py` | 지정 타 프로젝트의 표준단면 설계값 로드 API | diff --git a/docs/wiki/pages/B06_Section/frontend/B06_Section_Api_Fetch.md b/docs/wiki/pages/B06_Section/frontend/B06_Section_Api_Fetch.md index f1d0c7150..b35ff8272 100644 --- a/docs/wiki/pages/B06_Section/frontend/B06_Section_Api_Fetch.md +++ b/docs/wiki/pages/B06_Section/frontend/B06_Section_Api_Fetch.md @@ -15,8 +15,6 @@ last_updated: 2026-08-02 ## 2. 주요 연동 API 함수 * `getSectionsDetail(projectId)`: * `GET /sections/detail` 호출. 종단면도 지반선/계획선 및 다수 횡단면 데이터를 일괄 응답받습니다. -* `crossDesignOne(projectId, routeId, data)`: - * `POST /sections/{routeId}/cross-design` 호출. 측점별 지반유형/단면유형/측구/암반경계선 높이 갱신 시 실시간 단면적 계산 및 저장을 수행합니다. * `regenerateSections(projectId, routeId, width)`: * `POST /sections/{routeId}/regenerate` 호출. 횡단 반폭 재생성을 비동기로 요청합니다. * `confirmSections(projectId, routeId, massHaul)`: diff --git a/resources/tester/fixtures/golden_472/golden_472_values.json b/resources/tester/fixtures/golden_472/golden_472_values.json index 0d7d7117b..e2cd782ae 100644 --- a/resources/tester/fixtures/golden_472/golden_472_values.json +++ b/resources/tester/fixtures/golden_472/golden_472_values.json @@ -1850,11 +1850,56 @@ "pipe:p35100": 7.9 }, "warnings": [ - "chainage 1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.", - "chainage 340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.", - "chainage 351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.", - "chainage 360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.", - "종단기울기 56.60%가 기준 14.00%를 초과하는 구간이 34개 있습니다." + "100.0m: 종단곡선 R 57m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "1000.0m: 종단곡선 R 73m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "1020.0m: 종단곡선 R 31m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "1060.0m: 종단곡선 R 25m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.", + "1080.0m: 종단곡선 R 1m · L 0.1m — 법정 최소 R 100m · L 20m 미달", + "140.0m: 종단곡선 R 53m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "20.0m: 종단곡선 R 205m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "200.0m: 종단곡선 R 154m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "220.0m: 종단곡선 R 285m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "240.0m: 종단곡선 R 61m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "260.0m: 종단곡선 R 24m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "280.0m: 종단곡선 R 21m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "300.0m: 종단곡선 R 196m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "320.0m: 종단곡선 R 118m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.", + "340.0m: 종단곡선 R 26m · L 9.9m — 법정 최소 R 100m · L 20m 미달", + "351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.", + "351.0m: 종단곡선 R 25m · L 8.1m — 법정 최소 R 100m · L 20m 미달", + "360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.", + "40.0m: 종단곡선 R 55m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "400.0m: 종단곡선 R 105m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "420.0m: 종단곡선 R 100m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "440.0m: 종단곡선 R 117m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "460.0m: 종단곡선 R 48m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "480.0m: 종단곡선 R 46m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "520.0m: 종단곡선 R 24m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "540.0m: 종단곡선 R 41m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "560.0m: 종단곡선 R 53m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "580.0m: 종단곡선 R 123m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "600.0m: 종단곡선 R 40m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "620.0m: 종단곡선 R 88m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "640.0m: 종단곡선 R 147m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "660.0m: 종단곡선 R 28m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "680.0m: 종단곡선 R 14m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "700.0m: 종단곡선 R 49m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "720.0m: 종단곡선 R 38m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "740.0m: 종단곡선 R 55m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "760.0m: 종단곡선 R 36m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "780.0m: 종단곡선 R 54m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "80.0m: 종단곡선 R 58m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "800.0m: 종단곡선 R 13m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "820.0m: 종단곡선 R 45m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "840.0m: 종단곡선 R 29m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "880.0m: 종단곡선 R 29m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "900.0m: 종단곡선 R 115m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "920.0m: 종단곡선 R 44m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "940.0m: 종단곡선 R 66m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "960.0m: 종단곡선 R 23m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "980.0m: 종단곡선 R 42m · L 15.0m — 법정 최소 R 100m · L 20m 미달" ], "violations": 34 }, @@ -2783,6 +2828,7 @@ "1000.0m: 종단곡선 R 73m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "1020.0m: 종단곡선 R 31m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "1060.0m: 종단곡선 R 25m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.", "1080.0m: 종단곡선 R 1m · L 0.1m — 법정 최소 R 100m · L 20m 미달", "140.0m: 종단곡선 R 53m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "20.0m: 종단곡선 R 205m · L 15.0m — 법정 최소 R 100m · L 20m 미달", @@ -2793,8 +2839,11 @@ "280.0m: 종단곡선 R 21m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "300.0m: 종단곡선 R 196m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "320.0m: 종단곡선 R 118m · L 15.0m — 법정 최소 R 100m · L 20m 미달", + "340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.", "340.0m: 종단곡선 R 26m · L 9.9m — 법정 최소 R 100m · L 20m 미달", + "351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.", "351.0m: 종단곡선 R 25m · L 8.1m — 법정 최소 R 100m · L 20m 미달", + "360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.", "40.0m: 종단곡선 R 55m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "400.0m: 종단곡선 R 105m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "420.0m: 종단곡선 R 100m · L 15.0m — 법정 최소 R 100m · L 20m 미달", @@ -2824,12 +2873,7 @@ "920.0m: 종단곡선 R 44m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "940.0m: 종단곡선 R 66m · L 15.0m — 법정 최소 R 100m · L 20m 미달", "960.0m: 종단곡선 R 23m · L 15.0m — 법정 최소 R 100m · L 20m 미달", - "980.0m: 종단곡선 R 42m · L 15.0m — 법정 최소 R 100m · L 20m 미달", - "chainage 1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.", - "chainage 340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.", - "chainage 351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.", - "chainage 360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.", - "종단기울기 56.60%가 기준 14.00%를 초과하는 구간이 34개 있습니다." + "980.0m: 종단곡선 R 42m · L 15.0m — 법정 최소 R 100m · L 20m 미달" ], "violations": 34 }, diff --git a/resources/tester/helper_273_2_preview_stations.cjs b/resources/tester/helper_273_2_preview_stations.cjs index dccd78300..00589f01a 100644 --- a/resources/tester/helper_273_2_preview_stations.cjs +++ b/resources/tester/helper_273_2_preview_stations.cjs @@ -51,7 +51,7 @@ const drafts = loadTs("B06_Section/B06_Section_Section_Drafts.ts", { "../A00_Common/design/design_page_data": loadTs("A00_Common/design/design_page_data.ts", { "../../B04_PreProcess/B04_PreProcess_Api_Fetch": serverFetch, "../../B05_Profile/B05_Profile_Api_Structures": serverStructures, - "../../B06_Section/B06_Section_Api_Fetch": { + "../../B06_Section/B06_Section_Api_Context": { fetchSectionContext: async () => ({ route_id: 1 }), }, "./design_page_route": loadTs("A00_Common/design/design_page_route.ts"), // 노선 id 한 칸(386-2b) diff --git a/resources/tester/helper_384_2_page_data.cjs b/resources/tester/helper_384_2_page_data.cjs index 1835191dc..ac83ae4dc 100644 --- a/resources/tester/helper_384_2_page_data.cjs +++ b/resources/tester/helper_384_2_page_data.cjs @@ -23,7 +23,7 @@ const STUBS = { }, }, // 횡단 context(세션 캐시) — 노선 id 한 칸(386-2b) - "../../B06_Section/B06_Section_Api_Fetch": { + "../../B06_Section/B06_Section_Api_Context": { fetchSectionContext: async (projectId) => ({ route_id: projectId === "p-none" ? null : 304 }), }, "../../B05_Profile/B05_Profile_Api_Structures": { diff --git a/resources/tester/helper_472_1_paths.py b/resources/tester/helper_472_1_paths.py index 3c0c33035..587b9b9d7 100644 --- a/resources/tester/helper_472_1_paths.py +++ b/resources/tester/helper_472_1_paths.py @@ -1,8 +1,8 @@ """472-1 기준값 — 고정 자료(`fixtures/golden_472`) 하나로 길 넷의 값을 뜸(시험 · 값 뜨기 함께 씀). stored GET 상세(정본 그대로) → 누가토량(서버 Node 묶음 `computeMassHaul`) - server_save [저장] 서버 재계산 — 정본 상세 → `enforce_design_lines` → - `_enforce_stored_designs` → server-calc + server_save [저장] 서버 재계산 — 정본 상세 → 종단 경고 다시 셈(`refresh_alignment` · 472 덧) → + `enforce_design_lines` → `_enforce_stored_designs` → server-calc server_route 노선 저장 체인(같은 노선 · DB 없이) — `run_section_generation`(고정 geojson · npz) → `_carry_profile` 같은 노선 갈래(옛 base_pvi · 편집) → `_apply_alignment_edits` → 측점 열쇠 이월(`carry_cross_inputs`) → `compute_default_designs` → 상세 읽기 → @@ -125,19 +125,21 @@ def _context() -> dict[str, Any]: def _save_tail(detail: dict[str, Any], root: Path) -> tuple[list, dict | None]: - """[저장] 재계산 본문(`_recompute`) — 설계선 · 넓힘 · 보정 → Node 면적 · 누가토량 얹음.""" + """[저장] 재계산(`_recompute`) — 종단 경고 · 설계선 · 넓힘 · 보정 → Node 면적 · 누가토량.""" from B06_Section.B06_Section_Router_Design import enforce_design_lines from B06_Section.B06_Section_Server_Calc_Prebuild import ( _AREA_KEYS, _NPM_SCRIPT, BUNDLE, _enforce_stored_designs, + refresh_alignment, ) from common_util.common_util_node_bundle import run_bundle_json from common_util.common_util_surface_sampler import build_surface_sampler longitudinal = detail["longitudinal"] sections = detail["cross_sections"] + refresh_alignment(longitudinal) enforce_design_lines(longitudinal, sections, root, None) unclosed = any((s.get("design") or {}).get("slope_unclosed") for s in sections) sampler = ( @@ -163,9 +165,9 @@ def _save_tail(detail: dict[str, Any], root: Path) -> tuple[list, dict | None]: def server_save(root: Path) -> dict[str, Any]: detail = copy.deepcopy(read("detail.json")) - alignment = detail["longitudinal"].get("profile_alignment") or {} keys = _keys(detail["longitudinal"], detail["cross_sections"], root) sections, haul = _save_tail(detail, root) + alignment = detail["longitudinal"].get("profile_alignment") or {} return values_of( sections, keys, haul, alignment.get("warnings") or [], alignment.get("violations") or [] ) diff --git a/resources/tester/helper_472_save_warnings.cjs b/resources/tester/helper_472_save_warnings.cjs new file mode 100644 index 000000000..e1c90c6d0 --- /dev/null +++ b/resources/tester/helper_472_save_warnings.cjs @@ -0,0 +1,36 @@ +/* 472 덧 종단 경고 거울 헬퍼 — `logic/B05_Profile_UI_Profile_Alignment.ts` `buildAlignment` 를 그 자리에서 + * 트랜스파일해 돌림. 입력 argv[2] JSON { base, cases: [edits] } · 표준출력 JSON [{ warnings, violations }]. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const cache = new Map(); +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => + request.startsWith(".") + ? loadTs(path.join(path.dirname(full), `${request}.ts`)) + : require(request); + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +const align = loadTs( + path.join(__dirname, "..", "..", "B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"), +); +const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); +console.log( + JSON.stringify( + input.cases.map((edits) => { + const built = align.buildAlignment(input.base, edits); + return { warnings: built.warnings, violations: built.violations.length }; + }), + ), +); diff --git a/resources/tester/test_472_1_golden.py b/resources/tester/test_472_1_golden.py index abd7e47bd..aff996468 100644 --- a/resources/tester/test_472_1_golden.py +++ b/resources/tester/test_472_1_golden.py @@ -44,10 +44,15 @@ class Known: KNOWN: dict[tuple[str, str], list[Known]] = { - ( - "server_save", - "stored", - ): [], # [저장] 재계산 = 정본 그대로(사용자 원칙 — 입력 안 바뀌면 값 안 바뀜) + # [저장] 재계산 = 정본 그대로(사용자 원칙 — 입력 안 바뀌면 값 안 바뀜) · 경고만 지금 코드 + ("server_save", "stored"): [ + Known( + "W0", + {"warnings": None}, + "정본 종단 경고 = 10-17 저장 때 코드(법정 R · L 미달 46 줄 전) · " + "[저장] 재계산이 지금 코드로 다시 씀(472 덧 · 기하 그대로)", + ), + ], ("server_route", "stored"): [ Known( "R7", @@ -58,7 +63,7 @@ KNOWN: dict[tuple[str, str], list[Known]] = { "W0", {"warnings": None}, "정본 종단 경고 = 10-17 저장 때 코드(법정 R · L 미달 46 줄 전) · " - "[저장] 재계산은 종단 경고를 안 씀", + "노선 저장 체인은 지금 코드", ), ], ("browser", "server_route"): [ @@ -76,12 +81,6 @@ KNOWN: dict[tuple[str, str], list[Known]] = { "측점 자리 ≤ 1 mm → 표본 · 면적 ≤ 0.07 ㎡ · 층따기 밑수 ≤ 0.52 m · " "누가토량 끝값 0.69 ㎥(−17,570.01 ↔ −17,570.70)", ), - Known( - "W1", - {"warnings": None}, - "종단 경고 글 두 벌(X5 · P8) — TS 「340.0m: …」 · 파이썬 「chainage 340.0m: …」 · " - "최대 기울기 총괄 줄은 파이썬만", - ), ], } diff --git a/resources/tester/test_472_2_cross_order.py b/resources/tester/test_472_2_cross_order.py new file mode 100644 index 000000000..40d9be02f --- /dev/null +++ b/resources/tester/test_472_2_cross_order.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""472-2 덧 — 횡단 파일을 측점(누가거리) 차례로 읽는지. + +이름 차례면 `cross_01080.16m` 이 `cross_01080m` 앞(「.」 < 「m」) → GET 상세의 측점 차례가 +어긋나 화면 차례 짝(detailCross)이 갈리고, B07 측점 도면 장 나눔도 차례가 뒤집힘. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B06_Section.B06_Section_Router import _read_section_detail # noqa: E402 +from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _cross_files # noqa: E402 + +CHAINAGES = [1060.0, 1080.0, 1080.16, 1100.0, 999.5] +NAMES = { + 1060.0: "cross_01060m.json", + 1080.0: "cross_01080m.json", + 1080.16: "cross_01080.16m.json", + 1100.0: "cross_01100m.json", + 999.5: "cross_00999.50m.json", +} + + +def _stage(tmp_path: Path) -> Path: + stage = tmp_path / "B05_Profile" + (stage / "cross_sections").mkdir(parents=True) + (stage / "longitudinal").mkdir() + longitudinal = stage / "longitudinal" / "longitudinal.json" + longitudinal.write_text( + json.dumps({"stations": [{"chainage_m": m} for m in CHAINAGES]}), encoding="utf-8" + ) + for chainage, name in NAMES.items(): + (stage / "cross_sections" / name).write_text( + json.dumps({"chainage_m": chainage, "samples": []}), encoding="utf-8" + ) + return longitudinal + + +def test_상세_GET_은_측점_차례(tmp_path: Path) -> None: + longitudinal = _stage(tmp_path) + detail = _read_section_detail(tmp_path, str(longitudinal.relative_to(tmp_path))) + got = [section["chainage_m"] for section in detail["cross_sections"]] + assert got == sorted(CHAINAGES) + + +def test_B07_측점_파일도_측점_차례(tmp_path: Path) -> None: + longitudinal = _stage(tmp_path) + files = _cross_files(longitudinal, json.loads(longitudinal.read_text(encoding="utf-8"))) + assert [path.name for path in files] == [NAMES[m] for m in sorted(CHAINAGES)] + + +def test_B07_장_나눔도_측점_차례() -> None: + """장 나눔 쪽이 이름으로 다시 줄 세우지 않음(`sorted(_cross_files(…))` = 이름 차례).""" + source = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_Router_Support.py").read_text( + encoding="utf-8" + ) + assert "sorted(_cross_files(" not in source diff --git a/resources/tester/test_472_3_calc_registry.py b/resources/tester/test_472_3_calc_registry.py index 722abba13..a11287b51 100644 --- a/resources/tester/test_472_3_calc_registry.py +++ b/resources/tester/test_472_3_calc_registry.py @@ -54,9 +54,6 @@ SESSION_PULLS = ( "A00_Common/design/design_pick_match.ts", "A00_Common/design/design_pipe_ids.ts", "A00_Common/design/design_selection.ts", - "A00_Common/design/design_store.ts", - "B05_Profile/B05_Profile_UI_IrregularStations.ts", - "B05_Profile/B05_Profile_UI_Profile_History.ts", "B06_Section/B06_Section_Api_Fetch.ts", "B06_Section/B06_Section_UI_Section_Common.ts", "config/config_frontend.ts", diff --git a/resources/tester/test_472_save_warnings.py b/resources/tester/test_472_save_warnings.py new file mode 100644 index 000000000..f50f79307 --- /dev/null +++ b/resources/tester/test_472_save_warnings.py @@ -0,0 +1,130 @@ +"""PLAN 472 덧 (요구 272) — [저장] · [확정] 재계산이 종단 경고를 저장 입력으로 다시 씀. + +증상 — 검증_랩탑메인 정본 종단 경고 5 줄(10-17 옛 코드) · 지금 코드로 셈하면 50 줄 · +[저장] · [확정] 재계산(`B06_Section_Server_Calc_Prebuild._recompute`)이 경고를 다시 안 셈. +고침 — `_recompute` 맨 앞 `_refresh_alignment_file` → `refresh_alignment`(저장 base_pvi · 편집 · +정책 · 기준 → `rebuild_alignment_profile` 의 경고만) · 기하는 저장값 그대로 · 같으면 안 씀. +경고 글 = TS `buildAlignment` 와 한 벌(거울). 고정 자료 = `fixtures/golden_472/detail.json`. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_472_save_warnings.py -q +""" + +from __future__ import annotations + +import copy +import inspect +import json +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +sys.path.insert(0, str(ROOT)) + +from B05_Profile.B05_Profile_Engine_Grade import ground_profile # noqa: E402 +from B05_Profile.B05_Profile_Engine_Grade_Alignment import chainage_key # noqa: E402 +from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile # noqa: E402 +from B06_Section import B06_Section_Server_Calc_Prebuild as prebuild # noqa: E402 + +FIXTURE = HERE / "fixtures" / "golden_472" +DETAIL = json.loads((FIXTURE / "detail.json").read_text(encoding="utf-8")) + + +def _longitudinal() -> dict: + return copy.deepcopy(DETAIL["longitudinal"]) + + +def test_정본_경고는_옛_코드_5줄() -> None: + assert len(DETAIL["longitudinal"]["profile_alignment"]["warnings"]) == 5 + + +def test_다시_셈_경고만_새로_기하는_그대로() -> None: + longitudinal = _longitudinal() + before = copy.deepcopy(longitudinal) + summary = prebuild.refresh_alignment(longitudinal) + warnings = longitudinal["profile_alignment"]["warnings"] + assert len(warnings) == 50 + assert sum("법정 최소 R" in w for w in warnings) == 46 # 432 법정 R · L 미달 + assert not [w for w in warnings if w.startswith("chainage ")] + assert summary is not None and summary["warnings"] == warnings + assert summary["id"] == before["design_profiles"][0]["id"] + assert longitudinal["design_profiles"][0]["summary"]["warnings"] == warnings + # 경고 밖 = 저장값 그대로(계획고 · 곡선 · 표본 · 위반 · 정책 — 6 자리 반올림 끝자리도) + longitudinal["profile_alignment"]["warnings"] = before["profile_alignment"]["warnings"] + longitudinal["design_profiles"][0]["summary"]["warnings"] = before["design_profiles"][0][ + "summary" + ]["warnings"] + assert longitudinal == before + + +def test_선형_없는_옛_종단은_그대로() -> None: + longitudinal = _longitudinal() + longitudinal["profile_alignment"].pop("base_pvi") + keep = copy.deepcopy(longitudinal) + assert prebuild.refresh_alignment(longitudinal) is None + assert longitudinal == keep + + +def test_파일_다시_씀_같으면_안_씀(tmp_path) -> None: + path = tmp_path / "B06_Section" / "longitudinal" / "longitudinal.json" + path.parent.mkdir(parents=True) + path.write_text(json.dumps(_longitudinal()), encoding="utf-8") + relative = "B06_Section/longitudinal/longitudinal.json" + first = prebuild._refresh_alignment_file(tmp_path, relative) + stored = json.loads(path.read_text(encoding="utf-8")) + assert first is not None and len(stored["profile_alignment"]["warnings"]) == 50 + written = path.stat().st_mtime_ns + assert prebuild._refresh_alignment_file(tmp_path, relative) == first + assert path.stat().st_mtime_ns == written # 같은 값 — 다시 안 씀 + assert prebuild._refresh_alignment_file(tmp_path, "없음/longitudinal.json") is None + + +def test_재계산_맨_앞에서_선형_다시_셈() -> None: + source = inspect.getsource(prebuild._recompute) + assert "_refresh_alignment_file" in source and "update_longitudinal_grade_summary" in source + assert source.index("_refresh_alignment_file") < source.index("get_section_detail(") + + +def test_TS_거울_같은_경고_목록(tmp_path) -> None: + longitudinal = _longitudinal() + alignment = longitudinal["profile_alignment"] + # 옛 정책엔 법정 R · L 이 없음 — 두 쪽 다 기준으로 채운 정책(433-13 · TS `readAlignment`) + policy = rebuild_alignment_profile(longitudinal, alignment["edits"])[0]["policy"] + assert ( + policy["min_vertical_radius_m"] == 100.0 + and "min_vertical_radius_m" not in alignment["policy"] + ) + chainage, ground = ground_profile(longitudinal) + curve = alignment["curves"][1] + station = longitudinal["stations"][5] + cases = [ + alignment["edits"], + {"station_offsets": {}, "curve_radii": {chainage_key(curve["chainage_m"]): 500.0}}, + {"station_offsets": {chainage_key(station["chainage_m"]): 3.0}, "curve_radii": {}}, + ] + base = { + "policy": policy, + "onlyExplicitCurves": bool(alignment.get("only_explicit_curves")), + "basePvi": alignment["base_pvi"], + "chainage": chainage.tolist(), + "ground": ground.tolist(), + "stations": [ + {"station_id": s.get("station_id"), "chainage_m": s["chainage_m"]} + for s in longitudinal["stations"] + ], + } + source = tmp_path / "in.json" + source.write_text(json.dumps({"base": base, "cases": cases}), encoding="utf-8") + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HERE / "helper_472_save_warnings.cjs"), str(source)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + for edits, got in zip(cases, json.loads(proc.stdout), strict=True): + py, _profile = rebuild_alignment_profile(copy.deepcopy(longitudinal), edits) + assert got["warnings"] == py["warnings"] + assert got["violations"] == len(py["violations"]) 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", ] diff --git a/resources/tester/test_b06_stored_standard_reaches_browser.py b/resources/tester/test_b06_stored_standard_reaches_browser.py index 2e8203191..3fa7c5ec8 100644 --- a/resources/tester/test_b06_stored_standard_reaches_browser.py +++ b/resources/tester/test_b06_stored_standard_reaches_browser.py @@ -20,7 +20,7 @@ if str(PROJECT_ROOT) not in sys.path: from B06_Section.B06_Section_Schema import SectionContextResponse # noqa: E402 -_FETCH = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Fetch.ts" +_FETCH = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Context.ts" # 472-2 세션 창구 갈라냄 _ROUTER = PROJECT_ROOT / "B06_Section" / "B06_Section_Router.py" diff --git a/resources/tester/test_b06_user_touched_keys.py b/resources/tester/test_b06_user_touched_keys.py index 37665ae66..6832a1f2f 100644 --- a/resources/tester/test_b06_user_touched_keys.py +++ b/resources/tester/test_b06_user_touched_keys.py @@ -51,7 +51,6 @@ def test_확정_payload_도_손나열이_없다(): def test_서버_목록이_한_벌이다(): - """라우터가 목록을 다시 적지 않고 그 한 벌을 쓴다.""" + """라우터가 목록을 다시 적지 않는다(목록을 쓰던 `cross-design` 엔드포인트는 472-2 에서 지움).""" router = (ROOT / "B06_Section" / "B06_Section_Router.py").read_text(encoding="utf-8") - assert "USER_TOUCHED_KEYS" in router assert router.count('"revet_follow_grade"') == 0, "라우터에 목록 사본이 남아 있음" diff --git a/resources/tester/test_user_values_survive.py b/resources/tester/test_user_values_survive.py index ff3053c52..73f8ee72d 100644 --- a/resources/tester/test_user_values_survive.py +++ b/resources/tester/test_user_values_survive.py @@ -211,7 +211,9 @@ def test_저장된_표준단면이_브라우저까지_간다(): from B06_Section.B06_Section_Schema import SectionContextResponse assert "stored_standard_cross_section" in SectionContextResponse.model_fields - fetch = (PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Fetch.ts").read_text(encoding="utf-8") + fetch = (PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Context.ts").read_text( + encoding="utf-8" + ) assert "function seedStandardCross(" in fetch assert fetch.count("seedStandardCross(projectId,") >= 2, "한쪽 갈래만 세움"