/* ============================================================================= * E03_StabSection_UI_List.ts * E03 좌측 측점 목록(PLAN 532-3) — 측점 · 깎기/쌓기 · 조건별 최소 안전율 · 판정(아이콘 + 글) · * 대표 단면 별(자동 = 채움 · 사용자 켬/끔 = 밑줄 · 누르면 바꿈) · 판정 면(원호 · 비원호 + 면 확인 · 모형 끝 면 — * 567-1 · 좁아서 맨 끝 칸). 정렬 · 거름은 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 { stationLabel } from "../common_util/common_util_station"; import { e03Table } from "./E03_StabSection_UI_Table"; import { basisOf, 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; /** 측점 간격(m) — 측점 글 「No.+거리」 의 분모 */ interval: number; /** 사용자 켬/끔 — 서버 값(별 누름 = 바로 저장) */ 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") }, { label: t("E03_Col_Basis"), title: t("E03_Basis_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: stationLabel(row.chainage_m, draw.interval, 1), 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, basisBadges(row, draw.condition), ]; }); 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; } /** 판정 면 칸 — 원호 · 비원호 + 「면 확인」 · 「모형 끝 면」(그 조건 기준) */ export function basisBadges(row: StationRow, condition: Condition): HTMLElement { const box = el("span", { className: "e03-basis" }); const b = basisOf(row, condition); if (b.method) { const kind = createBadge( t(b.circular ? "E03_Basis_Circular" : "E03_Basis_NonCircular"), b.circular ? "neutral" : "info", { size: 18 }, ); kind.title = t(`E03_Method_${b.method}` as LocaleKey); kind.dataset.role = "basis-kind"; box.append(kind); } if (b.surfaceCheck) { const flag = createBadge(t("E03_SurfaceCheck_Short"), "warning", { size: 18 }); flag.title = `${t("E03_SurfaceCheck")} — ${t("E03_SurfaceCheck_Hint")}`; flag.dataset.role = "basis-check"; box.append(flag); } if (b.edge) { const flag = createBadge(t("E03_EdgeSurface_Short"), "warning", { size: 18 }); flag.title = `${t("E03_EdgeSurface")} — ${t("E03_EdgeSurface_Hint")}`; flag.dataset.role = "basis-edge"; box.append(flag); } return box; } function hookEmpty(text: string): HTMLElement { const empty = createEmpty({ text, small: true }); empty.classList.add("e03-empty"); return empty; }