- 새 M02_MasterTemplete_Practice.ts — 구조물 조합 밖 · 도면 양식 아래 · 마스터 층 · 시스템 관리자만 - [통째 지우기] = 물음 두 번 → DELETE /api/m02/practice(시스템 관리자 문 is_system_admin · 판 · 쓰는 곳 0 확인 · 줄을 비움) - Combo_Store.drop_practice · 시험 test_m02_practice.py(임시 사본) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
225 lines
8.0 KiB
TypeScript
225 lines
8.0 KiB
TypeScript
/* =============================================================================
|
|
* 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<boolean>;
|
|
/** 메인 칸에 화면을 띄움 */
|
|
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<void> {
|
|
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<void> {
|
|
if (opened || !(await opt.confirmLeave())) return;
|
|
opt.show(L("M02_PracticeTitle"), view);
|
|
opt.onOpen();
|
|
mark(true);
|
|
await drawList();
|
|
}
|
|
|
|
async function drawList(): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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) };
|
|
}
|