This commit is contained in:
2026-07-10 18:12:17 +09:00
parent 50d75fcfcd
commit e3d66e717c
28 changed files with 2542 additions and 99 deletions
+23
View File
@@ -544,6 +544,14 @@ export const ui_locales = {
B03_File_Error_Size: ["파일 크기 제한을 초과했습니다.", "File size limit exceeded."],
B03_File_Upload_Success: ["입력 파일 업로드를 완료했습니다.", "Input files uploaded."],
B03_File_Upload_Failed: ["파일 업로드에 실패했습니다.", "File upload failed."],
B03_File_Analysis_InProgress: [
"WF1 분석이 백그라운드에서 진행 중입니다. 완료되면 자동으로 이동합니다.",
"WF1 analysis is running in the background. You will move automatically when it completes.",
],
B03_File_Analysis_StillRunning: [
"분석이 계속 진행 중입니다. 잠시 후 다시 확인하세요.",
"Analysis is still running. Check again shortly.",
],
B03_File_Result_Path: ["저장 경로", "Stored path"],
B03_File_Group_Required: ["필수 파일", "Required files"],
B03_File_Group_Optional: ["선택 파일", "Optional files"],
@@ -574,6 +582,7 @@ export const ui_locales = {
B03_File_Status_Completed: ["완료", "Completed"],
B03_File_Status_Failed: ["실패", "Failed"],
B03_File_Status_Detected: ["중단된 업로드 감지", "Paused upload detected"],
B03_File_Restore_State: ["저장된 업로드/분석 상태 복구", "Restored upload/analysis state"],
B03_File_Resume_Button: ["업로드 재개", "Resume upload"],
B03_File_New_Button: ["새 파일로 시작", "Start new file"],
B03_File_ServiceWorker_Ready: [
@@ -594,6 +603,20 @@ export const ui_locales = {
B04_Surface_Field_Force: ["기존 결과 무시하고 재계산", "Force recompute"],
B04_Surface_Btn_Analyze: ["지표면 분석 실행", "Run Surface Analysis"],
B04_Surface_Btn_Refresh: ["목록 새로고침", "Refresh"],
B04_Surface_InputFiles: ["입력 포인트클라우드", "Input point cloud"],
B04_Surface_InputFiles_Empty: [
"분석 가능한 LAS/LAZ 파일이 없습니다.",
"No LAS/LAZ file is available.",
],
B04_Surface_Input_FileName: ["파일명", "File name"],
B04_Surface_Input_Crs: ["좌표계", "CRS"],
B04_Surface_Input_Size: ["크기(MB)", "Size (MB)"],
B04_Surface_PointCloud_Title: ["포인트클라우드 미리보기", "Point cloud preview"],
B04_Surface_Status_Unknown: ["상태 미확인", "Unknown"],
B04_Surface_GroundStats_Title: ["지면 필터 통계", "Ground filter stats"],
B04_Surface_GroundStats_Empty: ["표시할 지면 통계가 없습니다.", "No ground stats to display."],
B04_Surface_GroundStats_Filter: ["필터", "Filter"],
B04_Surface_GroundStats_SourcePoints: ["지면 포인트", "Ground points"],
B04_Surface_Result_Title: ["생성된 지표면 모델", "Generated Surface Models"],
B04_Surface_Result_Empty: [
"아직 생성된 지표면 모델이 없습니다.",
+117
View File
@@ -0,0 +1,117 @@
.ui-workflow-layout {
display: flex;
flex-direction: column;
min-height: calc(100vh - var(--wf-header-height));
background: var(--color-bg);
}
.ui-workflow-layout__header {
display: flex;
align-items: center;
gap: var(--spacing-16);
min-height: var(--wf-header-height);
padding: var(--spacing-8) var(--spacing-24);
border-bottom: 1px solid var(--color-border);
background: var(--color-surface-raised);
}
.ui-workflow-layout__menu-button {
width: 40px;
height: 40px;
border: 1px solid var(--color-border);
border-radius: var(--radius-buttons);
background: var(--color-canvas);
color: var(--color-text);
cursor: pointer;
}
.ui-workflow-layout__title-wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-24);
flex: 1;
min-width: 0;
}
.ui-workflow-layout__title {
font-size: var(--text-subheading);
white-space: nowrap;
}
.ui-workflow-layout__steps {
display: flex;
gap: var(--spacing-8);
overflow-x: auto;
}
.ui-workflow-layout__step {
flex: 0 0 auto;
padding: var(--spacing-4) var(--spacing-16);
border-radius: var(--radius-pills);
background: var(--color-paper);
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
.ui-workflow-layout__step.is-active {
background: var(--color-mist-violet);
color: var(--color-accent);
}
.ui-workflow-layout__body {
position: relative;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
flex: 1;
min-height: 0;
}
.ui-workflow-layout__panel {
z-index: var(--z-dropdown);
overflow-y: auto;
border-right: 1px solid var(--color-border);
background: var(--color-surface);
transition:
margin-left var(--transition-base),
box-shadow var(--transition-base);
}
.ui-workflow-layout:not(.is-menu-open) .ui-workflow-layout__panel {
margin-left: -280px;
}
.ui-workflow-layout__main {
min-width: 0;
min-height: 0;
overflow: auto;
background: var(--color-canvas);
}
@media (max-width: 860px) {
.ui-workflow-layout__title-wrap {
align-items: flex-start;
flex-direction: column;
gap: var(--spacing-8);
}
.ui-workflow-layout__body {
display: block;
}
.ui-workflow-layout__panel {
position: absolute;
inset: 0 auto 0 0;
width: 280px;
box-shadow: var(--shadow-lg);
}
.ui-workflow-layout:not(.is-menu-open) .ui-workflow-layout__panel {
margin-left: -280px;
box-shadow: none;
}
.ui-workflow-layout__main {
min-height: calc(100vh - var(--wf-header-height));
}
}
@@ -0,0 +1,79 @@
import "./ui_template_workflow_layout.css";
export interface WorkflowLayoutOptions {
title: string;
steps: string[];
activeStep: number;
leftPanel: HTMLElement;
mainContent: HTMLElement;
onMenuToggle?: (isOpen: boolean) => void;
}
export interface WorkflowLayoutHandle {
root: HTMLElement;
setMenuOpen: (isOpen: boolean) => void;
}
function createStepBar(steps: readonly string[], activeStep: number): HTMLElement {
const bar = document.createElement("div");
bar.className = "ui-workflow-layout__steps";
steps.forEach((step, index) => {
const item = document.createElement("span");
item.className = "ui-workflow-layout__step";
if (index === activeStep) item.classList.add("is-active");
item.textContent = step;
bar.append(item);
});
return bar;
}
export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLayoutHandle {
const root = document.createElement("div");
root.className = "ui-workflow-layout";
const header = document.createElement("header");
header.className = "ui-workflow-layout__header";
const menuButton = document.createElement("button");
menuButton.type = "button";
menuButton.className = "ui-workflow-layout__menu-button";
menuButton.setAttribute("aria-label", "Toggle workflow menu");
menuButton.textContent = "☰";
const titleWrap = document.createElement("div");
titleWrap.className = "ui-workflow-layout__title-wrap";
const title = document.createElement("h2");
title.className = "ui-workflow-layout__title";
title.textContent = options.title;
titleWrap.append(title, createStepBar(options.steps, options.activeStep));
header.append(menuButton, titleWrap);
const body = document.createElement("div");
body.className = "ui-workflow-layout__body";
const aside = document.createElement("aside");
aside.className = "ui-workflow-layout__panel";
aside.append(options.leftPanel);
const main = document.createElement("main");
main.className = "ui-workflow-layout__main";
main.append(options.mainContent);
body.append(aside, main);
root.append(header, body);
function setMenuOpen(isOpen: boolean): void {
root.classList.toggle("is-menu-open", isOpen);
menuButton.setAttribute("aria-expanded", String(isOpen));
options.onMenuToggle?.(isOpen);
}
menuButton.addEventListener("click", () => setMenuOpen(!root.classList.contains("is-menu-open")));
main.addEventListener("click", () => {
if (root.classList.contains("is-menu-open")) setMenuOpen(false);
});
setMenuOpen(true);
return { root, setMenuOpen };
}