- 기본 종류 = 지형도 하나(옛 기본 이관 더하기 지움) · 대피소 조각 · 자리 폼 · plan-marks 끝점 지움 - 모달 양식 칸 = 평면도면 숨김 · 종류 척도 영역 2열 · 항목 3열 유지 + 그룹 구분선(PIECE_GROUPS 한 곳) - 낡은 시험 수선: 기본 종류가 하나로 줄어 route · layout · landuse · watershed 기본 전제를 add_kind 로 세움 · 대피소 자리 시험 지움 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
259 lines
10 KiB
TypeScript
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;
|
|
}
|