Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts
T
eomsangdonandClaude Sonnet 5.5 a96a60fb86 feat(524-1): B07 평면도 설정 모달 정리 — 기본 항목 지형도 하나 · 양식 칸 숨김 · 종류 척도 2열 · 항목 그룹 구분선 · 대피소 사토장 자리 지움
- 기본 종류 = 지형도 하나(옛 기본 이관 더하기 지움) · 대피소 조각 · 자리 폼 · plan-marks 끝점 지움
- 모달 양식 칸 = 평면도면 숨김 · 종류 척도 영역 2열 · 항목 3열 유지 + 그룹 구분선(PIECE_GROUPS 한 곳)
- 낡은 시험 수선: 기본 종류가 하나로 줄어 route · layout · landuse · watershed 기본 전제를 add_kind 로 세움 · 대피소 자리 시험 지움

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 14:23:59 +09:00

259 lines
10 KiB
TypeScript

/* =============================================================================
* 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<K extends keyof HTMLElementTagNameMap>(
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<unknown>,
controls: PlanKindsControls,
): Promise<void> {
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<string, DesignDrawingItem[]>();
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;
}