/* ============================================================================= * B06_wf3_ProfileCross_UI_Page.ts * 로그인 후 06: 3차 워크플로우 (종·횡단 생성) * * 3단 레이아웃 (frontend.md §2): * 상단: 페이지 타이틀 + 진행 단계 스텝바 (createWorkflowShell) * 좌측: 대상 경로 ID + 지표면 참조 + 측점/횡단 옵션 폼 * 우측: 종·횡단 생성 결과(연장·횡단 개수·파일) 카드 * * 이벤트 핸들러 명명 (frontend.md §4): onB06_Profile_[기능]_[액션] * 텍스트는 ui_template_locale에 선(先) 등록 후 참조 (frontend.md §3). * ========================================================================== */ import { CURRENT_PROJECT_ID_KEY, ROUTES, type RoutePath } from "@config/config_frontend"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { createButton, createInputField, hideLoadingOverlay, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { createWorkflowLayout, type WorkflowStage } from "@ui/ui_template_workflow_layout"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; import { navigateTo } from "../A00_Common/router"; import { confirmSections, generateSections, type SectionGenerateResponse, } from "./B06_wf3_ProfileCross_Api_Fetch"; import "./B06_wf3_ProfileCross_UI_Style.css"; /** locale 헬퍼 */ function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } /** 라벨 + fieldset 그룹 컨테이너 생성. */ function buildGroup(legend: string): HTMLElement { const group = document.createElement("fieldset"); group.className = "b06-profile__group"; const legendEl = document.createElement("legend"); legendEl.className = "b06-profile__group-legend"; legendEl.textContent = legend; group.append(legendEl); return group; } /** 숫자 입력값을 파싱. 빈 값이면 null. */ function parseNumber(value: string): number | null { const trimmed = value.trim(); if (!trimmed) return null; const parsed = Number(trimmed); return Number.isFinite(parsed) ? parsed : null; } export function renderB06ProfileCross(root: HTMLElement): void { let currentRouteId: number | null = null; /* ---- 좌측: 대상 경로 ---- */ const routeGroup = buildGroup(L("B06_Profile_Group_Route")); const routeIdField = createInputField({ label: L("B06_Profile_Field_RouteId"), type: "number", min: 1, }); const filterField = createInputField({ label: L("B06_Profile_Field_Filter"), type: "text" }); const methodField = createInputField({ label: L("B06_Profile_Field_Method"), type: "text", value: "dtm", }); const crsField = createInputField({ label: L("B06_Profile_Field_Crs"), type: "text", placeholder: "EPSG:5178", }); routeGroup.append(routeIdField.root, filterField.root, methodField.root, crsField.root); /* ---- 좌측: 측점/횡단 옵션 ---- */ const optionGroup = buildGroup(L("B06_Profile_Group_Options")); const stationField = createInputField({ label: L("B06_Profile_Field_StationInterval"), type: "number", }); const halfWidthField = createInputField({ label: L("B06_Profile_Field_CrossHalfWidth"), type: "number", }); const crossSampleField = createInputField({ label: L("B06_Profile_Field_CrossSample"), type: "number", }); const longSampleField = createInputField({ label: L("B06_Profile_Field_LongSample"), type: "number", }); const smoothLabel = document.createElement("label"); smoothLabel.className = "b06-profile__check"; const smoothBox = document.createElement("input"); smoothBox.type = "checkbox"; const smoothText = document.createElement("span"); smoothText.textContent = L("B06_Profile_Field_Smooth"); smoothLabel.append(smoothBox, smoothText); optionGroup.append( stationField.root, halfWidthField.root, crossSampleField.root, longSampleField.root, smoothLabel, ); const generateButton = createButton({ label: L("B06_Profile_Btn_Generate"), variant: "filled", onClick: () => void onB06_Profile_Generate_Click(), }); const confirmButton = createButton({ label: L("B06_Profile_Btn_Confirm"), variant: "ghost", onClick: () => void onB06_Profile_Confirm_Click(), }); const actionRow = document.createElement("div"); actionRow.className = "b06-profile__actions"; actionRow.append(generateButton, confirmButton); const leftForm = document.createElement("div"); leftForm.className = "b06-profile__form"; leftForm.append(routeGroup, optionGroup, actionRow); /* ---- 우측: 결과 ---- */ const resultTitle = document.createElement("h3"); resultTitle.className = "b06-profile__result-title"; resultTitle.textContent = L("B06_Profile_Result_Title"); const resultBody = document.createElement("div"); resultBody.className = "b06-profile__result-body"; function renderEmptyResult(): void { resultBody.replaceChildren(); const empty = document.createElement("p"); empty.className = "b06-profile__empty"; empty.textContent = L("B06_Profile_Result_Empty"); resultBody.append(empty); } function metricRow(label: string, value: string): HTMLElement { const row = document.createElement("div"); row.className = "b06-profile__metric"; const key = document.createElement("span"); key.className = "b06-profile__metric-key"; key.textContent = label; const val = document.createElement("span"); val.className = "b06-profile__metric-val"; val.textContent = value; row.append(key, val); return row; } function renderResult(result: SectionGenerateResponse): void { resultBody.replaceChildren(); resultBody.append( metricRow(L("B06_Profile_Result_Length"), result.length_m.toFixed(2)), metricRow(L("B06_Profile_Result_CrossCount"), String(result.cross_section_count)), metricRow(L("B06_Profile_Result_Path"), result.longitudinal_file_path), ); } const resultCard = document.createElement("div"); resultCard.className = "b06-profile__result"; resultCard.append(resultTitle, resultBody); /* ---- 이벤트 핸들러 ---- */ function getProjectId(): string | null { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); if (!projectId) showToast(L("B06_Profile_Error_Project"), "error"); return projectId; } async function onB06_Profile_Generate_Click(): Promise { const projectId = getProjectId(); if (!projectId) return; const routeId = parseNumber(routeIdField.input.value); if (routeId === null || !Number.isInteger(routeId) || routeId <= 0) { routeIdField.setError(L("B06_Profile_Error_RouteId")); return; } routeIdField.setError(); const filterKey = filterField.input.value.trim(); if (!filterKey) { filterField.setError(L("B06_Profile_Error_Filter")); return; } filterField.setError(); showLoadingOverlay(); try { const result = await generateSections(projectId, { route_id: routeId, filter_key: filterKey, method: methodField.input.value.trim() || "dtm", smooth: smoothBox.checked, crs: crsField.input.value.trim() || null, station_interval_m: parseNumber(stationField.input.value), cross_half_width_m: parseNumber(halfWidthField.input.value), cross_sample_interval_m: parseNumber(crossSampleField.input.value), long_sample_interval_m: parseNumber(longSampleField.input.value), }); currentRouteId = result.route_id; renderResult(result); showToast(L("B06_Profile_Generate_Success"), "success"); } catch (error) { const detail = error instanceof Error ? error.message : L("B06_Profile_Generate_Failed"); showToast(`${L("B06_Profile_Generate_Failed")} ${detail}`, "error"); } finally { hideLoadingOverlay(); } } async function onB06_Profile_Confirm_Click(): Promise { const projectId = getProjectId(); if (!projectId) return; const routeId = currentRouteId ?? parseNumber(routeIdField.input.value); if (routeId === null || !Number.isInteger(routeId) || routeId <= 0) { routeIdField.setError(L("B06_Profile_Error_RouteId")); return; } showLoadingOverlay(); try { await confirmSections(projectId, routeId); showToast(L("B06_Profile_Confirm_Success"), "success"); } catch (error) { const detail = error instanceof Error ? error.message : L("B06_Profile_Confirm_Failed"); showToast(`${L("B06_Profile_Confirm_Failed")} ${detail}`, "error"); } finally { hideLoadingOverlay(); } } renderEmptyResult(); const workflowRoutes = [ ROUTES.B03_FILE_INPUT, ROUTES.B04_WF1_SURFACE, ROUTES.B05_WF2_ROUTE, ROUTES.B06_WF3_PROFILE_CROSS, ROUTES.B07_WF4_DESIGN_DETAIL, ROUTES.B08_WF5_QUANTITY, ROUTES.B09_WF6_ESTIMATION, ]; let workflowStages: WorkflowStage[] = []; const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); if (projectId) { fetch(`/api/projects/${projectId}/workflow-state`) .then((res) => res.json()) .then((data) => { workflowStages = data.stages || []; }) .catch(() => { /* silently fail */ }); } const layout = createWorkflowLayout({ title: L("B06_Profile_Title"), steps: workflowSteps(), activeStep: 3, leftPanel: leftForm, mainContent: resultCard, stages: workflowStages, routes: workflowRoutes, onStepClick: (_stepIndex, route) => { if (projectId) { localStorage.setItem(CURRENT_PROJECT_ID_KEY, projectId); navigateTo(route as RoutePath); } }, }); root.replaceChildren(layout.root); }