feat(B03): LAS 없이 설계 토글을 입력 컨테이너로 · 켜면 지형 자료 전체 차단

사용자 지시(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) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 20:55:29 +09:00
co-authored by Claude Opus 5
parent a00c7497c1
commit b9e6a2b6a8
2 changed files with 90 additions and 31 deletions
+37 -28
View File
@@ -303,28 +303,31 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
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<void> {
*/
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<void> {
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<HTMLButtonElement | HTMLInputElement>(
".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<HTMLButtonElement | HTMLInputElement>(
".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<void> {
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";