feat(B05,B06): 워크플로우 재정의 — 자동 계산 노란 상태·버튼 개편·초기화

자동 체인 상태 전이 분리:
- confirm_latest_route/confirm_sections에 mark_stage_complete 플래그 추가
- 자동 체인(신규·재확정)은 데이터만 확정하고 stage 2·3을 IN_PROGRESS로 남김
  (스텝바 기존 노란 테두리 = 계산됨·확정 전 상태로 재사용)
- B06 [확정]이 stage 2+3 동시 완료 + 경로 상태 CONFIRMED 보강 후 B07 수량 이동

B05 좌측 패널 개편:
- 포인트 팔레트 + 임도 기준·옵션을 경로 계산 설정 한 컨테이너로 병합,
  [최적 경로 계산] 버튼을 컨테이너 내부로 이동 (가끔 쓰는 무거운 재계산)
- 하단 액션 행 [초기화][임시저장][횡단 이동]으로 교체, 경로 단독 확정 폐지
- [임시저장] = 관로 + 계획선 델타 + 비정규 측점·상단측 저장(mark_stage_complete=false)
- [횡단 이동]/B06 [종단 이동] = 저장 없이 페이지 이동만 (B05·B06 캐시 공유 구조 유지)
- [초기화] = POST /route/reset 신설: 기존 경로 삭제 후 계획노선 CSV 기본값으로
  자동 체인 재실행(파일입력 직후 상태 복원), 분석용 타임아웃 적용

B06 액션 행: [종단 이동][임시저장][확정] 3버튼 구성, locale 키 정비

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 10:26:45 +09:00
co-authored by Claude Fable 5
parent ec9943417c
commit 98df22f789
8 changed files with 236 additions and 75 deletions
+13 -6
View File
@@ -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",
+25 -3
View File
@@ -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<RouteConfirmResponse> {
return requestJson<RouteConfirmResponse>(`/projects/${projectId}/route/confirm`, {
const query = markStageComplete ? "" : "?mark_stage_complete=false";
return requestJson<RouteConfirmResponse>(`/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<RouteResetResponse> {
return requestJson<RouteResetResponse>(
`/projects/${projectId}/route/reset`,
{ method: "POST" },
API_ANALYSIS_TIMEOUT_MS,
);
}
export async function fetchLatestRoute(projectId: string): Promise<RouteLatestResponse> {
return requestJson<RouteLatestResponse>(`/projects/${projectId}/route/latest`, {
method: "GET",
+72 -4
View File
@@ -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": "설계 초기화 처리 중 오류가 발생했습니다."},
)
+57 -36
View File
@@ -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<void> {
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<void> {
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<void> {
},
});
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<void> {
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<void> {
}>) ?? [],
);
}
updateConfirmGate();
}
async function applyContours(interval: number): Promise<void> {
@@ -582,37 +577,63 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
}
}
async function confirm(): Promise<void> {
if (!routeReady || stale) return;
// 관 매설 지점을 B04와 같은 저장소에 남긴다 — 저장 실패가 경로 확정을 막지는 않는다.
/** [임시저장] — 현재 편집을 영구저장소에 남기되 워크플로 단계·페이지는 그대로 둔다.
* 종·횡 통합 확정은 B06 [확정]이 담당한다(2026-08-08 워크플로우 재정의). */
async function tempSave(): Promise<void> {
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<void> {
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();
}
+22 -19
View File
@@ -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<RoutePointKind, string> = {
@@ -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;
},
};
}
+15 -2
View File
@@ -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()
+11 -3
View File
@@ -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<void> {
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<void> {
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";
+21 -2
View File
@@ -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.",