Files
Aislo/E03_StabSection/E03_StabSection_UI_List.ts
T
eomsangdonandClaude Opus 5.5 7d5f516b84 feat(E03): 측점 No.+거리 · 위험 띠 확대 · 이동 · 측점 글 · 900 높이 겹침 · 대표 단면 바로 저장 (PLAN 566-2)
- 좌측 목록 · 횡단 카드 제목 = 공용 stationLabel(누가, 측점 목록 추정 간격, 소수 한 자리)
- 평면 위험 띠 = 공용 attachSvgZoomPan(휠 · 가운데 끌기 · 두 번 누름 원배율) · 측점 글 솎음(logic labelStride · planLabelKeys) · 선 굵기 · 글 크기 고정 · 배율은 다시 그려도 유지
- .e03-page 행 셋(auto auto minmax) — 화면 높이 900 에서 띠 · 본문 겹침 고침
- 대표 단면 별 = 누르면 바로 PUT representative · 뒤에서 E05 보고서 갱신 길(GET report · POST refresh) · [대표 단면 저장] 단추 · 캐시 뺌
- 시험 532_3 logic TS 에 솎음 걸음 · 글 열쇠

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:58:44 +09:00

123 lines
4.4 KiB
TypeScript

/* =============================================================================
* 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 { stationLabel } from "../common_util/common_util_station";
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;
/** 측점 간격(m) — 측점 글 「No.+거리」 의 분모 */
interval: number;
/** 사용자 켬/끔 — 서버 값(별 누름 = 바로 저장) */
representative: (row: StationRow) => { on: boolean; user: boolean | null };
editable: boolean;
}
const VERDICT_TONE: Record<string, BadgeTone> = { 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: 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,
];
});
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;
}