Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Combo.ts
T
eomsangdonandClaude Opus 5.5 75bbeee0a6 feat(811-1): 같은 층 SK 사본 안 만듦 · 조합 다이어그램 참조 카드 · 옮기기 목록
- 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>
2026-11-08 21:09:26 +09:00

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();
},
};
}