/* ============================================================================= * M02_MasterTemplete_Hopyo_Side.ts * 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시 · 785-4) — * · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 엑셀 고르기 창 → 올림 → 본 칸 불러오기 보기 * (`M02_MasterTemplete_Hopyo_Import` · 엑셀 + 오른쪽 호표 목록 · 가운데 상세 페이지 없음 · 누름 하나) * · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임) * · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기 * · 다른 엑셀 [불러오기] · 다른 화면으로 = 지금 보기 떠남(올린 파일 지움 · 743-1) — 고르고 안 만든 확인 캐시가 있으면 물음 한 번 * 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층). * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { createEmpty } from "@ui/ui_template_empty"; import { createListRow } from "@ui/ui_template_list_row"; import { createSegment } from "@ui/ui_template_segment"; import { t as L } from "@ui/ui_template_locale"; import { listHopyo, pageScope, uploadHopyo, type HopyoFile, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger"; import { buildHopyoImport, LAYERS, layerText, writableLayers, type HopyoImport, } from "./M02_MasterTemplete_Hopyo_Import"; import type { ManageLayer } from "./M02_MasterTemplete_Layer"; export interface HopyoSectionOptions { /** 이 층 쓰기 권한 — 별칭 장부 고침(구조물 조합 층이 제자리에서 바뀌어 열 때 읽음 · 720-4) */ canWrite: () => boolean; /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ confirmLeave: () => Promise; /** 메인 칸에 화면을 띄움 */ show: (label: string, node: HTMLElement) => (dirty: boolean) => void; /** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */ onOpen: () => void; } export interface HopyoSection { title: string; body: HTMLElement; /** 다른 화면이 열렸으면 고름 표시를 지움 */ paint: () => void; } const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null }); export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { const title = L("M02_HopyoImport"); let layers: ManageLayer[] = []; let pick: ManageLayer = "personal"; const input = el("input", { attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" }, }) as HTMLInputElement; input.hidden = true; const start = createButton({ label: "불러오기", variant: "filled", onClick: () => void choose(), }); start.classList.add("m02-layers__btn"); start.title = "다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내거나 떠나면 올린 파일 지움)"; start.dataset.hopyoWizardOpen = ""; const seg = el("div", { className: "m02-side__row" }); seg.hidden = true; const jobs = el("div", { className: "m02-hopyo-side", attrs: { "data-hopyo-jobs": "" } }); const ledger = el("button", { className: "m02-side__item ui-sidebar-row", text: LEDGER_TITLE, attrs: { type: "button", title: LEDGER_HINT, "data-hopyo-ledger-open": "" }, }); ledger.addEventListener("click", () => void showLedger()); const body = el("div", { className: "m02-hopyo-side", children: [ el("div", { className: "m02-side__row", children: [start, input] }), seg, jobs, ledger, ], }); let open: { key: string; view: HTMLElement; imp?: HopyoImport } | null = null; /** 층 고름 · 작업 중 줄 — 쓸 수 있는 층을 처음 한 번 읽음 */ async function refresh(): Promise { if (!layers.length) { layers = await writableLayers(); if (!layers.includes(pick)) pick = layers[0] ?? "personal"; if (layers.length > 1) { const s = createSegment({ options: LAYERS.filter((x) => layers.includes(x.layer)).map((x) => ({ value: x.layer, label: x.text })), // prettier-ignore value: pick, size: 24, onChange: (v) => (pick = v as ManageLayer), }); s.root.dataset.hopyoLayer = ""; seg.replaceChildren(s.root); seg.hidden = false; } } const rows = ( await Promise.all( layers.map((layer) => listHopyo(scopeOf(layer)) .then((list) => list.map((f) => ({ layer, f }))) .catch(() => []), ), ) ).flat(); jobs.replaceChildren( ...(rows.length ? rows.map(({ layer, f }) => { const row = createListRow({ label: f.파일, sub: `호표 ${f.호표수} · ${layerText(layer)} — 이어서`, size: 28, active: open?.imp !== undefined && open.key === f.키, onClick: () => void show(layer, f), }); row.dataset.hopyoKey = f.키; return row; }) : [createEmpty({ text: "작업 중인 불러오기 없음", small: true })]), ); } /** [불러오기] — 지금 보기를 떠날 수 있는지 묻고 바로 파일 고르기 창 */ async function choose(): Promise { if (!layers.length) await refresh().catch(() => {}); if (!layers.length) return void showToast("쓸 수 있는 층 없음", "warning"); if (!(await leaveOk())) return; input.click(); } input.addEventListener("change", () => { const file = input.files?.[0]; input.value = ""; if (!file) return; start.disabled = true; uploadHopyo(scopeOf(pick), file) .then((got: HopyoFile & { 이어서?: boolean }) => { if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info"); else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success"); if (got.읽기불가) showToast(`가져온 식으로 열 수 없음 — ${got.읽기불가}`, "warning"); return show(pick, got, true); }) .catch((error) => showToast(why(error), "error")) .finally(() => (start.disabled = false)); }); /** 지금 보기를 떠나도 되는지 — 저장 안 한 고침 · 고르고 안 만든 확인 캐시 */ async function leaveOk(): Promise { if (!(await opt.confirmLeave())) return false; if (!open?.imp?.dirty()) return true; return showConfirmDialog( "지금 불러오기를 떠남 — 올린 파일을 지움 · 고르고 안 만든 호표는 빠짐(만든 것은 남음)", "떠나기", ); } /** 본 칸에 불러오기 보기 — 같은 파일이면 그대로 */ async function show(layer: ManageLayer, file: HopyoFile, checked = false): Promise { if (open?.imp && open.key === file.키 && open.view.isConnected) return; if (!checked && !(await leaveOk())) return; open?.imp?.leave(); // 불러오기 떠남 = 올린 파일 지움(743-1) const imp = buildHopyoImport({ layer, file, onEnded: () => { open = null; ledger.classList.remove("is-active"); opt.show(title, el("div", { className: "m02-hopyo" })); // 끝냄 — 빈 본 칸 void refresh(); }, }); opt.show(`${title} — ${file.파일}`, imp.root); opt.onOpen(); open = { key: file.키, view: imp.root, imp }; ledger.classList.remove("is-active"); void refresh(); } async function showLedger(): Promise { if (open?.key === LEDGER_TITLE && open.view.isConnected) return; if (!(await leaveOk())) return; open?.imp?.leave(); const view = buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() }); opt.show(`${title} — ${LEDGER_TITLE}`, view); opt.onOpen(); open = { key: LEDGER_TITLE, view }; ledger.classList.add("is-active"); void refresh(); } const paint = (): void => { if (open && !open.view.isConnected) { open.imp?.leave(); // 다른 화면으로 = 불러오기 떠남 open = null; ledger.classList.remove("is-active"); void refresh(); } }; void refresh(); return { title, body, paint }; }