refactor(B02·B03): 페이지 자체 변종을 부품 상자 부품으로 — 파일 카드 · 띠 알림 · 진행 막대 · 켜기 · 패널 머리 · 단추 (PLAN 553-1)
- B03 파일 카드 = 카드 부품 · [선택] · [×] = 단추 · 아이콘 단추 · 진행 막대 · 오류 띠 = 부품 - B03 현황 · 이어올리기 · 예정노선 바뀜 = 띠 알림 · LAS 없이 설계 = 켜기 · 노선 · 지형 제목 = 패널 머리 - B03 · B02 죽은 CSS 걷음 · 날 색 → 공통 값 · 가이드 촬영 선택자 · 지킴 시험 test_553_1_pages Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,12 @@ import {
|
||||
UPLOAD_MAX_FILES,
|
||||
} from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createButton, createTag } from "@ui/ui_template_elements";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton, createIconButton, createTag } from "@ui/ui_template_elements";
|
||||
import { createPanelHead } from "@ui/ui_template_panel_head";
|
||||
import { createProgressBar, type ProgressBarHandle } from "@ui/ui_template_progress";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createToggle } from "@ui/ui_template_toggle";
|
||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||
import { createWorkflowOverlays } from "@ui/ui_template_overlay";
|
||||
import { createStepBar, WORKFLOW_STEP_ICONS } from "@ui/ui_template_workflow_layout";
|
||||
@@ -63,6 +68,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
// 대시보드 임시 보관함에서 가져올 자료 선택기 — 선택되면 [업로드]가 이동을 수행한다.
|
||||
const tempPicker = createTempPicker(() => updateUploadButton());
|
||||
const cardMap = new Map<FileSlot, HTMLElement>();
|
||||
const progressMap = new Map<FileSlot, ProgressBarHandle>();
|
||||
let uploadButton: HTMLButtonElement;
|
||||
let resumeBanner: HTMLDivElement;
|
||||
let activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
@@ -75,9 +81,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
? localStorage.getItem(`b03_las_free_${activeProjectId}`) === "1"
|
||||
: false;
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "b03-file__subtitle";
|
||||
subtitle.textContent = L("B03_File_Subtitle");
|
||||
const subtitle = createText(L("B03_File_Subtitle"), "muted", 14);
|
||||
subtitle.classList.add("b03-file__subtitle");
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
@@ -94,7 +99,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
dropzone.append(dropzoneLabel, fileInput);
|
||||
|
||||
const pageError = document.createElement("p");
|
||||
pageError.className = "b03-file__error";
|
||||
pageError.className = "ui-text ui-text--error b03-file__error";
|
||||
pageError.setAttribute("role", "alert");
|
||||
|
||||
// 업로드 버튼 안에서 도는 원 — 공용 스피너를 버튼 크기로 줄여 쓴다.
|
||||
@@ -200,11 +205,11 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
|
||||
const fileName = card.querySelector<HTMLSpanElement>(".b03-file__file-name");
|
||||
const fileSize = card.querySelector<HTMLSpanElement>(".b03-file__file-size");
|
||||
const progress = card.querySelector<HTMLDivElement>(".b03-file__progress-bar");
|
||||
const progress = progressMap.get(slot);
|
||||
const progressBytes = card.querySelector<HTMLSpanElement>(".b03-file__progress-bytes");
|
||||
const progressSpeed = card.querySelector<HTMLSpanElement>(".b03-file__progress-speed");
|
||||
const progressEta = card.querySelector<HTMLSpanElement>(".b03-file__progress-eta");
|
||||
const error = card.querySelector<HTMLDivElement>(".b03-file__error-message");
|
||||
const error = card.querySelector<HTMLElement>(".b03-file__error-message .ui-banner__text");
|
||||
const remove = card.querySelector<HTMLButtonElement>(".b03-file__card-remove");
|
||||
|
||||
const percent = state.file ? Math.min(100, (state.progressBytes / state.file.size) * 100) : 0;
|
||||
@@ -217,7 +222,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
? `${state.serverUploaded.sizeMb.toFixed(2)} MB`
|
||||
: "";
|
||||
}
|
||||
if (progress) progress.style.width = `${percent}%`;
|
||||
progress?.set(percent / 100);
|
||||
if (progressBytes) {
|
||||
progressBytes.textContent = `${L("B03_File_Progress_Bytes")}: ${formatBytes(
|
||||
state.progressBytes,
|
||||
@@ -413,26 +418,27 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
const lock = overview.analysis_lock;
|
||||
if (lock?.running) {
|
||||
const who = lock.name ?? lock.email ?? null;
|
||||
const busy = document.createElement("p");
|
||||
busy.className = "b03-file__overview-lock";
|
||||
busy.textContent = who
|
||||
? `${who} 님이 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다.`
|
||||
: "다른 자리에서 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다.";
|
||||
notes.push(busy);
|
||||
notes.push(
|
||||
createBanner({
|
||||
tone: "warning",
|
||||
text: who
|
||||
? `${who} 님이 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다.`
|
||||
: "다른 자리에서 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (overview.required_complete && overview.analysis_complete) {
|
||||
const complete = document.createElement("p");
|
||||
complete.className = "b03-file__overview-complete";
|
||||
complete.textContent = L("B03_File_Overview_Complete");
|
||||
notes.push(complete);
|
||||
notes.push(createBanner({ tone: "success", text: L("B03_File_Overview_Complete") }));
|
||||
}
|
||||
for (const session of overview.pending_sessions) {
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "b03-file__overview-pending";
|
||||
pending.textContent =
|
||||
`${session.original_filename} — ${session.progress_percent}% ` +
|
||||
L("B03_File_Overview_Pending");
|
||||
notes.push(pending);
|
||||
notes.push(
|
||||
createBanner({
|
||||
tone: "info",
|
||||
text:
|
||||
`${session.original_filename} — ${session.progress_percent}% ` +
|
||||
L("B03_File_Overview_Pending"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (notes.length) {
|
||||
overviewBanner.append(...notes);
|
||||
@@ -453,18 +459,30 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
renderExtensionLabel(card, state);
|
||||
const input = card.querySelector<HTMLInputElement>(".b03-file__slot-input")!;
|
||||
input.accept = state.extensions.join(",");
|
||||
const select = card.querySelector<HTMLButtonElement>(".b03-file__card-select")!;
|
||||
select.textContent = L("B03_File_Card_Select");
|
||||
const select = createButton({ label: L("B03_File_Card_Select"), variant: "ghost", size: 24 });
|
||||
select.classList.add("b03-file__card-select");
|
||||
card.querySelector('[data-slot="select"]')!.replaceWith(select);
|
||||
select.addEventListener("click", () => input.click());
|
||||
input.addEventListener("change", () => {
|
||||
onFileSelected(input.files ? Array.from(input.files) : [], state.slot);
|
||||
input.value = "";
|
||||
});
|
||||
const remove = card.querySelector<HTMLButtonElement>(".b03-file__card-remove")!;
|
||||
remove.textContent = "×";
|
||||
remove.title = L("B03_File_Card_Remove");
|
||||
remove.setAttribute("aria-label", L("B03_File_Card_Remove"));
|
||||
remove.addEventListener("click", () => removeFile(state.slot));
|
||||
const remove = createIconButton({
|
||||
glyph: "×",
|
||||
title: L("B03_File_Card_Remove"),
|
||||
variant: "outline",
|
||||
size: 24,
|
||||
onClick: () => removeFile(state.slot),
|
||||
});
|
||||
remove.classList.add("b03-file__card-remove");
|
||||
card.querySelector('[data-slot="remove"]')!.replaceWith(remove);
|
||||
const progress = createProgressBar({ thick: 6 });
|
||||
progress.root.classList.add("b03-file__progress");
|
||||
card.querySelector('[data-slot="progress"]')!.replaceWith(progress.root);
|
||||
progressMap.set(state.slot, progress);
|
||||
const errorBanner = createBanner({ tone: "danger", text: "" });
|
||||
errorBanner.classList.add("b03-file__error-message");
|
||||
card.querySelector('[data-slot="error"]')!.replaceWith(errorBanner);
|
||||
cardMap.set(state.slot, card);
|
||||
return card;
|
||||
}
|
||||
@@ -478,18 +496,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
const group = document.createElement("section");
|
||||
group.className = "b03-file__group";
|
||||
if (modifier) group.classList.add(modifier);
|
||||
if (title) {
|
||||
const groupTitle = document.createElement("h3");
|
||||
groupTitle.className = "b03-file__group-title";
|
||||
groupTitle.textContent = title;
|
||||
group.append(groupTitle);
|
||||
}
|
||||
if (hint) {
|
||||
const groupHint = document.createElement("p");
|
||||
groupHint.className = "b03-file__group-hint";
|
||||
groupHint.textContent = hint;
|
||||
group.append(groupHint);
|
||||
}
|
||||
if (title) group.append(createPanelHead({ title, sub: hint }).root);
|
||||
const content = document.createElement("div");
|
||||
content.className = "b03-file__group-content";
|
||||
for (const slot of groupSlots) {
|
||||
@@ -560,7 +567,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
pickRow.append(dropzone, tempPicker.root, uploadButton);
|
||||
|
||||
const uploadControlPanel = document.createElement("div");
|
||||
uploadControlPanel.className = "b03-file__control-panel";
|
||||
uploadControlPanel.dataset.pad = "16";
|
||||
uploadControlPanel.className = "ui-card b03-file__control-panel";
|
||||
uploadControlPanel.append(subtitle, pickRow, resumeBanner, pageError);
|
||||
|
||||
// 자료의 출처가 둘로 갈린다 — 원청이 준 계획노선, 측량이 준 지형(LAS·래스터).
|
||||
@@ -581,16 +589,17 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
);
|
||||
|
||||
// LAS 없는 설계 토글 — 켜면 포인트클라우드 카드를 비활성화하고 필수에서 뺀다.
|
||||
const lasFreeRow = document.createElement("label");
|
||||
lasFreeRow.className = "b03-file__lasfree";
|
||||
const lasFreeToggle = createToggle({
|
||||
label: L("B03_File_LasFree_Toggle"),
|
||||
checked: lasFreeDesign,
|
||||
size: 36,
|
||||
onChange: onLasFreeChange,
|
||||
});
|
||||
const lasFreeRow = lasFreeToggle.root;
|
||||
lasFreeRow.classList.add("b03-file__lasfree");
|
||||
lasFreeRow.title = L("B03_File_LasFree_Hint");
|
||||
const lasFreeCheck = document.createElement("input");
|
||||
lasFreeCheck.type = "checkbox";
|
||||
const lasFreeText = document.createElement("span");
|
||||
lasFreeText.textContent = L("B03_File_LasFree_Toggle");
|
||||
lasFreeRow.append(lasFreeCheck, lasFreeText);
|
||||
function applyLasFreeState(): void {
|
||||
lasFreeCheck.checked = lasFreeDesign;
|
||||
lasFreeToggle.set(lasFreeDesign);
|
||||
terrainGroup.classList.toggle("b03-file__group--disabled", lasFreeDesign);
|
||||
for (const slot of TERRAIN_SLOTS) {
|
||||
const card = cardMap.get(slot);
|
||||
@@ -608,8 +617,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
if (lasFreeDesign && slots.get(slot)?.file) removeFile(slot);
|
||||
}
|
||||
}
|
||||
lasFreeCheck.addEventListener("change", () => {
|
||||
lasFreeDesign = lasFreeCheck.checked;
|
||||
function onLasFreeChange(checked: boolean): void {
|
||||
lasFreeDesign = checked;
|
||||
if (activeProjectId) {
|
||||
localStorage.setItem(`b03_las_free_${activeProjectId}`, lasFreeDesign ? "1" : "0");
|
||||
}
|
||||
@@ -619,19 +628,21 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
// 골라 놓고도 [파일 업로드]가 잠긴 채 남는다(2026-09-03 실측: 파일을 먼저 고르고
|
||||
// 토글을 나중에 켠 순서에서 재현).
|
||||
updateUploadButton();
|
||||
});
|
||||
}
|
||||
|
||||
// LAS 토글은 **파일 입력 컨테이너**의 것이다(2026-09-03 사용자 지시) — 무엇을 받을지
|
||||
// 정하는 스위치라 고르는 자리 옆에 선다. 켜면 지형 컨테이너가 통째로 잠긴다.
|
||||
uploadControlPanel.insertBefore(lasFreeRow, resumeBanner);
|
||||
|
||||
const routePanel = document.createElement("div");
|
||||
routePanel.className = "b03-file__control-panel b03-file__cards-container-panel";
|
||||
routePanel.dataset.pad = "16";
|
||||
routePanel.className = "ui-card b03-file__control-panel b03-file__cards-container-panel";
|
||||
routePanel.append(routeGroup);
|
||||
mountPlanImport(routePanel, uploadControlPanel, () => void applyUploadOverview());
|
||||
|
||||
const terrainPanel = document.createElement("div");
|
||||
terrainPanel.className = "b03-file__control-panel b03-file__cards-container-panel";
|
||||
terrainPanel.dataset.pad = "16";
|
||||
terrainPanel.className = "ui-card b03-file__control-panel b03-file__cards-container-panel";
|
||||
terrainPanel.append(terrainGroup);
|
||||
|
||||
const cardsContainer = document.createElement("div");
|
||||
|
||||
Reference in New Issue
Block a user