diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index 85b43cbe..7801731b 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -118,8 +118,9 @@ async def run_auto_design_chain(project_id: UUID, surface_model_id: int | None = float(solve_result.total_length_m or 0.0), ) - # 4) B05 경로 확정 — stage 2 완료 전이 포함. - confirm_result = await confirm_latest_route(project_id, None) + # 4) B05 경로 확정 — 데이터만 CONFIRMED, stage 2는 IN_PROGRESS(사용자 검토 대기)로 + # 남긴다. 완료 전이는 B06 [확정]이 stage 2·3을 함께 처리한다(2026-08-08 재정의). + confirm_result = await confirm_latest_route(project_id, None, mark_stage_complete=False) if isinstance(confirm_result, JSONResponse): logger.error( "자동 설계 체인 중단(B05 경로 확정 실패): project_id=%s status=%s", @@ -128,8 +129,11 @@ async def run_auto_design_chain(project_id: UUID, surface_model_id: int | None = ) return - # 5) B06 횡단 설계 확정 — 미지정 측점을 기본값으로 채워 저장, stage 3 완료 전이 포함. - sections_result = await confirm_sections(project_id, route_id, None) + # 5) B06 횡단 설계 확정 — 미지정 측점을 기본값으로 채워 저장. stage 3은 + # IN_PROGRESS(스텝바 노란 표시)로 남겨 사용자 검토·확정을 기다린다. + sections_result = await confirm_sections( + project_id, route_id, None, mark_stage_complete=False + ) if isinstance(sections_result, JSONResponse): logger.error( "자동 설계 체인 중단(B06 횡단 확정 실패): project_id=%s route_id=%s status=%s", @@ -292,7 +296,8 @@ async def run_redesign_chain( logger.info("재확정 체인 설계 이월: project_id=%s %d건", project_id, carried) # 4) B05 확정 → B06 확정(옛 표준단면 설정 이월, 미지정 측점 기본값 채움). - confirm_result = await confirm_latest_route(project_id, None) + # 재확정 후에도 stage 2·3은 IN_PROGRESS로 남겨 사용자 재검토를 받는다. + confirm_result = await confirm_latest_route(project_id, None, mark_stage_complete=False) if isinstance(confirm_result, JSONResponse): logger.error( "재확정 체인 중단(B05 확정 실패): project_id=%s status=%s", @@ -306,7 +311,9 @@ async def run_redesign_chain( if old_options.get("standard_cross_section") else None ) - sections_result = await confirm_sections(project_id, new_route_id, section_request) + sections_result = await confirm_sections( + project_id, new_route_id, section_request, mark_stage_complete=False + ) if isinstance(sections_result, JSONResponse): logger.error( "재확정 체인 중단(B06 확정 실패): project_id=%s status=%s", diff --git a/B05_Profile/B05_Profile_Api_Fetch.ts b/B05_Profile/B05_Profile_Api_Fetch.ts index 7eb2751b..eb8052e2 100644 --- a/B05_Profile/B05_Profile_Api_Fetch.ts +++ b/B05_Profile/B05_Profile_Api_Fetch.ts @@ -11,7 +11,7 @@ * - 오류 응답 형식 {status:"error", message:"..."}을 Error로 변환. * ========================================================================== */ -import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; +import { API_ANALYSIS_TIMEOUT_MS, API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; /** 경로 제어점 (BP/EP/CP) */ export interface RoutePoint { @@ -244,17 +244,39 @@ export interface RouteConfirmRequest { uphill_overrides?: Array<{ chainage_m: number; side: "left" | "right" }>; } -/** 프로젝트의 최신 경로를 확정한다. 비정규 측점이 있으면 그 횡단까지 생성한다. */ +/** 프로젝트의 최신 경로를 확정한다. 비정규 측점이 있으면 그 횡단까지 생성한다. + * `markStageComplete=false`는 [임시저장]용 — 데이터(경로 CONFIRMED·비정규 횡단·상단측 + * 병합)는 그대로 저장하되 워크플로 stage 2 완료 전이를 하지 않는다(2026-08-08 재정의). */ export async function confirmRoute( projectId: string, body: RouteConfirmRequest = {}, + markStageComplete = true, ): Promise { - return requestJson(`/projects/${projectId}/route/confirm`, { + const query = markStageComplete ? "" : "?mark_stage_complete=false"; + return requestJson(`/projects/${projectId}/route/confirm${query}`, { method: "POST", body: JSON.stringify(body), }); } +/** [초기화] 응답 — 초기 자동 계산 상태로 재구성된 경로. */ +export interface RouteResetResponse { + status: string; + project_id: string; + route_id: number; + deleted_routes: number; +} + +/** [초기화] — 사용자 편집을 전부 버리고 계획노선 CSV 기본값으로 B05·B06을 재계산한다. + * 경로 재탐색을 포함하므로 분석용 타임아웃을 쓴다. */ +export async function resetRouteDesign(projectId: string): Promise { + return requestJson( + `/projects/${projectId}/route/reset`, + { method: "POST" }, + API_ANALYSIS_TIMEOUT_MS, + ); +} + export async function fetchLatestRoute(projectId: string): Promise { return requestJson(`/projects/${projectId}/route/latest`, { method: "GET", diff --git a/B05_Profile/B05_Profile_Router.py b/B05_Profile/B05_Profile_Router.py index e1a42985..51be0066 100644 --- a/B05_Profile/B05_Profile_Router.py +++ b/B05_Profile/B05_Profile_Router.py @@ -498,12 +498,18 @@ async def read_latest_route(project_id: UUID) -> RouteLatestResponse | JSONRespo @router.post("/{project_id}/route/confirm", response_model=RouteConfirmResponse) async def confirm_latest_route( - project_id: UUID, request: RouteConfirmRequest | None = None + project_id: UUID, + request: RouteConfirmRequest | None = None, + mark_stage_complete: bool = True, ) -> RouteConfirmResponse | JSONResponse: """프로젝트의 최신 경로를 확정(CONFIRMED)한다. 비정규 측점(구조물)이 있으면 확정 시 해당 측점의 횡단을 생성해 종단 파일에 병합한다. 이 생성은 **비치명적**이다 — 실패해도 경로 확정(다음 단계 진행)은 그대로 진행한다. + + `mark_stage_complete=False`는 자동 계산 체인용 — 데이터는 CONFIRMED로 저장하되 + stage 2를 IN_PROGRESS(사용자 검토 대기, 스텝바 노란 표시)로 남긴다. stage 2 완료는 + B06 종횡단 [확정]에서 stage 3과 함께 처리한다(2026-08-08 워크플로우 재정의). """ request = request or RouteConfirmRequest() pool = get_db_pool() @@ -568,15 +574,16 @@ async def confirm_latest_route( next_status="CONFIRMED", ) await confirm_route(connection, latest["id"]) - async with connection.cursor() as cursor: - await complete_stage(cursor, str(project_id), 2) + if mark_stage_complete: + async with connection.cursor() as cursor: + await complete_stage(cursor, str(project_id), 2) await connection.commit() log_b05_debug( logger, "db.route_confirmation.committed", project_id=str(project_id), route_id=latest["id"], - completed_stage=2, + completed_stage=2 if mark_stage_complete else None, ) except Exception as exc: await connection.rollback() @@ -595,3 +602,64 @@ async def confirm_latest_route( status_code=500, content={"status": "error", "message": "경로 확정 처리 중 오류가 발생했습니다."}, ) + + +@router.post("/{project_id}/route/reset") +async def reset_route_design(project_id: UUID) -> JSONResponse: + """B05·B06 설계를 초기 자동 계산 상태로 되돌린다 ([초기화] 버튼, 2026-08-08 재정의). + + 사용자 편집(제어점 이동·계획선 편집·횡단 설계 지정)을 전부 버리고, 계획노선 CSV와 + config 기본값으로 자동 설계 체인을 다시 돌려 파일입력 직후와 같은 상태를 만든다. + 기존 경로 행을 지워야 체인의 수동 이력 보호 가드를 통과하며, 파생 데이터 + (route_points·종횡단·설계 지정)는 FK CASCADE와 재계산이 정리한다. 재계산 뒤 + stage 2·3은 IN_PROGRESS(검토 대기)가 된다. + """ + from B03_FileInput.B03_FileInput_Service_Chain import run_auto_design_chain + from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection + from common_util.common_util_surface_confirmation import surface_confirmation_defaults + + pool = get_db_pool() + try: + async with pool.acquire() as connection: + # 확정 지표면 모델을 초기 체인과 같은 기준(config 기본값)으로 다시 찾는다. + try: + surface_model_id: int | None = await find_surface_model_for_selection( + connection, project_id, surface_confirmation_defaults() + ) + except Exception: + surface_model_id = None + await connection.begin() + try: + async with connection.cursor() as cursor: + await cursor.execute( + "DELETE FROM routes WHERE project_id = %s", (str(project_id),) + ) + deleted = cursor.rowcount + await connection.commit() + except Exception: + await connection.rollback() + raise + + await run_auto_design_chain(project_id, surface_model_id=surface_model_id) + + async with pool.acquire() as connection: + latest = await get_latest_route(connection, project_id) + if not latest: + return JSONResponse( + status_code=500, + content={"status": "error", "message": "초기 경로 재계산에 실패했습니다."}, + ) + return JSONResponse( + content={ + "status": "success", + "project_id": str(project_id), + "route_id": latest["id"], + "deleted_routes": deleted, + } + ) + except Exception: + logger.exception("B05 설계 초기화 실패: project_id=%s", project_id) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "설계 초기화 처리 중 오류가 발생했습니다."}, + ) diff --git a/B05_Profile/B05_Profile_UI_Page.ts b/B05_Profile/B05_Profile_UI_Page.ts index 40f15b64..8cbf16e9 100644 --- a/B05_Profile/B05_Profile_UI_Page.ts +++ b/B05_Profile/B05_Profile_UI_Page.ts @@ -1,4 +1,4 @@ -import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; +import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements"; import { purgeOtherProjects } from "../A00_Common/b_asset_cache"; @@ -16,9 +16,11 @@ import { type SurfaceModelSummary, } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { + clearRouteLatestCache, confirmRoute, fetchLatestRoute, readRouteLatestCache, + resetRouteDesign, writeRouteLatestCache, solveRoute, updateContourInterval, @@ -34,6 +36,7 @@ import { } from "./B05_Profile_UI_Markers"; import { createRoutePanel, type RoutePanelValues } from "./B05_Profile_UI_Panel"; import { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel"; +import { navigateTo } from "../A00_Common/router"; import { createSelectionSync } from "./B05_Profile_UI_Selection"; import { createRouteViewer } from "./B05_Profile_UI_Viewer"; import { @@ -233,7 +236,6 @@ export async function renderB05Route(root: HTMLElement): Promise { let roadWidths = DEFAULT_ROAD_WIDTHS; let currentSectionDetail: SectionDetailResponse | null = null; let routeReady = false; - let stale = false; let restoring = true; let irregularStations: IrregularStation[] = []; // 선택 동기화 재진입 가드(3D↔그래프↔사이드바 상호 갱신의 무한 재귀 차단). @@ -303,7 +305,9 @@ export async function renderB05Route(root: HTMLElement): Promise { const panel = createRoutePanel({ onSolve: () => void solve(), - onConfirm: () => void confirm(), + onTempSave: () => void tempSave(), + onGoCross: () => navigateTo(ROUTES.B06_SECTION), + onReset: () => void resetDesign(), onContourApply: (interval) => void applyContours(interval), onSurfaceVisible: viewer.setSurfaceVisible, onContoursVisible: viewer.setContoursVisible, @@ -334,15 +338,9 @@ export async function renderB05Route(root: HTMLElement): Promise { }, }); - function updateConfirmGate(): void { - panel.setCanConfirm(routeReady && !stale); - } - + /** 입력·마커 변경 시 측점 라인만 다시 그린다 — 확정 게이트는 폐지(B06 통합 확정). */ function markStale(): void { if (restoring || !routeReady) return; - stale = true; - panel.setStale(true); - updateConfirmGate(); if (currentSectionDetail) renderStationLines(currentSectionDetail); } @@ -487,8 +485,6 @@ export async function renderB05Route(root: HTMLElement): Promise { function renderLatest(next: RouteLatestResponse): void { latest = next; routeReady = Boolean(next.route && next.route_points.length > 1); - stale = false; - panel.setStale(false); profilePanel.setRoutePolyline(next.route_points ?? []); if (next.route) { const stored = next.route.algorithm_params ?? {}; @@ -500,7 +496,6 @@ export async function renderB05Route(root: HTMLElement): Promise { }>) ?? [], ); } - updateConfirmGate(); } async function applyContours(interval: number): Promise { @@ -582,37 +577,63 @@ export async function renderB05Route(root: HTMLElement): Promise { } } - async function confirm(): Promise { - if (!routeReady || stale) return; - // 관 매설 지점을 B04와 같은 저장소에 남긴다 — 저장 실패가 경로 확정을 막지는 않는다. + /** [임시저장] — 현재 편집을 영구저장소에 남기되 워크플로 단계·페이지는 그대로 둔다. + * 종·횡 통합 확정은 B06 [확정]이 담당한다(2026-08-08 워크플로우 재정의). */ + async function tempSave(): Promise { + if (!routeReady) return; + // 관 매설 지점을 B04와 같은 저장소에 남긴다 — 저장 실패가 임시저장을 막지는 않는다. await profilePanel.drainage.savePipes().catch(() => 0); showLoadingOverlay(); try { - // 종단 계획선 편집은 화면에서만 계산해 두었으므로 확정 직전에 영속화한다. + // 종단 계획선 편집은 화면에서만 계산해 두었으므로 저장 시점에 영속화한다. await profilePanel.save(); // 서버가 종단 정본의 계획선을 다시 썼다 — 공유 캐시를 비워 B06이 옛 계획선을 못 보게 한다. invalidateSectionDetail(activeProjectId); - // 비정규 측점(구조물)이 있으면 확정 시 그 횡단까지 생성하도록 지표 샘플러 입력을 함께 보낸다. - await confirmRoute(activeProjectId, { - filter_key: latest?.surface_params.source_filter, - method: latest?.surface_params.method, - smooth: latest?.surface_params.smooth, - surface_model_id: confirmedSurface?.id, - irregular_stations: irregularStations.map((station) => ({ - chainage_m: station.chainage_m, - structure: station.structure, - })), - // 상단측(측구 방향) 사용자 변경분 — 종단 정본에 병합되어 B06이 그대로 소비한다. - uphill_overrides: [...uphillOverrides.entries()].map(([chainage, side]) => ({ - chainage_m: Number(chainage), - side, - })), - }); + // 비정규 측점·상단측 변경분까지 데이터로는 확정 저장하되, 단계 완료 전이는 하지 않는다. + await confirmRoute( + activeProjectId, + { + filter_key: latest?.surface_params.source_filter, + method: latest?.surface_params.method, + smooth: latest?.surface_params.smooth, + surface_model_id: confirmedSurface?.id, + irregular_stations: irregularStations.map((station) => ({ + chainage_m: station.chainage_m, + structure: station.structure, + })), + // 상단측(측구 방향) 사용자 변경분 — 종단 정본에 병합되어 B06이 그대로 소비한다. + uphill_overrides: [...uphillOverrides.entries()].map(([chainage, side]) => ({ + chainage_m: Number(chainage), + side, + })), + }, + false, + ); renderLatest(await loadLatest(true)); - showToast("경로를 확정했습니다.", "success"); - goToWorkflowStage(activeProjectId, WORKFLOW_STEP_ROUTES[3]); + showToast(L("B05_Route_TempSave_Success"), "success"); } catch (error) { - showToast(error instanceof Error ? error.message : "경로 확정에 실패했습니다.", "error"); + showToast( + error instanceof Error ? error.message : L("B05_Route_TempSave_Failed"), + "error", + ); + } finally { + hideLoadingOverlay(); + } + } + + /** [초기화] — 사용자 편집 전부 폐기, 계획노선 CSV 기본값으로 B05·B06 재계산 후 재진입. */ + async function resetDesign(): Promise { + if (!window.confirm(L("B05_Route_Reset_Confirm"))) return; + showLoadingOverlay(); + try { + await resetRouteDesign(activeProjectId); + // 옛 경로 기준 캐시를 전부 비우고 페이지를 새로 그린다 — 초기 계산본이 정본이 된다. + clearRouteLatestCache(activeProjectId); + invalidateSectionDetail(activeProjectId); + showToast(L("B05_Route_Reset_Success"), "success"); + navigateTo(ROUTES.B05_PROFILE); + } catch (error) { + showToast(error instanceof Error ? error.message : L("B05_Route_Reset_Failed"), "error"); } finally { hideLoadingOverlay(); } diff --git a/B05_Profile/B05_Profile_UI_Panel.ts b/B05_Profile/B05_Profile_UI_Panel.ts index b8be62af..a74a278e 100644 --- a/B05_Profile/B05_Profile_UI_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Panel.ts @@ -46,7 +46,12 @@ const PROFILE_CRITERIA: Record< interface PanelCallbacks { onSolve: () => void; - onConfirm: () => void; + /** [임시저장] — 현재 편집(계획선 델타·관로·비정규 측점·상단측)을 확정 전이 없이 저장. */ + onTempSave: () => void; + /** [횡단 이동] — 저장 없이 B06 횡단 페이지로 이동만. */ + onGoCross: () => void; + /** [초기화] — 사용자 편집을 버리고 초기 자동 계산 상태로 롤백. */ + onReset: () => void; onContourApply: (interval: number) => void; onSurfaceVisible: (visible: boolean) => void; onContoursVisible: (visible: boolean) => void; @@ -190,8 +195,11 @@ export function createRoutePanel(callbacks: PanelCallbacks) { ); contour.body.append(contourRow); - const palette = section("포인트 팔레트"); - palette.root.classList.add("is-collapsed"); + // 포인트 팔레트 + 임도 기준·옵션을 한 컨테이너로 병합하고 [최적 경로 계산]도 이 안에 + // 둔다 — 경로 재탐색은 B04~B06 재계산을 부르는 무거운 작업이라 가끔만 쓴다 + // (2026-08-08 사용자 지시). + const routeCalc = section("경로 계산 설정"); + routeCalc.root.classList.add("is-collapsed"); const paletteGrid = document.createElement("div"); paletteGrid.className = "b05-route__palette"; const pointLabels: Record = { @@ -209,7 +217,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) { chip.addEventListener("dragstart", (event) => event.dataTransfer?.setData("pointType", kind)); paletteGrid.append(chip); }); - palette.body.append(paletteGrid); + routeCalc.body.append(paletteGrid); const selected = section("선택 포인트 상세 설정"); selected.root.hidden = true; @@ -224,8 +232,6 @@ export function createRoutePanel(callbacks: PanelCallbacks) { ); selected.body.append(selectedName, radius.wrapper, selectedActions); - const conditions = section("임도 기준·옵션"); - conditions.root.classList.add("is-collapsed"); // 드롭다운은 공통 컴포넌트(createSelectField) 재사용. `.select`로 받아 이하 로직 불변. const algorithmField = createSelectField({ label: "알고리즘", @@ -262,12 +268,14 @@ export function createRoutePanel(callbacks: PanelCallbacks) { minUphillGrade.wrapper, minDownhillGrade.wrapper, ); - conditions.body.append( + const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled"); + routeCalc.body.append( algorithmField.root, gradeField.root, paved.wrapper, avoidPass.wrapper, details, + solveButton, ); const sectionOptions = section(L("B05_Route_Group_SectionOptions")); @@ -358,13 +366,15 @@ export function createRoutePanel(callbacks: PanelCallbacks) { terrainType.addEventListener("change", syncCriteria); syncCriteria(); - const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled"); - const confirmButton = button("경로 확정", callbacks.onConfirm); - confirmButton.disabled = true; + // 하단 고정 액션 행: [초기화][임시저장][횡단 이동] — 경로 확정 개념 폐지, 종·횡 통합 + // 확정은 B06에서 한다(2026-08-08 워크플로우 재정의). + const resetButton = button(L("Common_Btn_Reset"), callbacks.onReset, "danger"); + const tempSaveButton = button(L("B05_Route_Btn_TempSave"), callbacks.onTempSave); + const goCrossButton = button(L("B05_Route_Btn_GoCross"), callbacks.onGoCross, "filled"); const actionRow = document.createElement("div"); // 사이드 최하단 고정(공용 ui-sidebar-actions) — 스크롤에서 제외(2026-08-05 사용자 지시). actionRow.className = "b05-route__actions ui-sidebar-actions"; - actionRow.append(solveButton, confirmButton); + actionRow.append(resetButton, tempSaveButton, goCrossButton); const inputElements = [ algorithm, @@ -392,9 +402,8 @@ export function createRoutePanel(callbacks: PanelCallbacks) { contour.root, sectionOptions.root, irregular.root, - palette.root, + routeCalc.root, selected.root, - conditions.root, actionRow, ); @@ -466,12 +475,6 @@ export function createRoutePanel(callbacks: PanelCallbacks) { radius.wrapper.hidden = point.type !== "ap" && point.type !== "fp"; radius.value = String(point.radius_m ?? 25); }, - setStale(value: boolean) { - confirmButton.disabled = value; - }, - setCanConfirm(value: boolean) { - confirmButton.disabled = !value; - }, }; } diff --git a/B06_Section/B06_Section_Router_Confirm.py b/B06_Section/B06_Section_Router_Confirm.py index 8af3ef11..bb603b3b 100644 --- a/B06_Section/B06_Section_Router_Confirm.py +++ b/B06_Section/B06_Section_Router_Confirm.py @@ -19,6 +19,7 @@ from fastapi import APIRouter, Body from fastapi.responses import JSONResponse from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path +from B05_Profile.B05_Profile_Repository import confirm_route as confirm_route_status from B05_Profile.B05_Profile_Router_Confirm import _merge_uphill_overrides_into_longitudinal from B06_Section.B06_Section_Repository import ( confirm_sections_for_route, @@ -36,7 +37,7 @@ from B06_Section.B06_Section_Schema import ( SectionConfirmResponse, ) from common_util.common_util_storage import resolve_stored_project_path -from common_util.common_util_workflow_state import complete_stage +from common_util.common_util_workflow_state import complete_stage, start_stage from config.config_db import get_db_pool logger = logging.getLogger(__name__) @@ -128,11 +129,16 @@ async def confirm_sections( project_id: UUID, route_id: int, request: SectionConfirmRequest | None = Body(default=None), + mark_stage_complete: bool = True, ) -> SectionConfirmResponse | JSONResponse: """경로의 종횡단면을 확정(CONFIRMED)한다. 지반유형을 지정하지 않은 측점은 기본값(토사/좌절토)으로 자동 채운 뒤 확정한다. 표준 횡단면 설정값이 함께 오면 longitudinal_sections.data.options에 저장한다. + + B05·B06은 한 흐름이라 사용자 [확정]은 stage 2(종단)와 3(횡단)을 함께 닫는다 + (B05 단독 확정 폐지, 2026-08-08 워크플로우 재정의). `mark_stage_complete=False`는 + 자동 계산 체인용 — 데이터만 확정하고 stage 3을 IN_PROGRESS(검토 대기)로 남긴다. """ pool = get_db_pool() try: @@ -166,7 +172,14 @@ async def confirm_sections( ) await confirm_sections_for_route(connection, route_id) async with connection.cursor() as cursor: - await complete_stage(cursor, str(project_id), 3) + if mark_stage_complete: + # B05 단독 확정 폐지 보완 — 재탐색 후 임시저장 없이 바로 확정해도 + # 경로 상태(DRAFT)가 남지 않도록 여기서 함께 CONFIRMED로 닫는다. + await confirm_route_status(connection, route_id) + await complete_stage(cursor, str(project_id), 2) + await complete_stage(cursor, str(project_id), 3) + else: + await start_stage(cursor, str(project_id), 3) await connection.commit() except Exception: await connection.rollback() diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 212b33e6..148d9031 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -1,4 +1,5 @@ -import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; +import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend"; +import { navigateTo } from "../A00_Common/router"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { createButton, @@ -91,7 +92,14 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { crossHalfWidthField.input.min = "0.1"; crossHalfWidthField.input.step = "0.1"; - // 임시 저장 — 확정과 같은 내용을 남기되 페이지 이동이 없다(2026-08-02 사용자 지시). + // 하단 액션 [종단 이동][임시저장][확정] — 종단 이동은 저장 없이 B05로 페이지 이동만, + // 확정은 임시저장과 같은 내용 저장 후 stage 2·3을 닫고 B07 수량으로 넘어간다 + // (2026-08-08 워크플로우 재정의). + const goProfileButton = createButton({ + label: L("B06_Profile_Btn_GoProfile"), + variant: "ghost", + onClick: () => navigateTo(ROUTES.B05_PROFILE), + }); const saveButton = createButton({ label: L("B06_Profile_Btn_Save"), variant: "ghost", @@ -108,7 +116,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { const actionRow = document.createElement("div"); // 사이드 최하단 고정(공용 ui-sidebar-actions) — 스크롤에서 제외(2026-08-05 사용자 지시). actionRow.className = "b06-profile__actions ui-sidebar-actions"; - actionRow.append(saveButton, confirmButton); + actionRow.append(goProfileButton, saveButton, confirmButton); const leftForm = document.createElement("div"); leftForm.className = "b06-profile__form"; diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index b606ea79..8ec12aa9 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -124,6 +124,23 @@ export const ui_locales_b2 = { B05_Route_Field_Smooth: ["경로 스무딩", "Smooth route"], B05_Route_Btn_Solve: ["경로 탐색 실행", "Solve Route"], B05_Route_Btn_Confirm: ["경로 확정", "Confirm Route"], + /* 2026-08-08 워크플로우 재정의 — B05 하단 액션 [초기화][임시저장][횡단 이동] */ + B05_Route_Btn_TempSave: ["임시저장", "Save Draft"], + B05_Route_Btn_GoCross: ["횡단 이동", "Go to Cross"], + B05_Route_TempSave_Success: [ + "현재 편집 내용을 임시 저장했습니다.", + "Current edits saved as draft.", + ], + B05_Route_TempSave_Failed: ["임시 저장에 실패했습니다.", "Failed to save draft."], + B05_Route_Reset_Confirm: [ + "초기 자동 계산 상태로 되돌립니다. 사용자 수정 내용(경로·계획선·횡단 설계)이 모두 사라지고 재계산에 시간이 걸립니다. 계속할까요?", + "Reset to the initial auto-computed state? All user edits (route, profile, cross design) will be discarded and recomputation takes a while.", + ], + B05_Route_Reset_Success: [ + "초기 계산 상태로 되돌렸습니다.", + "Reset to the initial computed state.", + ], + B05_Route_Reset_Failed: ["초기화에 실패했습니다.", "Failed to reset."], B05_Route_Result_Title: ["경로 탐색 결과", "Route Result"], B05_Route_Result_Empty: ["아직 계산된 경로가 없습니다.", "No route computed yet."], B05_Route_Result_Length: ["총 연장(m)", "Total length (m)"], @@ -162,9 +179,11 @@ export const ui_locales_b2 = { B06_Profile_Field_Smooth: ["지표면 스무딩", "Smooth surface"], B06_Profile_Smooth_On: ["사용", "On"], B06_Profile_Smooth_Off: ["미사용", "Off"], - B06_Profile_Btn_Confirm: ["종·횡단 확정", "Confirm Sections"], + /* 2026-08-08 재정의 — 확정은 종+횡 통합(stage 2·3 함께 완료), 종단 이동은 저장 없이 B05로. */ + B06_Profile_Btn_Confirm: ["확정", "Confirm"], + B06_Profile_Btn_GoProfile: ["종단 이동", "Go to Profile"], /* 임시 저장 — 확정과 같은 내용을 저장하되 경로 상태·워크플로 단계는 건드리지 않는다. */ - B06_Profile_Btn_Save: ["임시 저장", "Save draft"], + B06_Profile_Btn_Save: ["임시저장", "Save draft"], B06_Profile_Btn_Save_Tip: [ "지금까지 편집한 내용을 확정하지 않고 저장합니다. 페이지는 그대로 있습니다.", "Saves the current edits without confirming. You stay on this page.",