- 새로 = 서버가 그 층 다음 SP · SC 키 · 복사 = 본뜬 원본 → 고른 이름으로 고쳐 씀
- 일위대가 문서 줄의 참조 {층, 주인, 키} 를 화면 캐시에서 안 버림 · 로직 입력 받기에서 참조 줄 건너뜀
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
241 lines
9.7 KiB
TypeScript
241 lines
9.7 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_UI_Side_Precombo.ts
|
|
* 좌측 「일위대가 조합」 컨테이너(50-3 ③) — 구조물 조합 아래 · 층 단추(페이지가 만듦) ·
|
|
* 하위 둘 「단가승격」(promote · 단가산출 로직 하나를 입력값 고정해 일위대가 꼴로) ·
|
|
* 「사전조합」(precombo · 승격 + 다른 사전조합 묶음) — 목록 · 새로 · 복사 · 지우기.
|
|
* 상세는 편집 칸(UI_Main 종류 promote · precombo = 일위대가 상세 모드 · 도면 · 면적 · 자재 없음).
|
|
* 파일 이름 = 키(SP- · SC-) · 화면 이름 = 문서 `이름`(목록 `제목`) · 쓰는 곳 있으면 서버가 지우기를 막음(409 까닭).
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
el,
|
|
showConfirmDialog,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import {
|
|
deleteLayerTemplate,
|
|
deleteTemplate,
|
|
getActiveOwner,
|
|
newLibrary,
|
|
readTemplate,
|
|
saveTemplate,
|
|
type Kind,
|
|
type Layer,
|
|
type LibraryBody,
|
|
type TemplateInfo,
|
|
} from "./M02_MasterTemplete_Api_Fetch";
|
|
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
|
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
|
|
|
export type PrecomboKind = Extract<Kind, "promote" | "precombo">;
|
|
|
|
/** 종류 · 화면 이름(= 문서 `종류` 글 · 키 SP- · SC- 는 서버가 줌) */
|
|
const PARTS: { kind: PrecomboKind; label: string }[] = [
|
|
{ kind: "promote", label: "단가승격" },
|
|
{ kind: "precombo", label: "사전조합" },
|
|
];
|
|
export const PRECOMBO_TITLE = "일위대가 조합";
|
|
export const isPrecombo = (kind: Kind): kind is PrecomboKind =>
|
|
kind === "promote" || kind === "precombo";
|
|
|
|
/** 목록 줄 — 서버가 문서 이름을 `제목` 으로 실어 보냄(없으면 키만) */
|
|
type Info = TemplateInfo & { 제목?: string };
|
|
export const precomboLabel = (info: Info): string =>
|
|
info.제목 ? `${info.이름} ${info.제목}` : info.이름;
|
|
|
|
export interface PrecomboOptions {
|
|
layer: Layer;
|
|
canEdit: boolean;
|
|
/** 층 단추 줄(페이지가 만듦 · 누르면 그 층 화면 + 이 컨테이너 펼침) */
|
|
bar: HTMLElement[];
|
|
confirmLeave: () => Promise<boolean>;
|
|
onOpen: (sel: Selection | null) => void;
|
|
isOpen: (kind: Kind, name: string) => boolean;
|
|
/** 목록 다시 받음(좌측 전체) — 받은 뒤 `paint` 가 불림 */
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
export interface PrecomboHandle {
|
|
body: HTMLElement;
|
|
/** 좌측이 받은 층 목록 중 승격 · 사전조합만 그림 */
|
|
paint: (items: TemplateInfo[]) => void;
|
|
}
|
|
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
|
const failed = (error: unknown): void =>
|
|
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error");
|
|
|
|
export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle {
|
|
let items: Info[] = [];
|
|
const lists = new Map<PrecomboKind, HTMLElement>();
|
|
const buttons = new Map<PrecomboKind, { copy: HTMLButtonElement; del: HTMLButtonElement }>();
|
|
const mine = (kind: PrecomboKind): Info[] => items.filter((i) => i.종류 === kind);
|
|
const current = (kind: PrecomboKind): Info | undefined =>
|
|
mine(kind).find((i) => opt.isOpen(kind, i.이름));
|
|
|
|
const select = async (info: Info): Promise<void> => {
|
|
if (!opt.isOpen(info.종류, info.이름) && !(await opt.confirmLeave())) return;
|
|
opt.onOpen({
|
|
layer: opt.layer,
|
|
projectId: null,
|
|
kind: info.종류,
|
|
name: info.이름,
|
|
label: precomboLabel(info),
|
|
});
|
|
};
|
|
|
|
/** 새로 — 서버가 그 층 다음 키로 씀 → 엶 */
|
|
const create = async (kind: PrecomboKind, body: LibraryBody): Promise<void> => {
|
|
const key = (await newLibrary(opt.layer, kind, body)).이름;
|
|
showToast(L("M02_Created"), "success");
|
|
await opt.refresh();
|
|
const info = mine(kind).find((i) => i.이름 === key);
|
|
if (info) await select(info);
|
|
};
|
|
|
|
const nameDialog = (title: string, start: string, onOk: (name: string) => Promise<void>) => {
|
|
const name = createInputField({ label: L("M02_Name"), placeholder: L("M02_Name") });
|
|
name.input.value = start;
|
|
openModal({
|
|
title,
|
|
closeLabel: L("M02_Close"),
|
|
dialogClass: "m02-modal",
|
|
mount: (body, close) => {
|
|
const ok = createButton({ label: L("M02_Create"), variant: "filled" });
|
|
ok.addEventListener("click", async () => {
|
|
const text = name.input.value.trim();
|
|
if (!text) return showToast(L("M02_NameNeeded"), "error");
|
|
ok.disabled = true; // 연달아 눌러도 한 번만
|
|
try {
|
|
await onOk(text);
|
|
close();
|
|
} catch (error) {
|
|
failed(error);
|
|
} finally {
|
|
ok.disabled = false;
|
|
}
|
|
});
|
|
name.input.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" && !event.isComposing) ok.click();
|
|
});
|
|
body.append(name.root, ok);
|
|
},
|
|
});
|
|
};
|
|
|
|
/** 단가승격 새로 = 단가산출 로직 고르기 → 이름 · 단위는 로직 것(입력값은 상세에서 고정) */
|
|
const newPromote = (): void =>
|
|
openLogicPicker((x) => {
|
|
void opt.confirmLeave().then((go) =>
|
|
!go
|
|
? undefined
|
|
: create("promote", {
|
|
문서: {
|
|
이름: x.이름,
|
|
단위: x.결과단위 || null,
|
|
담은로직: [{ 로직: x.키, 입력값: {} }],
|
|
},
|
|
}).catch(failed),
|
|
);
|
|
});
|
|
/** 사전조합 새로 = 이름만 · 빈 묶음(승격 · 사전조합은 상세 [로직 추가]로) */
|
|
const newPrecombo = (): void =>
|
|
nameDialog(L("M02_NewTitle"), "", async (name) => {
|
|
if (!(await opt.confirmLeave())) return;
|
|
await create("precombo", { 문서: { 이름: name, 단위: null, 담은로직: [] } });
|
|
});
|
|
|
|
/** 복사 = 같은 층 새 키(서버 · 본뜬 = 원래 키) → 고른 이름으로 고쳐 씀 */
|
|
const copy = (kind: PrecomboKind): void => {
|
|
const from = current(kind);
|
|
if (!from) return;
|
|
nameDialog(L("M02_CopyTitle"), `${from.제목 ?? from.이름} (2)`, async (name) => {
|
|
if (!(await opt.confirmLeave())) return;
|
|
const made = await newLibrary(opt.layer, kind, {
|
|
본뜬: { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 },
|
|
});
|
|
const got = await readTemplate(opt.layer, kind, made.이름, null);
|
|
await saveTemplate(opt.layer, kind, made.이름, null, made.판, {
|
|
...(got.문서 as object),
|
|
이름: name,
|
|
});
|
|
showToast(L("M02_Created"), "success");
|
|
await opt.refresh();
|
|
const info = mine(kind).find((i) => i.이름 === made.이름);
|
|
if (info) await select(info);
|
|
});
|
|
};
|
|
|
|
const remove = async (kind: PrecomboKind): Promise<void> => {
|
|
await opt.refresh(); // 저장 직후여도 최신 판으로
|
|
const info = current(kind);
|
|
if (!info) return;
|
|
const name = precomboLabel(info);
|
|
if (!(await showConfirmDialog(L("M02_DeleteConfirm").replace("{value}", name)))) return;
|
|
try {
|
|
if (opt.layer === "system") await deleteTemplate(kind, info.이름, info.판);
|
|
else await deleteLayerTemplate(opt.layer, kind, info.이름, info.판);
|
|
showToast(L("M02_Deleted"), "success");
|
|
opt.onOpen(null);
|
|
await opt.refresh();
|
|
} catch (error) {
|
|
// 쓰는 곳이 있으면 서버가 막음 — 까닭(쓰는 곳 이름) 그대로
|
|
showToast(why(error) || L("M02_ActionFailed").replace("{value}", ""), "error");
|
|
}
|
|
};
|
|
|
|
const block = (part: (typeof PARTS)[number]): HTMLElement => {
|
|
const list = el("div", { className: "m02-side__items ui-sidebar-list" });
|
|
lists.set(part.kind, list);
|
|
const row: HTMLElement[] = [];
|
|
if (opt.canEdit) {
|
|
const btnNew = createButton({ label: L("M02_New"), variant: "filled", size: "sm" });
|
|
const btnCopy = createButton({ label: L("M02_Copy"), variant: "ghost", size: "sm" });
|
|
const del = createButton({ label: L("M02_Delete"), variant: "danger", size: "sm" });
|
|
buttons.set(part.kind, { copy: btnCopy, del });
|
|
btnNew.addEventListener("click", part.kind === "promote" ? newPromote : newPrecombo);
|
|
btnCopy.addEventListener("click", () => copy(part.kind));
|
|
del.addEventListener("click", () => void remove(part.kind));
|
|
row.push(el("div", { className: "m02-side__row", children: [btnNew, btnCopy, del] }));
|
|
}
|
|
return el("div", {
|
|
className: "m02-side__sub",
|
|
attrs: { "data-precombo": part.kind },
|
|
children: [el("h4", { className: "m02-side__subtitle", text: part.label }), ...row, list],
|
|
});
|
|
};
|
|
|
|
const body = el("div", {
|
|
className: "m02-side__items ui-sidebar-list",
|
|
children: [...opt.bar, ...PARTS.map(block)],
|
|
});
|
|
|
|
function paint(all: TemplateInfo[]): void {
|
|
items = all.filter((i) => isPrecombo(i.종류));
|
|
for (const part of PARTS) {
|
|
const found = mine(part.kind);
|
|
lists.get(part.kind)?.replaceChildren(
|
|
...(found.length
|
|
? found.map((info) => {
|
|
const b = el("button", {
|
|
className: `m02-side__item ui-sidebar-row${opt.isOpen(part.kind, info.이름) ? " is-active" : ""}`,
|
|
text: precomboLabel(info),
|
|
attrs: { type: "button", title: info.수정일, "data-precombo-key": info.이름 },
|
|
});
|
|
b.addEventListener("click", () => void select(info));
|
|
return b;
|
|
})
|
|
: [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]),
|
|
);
|
|
const b = buttons.get(part.kind);
|
|
if (b) b.copy.disabled = b.del.disabled = !current(part.kind);
|
|
}
|
|
}
|
|
|
|
return { body, paint };
|
|
}
|