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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "설계 초기화 처리 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user