Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_AreaPanel.ts
T

149 lines
6.1 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_AreaPanel.ts
* 면적산출 상세 오른쪽 패널(PLAN 39-4 D2 · D6) — 그 열 일위대가의 담은 로직 + 자재목록(기준 · 반영).
* 몸 = 일위대가 보기 갈래(`Labor_View` · 고치기 없음) · 틀 = 공용 사이드바 카드 · 접기 손잡이 · 폭 손잡이
* (폭 · 접힘 기억은 옛 조합 패널 키 그대로).
* · 줄 누름 = 칸 지정 모달(`AreaLink` · 그 줄 알약 키를 면적 칸에 잇기 · 풀기)
* · 시트 칸 값 · 연결이 바뀌면 `refresh` — 받은 기준에 바로 곱함 · 칸 주소도 넘김(서버 안 부름 · `Labor_View.setQty`)
* ========================================================================== */
import { el, showToast } from "@ui/ui_template_elements";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
import { createPanelResizer } from "@ui/ui_template_resizer";
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
import type { LinkItem, SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
import { openAreaLink } from "./M02_MasterTemplete_AreaLink";
import { createLaborView, type LaborViewRow } from "./M02_MasterTemplete_Labor_View";
import "./M02_MasterTemplete_AreaPanel.css";
const TEXT = {
handle: "일위대가",
title: "이 열의 일위대가",
noPill: "이 줄은 면적산출 알약이 없음 — 돈 아닌 로직이거나 계산 전",
};
/** 최소 폭(px) · 최대 = 편집 칸 폭의 비율 */
const MIN_WIDTH = 280;
const MAX_RATIO = 0.6;
export interface AreaPanelOptions {
layer: Layer;
/** 집계표 열 id */
column: string;
sheet: () => SpreadsheetHandle | null;
/** 시트에서 지금 고른 칸(A1) */
picked: () => string;
/** 알약 막대 항목 — 칸 지정 때 연결에 같이 적을 이름 · 단위 · 자재 */
pills: () => LinkItem[];
readOnly: boolean;
}
export interface AreaPanel {
root: HTMLElement;
/** 시트 칸 값 · 연결이 바뀜 — 반영을 바로 다시 곱함 */
refresh: () => void;
destroy: () => void;
}
/** 칸 값 → 수(쉼표 뗌) — 빈 글 · 글 · 참거짓은 null(서버 `M02_Labor_Doc._number` 와 같음) */
function toNumber(raw: unknown): number | null {
if (raw === null || raw === undefined || typeof raw === "boolean") return null;
const text = String(raw).replace(/,/g, "").trim();
const n = Number(text);
return text !== "" && Number.isFinite(n) ? n : null;
}
/** 시트 알약 값 → `{키: 수}` — 서버 `M02_Labor_Doc.area_values` 와 같은 규칙(짝): 식 칸 = 계산 값의 수 ·
* 그 밖 = 칸 값 · 빈 칸 · 글은 빠짐(「수량 없음」 · 반영 「—」) */
function quantities(sheet: SpreadsheetHandle | null): Record<string, number> {
const out: Record<string, number> = {};
if (!sheet) return out;
const values = sheet.getLinkedValues();
for (const s of sheet.getDoc().시트)
for (const l of s.연결 ?? []) {
const cell = s.칸[l.칸];
const v = values[l.키];
const n = toNumber(cell?.식 !== undefined ? (v && "수" in v ? v.수 : null) : cell?.값);
if (n !== null) out[l.키] = n;
}
return out;
}
/** 시트 연결 → `{알약 키: 칸 주소}` — 저장 전 잇기 · 풀기도(줄 아래 「수량 B6 = …」) */
function cellsOf(sheet: SpreadsheetHandle | null): Record<string, string> {
const out: Record<string, string> = {};
for (const s of sheet?.getDoc().시트 ?? []) for (const l of s.연결 ?? []) out[l.키] = l.칸;
return out;
}
export function buildAreaPanel(opts: AreaPanelOptions): AreaPanel {
const openLink = (row: LaborViewRow): void => {
const sheet = opts.sheet();
if (!sheet) return;
if (!row.pill) return showToast(TEXT.noPill, "info");
const item = opts.pills().find((p) => p.키 === row.pill);
openAreaLink({
label: row.name,
key: row.pill,
info: {
이름: item?.이름 ?? row.name,
...((item?.단위 ?? row.unit) ? { 단위: item?.단위 ?? row.unit } : {}),
...(item?.자재 ? { 자재: item.자재 } : {}),
},
sheet,
picked: opts.picked,
readOnly: opts.readOnly,
});
};
const view = createLaborView(openLink);
const body = el("div", {
className: "m02-apanel__body ui-sidebar-panel",
children: [
el("section", {
className: "ui-collapsible ui-sidebar-section ui-sidebar-card",
children: [el("h3", { className: "ui-collapsible__title", text: TEXT.title }), view.root],
}),
],
});
attachCollapsible(body);
const root = el("aside", { className: "m02-apanel" });
const handle = createWorkflowPanelHandle("side", "right", TEXT.handle);
// 왼쪽 경계를 끌어 폭 조절 — 폭은 브라우저에 기억 · 더블클릭 = 기본 폭
const resizer = createPanelResizer({
axis: "horizontal",
target: root,
cssVar: "--m02-apanel-width",
direction: -1,
min: MIN_WIDTH,
max: () => (root.parentElement?.clientWidth ?? window.innerWidth) * MAX_RATIO,
storageKey: stateKey("m02-combo-panel-width") ?? undefined,
});
root.append(handle.root, resizer.root, body);
const setCollapsed = (collapsed: boolean): void => {
root.classList.toggle("is-collapsed", collapsed);
handle.setOpen(!collapsed);
writeStateRaw("m02-combo-panel-collapsed", String(collapsed));
};
handle.root.addEventListener("click", () =>
setCollapsed(!root.classList.contains("is-collapsed")),
);
setCollapsed(readStateRaw("m02-combo-panel-collapsed") === "true");
// 처음 = 저장 안 한 시트 칸 값으로 셈(서버 calc 한 번)
const refresh = (): void => view.setQty(quantities(opts.sheet()), cellsOf(opts.sheet()));
void view.show(opts.layer, opts.column, quantities(opts.sheet()));
refresh(); // 칸 주소는 지금 시트 것 — 보기가 저장본 대신 씀
return {
root,
refresh,
destroy: () => {
view.destroy();
resizer.dispose();
root.remove();
},
};
}