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:
2026-10-26 22:09:36 +09:00
co-authored by Claude Sonnet 5.5
parent a826804277
commit 58dafb1572
14 changed files with 441 additions and 858 deletions
+97 -113
View File
@@ -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);