252 lines
8.6 KiB
TypeScript
252 lines
8.6 KiB
TypeScript
/* =============================================================================
|
|
* 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 } 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 } from "@ui/ui_template_workflow_layout";
|
|
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
|
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<void> {
|
|
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<void> {
|
|
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 layout = createWorkflowLayout({
|
|
title: L("B06_Profile_Title"),
|
|
steps: workflowSteps(),
|
|
activeStep: 3,
|
|
leftPanel: leftForm,
|
|
mainContent: resultCard,
|
|
});
|
|
root.replaceChildren(layout.root);
|
|
}
|