From b9e6a2b6a8ecc030702c13a6ad6423f653dfe492 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:55:29 +0900 Subject: [PATCH] =?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 {