/* ============================================================================= * B08_Quantity_UI_StructureSheet_Library.ts * 구조물도 양식 가져오기 칸 (PLAN 4장) — 로그인한 사람의 개인·회사 단 + 프로그램 기본에서 * 골라 **프로젝트 작업본에 박음**. * * ⛔ 표를 그릴 때는 라이브러리를 안 읽음 — [목록 보기]를 눌렀을 때만 목록을 받음(판정 Ⓑ). * ⚠ 모양(클래스)은 옆 제원 칸(`b08-spec*`) 것을 그대로 씀 — 그 칸이 스타일을 넣음. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { buildStmatePanel } from "./B08_Quantity_UI_StructureSheet_Stmate"; const TIER_LABELS: Record = { personal: "개인", company: "회사", program: "기본" }; /** 항목 종류 배지 — 양식형 = 제원을 바꾸면 수량이 다시 남 · 고정형 = 박힌 수량(명세 13장). */ const KIND_LABELS: Record = { form: "양식형", fixed: "고정형" }; interface LibraryItem { tier: string; code: string; name: string; kind: string; } /** 장 머리의 양식 표시 — **어느 단에서 가져왔나** + **그 뒤 고쳤나**(지금 읽는 단이 아님). */ export function libraryLabel( item: { name: string; imported_from?: string | null }, editedRows: number, ): string { const from = item.imported_from ? `${TIER_LABELS[item.imported_from] ?? item.imported_from}에서 가져옴` : "기본 · 가져오기 전"; return `양식 「${item.name}」(${from})${editedRows ? ` · 고친 식 ${editedRows}줄` : ""}`; } function libraryUrl(projectId: string, tail: string): string { return `${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-sheets/library${tail}`; } async function readJson(response: Response): Promise { const payload = (await response.json().catch(() => ({}))) as T & { message?: string }; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); return payload; } export interface LibraryPanelOptions { projectId: string; sheetKey: string; typeId: string; currentCode: string | null; /** 거짓이면 안 가져옴 — 서버가 그 종류의 고친 식을 비우므로 부르는 쪽이 먼저 물음. */ confirmTake: () => boolean; /** 저장 안 한 식이 있으면 [내 라이브러리에 저장]을 막음 — 저장된 식만 개인 단으로 감. */ isDirty: () => boolean; onImported: (notes: string[]) => Promise; } /** 가져오기 · [내 라이브러리에 저장] · [내 것 지우기] 칸. 개인 단 두 단추는 프로젝트를 안 바꿈. */ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement { const { projectId, sheetKey, typeId, currentCode, confirmTake, isDirty, onImported } = options; const panel = document.createElement("div"); panel.className = "b08-spec ui-sidebar-section"; const title = document.createElement("h3"); title.className = "b08-spec__title"; title.textContent = "양식 가져오기"; const scope = document.createElement("p"); scope.className = "b08-spec__scope"; scope.textContent = "가져온 양식은 이 프로젝트에 박혀 누가 열어도 같은 값으로 섭니다."; const status = document.createElement("p"); status.className = "b08-spec__scope"; const list = document.createElement("select"); list.className = "b08-spec__input"; list.hidden = true; const load = document.createElement("button"); load.type = "button"; load.className = "b08-quantity__tab"; load.textContent = "목록 보기"; const take = document.createElement("button"); take.type = "button"; take.className = "b08-spec__save"; take.textContent = "가져오기"; take.hidden = true; // 복제 — 기본·회사 항목을 개인 단에 베낌(PLAN 4장). 개인 단 것은 이미 내 것이라 안 눌림. const clone = document.createElement("button"); clone.type = "button"; clone.className = "b08-quantity__tab"; clone.textContent = "복제해서 내 것으로"; clone.hidden = true; /** 목록을 다시 받은 뒤 보일 한 줄 — 받는 동안 상태 줄이 지워져 복제 결과가 사라지지 않게. */ let afterLoad = ""; const syncClone = (): void => { const [tier] = list.value.split("|"); clone.disabled = tier === "personal"; }; list.addEventListener("change", syncClone); clone.addEventListener("click", () => { const [tier, code] = list.value.split("|"); const label = list.selectedOptions[0]?.textContent ?? ""; if (!tier || !code || tier === "personal") return; if ( !window.confirm( `「${label}」을 내 라이브러리로 베낌 — 같은 종류 내 것이 있으면 덮어씀 · 프로젝트 값은 안 바뀜`, ) ) { return; } void (async () => { clone.disabled = true; try { await readJson( await fetch(libraryUrl(projectId, "/clone"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type_id: typeId, tier, code }), }), ); afterLoad = "내 라이브러리에 베낌 — 가져와 고친 뒤 [내 라이브러리에 저장]"; load.click(); } catch (error) { status.textContent = error instanceof Error ? error.message : "복제 못함"; syncClone(); } })(); }); load.addEventListener("click", () => { void (async () => { load.disabled = true; status.textContent = "목록 받는 중…"; try { const { items, can_publish: canPublish } = await readJson<{ items: LibraryItem[]; can_publish?: { company: boolean; program: boolean }; }>( await fetch(libraryUrl(projectId, `?type_id=${encodeURIComponent(typeId)}`), { credentials: "include", }), ); list.replaceChildren( ...items.map((item) => { const option = document.createElement("option"); option.value = `${item.tier}|${item.code}`; const now = item.code === currentCode ? " (지금)" : ""; const kind = KIND_LABELS[item.kind] ?? item.kind; option.textContent = `${TIER_LABELS[item.tier] ?? item.tier} · [${kind}] ${item.name}${now}`; return option; }), ); list.hidden = take.hidden = clone.hidden = items.length === 0; syncClone(); // 발행 단추 — 서버가 준 권한대로만 보임(회사 = 마스터 · 기본 = 시스템 관리자). toCompany.hidden = !canPublish?.company; toProgram.hidden = !canPublish?.program; status.textContent = items.length ? afterLoad : "가져올 항목이 없음"; afterLoad = ""; } catch (error) { status.textContent = error instanceof Error ? error.message : "목록을 받지 못함"; } finally { load.disabled = false; } })(); }); take.addEventListener("click", () => { const [tier, code] = list.value.split("|"); if (!tier || !code || !confirmTake()) return; void (async () => { take.disabled = true; status.textContent = "가져오는 중…"; try { const result = await readJson<{ cleared_formulas: number }>( await fetch(libraryUrl(projectId, "/import"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type_id: typeId, tier, code }), }), ); const cleared = result.cleared_formulas ? ` · 고친 식 ${result.cleared_formulas}줄 비움` : ""; // 프로젝트 작업본이 바뀌었으니 표를 **다시 받아** 그림. await onImported([`양식을 가져왔습니다${cleared}.`]); } catch (error) { status.textContent = error instanceof Error ? error.message : "가져오지 못함"; take.disabled = false; } })(); }); // 개인 단 두 단추 — 목록을 다시 받아야 보이므로 끝나면 [목록 보기]를 한 번 누른 것처럼 갱신. const personal = (label: string, run: () => Promise): HTMLButtonElement => { const button = document.createElement("button"); button.type = "button"; button.className = "b08-quantity__tab"; button.textContent = label; button.addEventListener("click", () => { void (async () => { button.disabled = true; try { status.textContent = await run(); if (!list.hidden) load.click(); } catch (error) { status.textContent = error instanceof Error ? error.message : `${label} 못함`; } finally { button.disabled = false; } })(); }); return button; }; const save = personal("내 라이브러리에 저장", async () => { if (isDirty()) return "저장 안 한 식이 있음 — [식 저장] 먼저"; if ( !window.confirm("이 장의 양식과 고친 식을 내 라이브러리에 저장 — 같은 종류가 있으면 덮어씀") ) { return ""; } const result = await readJson<{ edited: number }>( await fetch(libraryUrl(projectId, "/personal"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sheet_key: sheetKey }), }), ); return `내 라이브러리에 저장함${result.edited ? ` · 고친 식 ${result.edited}줄 포함` : ""}`; }); const remove = personal("내 것 지우기", async () => { if (!window.confirm("내 라이브러리의 이 종류 양식을 지움 — 이 프로젝트 값은 안 바뀜")) return ""; const result = await readJson<{ deleted: number }>( await fetch(libraryUrl(projectId, `/personal?type_id=${encodeURIComponent(typeId)}`), { method: "DELETE", credentials: "include", }), ); return result.deleted ? "내 라이브러리에서 지움" : "지울 내 양식이 없음"; }); // 발행 — [내 라이브러리에 저장]과 같은 모양으로 회사·프로그램 기본 단에(2026-09-14 브레인 승인). const publish = (label: string, tier: "company" | "program"): HTMLButtonElement => { const button = personal(label, async () => { if (isDirty()) return "저장 안 한 식이 있음 — [식 저장] 먼저"; const whom = tier === "program" ? "모든 회사가 쓰는 프로그램 기본" : "우리 회사 라이브러리"; if (!window.confirm(`이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀`)) { return ""; } await readJson( await fetch(libraryUrl(projectId, "/publish"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sheet_key: sheetKey, tier }), }), ); return `${whom}에 발행함`; }); button.hidden = true; return button; }; const toCompany = publish("회사 라이브러리에 발행", "company"); const toProgram = publish("프로그램 기본으로 발행", "program"); const mine = document.createElement("div"); mine.className = "b08-sheet__actions"; mine.append(save, remove, toCompany, toProgram); panel.append(title, scope, load, list, take, clone, mine, status); // 고정형 항목을 만드는 둘째 길 — STmate 출력 엑셀에서 호표 하나를 뽑아 개인 단에(PLAN 4장). panel.append( buildStmatePanel({ projectId, typeId, onSaved: () => { if (!list.hidden) load.click(); }, }), ); return panel; }