Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts
T
eomsangdonandClaude Sonnet 5.5 2d9a469fc7 feat(806-1): 구조물 조합 일위대가 보기 = 일위대가 컨테이너 한 부품 · 단가산출 줄 = 그 SK 문서 모달(수식 탭 · 저장 = 그 SK)
- 조합 상세 · 카드 · 서고 모두 Labor_Table 한 부품 — 조합 전용 칸(면적 잇기 · 수량산출 모달 · 규격 열 · 다이어그램)은 새 Labor_Combo
- 단가산출(SK) 참조 줄 누름 = 새 Labor_SkEdit — 그 SK 문서를 단가산출 상세 부품(수식 탭 첫 화면)으로 열고 [저장] = 수식 저장 → SK 문서 저장 → 일위대가 금액 다시 셈 · 위 층 SK = 읽기만 · 두 번 눌러도 모달 하나
- 옛 조합 전용 표 · 단가산출 보기 모달(Labor_SangeunModal) 지움 · 전국 자재 줄은 Labor_Table 이 그림
- 새 시험 test_806_1_labor_view · 소스 지킴 시험 12 개를 새 자리(Labor_Table · Labor_RowModal · Labor_SkEdit)로 고침 — 옛 Detail 자리 문구가 없어져 낡았음

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 16:31:49 +09:00

