- Labor_SkPick — 주인 null(자기 층) = 로그인 사용자 층 주인으로 견줌 · 같은 층 SK 고름 = 그대로 참조(화면 확인 중 사본 생기던 흠) - Labor_Combo — 다이어그램에 SK 참조 줄 카드(이름 · 값 · 누름 = 그 줄 모달) · 옮긴 뒤 다이어그램이 비지 않게 - 옮기기 보기 목록 _목록_811-1_일위대가참조.xlsx — 층 문서 26 · 로직 줄 172 · 같음 172 · 새 SK 130 · 다름 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
230 lines
8.5 KiB
TypeScript
230 lines
8.5 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Labor_Combo.ts
|
|
* 구조물 조합 일위대가의 덧칸(PLAN 806-1) — 일위대가 컨테이너 부품(`Labor_Table`)에 조합만 얹는 것:
|
|
* · 면적산출 잇기 = 그 열 면적산출의 알약 → 칸 · 값(`links`) · 수량산출 모달(43-1 · 손 · 칸 따름 · 잇기 · 풀기) ·
|
|
* [저장] 앞 면적 문서 저장(`save`) — 잇기 · 풀기는 화면 문서만
|
|
* · 다이어그램 상자(열 상세만) — 담은 로직 · 자재 → 흐름도
|
|
* 표 · 줄 · 합계 · 단추는 컨테이너와 한 벌 — 여기엔 조합 전용 칸만 둠.
|
|
* ========================================================================== */
|
|
|
|
import { el, showToast } from "@ui/ui_template_elements";
|
|
import { ROUTES } from "@config/config_frontend";
|
|
import { navigateTo } from "../A00_Common/router";
|
|
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
|
import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side";
|
|
import {
|
|
readTemplate,
|
|
saveTemplate,
|
|
type ItemLogic,
|
|
type Layer,
|
|
} from "./M02_MasterTemplete_Api_Fetch";
|
|
import { linkAreaCell } from "./M02_MasterTemplete_AreaCellPick";
|
|
import {
|
|
areaLinks,
|
|
type LaborCalc,
|
|
type LaborDoc,
|
|
type LaborQty,
|
|
} from "./M02_MasterTemplete_Labor_Api";
|
|
import {
|
|
buildLaborDiagram,
|
|
diagramTitle,
|
|
renderComboDiagram,
|
|
type DiagramLogic,
|
|
type DiagramOut,
|
|
} from "./M02_MasterTemplete_Labor_Diagram";
|
|
import { lineId } from "./M02_MasterTemplete_Labor_Calc";
|
|
import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal";
|
|
import { qtyShort } from "./M02_MasterTemplete_Labor_Rows";
|
|
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
|
|
|
type Recalc = (typeof import("../A00_Common/spreadsheet/spreadsheet_recalc"))["recalcWorkbook"];
|
|
|
|
export interface ComboOptions {
|
|
layer: Layer;
|
|
column: string;
|
|
edit: boolean;
|
|
/** 셈 · 면적 자리 주입(142-1) — 없으면 층 길 */
|
|
readArea?: () => Promise<{ 문서: unknown; 판?: string | null }>;
|
|
}
|
|
|
|
export interface Combo {
|
|
/** 칸 이음 · 값(서버 calc 와 같은 것) */
|
|
cells: () => Map<string, string>;
|
|
given: () => Record<string, number>;
|
|
/** 저장 안 한 칸 연결이 있음 — 서버 셈이 그 칸 값을 써야 함 */
|
|
dirty: () => boolean;
|
|
setPills: (pills: { 키: string; 이름?: string; 단위?: string }[]) => void;
|
|
/** 면적산출 + 엔진을 받음(없으면 빈 것 — 새 구조물) */
|
|
load: () => Promise<void>;
|
|
/** 수량산출 모달 — 줄 · 자재 같음 · 바뀌면 `changed` */
|
|
openQty: (
|
|
title: string,
|
|
target: { 수량: LaborQty },
|
|
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
|
|
unit: string,
|
|
changed: () => void,
|
|
) => void;
|
|
/** 면적 문서 저장 · 저장했으면 참 */
|
|
save: () => Promise<boolean>;
|
|
}
|
|
|
|
export function createCombo(opt: ComboOptions): Combo {
|
|
let links = new Map<string, { 칸: string; 값: number | null }>();
|
|
// 엔진(recalc)은 면적 문서와 함께 늦게 받음 — 정적 import 금지(계약 `spreadsheet_types.ts` 머리)
|
|
let area: { doc: Workbook; 판: string; recalc: Recalc } | null = null;
|
|
let areaDirty = false;
|
|
let pills = new Map<string, { 이름?: string; 단위?: string }>();
|
|
const cells = (): Map<string, string> => new Map([...links].map(([k, v]) => [k, v.칸]));
|
|
|
|
/** 칸 잇기 · 풀기(`cell` null) — 화면 문서만(저장은 [저장]) · 됐으면 참 */
|
|
const linkCell = (pill: string, cell: string | null, replace = false): boolean => {
|
|
if (!area) {
|
|
showToast(tc("Labor_NoArea"), "error");
|
|
return false;
|
|
}
|
|
const info = pills.get(pill);
|
|
const next = linkAreaCell(
|
|
area.doc,
|
|
pill,
|
|
cell,
|
|
{
|
|
...(info?.이름 ? { 이름: info.이름 } : {}),
|
|
...(info?.단위 ? { 단위: info.단위 } : {}),
|
|
},
|
|
replace, // 그 칸 다른 알약 — 기본 함께 · [바꾸기]면 뺌(물음은 시트 안)
|
|
);
|
|
if (!next) {
|
|
showToast(tc("Labor_BadCell"), "error");
|
|
return false;
|
|
}
|
|
next.계산값 = area.recalc(next); // 식 칸 값도 바로 — 저장 때 서버가 다시 셈
|
|
area.doc = next;
|
|
links = areaLinks(next);
|
|
areaDirty = true;
|
|
return true;
|
|
};
|
|
|
|
return {
|
|
cells,
|
|
given: () =>
|
|
Object.fromEntries(
|
|
[...links].flatMap(([k, v]) => (v.값 === null ? [] : [[k, v.값] as [string, number]])),
|
|
),
|
|
dirty: () => areaDirty,
|
|
setPills: (list) => {
|
|
pills = new Map(list.map((p) => [p.키, p]));
|
|
},
|
|
load: () =>
|
|
Promise.all([
|
|
opt.readArea ? opt.readArea() : readTemplate(opt.layer, "area", opt.column, null),
|
|
import("../A00_Common/spreadsheet/spreadsheet_recalc"),
|
|
]).then(
|
|
([got, { recalcWorkbook }]) => {
|
|
area = { doc: got.문서 as Workbook, 판: got.판 ?? "", recalc: recalcWorkbook };
|
|
links = areaLinks(got.문서);
|
|
},
|
|
() => undefined,
|
|
),
|
|
openQty: (title, target, line, unit, changed) => {
|
|
const pill = line?.알약 ?? null;
|
|
openQtyModal({
|
|
title,
|
|
now: qtyShort(line, unit, cells()),
|
|
linked: !!pill && links.has(pill),
|
|
hand: target.수량?.손 ?? null,
|
|
unit,
|
|
readOnly: !opt.edit,
|
|
sheet: { doc: area?.doc ?? null, layer: opt.layer, column: opt.column, pill },
|
|
done: (r) => {
|
|
if (r.kind === "keep") return;
|
|
if (r.kind === "hand") target.수량 = { 손: r.손 };
|
|
else if (r.kind === "follow") target.수량 = null;
|
|
else {
|
|
const cell = r.kind === "cell" ? r.pick.칸 : null;
|
|
if (!pill || !linkCell(pill, cell, r.kind === "cell" && r.pick.바꾸기 === true)) return;
|
|
target.수량 = null; // 칸을 따름
|
|
}
|
|
changed();
|
|
},
|
|
});
|
|
},
|
|
save: async () => {
|
|
if (!area || !areaDirty) return false;
|
|
const info = await saveTemplate(opt.layer, "area", opt.column, null, area.판, area.doc);
|
|
area.판 = info.판;
|
|
areaDirty = false;
|
|
return true;
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 참조 줄 카드(811-1 · 단가산출 = SK 참조 한 행) — 이름 · 값 = 셈 답 · 넣을 값 없음 · 누름 = 그 줄 모달 */
|
|
function refCards(doc: LaborDoc, calc: LaborCalc | null): DiagramLogic[] {
|
|
return doc.담은로직.flatMap((r) => {
|
|
if (!r.참조) return [];
|
|
const line = calc?.줄.find((l) => lineId(l) === lineId(r));
|
|
const money = line?.반영 ?? line?.기준;
|
|
const out: DiagramOut = line?.까닭 ? { 멈춤: line.까닭 } : money ? { ...money } : null;
|
|
return [
|
|
{
|
|
key: r.참조.키, // 같은 참조 두 번 금지 = 줄 하나
|
|
name: r.별칭 || line?.이름 || r.참조.키,
|
|
unit: line?.분모 ?? "",
|
|
inputs: [],
|
|
out,
|
|
...(money ? { basis: line?.반영 ? "반영" : "기준" } : {}),
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
/** 다이어그램 상자(열 상세) — `draw` 를 표가 다시 그릴 때마다 부름 */
|
|
export function diagramBox(): {
|
|
root: HTMLElement;
|
|
draw: (
|
|
doc: LaborDoc,
|
|
logics: ReadonlyMap<string, ItemLogic>,
|
|
calc: LaborCalc | null,
|
|
focus: (index: number) => void,
|
|
) => void;
|
|
} {
|
|
const host = el("div");
|
|
const root = el("details", {
|
|
className: "m01lab__box m01-logic__section m02-labor__dg",
|
|
attrs: { open: "" },
|
|
children: [el("summary", { text: diagramTitle() }), host],
|
|
});
|
|
let again: () => void = () => undefined;
|
|
root.addEventListener("toggle", () => again());
|
|
return {
|
|
root,
|
|
draw: (doc, logics, calc, focus) => {
|
|
again = () => {
|
|
if (!root.open) return;
|
|
const data = buildLaborDiagram({
|
|
rows: doc.담은로직.filter((r) => !r.참조),
|
|
logics: [...logics.values()],
|
|
calc,
|
|
mats: doc.자재.length,
|
|
});
|
|
const at = data.logics.findIndex((l) => l.material);
|
|
data.logics.splice(at < 0 ? data.logics.length : at, 0, ...refCards(doc, calc));
|
|
renderComboDiagram(host, data, {
|
|
focusInput: (key) => {
|
|
const i = doc.담은로직.findIndex((r) => r.로직 === key);
|
|
if (i >= 0) focus(i);
|
|
},
|
|
// 로직 카드 → 단가산출 로직 화면(떠날 때 물음은 UI_Main 의 leave guard) · 참조 카드(811-1) = 그 줄 모달
|
|
openLogic: (key) => {
|
|
const ref = doc.담은로직.findIndex((r) => r.참조?.키 === key);
|
|
if (ref >= 0) return focus(ref);
|
|
setPendingLogicKey(key);
|
|
navigateTo(ROUTES.M01_MASTER_DATA);
|
|
},
|
|
});
|
|
};
|
|
again();
|
|
},
|
|
};
|
|
}
|