- 좌측 목록 계획평면도 컨테이너 하나 · 종류별 단추 + × 지우기(기본 넷 포함) - 종류 더하기 = 이름 + 정보 조각 고르기 · 대피소 · 사토장 자리 = 측점 + 대피소/사토장(한 자리 둘 다) - 고친 뒤 목록 · 도면을 서버에서 다시 받아 화면 다시 세움 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
197 lines
7.6 KiB
TypeScript
197 lines
7.6 KiB
TypeScript
/* =============================================================================
|
||
* B07_DesignDetail_UI_PlanKinds.ts
|
||
* 좌측 도면 목록의 「계획평면도」 한 묶음 — 종류(지형 · 노선배치도 · 대피소 · 라이다 ·
|
||
* 사용자 추가)별 도면 단추, 종류 지우기 · 더하기, 대피소 · 사토장 자리 (PLAN 60장).
|
||
*
|
||
* 종류 = 정보 조각 고르기 목록이다. 고치면 서버 파일이 바뀌고 `onChanged` 로 화면을 다시 세운다.
|
||
* ========================================================================== */
|
||
|
||
import { showToast } from "@ui/ui_template_elements";
|
||
import {
|
||
addPlanKind,
|
||
deletePlanKind,
|
||
fetchPlanKinds,
|
||
savePlanMarks,
|
||
type DesignDrawingItem,
|
||
type PlanKindsState,
|
||
type PlanMark,
|
||
} 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;
|
||
}
|
||
|
||
/** 실패는 토스트로 알리고 삼킨다 — 성공했을 때만 화면을 다시 세운다. */
|
||
async function run(action: () => Promise<unknown>, controls: PlanKindsControls): Promise<void> {
|
||
try {
|
||
await action();
|
||
controls.onChanged();
|
||
} catch (error) {
|
||
showToast(error instanceof Error ? error.message : "계획평면도를 고치지 못했습니다.", "error");
|
||
}
|
||
}
|
||
|
||
function addKindForm(state: PlanKindsState, controls: PlanKindsControls): HTMLElement {
|
||
const form = el("div", "b07-plan-tools__block");
|
||
form.append(el("strong", "", "종류 더하기"));
|
||
const name = el("input", "b07-plan-tools__name");
|
||
name.type = "text";
|
||
name.placeholder = "종류 이름";
|
||
name.maxLength = 40;
|
||
const boxes = state.pieces.map((piece) => {
|
||
const label = el("label", "b07-plan-tools__piece");
|
||
const box = el("input", "");
|
||
box.type = "checkbox";
|
||
box.value = piece.id;
|
||
label.append(box, document.createTextNode(piece.name));
|
||
return { label, box };
|
||
});
|
||
const pieceList = el("div", "b07-plan-tools__pieces");
|
||
pieceList.append(...boxes.map((item) => item.label));
|
||
const add = el("button", "b07-plan-tools__button", "더하기");
|
||
add.type = "button";
|
||
add.addEventListener("click", () => {
|
||
const pieces = boxes.filter((item) => item.box.checked).map((item) => item.box.value);
|
||
void run(() => addPlanKind(controls.projectId, name.value.trim(), pieces), controls);
|
||
});
|
||
form.append(name, pieceList, add);
|
||
return form;
|
||
}
|
||
|
||
function marksForm(state: PlanKindsState, controls: PlanKindsControls): HTMLElement {
|
||
const form = el("div", "b07-plan-tools__block");
|
||
form.append(el("strong", "", "대피소 · 사토장 자리"));
|
||
form.append(
|
||
el(
|
||
"p",
|
||
"b07-plan-tools__hint",
|
||
"구조물로 놓은 대피소 · 사토장은 저절로 실립니다. 더 놓을 측점(m)만 적으세요.",
|
||
),
|
||
);
|
||
const rows = el("div", "b07-plan-tools__marks");
|
||
const readers = new Map<HTMLElement, () => PlanMark>();
|
||
const addRow = (mark: PlanMark): void => {
|
||
const row = el("div", "b07-plan-tools__mark");
|
||
const chainage = el("input", "b07-plan-tools__chainage");
|
||
chainage.type = "number";
|
||
chainage.min = "0";
|
||
chainage.step = "0.1";
|
||
chainage.value = String(mark.chainage_m);
|
||
const flag = (text: string, checked: boolean): [HTMLLabelElement, HTMLInputElement] => {
|
||
const label = el("label", "b07-plan-tools__piece");
|
||
const box = el("input", "");
|
||
box.type = "checkbox";
|
||
box.checked = checked;
|
||
label.append(box, document.createTextNode(text));
|
||
return [label, box];
|
||
};
|
||
const [refugeLabel, refuge] = flag("대피소", mark.refuge);
|
||
const [spoilLabel, spoil] = flag("사토장", mark.spoil);
|
||
const remove = el("button", "b07-plan-tools__remove", "×");
|
||
remove.type = "button";
|
||
remove.title = "이 자리 빼기";
|
||
remove.addEventListener("click", () => {
|
||
readers.delete(row);
|
||
row.remove();
|
||
});
|
||
row.append(chainage, refugeLabel, spoilLabel, remove);
|
||
readers.set(row, () => ({
|
||
chainage_m: Number(chainage.value),
|
||
refuge: refuge.checked,
|
||
spoil: spoil.checked,
|
||
}));
|
||
rows.append(row);
|
||
};
|
||
state.marks.forEach(addRow);
|
||
const more = el("button", "b07-plan-tools__button", "자리 더하기");
|
||
more.type = "button";
|
||
more.addEventListener("click", () => addRow({ chainage_m: 0, refuge: true, spoil: false }));
|
||
const save = el("button", "b07-plan-tools__button", "자리 저장");
|
||
save.type = "button";
|
||
save.addEventListener("click", () => {
|
||
const marks = [...readers.values()].map((read) => read());
|
||
void run(() => savePlanMarks(controls.projectId, marks), controls);
|
||
});
|
||
const actions = el("div", "b07-plan-tools__actions");
|
||
actions.append(more, save);
|
||
form.append(rows, actions);
|
||
return form;
|
||
}
|
||
|
||
/** 「계획평면도」 묶음 — 종류마다 이름 줄(× 지우기) + 장 단추, 아래에 더하기 · 대피소 자리. */
|
||
export function buildPlanSection(
|
||
items: DesignDrawingItem[],
|
||
makeButton: (drawing: DesignDrawingItem, label: string) => HTMLButtonElement,
|
||
controls?: PlanKindsControls,
|
||
): HTMLElement {
|
||
const section = el("section", "b07-drawing-group b07-plan ui-collapsible ui-sidebar-section");
|
||
section.dataset.kind = "plan";
|
||
section.append(el("h3", "ui-collapsible__title", `계획평면도 ${items.length}`));
|
||
|
||
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]);
|
||
}
|
||
for (const [kindId, sheets] of kinds) {
|
||
const name = sheets[0].plan_kind_name ?? sheets[0].label;
|
||
const block = el("div", "b07-plan-kind");
|
||
block.dataset.planKind = kindId;
|
||
// 한 장이면 [도면 단추][×] 한 줄 · 여러 장이면 [이름][×] 아래 장 단추.
|
||
const head = el("div", "b07-plan-kind__head");
|
||
const buttons = sheets.map((drawing, index) =>
|
||
makeButton(drawing, sheets.length > 1 ? `${name} ${index + 1}장` : name),
|
||
);
|
||
head.append(sheets.length > 1 ? el("span", "b07-plan-kind__name", name) : buttons[0]);
|
||
if (controls) {
|
||
const remove = el("button", "b07-plan-tools__remove", "×");
|
||
remove.type = "button";
|
||
remove.title = `「${name}」 종류 지우기`;
|
||
remove.addEventListener("click", () => {
|
||
if (
|
||
!window.confirm(`「${name}」 종류를 지웁니다. 다시 쓰려면 종류 더하기로 새로 만듭니다.`)
|
||
)
|
||
return;
|
||
void run(() => deletePlanKind(controls.projectId, kindId), controls);
|
||
});
|
||
head.append(remove);
|
||
}
|
||
block.append(head, ...(sheets.length > 1 ? buttons : []));
|
||
section.append(block);
|
||
}
|
||
|
||
if (controls) {
|
||
// 더하기 · 대피소 자리는 펼칠 때 서버에서 지금 상태를 받는다.
|
||
const tools = el("details", "b07-plan-tools");
|
||
tools.append(el("summary", "", "종류 더하기 · 대피소 자리"));
|
||
tools.addEventListener("toggle", () => {
|
||
if (!tools.open || tools.dataset.loaded) return;
|
||
tools.dataset.loaded = "true";
|
||
void fetchPlanKinds(controls.projectId)
|
||
.then((state) => tools.append(addKindForm(state, controls), marksForm(state, controls)))
|
||
.catch((error: unknown) => {
|
||
delete tools.dataset.loaded;
|
||
showToast(
|
||
error instanceof Error ? error.message : "종류 목록을 받지 못했습니다.",
|
||
"error",
|
||
);
|
||
});
|
||
});
|
||
section.append(tools);
|
||
}
|
||
return section;
|
||
}
|