546 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M02_MasterTemplete_Labor_Table.ts
* M02 일위대가(서고 · 사전조합) 상세 = 일위대가표만(PLAN 723-2 · 내역 16 · 17 · 요구서_내역16 2장 3~5).
* · 차례 = 정보(이름 · 구분 · 단위 · 본뜬) → 일위대가표(엑셀 꼴 격자 · `Labor_Grid.laborBlock`) · 오른쪽 원문 패널
* · 옵션 값 = 규격 칸 · 「빈 값 n」 = 비고 칸 붉게(입력 빈 로직 줄)
* · 수량 칸 = 일위대가 1 단위에 드는 양(742-1 · 요구 584-1 · 문서에 저장 · 새 줄 1 · 빈 칸 = 「수량 입력」 ·
* 금액 빠짐) → 그 줄 금액 · 합계 바로(짝 셈 `Labor_Calc.settle`) · 구조물 조합 · 공사 알약이 이어지면 그 값이 앞섬
* · 합계 열 = 「기준단위 단가」(줄 1 단위) · 「수량 반영 금액」(× 수량)
* · 머리 단추 = [항목 추가](736-1 · 자재는 찾기 창에서 고름 → 자재 목록) · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제
* · 줄 누름 = 줄 모달(`Labor_RowModal` — 옵션 · 메모 · 켜쌓기율 · 로직 자재 바꾸기 · 세 칸 · 단가산출) ·
* 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸)
* · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 다시 셈)
* · 안 이은 고정 줄(729-3) = 격자 위 띠 · [연결](`Labor_Unlinked` — 이음만 · 저장은 [저장])
* 구조물 조합 일위대가(카드 · 열 상세 · PLAN 806-1)도 이 한 부품 — `mode` 없음 = 조합 갈래: 정보 칸(이름 · 구분)
* 없음 · 수량 칸 = 수량산출 모달(`Labor_Combo` — 면적산출 잇기) · 규격 열 줄(`Labor_Spec`) · 다이어그램(열 상세) ·
* 새 줄 수량 = 면적을 따름(빈 값). 단가산출(SK) 참조 줄 누름 = 그 단가산출 문서 모달(`Labor_SkEdit` — 수식 탭 · 저장 = 그 SK).
* ========================================================================== */
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
import { createSidePanels } from "@ui/ui_template_side_panels";
import { createHopyoGrid, hopyoCellClass, hopyoGrid } from "@ui/ui_template_hopyo_grid";
import { openTreeMenu, type TreeMenuItem } from "@ui/ui_template_tree";
import { askText, showConfirmDialog } from "@ui/ui_template_modal";
import { createText } from "@ui/ui_template_text";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source";
import { getActiveLayer, getActiveOwner, type ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
import { lineId, settle, type LaborFixed, type LaborRef } from "./M02_MasterTemplete_Labor_Calc";
import {
calcLabor,
fetchLogicInputs,
normalizeLabor,
type LaborCalc,
type LaborDoc,
type LaborRow,
originText,
} from "./M02_MasterTemplete_Labor_Api";
import type { LaborDetailHandle, LaborDetailOptions } from "./M02_MasterTemplete_Labor_Detail";
import { laborBlock } from "./M02_MasterTemplete_Labor_Grid";
import { tipBorn } from "./M02_MasterTemplete_Labor_Born";
import { nationalRows } from "./M02_MasterTemplete_Labor_National";
import { createCombo, diagramBox } from "./M02_MasterTemplete_Labor_Combo";
import { isSkRef, openSkEdit } from "./M02_MasterTemplete_Labor_SkEdit";
import { specBar, specCells } from "./M02_MasterTemplete_Labor_Spec";
import { unlinkedBar } from "./M02_MasterTemplete_Labor_Unlinked";
import { pasteInto } from "./M02_MasterTemplete_Labor_Paste";
import { openLogicPicker, withSangeun } from "./M02_MasterTemplete_Labor_Pick";
import { openRefEdit } from "./M02_MasterTemplete_Labor_RefEdit";
import { openMatModal, openRowModal } from "./M02_MasterTemplete_Labor_RowModal";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { insideKeys } from "./M02_Labor_LogicMats_Model";
import { valueLine } from "./M02_Labor_Values_Model";
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
import { libMeta } from "./M02_MasterTemplete_Labor_LibMeta";
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css";
import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css";
import "@ui/ui_template_general_layout.css";
import "./M02_MasterTemplete_Labor.css";
const TEXT = {
QtyHint: [
"수량 = 이 일위대가 1 단위에 드는 양(저장) — 구조물 조합 · 공사에서 알약 · 계산 값이 이어지면 그 값이 앞섬",
"Quantity = amount per 1 unit of this item (saved) — linked pills · project values take precedence",
],
NeedQty: ["수량 입력", "Enter quantity"],
BasePrice: ["기준단위 단가", "Unit price"],
QtyAmount: ["수량 반영 금액", "Amount × qty"],
Alias: ["별칭", "Alias"],
AliasAsk: ["별칭(비우면 지움)", "Alias (empty = clear)"],
RemoveAsk: ["「{value}」 줄을 지울까?", "Delete line “{value}”?"],
Empty: ["빈 값 {n}", "{n} empty"],
NotNumber: ["수가 아님", "Not a number"],
} as const satisfies Record<string, readonly [string, string]>;
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
/** 비율 고정 줄(705-3) — 수량 = 율 */
const isRate = (row: LaborRow): boolean => !!row.고정?.비율;
/** 새 줄 수량(742-1) — 1 · 사용자가 고침 */
const ONE = { 손: 1 } as const;
export function mountLaborTable(
host: HTMLElement,
doc: unknown,
opt: LaborDetailOptions,
): LaborDetailHandle {
const cur = normalizeLabor(doc, opt.column);
const saved = JSON.stringify(cur);
const edit = !opt.readOnly;
const logics = new Map<string, ItemLogic>();
let calc: LaborCalc | null = null;
let last: LaborCalc | null = null;
let fail: string | null = null;
let seq = 0;
let timer = 0;
let alive = true;
let picked: string | null = null;
/** 조합 갈래(806-1) — 면적 잇기 · 수량산출 덧칸 · 컨테이너(`precombo`)는 없음 */
const combo = opt.mode === "precombo" ? null : createCombo({ ...opt, edit });
const dg = combo && !opt.card ? diagramBox() : null;
const fresh = (): LaborRow["수량"] => (combo ? null : { ...ONE }); // 새 줄 수량 — 조합 = 면적을 따름
const dirty = (): boolean => !!combo?.dirty() || JSON.stringify(cur) !== saved;
/** 줄 모달이 받는 셈 답(다시 셀 때마다) */
const watchers = new Set<(c: LaborCalc | null) => void>();
const watch = (fn: (c: LaborCalc | null) => void): (() => void) => {
watchers.add(fn);
fn(calc ?? last);
return () => watchers.delete(fn);
};
const gridHost = el("div", { attrs: { "data-labor-grid": "", "data-labor-table": "" } });
const source = createSourceView({
path: "/m02/logic-lib/source",
pdf: "/m02/logic-lib/source-pdf",
});
const sourceHost = el("div", { attrs: { "data-labor-source": "" } });
const side = opt.card
? null // 카드 갈래엔 원문 패널 없음
: createSidePanels({
prefKey: "m02-labor-table-side", // 일위대가표는 넓게 — 원문은 처음 접힘(누르면 엶)
defaultOpen: null,
follow: false,
panels: [{ id: "source", title: tc("Labor_Source"), body: sourceHost }],
onChange: () => showSource(),
});
function showSource(): void {
if (side?.current() !== "source") return;
if (!picked) return void sourceHost.replaceChildren(createText(tc("Labor_SourceHint")));
source.show(logics.get(picked)?.출처 ?? "");
if (sourceHost.firstChild !== source.root) sourceHost.replaceChildren(source.root);
}
/** 일위대가표 — 줄 누름 · 우클릭 · 수량 칸(저장) · 빈 값 · 빈 수량 붉게 */
const draw = (): void => {
const block = laborBlock(cur, calc ?? last, logics);
const empties = new Set<number>();
block.lines.forEach((ln, n) => {
const row = cur.담은로직[n];
if (ln.note === tc("Grid_NoQty")) ln.note = t("NeedQty"); // 742-1 빈 수량 = 사용자가 넣음
const empty = row?.로직 ? valueLine(logics.get(row.로직)?.입력 ?? [], row.입력값).empty : 0;
if (empty) ln.note = t("Empty").replace("{n}", String(empty));
if (empty || ln.note === t("NeedQty")) empties.add(n);
});
const have = calc ?? last;
// 빈 줄 = 금액 모름(단가 까닭 · 빈 수량) · 빈 값
block.blank = have
? block.lines.filter((l, n) => l.amounts.sum == null || empties.has(n)).length
: 0;
if (fail && block.lines[0]) block.lines[0].note = fail;
const g = hopyoGrid([block]);
const at = (i: number): number | null => g.at.get(i)?.[1] ?? null;
const spec = combo && edit ? specCells(block, cur, (qtyOnly) => changed(qtyOnly)) : {};
let qtyHit = false;
const label: Record<string, string> = { sum_price: t("BasePrice"), sum: t("QtyAmount") };
const grid = createHopyoGrid(
[block],
{
columns: g.columns.map((c) => (label[c.key] ? { ...c, label: label[c.key] } : c)),
// 조합 갈래 — 수량 칸 누름 = 수량산출 모달(줄 모달은 안 엶)
onCellClick: combo
? (i, key) => {
const n = at(i);
if (key !== "qty" || !edit || n === null) return;
qtyHit = true;
openQty(n);
}
: undefined,
onRowClick: (i) => {
if (qtyHit) return void (qtyHit = false);
const n = at(i);
if (n !== null) openAt(n);
},
// 줄 끝 ⋯(731-1) — 우클릭 · 길게 누름이 없어도 같은 메뉴(위로 · 아래로 · 별칭 · 삭제)
rowMenu: edit
? {
show: (i) => at(i) !== null,
open: (i, ev) => openTreeMenu(ev, menuAt(at(i) ?? 0)),
}
: undefined,
editable: (i, key) => {
if (combo) return !!spec.editable?.(i, key); // 조합 = 규격 열 수량 칸만 손으로
const n = at(i);
if (!edit || key !== "qty" || n === null) return false;
const row = cur.담은로직[n];
return !row || !isRate(row);
},
onEdit: (i, key, text) => {
if (combo) return spec.onEdit?.(i, key, text);
const n = at(i);
if (n !== null) setQty(n, text);
},
cellClass: (i, key) => {
const n = at(i);
if (key === "note" && n !== null && empties.has(n)) return "ui-hopyo-grid__blank";
return hopyoCellClass(g.rows, i, key);
},
},
edit ? (text) => void pasteInto(cur, text, afterPaste) : undefined,
);
grid.root.addEventListener("contextmenu", (ev) => {
const tr = (ev.target as Element).closest<HTMLElement>("tr[data-i]");
const n = tr ? at(Number(tr.dataset.i)) : null;
if (n === null || !edit) return;
ev.preventDefault();
// 칸 초점이 표를 밀면(scroll) 메뉴가 바로 닫힘 — 밀림이 끝난 다음 그림에 엶
requestAnimationFrame(() => requestAnimationFrame(() => openTreeMenu(ev, menuAt(n))));
});
tipBorn(grid.root, at, (n) => (cur.담은로직[n] ?? cur.자재[n - cur.담은로직.length])?.본뜬); // 740-2
const bar = unlinkedBar(cur, edit && ((qtyOnly) => changed(qtyOnly))); // 729-3 안 이은 줄 띠
const specs = combo && edit ? [specBar(cur, calc ?? last, (qtyOnly) => changed(qtyOnly))] : [];
gridHost.replaceChildren(...(bar ? [bar] : []), ...specs, grid.root);
dg?.draw(cur, logics, calc ?? last, openAt);
showSource();
for (const fn of watchers) fn(calc ?? last);
};
/** 서버 다시 셈 — 늦게 온 답은 버림 */
const recalc = (): void => {
window.clearTimeout(timer);
timer = window.setTimeout(async () => {
const mine = ++seq;
calc = null;
fail = null;
draw();
try {
// 저장 안 한 칸 연결이 있으면 그 칸 값으로(없으면 서버가 저장된 면적산출을 읽음)
const qty = combo?.dirty() ? combo.given() : undefined;
const got = opt.calc
? await opt.calc(cur, qty)
: await calcLabor(opt.layer, opt.column, cur, qty, opt.owner);
if (!alive || mine !== seq) return;
calc = last = got;
combo?.setPills(got.알약 as { 키: string; 이름?: string; 단위?: string }[]);
} catch (error) {
if (!alive || mine !== seq) return;
fail = error instanceof Error ? error.message : tc("Labor_Calc_Fail");
}
draw();
}, 150);
};
/** `qtyOnly` = 수량만 — 짝 셈으로 바로 */
const changed = (qtyOnly = false): void => {
opt.onChanged(dirty());
if (qtyOnly && calc) {
calc = settle(calc, cur, combo?.given() ?? {});
draw();
} else recalc();
};
/** 수량 칸(742-1 · 저장) — 빈 글 = 빈 수량(「수량 입력」) · 수 아님 · 음수 = 알림 */
function setQty(n: number, text: string): void {
const row = cur.담은로직[n] ?? cur.자재[n - cur.담은로직.length];
const raw = text.replace(/,/g, "").trim();
const v = Number(raw);
if (!row) return;
if (raw && !(Number.isFinite(v) && v >= 0)) return showToast(t("NotNumber"), "error");
row.수량 = raw ? { 손: v } : null;
changed(true);
}
/** 붙여넣기(705-2) 뒤 — 엑셀 수량 = 저장 수량 */
function afterPaste(): void {
void loadLogics().then(() => changed());
}
const loadLogics = async (): Promise<void> => {
const want = cur.담은로직.map((r) => r.로직).filter((k) => k && !logics.has(k));
if (!want.length) return;
try {
const got = await fetchLogicInputs(want);
for (const l of got.로직) logics.set(l.로직, l);
} catch (error) {
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
}
};
const lineOf = (row: LaborRow) => (calc ?? last)?.줄.find((l) => lineId(l) === lineId(row));
const nameOf = (row: LaborRow): string =>
row.별칭 ||
row.고정?.이름 ||
(row.참조 ? lineOf(row)?.이름 : logics.get(row.로직)?.이름) ||
row.참조?.키 ||
row.로직;
/** 모달 머리 글(이름 · 「1 ㎡ 당」) */
const head = (): string =>
[String((cur as { 이름?: string }).이름 || opt.column), cur.단위 ? `1 ${cur.단위} 당` : ""]
.filter(Boolean)
.join(" · ");
/** 수량산출 모달(조합) — 로직 줄 · 자재 줄 같음 */
function openQty(n: number): void {
const have = calc ?? last;
const mat = cur.자재[n - cur.담은로직.length];
const row = cur.담은로직[n];
if (row)
combo?.openQty(nameOf(row), row, lineOf(row), lineOf(row)?.분모 ?? "", () => changed(true));
else if (mat)
combo?.openQty(
`${mat.코드} ${mat.이름 ?? ""}`.trim(),
mat,
have?.자재.find((m) => m.코드 === mat.코드),
mat.단위 ?? "",
() => changed(true),
);
}
/** 줄 누름 — 줄 갈래마다 */
function openAt(n: number): void {
const row = cur.담은로직[n];
if (!row) return openMat(n - cur.담은로직.length);
picked = row.로직 || null;
showSource();
if (row.참조 && row.참조.키.startsWith("SC-"))
return void openRefEdit({
ref: row.참조,
title: nameOf(row),
readOnly: !edit || row.참조.층 !== opt.layer,
closed: () => alive && recalc(),
});
if (row.고정) return; // 고정 줄 = 숫자 그대로(이름 · 별칭은 우클릭)
// 단가산출(SK) 줄 = 그 단가산출 문서 모달(수식 탭 · 806-1) — 셈 주입(프로젝트 복사본)엔 옛 줄 모달
if (isSkRef(row.참조) && !opt.calc)
return void openSkEdit({
ref: row.참조,
title: nameOf(row),
head: head(),
readOnly: !edit || row.참조.층 !== opt.layer,
closed: (done) => alive && done && recalc(), // 저장했으면 금액을 다시 셈
});
openRowModal({
row,
logic: logics.get(row.로직) ?? null,
title: nameOf(row),
head: head(),
unit: lineOf(row)?.분모 ?? "",
watch,
scope: { layer: opt.layer, owner: library.owner },
readOnly: !edit,
inject: !!opt.calc,
userMats: cur.자재.map((m) => m.코드),
done: (again) => changed(!again),
});
}
function openMat(i: number): void {
const mat = cur.자재[i];
if (!mat) return;
openMatModal({
mat,
watch,
readOnly: !edit,
done: (money) => changed(!money),
});
}
/** 우클릭 — 위로 · 아래로 · 별칭 · 삭제 */
function menuAt(n: number): TreeMenuItem[] {
const rows = cur.담은로직.length;
const isMat = n >= rows;
const name = isMat ? (cur.자재[n - rows]?.이름 ?? "") : nameOf(cur.담은로직[n]);
const items: TreeMenuItem[] = [];
if (!isMat) {
if (n > 0) items.push({ label: tc("Up"), run: () => move(n, n - 1) });
if (n < rows - 1) items.push({ label: tc("Down"), run: () => move(n, n + 1) });
items.push({ label: t("Alias"), run: () => void alias(n) });
}
items.push({
label: tc("Remove"),
run: () =>
void showConfirmDialog(t("RemoveAsk").replace("{value}", name), tc("Remove")).then(
(ok) => ok && remove(n),
),
});
return items;
}
function move(from: number, to: number): void {
cur.담은로직.splice(to, 0, ...cur.담은로직.splice(from, 1));
changed();
}
async function alias(n: number): Promise<void> {
const row = cur.담은로직[n];
const got = await askText({ message: t("AliasAsk"), value: row.별칭 ?? "" });
if (got === null || got === undefined) return;
row.별칭 = got.trim() || null;
opt.onChanged(dirty());
draw();
}
function remove(n: number): void {
const rows = cur.담은로직.length;
if (n < rows) cur.담은로직.splice(n, 1);
else cur.자재.splice(n - rows, 1);
changed();
}
/** 서고 고르기 — 참조 줄 · 고정 줄(721-2 새 고르기 창 · 인력 · 재료) */
const library = {
layer: opt.layer,
owner:
opt.owner !== undefined
? opt.owner
: opt.layer === getActiveLayer()
? getActiveOwner()
: null,
pick: (ref: LaborRef, name: string, 별칭: string | null = null) => {
const id = lineId({ 참조: ref });
if (cur.담은로직.some((r) => lineId(r) === id))
return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error");
cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: fresh() });
changed();
},
fixed: (fx: LaborFixed, name: string) => {
const id = lineId({ 고정: fx });
if (cur.담은로직.some((r) => lineId(r) === id))
return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error");
cur.담은로직.push({
로직: "",
고정: fx,
별칭: null,
메모: null,
입력값: {},
수량: fresh(),
});
changed();
},
/** 736-1 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */
material: (m: {
코드: string;
이름: string | null;
규격: string | null;
단위: string | null;
}) => {
const why = insideKeys((calc ?? last)?.줄).has(m.코드)
? "Labor_InLogicMat"
: cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat";
if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error");
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: fresh() });
changed();
},
};
const addLogic = createButton({
label: tc("AddLogic"),
variant: "filled",
onClick: () =>
openLogicPicker((x) => {
if (cur.담은로직.some((r) => r.로직 === x.키))
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: fresh() });
void loadLogics().then(() => changed());
}, withSangeun(library)),
});
// 정보 칸 — 이름 · 구분 · 상세구분 · 단위 · 본뜬
const nameField = createInputField({ type: "text", label: tc("Lib_Name") });
nameField.root.dataset.laborField = "이름";
nameField.input.value = String((cur as { 이름?: unknown }).이름 ?? "");
nameField.input.readOnly = !edit;
nameField.input.addEventListener("input", () => {
(cur as { 이름?: string }).이름 = nameField.input.value.trim();
opt.onChanged(dirty());
});
const unit = el("div", { className: "ui-field", attrs: { "data-labor-field": "단위" } });
const unit0 = cur.단위;
const drawUnit = (): void =>
unit.replaceChildren(
el("label", { className: "ui-field__label", text: tc("Labor_Unit") }),
createUnitPick({
value: cur.단위 || NO_UNIT,
extra: [unit0, cur.단위].filter((u): u is string => !!u),
readOnly: !edit,
ariaLabel: tc("Labor_Unit"),
onPick: (u) => {
cur.단위 = u === NO_UNIT ? null : u;
opt.onChanged(dirty());
drawUnit();
draw();
},
}),
);
drawUnit();
const origin = createInputField({ type: "text", label: tc("Labor_Origin") });
origin.input.value = originText(cur.본뜬) ?? tc("Origin_Self");
origin.input.readOnly = true;
const info = combo
? opt.card
? [] // 카드 갈래 = 정보 칸 없음
: [el("div", { className: "m01lab__info", children: [unit, origin.root] })]
: [
el("div", {
className: "m01lab__info",
children: [
nameField.root,
...libMeta(cur, edit, () => opt.onChanged(dirty())),
unit,
origin.root,
],
}),
];
const editor = el("div", {
className: "m01-logic__editor m01-combo m02-labor__edit",
attrs: { "data-labor": opt.column },
children: [
...(info.length
? [el("section", { className: "m01lab__box m01-logic__section", children: info })]
: []),
el("section", {
className: "m01lab__box m01-logic__section",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("h3", { text: tc("Grid_Title") }),
...(edit && !(combo && opt.card)
? [el("div", { className: "ui-btn-group", children: [addLogic] })]
: []),
],
}),
...(combo ? [] : [createText(t("QtyHint"), "hint")]),
gridHost,
],
}),
...nationalRows(cur.본뜬), // 361-2 전국 자재(보기만 · 없으면 아무것도 안 그림)
...(dg ? [dg.root] : []),
],
});
host.replaceChildren(
side ? el("div", { className: "ui-side-panels-row", children: [editor, side.root] }) : editor,
);
draw();
// 로직 입력 목록 · 면적산출 이은 칸(조합 — 없으면 빈 것)을 받은 뒤 첫 계산
void Promise.all([loadLogics(), combo?.load()]).then(() => alive && recalc());
return {
getDoc: () => {
if (combo) return cur;
const out: Record<string, unknown> = { ...cur, 종류: "사전조합" };
delete out.열;
return out as unknown as LaborDoc;
},
saveArea: async () => (await combo?.save()) ?? false,
destroy: () => {
alive = false;
window.clearTimeout(timer);
host.replaceChildren();
},
};
}