/* ============================================================================= * B04_wf1_Surface_UI_Page.ts * 로그인 후 04: 1차 워크플로우 (지표면 모델 분석) * * 3단 레이아웃 (frontend.md §2): * 상단: 페이지 타이틀 + 진행 단계 스텝바 (createWorkflowShell) * 좌측: 입력 파일 ID + 지면 필터/지표면 표현 선택 + 실행 옵션 폼 * 우측: 생성된 지표면 모델 목록 그리드 * * 이벤트 핸들러 명명 (frontend.md §4): onB04_Surface_[기능]_[액션] * 텍스트는 ui_template_locale에 선(先) 등록 후 참조 (frontend.md §3). * ========================================================================== */ import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { createButton, createInputField, createTag, createWorkflowShell, hideLoadingOverlay, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; import { analyzeSurface, listSurfaceModels, type SurfaceModelSummary, } from "./B04_wf1_Surface_Api_Fetch"; import "./B04_wf1_Surface_UI_Style.css"; /** locale 헬퍼 */ function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } /** 선택 가능한 지면 필터 (config_system.SURFACE_MODEL_SOURCE_FILTERS + ransac) */ const SOURCE_FILTERS = ["grid_min_z", "csf", "pmf", "ransac"] as const; /** 선택 가능한 지표면 표현 (config_system.SURFACE_MODEL_PRECOMPUTE) */ const MODEL_METHODS = ["tin", "dtm", "nurbs", "implicit", "meshfree"] as const; /** 체크박스 그룹 하나 생성 (라벨 + 항목들). 선택 값 Set을 반환. */ function buildCheckboxGroup( legend: string, values: readonly string[], defaults: readonly string[], ): { root: HTMLElement; selected: Set } { const selected = new Set(defaults); const root = document.createElement("fieldset"); root.className = "b04-surface__group"; const legendEl = document.createElement("legend"); legendEl.className = "b04-surface__group-legend"; legendEl.textContent = legend; root.append(legendEl); for (const value of values) { const item = document.createElement("label"); item.className = "b04-surface__check"; const box = document.createElement("input"); box.type = "checkbox"; box.value = value; box.checked = selected.has(value); box.addEventListener("change", () => { if (box.checked) selected.add(value); else selected.delete(value); }); const text = document.createElement("span"); text.textContent = value; item.append(box, text); root.append(item); } return { root, selected }; } export function renderB04Surface(root: HTMLElement): void { const shell = createWorkflowShell({ title: L("B04_Surface_Title"), steps: workflowSteps(), activeStep: 0, }); /* ---- 좌측 입력 패널 ---- */ const inputFileField = createInputField({ label: L("B04_Surface_Field_InputId"), type: "number", min: 1, placeholder: L("B04_Surface_Field_InputId_Placeholder"), }); const filterGroup = buildCheckboxGroup(L("B04_Surface_Group_Filters"), SOURCE_FILTERS, [ "grid_min_z", "csf", "pmf", ]); const methodGroup = buildCheckboxGroup(L("B04_Surface_Group_Methods"), MODEL_METHODS, [ "dtm", "tin", ]); const forceLabel = document.createElement("label"); forceLabel.className = "b04-surface__check"; const forceBox = document.createElement("input"); forceBox.type = "checkbox"; const forceText = document.createElement("span"); forceText.textContent = L("B04_Surface_Field_Force"); forceLabel.append(forceBox, forceText); const analyzeButton = createButton({ label: L("B04_Surface_Btn_Analyze"), variant: "filled", onClick: () => void onB04_Surface_Analyze_Click(), }); const leftForm = document.createElement("div"); leftForm.className = "b04-surface__form"; leftForm.append( inputFileField.root, filterGroup.root, methodGroup.root, forceLabel, analyzeButton, ); shell.leftPanel.append(leftForm); /* ---- 우측 결과 영역 ---- */ const resultHeader = document.createElement("div"); resultHeader.className = "b04-surface__result-head"; const resultTitle = document.createElement("h3"); resultTitle.textContent = L("B04_Surface_Result_Title"); const refreshButton = createButton({ label: L("B04_Surface_Btn_Refresh"), variant: "ghost", onClick: () => void onB04_Surface_Refresh_Click(), }); resultHeader.append(resultTitle, refreshButton); const modelList = document.createElement("div"); modelList.className = "b04-surface__models"; const resultArea = document.createElement("div"); resultArea.className = "b04-surface__result"; resultArea.append(resultHeader, modelList); shell.rightArea.append(resultArea); function renderModels(models: readonly SurfaceModelSummary[]): void { modelList.replaceChildren(); if (models.length === 0) { const empty = document.createElement("p"); empty.className = "b04-surface__empty"; empty.textContent = L("B04_Surface_Result_Empty"); modelList.append(empty); return; } for (const model of models) { const card = document.createElement("div"); card.className = "b04-surface__model-card"; const head = document.createElement("div"); head.className = "b04-surface__model-head"; const type = document.createElement("strong"); type.textContent = model.model_type; const variant = model.status === "CONFIRMED" ? "success" : model.status === "FAILED" ? "danger" : "neutral"; head.append(type, createTag(model.status, variant)); const meta = document.createElement("div"); meta.className = "b04-surface__model-meta"; const resolution = document.createElement("span"); resolution.textContent = `${L("B04_Surface_Model_Resolution")}: ${ model.resolution_m ?? "-" }`; const path = document.createElement("span"); path.textContent = `${L("B04_Surface_Model_Path")}: ${model.model_file_path ?? "-"}`; meta.append(resolution, path); card.append(head, meta); modelList.append(card); } } function getProjectId(): string | null { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); if (!projectId) { inputFileField.setError(L("B04_Surface_Error_Project")); showToast(L("B04_Surface_Error_Project"), "error"); } return projectId; } async function onB04_Surface_Analyze_Click(): Promise { const projectId = getProjectId(); if (!projectId) return; const rawId = inputFileField.input.value.trim(); const inputFileId = Number(rawId); if (!rawId || !Number.isInteger(inputFileId) || inputFileId <= 0) { inputFileField.setError(L("B04_Surface_Error_InputId")); return; } if (filterGroup.selected.size === 0 || methodGroup.selected.size === 0) { inputFileField.setError(L("B04_Surface_Error_Selection")); return; } inputFileField.setError(); showLoadingOverlay(); try { const response = await analyzeSurface(projectId, { input_file_id: inputFileId, source_filters: [...filterGroup.selected], methods: [...methodGroup.selected], force: forceBox.checked, }); showToast( `${L("B04_Surface_Analyze_Success")} (${response.surface_model_ids.length})`, "success", ); await loadModels(projectId); } catch (error) { const detail = error instanceof Error ? error.message : L("B04_Surface_Analyze_Failed"); inputFileField.setError(`${L("B04_Surface_Analyze_Failed")} ${detail}`); showToast(L("B04_Surface_Analyze_Failed"), "error"); } finally { hideLoadingOverlay(); } } async function loadModels(projectId: string): Promise { showLoadingOverlay(); try { const response = await listSurfaceModels(projectId); renderModels(response.models); } catch { showToast(L("B04_Surface_Load_Failed"), "error"); } finally { hideLoadingOverlay(); } } async function onB04_Surface_Refresh_Click(): Promise { const projectId = getProjectId(); if (projectId) await loadModels(projectId); } renderModels([]); root.replaceChildren(shell.root); const initialProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); if (initialProjectId) void loadModels(initialProjectId); }