/* ============================================================================= * E03_StabSection_UI_List.ts * E03 좌측 측점 목록(PLAN 532-3) — 측점 · 깎기/쌓기 · 조건별 최소 안전율 · 판정(아이콘 + 글) · * 대표 단면 별(자동 = 채움 · 사용자 켬/끔 = 밑줄 · 누르면 바꿈). 정렬 · 거름은 logic `listRows`. * ========================================================================== */ import { createBadge, type BadgeTone } from "@ui/ui_template_badge"; import { createIconButton } from "@ui/ui_template_button"; import { el } from "@ui/ui_template_elements"; import { createEmpty } from "@ui/ui_template_empty"; import { t, type LocaleKey } from "@ui/ui_template_locale"; import { e03Table } from "./E03_StabSection_UI_Table"; import { listRows, roleText, verdictOf, type Condition, type SortMode, type StationRow, } from "./logic/E03_StabSection_Logic"; const fmt = (v: number | null | undefined): string => v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(2); export interface ListDraw { rows: StationRow[]; conditions: Condition[]; condition: Condition; sort: SortMode; ngOnly: boolean; active: string | null; /** 사용자 켬/끔 — 저장 전 캐시 포함 · 없으면 서버 값 */ representative: (row: StationRow) => { on: boolean; user: boolean | null }; editable: boolean; } const VERDICT_TONE: Record = { ok: "success", ng: "danger", none: "neutral" }; export function buildList( draw: ListDraw, onSelect: (key: string) => void, onToggle: (key: string) => void, ): HTMLElement { const box = el("div", { className: "e03-list" }); const rows = listRows(draw.rows, draw.condition, draw.sort, draw.ngOnly); const columns = [ { label: t("E03_Col_Station") }, { label: t("E03_Col_Role") }, ...draw.conditions.map((c) => ({ label: t(`E03_Cond_${c}` as LocaleKey), num: true })), { label: t("E03_Verdict") }, { label: t("E03_Col_Rep"), title: t("E03_Rep_Hint") }, ]; const cells = rows.map((row) => { const v = verdictOf(row, draw.condition); const rep = draw.representative(row); const title = [ ...(row.representative?.auto ?? []).map((r) => t(`E03_Rep_${r}` as LocaleKey)), rep.user !== null ? t("E03_Rep_User") : "", ] .filter(Boolean) .join(" · ") || t("E03_Rep_Hint"); const star = createIconButton({ glyph: rep.on ? "★" : "☆", title, size: 20, pressed: rep.on, disabled: !draw.editable, onClick: (ev) => { ev.stopPropagation(); onToggle(row.key); }, }); star.classList.add("e03-star"); star.classList.toggle("is-on", rep.on); star.classList.toggle("is-user", rep.user !== null); const verdict = createBadge( row.error ? t("E03_Error") : v === "none" ? "-" : t(`E03_Verdict_${v}` as LocaleKey), VERDICT_TONE[v], { kind: "lamp" }, ); verdict.classList.add("e03-verdict", `e03-verdict--${v}`); const station = el("span", { text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key, children: row.walls ? [el("span", { className: "e03-wall-tag", text: "▦", attrs: { title: t("E03_Walls") } })] : [], }); return [ station, row.sides.length ? roleText(row, { cut: t("E03_CutShort"), fill: t("E03_FillShort") }) : "-", ...draw.conditions.map((c) => fmt(row.min[c])), verdict, star, ]; }); const list = e03Table(columns, cells, "station-list"); list.table.tHead?.rows[0].cells .item(2 + draw.conditions.indexOf(draw.condition)) ?.setAttribute("aria-sort", "other"); list.rows.forEach((tr, i) => { const row = rows[i]; tr.dataset.key = row.key; tr.dataset.verdict = verdictOf(row, draw.condition); tr.classList.toggle("is-active", row.key === draw.active); if (row.error) tr.title = row.error; tr.addEventListener("click", () => onSelect(row.key)); }); box.append(list.root); if (!rows.length) box.append(hookEmpty(t("E03_ListEmpty"))); queueMicrotask(() => box.querySelector("tr.is-active")?.scrollIntoView({ block: "nearest" })); return box; } function hookEmpty(text: string): HTMLElement { const empty = createEmpty({ text, small: true }); empty.classList.add("e03-empty"); return empty; }