refactor(B03): 파일 입력 화면 마무리 — 파일 놓기 칸 · 모달 · 확인창 · 고르는 카드 · 목록 줄 부품 (PLAN 553-1)
- 놓기 칸 = 파일 놓기 칸 부품(한 줄로 눕힘) · 임시 보관함 창 = 모달 + 고르는 카드 + 빈 상태 · 교체 확인 = 확인창 - 업로드 도중 window.confirm 둘 → 확인창 부품(글 그대로 · 단추 이름으로 선택 안내) · 예정노선 고름 줄 = 목록 줄 - 모달 CSS · 임시 목록 옛 CSS 걷음 · 가이드 촬영 선택자 · 지킴 시험 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
} from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createDrop } from "@ui/ui_template_drop";
|
||||
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";
|
||||
@@ -84,19 +85,15 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
const subtitle = createText(L("B03_File_Subtitle"), "muted", 14);
|
||||
subtitle.classList.add("b03-file__subtitle");
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.multiple = true;
|
||||
fileInput.accept = UPLOAD_ALLOWED_EXT.join(",");
|
||||
fileInput.className = "b03-file__native-input";
|
||||
|
||||
const dropzone = document.createElement("div");
|
||||
dropzone.className = "b03-file__dropzone";
|
||||
dropzone.tabIndex = 0;
|
||||
const dropzoneLabel = document.createElement("strong");
|
||||
dropzoneLabel.textContent = L("B03_File_Select_Label");
|
||||
// 설명은 좌측 안내 패널 몫이다(2026-09-03 사용자 지시) — 여기는 자리 이름만 남긴다.
|
||||
dropzone.append(dropzoneLabel, fileInput);
|
||||
const drop = createDrop({
|
||||
accept: UPLOAD_ALLOWED_EXT.join(","),
|
||||
multiple: true,
|
||||
text: L("B03_File_Select_Label"),
|
||||
onFiles: (files) => onFileSelected(files),
|
||||
});
|
||||
const dropzone = drop.root;
|
||||
dropzone.classList.add("b03-file__dropzone");
|
||||
|
||||
const pageError = document.createElement("p");
|
||||
pageError.className = "ui-text ui-text--error b03-file__error";
|
||||
@@ -530,29 +527,6 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
const { detectPausedUploads, pollAnalysis, relockWhileInitialPipelineRuns, startChunkedUpload } =
|
||||
flow;
|
||||
|
||||
function onB03_File_Select_Change(): void {
|
||||
onFileSelected(fileInput.files ? Array.from(fileInput.files) : []);
|
||||
fileInput.value = "";
|
||||
}
|
||||
|
||||
function onB03_File_Drop(event: DragEvent): void {
|
||||
event.preventDefault();
|
||||
dropzone.classList.remove("is-dragging");
|
||||
onFileSelected(event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : []);
|
||||
}
|
||||
|
||||
fileInput.addEventListener("change", onB03_File_Select_Change);
|
||||
dropzone.addEventListener("click", () => fileInput.click());
|
||||
dropzone.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") fileInput.click();
|
||||
});
|
||||
dropzone.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
dropzone.classList.add("is-dragging");
|
||||
});
|
||||
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("is-dragging"));
|
||||
dropzone.addEventListener("drop", onB03_File_Drop);
|
||||
|
||||
uploadButton = createButton({
|
||||
label: L("B03_File_Upload_Button"),
|
||||
variant: "filled",
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
margin-top: 0; /* 단일 layout gap에 의해 제어되도록 설정 */
|
||||
}
|
||||
|
||||
.b03-file__native-input,
|
||||
.b03-file__slot-input {
|
||||
display: none;
|
||||
}
|
||||
@@ -58,49 +57,13 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 한 줄 바(2026-09-03) — 종전 160px 블록은 안내 문구를 담느라 컸다. 문구는 좌측
|
||||
패널로 갔으므로 여기는 「끌어 놓거나 눌러서 고르는 자리」 표시만 한다. */
|
||||
/* 한 줄 바(2026-09-03) — 파일 놓기 칸 부품을 [아이콘 · 글 · 고르기] 한 줄로 눕힌다. 종전 160px 블록은
|
||||
안내 문구를 담느라 컸고 문구는 좌측 패널로 갔다. 칸을 꽉 채워 세 칸 높이를 맞춘다. */
|
||||
.b03-file__dropzone {
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-8);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition:
|
||||
background var(--transition-base),
|
||||
border-color var(--transition-base),
|
||||
box-shadow var(--transition-base);
|
||||
}
|
||||
|
||||
.b03-file__dropzone:hover {
|
||||
background: var(--color-accent-soft);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.b03-file__dropzone:focus-visible,
|
||||
.b03-file__dropzone.is-dragging {
|
||||
outline: none;
|
||||
background: var(--color-accent-soft);
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.b03-file__dropzone strong {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.b03-file__dropzone span {
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-secondary);
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
}
|
||||
|
||||
/* 빈 오류 줄이 자리를 잡고 있으면 토글 아래가 통째로 비어 보인다 — 글자가 있을 때만 자리를 차지한다. */
|
||||
@@ -122,41 +85,6 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 완료 슬롯 재업로드 확인 모달 — 페이지 위를 덮는 단순 확인창. */
|
||||
.b03-file__modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-scrim);
|
||||
}
|
||||
|
||||
.b03-file__modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
max-width: 420px;
|
||||
padding: var(--spacing-24);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.b03-file__modal p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.b03-file__modal-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 계획노선 | 지형(LAS) — 컨테이너를 둘로 나눈다(2026-08-31 사용자 지시).
|
||||
노선은 카드 5장(shapefile 한 벌), 지형은 4장이라 폭을 5:4 비슷하게 준다.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* =============================================================================
|
||||
* B03_FileInput_UI_Style_Temp.css
|
||||
* 임시 보관함 불러오기 — 업로드 컨테이너 안 버튼 + 선택 모달
|
||||
* 모달 껍데기(.b03-file__modal / -backdrop)는 기존 확인 모달 스타일을 그대로 쓴다.
|
||||
* 임시 보관함 불러오기 — 업로드 컨테이너 안 버튼 + 선택 모달(모달 부품)
|
||||
* ========================================================================== */
|
||||
|
||||
/* 버튼 하나만 담는 자리 — 테두리를 또 두르면 버튼 테두리와 이중이 된다
|
||||
@@ -11,73 +10,10 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.b03-file__temp-modal {
|
||||
min-width: min(560px, 92vw);
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
/* 제목은 브라우저 기본 h3 서식이 남지 않게 템플릿 값으로 맞춘다. */
|
||||
.b03-file__temp-modal h3 {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-body);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 임시 보관함 목록 — 모달 · 카드(고르는 카드) 부품 · 목록 칸만 */
|
||||
.b03-file__temp-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.b03-file__temp-empty {
|
||||
padding: var(--spacing-16);
|
||||
text-align: center;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 목록 항목은 <button>이라 글꼴을 물려받지 않는다 — 본문 글꼴을 직접 지정한다.
|
||||
글자색도 본문색을 쓴다(--color-primary-text는 '주 액션 배경 위' 색이라 밝은 배경에서 안 보인다). */
|
||||
.b03-file__temp-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-paper);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-body);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b03-file__temp-option strong {
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b03-file__temp-option:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.b03-file__temp-option.is-selected {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-accent-soft);
|
||||
}
|
||||
|
||||
.b03-file__temp-option-files {
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b03-file__temp-modal-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
* [업로드] 버튼을 눌렀을 때 일어난다(2026-08-08 사용자 지시).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { createCard, createButton, setCardSelected, showToast } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { openModal as openDialog } from "@ui/ui_template_modal";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { fetchTempBatches, type TempBatchItem } from "../B01_Dashboard/B01_Dashboard_Api_Temp";
|
||||
import "./B03_FileInput_UI_Style_Temp.css";
|
||||
|
||||
@@ -71,76 +74,59 @@ export function createTempPicker(
|
||||
return;
|
||||
}
|
||||
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.className = "b03-file__modal-backdrop";
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "b03-file__modal b03-file__temp-modal ui-popup";
|
||||
modal.setAttribute("role", "dialog");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = L("B03_Temp_Modal_Title");
|
||||
const list = document.createElement("div");
|
||||
list.className = "b03-file__temp-list";
|
||||
|
||||
let pending: TempBatchItem | null = selectedBatch;
|
||||
|
||||
if (batches.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "b03-file__temp-empty";
|
||||
empty.textContent = L("B03_Temp_Modal_Empty");
|
||||
list.append(empty);
|
||||
}
|
||||
for (const batch of batches) {
|
||||
const option = document.createElement("button");
|
||||
option.type = "button";
|
||||
option.className = "b03-file__temp-option";
|
||||
option.classList.toggle("is-selected", pending?.batch_id === batch.batch_id);
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = batch.name;
|
||||
const meta = document.createElement("span");
|
||||
meta.textContent = `${batch.files.length}${L("B03_Temp_FileCount")} · ${formatBytes(
|
||||
batch.total_size_bytes,
|
||||
)}`;
|
||||
const files = document.createElement("span");
|
||||
files.className = "b03-file__temp-option-files";
|
||||
files.textContent = batch.files.map((file) => file.file_type.toUpperCase()).join(", ");
|
||||
option.append(name, meta, files);
|
||||
option.addEventListener("click", () => {
|
||||
pending = batch;
|
||||
list
|
||||
.querySelectorAll(".b03-file__temp-option")
|
||||
.forEach((element) => element.classList.remove("is-selected"));
|
||||
option.classList.add("is-selected");
|
||||
});
|
||||
list.append(option);
|
||||
}
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "b03-file__temp-modal-actions";
|
||||
const close = (): void => backdrop.remove();
|
||||
actions.append(
|
||||
createButton({ label: L("Common_Btn_Cancel"), variant: "ghost", onClick: close }),
|
||||
createButton({
|
||||
label: L("Common_Btn_Confirm"),
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
if (!pending) {
|
||||
showToast(L("B03_Temp_Select_Required"), "warning");
|
||||
return;
|
||||
}
|
||||
applySelection(pending);
|
||||
close();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
modal.append(title, list, actions);
|
||||
backdrop.append(modal);
|
||||
backdrop.addEventListener("click", (event) => {
|
||||
if (event.target === backdrop) close();
|
||||
openDialog({
|
||||
title: L("B03_Temp_Modal_Title"),
|
||||
closeLabel: L("Common_Btn_Cancel"),
|
||||
width: "40rem",
|
||||
dialogClass: "b03-file__temp-modal",
|
||||
mount: (body, close, actions) => {
|
||||
const list = document.createElement("div");
|
||||
list.className = "b03-file__temp-list";
|
||||
if (batches.length === 0) {
|
||||
list.append(createEmpty({ text: L("B03_Temp_Modal_Empty"), small: true }));
|
||||
}
|
||||
for (const batch of batches) {
|
||||
const files = batch.files.map((file) => file.file_type.toUpperCase()).join(", ");
|
||||
const option = createCard({
|
||||
title: batch.name,
|
||||
pad: 12,
|
||||
selected: pending?.batch_id === batch.batch_id,
|
||||
body: [
|
||||
createText(
|
||||
`${batch.files.length}${L("B03_Temp_FileCount")} · ${formatBytes(
|
||||
batch.total_size_bytes,
|
||||
)}`,
|
||||
),
|
||||
createText(files),
|
||||
],
|
||||
onSelect: () => {
|
||||
pending = batch;
|
||||
list
|
||||
.querySelectorAll<HTMLElement>(".ui-card--select")
|
||||
.forEach((card) => setCardSelected(card, card === option));
|
||||
},
|
||||
});
|
||||
list.append(option);
|
||||
}
|
||||
body.append(list);
|
||||
actions.append(
|
||||
createButton({
|
||||
label: L("Common_Btn_Confirm"),
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
if (!pending) {
|
||||
showToast(L("B03_Temp_Select_Required"), "warning");
|
||||
return;
|
||||
}
|
||||
applySelection(pending);
|
||||
close();
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
document.body.append(backdrop);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { PROGRESS_UPDATE_INTERVAL_MS, UPLOAD_CHUNK_SIZE_MB } from "@config/config_frontend";
|
||||
import { fetchWorkflowState, type WorkflowState } from "../A00_Common/b_workflow_nav";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import { fileFingerprint } from "./B03_FileInput_Fingerprint";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import {
|
||||
@@ -35,41 +35,12 @@ function L(key: keyof typeof ui_locales): string {
|
||||
* 명시적 확인을 받는다(2026-08-04 사용자 지시). 확인 시에만 resolve(true).
|
||||
*/
|
||||
export function confirmReplaceUpload(slotLabel: string, fileName: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.className = "b03-file__modal-backdrop";
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "b03-file__modal";
|
||||
modal.setAttribute("role", "alertdialog");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = L("B03_File_Replace_Title");
|
||||
const message = document.createElement("p");
|
||||
message.textContent = `${slotLabel}: ${fileName}\n${L("B03_File_Replace_Message")}`;
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "b03-file__modal-actions";
|
||||
const done = (accepted: boolean): void => {
|
||||
backdrop.remove();
|
||||
resolve(accepted);
|
||||
};
|
||||
const cancel = createButton({
|
||||
label: L("B03_File_Replace_Cancel"),
|
||||
variant: "ghost",
|
||||
onClick: () => done(false),
|
||||
});
|
||||
const accept = createButton({
|
||||
label: L("B03_File_Replace_Confirm"),
|
||||
variant: "filled",
|
||||
onClick: () => done(true),
|
||||
});
|
||||
actions.append(cancel, accept);
|
||||
modal.append(title, message, actions);
|
||||
backdrop.append(modal);
|
||||
backdrop.addEventListener("click", (event) => {
|
||||
if (event.target === backdrop) done(false);
|
||||
});
|
||||
document.body.append(backdrop);
|
||||
accept.focus();
|
||||
return askConfirm({
|
||||
title: L("B03_File_Replace_Title"),
|
||||
message: `${slotLabel}: ${fileName}\n${L("B03_File_Replace_Message")}`,
|
||||
okLabel: L("B03_File_Replace_Confirm"),
|
||||
cancelLabel: L("B03_File_Replace_Cancel"),
|
||||
danger: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,21 +66,23 @@ async function askReplaceOutputs<T>(
|
||||
|
||||
지워지는 것: ${error.targets.join(" · ")}`
|
||||
: "";
|
||||
const agreed = window.confirm(
|
||||
`${error.message}${detail}
|
||||
const agreed = await askConfirm({
|
||||
message: `${error.message}${detail}
|
||||
|
||||
되돌릴 수 없습니다. 계속할까요?`,
|
||||
);
|
||||
danger: true,
|
||||
});
|
||||
if (!agreed) throw new Error("업로드를 취소했습니다 — 기존 결과는 그대로 있습니다.");
|
||||
// 지형 파일이 그대로면 전처리 결과는 살린다 — 「처음부터 다시」는 여기서 따로 고른다.
|
||||
const freshStart =
|
||||
error.preprocessKept &&
|
||||
window.confirm(
|
||||
`같은 지형 파일이라 전처리 결과(지표면)는 살려 다시 씁니다.
|
||||
(await askConfirm({
|
||||
message: `같은 지형 파일이라 전처리 결과(지표면)는 살려 다시 씁니다.
|
||||
|
||||
전처리까지 처음부터 다시 할까요?
|
||||
[확인] 처음부터 다시 · [취소] 전처리 결과 살림`,
|
||||
);
|
||||
전처리까지 처음부터 다시 할까요?`,
|
||||
okLabel: "처음부터 다시",
|
||||
cancelLabel: "전처리 결과 살림",
|
||||
}));
|
||||
return await run(true, freshStart);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user