Files
Aislo/B04_wf1_Surface/B04_wf1_Surface_UI_Page.ts
T
2026-07-05 21:27:23 +09:00

251 lines
8.4 KiB
TypeScript

/* =============================================================================
* 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<string> } {
const selected = new Set<string>(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<void> {
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<void> {
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<void> {
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);
}