refactor(B04): 전처리 화면 자체 변종을 부품 상자 부품으로 — 켜기 칩 · 분할 고르기 · 고르기 · 슬라이더 · 값 목록 · 카드 · 도구줄 (PLAN 553-1)
- 지도 층 단추 열다섯 안팎 · 표시 토글 셋 · 라이다 겹침 = 켜기 칩(층 색 점) · 도엽 보간 방식 = 분할 고르기 · 도구줄 부품 - 고르기 38 → 고르기 부품 · 점 크기 · 밀도 = 슬라이더 · 입력 파일 정보 = 값 목록 · 초기화 = 위험 고스트 · 묶음 상자 = 카드 + 패널 머리 - 죽은 CSS 열여섯 걷음 · 날 색 → 공통 값 · 가이드 촬영 선택자 · 지킴 시험 test_553_1_pages 늘림 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,17 @@ import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
createSelectField,
|
||||
createTag,
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createKv, type KvRow } from "@ui/ui_template_kv";
|
||||
import { createSegment, type SegmentHandle } from "@ui/ui_template_segment";
|
||||
import { createToggle } from "@ui/ui_template_toggle";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
||||
import { clearPreloadMark, purgeOtherProjects } from "../A00_Common/b_asset_cache";
|
||||
@@ -35,6 +40,7 @@ import {
|
||||
type SurfacePointCloudSampleResponse,
|
||||
type SurfaceStatusResponse,
|
||||
} from "./B04_PreProcess_Api_Fetch";
|
||||
import { buildSurfaceGroup } from "./B04_PreProcess_UI_Group";
|
||||
import { createSurfaceMapViewer } from "./B04_PreProcess_UI_MapViewer";
|
||||
import { createSurfaceTerrainViewer } from "./B04_PreProcess_UI_TerrainViewer";
|
||||
import { createSurfacePointCloudViewer } from "./B04_PreProcess_UI_Viewer";
|
||||
@@ -56,15 +62,8 @@ function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
function buildInfoLine(label: string, value: unknown): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "b04-surface__line";
|
||||
const key = document.createElement("span");
|
||||
key.textContent = label;
|
||||
const val = document.createElement("strong");
|
||||
val.textContent = value == null || value === "" ? "-" : String(value);
|
||||
row.append(key, val);
|
||||
return row;
|
||||
function infoRow(label: string, value: unknown): KvRow {
|
||||
return { label, value: value == null || value === "" ? "-" : String(value) };
|
||||
}
|
||||
|
||||
/** 라벨 한 줄 + 드롭다운. 지표면 분석 컨테이너 안의 항목 하나를 만든다. */
|
||||
@@ -73,35 +72,19 @@ function buildSelectField(
|
||||
values: readonly string[],
|
||||
defaultValue: string,
|
||||
): { root: HTMLElement; select: HTMLSelectElement } {
|
||||
const root = document.createElement("label");
|
||||
root.className = "b04-surface__field";
|
||||
const caption = document.createElement("span");
|
||||
caption.textContent = label;
|
||||
const select = document.createElement("select");
|
||||
select.className = "b04-surface__select";
|
||||
values.forEach((value) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.textContent = value.replaceAll("_", " ").toUpperCase();
|
||||
select.append(option);
|
||||
const { root, select } = createSelectField({
|
||||
label,
|
||||
options: values.map((value) => ({
|
||||
value,
|
||||
text: value.replaceAll("_", " ").toUpperCase(),
|
||||
})),
|
||||
value: defaultValue,
|
||||
size: 36,
|
||||
});
|
||||
select.value = defaultValue;
|
||||
root.append(caption, select);
|
||||
root.classList.add("b04-surface__field");
|
||||
return { root, select };
|
||||
}
|
||||
|
||||
/** 제목이 달린 사이드 패널 컨테이너. */
|
||||
function buildGroup(title: string, ...children: HTMLElement[]): HTMLElement {
|
||||
const root = document.createElement("section");
|
||||
// ui-sidebar-section: 사이드 컨테이너 공통 외곽선(진하게, 2026-08-05 사용자 지시).
|
||||
root.className = "b04-surface__group ui-sidebar-section";
|
||||
const heading = document.createElement("h3");
|
||||
heading.className = "b04-surface__panel-title";
|
||||
heading.textContent = title;
|
||||
root.append(heading, ...children);
|
||||
return root;
|
||||
}
|
||||
|
||||
function getModelFilter(model: SurfaceModelSummary): string {
|
||||
const configured = model.generation_params?.source_filter;
|
||||
if (typeof configured === "string") return configured.toLowerCase();
|
||||
@@ -131,8 +114,12 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
let models: SurfaceModelSummary[] = [];
|
||||
let pointCloud: SurfacePointCloudSampleResponse | null = null;
|
||||
|
||||
const inputSelect = document.createElement("select");
|
||||
inputSelect.className = "b04-surface__select";
|
||||
const inputSelectField = createSelectField({
|
||||
options: [],
|
||||
size: 36,
|
||||
ariaLabel: L("B04_Surface_InputFiles"),
|
||||
});
|
||||
const inputSelect = inputSelectField.select;
|
||||
const inputInfo = document.createElement("div");
|
||||
inputInfo.className = "b04-surface__input-info";
|
||||
const statusBox = document.createElement("div");
|
||||
@@ -157,23 +144,15 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
// 도엽등고 3D 서피스 — 전처리에서 함께 생성되는 참고 서피스(LAS 없는 설계의 지형 원천,
|
||||
// 2026-08-30). 모델 목록에 sheet/dtm이 있을 때만 별도 컨테이너로 보여준다.
|
||||
const sheetViewer = createSurfaceTerrainViewer();
|
||||
const sheetSection = document.createElement("section");
|
||||
sheetSection.className = "b04-surface__sheet-section ui-sidebar-section";
|
||||
const sheetTitle = document.createElement("h3");
|
||||
sheetTitle.className = "b04-surface__panel-title";
|
||||
sheetTitle.textContent = L("B04_Surface_SheetSurface");
|
||||
// 보간 방식 전환 줄 — 어느 방식이 이 지형에 맞는지 눈으로 비교해 정한다
|
||||
// (2026-08-30 사용자 지시). 버튼 목록은 실제 생성된 모델에서 만든다.
|
||||
const sheetToolbar = document.createElement("div");
|
||||
sheetToolbar.className = "b04-surface__sheet-toolbar";
|
||||
const sheetMethodButtons = new Map<string, HTMLButtonElement>();
|
||||
// (2026-08-30 사용자 지시). 방식 목록은 실제 생성된 모델에서 만든다.
|
||||
let sheetToolbar = createToolbar({ items: [], size: 28 });
|
||||
let sheetSegment: SegmentHandle | null = null;
|
||||
let sheetMethod = "";
|
||||
|
||||
function selectSheetMethod(method: string): void {
|
||||
sheetMethod = method;
|
||||
for (const [key, button] of sheetMethodButtons) {
|
||||
button.classList.toggle("is-active", key === method);
|
||||
}
|
||||
sheetSegment?.set(method);
|
||||
const projectId = getProjectId();
|
||||
if (!projectId) return;
|
||||
sheetViewer.setSelection(`sheet_${method}`, "dtm");
|
||||
@@ -195,7 +174,10 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
const message = L("B04_Surface_Build_Confirm")
|
||||
.replace("{filter}", label)
|
||||
.replace("{method}", "도엽등고");
|
||||
if (!(await showConfirmDialog(message, L("B04_Surface_Build_Action")))) return;
|
||||
if (!(await showConfirmDialog(message, L("B04_Surface_Build_Action")))) {
|
||||
sheetSegment?.set(sheetMethod); // 취소 — 고름 표시를 원래 방식으로 되돌린다
|
||||
return;
|
||||
}
|
||||
|
||||
showLoadingOverlay();
|
||||
showToast(L("B04_Surface_Build_Running"), "info");
|
||||
@@ -204,7 +186,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
models = (await listSurfaceModels(projectId)).models;
|
||||
// 만들었으니 「미생성」 표시를 걷는다 — 버튼 줄을 통째로 다시 세우지는 않는다
|
||||
// (현재 선택·스무딩 상태가 날아간다).
|
||||
const button = sheetMethodButtons.get(method);
|
||||
const button = sheetSegment?.root.querySelector<HTMLElement>(`[data-value="${method}"]`);
|
||||
if (button) {
|
||||
button.classList.remove("is-unbuilt");
|
||||
button.removeAttribute("title");
|
||||
@@ -217,33 +199,34 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : "";
|
||||
showToast(`${L("B04_Surface_Build_Failed")} ${detail}`, "error");
|
||||
sheetSegment?.set(sheetMethod);
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// 라이다 지표면 겹쳐 보기 — 확정 필터의 DTM을 반투명으로 얹는다.
|
||||
const lidarLabel = document.createElement("label");
|
||||
lidarLabel.className = "toggle-label toggle-button b04-surface__sheet-lidar";
|
||||
const lidarCheck = document.createElement("input");
|
||||
lidarCheck.type = "checkbox";
|
||||
lidarLabel.append(lidarCheck, document.createTextNode(` ${L("B04_Surface_SheetLidar")}`));
|
||||
lidarCheck.addEventListener("change", () => {
|
||||
void sheetViewer
|
||||
.showOverlay(
|
||||
lidarCheck.checked ? filterGroup.select.value : "",
|
||||
"dtm",
|
||||
terrainViewer.isSmoothingEnabled(),
|
||||
)
|
||||
.then((loaded) => {
|
||||
if (lidarCheck.checked && !loaded) {
|
||||
showToast(L("B04_Surface_SheetLidar_Missing"), "warning");
|
||||
lidarCheck.checked = false;
|
||||
}
|
||||
});
|
||||
const lidarToggle = createToggle({
|
||||
kind: "chip",
|
||||
label: L("B04_Surface_SheetLidar"),
|
||||
size: 28,
|
||||
onChange: (on) => {
|
||||
void sheetViewer
|
||||
.showOverlay(on ? filterGroup.select.value : "", "dtm", terrainViewer.isSmoothingEnabled())
|
||||
.then((loaded) => {
|
||||
if (on && !loaded) {
|
||||
showToast(L("B04_Surface_SheetLidar_Missing"), "warning");
|
||||
lidarToggle.set(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
sheetSection.append(sheetTitle, sheetToolbar, sheetViewer.root);
|
||||
const sheetSection = buildSurfaceGroup(
|
||||
L("B04_Surface_SheetSurface"),
|
||||
[sheetToolbar, sheetViewer.root],
|
||||
"b04-surface__sheet-section",
|
||||
);
|
||||
sheetSection.hidden = true;
|
||||
|
||||
let syncingCamera = false;
|
||||
@@ -271,32 +254,27 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
const resetButton = createButton({
|
||||
label: L("Common_Btn_Reset"),
|
||||
variant: "ghost",
|
||||
// 가시성 톤 다운(2026-08-18 사용자 지시) — 붉은 테두리·글자만, 채움 없음(위험 고스트).
|
||||
variant: "danger-ghost",
|
||||
onClick: () => void onB04_Surface_Reset_Click(),
|
||||
});
|
||||
// 가시성 톤 다운(2026-08-18 사용자 지시) — 붉은 테두리·글자만, 채움 없음(ghost 배경).
|
||||
resetButton.classList.add("b04-surface__reset");
|
||||
|
||||
const inputGroup = document.createElement("section");
|
||||
inputGroup.className = "b04-surface__group ui-sidebar-section";
|
||||
const inputTitle = document.createElement("h3");
|
||||
inputTitle.className = "b04-surface__panel-title";
|
||||
inputTitle.textContent = L("B04_Surface_InputFiles");
|
||||
inputGroup.append(inputTitle, inputSelect, inputInfo);
|
||||
const inputGroup = buildSurfaceGroup(L("B04_Surface_InputFiles"), [
|
||||
inputSelectField.root,
|
||||
inputInfo,
|
||||
]);
|
||||
|
||||
// 지면 필터 · 서피스 · 스무딩은 함께 모델 하나를 정하는 값이라 한 컨테이너에 모은다.
|
||||
const analysisGroup = buildGroup(
|
||||
L("B04_Surface_Group_Analysis"),
|
||||
const analysisGroup = buildSurfaceGroup(L("B04_Surface_Group_Analysis"), [
|
||||
filterGroup.root,
|
||||
methodGroup.root,
|
||||
terrainViewer.smoothingField,
|
||||
);
|
||||
]);
|
||||
// 표시 옵션은 3D 모델 토글과 포인트 슬라이더를 한 칸에 둔다(슬라이더가 맨 아래).
|
||||
const displayGroup = buildGroup(
|
||||
L("B04_Surface_Group_Display"),
|
||||
const displayGroup = buildSurfaceGroup(L("B04_Surface_Group_Display"), [
|
||||
terrainViewer.optionsContent,
|
||||
viewer.optionsContent,
|
||||
);
|
||||
]);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b04-surface__form";
|
||||
@@ -383,19 +361,18 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
? `${bounds.z_min.toFixed(2)} m ~ ${bounds.z_max.toFixed(2)} m`
|
||||
: null;
|
||||
inputInfo.append(
|
||||
buildInfoLine(
|
||||
"좌표계",
|
||||
selectedInputFile.crs_epsg ? `EPSG:${selectedInputFile.crs_epsg}` : null,
|
||||
),
|
||||
buildInfoLine(
|
||||
"크기",
|
||||
selectedInputFile.file_size_mb == null
|
||||
? null
|
||||
: `${selectedInputFile.file_size_mb.toFixed(2)} MB`,
|
||||
),
|
||||
buildInfoLine("포인트 수", pointCloud?.point_count.toLocaleString()),
|
||||
buildInfoLine("표시 포인트 수", pointCloud?.sampled_count.toLocaleString()),
|
||||
buildInfoLine("높이 범위", heightRange),
|
||||
createKv([
|
||||
infoRow("좌표계", selectedInputFile.crs_epsg ? `EPSG:${selectedInputFile.crs_epsg}` : null),
|
||||
infoRow(
|
||||
"크기",
|
||||
selectedInputFile.file_size_mb == null
|
||||
? null
|
||||
: `${selectedInputFile.file_size_mb.toFixed(2)} MB`,
|
||||
),
|
||||
infoRow("포인트 수", pointCloud?.point_count.toLocaleString()),
|
||||
infoRow("표시 포인트 수", pointCloud?.sampled_count.toLocaleString()),
|
||||
infoRow("높이 범위", heightRange),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -573,25 +550,32 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
);
|
||||
sheetSection.hidden = sheetMethods.length === 0;
|
||||
if (sheetMethods.length) {
|
||||
sheetToolbar.replaceChildren();
|
||||
sheetMethodButtons.clear();
|
||||
const segment = createSegment({
|
||||
options: sheetMethods.map((method) => ({
|
||||
value: method.key,
|
||||
label: method.label,
|
||||
// 아직 안 만든 방식은 눌러야 만들어진다 — 눌러 보기 전에 알 수 있게 표시한다.
|
||||
title: method.built ? undefined : L("B04_Surface_Build_Action"),
|
||||
})),
|
||||
value: "",
|
||||
size: 28,
|
||||
ariaLabel: L("B04_Surface_SheetSurface"),
|
||||
onChange: (key) =>
|
||||
void onSheetMethodClick(key, sheetMethods.find((m) => m.key === key)?.label ?? key),
|
||||
});
|
||||
for (const method of sheetMethods) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b04-surface__sheet-method";
|
||||
button.textContent = method.label;
|
||||
// 아직 안 만든 방식은 눌러야 만들어진다 — 눌러 보기 전에 알 수 있게 표시한다.
|
||||
button.classList.toggle("is-unbuilt", !method.built);
|
||||
if (!method.built) button.title = L("B04_Surface_Build_Action");
|
||||
button.addEventListener("click", () => {
|
||||
void onSheetMethodClick(method.key, method.label);
|
||||
});
|
||||
sheetToolbar.append(button);
|
||||
sheetMethodButtons.set(method.key, button);
|
||||
segment.root
|
||||
.querySelector(`[data-value="${method.key}"]`)
|
||||
?.classList.toggle("is-unbuilt", !method.built);
|
||||
}
|
||||
sheetSegment = segment;
|
||||
// 스무딩 드롭다운과 라이다 토글은 오른쪽 끝에 함께 둔다.
|
||||
sheetViewer.smoothingField.classList.add("b04-surface__sheet-smoothing");
|
||||
sheetToolbar.append(sheetViewer.smoothingField, lidarLabel);
|
||||
const nextToolbar = createToolbar({
|
||||
items: [segment.root, "spacer", sheetViewer.smoothingField, lidarToggle.root],
|
||||
size: 28,
|
||||
});
|
||||
sheetToolbar.replaceWith(nextToolbar);
|
||||
sheetToolbar = nextToolbar;
|
||||
sheetViewer.setSmoothing(true);
|
||||
// 처음 열 때 안 만든 방식을 고르면 묻지도 않았는데 모달이 뜬다 — 만들어 둔 것을 고른다.
|
||||
const firstBuilt = sheetMethods.find((method) => method.built);
|
||||
|
||||
Reference in New Issue
Block a user