From a059c5f1527f07c9431ef910793582b475c6b091 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:31:00 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat(B03):=20=ED=8C=8C=EC=9D=BC=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=96=91=EC=8B=9D=20=EC=A0=95=EB=A6=AC=20=E2=80=94?= =?UTF-8?q?=20=EC=95=88=EB=82=B4=EB=8A=94=20=EC=A2=8C=EC=B8=A1=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90,=20=EB=B3=B8=EB=AC=B8=EC=9D=80=20=EA=B3=A0=EB=A5=B4?= =?UTF-8?q?=EB=8A=94=20=EC=9E=90=EB=A6=AC=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지적(2026-09-03) — 선택 항목이 너무 크고 대중없이 놓임. 안내는 B04·B05 와 같은 공용 좌측 패널로 빼고 본문은 고르는 자리만 남김. - `B03_FileInput_UI_Guide.ts` 신설 — 필요한 파일 / 고르는 방법 / 알아 둘 것 3묶음을 공용 오버레이 `optionsContent` 에 실음. 문단 구획은 공용 `ui-sidebar-section`. - 선택 영역 160px 블록 → 한 줄 바. 긴 문구는 패널로 옮기고 힌트는 한 줄로 교체. - 카드 `min-height: 220px` 폐지, [선택]을 제목 줄 오른쪽 끝 작은 버튼으로 이동, 빈 칸은 제목 줄만 남김. 진행바·속도·예상완료는 올라가는 동안만 표시. - 여백 정리 — 컨테이너 패딩 32 → 20, 그룹 제목 24 → 16px, 격자 간격 24 → 12. - 좌측 패널이 열리면 본문을 밀어내는 규칙 추가 — B03 은 일반 레이아웃이라 공용 워크플로 레이아웃의 padding 규칙이 없어 패널이 카드를 덮었음. 검증: 카드 높이 232 → 66px(빈 칸)·117px(파일 있음), 선택 영역 160 → 47px, 문서 높이 1408px 로 한 화면 남짓. typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_UI_Guide.ts | 65 +++++++++++ B03_FileInput/B03_FileInput_UI_Page.ts | 8 +- B03_FileInput/B03_FileInput_UI_Style.css | 129 +++++++++++++++++----- B03_FileInput/B03_FileInput_UI_Support.ts | 2 +- ui_template/ui_template_locale_b1.ts | 46 +++++++- 5 files changed, 216 insertions(+), 34 deletions(-) create mode 100644 B03_FileInput/B03_FileInput_UI_Guide.ts diff --git a/B03_FileInput/B03_FileInput_UI_Guide.ts b/B03_FileInput/B03_FileInput_UI_Guide.ts new file mode 100644 index 00000000..fce2ccd7 --- /dev/null +++ b/B03_FileInput/B03_FileInput_UI_Guide.ts @@ -0,0 +1,65 @@ +/* ============================================================================= + * B03_FileInput_UI_Guide.ts + * 파일 입력 좌측 안내 패널 (2026-09-03 사용자 지시). + * + * 종전에는 고르는 방법·필요한 파일 목록이 **선택 영역과 카드 안에** 길게 들어가 있어 + * 카드 한 장이 220px씩 차지했다. 안내는 한 번 읽으면 되는 것이라 다른 단계(B04·B05)와 + * 같은 자리 — 공용 오버레이의 좌측 패널(`createWorkflowOverlays.optionsContent`) — 로 + * 옮기고, 본문은 고르는 자리만 남긴다. + * + * 문단 구획은 공용 `ui-sidebar-section`을 쓴다(B04·B05 좌측 패널과 같은 테두리·간격). + * ========================================================================== */ + +import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; + +function L(key: keyof typeof ui_locales): string { + return ui_locales[key][currentLanguageIndex]; +} + +/** 안내 문단 하나 — 제목 + 항목 목록. */ +function section( + titleKey: keyof typeof ui_locales, + itemKeys: (keyof typeof ui_locales)[], +): HTMLElement { + const group = document.createElement("section"); + group.className = "ui-sidebar-section b03-file__guide-group"; + + const title = document.createElement("h3"); + title.className = "b03-file__guide-title"; + title.textContent = L(titleKey); + + const list = document.createElement("ul"); + list.className = "b03-file__guide-list"; + for (const key of itemKeys) { + const item = document.createElement("li"); + item.textContent = L(key); + list.append(item); + } + + group.append(title, list); + return group; +} + +/** 좌측 패널 본문 — 필요한 파일 · 고르는 방법 · 알아 둘 것. */ +export function createInputGuide(): HTMLElement { + const guide = document.createElement("div"); + guide.className = "b03-file__guide"; + guide.append( + section("B03_Guide_Files_Title", [ + "B03_Guide_Files_Route", + "B03_Guide_Files_Terrain", + "B03_Guide_Files_Optional", + ]), + section("B03_Guide_How_Title", [ + "B03_Guide_How_Drop", + "B03_Guide_How_Card", + "B03_Guide_How_Temp", + ]), + section("B03_Guide_Notes_Title", [ + "B03_Guide_Notes_Replace", + "B03_Guide_Notes_Crs", + "B03_Guide_Notes_LasFree", + ]), + ); + return guide; +} diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index d00be854..1cb44dd8 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -16,6 +16,7 @@ import { navigateTo } from "../A00_Common/router"; import { attachTempBatch } from "../B01_Dashboard/B01_Dashboard_Api_Temp"; import { clearRouteLatestCache } from "../B05_Profile/B05_Profile_Api_Fetch"; import { invalidateSectionDetail } from "../B06_Section/B06_Section_Section_Store"; +import { createInputGuide } from "./B03_FileInput_UI_Guide"; import { createTempPicker } from "./B03_FileInput_UI_TempPicker"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; import { @@ -842,10 +843,15 @@ export async function renderB03FileInput(root: HTMLElement): Promise { content: [uploadControlPanel, cardsContainer], }); layout.content.classList.add("b03-file__main-layout"); + // 고르는 방법·필요한 파일 안내는 좌측 패널로 뺐다 — 본문은 고르는 자리만 남긴다 + // (2026-09-03 사용자 지시). 자리·여닫기는 B04·B05 좌측 패널과 같은 공용 오버레이다. const overlays = createWorkflowOverlays({ title: L("B03_File_Title"), progressContent, - showTitlePanel: false, + optionsContent: createInputGuide(), + // 공용 워크플로 레이아웃(B04·B05)이 하는 일을 여기서도 한다 — 패널이 열리면 본문을 + // 그만큼 밀어내지 않으면 안내가 카드 위를 덮는다. + onOptionsOpenChange: (isOpen) => layout.root.classList.toggle("is-options-open", isOpen), }); layout.root.append(overlays.root); pageRoot = layout.root; diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css index c0f8768f..e77e94c2 100644 --- a/B03_FileInput/B03_FileInput_UI_Style.css +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -19,9 +19,10 @@ .b03-file__control-panel { display: flex; flex-direction: column; - gap: var(--spacing-20); + gap: var(--spacing-16); background: var(--color-canvas, #ffffff); - padding: var(--spacing-32); + /* 여백 32 → 20 (2026-09-03) — 고르는 자리만 남기고 안내를 좌측 패널로 뺐다. */ + padding: var(--spacing-20); border-radius: var(--radius-large, 24px); /* Wiza 24px 대형 둥글기 */ border: 1px solid var(--color-mist, #e6e2e3); box-shadow: var(--shadow-lg); @@ -43,15 +44,17 @@ } /* Wiza 스타일 가이드의 Lavender Glow 그라데이션 및 soft purple border hover */ +/* 한 줄 바(2026-09-03) — 종전 160px 블록은 안내 문구를 담느라 컸다. 문구는 좌측 + 패널로 갔으므로 여기는 「끌어 놓거나 눌러서 고르는 자리」 표시만 한다. */ .b03-file__dropzone { - min-height: 160px; - padding: var(--spacing-32) var(--spacing-24); - border: 2px dashed var(--color-mist, #e6e2e3); - border-radius: var(--radius-large, 24px); + padding: var(--spacing-12) var(--spacing-16); + border: 1px dashed var(--color-mist, #e6e2e3); + border-radius: var(--radius-cards, 8px); background: var(--color-paper, #f6f7fa); display: flex; - flex-direction: column; - align-items: center; + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; justify-content: center; gap: var(--spacing-8); cursor: pointer; @@ -77,7 +80,7 @@ .b03-file__dropzone strong { color: var(--color-deep-iris, #26114a); - font-size: var(--text-body, 16px); + font-size: var(--text-body-sm, 14px); font-weight: var(--font-weight-medium, 500); } @@ -185,14 +188,67 @@ .b03-file__columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--spacing-24); + gap: var(--spacing-16); align-items: start; } +/* 좌측 안내 패널이 열리면 본문을 그만큼 밀어낸다 — 공용 워크플로 레이아웃(B04·B05)의 + `.ui-workflow-layout__body` 규칙과 같은 값. 접히면 손잡이 폭만 남긴다. */ +.b03-file__main-layout { + transition: padding-left var(--transition-base); +} + +.b03-file.is-options-open .b03-file__main-layout { + padding-left: min(var(--wf-left-panel-width), calc(100vw - var(--spacing-48))); +} + +.b03-file:not(.is-options-open) .b03-file__main-layout { + padding-left: var(--spacing-24); +} + +@media (max-width: 860px) { + .b03-file.is-options-open .b03-file__main-layout { + padding-left: 0; + } +} + +/* --- 좌측 안내 패널 (2026-09-03) — 본문에서 뺀 설명이 사는 자리. --- */ +.b03-file__guide { + display: flex; + flex-direction: column; + gap: var(--spacing-12); + padding: var(--spacing-12); +} + +.b03-file__guide-group { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + padding: var(--spacing-12); +} + +.b03-file__guide-title { + margin: 0; + font-size: var(--text-body-sm, 14px); + font-weight: var(--font-weight-medium, 500); + color: var(--color-text, #26114a); +} + +.b03-file__guide-list { + margin: 0; + padding-left: var(--spacing-16); + display: flex; + flex-direction: column; + gap: var(--spacing-4); + font-size: var(--text-caption, 12px); + line-height: 1.55; + color: var(--color-text-secondary, #615e6e); +} + .b03-file__group { display: flex; flex-direction: column; - gap: var(--spacing-20); + gap: var(--spacing-12); } .b03-file__group-hint { @@ -201,17 +257,18 @@ margin: calc(var(--spacing-8) * -1) 0 0 0; } +/* 제목 24px → 본문 크기(2026-09-03) — 컨테이너가 둘뿐이라 큰 제목이 자리만 먹었다. */ .b03-file__group-title { - font-size: var(--text-subheading, 24px); + font-size: var(--text-body, 16px); color: var(--color-deep-iris, #26114a); font-family: var(--font-britti-sans, inherit); - margin: 0 0 var(--spacing-8) 0; + margin: 0; } .b03-file__group-content { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--spacing-24); + gap: var(--spacing-12); } /* 업로드 중에는 화면 조작을 막는다 — 중복 업로드·단계 이동 방지(2026-08-08 사용자 지시). */ @@ -235,16 +292,17 @@ } /* Wiza 8px radius 카드 */ +/* 카드 한 장 = 파일 한 칸. 빈 칸은 **한 줄**로 끝난다(2026-09-03 사용자 지시) — + 파일이 붙거나 올라가는 중에만 아래 내용(파일명·진행바)이 펼쳐진다. */ .b03-file__card { - min-height: 220px; - padding: var(--spacing-24); /* 내부 전체 패딩 확대 */ + padding: var(--spacing-12) var(--spacing-16); border: 1px solid var(--color-mist, #e6e2e3); border-radius: var(--radius-cards, 8px); background: var(--color-canvas, #ffffff); box-shadow: var(--shadow-sm); display: flex; flex-direction: column; - gap: var(--spacing-20); + gap: var(--spacing-8); transition: transform var(--transition-base, 0.2s), border-color var(--transition-base, 0.2s), @@ -277,9 +335,11 @@ /* 카드가 좁아졌다(컨테이너 2열 × 카드 2열, 2026-08-31). 격자로 고정하면 제목이 글자 단위로 접히므로, 자리가 모자라면 배지·삭제 버튼이 다음 줄로 내려가게 한다. */ +/* 제목·상태·[선택]이 **한 줄**에 선다(2026-09-03) — 줄바꿈을 허용하면 [선택]이 아래로 + 내려가 카드가 두 줄이 됐다. 자리가 모자라면 제목이 줄어든다(min-width: 0). */ .b03-file__card-header { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; gap: var(--spacing-8); align-items: center; } @@ -300,11 +360,10 @@ .b03-file__card-heading { min-width: 0; - flex: 1 1 55%; + flex: 1 1 auto; display: flex; flex-direction: column; - gap: 2px; - margin-top: 2px; /* 제목 상단 여유 추가 */ + gap: 1px; } .b03-file__card-label { @@ -357,25 +416,29 @@ } .b03-file__card-content { - flex: 1; display: flex; flex-direction: column; - justify-content: flex-end; - gap: var(--spacing-16); /* 버튼 하단 및 내부 간격 확대 */ - padding-bottom: var(--spacing-8); /* 파일 선택 버튼 하단 패딩 확보 */ + gap: var(--spacing-8); } +/* 빈 칸은 제목 줄만 남긴다 — 파일이 붙으면 이름·크기가, 올라가는 중에는 진행바가 선다. */ +.b03-file__card--empty .b03-file__card-content { + display: none; +} + +/* [선택]은 제목 줄 오른쪽 끝의 작은 버튼 — 종전 전폭 40px 버튼이 카드 높이를 키웠다. */ .b03-file__card-select { - width: 100%; - min-height: 40px; /* 버튼 높이 증가 */ + flex: 0 0 auto; + min-height: 26px; + padding: 0 var(--spacing-8); border: 1px solid var(--color-mist, #e6e2e3); border-radius: var(--radius-buttons, 8px); color: var(--color-deep-iris, #26114a); background: var(--color-canvas, #ffffff); font-weight: var(--font-weight-medium, 500); - font-size: var(--text-body-sm, 14px); + font-size: var(--text-caption, 12px); + white-space: nowrap; cursor: pointer; - margin-bottom: var(--spacing-8); /* 파일 선택 버튼 하단 마진 추가 */ transition: background var(--transition-base, 0.2s), border-color var(--transition-base, 0.2s); @@ -395,12 +458,18 @@ overflow-wrap: anywhere; } +/* 진행바·속도·예상완료는 **올라가는 동안만** 보인다(2026-09-03) — 끝난 카드에 남겨 두면 + 읽을 일 없는 세 줄이 카드 높이를 그대로 차지했다. */ .b03-file__progress-section { - display: flex; + display: none; flex-direction: column; gap: var(--spacing-8); } +.b03-file__card--uploading .b03-file__progress-section { + display: flex; +} + .b03-file__progress-bar-container { width: 100%; height: 6px; diff --git a/B03_FileInput/B03_FileInput_UI_Support.ts b/B03_FileInput/B03_FileInput_UI_Support.ts index 4ee67cc8..93f17894 100644 --- a/B03_FileInput/B03_FileInput_UI_Support.ts +++ b/B03_FileInput/B03_FileInput_UI_Support.ts @@ -208,10 +208,10 @@ export function createFileCardTemplate(): HTMLTemplateElement {
+
-
diff --git a/ui_template/ui_template_locale_b1.ts b/ui_template/ui_template_locale_b1.ts index 97b8a443..6f54fd60 100644 --- a/ui_template/ui_template_locale_b1.ts +++ b/ui_template/ui_template_locale_b1.ts @@ -224,9 +224,51 @@ export const ui_locales_b1 = { "Upload the required planned route, terrain, and point cloud files.", ], B03_File_Select_Label: ["입력 파일 선택", "Select input files"], + /* 긴 설명은 좌측 안내 패널(B03_Guide_*)로 옮겼다 — 선택 영역은 한 줄만 남긴다 + (2026-09-03 사용자 지시). */ B03_File_Select_Hint: [ - "계획노선(CSV 또는 shapefile 5개)과 LAS/LAZ 1개, 지형 PRJ·TFW를 함께 고르면 카드에 나뉩니다. TIF는 선택 사항입니다.", - "Pick the planned route (a CSV or the five shapefile files), one LAS/LAZ, and the terrain PRJ and TFW together — they are sorted into cards. TIF is optional.", + "끌어 놓거나 눌러서 고르면 확장자대로 카드에 나뉩니다.", + "Drop files here or click to pick — they are sorted into cards by extension.", + ], + /* --- 좌측 안내 패널 (2026-09-03) --- */ + B03_Guide_Files_Title: ["필요한 파일", "Files you need"], + B03_Guide_Files_Route: [ + "계획노선 — CSV 한 장, 또는 shapefile 한 벌(.shp·.shx·.dbf·.cpg·.prj) 5개.", + "Planned route — one CSV, or the five shapefile parts (.shp, .shx, .dbf, .cpg, .prj).", + ], + B03_Guide_Files_Terrain: [ + "지형 — 포인트클라우드 LAS/LAZ 1개와 좌표계 PRJ, 래스터 기준 TFW.", + "Terrain — one LAS/LAZ point cloud plus the PRJ projection and TFW world file.", + ], + B03_Guide_Files_Optional: [ + "지형 TIF(DEM)는 선택 사항 — 없어도 지표면 분석이 진행됩니다.", + "The terrain TIF (DEM) is optional — surface analysis runs without it.", + ], + B03_Guide_How_Title: ["고르는 방법", "How to pick"], + B03_Guide_How_Drop: [ + "여러 개를 한 번에 끌어 놓으면 확장자로 카드에 자동 배정됩니다.", + "Drop several files at once — each is assigned to a card by its extension.", + ], + B03_Guide_How_Card: [ + "카드의 [선택]으로 한 칸씩 지정하거나 ✕로 되돌릴 수 있습니다.", + "Use a card's [Select] to set one slot, or ✕ to clear it.", + ], + B03_Guide_How_Temp: [ + "[임시 보관함에서 불러오기]는 대시보드에 미리 올려 둔 자료를 가져옵니다.", + "[Load from temporary storage] pulls files you staged on the dashboard.", + ], + B03_Guide_Notes_Title: ["알아 둘 것", "Before you upload"], + B03_Guide_Notes_Replace: [ + "이미 완료된 프로젝트에 다시 올리면 기존 분석 결과가 교체됩니다.", + "Uploading again to a finished project replaces the existing analysis.", + ], + B03_Guide_Notes_Crs: [ + "작업 좌표계는 라이다 PRJ가 기준 — 노선 좌표계와 다르면 업로드가 막힙니다.", + "The LiDAR PRJ sets the working CRS — a mismatched route CRS blocks the upload.", + ], + B03_Guide_Notes_LasFree: [ + "라이다가 없으면 [LAS 없는 설계]를 켜고 도엽 등고선으로 진행합니다.", + "With no LiDAR, turn on [Design without LAS] and work from sheet contours.", ], B03_File_Selected_Title: ["선택한 파일", "Selected files"], B03_File_Selected_Empty: ["선택한 파일이 없습니다.", "No files selected."], From c2b3c566640219328e3edb902ab415b76105dbe0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:35:29 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix(B03):=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=ED=8C=A8=EB=94=A9=20=EB=B3=B5=EA=B5=AC=20=E2=80=94?= =?UTF-8?q?=20=ED=85=8C=EB=A7=88=EC=97=90=20=EC=97=86=EB=8A=94=20`--spacin?= =?UTF-8?q?g-20`=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `padding: var(--spacing-20)` 은 정의되지 않은 토큰이라 선언째 무효가 되어 카드 컨테이너의 패딩이 통째로 사라졌음(2026-09-03 사용자 보고). 16px 토큰으로 교체. 검증: 실측 계산값 — 컨테이너 16px, 카드 12/16px, 선택 바 12/16px, 안내 문단 12px. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_UI_Style.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css index e77e94c2..d53a46a4 100644 --- a/B03_FileInput/B03_FileInput_UI_Style.css +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -21,8 +21,9 @@ flex-direction: column; gap: var(--spacing-16); background: var(--color-canvas, #ffffff); - /* 여백 32 → 20 (2026-09-03) — 고르는 자리만 남기고 안내를 좌측 패널로 뺐다. */ - padding: var(--spacing-20); + /* 여백 32 → 16 (2026-09-03) — 고르는 자리만 남기고 안내를 좌측 패널로 뺐다. + `--spacing-20`은 테마에 없는 토큰이라 선언째 무효가 된다(패딩이 통째로 사라짐). */ + padding: var(--spacing-16); border-radius: var(--radius-large, 24px); /* Wiza 24px 대형 둥글기 */ border: 1px solid var(--color-mist, #e6e2e3); box-shadow: var(--shadow-lg); From 68e0a1c67f225b58142d5e817bfd0d6ba1a7a5a8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:39:52 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat(B03):=20=EA=B3=A0=EB=A5=B4=EB=8A=94=20?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=ED=95=9C=20=EC=A4=84=20=C2=B7=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=95=88=EB=82=B4=EB=8A=94=20=EC=A2=8C=EC=B8=A1=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=C2=B7=20=EA=B2=B0=EA=B3=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시 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) --- B03_FileInput/B03_FileInput_UI_Guide.ts | 22 ++++-- B03_FileInput/B03_FileInput_UI_Page.ts | 43 +++++------ B03_FileInput/B03_FileInput_UI_Style.css | 94 +++++++----------------- B03_FileInput/B03_FileInput_UI_Upload.ts | 16 ---- 4 files changed, 58 insertions(+), 117 deletions(-) diff --git a/B03_FileInput/B03_FileInput_UI_Guide.ts b/B03_FileInput/B03_FileInput_UI_Guide.ts index fce2ccd7..dfa389d3 100644 --- a/B03_FileInput/B03_FileInput_UI_Guide.ts +++ b/B03_FileInput/B03_FileInput_UI_Guide.ts @@ -40,21 +40,29 @@ function section( return group; } -/** 좌측 패널 본문 — 필요한 파일 · 고르는 방법 · 알아 둘 것. */ -export function createInputGuide(): HTMLElement { +/** + * 좌측 패널 본문 — 필요한 파일 · 고르는 방법 · 알아 둘 것. + * + * `statusNote`(재접속 현황 배너)는 「고르는 방법」 끝에 붙는다 — 프로젝트가 이미 완료라 + * 다시 올리면 교체된다는 안내라서, 고르기 전에 읽을 것들과 같은 자리에 둔다 + * (2026-09-03 사용자 지시). + */ +export function createInputGuide(statusNote?: HTMLElement): HTMLElement { const guide = document.createElement("div"); 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( section("B03_Guide_Files_Title", [ "B03_Guide_Files_Route", "B03_Guide_Files_Terrain", "B03_Guide_Files_Optional", ]), - section("B03_Guide_How_Title", [ - "B03_Guide_How_Drop", - "B03_Guide_How_Card", - "B03_Guide_How_Temp", - ]), + howTo, section("B03_Guide_Notes_Title", [ "B03_Guide_Notes_Replace", "B03_Guide_Notes_Crs", diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index 1cb44dd8..300589f4 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -10,7 +10,7 @@ import { createButton, createTag, showToast } from "@ui/ui_template_elements"; 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"; -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 { navigateTo } from "../A00_Common/router"; import { attachTempBatch } from "../B01_Dashboard/B01_Dashboard_Api_Temp"; @@ -29,7 +29,6 @@ import { confirmReplaceUpload, isInitialPipelineRunning, pollInitialPipeline, - renderUploadResults, uploadOneFile, } from "./B03_FileInput_UI_Upload"; import { @@ -63,8 +62,6 @@ export async function renderB03FileInput(root: HTMLElement): Promise { // 대시보드 임시 보관함에서 가져올 자료 선택기 — 선택되면 [업로드]가 이동을 수행한다. const tempPicker = createTempPicker(() => updateUploadButton()); const cardMap = new Map(); - const resultList = document.createElement("ul"); - resultList.className = "b03-file__results"; let uploadButton: HTMLButtonElement; let resumeBanner: HTMLDivElement; let activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? ""; @@ -653,22 +650,20 @@ export async function renderB03FileInput(root: HTMLElement): Promise { pageError.textContent = ""; setUploading(true); - const uploaded: UploadedFileResult[] = []; try { for (let index = 0; index < targetStates.length; index += 1) { const state = targetStates[index]; - uploaded.push( - ...(await uploadOneFile( - activeProjectId, - state, - index === targetStates.length - 1, - () => renderSlot(state.slot), - lasFreeDesign, - )), + await uploadOneFile( + activeProjectId, + state, + index === targetStates.length - 1, + () => renderSlot(state.slot), + lasFreeDesign, ); } clearDerivedCaches(activeProjectId); - renderUploadResults(resultList, uploaded); + // 업로드 결과는 카드가 이미 완료 상태로 보여 준다 — 같은 내용을 목록으로 또 쌓지 + // 않는다(2026-09-03 사용자 지시). showToast(L("B03_File_Upload_Success"), "success"); showToast(L("B03_File_Analysis_InProgress"), "info"); @@ -737,19 +732,15 @@ export async function renderB03FileInput(root: HTMLElement): Promise { 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"); uploadControlPanel.className = "b03-file__control-panel"; - uploadControlPanel.append( - subtitle, - overviewBanner, - dropzone, - // 대시보드 임시 보관함에서 자료를 끌어오는 자리 — 업로드 컨테이너 안에 둔다. - tempPicker.root, - resumeBanner, - pageError, - uploadButton, - resultList, - ); + uploadControlPanel.append(subtitle, pickRow, resumeBanner, pageError); // 자료의 출처가 둘로 갈린다 — 원청이 준 계획노선, 측량이 준 지형(LAS·래스터). // 좌표계 파일(.prj)도 각각 하나씩 오므로 컨테이너를 나눠야 어느 칸에 무엇을 넣는지 @@ -848,7 +839,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise { const overlays = createWorkflowOverlays({ title: L("B03_File_Title"), progressContent, - optionsContent: createInputGuide(), + optionsContent: createInputGuide(overviewBanner), // 공용 워크플로 레이아웃(B04·B05)이 하는 일을 여기서도 한다 — 패널이 열리면 본문을 // 그만큼 밀어내지 않으면 안내가 카드 위를 덮는다. onOptionsOpenChange: (isOpen) => layout.root.classList.toggle("is-options-open", isOpen), diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css index d53a46a4..e654bc45 100644 --- a/B03_FileInput/B03_FileInput_UI_Style.css +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -45,6 +45,25 @@ } /* 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 블록은 안내 문구를 담느라 컸다. 문구는 좌측 패널로 갔으므로 여기는 「끌어 놓거나 눌러서 고르는 자리」 표시만 한다. */ .b03-file__dropzone { @@ -266,9 +285,11 @@ margin: 0; } +/* 카드가 좁아지면 한 줄에 제목·상태·[선택]이 못 들어가 제목이 글자 단위로 접힌다 + (2026-09-03 실측: 안내 패널을 연 상태에서 카드 폭 210px). 폭이 모자라면 열을 줄인다. */ .b03-file__group-content { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--spacing-12); } @@ -371,11 +392,10 @@ color: var(--color-deep-iris, #26114a); font-size: var(--text-body-sm, 14px); font-weight: var(--font-weight-medium, 500); - /* 한글은 글자 단위로 끊기므로 어절을 지켜 준다 — "계획 -노선 -도형" 방지. */ - word-break: keep-all; - overflow-wrap: break-word; + /* 제목은 한 줄로 두고 모자라면 말줄임 — 접히면 카드가 세로로 길어진다(2026-09-03). */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .b03-file__card-ext, @@ -525,65 +545,3 @@ .b03-file__card--error .b03-file__progress-section { 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; -} diff --git a/B03_FileInput/B03_FileInput_UI_Upload.ts b/B03_FileInput/B03_FileInput_UI_Upload.ts index dafe030b..3f69e449 100644 --- a/B03_FileInput/B03_FileInput_UI_Upload.ts +++ b/B03_FileInput/B03_FileInput_UI_Upload.ts @@ -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건을 청크로 올린다. 중단된 세션이 있으면 그 지점부터 이어 올린다. * 진행 상황은 `onProgress`로만 알린다 — 갱신 주기는 config 값으로 제한한다. From d9fcfa3b96564c439dc80c3f5613d6a50d1fb536 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:42:50 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor(B03):=20=EC=A2=8C=EC=B8=A1=20?= =?UTF-8?q?=EC=95=88=EB=82=B4=20=ED=8C=A8=EB=84=90=EC=9D=84=20B04~B07=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=EC=96=91=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 처음엔 B03 전용 스타일로 문단을 만들었으나, 좌측 패널 양식은 다른 단계와 같아야 한다는 사용자 지시(2026-09-03). 공용 조립으로 교체함. - 패널 루트 `b03-file__form`, 문단은 `ui-collapsible ui-sidebar-section`, 제목은 `ui-collapsible__title` — 외곽선·캐럿·접힘 동작이 전부 공용 CSS·동작이다. - 값도 `b04-surface__form`·`__group`·`__group-legend` 와 같게 맞춤 (gap·padding 16, 배경 `--color-surface-raised`, legend `--text-caption`). 검증: 실측 — 문단 클래스 `b03-file__guide-group ui-collapsible ui-sidebar-section`, 패딩 16px, 배경 `rgb(37,31,56)`, 제목 클릭 시 `is-collapsed` 토글·복귀 확인. typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_UI_Guide.ts | 14 ++++++++----- B03_FileInput/B03_FileInput_UI_Style.css | 25 +++++++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/B03_FileInput/B03_FileInput_UI_Guide.ts b/B03_FileInput/B03_FileInput_UI_Guide.ts index dfa389d3..f0e40830 100644 --- a/B03_FileInput/B03_FileInput_UI_Guide.ts +++ b/B03_FileInput/B03_FileInput_UI_Guide.ts @@ -7,25 +7,28 @@ * 같은 자리 — 공용 오버레이의 좌측 패널(`createWorkflowOverlays.optionsContent`) — 로 * 옮기고, 본문은 고르는 자리만 남긴다. * - * 문단 구획은 공용 `ui-sidebar-section`을 쓴다(B04·B05 좌측 패널과 같은 테두리·간격). + * 패널 양식은 **B04~B07과 같은 공용 양식**을 그대로 쓴다(2026-09-03 사용자 지시): + * 패널 루트 `{page}__form`, 문단은 `ui-collapsible ui-sidebar-section` + 제목에 + * `ui-collapsible__title`. 제목 행을 누르면 접히는 동작·캐럿·외곽선이 전부 공용 것이다. * ========================================================================== */ +import { attachCollapsible } from "@ui/ui_template_collapsible"; import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } -/** 안내 문단 하나 — 제목 + 항목 목록. */ +/** 안내 문단 하나 — 공용 접기 컨테이너(B06 `buildGroup`과 같은 조립). */ function section( titleKey: keyof typeof ui_locales, itemKeys: (keyof typeof ui_locales)[], ): HTMLElement { const group = document.createElement("section"); - group.className = "ui-sidebar-section b03-file__guide-group"; + group.className = "b03-file__guide-group ui-collapsible ui-sidebar-section"; const title = document.createElement("h3"); - title.className = "b03-file__guide-title"; + title.className = "b03-file__guide-legend ui-collapsible__title"; title.textContent = L(titleKey); const list = document.createElement("ul"); @@ -49,7 +52,7 @@ function section( */ export function createInputGuide(statusNote?: HTMLElement): HTMLElement { const guide = document.createElement("div"); - guide.className = "b03-file__guide"; + guide.className = "b03-file__form"; const howTo = section("B03_Guide_How_Title", [ "B03_Guide_How_Drop", "B03_Guide_How_Card", @@ -69,5 +72,6 @@ export function createInputGuide(statusNote?: HTMLElement): HTMLElement { "B03_Guide_Notes_LasFree", ]), ); + attachCollapsible(guide); return guide; } diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css index e654bc45..ce3677b2 100644 --- a/B03_FileInput/B03_FileInput_UI_Style.css +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -232,26 +232,33 @@ } } -/* --- 좌측 안내 패널 (2026-09-03) — 본문에서 뺀 설명이 사는 자리. --- */ -.b03-file__guide { +/* --- 좌측 안내 패널 (2026-09-03) — 본문에서 뺀 설명이 사는 자리. + 양식은 B04~B07 좌측 패널과 **같은 공용 양식**이다: 루트 `__form`, 문단은 + `ui-collapsible ui-sidebar-section`(외곽선·접힘·캐럿은 공용 CSS 몫), 제목은 + `__group-legend` 크기. 여기서는 값만 맞추고 새로 만들지 않는다. --- */ +.b03-file__form { display: flex; flex-direction: column; - gap: var(--spacing-12); - padding: var(--spacing-12); + gap: var(--spacing-16); + padding: var(--spacing-16); } .b03-file__guide-group { display: flex; flex-direction: column; gap: var(--spacing-8); - padding: var(--spacing-12); + margin: 0; + padding: var(--spacing-16); + border-radius: var(--radius-cards); + background-color: var(--color-surface-raised); } -.b03-file__guide-title { +.b03-file__guide-legend { margin: 0; - font-size: var(--text-body-sm, 14px); - font-weight: var(--font-weight-medium, 500); - color: var(--color-text, #26114a); + padding: 0 var(--spacing-8); + font-size: var(--text-caption); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); } .b03-file__guide-list { From a00c7497c15afe1efbda78a1c2e46da0424408d6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:47:02 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat(B03):=20=EB=B3=B4=EA=B4=80=ED=95=A8=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=9A=94=EC=95=BD=C2=B7=E3=80=8C=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=ED=95=A0=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=ED=95=98=EC=84=B8=EC=9A=94=E3=80=8D=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=A0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시 2건(2026-09-03). - 임시 보관함 선택 요약(「선택된 보관 자료 없음」·묶음 이름·파일 수)을 없앰. 불러온 자료는 카드가 곧바로 보여 주고, 고르기 전 상태는 [파일 업로드]가 잠긴 것으로 이미 드러남. 쓰이지 않게 된 `.b03-file__temp-summary` 규칙과 로케일 2건도 함께 제거. - 입력 컨테이너의 「업로드할 파일을 선택하세요」 경고를 띄우지 않음 — 아직 아무것도 고르지 않은 상태를 붉은 글씨로 알릴 이유가 없음. 확장자·개수·필수 슬롯 같은 나머지 사유와 [파일 업로드]를 눌렀을 때의 검사 결과는 그대로 표시. 검증: 실측 — 보관함 자리에 버튼 하나만 남음(요약 span 없음), 오류 줄 빈 문자열, 한 줄 배치 선택 518px · 보관함 210px · 업로드 102px. typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_UI_Page.ts | 5 ++++- B03_FileInput/B03_FileInput_UI_Style_Temp.css | 11 ----------- B03_FileInput/B03_FileInput_UI_TempPicker.ts | 12 ++++-------- ui_template/ui_template_locale_b1.ts | 2 -- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index 300589f4..3a95f7be 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -182,7 +182,10 @@ export async function renderB03FileInput(root: HTMLElement): Promise { } const validation = validateSlots(); uploadButton.disabled = validation !== null; - pageError.textContent = validation ?? ""; + // 「업로드할 파일을 선택하세요」는 버튼이 잠긴 것으로 이미 드러난다 — 고르기도 전에 + // 붉은 경고를 띄우지 않는다(2026-09-03 사용자 지시). 나머지 사유는 그대로 알린다. + pageError.textContent = + validation === null || validation === L("B03_File_Error_Required") ? "" : validation; } /** 확장자 줄 — 지금 필수인지에 따라 "· 선택" 꼬리표가 붙고 떨어진다. */ diff --git a/B03_FileInput/B03_FileInput_UI_Style_Temp.css b/B03_FileInput/B03_FileInput_UI_Style_Temp.css index 4dfc2cde..345b0ebf 100644 --- a/B03_FileInput/B03_FileInput_UI_Style_Temp.css +++ b/B03_FileInput/B03_FileInput_UI_Style_Temp.css @@ -14,17 +14,6 @@ border-radius: var(--radius-cards); } -.b03-file__temp-summary { - font-family: var(--font-body); - font-size: var(--text-caption); - color: var(--color-text-secondary); -} - -.b03-file__temp-summary.is-active { - color: var(--color-success); - font-weight: 700; -} - .b03-file__temp-modal { min-width: min(560px, 92vw); max-height: 80vh; diff --git a/B03_FileInput/B03_FileInput_UI_TempPicker.ts b/B03_FileInput/B03_FileInput_UI_TempPicker.ts index a94754dd..52d84a3c 100644 --- a/B03_FileInput/B03_FileInput_UI_TempPicker.ts +++ b/B03_FileInput/B03_FileInput_UI_TempPicker.ts @@ -46,19 +46,15 @@ export function createTempPicker( variant: "ghost", onClick: () => void openModal(), }); - const summary = document.createElement("span"); - summary.className = "b03-file__temp-summary"; - summary.textContent = L("B03_Temp_None"); - root.append(openButton, summary); + // 선택 요약(「선택된 보관 자료 없음」·묶음 이름)은 두지 않는다 — 불러온 자료는 카드가 + // 곧바로 보여 주고, 고르기 전 상태는 [파일 업로드]가 잠긴 것으로 이미 드러난다 + // (2026-09-03 사용자 지시). + root.append(openButton); let selectedBatch: TempBatchItem | null = null; function applySelection(batch: TempBatchItem | null): void { selectedBatch = batch; - summary.textContent = batch - ? `${L("B03_Temp_Selected")} ${batch.name} (${batch.files.length}${L("B03_Temp_FileCount")})` - : L("B03_Temp_None"); - summary.classList.toggle("is-active", Boolean(batch)); onSelected(batch); } diff --git a/ui_template/ui_template_locale_b1.ts b/ui_template/ui_template_locale_b1.ts index 6f54fd60..317ad7e4 100644 --- a/ui_template/ui_template_locale_b1.ts +++ b/ui_template/ui_template_locale_b1.ts @@ -338,8 +338,6 @@ export const ui_locales_b1 = { B03_File_Slot_CadDrawing: ["CAD 도면", "CAD Drawing"], /* --- B03 임시 보관함 불러오기 (2026-08-08) --- */ B03_Temp_Btn_Open: ["임시 보관함에서 불러오기", "Load from temporary storage"], - B03_Temp_None: ["선택된 보관 자료 없음", "No stored set selected"], - B03_Temp_Selected: ["선택됨:", "Selected:"], B03_Temp_FileCount: ["개 파일", " files"], B03_Temp_Modal_Title: ["보관 자료 선택", "Select stored files"], B03_Temp_Modal_Empty: [ From b9e6a2b6a8ecc030702c13a6ad6423f653dfe492 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:55:29 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat(B03):=20LAS=20=EC=97=86=EC=9D=B4=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=ED=86=A0=EA=B8=80=EC=9D=84=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=EB=A1=9C=20?= =?UTF-8?q?=C2=B7=20=EC=BC=9C=EB=A9=B4=20=EC=A7=80=ED=98=95=20=EC=9E=90?= =?UTF-8?q?=EB=A3=8C=20=EC=A0=84=EC=B2=B4=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시(2026-09-03). - 토글을 지형 컨테이너에서 **파일 입력 컨테이너**로 옮김 — 무엇을 받을지 정하는 스위치라 고르는 자리 옆에 선다. 서식도 입혔음(공용 체크 줄 + 켜짐 강조). - 켜면 지형 자료를 **통째로** 받지 않음: 포인트클라우드·좌표계·래스터 카드를 모두 잠그고 컨테이너를 흐림. 필수 판정에서도 지형 슬롯 전체가 빠진다. - 파일 거르기를 확장자가 아니라 **배정된 카드** 기준으로 바꿈 — `.prj` 는 노선·지형이 같은 확장자라 확장자로 거르면 노선 좌표계까지 함께 떨어졌다. - 이미 올라간 자료(재입력)는 지우지 않고 **조작만 잠금** — 로컬로 고른 파일만 내린다. 좌측 안내 패널의 글자 크기도 공용 양식에 맞춤 — 본문 `--text-body-sm`·`--color-text-body` (B04 `b04-surface__check` 와 같음), 제목 `--text-caption`. 검증: 실측 — 토글이 입력 컨테이너 안에 있고, 켜면 지형 카드 4장이 모두 disabled + [파일 선택] 잠김, 기존 업로드분은 「완료」·파일명 유지. 끄면 원복. 패널 글자 14px/12px. typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_UI_Page.ts | 65 ++++++++++++++---------- B03_FileInput/B03_FileInput_UI_Style.css | 56 ++++++++++++++++++-- 2 files changed, 90 insertions(+), 31 deletions(-) diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index 3a95f7be..e3a39138 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -303,28 +303,31 @@ export async function renderB03FileInput(root: HTMLElement): Promise { function onFileSelected(selection: readonly File[], targetSlot?: FileSlot): void { if (selection.length === 0) return; - // LAS 없이 설계를 켜면 포인트클라우드는 아예 받지 않는다 (2026-08-30 사용자 지시) — - // 카드를 회색으로 덮어도 파일 선택 영역·드롭으로 들어올 수 있어 여기서 걸러 낸다. - const pointCloudExtensions = slots.get("las_laz")?.extensions ?? []; - const files = lasFreeDesign - ? selection.filter((file) => !pointCloudExtensions.includes(getExtension(file.name))) - : selection; - const blocked = files.length !== selection.length; - if (blocked && files.length === 0) { - pageError.textContent = L("B03_File_Error_LasFreeBlocked"); - return; - } // 개수는 "고른 파일 수"가 아니라 **최종적으로 차는 슬롯 수**로 센다. // 같은 슬롯을 다시 고르는 것은 교체라 개수가 늘지 않는다 — 더하기로 세면 5개를 고른 // 뒤 파일 선택 영역으로 하나만 바꾸려 해도 초과로 막힌다(2026-08-08). // 파일 하나에 카드 하나다. `.prj`만 확장자로 안 갈리므로 노선 도형과 basename이 // 같은지로 노선/지형 좌표계 카드를 정한다(2026-08-31 사용자 지시). const routeFile = slots.get("csv")?.file?.name; - const assignments = planSlotAssignments( - files, + const planned = planSlotAssignments( + selection, slotConfigs(), routeFile ? routeFile.replace(/\.[^.]*$/, "") : undefined, ); + // LAS 없이 설계를 켜면 **지형 자료를 통째로** 받지 않는다(2026-09-03 사용자 지시 — + // 종전에는 포인트클라우드만 걸렀다). 확장자가 아니라 **배정된 카드**로 거른다: `.prj`는 + // 노선·지형이 같은 확장자라 확장자로 거르면 노선 좌표계까지 함께 떨어진다. + const assignments = lasFreeDesign + ? planned.filter((item) => { + const slot = targetSlot ?? item.slot; + return slot === undefined || !TERRAIN_SLOTS.includes(slot); + }) + : planned; + const blocked = assignments.length !== planned.length; + if (assignments.length === 0) { + pageError.textContent = blocked ? L("B03_File_Error_LasFreeBlocked") : ""; + return; + } const occupied = new Set(selectedStates().map((state) => state.slot)); for (const item of assignments) { const slot = targetSlot ?? item.slot; @@ -380,7 +383,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise { */ function isSlotRequired(state: FileSlotState): boolean { if (SHAPEFILE_DEPENDENT_SLOTS.includes(state.slot)) return routeIsShapefile(); - if (state.slot === "las_laz") return !lasFreeDesign; + // LAS 없이 설계면 지형 자료(포인트클라우드·좌표계·래스터)는 통째로 받지 않는다. + if (TERRAIN_SLOTS.includes(state.slot)) return lasFreeDesign ? false : state.isRequired; return state.isRequired; } @@ -772,18 +776,22 @@ export async function renderB03FileInput(root: HTMLElement): Promise { lasFreeRow.append(lasFreeCheck, lasFreeText); function applyLasFreeState(): void { lasFreeCheck.checked = lasFreeDesign; - const card = cardMap.get("las_laz"); - card?.classList.toggle("b03-file__card--disabled", lasFreeDesign); - // 카드를 회색으로 덮는 것만으로는 선택이 막히지 않는다 — 버튼·input을 실제로 잠근다. - card - ?.querySelectorAll( - ".b03-file__card-select, .b03-file__slot-input", - ) - .forEach((element) => { - element.disabled = lasFreeDesign; - }); - // 켜기 전에 이미 골라 둔 LAS는 내린다 — 켠 채로 남아 올라가는 사고를 막는다. - if (lasFreeDesign && slots.get("las_laz")?.file) removeFile("las_laz"); + terrainGroup.classList.toggle("b03-file__group--disabled", lasFreeDesign); + for (const slot of TERRAIN_SLOTS) { + const card = cardMap.get(slot); + card?.classList.toggle("b03-file__card--disabled", lasFreeDesign); + // 카드를 회색으로 덮는 것만으로는 선택이 막히지 않는다 — 버튼·input을 실제로 잠근다. + card + ?.querySelectorAll( + ".b03-file__card-select, .b03-file__card-remove, .b03-file__slot-input", + ) + .forEach((element) => { + element.disabled = lasFreeDesign; + }); + // 켜기 전에 골라 둔 로컬 파일은 내린다 — 켠 채로 남아 올라가는 사고를 막는다. + // 이미 서버에 올라간 자료(재입력)는 그대로 두고 조작만 잠근다(2026-09-03 사용자 지시). + if (lasFreeDesign && slots.get(slot)?.file) removeFile(slot); + } } lasFreeCheck.addEventListener("change", () => { lasFreeDesign = lasFreeCheck.checked; @@ -798,8 +806,9 @@ export async function renderB03FileInput(root: HTMLElement): Promise { updateUploadButton(); }); - // LAS 토글은 지형 컨테이너의 것이다 — 켜면 그 안의 포인트클라우드 카드만 잠긴다. - terrainGroup.append(lasFreeRow); + // LAS 토글은 **파일 입력 컨테이너**의 것이다(2026-09-03 사용자 지시) — 무엇을 받을지 + // 정하는 스위치라 고르는 자리 옆에 선다. 켜면 지형 컨테이너가 통째로 잠긴다. + uploadControlPanel.insertBefore(lasFreeRow, resumeBanner); const routePanel = document.createElement("div"); routePanel.className = "b03-file__control-panel b03-file__cards-container-panel"; diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css index ce3677b2..24ba87d1 100644 --- a/B03_FileInput/B03_FileInput_UI_Style.css +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -212,6 +212,55 @@ align-items: start; } +/* LAS 없이 설계 토글 — 파일 입력 컨테이너의 스위치(2026-09-03 사용자 지시). + B04 좌측 패널의 체크 줄(`b04-surface__check`)과 같은 서식이다. */ +.b03-file__lasfree { + display: flex; + align-items: center; + gap: var(--spacing-8); + align-self: flex-start; + padding: var(--spacing-8) var(--spacing-12); + border: 1px solid var(--color-mist, #e6e2e3); + border-radius: var(--radius-buttons, 8px); + font-size: var(--text-body-sm, 14px); + color: var(--color-deep-iris, #26114a); + cursor: pointer; + transition: border-color var(--transition-base, 0.2s); +} + +.b03-file__lasfree:hover { + border-color: var(--color-royal-amethyst, #3e0079); +} + +.b03-file__lasfree input { + width: 16px; + height: 16px; + margin: 0; + accent-color: var(--color-royal-amethyst, #3e0079); + cursor: pointer; +} + +/* 켜져 있으면 스위치 자신도 켜진 티를 낸다 — 지형 컨테이너가 왜 잠겼는지의 근거. */ +.b03-file__lasfree:has(input:checked) { + border-color: var(--color-royal-amethyst, #3e0079); + background: var(--color-mist-violet, #edecff); + font-weight: var(--font-weight-medium, 500); +} + +/* LAS 없이 설계를 켜면 지형 컨테이너는 받지 않는다 — 잠긴 것이 보이게 흐린다. */ +.b03-file__group--disabled { + opacity: 0.55; +} + +.b03-file__card--disabled { + background: var(--color-paper, #f6f7fa); + border-style: dashed; +} + +.b03-file__card--disabled .b03-file__card-select { + cursor: not-allowed; +} + /* 좌측 안내 패널이 열리면 본문을 그만큼 밀어낸다 — 공용 워크플로 레이아웃(B04·B05)의 `.ui-workflow-layout__body` 규칙과 같은 값. 접히면 손잡이 폭만 남긴다. */ .b03-file__main-layout { @@ -261,15 +310,16 @@ color: var(--color-text-secondary); } +/* 글자 크기·색도 공용 양식 그대로 — B04 좌측 패널 본문(`b04-surface__check`)과 같은 + `--text-body-sm` · `--color-text-body`(2026-09-03 사용자 지시: 양식 = 글자 크기 포함). */ .b03-file__guide-list { margin: 0; padding-left: var(--spacing-16); display: flex; flex-direction: column; gap: var(--spacing-4); - font-size: var(--text-caption, 12px); - line-height: 1.55; - color: var(--color-text-secondary, #615e6e); + font-size: var(--text-body-sm); + color: var(--color-text-body); } .b03-file__group { From 7c3a65e814e9d96d04d7121a2ff3c534fadf9b14 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 21:02:19 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat(B03):=20=EC=84=A0=ED=83=9D=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=20=EC=84=A4=EB=AA=85=20=EC=9D=B4=EA=B4=80=20=C2=B7=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EB=B3=B4=EA=B4=80=ED=95=A8=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=C2=B7=ED=85=8C=EB=91=90=EB=A6=AC=20=EC=A0=95=EB=A6=AC?= =?UTF-8?q?=20=C2=B7=20=EA=B3=84=ED=9A=8D=EB=85=B8=EC=84=A0=EC=9D=80=20sha?= =?UTF-8?q?pefile=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시 4건(2026-09-03). - 선택 영역의 설명 문구를 좌측 안내 패널로 옮기고 본문에는 「입력 파일 선택」만 남김. - [임시 보관함에서 불러오기] → [임시 보관함]. 버튼을 감싸던 점선 테두리 삭제 — 버튼 자신의 테두리와 이중이었음. - 계획노선 도형 카드는 `.shp` 만 받음. CSV 는 내부 계산이 만드는 파일이라 사용자가 넣는 자료가 아님(사용자 확인). 컨테이너의 CSV 안내 문구도 삭제. - 이미 `.csv` 로 올라간 옛 프로젝트가 「미등록」으로 보이지 않도록 **서버 파일 매핑에만** `.csv → 계획노선 도형` 예외를 둠. 새로 받는 확장자와 이미 받은 것을 갈라 둔다. 좌측 안내 문구도 함께 갱신 — CSV 언급 제거, 버튼 새 이름 반영. 검증: 실측 — 선택 영역 텍스트 「입력 파일 선택」, 보관함 테두리 none·라벨 「임시 보관함」, 계획노선 안내 문구 없음, 카드 확장자 `.shp`, 옛 CSV 는 「완료 · 용화_계획노선.csv」 유지. typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_UI_Page.ts | 20 +++++++++++-------- B03_FileInput/B03_FileInput_UI_Style_Temp.css | 7 ++----- B03_FileInput/B03_FileInput_UI_Support.ts | 4 +++- ui_template/ui_template_locale_b1.ts | 20 +++++-------------- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index e3a39138..4a74dc82 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -95,9 +95,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise { dropzone.tabIndex = 0; const dropzoneLabel = document.createElement("strong"); dropzoneLabel.textContent = L("B03_File_Select_Label"); - const dropzoneHint = document.createElement("span"); - dropzoneHint.textContent = L("B03_File_Select_Hint"); - dropzone.append(dropzoneLabel, dropzoneHint, fileInput); + // 설명은 좌측 안내 패널 몫이다(2026-09-03 사용자 지시) — 여기는 자리 이름만 남긴다. + dropzone.append(dropzoneLabel, fileInput); const pageError = document.createElement("p"); pageError.className = "b03-file__error"; @@ -435,10 +434,14 @@ export async function renderB03FileInput(root: HTMLElement): Promise { ? inRouteSet ? "route_prj" : "prj" - : Array.from(slots.values()).find( - (candidate) => - candidate.slot !== "route_prj" && candidate.extensions.includes(extension), - )?.slot; + : // 옛 프로젝트의 노선은 `.csv`로 올라가 있다 — 이제 받지는 않지만(2026-09-03) + // 이미 올라간 것은 계획노선 도형 카드에 그대로 보여야 한다. + extension === ".csv" + ? "csv" + : Array.from(slots.values()).find( + (candidate) => + candidate.slot !== "route_prj" && candidate.extensions.includes(extension), + )?.slot; const state = slot ? slots.get(slot) : undefined; if (state) { state.serverUploaded = { @@ -752,11 +755,12 @@ export async function renderB03FileInput(root: HTMLElement): Promise { // 자료의 출처가 둘로 갈린다 — 원청이 준 계획노선, 측량이 준 지형(LAS·래스터). // 좌표계 파일(.prj)도 각각 하나씩 오므로 컨테이너를 나눠야 어느 칸에 무엇을 넣는지 // 화면만 보고 안다(2026-08-31 사용자 지시). + // 안내 문구 없음 — CSV 관련 설명이 붙어 있었으나 CSV는 사용자가 넣는 자료가 아니다 + // (2026-09-03 사용자 지시: 내부 계산 파일). const routeGroup = createCardGroup( L("B03_File_Group_Route"), ROUTE_SLOTS, "b03-file__group--route", - L("B03_File_Group_Route_Hint"), ); const terrainGroup = createCardGroup( L("B03_File_Group_Terrain"), diff --git a/B03_FileInput/B03_FileInput_UI_Style_Temp.css b/B03_FileInput/B03_FileInput_UI_Style_Temp.css index 345b0ebf..11edd0e0 100644 --- a/B03_FileInput/B03_FileInput_UI_Style_Temp.css +++ b/B03_FileInput/B03_FileInput_UI_Style_Temp.css @@ -4,14 +4,11 @@ * 모달 껍데기(.b03-file__modal / -backdrop)는 기존 확인 모달 스타일을 그대로 쓴다. * ========================================================================== */ +/* 버튼 하나만 담는 자리 — 테두리를 또 두르면 버튼 테두리와 이중이 된다 + (2026-09-03 사용자 지시). */ .b03-file__temp-picker { display: flex; align-items: center; - gap: var(--spacing-12); - flex-wrap: wrap; - padding: var(--spacing-12); - border: 1px dashed var(--color-border); - border-radius: var(--radius-cards); } .b03-file__temp-modal { diff --git a/B03_FileInput/B03_FileInput_UI_Support.ts b/B03_FileInput/B03_FileInput_UI_Support.ts index 93f17894..7b4e1707 100644 --- a/B03_FileInput/B03_FileInput_UI_Support.ts +++ b/B03_FileInput/B03_FileInput_UI_Support.ts @@ -60,7 +60,9 @@ const SLOT_CONFIGS: readonly SlotConfig[] = [ slot: "csv", labelKey: "B03_File_Slot_PlannedRoute", icon: "⌁", - extensions: [".csv", ".shp"], + // `.csv`는 받지 않는다 — 내부 계산이 만드는 파일이라 사용자가 넣는 자료가 아니다 + // (2026-09-03 사용자 지시). 슬롯 키 `csv`는 저장·서버 규약이라 그대로 둔다. + extensions: [".shp"], isRequired: true, }, { diff --git a/ui_template/ui_template_locale_b1.ts b/ui_template/ui_template_locale_b1.ts index 317ad7e4..cbcaa673 100644 --- a/ui_template/ui_template_locale_b1.ts +++ b/ui_template/ui_template_locale_b1.ts @@ -224,17 +224,11 @@ export const ui_locales_b1 = { "Upload the required planned route, terrain, and point cloud files.", ], B03_File_Select_Label: ["입력 파일 선택", "Select input files"], - /* 긴 설명은 좌측 안내 패널(B03_Guide_*)로 옮겼다 — 선택 영역은 한 줄만 남긴다 - (2026-09-03 사용자 지시). */ - B03_File_Select_Hint: [ - "끌어 놓거나 눌러서 고르면 확장자대로 카드에 나뉩니다.", - "Drop files here or click to pick — they are sorted into cards by extension.", - ], /* --- 좌측 안내 패널 (2026-09-03) --- */ B03_Guide_Files_Title: ["필요한 파일", "Files you need"], B03_Guide_Files_Route: [ - "계획노선 — CSV 한 장, 또는 shapefile 한 벌(.shp·.shx·.dbf·.cpg·.prj) 5개.", - "Planned route — one CSV, or the five shapefile parts (.shp, .shx, .dbf, .cpg, .prj).", + "계획노선 — shapefile 한 벌(.shp·.shx·.dbf·.cpg·.prj) 5개.", + "Planned route — the five shapefile parts (.shp, .shx, .dbf, .cpg, .prj).", ], B03_Guide_Files_Terrain: [ "지형 — 포인트클라우드 LAS/LAZ 1개와 좌표계 PRJ, 래스터 기준 TFW.", @@ -254,8 +248,8 @@ export const ui_locales_b1 = { "Use a card's [Select] to set one slot, or ✕ to clear it.", ], B03_Guide_How_Temp: [ - "[임시 보관함에서 불러오기]는 대시보드에 미리 올려 둔 자료를 가져옵니다.", - "[Load from temporary storage] pulls files you staged on the dashboard.", + "[임시 보관함]은 대시보드에 미리 올려 둔 자료를 가져옵니다.", + "[Temporary storage] pulls files you staged on the dashboard.", ], B03_Guide_Notes_Title: ["알아 둘 것", "Before you upload"], B03_Guide_Notes_Replace: [ @@ -310,10 +304,6 @@ export const ui_locales_b1 = { B03_File_Group_Inputs: ["입력 자료", "Input files"], B03_File_Group_Route: ["계획노선 자료", "Planned route files"], B03_File_Group_Terrain: ["지형 자료 (LAS)", "Terrain files (LAS)"], - B03_File_Group_Route_Hint: [ - "shapefile은 파일 5개가 한 벌입니다. CSV 한 장으로 넣어도 됩니다.", - "A shapefile is a set of five files. A single CSV also works.", - ], B03_File_Group_Terrain_Hint: [ "여기의 PRJ·TFW는 지형 자료의 좌표계입니다 — 노선 PRJ와 별개입니다.", "The PRJ and TFW here describe the terrain data, separate from the route PRJ.", @@ -337,7 +327,7 @@ export const ui_locales_b1 = { B03_File_Slot_TerrainDem: ["지형 래스터", "Terrain DEM"], B03_File_Slot_CadDrawing: ["CAD 도면", "CAD Drawing"], /* --- B03 임시 보관함 불러오기 (2026-08-08) --- */ - B03_Temp_Btn_Open: ["임시 보관함에서 불러오기", "Load from temporary storage"], + B03_Temp_Btn_Open: ["임시 보관함", "Temporary storage"], B03_Temp_FileCount: ["개 파일", " files"], B03_Temp_Modal_Title: ["보관 자료 선택", "Select stored files"], B03_Temp_Modal_Empty: [ From db91109b5a70d10e25db238140c0e43f01270408 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 21:06:48 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix(B03):=20=EC=9E=85=EB=A0=A5=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=203=EC=B9=B8=201:1:1=C2=B7=EB=86=92=EC=9D=B4=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC,=20=ED=86=A0=EA=B8=80=20=EC=95=84=EB=9E=98?= =?UTF-8?q?=20=EB=B9=88=20=EC=9E=90=EB=A6=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시(2026-09-03). - 고르는 자리를 flex 에서 **격자 3칸**으로 바꿈. flex 로는 내용 폭이 배분에 섞여 288·254·288 로 어긋났고 버튼 높이도 달랐다. 격자는 칸을 먼저 나누므로 1:1:1 이고, 칸을 꽉 채우게 해 세 칸 높이가 같아진다(실측 277×47 셋 다 동일). - 빈 오류 줄이 자리를 잡아 토글 아래가 비어 보이던 것 제거 — 글자가 있을 때만 자리를 차지한다(`:empty { display: none }`, min-height·마진 제거). Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_UI_Style.css | 29 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css index 24ba87d1..ac1f2dcc 100644 --- a/B03_FileInput/B03_FileInput_UI_Style.css +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -47,21 +47,25 @@ /* Wiza 스타일 가이드의 Lavender Glow 그라데이션 및 soft purple border hover */ /* 고르는 자리 한 줄 — [입력 파일 선택] · [임시 보관함] · [파일 업로드](2026-09-03). 좁아지면 아래로 접히되, 선택 영역이 남는 폭을 먹는다. */ +/* 셋을 같은 폭·같은 높이로 나눈다 — 열 1:1:1(2026-09-03 사용자 지시). + flex 로는 버튼·드롭존의 내용 폭이 배분에 섞여 288·254·288 로 어긋났다. 격자는 + 칸을 먼저 나누므로 내용과 무관하게 정확히 1:1:1 이 된다. 좁아지면 칸이 접힌다. */ .b03-file__pick-row { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); align-items: stretch; gap: var(--spacing-8); } -.b03-file__pick-row .b03-file__dropzone { - flex: 1 1 320px; +.b03-file__pick-row > * { + min-width: 0; } -.b03-file__pick-row .b03-file__temp-picker, -.b03-file__pick-row .ui-btn { - flex: 0 0 auto; - align-self: stretch; +/* 칸을 꽉 채워야 세 칸의 높이가 같아진다 — 버튼은 기본이 내용 크기다. */ +.b03-file__pick-row .b03-file__temp-picker > .ui-btn, +.b03-file__pick-row > .ui-btn { + width: 100%; + height: 100%; } /* 한 줄 바(2026-09-03) — 종전 160px 블록은 안내 문구를 담느라 컸다. 문구는 좌측 @@ -109,11 +113,16 @@ color: var(--color-slate, #615e6e); } +/* 빈 오류 줄이 자리를 잡고 있으면 토글 아래가 통째로 비어 보인다 — 글자가 있을 때만 + 자리를 차지한다(2026-09-03 사용자 지시). */ .b03-file__error { - min-height: var(--spacing-16); color: var(--color-danger, #dc2626); font-size: var(--text-body-sm, 14px); - margin: var(--spacing-8) 0; + margin: 0; +} + +.b03-file__error:empty { + display: none; } .b03-file__resume {