/* ============================================================================= * M02_MasterTemplete_Hopyo_SheetSk.ts * 엑셀 보기 창 오른쪽 판 바꾸기(PLAN 780-2 · 785-6 · 요구 627 · 642) — 탭 따라: * · 일위대가표 = 호표 목록(`Hopyo_Sheet` 판 그대로) · 단가산출표 · 단가산출 상세 = 단가산출 목록 * (새 블록 판정 `hopyo-judge` 목록 블록 줄 = 판정 · 고를 것 n · 표지 · 부름 · 만든 SK · 캐시 확인) * · 목록 블록 줄 누름 → 지금 탭의 그 블록 자리 + 시트 아래 줄 판 = 블록 판(`Hopyo_Block.createJudgeBlock` · [확인]) * · 옮김 = 이 판 아래 [옮기기](`Hopyo_Rows.createJudgeMove` · 호표 목록 판 것과 같은 캐시 · 한 몸) * · 시트 칸 고름 → 그 행 블록을 목록에서 강조(`SheetNav.blockAt`) * · 목록은 처음 단가산출 탭에 올 때 읽음 · 만들 층 ≠ 파일 층 · 쓰기 권한 없음 = 보기만 * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createListRow } from "@ui/ui_template_list_row"; import { createText } from "@ui/ui_template_text"; import { POLL_MS, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; import { createJudgeBlock, type JudgeState } from "./M02_MasterTemplete_Hopyo_Block"; import { createJudgeMove } from "./M02_MasterTemplete_Hopyo_Rows"; import { readJudge, type JudgeBlockBrief } from "./M02_MasterTemplete_Hopyo_JudgeApi"; import type { SheetNav } from "./M02_MasterTemplete_Hopyo_SheetNav"; export interface SheetSkOptions { scope: HopyoScope; key: string; target: HopyoScope; canWrite: boolean; nav: SheetNav; state: JudgeState; /** 호표 목록 판 — 단가산출 탭이면 숨김 · 그 뒤에 단가산출 판을 붙임 */ side: HTMLElement; /** 시트 아래 줄 판 자리 열기 · 닫기(`Hopyo_Sheet` 가 쥠) */ openLines: (make: (host: HTMLElement) => { destroy: () => void }) => void; closeLines: () => void; /** 판정 · 옮김 뒤(부르는 쪽 끝내기 글) */ onChange?: () => void; /** 옮긴 뒤(목록 · 칠 다시 읽음) */ onMoved?: () => void; } const sameLayer = (a: HopyoScope, b: HopyoScope): boolean => a.layer === b.layer && (a.owner ?? null) === (b.owner ?? null); const STRIPE = { done: "var(--color-success)", ready: "var(--color-info)", pick: "var(--color-warning)", } as const; export function createSheetSk(opt: SheetSkOptions) { const list = el("div", { className: "m02-hopyo-sheet__list" }); const readOnly = !opt.canWrite || !sameLayer(opt.scope, opt.target); const move = createJudgeMove({ file: opt.scope, key: opt.key, target: opt.target, state: opt.state, readOnly, ...(opt.onMoved ? { onMoved: opt.onMoved } : {}), }); const root = el("div", { className: "m02-hopyo-sheet__side m02-hopyo-sheet__sk", children: [list, move.root], }); root.setAttribute("data-hopyo-sk-side", ""); root.hidden = true; opt.side.after(root); let blocks: JudgeBlockBrief[] | null = null; let asked = false; let sheet = ""; let current: number | null = null; function draw(): void { if (!blocks) return; const d = opt.state.get(); list.replaceChildren( ...blocks.map((b) => { const ok = !!d.블록[String(b.번)]?.확인; const sub = [ b.단위, b.판정 ? `${b.판정}${b.판정 === "고를 것" && b.고를수 ? ` ${b.고를수}` : ""}` : "", b.표지 ?? "", b.부름.length ? `${L("M02_JudgeCalls")} ${b.부름.length}` : L("M02_JudgeOutside"), ].filter(Boolean).join(" · "); // prettier-ignore const tail = b.만든 ? createBadge(`${L("M02_JudgeMade")} ${b.만든}`, "success") : ok ? createBadge(L("M02_JudgeConfirmed"), "accent") : ""; const row = createListRow({ label: `${L("M02_JudgeBlock").replace("{n}", String(b.번))} ${b.이름}`, sub, trailing: tail, stripe: b.만든 || ok ? STRIPE.done : b.판정 === "고를 것" ? STRIPE.pick : STRIPE.ready, active: b.번 === current, size: 36, onClick: () => pick(b.번), }); row.dataset.judgeSk = String(b.번); return row; }), ); } /** 목록 — 처음 단가산출 탭에 올 때 한 번 */ function ensure(): void { if (asked) return; asked = true; if (!blocks) list.replaceChildren(createText(L("M02_JudgeLoading"), "hint")); void readJudge(opt.scope, opt.key, opt.target) .then((got) => { blocks = got.블록; draw(); opt.onChange?.(); // 블록 풀이 뒤 셈 중 — 끝나면 판정 · 고를 것이 실림(3 초마다 · 판이 붙어 있는 동안) if (got.셈중) setTimeout(() => list.isConnected && ((asked = false), ensure()), POLL_MS); }) .catch((e: Error) => list.replaceChildren(createText(e.message, "error"))); } /** 블록 고름 — 그 자리 뜀 + 줄 판 = 블록 판 */ function pick(no: number): void { current = no; draw(); const list = opt.nav.role(sheet) === "단가산출표"; const spot = list ? (opt.nav.spotOf(no, "단가산출표") ?? opt.nav.spotOf(no, "단가산출상세")) : (opt.nav.spotOf(no, "단가산출상세") ?? opt.nav.spotOf(no, "단가산출표")); if (spot) opt.nav.jumpTo(spot.시트, spot.행); const detail = opt.nav.spotOf(no, "단가산출상세")?.시트; opt.openLines((host) => createJudgeBlock({ host, file: opt.scope, key: opt.key, target: opt.target, state: opt.state, no, readOnly, onClose: opt.closeLines, ...(detail ? { onSheet: (r: number) => opt.nav.jumpTo(detail, r) } : {}), }), ); } const off = opt.state.on(draw); return { root, /** 아직 안 옮긴 블록 수 · 그중 일위대가 밖 · 고를 것 합 — 목록 전 null */ count: (): { 블록: number; 밖: number; 줄: number } | null => { if (!blocks) return null; const left = blocks.filter((b) => !b.만든); const 밖 = left.filter((b) => !b.부름.length).length; return { 블록: left.length, 밖, 줄: left.reduce((a, b) => a + (b.고를수 ?? 0), 0) }; }, /** 그 블록으로 뜀 + 블록 판 */ show(번: number): void { ensure(); pick(번); }, /** 옮긴 뒤 — 목록 다시 읽음(만든 SK) */ reload(): void { asked = false; if (!root.hidden) ensure(); }, /** 시트 고름 · 탭 바꿈 — 판 바꾸기 · 강조 */ at(sel: { 시트: string; 칸: string }): void { sheet = sel.시트; const role = opt.nav.role(sel.시트); const sk = role === "단가산출표" || role === "단가산출상세"; opt.side.hidden = sk; root.hidden = !sk; opt.side.parentElement?.classList.toggle("is-sk", sk); if (!sk) return; ensure(); const no = opt.nav.blockAt(sel.시트, Number(/\d+/.exec(sel.칸)?.[0] ?? 0)); if (no !== null && no !== current) { current = no; draw(); } }, destroy(): void { off(); move.destroy(); }, }; } export type SheetSk = ReturnType;