/* ============================================================================= * M02_MasterTemplete_Hopyo_Wizard.ts * 일위대가 불러오기 = 한시 마법사(PLAN 718-1 · 요구서 내역12 4장 22) — 본 칸 한 화면 · 걸음 셋: * ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과(일위대가 늘 · 단가산출 체크) · 엑셀 고르기 → 올림 * (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 서버 409 안내) · 작업 중 파일 이어서 * ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] → * 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층 · * 단가산출 체크면 [단가산출 만들기] = 산근 블록마다 바탕 없는 단가산출(서버 `M02_Hopyo_Sangeun` · 겹침 없음) * ③ [끝내기] = 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가 · 단가산출 · 별칭 장부 남음 · 지문 기록 * 작업 중 = 서버에 보관(떠났다 와도 ① 목록에서 이어서) · 옛 HP(지문 없음)는 서버 목록에서 숨김. * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { createCheckbox } from "@ui/ui_template_checkbox"; import { createEmpty } from "@ui/ui_template_empty"; import { createPanelHead } from "@ui/ui_template_panel_head"; import { createSegment } from "@ui/ui_template_segment"; import { createText } from "@ui/ui_template_text"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { deleteHopyo, listHopyo, uploadHopyo, type HopyoFile, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; import { call } from "./M02_MasterTemplete_Api_Fetch"; import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer"; const LAYERS: { layer: ManageLayer; text: string }[] = [ { layer: "personal", text: "개인" }, { layer: "company", text: "회사" }, { layer: "system", text: "마스터" }, ]; const STEPS = [ { value: "1", label: "① 층 · 엑셀" }, { value: "2", label: "② 보기 · 확인 · 만들기" }, { value: "3", label: "③ 끝내기" }, ]; type Got = HopyoFile & { 이어서?: boolean }; const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); const textOf = (layer: string): string => LAYERS.find((x) => x.layer === layer)?.text ?? layer; /** 쓸 수 있는 자기 층(개인 · 회사 · 마스터 차례) */ export async function writableLayers(): Promise { const user = await fetchSessionUser().catch(() => null); return LAYERS.map((x) => x.layer).filter((layer) => accessFor(layer, null, user).canWrite); } export function buildHopyoWizard(): HTMLElement { const steps = createSegment({ options: STEPS.map((s) => ({ ...s, disabled: true })), value: "1", }); steps.root.dataset.hopyoSteps = ""; const body = el("div", { className: "m02-hopyo" }); const root = el("div", { className: "m02-hopyo", attrs: { "data-hopyo-wizard": "" }, children: [ createText( "한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내면 올린 파일 지움 · 별칭 장부는 남음", "hint", ), el("div", { className: "ui-toolbar", children: [steps.root] }), body, ], }); let layers: ManageLayer[] = []; let pick: ManageLayer = "personal"; let wantSangeun = false; // 결과 「단가산출」 체크 const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null }); async function first(): Promise { steps.set("1"); layers = await writableLayers(); if (!layers.includes(pick)) pick = layers[0] ?? "personal"; const input = el("input", { attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" }, }) as HTMLInputElement; input.hidden = true; const upload = createButton({ label: "엑셀 고르기", variant: "filled", disabled: !layers.length, onClick: () => input.click(), }); upload.title = layers.length ? "설계내역서 엑셀(일위대가 시트)" : "쓸 수 있는 층 없음"; input.addEventListener("change", () => { const file = input.files?.[0]; input.value = ""; if (!file) return; upload.disabled = true; uploadHopyo(scopeOf(pick), file) .then((got: Got) => { if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info"); else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success"); second(pick, got); }) .catch((error) => showToast(why(error), "error")) .finally(() => (upload.disabled = !layers.length)); }); const seg = createSegment({ options: LAYERS.map((x) => ({ value: x.layer, label: x.text, disabled: !layers.includes(x.layer), })), value: pick, onChange: (v) => (pick = v as ManageLayer), }); seg.root.dataset.hopyoLayer = ""; const ilwi = createCheckbox("일위대가(고정 줄)", { checked: true, disabled: true }); const sangeun = createCheckbox("단가산출(산근 블록 · 바탕 없음)", { checked: wantSangeun, onChange: (on) => (wantSangeun = on), }); sangeun.root.dataset.hopyoSangeun = ""; const busy = el("div", { className: "m02-hopyo" }); body.replaceChildren( createText("결과 층", "muted", 14), el("div", { className: "ui-toolbar", children: [seg.root] }), createText("결과", "muted", 14), el("div", { className: "ui-toolbar", children: [ilwi.root, sangeun.root] }), el("div", { className: "ui-toolbar", children: [upload, input] }), createPanelHead({ title: "작업 중", level: 4, divider: true }).root, busy, ); const rows = ( await Promise.all( layers.map((layer) => listHopyo(scopeOf(layer)) .then((list) => list.map((f) => ({ layer, f }))) .catch(() => []), ), ) ).flat(); busy.replaceChildren( ...(rows.length ? rows.map(({ layer, f }) => { const row = createButton({ label: `${f.파일} · 호표 ${f.호표수} · ${textOf(layer)} — 이어서`, variant: "ghost", onClick: () => second(layer, f), }); row.dataset.hopyoKey = f.키; return row; }) : [createEmpty({ text: "작업 중인 불러오기 없음", small: true })]), ); } function second(layer: ManageLayer, file: HopyoFile): void { steps.set("2"); const scope = scopeOf(layer); const view = createButton({ label: "엑셀 보기 · 확인 · 만들기", variant: "filled", onClick: () => void import("./M02_MasterTemplete_Hopyo_Sheet") .then((m) => m.openHopyoSheet({ scope, key: file.키, 파일: file.파일, target: scope, canWrite: true, onDeleted: () => void first(), }), ) .catch((error: unknown) => showToast(why(error), "error")), }); const sangeun = createButton({ label: "단가산출 만들기", variant: "filled", onClick: () => void makeSangeun(), }); sangeun.title = "엑셀 산근 블록마다 바탕 없는 단가산출(엑셀 단가 그대로 · 이미 만든 블록은 그대로)"; sangeun.dataset.hopyoSangeunMake = ""; const finish = createButton({ label: "끝내기", variant: "ghost", onClick: () => void end() }); finish.dataset.hopyoFinish = ""; const back = createButton({ label: "처음으로", variant: "link", onClick: () => void first() }); back.title = "작업은 그대로 보관 — 처음 화면 「작업 중」에서 이어서"; const wrong = file.어긋남 ? ` · 검산 어긋남 ${file.어긋남}` : ""; body.replaceChildren( createText(`${file.파일} · ${textOf(layer)} 층 · 호표 ${file.호표수}${wrong}`, "muted", 14), createText( "엑셀 보기 창에서 [모두 고정으로 들여오기] 또는 호표 [수정] → 확인하고 만들기 · [구조물 조합으로] · 만든 일위대가 · 단가산출은 고른 층에 들어감", "hint", ), el("div", { className: "ui-toolbar", children: [view, ...(wantSangeun ? [sangeun] : []), finish, back], }), ); async function makeSangeun(): Promise { sangeun.disabled = true; try { const got = await call<{ 블록: number; 만듦: unknown[]; 그대로: number }>( `/layers/${layer}/hopyo/${encodeURIComponent(file.키)}/sangeun`, { method: "POST" }, ); const kind = got.블록 ? "success" : "warning"; showToast( `단가산출 ${got.만듦.length} 만듦 · 그대로 ${got.그대로} · 산근 블록 ${got.블록}`, kind, ); } catch (error) { showToast(why(error), "error"); } finally { sangeun.disabled = false; } } async function end(): Promise { steps.set("3"); const ok = await showConfirmDialog( `「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출과 별칭 장부는 남음 · 하나라도 만들었으면 같은 엑셀은 다시 올리지 못함`, "끝내기", ); if (!ok) return steps.set("2"); try { const got = (await deleteHopyo(scope, file.키)) as { 만든?: number }; showToast(`끝냄 — 만든 일위대가 · 단가산출 ${got.만든 ?? 0} · 올린 파일 지움`, "success"); } catch (error) { showToast(why(error), "error"); return steps.set("2"); } await first(); } } void first(); return root; }