feat(B03): 고르는 자리 한 줄 · 완료 안내는 좌측 패널 · 결과 목록 제거

사용자 지시 3건(2026-09-03).

- 재접속 현황 배너(「…모두 완료된 프로젝트입니다」)를 좌측 안내 패널 「고르는 방법」
  안으로 옮김 — 고르기 전에 읽을 것과 같은 자리.
- [입력 파일 선택]·[임시 보관함에서 불러오기]·[파일 업로드]를 한 줄(`b03-file__pick-row`)로
  묶음. 좁아지면 접히되 선택 영역이 남는 폭을 먹음.
- 업로드 완료 항목 목록 삭제 — 같은 내용을 카드가 완료 상태로 이미 보여 줌.
  쓰이지 않게 된 `renderUploadResults`와 `.b03-file__results` 규칙도 제거.

곁들여 고친 것 — 카드 격자를 2열 고정에서 `auto-fill minmax(260px, 1fr)` 로 바꿈.
안내 패널을 연 상태에서 카드 폭이 210px 로 눌려 제목이 글자 단위로 접혔음(실측).
제목은 한 줄 말줄임으로 둠.

검증: 한 줄 배치 실측 — 선택 397px · 보관함 331px · 업로드 102px, 높이 76px 로 정렬.
카드 66px(빈 칸)/117px(파일 있음), 제목 줄 높이 21px(한 줄). typecheck·prettier 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 20:39:52 +09:00
co-authored by Claude Opus 5
parent c2b3c56664
commit 68e0a1c67f
4 changed files with 58 additions and 117 deletions
+15 -7
View File
@@ -40,21 +40,29 @@ function section(
return group; return group;
} }
/** 좌측 패널 본문 — 필요한 파일 · 고르는 방법 · 알아 둘 것. */ /**
export function createInputGuide(): HTMLElement { * 좌측 패널 본문 — 필요한 파일 · 고르는 방법 · 알아 둘 것.
*
* `statusNote`(재접속 현황 배너)는 「고르는 방법」 끝에 붙는다 — 프로젝트가 이미 완료라
* 다시 올리면 교체된다는 안내라서, 고르기 전에 읽을 것들과 같은 자리에 둔다
* (2026-09-03 사용자 지시).
*/
export function createInputGuide(statusNote?: HTMLElement): HTMLElement {
const guide = document.createElement("div"); const guide = document.createElement("div");
guide.className = "b03-file__guide"; guide.className = "b03-file__guide";
const howTo = section("B03_Guide_How_Title", [
"B03_Guide_How_Drop",
"B03_Guide_How_Card",
"B03_Guide_How_Temp",
]);
if (statusNote) howTo.append(statusNote);
guide.append( guide.append(
section("B03_Guide_Files_Title", [ section("B03_Guide_Files_Title", [
"B03_Guide_Files_Route", "B03_Guide_Files_Route",
"B03_Guide_Files_Terrain", "B03_Guide_Files_Terrain",
"B03_Guide_Files_Optional", "B03_Guide_Files_Optional",
]), ]),
section("B03_Guide_How_Title", [ howTo,
"B03_Guide_How_Drop",
"B03_Guide_How_Card",
"B03_Guide_How_Temp",
]),
section("B03_Guide_Notes_Title", [ section("B03_Guide_Notes_Title", [
"B03_Guide_Notes_Replace", "B03_Guide_Notes_Replace",
"B03_Guide_Notes_Crs", "B03_Guide_Notes_Crs",
+17 -26
View File
@@ -10,7 +10,7 @@ import { createButton, createTag, showToast } from "@ui/ui_template_elements";
import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { createGeneralLayout } from "@ui/ui_template_general_layout";
import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { createWorkflowOverlays } from "@ui/ui_template_overlay";
import { createStepBar, WORKFLOW_STEP_ICONS } from "@ui/ui_template_workflow_layout"; import { createStepBar, WORKFLOW_STEP_ICONS } from "@ui/ui_template_workflow_layout";
import { fetchUploadOverview, type UploadedFileResult } from "./B03_FileInput_Api_Fetch"; import { fetchUploadOverview } from "./B03_FileInput_Api_Fetch";
import { clearPreloadMark } from "../A00_Common/b_asset_cache"; import { clearPreloadMark } from "../A00_Common/b_asset_cache";
import { navigateTo } from "../A00_Common/router"; import { navigateTo } from "../A00_Common/router";
import { attachTempBatch } from "../B01_Dashboard/B01_Dashboard_Api_Temp"; import { attachTempBatch } from "../B01_Dashboard/B01_Dashboard_Api_Temp";
@@ -29,7 +29,6 @@ import {
confirmReplaceUpload, confirmReplaceUpload,
isInitialPipelineRunning, isInitialPipelineRunning,
pollInitialPipeline, pollInitialPipeline,
renderUploadResults,
uploadOneFile, uploadOneFile,
} from "./B03_FileInput_UI_Upload"; } from "./B03_FileInput_UI_Upload";
import { import {
@@ -63,8 +62,6 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
// 대시보드 임시 보관함에서 가져올 자료 선택기 — 선택되면 [업로드]가 이동을 수행한다. // 대시보드 임시 보관함에서 가져올 자료 선택기 — 선택되면 [업로드]가 이동을 수행한다.
const tempPicker = createTempPicker(() => updateUploadButton()); const tempPicker = createTempPicker(() => updateUploadButton());
const cardMap = new Map<FileSlot, HTMLElement>(); const cardMap = new Map<FileSlot, HTMLElement>();
const resultList = document.createElement("ul");
resultList.className = "b03-file__results";
let uploadButton: HTMLButtonElement; let uploadButton: HTMLButtonElement;
let resumeBanner: HTMLDivElement; let resumeBanner: HTMLDivElement;
let activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? ""; let activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
@@ -653,22 +650,20 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
pageError.textContent = ""; pageError.textContent = "";
setUploading(true); setUploading(true);
const uploaded: UploadedFileResult[] = [];
try { try {
for (let index = 0; index < targetStates.length; index += 1) { for (let index = 0; index < targetStates.length; index += 1) {
const state = targetStates[index]; const state = targetStates[index];
uploaded.push( await uploadOneFile(
...(await uploadOneFile( activeProjectId,
activeProjectId, state,
state, index === targetStates.length - 1,
index === targetStates.length - 1, () => renderSlot(state.slot),
() => renderSlot(state.slot), lasFreeDesign,
lasFreeDesign,
)),
); );
} }
clearDerivedCaches(activeProjectId); clearDerivedCaches(activeProjectId);
renderUploadResults(resultList, uploaded); // 업로드 결과는 카드가 이미 완료 상태로 보여 준다 — 같은 내용을 목록으로 또 쌓지
// 않는다(2026-09-03 사용자 지시).
showToast(L("B03_File_Upload_Success"), "success"); showToast(L("B03_File_Upload_Success"), "success");
showToast(L("B03_File_Analysis_InProgress"), "info"); showToast(L("B03_File_Analysis_InProgress"), "info");
@@ -737,19 +732,15 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
disabled: true, disabled: true,
}); });
// 고르는 자리 한 줄 — [입력 파일 선택] · [임시 보관함에서 불러오기] · [파일 업로드]
// (2026-09-03 사용자 지시). 셋이 한 동작의 앞뒤라 흩어 놓을 이유가 없다.
const pickRow = document.createElement("div");
pickRow.className = "b03-file__pick-row";
pickRow.append(dropzone, tempPicker.root, uploadButton);
const uploadControlPanel = document.createElement("div"); const uploadControlPanel = document.createElement("div");
uploadControlPanel.className = "b03-file__control-panel"; uploadControlPanel.className = "b03-file__control-panel";
uploadControlPanel.append( uploadControlPanel.append(subtitle, pickRow, resumeBanner, pageError);
subtitle,
overviewBanner,
dropzone,
// 대시보드 임시 보관함에서 자료를 끌어오는 자리 — 업로드 컨테이너 안에 둔다.
tempPicker.root,
resumeBanner,
pageError,
uploadButton,
resultList,
);
// 자료의 출처가 둘로 갈린다 — 원청이 준 계획노선, 측량이 준 지형(LAS·래스터). // 자료의 출처가 둘로 갈린다 — 원청이 준 계획노선, 측량이 준 지형(LAS·래스터).
// 좌표계 파일(.prj)도 각각 하나씩 오므로 컨테이너를 나눠야 어느 칸에 무엇을 넣는지 // 좌표계 파일(.prj)도 각각 하나씩 오므로 컨테이너를 나눠야 어느 칸에 무엇을 넣는지
@@ -848,7 +839,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
const overlays = createWorkflowOverlays({ const overlays = createWorkflowOverlays({
title: L("B03_File_Title"), title: L("B03_File_Title"),
progressContent, progressContent,
optionsContent: createInputGuide(), optionsContent: createInputGuide(overviewBanner),
// 공용 워크플로 레이아웃(B04·B05)이 하는 일을 여기서도 한다 — 패널이 열리면 본문을 // 공용 워크플로 레이아웃(B04·B05)이 하는 일을 여기서도 한다 — 패널이 열리면 본문을
// 그만큼 밀어내지 않으면 안내가 카드 위를 덮는다. // 그만큼 밀어내지 않으면 안내가 카드 위를 덮는다.
onOptionsOpenChange: (isOpen) => layout.root.classList.toggle("is-options-open", isOpen), onOptionsOpenChange: (isOpen) => layout.root.classList.toggle("is-options-open", isOpen),
+26 -68
View File
@@ -45,6 +45,25 @@
} }
/* Wiza 스타일 가이드의 Lavender Glow 그라데이션 및 soft purple border hover */ /* Wiza 스타일 가이드의 Lavender Glow 그라데이션 및 soft purple border hover */
/* 고르는 자리 한 줄 — [입력 파일 선택] · [임시 보관함] · [파일 업로드](2026-09-03).
좁아지면 아래로 접히되, 선택 영역이 남는 폭을 먹는다. */
.b03-file__pick-row {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: var(--spacing-8);
}
.b03-file__pick-row .b03-file__dropzone {
flex: 1 1 320px;
}
.b03-file__pick-row .b03-file__temp-picker,
.b03-file__pick-row .ui-btn {
flex: 0 0 auto;
align-self: stretch;
}
/* 한 줄 바(2026-09-03) — 종전 160px 블록은 안내 문구를 담느라 컸다. 문구는 좌측 /* 한 줄 바(2026-09-03) — 종전 160px 블록은 안내 문구를 담느라 컸다. 문구는 좌측
패널로 갔으므로 여기는 「끌어 놓거나 눌러서 고르는 자리」 표시만 한다. */ 패널로 갔으므로 여기는 「끌어 놓거나 눌러서 고르는 자리」 표시만 한다. */
.b03-file__dropzone { .b03-file__dropzone {
@@ -266,9 +285,11 @@
margin: 0; margin: 0;
} }
/* 카드가 좁아지면 한 줄에 제목·상태·[선택]이 못 들어가 제목이 글자 단위로 접힌다
(2026-09-03 실측: 안내 패널을 연 상태에서 카드 폭 210px). 폭이 모자라면 열을 줄인다. */
.b03-file__group-content { .b03-file__group-content {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--spacing-12); gap: var(--spacing-12);
} }
@@ -371,11 +392,10 @@
color: var(--color-deep-iris, #26114a); color: var(--color-deep-iris, #26114a);
font-size: var(--text-body-sm, 14px); font-size: var(--text-body-sm, 14px);
font-weight: var(--font-weight-medium, 500); font-weight: var(--font-weight-medium, 500);
/* 한글은 글자 단위로 끊기므로 어절을 지켜 준다 — "계획 /* 제목은 한 줄로 두고 모자라면 말줄임 — 접히면 카드가 세로로 길어진다(2026-09-03). */
노선 white-space: nowrap;
도형" 방지. */ overflow: hidden;
word-break: keep-all; text-overflow: ellipsis;
overflow-wrap: break-word;
} }
.b03-file__card-ext, .b03-file__card-ext,
@@ -525,65 +545,3 @@
.b03-file__card--error .b03-file__progress-section { .b03-file__card--error .b03-file__progress-section {
display: none; display: none;
} }
.b03-file__results {
margin: var(--spacing-8) 0 0 0;
padding: 0;
list-style: none;
border: 1px solid var(--color-mist, #e6e2e3);
border-radius: var(--radius-cards, 8px);
overflow: hidden;
background: var(--color-canvas, #ffffff);
}
.b03-file__results:empty {
display: none;
}
.b03-file__results li {
padding: var(--spacing-12) var(--spacing-16);
display: flex;
flex-direction: column;
gap: 2px;
border-bottom: 1px solid var(--color-mist, #e6e2e3);
color: var(--color-deep-iris, #26114a);
font-size: var(--text-body-sm, 14px);
}
.b03-file__results li:last-child {
border-bottom: 0;
}
@media (max-width: 1440px) {
/* 좁아지면 두 컨테이너를 위아래로 쌓는다 — 카드가 눌려 글자가 접히는 것을 막는다. */
.b03-file__columns {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.b03-file {
padding: var(--spacing-16) var(--spacing-12);
}
.b03-file__group-content {
grid-template-columns: 1fr; /* 모바일에서는 1행 1열 구조 */
}
}
/* LAS 없는 설계(도엽등고선 기반) 토글 — 2026-08-30 */
.b03-file__lasfree {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 2px;
font-size: var(--text-body);
color: var(--color-text-body);
cursor: pointer;
user-select: none;
}
.b03-file__card--disabled {
opacity: 0.45;
pointer-events: none;
}
-16
View File
@@ -73,22 +73,6 @@ export function confirmReplaceUpload(slotLabel: string, fileName: string): Promi
} }
/** 업로드 결과 목록(파일명 + 저장 경로)을 다시 그린다. */ /** 업로드 결과 목록(파일명 + 저장 경로)을 다시 그린다. */
export function renderUploadResults(
list: HTMLElement,
results: readonly UploadedFileResult[],
): void {
list.replaceChildren();
for (const result of results) {
const item = document.createElement("li");
const filename = document.createElement("strong");
filename.textContent = result.original_filename;
const path = document.createElement("span");
path.textContent = `${L("B03_File_Result_Path")}: ${result.relative_path}`;
item.append(filename, path);
list.append(item);
}
}
/** /**
* 파일 1건을 청크로 올린다. 중단된 세션이 있으면 그 지점부터 이어 올린다. * 파일 1건을 청크로 올린다. 중단된 세션이 있으면 그 지점부터 이어 올린다.
* 진행 상황은 `onProgress`로만 알린다 — 갱신 주기는 config 값으로 제한한다. * 진행 상황은 `onProgress`로만 알린다 — 갱신 주기는 config 값으로 제한한다.