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>
This commit is contained in:
@@ -11,7 +11,9 @@
|
||||
* 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸)
|
||||
* · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 다시 셈)
|
||||
* · 안 이은 고정 줄(729-3) = 격자 위 띠 · [연결](`Labor_Unlinked` — 이음만 · 저장은 [저장])
|
||||
* 구조물 조합 일위대가(카드 · 열 상세)는 그대로 `Labor_Detail`(면적 잇기 · 수량산출).
|
||||
* 구조물 조합 일위대가(카드 · 열 상세 · PLAN 806-1)도 이 한 부품 — `mode` 없음 = 조합 갈래: 정보 칸(이름 · 구분)
|
||||
* 없음 · 수량 칸 = 수량산출 모달(`Labor_Combo` — 면적산출 잇기) · 규격 열 줄(`Labor_Spec`) · 다이어그램(열 상세) ·
|
||||
* 새 줄 수량 = 면적을 따름(빈 값). 단가산출(SK) 참조 줄 누름 = 그 단가산출 문서 모달(`Labor_SkEdit` — 수식 탭 · 저장 = 그 SK).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
@@ -36,6 +38,10 @@ import {
|
||||
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";
|
||||
@@ -88,7 +94,11 @@ export function mountLaborTable(
|
||||
let timer = 0;
|
||||
let alive = true;
|
||||
let picked: string | null = null;
|
||||
const dirty = (): boolean => JSON.stringify(cur) !== saved;
|
||||
/** 조합 갈래(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) => {
|
||||
@@ -103,15 +113,17 @@ export function mountLaborTable(
|
||||
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(),
|
||||
});
|
||||
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 (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);
|
||||
@@ -136,12 +148,24 @@ export function mountLaborTable(
|
||||
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);
|
||||
},
|
||||
@@ -153,12 +177,14 @@ export function mountLaborTable(
|
||||
}
|
||||
: 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) => {
|
||||
onEdit: (i, key, text) => {
|
||||
if (combo) return spec.onEdit?.(i, key, text);
|
||||
const n = at(i);
|
||||
if (n !== null) setQty(n, text);
|
||||
},
|
||||
@@ -180,7 +206,9 @@ export function mountLaborTable(
|
||||
});
|
||||
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);
|
||||
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);
|
||||
};
|
||||
@@ -194,11 +222,14 @@ export function mountLaborTable(
|
||||
fail = null;
|
||||
draw();
|
||||
try {
|
||||
// 저장 안 한 칸 연결이 있으면 그 칸 값으로(없으면 서버가 저장된 면적산출을 읽음)
|
||||
const qty = combo?.dirty() ? combo.given() : undefined;
|
||||
const got = opt.calc
|
||||
? await opt.calc(cur)
|
||||
: await calcLabor(opt.layer, opt.column, cur, undefined, opt.owner);
|
||||
? 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");
|
||||
@@ -210,7 +241,7 @@ export function mountLaborTable(
|
||||
const changed = (qtyOnly = false): void => {
|
||||
opt.onChanged(dirty());
|
||||
if (qtyOnly && calc) {
|
||||
calc = settle(calc, cur, {});
|
||||
calc = settle(calc, cur, combo?.given() ?? {});
|
||||
draw();
|
||||
} else recalc();
|
||||
};
|
||||
@@ -249,6 +280,29 @@ export function mountLaborTable(
|
||||
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];
|
||||
@@ -263,13 +317,20 @@ export function mountLaborTable(
|
||||
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: [String((cur as { 이름?: string }).이름 ?? ""), cur.단위 ? `1 ${cur.단위} 당` : ""]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
head: head(),
|
||||
unit: lineOf(row)?.분모 ?? "",
|
||||
watch,
|
||||
scope: { layer: opt.layer, owner: library.owner },
|
||||
@@ -342,7 +403,7 @@ export function mountLaborTable(
|
||||
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 } });
|
||||
cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: fresh() });
|
||||
changed();
|
||||
},
|
||||
fixed: (fx: LaborFixed, name: string) => {
|
||||
@@ -355,7 +416,7 @@ export function mountLaborTable(
|
||||
별칭: null,
|
||||
메모: null,
|
||||
입력값: {},
|
||||
수량: { ...ONE },
|
||||
수량: fresh(),
|
||||
});
|
||||
changed();
|
||||
},
|
||||
@@ -370,7 +431,7 @@ export function mountLaborTable(
|
||||
? "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 } });
|
||||
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: fresh() });
|
||||
changed();
|
||||
},
|
||||
};
|
||||
@@ -381,7 +442,7 @@ export function mountLaborTable(
|
||||
openLogicPicker((x) => {
|
||||
if (cur.담은로직.some((r) => r.로직 === x.키))
|
||||
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: { ...ONE } });
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: fresh() });
|
||||
void loadLogics().then(() => changed());
|
||||
}, withSangeun(library)),
|
||||
});
|
||||
@@ -418,60 +479,63 @@ export function mountLaborTable(
|
||||
origin.input.value = originText(cur.본뜬) ?? tc("Origin_Self");
|
||||
origin.input.readOnly = true;
|
||||
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "ui-side-panels-row",
|
||||
children: [
|
||||
const info = combo
|
||||
? opt.card
|
||||
? [] // 카드 갈래 = 정보 칸 없음
|
||||
: [el("div", { className: "m01lab__info", children: [unit, origin.root] })]
|
||||
: [
|
||||
el("div", {
|
||||
className: "m01-logic__editor m01-combo m02-labor__edit",
|
||||
attrs: { "data-labor": opt.column },
|
||||
className: "m01lab__info",
|
||||
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,
|
||||
],
|
||||
}),
|
||||
nameField.root,
|
||||
...libMeta(cur, edit, () => opt.onChanged(dirty())),
|
||||
unit,
|
||||
origin.root,
|
||||
],
|
||||
}),
|
||||
side.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 loadLogics().then(() => alive && recalc());
|
||||
// 로직 입력 목록 · 면적산출 이은 칸(조합 — 없으면 빈 것)을 받은 뒤 첫 계산
|
||||
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 () => false,
|
||||
saveArea: async () => (await combo?.save()) ?? false,
|
||||
destroy: () => {
|
||||
alive = false;
|
||||
window.clearTimeout(timer);
|
||||
|
||||
Reference in New Issue
Block a user