/* ============================================================================= * M02_MasterTemplete_Practice.ts * 좌측 「실무문서」 컨테이너(구조물 조합 밖 · 도면 양식 아래 · 마스터 층 · 시스템 관리자만 · PLAN 39-2) — * 줄 하나를 누르면 메인 칸에 옛 마스터 일위대가 조합(`일위대가조합.json` 통째) 목록 · 상세(읽기만) · * [통째 지우기](물음 두 번 → `DELETE /api/m02/practice` · 서버도 시스템 관리자 · 쓰는 곳 0 확인). * 읽기 = 마스터 조합 길(`Labor_Api`) · 복사 원본 고르기는 목록 화면(ItemList)의 실무문서 탭 몫. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; import { t as L } from "@ui/ui_template_locale"; import { fetchCombo, fetchComboList, type ComboList, type ComboOne, } from "./M02_MasterTemplete_Labor_Api"; export interface PracticeSectionOptions { /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ confirmLeave: () => Promise; /** 메인 칸에 화면을 띄움 */ show: (label: string, node: HTMLElement) => (dirty: boolean) => void; /** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */ onOpen: () => void; } export interface PracticeSection { title: string; body: HTMLElement; /** 다른 화면을 열었음 — 고름 표시만 지움 */ clear: () => void; } const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); /** 서버 까닭 글 — `{errors}` · 판 다름 · 글 */ function reasonOf(detail: unknown, status: number): string { if (typeof detail === "string") return detail; const d = detail as { errors?: string[]; stale?: unknown } | null; if (d?.errors?.length) return d.errors.join(" · "); return d?.stale ? L("M02_PracticeStale") : `HTTP ${status}`; } /** 통째 지우기 — 줄을 모두 비움(판 = 읽은 표 판) */ async function dropPractice(판: string): Promise { const res = await fetch(`${API_BASE_URL}/m02/practice?판=${encodeURIComponent(판)}`, { method: "DELETE", credentials: "include", }); if (res.ok) return; const data = (await res.json().catch(() => ({}))) as { detail?: unknown }; throw new Error(reasonOf(data.detail, res.status)); } const table = (heads: string[], rows: HTMLElement[]): HTMLElement => el("div", { className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", children: [ el("table", { className: "ui-general-block__table m02-practice__table", children: [ el("thead", { children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], }), el("tbody", { children: rows }), ], }), ], }); const cells = (values: string[]): HTMLElement[] => values.map((v) => el("td", { text: v, attrs: { title: v } })); export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSection { const row = el("button", { className: "m02-side__item ui-sidebar-row", text: L("M02_PracticeRow"), attrs: { type: "button", "data-practice-row": "" }, }); const body = el("div", { className: "m02-side__items ui-sidebar-list", children: [row] }); const view = el("div", { className: "m02-practice" }); let opened = false; let nav = 0; // 늦게 온 응답 버림 const mark = (on: boolean): void => { opened = on; row.classList.toggle("is-active", on); }; row.addEventListener("click", () => void open()); async function open(): Promise { if (opened || !(await opt.confirmLeave())) return; opt.show(L("M02_PracticeTitle"), view); opt.onOpen(); mark(true); await drawList(); } async function drawList(): Promise { const my = ++nav; const stop = showLoading(view); let list: ComboList; try { list = await fetchComboList(); } catch (error) { stop(); if (my === nav) view.replaceChildren(loadingError(why(error), () => void drawList())); return; } finally { stop(); } if (my !== nav) return; const drop = createButton({ label: L("M02_PracticeDrop"), variant: "danger", size: "sm" }); drop.disabled = !list.줄.length; drop.addEventListener("click", () => void dropAll(list, drop)); const rows = list.줄.map((c) => { const tr = el("tr", { className: "m02-practice__row", attrs: { "data-practice-key": c.키 }, children: cells([c.키, c.이름, c.구분 ?? "", c.단위 ?? "", String(c.count)]), }); tr.addEventListener("click", () => void drawOne(c.키)); return tr; }); view.replaceChildren( el("div", { className: "m02-practice__bar", children: [ el("span", { className: "m02-practice__note", text: L("M02_PracticeCount").replace("{n}", String(list.줄.length)), }), drop, ], }), rows.length ? table( [ L("M02_PracticeKey"), L("M02_Name"), L("M02_PracticeGroup"), L("M02_PracticeUnit"), L("M02_PracticeLogics"), ], rows, ) : el("p", { className: "m02-practice__note", text: L("M02_PracticeEmpty") }), ); } /** 상세 — 읽기만(이름 · 키 · 구분 · 단위 · 비고 + 담은 로직 표) */ async function drawOne(key: string): Promise { const my = ++nav; const stop = showLoading(view); let one: ComboOne; try { one = await fetchCombo(key); } catch (error) { stop(); if (my === nav) view.replaceChildren(loadingError(why(error), () => void drawOne(key))); return; } finally { stop(); } if (my !== nav) return; const back = createButton({ label: L("M02_PracticeBack"), variant: "ghost", size: "sm" }); back.addEventListener("click", () => void drawList()); const c = one.combo; const info = [ [L("M02_PracticeKey"), c.키], [L("M02_PracticeGroup"), c.구분 ?? ""], [L("M02_PracticeUnit"), c.단위 ?? ""], ].map(([k, v]) => el("span", { className: "m02-practice__note", text: `${k} ${v}` })); const lines = one.줄.map((x) => el("tr", { children: cells([ String(x.차례), x.로직, x.ok ? (x.이름 ?? "") : (x.까닭 ?? ""), x.결과단위 ?? "", x.메모 ?? "", ]), }), ); view.replaceChildren( el("div", { className: "m02-practice__bar", children: [back, el("strong", { text: c.이름 }), ...info], }), ...(c.비고 ? [el("p", { className: "m02-practice__memo", text: c.비고 })] : []), lines.length ? table( [ L("M02_PracticeOrder"), L("M02_PracticeKey"), L("M02_Name"), L("M02_PracticeUnit"), L("M02_PracticeMemo"), ], lines, ) : el("p", { className: "m02-practice__note", text: L("M02_PracticeNoLogic") }), ); } /** 통째 지우기 — 물음 두 번 · 서버가 쓰는 곳 · 판을 다시 봄 */ async function dropAll(list: ComboList, button: HTMLButtonElement): Promise { const n = String(list.줄.length); if (!(await showConfirmDialog(L("M02_PracticeDropAsk").replace("{n}", n), L("M02_Delete")))) return; if (!(await showConfirmDialog(L("M02_PracticeDropAsk2"), L("M02_PracticeDrop")))) return; button.disabled = true; try { await dropPractice(list.판); showToast(L("M02_PracticeDropped").replace("{n}", n), "success"); } catch (error) { showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); } await drawList(); } return { title: L("M02_PracticeTitle"), body, clear: () => mark(false) }; }