/* ============================================================================= * M02_MasterTemplete_Hopyo_SheetNav.ts * 엑셀 보기 창 세 보기 막대(PLAN 779-4 · 779-6 · 요구서 621 3장 4 · 5) — 범례(부품 `createLegend`) · * 머리 막대 = [◂ 뒤로] · 지금 고른 행에 맞는 뛰기 단추(▸ 단가산출 상세 · ▸ 단가산출표 · ▸ 제N호표 · ▸ 부르는 줄 i/n) · * 원본 시트 이름 · 그 행 칠 까닭 글(블록 「예상」 가름). * · 탭 = 서버 역할 id(779-1) 시트를 같은 글(일위대가표 · 단가산출표 · 단가산출 상세)로 · 원본 이름은 막대 * · 칠 = `logic/M02_MasterTemplete_Hopyo_SheetMap` 이 셈 · 격자 옵션 `칠`(779-3) 로 보이는 칸만 · A 열 ~ 끝 열 · 띠는 A 열만 * · 다시 칠 = 목록 다시 읽음(`marks`) · 확인 창 캐시 바뀜(`HOPYO_DRAFT`) · 만들기(`HOPYO_BUILT` → 목록 · 블록 다시 읽음) * · 블록을 여러 줄이 부르면 [▸ 부르는 줄] 이 차례로 돌림 · 뛸 때마다 그 앞 자리를 쌓고 [◂ 뒤로] 가 꺼냄 · 두 번 누름 = 첫 단추 * · 779-6 — 마지막 격자 다시 씀(`gridOf`) · 열린 창 앞으로 꺼냄 · 닫기 = 제자리로(`raise` · `lower`) * 블록 재료 = 서버 `GET …/hopyo-marks/{키}`(779-2 · 읽기만). * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; import { createLegend } from "@ui/ui_template_flow_legend"; import { t as L } from "@ui/ui_template_locale"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import { call } from "./M02_MasterTemplete_Api_Fetch"; import { draftKey, hasDraft, HOPYO_BUILT, loadDraft } from "./M02_MasterTemplete_Hopyo_Check"; import { at, readHopyo, readHopyoGrid, type HopyoGrid, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; import { indexOf, jumpsAt, paintOf, ROLES, type BlockMark, type BriefMarks, type GridMore, type Jump, type Mark, type Role, } from "./logic/M02_MasterTemplete_Hopyo_SheetMap"; /** 확인 창 캐시를 고쳤음(document 알림 · `Hopyo_Check.saveDraft` 가 같은 글로 냄) — 엑셀 보기 창 다시 칠 */ export const HOPYO_DRAFT = "m02-hopyo-draft"; const TAB = { 일위대가표: "M02_SheetIlwi", 단가산출표: "M02_SheetList", 단가산출상세: "M02_SheetDetail" } as const; // prettier-ignore const enc = encodeURIComponent; export const readHopyoMarks = async ( scope: HopyoScope, key: string, to: HopyoScope, ): Promise => { const want = `?to=${to.layer}${to.owner ? `&to_owner=${enc(to.owner)}` : ""}`; return (await call<{ 블록: BlockMark[] }>(at(scope, `/hopyo-marks/${enc(key)}${want}`))).블록; }; /** 마지막 격자(최대 1 MB · 779-6) — 같은 파일 창을 다시 열면 다시 안 받음 · 못 받음은 안 둠 */ let kept: { id: string; got: Promise } | null = null; export function gridOf(id: string, scope: HopyoScope, key: string): Promise { if (kept?.id !== id) kept = { id, got: readHopyoGrid(scope, key) }; const got = kept.got; got.catch(() => kept?.got === got && (kept = null)); return got; } export const dropGrid = (): void => void (kept = null); /** 창(모달 바탕)을 맨 앞으로 — 원래 위에 있던 것들(제자리 표시) · 이미 맨 앞이면 [] */ export function raise(node: Element): Element[] { const back = node.closest(".ui-modal__backdrop"); let above = back?.nextElementSibling ?? null; while (above && !above.matches(".ui-modal__backdrop")) above = above.nextElementSibling; if (!back || !above) return []; document.body.append(back); return [above]; } /** 꺼낸 창을 제자리로 — 했으면 참(닫기 대신) */ export function lower(node: Element, above: Element[]): boolean { const back = node.closest(".ui-modal__backdrop"); if (!back || !above[0]?.isConnected) return false; document.body.insertBefore(back, above[0]); return true; } /** 역할 시트 탭 글 통일 — 역할 → 시트 id */ export function nameTabs(book: Workbook, grid: HopyoGrid): Partial> { const ids = (grid as HopyoGrid & GridMore).보기 ?? {}; for (const r of ROLES) { const s = book.시트.find((x) => x.id === ids[r]); if (s) s.이름 = L(TAB[r]); } return ids; } export interface SheetNavOptions { scope: HopyoScope; key: string; target: HopyoScope; grid: HopyoGrid; /** 그 시트 끝 열 글자(범위 A1 · 칠 열 끝) */ last: (sheet: string) => string; sheet: () => SpreadsheetHandle | null; /** 두 번 누름을 들을 자리(시트 칸) */ host: HTMLElement; } const toCol = (letters: string): number => [...letters].reduce((n, ch) => n * 26 + ch.charCodeAt(0) - 64, 0) - 1; export function createSheetNav(opt: SheetNavOptions) { const grid = opt.grid as HopyoGrid & GridMore; const ix = indexOf(grid); const roleOf = new Map(Object.entries(ix.ids).map(([r, id]) => [id, r as Role])); let briefs: BriefMarks[] = []; let blocks: BlockMark[] = []; let paint = new Map>(); let here = { 시트: ix.ids.일위대가표 ?? "", 범위: "", 행: 0 }; const back: { 시트: string; 범위: string }[] = []; const turn = new Map(); // 블록 → 다음 부르는 줄 차례 const lastCol = new Map(); const colOf = (sheet: string): number => lastCol.get(sheet) ?? lastCol.set(sheet, toCol(opt.last(sheet))).get(sheet)!; const draftOf = (no: number) => { const k = draftKey(opt.scope, opt.key, no); return hasDraft(k) ? loadDraft(k) : null; }; const go = (range: string, sheet: string): void => void opt.sheet()?.select(range, sheet); const buttons = el("div", { className: "ui-toolbar m02-hopyo__bar" }); const source = el("span", { className: "ui-text ui-text--muted", attrs: { "data-text": "14" } }); const why = el("span", { className: "ui-text ui-text--muted", attrs: { "data-text": "14" } }); why.setAttribute("data-hopyo-why", ""); const legend = createLegend([ { label: L("M02_ToneSuccess"), color: "var(--color-success)" }, { label: L("M02_ToneWarning"), color: "var(--color-warning)" }, { label: L("M02_ToneInfo"), color: "var(--color-info)" }, { label: L("M02_ToneDanger"), color: "var(--color-danger)" }, { label: L("M02_ToneMuted"), color: "var(--color-text-muted)" }, { label: L("M02_ToneMine"), color: "var(--color-accent)", kind: "line" }, ]); const bar = el("div", { className: "ui-toolbar m02-hopyo__bar", children: [buttons, source, why] }); // prettier-ignore const root = el("div", { className: "m02-hopyo-sheet__nav", children: [bar, legend] }); root.setAttribute("data-hopyo-nav", ""); /** 뛰기 — 지금 자리를 쌓고 감 · 행 하나면 [n, n] */ function jumpTo(sheet: string, rows: number | [number, number]): void { const [a, b] = typeof rows === "number" ? [rows, rows] : rows; if (here.시트) back.push({ 시트: here.시트, 범위: here.범위 || `A${here.행 || 1}` }); go(`A${a}:${opt.last(sheet)}${b}`, sheet); } const word = (j: Jump, i: number): string => { const key = { 상세: "M02_SheetToDetail", 목록: "M02_SheetToList", 호표: "M02_SheetToHopyo", 부름: "M02_SheetToCaller" } as const; // prettier-ignore return L(key[j.갈래]) .replace("{n}", String(j.번)) .replace("{i}", String(i + 1)) .replace("{all}", String(j.곳.length)); }; function paintBar(): void { const backBtn = createButton({ label: L("M02_SheetBack"), variant: "ghost", size: "sm", onClick: () => { const to = back.pop(); if (to) go(to.범위, to.시트); }, }); backBtn.disabled = !back.length; backBtn.setAttribute("data-hopyo-back", ""); const jumps = jumpsAt(ix, here.시트, here.행).map((j) => { const self = j.곳.findIndex((s) => s.시트 === here.시트 && s.행[0] === here.행); const next = self >= 0 ? self + 1 : (turn.get(j.번) ?? 0); // 부르는 줄 위 = 그 다음 const i = j.갈래 === "부름" ? next % j.곳.length : 0; const b = createButton({ label: word(j, i), variant: "ghost", size: "sm", onClick: () => { const mine = here.시트 === ix.ids.일위대가표 ? (ix.callers.get(j.번) ?? []) : []; const from = mine.findIndex((l) => l.행 === here.행); // 부르는 줄에서 옴 = 그 다음부터 turn.set(j.번, j.갈래 === "부름" ? i + 1 : from + 1); jumpTo(j.곳[i].시트, j.곳[i].행); }, }); b.setAttribute("data-hopyo-jump", j.갈래); return b; }); buttons.replaceChildren(backBtn, ...jumps); const role = roleOf.get(here.시트); const name = role ? grid.원본?.[role] : undefined; source.textContent = name ? L("M02_SheetSource").replace("{name}", name) : ""; why.textContent = paint.get(here.시트)?.get(here.행)?.글 ?? ""; } function repaint(): void { paint = paintOf(ix, briefs, blocks, draftOf); opt.sheet()?.repaint(); paintBar(); } async function readBlocks(): Promise { blocks = await readHopyoMarks(opt.scope, opt.key, opt.target).catch(() => blocks); repaint(); } const onDraft = (): void => repaint(); const onBuilt = (): void => { void readHopyo(opt.scope, opt.key).then((v) => (briefs = v.호표 as BriefMarks[]), () => {}).then(readBlocks); // prettier-ignore }; const onTwice = (): void => root.querySelector("[data-hopyo-jump]")?.click(); document.addEventListener(HOPYO_DRAFT, onDraft); document.addEventListener(HOPYO_BUILT, onBuilt); opt.host.addEventListener("dblclick", onTwice); paintBar(); return { root, /** 격자 옵션 `칠` — 시트 · 행 · 열(0부터) */ fill(sheet: string, r: number, c: number): string | null { const m = c <= colOf(sheet) ? paint.get(sheet)?.get(r + 1) : undefined; // A 열부터(STmate 명칭 = A) return !m ? null : c === 0 ? m.tone : m.tone.replace(" mine", ""); }, /** 고름 바뀜 — 맥락 단추 다시 */ at(sel: { 시트: string; 칸: string; 범위: string }): void { here = { 시트: sel.시트, 범위: sel.범위, 행: Number(/\d+/.exec(sel.칸)?.[0] ?? 0) }; paintBar(); }, /** 목록 다시 읽음(줄상태 · 만든) — 블록 판정도 다시(셈 중 → 끝) */ marks(next: unknown[]): void { briefs = next as BriefMarks[]; repaint(); void readBlocks(); }, jumpTo, destroy(): void { document.removeEventListener(HOPYO_DRAFT, onDraft); document.removeEventListener(HOPYO_BUILT, onBuilt); opt.host.removeEventListener("dblclick", onTwice); }, }; } export type SheetNav = ReturnType;