- 지움(git rm) 15 파일 3,318 줄: Hopyo_Check · CheckRows · Choose · WizardSk · SkApi · SkLines · logic/SkPick · logic/Draft · Left · Lost · Pick · SheetMake · Kids · ToItem · Compare(모두 옛 확인 창 · 마법사 단가산출 · 안 이은 줄 길 몫 · 부르는 곳 0) - 단추 판단: 엑셀 보기 [저장](체크 호표) · [모두 고정으로 들여오기] 화면 단추 걷음 → 만들기 = [옮기기] 하나 · 모두 고정 서버 길(Builder · build_all)은 B09 · 붙여넣기가 써서 남김 · 목록 체크 칸 · 모달 껍데기(openHopyoSheet · raise · lower) 걷음 - Import: 「안 이은 줄」 띠 · [안 이은 줄 고르기] · 남은 줄 셈 걷음 · [끝내기] = 물음(확인하고 안 옮긴 수 = 판정 고름 캐시 moveBody) - Api: 옛 맞춤 타입 · 길(readHopyoOne · guessHopyo · buildHopyo · buildHopyoAll · leftHopyo · listHopyoPicks · hopyoSiblings · buildHopyoItem …) 걷음 · POLL_MS · HOPYO_BUILT 옮겨 옴 · 목록 꼴 = 1차 판정 줄상태(Judge.view) - Labor_Pick: withHopyo(실무 호표 덧줄) → withSangeun(찾아 넣기 창 표시만) · Detail · Table 부름 바꿈 - Ledger: unitField 옮겨 옴 · SheetNav: 옛 확인 캐시 칠 걷음 · UI_Page: [구조물 조합으로] 듣기 걷음 - CSS: 옛 확인 창 · 고르기 창 규칙 34 걷음(481 → 287 줄) · 390 폭 = 줄 판 열리면 위 칸 넓힘 · 줄 판 높이 바닥 16rem 시험 정리(까닭 = 지운 화면 파일만 읽음): - 지움: test_729_3_hopyo_left_screen · test_746_2_hopyo_compare_screen · test_778_5_sk_pick_screen · helper_729_3 · helper_m02_hopyo_check · pick · seam - test_m02_hopyo_screen = 좌측 판 · 끝내기 둘만 · 627 · helper_778_5 = SheetSk 판 바꿈만 · 785_4 · 721 · 717_4 · 720 · 739_1 = 지금 꼴로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
273 lines
12 KiB
TypeScript
273 lines
12 KiB
TypeScript
/* =============================================================================
|
|
* 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`) · 판정 화면 확인 바뀜(`repaint`) · 만들기(`HOPYO_BUILT` → 목록 · 블록 다시 읽음)
|
|
* · 블록을 여러 줄이 부르면 [▸ 부르는 줄] 이 차례로 돌림 · 뛸 때마다 그 앞 자리를 쌓고 [◂ 뒤로] 가 꺼냄 · 두 번 누름 = 첫 단추
|
|
* · 779-6 — 마지막 격자 다시 씀(`gridOf`)
|
|
* · 778-5 D-6 — 머리 막대 아래 띠(부품 `createBanner`) 「엑셀 셈 어긋남 n」 + [보기 i/m] = 상세 시트 그 행으로 차례로 뛰기 ·
|
|
* 그 행 까닭은 막대 글에 덧붙임 · 칠은 안 바꿈
|
|
* · 780-2 — `role(시트)` · `blockAt(시트, 행)`(단가산출표 목록 행 · 상세 블록 범위) · `spotOf(번, 역할)` 내보냄(오른쪽 단가산출 목록)
|
|
* 블록 재료 = 서버 `GET …/hopyo-marks/{키}`(779-2 · 읽기만).
|
|
* ========================================================================== */
|
|
|
|
import { createBanner } from "@ui/ui_template_banner";
|
|
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 {
|
|
at,
|
|
HOPYO_BUILT,
|
|
readHopyo,
|
|
readHopyoGrid,
|
|
type HopyoGrid,
|
|
type HopyoScope,
|
|
} from "./M02_MasterTemplete_Hopyo_Api";
|
|
import {
|
|
indexOf,
|
|
jumpsAt,
|
|
missesOf,
|
|
numAt,
|
|
paintOf,
|
|
ROLES,
|
|
type BlockMark,
|
|
type BriefMarks,
|
|
type GridMore,
|
|
type Jump,
|
|
type Mark,
|
|
type Role,
|
|
type Spot,
|
|
} from "./logic/M02_MasterTemplete_Hopyo_SheetMap";
|
|
|
|
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<BlockMark[]> => {
|
|
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<HopyoGrid> } | null = null;
|
|
export function gridOf(id: string, scope: HopyoScope, key: string): Promise<HopyoGrid> {
|
|
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);
|
|
|
|
/** 역할 시트 탭 글 통일 — 역할 → 시트 id */
|
|
export function nameTabs(book: Workbook, grid: HopyoGrid): Partial<Record<Role, string>> {
|
|
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;
|
|
/** 판정 화면에서 확인한 줄(785-6 고름 캐시) */
|
|
judged?: (no: number, at: number) => boolean;
|
|
}
|
|
|
|
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<string, Map<number, Mark>>();
|
|
let here = { 시트: ix.ids.일위대가표 ?? "", 범위: "", 행: 0 };
|
|
const back: { 시트: string; 범위: string }[] = [];
|
|
const turn = new Map<number, number>(); // 블록 → 다음 부르는 줄 차례
|
|
const lastCol = new Map<string, number>();
|
|
const colOf = (sheet: string): number =>
|
|
lastCol.get(sheet) ?? lastCol.set(sheet, toCol(opt.last(sheet))).get(sheet)!;
|
|
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 band = el("div", { className: "m02-hopyo-sheet__miss" });
|
|
band.setAttribute("data-hopyo-miss", "");
|
|
const root = el("div", { className: "m02-hopyo-sheet__nav", children: [bar, band, legend] });
|
|
let misses: ReturnType<typeof missesOf> = [];
|
|
let missAt = 0;
|
|
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) : "";
|
|
const miss = here.시트 === ix.ids.단가산출상세 ? misses.filter((m) => m.행 === here.행) : [];
|
|
const text = [paint.get(here.시트)?.get(here.행)?.글, ...miss.map((m) => `${L("M02_SheetMissOne")} ${m.글}`)]; // prettier-ignore
|
|
why.textContent = text.filter(Boolean).join(" · ");
|
|
}
|
|
|
|
/** 엑셀 셈 어긋남 띠 — 없으면 비움 · [보기] = 다음 자리로 */
|
|
function paintBand(): void {
|
|
misses = missesOf(ix, blocks);
|
|
const detail = ix.ids.단가산출상세;
|
|
if (!misses.length || !detail) return void band.replaceChildren();
|
|
missAt %= misses.length;
|
|
const go = createButton({
|
|
label: L("M02_SheetMissGo").replace("{i}", String(missAt + 1)).replace("{all}", String(misses.length)), // prettier-ignore
|
|
variant: "ghost",
|
|
size: "sm",
|
|
onClick: () => {
|
|
const m = misses[missAt];
|
|
missAt = (missAt + 1) % misses.length;
|
|
jumpTo(detail, m.행);
|
|
paintBand();
|
|
},
|
|
});
|
|
go.setAttribute("data-hopyo-miss-go", "");
|
|
const n = new Set(misses.map((m) => m.번)).size;
|
|
band.replaceChildren(
|
|
createBanner({ tone: "warning", text: L("M02_SheetMiss").replace("{n}", String(n)), actions: [go] }), // prettier-ignore
|
|
);
|
|
}
|
|
|
|
function repaint(): void {
|
|
paint = paintOf(ix, briefs, blocks, () => null, opt.judged); // 옛 확인 창 캐시 없음(785-7)
|
|
opt.sheet()?.repaint();
|
|
paintBar();
|
|
}
|
|
|
|
async function readBlocks(): Promise<void> {
|
|
blocks = await readHopyoMarks(opt.scope, opt.key, opt.target).catch(() => blocks);
|
|
paintBand();
|
|
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<HTMLButtonElement>("[data-hopyo-jump]")?.click();
|
|
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,
|
|
/** 다시 칠(판정 화면 고름 바뀜) */
|
|
repaint,
|
|
/** 시트 id → 역할(세 보기 밖 = undefined) */
|
|
role: (sheet: string): Role | undefined => roleOf.get(sheet),
|
|
/** 그 시트 · 행의 단산 블록 번 — 단가산출표 = 목록 행 · 상세 = 블록 범위 · 아니면 null */
|
|
blockAt(sheet: string, row: number): number | null {
|
|
if (sheet === ix.ids.단가산출표) return ix.listAt.get(row) ?? null;
|
|
if (sheet === ix.ids.단가산출상세) return numAt(ix.block, row);
|
|
return null;
|
|
},
|
|
/** 그 블록 자리 — 단가산출표 = 목록 행 · 상세 = 블록 범위 · 없으면 null */
|
|
spotOf(no: number, role: Role): Spot | null {
|
|
const sheet = ix.ids[role];
|
|
const at = role === "단가산출표" ? ix.listRow.get(no) : undefined;
|
|
const rows: [number, number] | undefined = at ? [at, at] : role === "단가산출상세" ? ix.block.get(no) : undefined; // prettier-ignore
|
|
return sheet && rows ? { 시트: sheet, 행: rows } : null;
|
|
},
|
|
destroy(): void {
|
|
document.removeEventListener(HOPYO_BUILT, onBuilt);
|
|
opt.host.removeEventListener("dblclick", onTwice);
|
|
},
|
|
};
|
|
}
|
|
|
|
export type SheetNav = ReturnType<typeof createSheetNav>;
|