/* ============================================================================= * B07_DesignDetail_UI_PlanKinds.ts * 좌측 도면 목록의 「평면도」 한 묶음 — 종류(지형도 · * 사용자 추가)별 도면 단추 + [평면도 관리] 단추 (PLAN 60 · 67 · 130장). * 종류 추가 · 업데이트 폼은 [평면도 관리] 모달(`_UI_PlanManage.ts`)이 씀. * * 종류 = 정보 조각 고르기 목록이다. 고치면 서버 파일이 바뀌고 `onChanged` 로 화면을 다시 세운다. * 드롭다운 · 체크박스는 공용(`createSelectField` · `ui_template_checkbox`). * ========================================================================== */ import { createCheckbox } from "@ui/ui_template_checkbox"; import { createPanelCard } from "@ui/ui_template_collapsible"; import { createSelectField, showToast } from "@ui/ui_template_elements"; import { addPlanKind, deletePlanKind, updatePlanKind, type DesignDrawingItem, type PlanKindLayout, type PlanKindsState, } from "./B07_DesignDetail_Api_Fetch"; export interface PlanKindsControls { projectId: string; /** 종류를 고친 뒤 — 목록과 도면을 다시 받는다. */ onChanged: () => void; } function el( tag: K, className: string, text = "", ): HTMLElementTagNameMap[K] { const node = document.createElement(tag); node.className = className; if (text) node.textContent = text; return node; } /** 실패는 토스트로 알리고 삼킨다 — 성공했을 때만 화면을 다시 세운다. */ export async function run( action: () => Promise, controls: PlanKindsControls, ): Promise { try { await action(); controls.onChanged(); } catch (error) { showToast(error instanceof Error ? error.message : "평면도를 고치지 못했습니다.", "error"); } } /** 항목 체크박스 그룹 — 그룹 사이는 구분선만 · 그룹 추가 · 바꿈은 이 목록 한 곳. * 어느 그룹에도 없는 항목은 맨 끝 그룹에 붙는다. */ const PIECE_GROUPS: string[][] = [ ["lidar", "contour", "stream", "parcel", "parcel_info", "emd", "sgg"], // 수치지형 ["basin", "basin_no", "drainage"], // 배수유역 [ "centerline", "road_edge", "fill_outline", "cut_outline", "slope_ticks", "ip", "radius", "ip_table", ], // 노선 ["station", "endpoints"], // 측점 관련 ]; /** 양식 — 평면도(조각 고름 · 축척 고름 · 장 나눔) · 유역도(1/6,000 한 장 · 그림 고정). */ const LAYOUTS: { value: PlanKindLayout; text: string }[] = [ { value: "plan", text: "평면도" }, { value: "watershed", text: "유역도" }, ]; /** 종류 폼 — 밖(장 단추)에서 고른 종류로 폼을 맞추는 `pick` 과 함께. */ export interface KindForm { root: HTMLElement; pick: (kindId: string) => void; } /** 종류 — 맨 위 [종류 ▼] 아래 상세. 종류를 고르면 든 조각이 체크로 보이고 [업데이트] · [지우기] · * 「새 종류」면 양식 고르고 [추가]. onPick = 드롭다운으로 고른 종류(미리보기 따라감 · 140-2). */ export function kindForm( state: PlanKindsState, controls: PlanKindsControls, selected = "", onPick?: (kindId: string) => void, ): KindForm { const form = el("div", "b07-plan-tools__block"); form.append(el("strong", "", "종류")); const pick = createSelectField({ options: [ ...state.kinds.map((kind) => ({ value: kind.id, text: kind.name })), { value: "", text: "새 종류" }, ], compact: true, ariaLabel: "종류", onChange: () => { show(); if (pick.select.value) onPick?.(pick.select.value); }, }); const layout = createSelectField({ options: LAYOUTS, compact: true, ariaLabel: "양식", onChange: () => showPieces(), }); // 축척 — 목록은 서버 `scales` 한 곳 · 유역도 양식은 1/6,000 고정(잠금). const scaleText = (value: number): string => `1/${value.toLocaleString("en-US")}`; const scale = createSelectField({ options: [], compact: true, ariaLabel: "축척" }); // 등고선 간격 — 유역도 양식만(5 m 기본 · 10 m) · 1 m 는 B05 · B06 전용이라 없음. const contour = createSelectField({ options: state.contour_intervals.map((value) => ({ value: String(value), text: `등고선 ${value} m`, })), compact: true, ariaLabel: "등고선 간격", }); const name = el("input", "b07-plan-tools__name"); name.type = "text"; name.placeholder = "종류 이름"; name.maxLength = 40; const boxes = state.pieces.map((piece) => createCheckbox(piece.name, { value: piece.id })); const pieceList = el("div", "b07-plan-tools__pieces"); const grouped = PIECE_GROUPS.map((ids) => boxes.filter(({ input }) => ids.includes(input.value))); grouped[grouped.length - 1].push( ...boxes.filter((box) => !PIECE_GROUPS.some((ids) => ids.includes(box.input.value))), ); grouped .filter((group) => group.length) .forEach((group, index) => { if (index) pieceList.append(el("hr", "b07-plan-tools__divider")); pieceList.append(...group.map((box) => box.root)); }); const action = el("button", "b07-plan-tools__button"); action.type = "button"; const remove = el("button", "b07-plan-tools__button", "지우기"); remove.type = "button"; // 유역도 양식은 그림이 정해져 있어 얹을 정보를 고르지 않는다. const showPieces = (): void => { const watershed = layout.select.value === "watershed"; pieceList.hidden = watershed; const kind = state.kinds.find((item) => item.id === pick.select.value); if (watershed) { const fixed = String(state.watershed_scale); scale.setOptions( [{ value: fixed, text: `${scaleText(state.watershed_scale)} (고정)` }], fixed, ); } else { scale.setOptions( state.scales.map((value) => ({ value: String(value), text: scaleText(value) })), String(kind?.layout === "plan" ? kind.scale : state.default_scale), ); } scale.select.disabled = watershed; contour.root.hidden = !watershed; contour.select.value = String(kind?.contour_interval_m ?? state.default_contour_interval); }; const show = (): void => { const kind = state.kinds.find((item) => item.id === pick.select.value); name.value = kind?.name ?? ""; boxes.forEach(({ input }) => (input.checked = kind?.pieces.includes(input.value) ?? false)); // 양식은 새로 만들 때만 고른다 — 있는 종류는 그 양식을 보이기만. layout.select.value = kind?.layout ?? "plan"; layout.select.disabled = Boolean(kind); layout.root.hidden = kind?.layout === "plan"; // 평면도는 늘 같은 값 — 새 종류 · 유역도만 보임 action.textContent = kind ? "업데이트" : "추가"; remove.hidden = !kind; showPieces(); }; if (state.kinds.some((kind) => kind.id === selected)) pick.select.value = selected; show(); remove.addEventListener("click", () => { const kind = state.kinds.find((item) => item.id === pick.select.value); if (!kind) return; if (!window.confirm(`「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`)) return; void run(() => deletePlanKind(controls.projectId, kind.id), controls); }); action.addEventListener("click", () => { const pieces = boxes.filter((box) => box.input.checked).map((box) => box.input.value); const kindId = pick.select.value; const label = name.value.trim(); const chosen = layout.select.value as PlanKindLayout; const denominator = Number(scale.select.value); const interval = chosen === "watershed" ? Number(contour.select.value) : undefined; void run( () => kindId ? updatePlanKind(controls.projectId, kindId, label, pieces, denominator, interval) : addPlanKind(controls.projectId, label, pieces, chosen, denominator, interval), controls, ); }); const actions = el("div", "b07-plan-tools__actions"); actions.append(action, remove); const fields = el("div", "b07-plan-tools__fields"); fields.append(pick.root, layout.root, scale.root, contour.root); form.append(fields, name, pieceList, actions); return { root: form, pick: (kindId) => { pick.select.value = kindId; show(); }, }; } /** 장을 종류별로 묶음 — 여러 장이면 「이름 N장」 · 한 장이면 이름(좌측 목록 · 관리 모달 공용). */ export function groupPlanSheets( items: DesignDrawingItem[], ): { kindId: string; name: string; sheets: { drawing: DesignDrawingItem; label: string }[] }[] { const kinds = new Map(); for (const item of items) { const key = item.plan_kind ?? item.id; kinds.set(key, [...(kinds.get(key) ?? []), item]); } return [...kinds].map(([kindId, sheets]) => { const name = sheets[0].plan_kind_name ?? sheets[0].label; return { kindId, name, sheets: sheets.map((drawing, index) => ({ drawing, label: sheets.length > 1 ? `${name} ${index + 1}장` : name, })), }; }); } /** 「평면도」 묶음 — 종류마다 이름 줄 + 장 단추 · 제목 ⚙ = 평면도 관리 모달(130-1 · 280-5). */ export function buildPlanSection( items: DesignDrawingItem[], makeButton: (drawing: DesignDrawingItem, label: string) => HTMLButtonElement, onManage?: () => void, collapsed = true, ): HTMLElement { // 컨테이너 = 좌측 패널 공용 카드(269) const section = createPanelCard(`평면도 ${items.length}`, collapsed, onManage); section.classList.add("b07-drawing-group", "b07-plan"); section.dataset.kind = "plan"; for (const { kindId, name, sheets } of groupPlanSheets(items)) { const block = el("div", "b07-plan-kind"); block.dataset.planKind = kindId; // 한 장이면 [도면 단추] 한 줄 · 여러 장이면 [이름] 아래 장 단추. const buttons = sheets.map(({ drawing, label }) => makeButton(drawing, label)); if (sheets.length > 1) block.append(el("span", "b07-plan-kind__name", name)); block.append(...buttons); section.append(block); } return section; }