- 충돌 시험 셋(717-4 · 719-2 · hopyo build) = 740-1 펼침 단언 + 742-1 저장 수량 단언 합침 - 740-2 일위대가표 풍선 줄의 body(742-1 이 걷은 휘발 셈 몸) → cur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
482 lines
20 KiB
TypeScript
482 lines
20 KiB
TypeScript
/* =============================================================================
|
||
* 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` — 이음만 · 저장은 [저장])
|
||
* 구조물 조합 일위대가(카드 · 열 상세)는 그대로 `Labor_Detail`(면적 잇기 · 수량산출).
|
||
* ========================================================================== */
|
||
|
||
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 { unlinkedBar } from "./M02_MasterTemplete_Labor_Unlinked";
|
||
import { pasteInto } from "./M02_MasterTemplete_Labor_Paste";
|
||
import { openLogicPicker, withHopyo } 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;
|
||
const dirty = (): boolean => 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 = 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 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)),
|
||
onRowClick: (i) => {
|
||
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) => {
|
||
const n = at(i);
|
||
if (!edit || key !== "qty" || n === null) return false;
|
||
const row = cur.담은로직[n];
|
||
return !row || !isRate(row);
|
||
},
|
||
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 안 이은 줄 띠
|
||
gridHost.replaceChildren(...(bar ? [bar] : []), grid.root);
|
||
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 got = opt.calc
|
||
? await opt.calc(cur)
|
||
: await calcLabor(opt.layer, opt.column, cur, undefined, opt.owner);
|
||
if (!alive || mine !== seq) return;
|
||
calc = last = got;
|
||
} 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, {});
|
||
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.로직;
|
||
|
||
/** 줄 누름 — 줄 갈래마다 */
|
||
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; // 고정 줄 = 숫자 그대로(이름 · 별칭은 우클릭)
|
||
openRowModal({
|
||
row,
|
||
logic: logics.get(row.로직) ?? null,
|
||
title: nameOf(row),
|
||
head: [String((cur as { 이름?: string }).이름 ?? ""), cur.단위 ? `1 ${cur.단위} 당` : ""]
|
||
.filter(Boolean)
|
||
.join(" · "),
|
||
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, 입력값: {}, 수량: { ...ONE } });
|
||
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,
|
||
입력값: {},
|
||
수량: { ...ONE },
|
||
});
|
||
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.단위, 수량: { ...ONE } });
|
||
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, 입력값: {}, 수량: { ...ONE } });
|
||
void loadLogics().then(() => changed());
|
||
}, withHopyo(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;
|
||
|
||
host.replaceChildren(
|
||
el("div", {
|
||
className: "ui-side-panels-row",
|
||
children: [
|
||
el("div", {
|
||
className: "m01-logic__editor m01-combo m02-labor__edit",
|
||
attrs: { "data-labor": opt.column },
|
||
children: [
|
||
el("section", {
|
||
className: "m01lab__box m01-logic__section",
|
||
children: [
|
||
el("div", {
|
||
className: "m01lab__info",
|
||
children: [
|
||
nameField.root,
|
||
...libMeta(cur, edit, () => opt.onChanged(dirty())),
|
||
unit,
|
||
origin.root,
|
||
],
|
||
}),
|
||
],
|
||
}),
|
||
el("section", {
|
||
className: "m01lab__box m01-logic__section",
|
||
children: [
|
||
el("div", {
|
||
className: "m01-logic__section-head",
|
||
children: [
|
||
el("h3", { text: tc("Grid_Title") }),
|
||
...(edit
|
||
? [el("div", { className: "ui-btn-group", children: [addLogic] })]
|
||
: []),
|
||
],
|
||
}),
|
||
createText(t("QtyHint"), "hint"),
|
||
gridHost,
|
||
],
|
||
}),
|
||
],
|
||
}),
|
||
side.root,
|
||
],
|
||
}),
|
||
);
|
||
draw();
|
||
void loadLogics().then(() => alive && recalc());
|
||
|
||
return {
|
||
getDoc: () => {
|
||
const out: Record<string, unknown> = { ...cur, 종류: "사전조합" };
|
||
delete out.열;
|
||
return out as unknown as LaborDoc;
|
||
},
|
||
saveArea: async () => false,
|
||
destroy: () => {
|
||
alive = false;
|
||
window.clearTimeout(timer);
|
||
host.replaceChildren();
|
||
},
|
||
};
|
||
}
|