merge(M02): origin/sub_laptop_2 받음 — Api_Fetch Kind 설명 충돌 풂(labor 한 번)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
@@ -123,3 +123,16 @@
|
||||
gap: var(--spacing-8);
|
||||
margin-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
/* 보기 갈래(면적산출 오른쪽 패널 · 일위대가 카드) */
|
||||
.m02-labview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m02-labview__title {
|
||||
margin: 0;
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type LogicSummary,
|
||||
type SubBrief,
|
||||
} from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import type { LaborCalc } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import {
|
||||
getActiveLayer,
|
||||
getActiveOwner,
|
||||
@@ -215,39 +216,64 @@ export interface LaborDoc {
|
||||
자재: LaborMat[];
|
||||
계산값?: unknown;
|
||||
}
|
||||
export interface LaborCost {
|
||||
노무비: number;
|
||||
재료비: number;
|
||||
경비: number;
|
||||
계: number;
|
||||
/** 받은 문서를 화면 모양으로 — 빈 칸 · 옛 모양도 받음(빈 일위대가) */
|
||||
export function normalizeLabor(doc: unknown, column: string): LaborDoc {
|
||||
const d = (doc && typeof doc === "object" ? doc : {}) as Partial<LaborDoc>;
|
||||
return {
|
||||
...d,
|
||||
종류: "일위대가",
|
||||
열: d.열 ?? column,
|
||||
단위: d.단위 ?? null,
|
||||
담은로직: (d.담은로직 ?? []).map((r) => ({
|
||||
로직: r.로직,
|
||||
메모: r.메모 ?? null,
|
||||
입력값: r.입력값 ?? {},
|
||||
수량: r.수량 ?? null,
|
||||
})),
|
||||
자재: (d.자재 ?? []).map((m) => ({
|
||||
코드: m.코드,
|
||||
이름: m.이름 ?? null,
|
||||
규격: m.규격 ?? null,
|
||||
단위: m.단위 ?? null,
|
||||
수량: m.수량 ?? null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
/** calc 줄 — 기준 = 결과단위 그대로 · 반영 = 기준 × 수량 ÷ 배(수량 없거나 돈 아닌 로직은 null) */
|
||||
export interface LaborCalcLine {
|
||||
로직: string;
|
||||
분모: string | null;
|
||||
배: number | null;
|
||||
수량: number | null;
|
||||
수량출처: string | null;
|
||||
기준: LaborCost | null;
|
||||
반영: LaborCost | null;
|
||||
결과?: number | null;
|
||||
까닭: string | null;
|
||||
}
|
||||
export interface LaborCalcMat {
|
||||
코드: string;
|
||||
기준단가: number | null;
|
||||
수량: number | null;
|
||||
수량출처: string | null;
|
||||
반영: number | null;
|
||||
까닭: string | null;
|
||||
}
|
||||
export interface LaborCalc {
|
||||
줄: LaborCalcLine[];
|
||||
자재: LaborCalcMat[];
|
||||
기준합: Partial<LaborCost>;
|
||||
반영합: Partial<LaborCost>;
|
||||
|
||||
/** 면적산출 문서의 알약 → {칸, 값}(식 칸 = 서버 `계산값` · 그 밖 = 칸 값 · 수만) — 서버 `area_values` 와 같은 규칙 */
|
||||
export function areaLinks(area: unknown): Map<string, { 칸: string; 값: number | null }> {
|
||||
const out = new Map<string, { 칸: string; 값: number | null }>();
|
||||
const doc = (area && typeof area === "object" ? area : {}) as {
|
||||
시트?: {
|
||||
id?: string;
|
||||
칸?: Record<string, { 값?: unknown }>;
|
||||
연결?: { 키?: string; 칸?: string }[];
|
||||
}[];
|
||||
계산값?: Record<string, Record<string, { 수?: unknown }>>;
|
||||
};
|
||||
for (const sheet of doc.시트 ?? []) {
|
||||
const solved = doc.계산값?.[sheet.id ?? ""] ?? {};
|
||||
for (const link of sheet.연결 ?? []) {
|
||||
if (!link.키 || !link.칸) continue;
|
||||
const raw = solved[link.칸]?.수 ?? sheet.칸?.[link.칸]?.값;
|
||||
const n =
|
||||
raw === null || raw === undefined || raw === ""
|
||||
? NaN
|
||||
: Number(String(raw).replace(/,/g, ""));
|
||||
out.set(link.키, { 칸: link.칸, 값: Number.isFinite(n) ? n : null });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** calc 답 모양 · 수량 · 반영 짝 셈 = `M02_MasterTemplete_Labor_Calc.ts`(sub7 · ↔ `M02_Labor_Doc.py`) */
|
||||
export type {
|
||||
CalcLine as LaborCalcLine,
|
||||
CalcMaterial as LaborCalcMat,
|
||||
LaborCalc,
|
||||
Money as LaborCost,
|
||||
} from "./M02_MasterTemplete_Labor_Calc";
|
||||
|
||||
/** 지금 층이면 `owner` 를 붙인 층 길(`/layers/{층}/labor/{열}…`) */
|
||||
const laborAt = (layer: Layer, column: string, tail: string): string => {
|
||||
const owner = layer === getActiveLayer() ? getActiveOwner() : null;
|
||||
@@ -255,13 +281,13 @@ const laborAt = (layer: Layer, column: string, tail: string): string => {
|
||||
return `/layers/${layer}/labor/${enc(column)}${tail}${q}`;
|
||||
};
|
||||
|
||||
/** 계산(서버 · 저장 안 함) — `doc` = 화면 캐시(없으면 저장본) · `수량` = 칸 값 바꿔 끼우기 */
|
||||
/** 계산(서버 · 저장 안 함) — `doc` = 화면 캐시(없으면 저장본) · `수량` = {알약 키: 칸 값} 바꿔 끼우기 */
|
||||
export const calcLabor = (
|
||||
layer: Layer,
|
||||
column: string,
|
||||
doc: LaborDoc | null,
|
||||
수량?: Record<string, number>,
|
||||
): Promise<LaborCalc> =>
|
||||
): Promise<LaborCalc & { 알약: unknown[] }> =>
|
||||
request(laborAt(layer, column, "/calc"), "POST", {
|
||||
...(doc ? { doc } : {}),
|
||||
...(수량 ? { 수량 } : {}),
|
||||
|
||||
@@ -11,10 +11,13 @@ import { createButton, createInputField, el, showToast } from "@ui/ui_template_e
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side";
|
||||
import type { ItemLogic, Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { readTemplate, type ItemLogic, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { settle } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import {
|
||||
areaLinks,
|
||||
calcLabor,
|
||||
fetchLogicInputs,
|
||||
normalizeLabor,
|
||||
type LaborCalc,
|
||||
type LaborDoc,
|
||||
type LaborRow,
|
||||
@@ -26,7 +29,7 @@ import {
|
||||
} from "./M02_MasterTemplete_Labor_Diagram";
|
||||
import { openLaborInput, openQtyModal } from "./M02_MasterTemplete_Labor_Input";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { logicTable, matTable, qtyText } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { keepKnown } from "./M02_Labor_Values_Model";
|
||||
import { pickMaterial } from "./M02_Material_Pick";
|
||||
@@ -35,30 +38,6 @@ import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css";
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
|
||||
/** 받은 문서를 화면 모양으로 — 빈 칸 · 옛 모양도 받음(빈 일위대가) */
|
||||
export function normalizeLabor(doc: unknown, column: string): LaborDoc {
|
||||
const d = (doc && typeof doc === "object" ? doc : {}) as Partial<LaborDoc>;
|
||||
return {
|
||||
...d,
|
||||
종류: "일위대가",
|
||||
열: d.열 ?? column,
|
||||
단위: d.단위 ?? null,
|
||||
담은로직: (d.담은로직 ?? []).map((r) => ({
|
||||
로직: r.로직,
|
||||
메모: r.메모 ?? null,
|
||||
입력값: r.입력값 ?? {},
|
||||
수량: r.수량 ?? null,
|
||||
})),
|
||||
자재: (d.자재 ?? []).map((m) => ({
|
||||
코드: m.코드,
|
||||
이름: m.이름 ?? null,
|
||||
규격: m.규격 ?? null,
|
||||
단위: m.단위 ?? null,
|
||||
수량: m.수량 ?? null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export interface LaborDetailOptions {
|
||||
layer: Layer;
|
||||
column: string;
|
||||
@@ -88,6 +67,11 @@ export function mountLaborDetail(
|
||||
let seq = 0;
|
||||
let timer = 0;
|
||||
let alive = true;
|
||||
/** 그 열 면적산출의 알약 → 칸 · 값(저장본 · 서버 calc 와 같은 것) */
|
||||
let links = new Map<string, { 칸: string; 값: number | null }>();
|
||||
const cells = (): Map<string, string> => new Map([...links].map(([k, v]) => [k, v.칸]));
|
||||
const given = (): Record<string, number> =>
|
||||
Object.fromEntries([...links].flatMap(([k, v]) => (v.값 === null ? [] : [[k, v.값]])));
|
||||
|
||||
const logicHost = el("div");
|
||||
const matHost = el("div");
|
||||
@@ -126,14 +110,14 @@ export function mountLaborDetail(
|
||||
const draw = (): void => {
|
||||
logicHost.replaceChildren(
|
||||
logicTable(
|
||||
{ rows: cur.담은로직, logics, calc, fail },
|
||||
{ rows: cur.담은로직, logics, calc, fail, cells: cells() },
|
||||
// 읽기만이면 순서 · 빼기 없음(줄 누름 = 보기 모달은 둠)
|
||||
edit ? { open: openRow, move, remove: removeRow } : { open: openRow },
|
||||
),
|
||||
);
|
||||
matHost.replaceChildren(
|
||||
matTable(
|
||||
{ mats: cur.자재, calc, fail },
|
||||
{ mats: cur.자재, calc, fail, cells: cells() },
|
||||
edit ? { open: openMat, remove: removeMat } : { open: openMat },
|
||||
),
|
||||
);
|
||||
@@ -160,9 +144,20 @@ export function mountLaborDetail(
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const changed = (): void => {
|
||||
/** `qtyOnly` = 수량만 바뀜 — 기준은 그대로라 짝 셈(`Labor_Calc.settle`)으로 바로 곱함(서버 안 부름) */
|
||||
const changed = (qtyOnly = false): void => {
|
||||
opt.onChanged(JSON.stringify(cur) !== saved);
|
||||
recalc();
|
||||
if (qtyOnly && calc) {
|
||||
calc = settle(calc, cur, given());
|
||||
draw();
|
||||
} else recalc();
|
||||
};
|
||||
|
||||
/** 이은 칸 글 「B6 = 12.5 ㎡」 — 알약이 칸에 안 이어졌으면 null */
|
||||
const linkedText = (pill: string | null | undefined, unit: string): string | null => {
|
||||
const at = pill ? links.get(pill) : undefined;
|
||||
if (!at) return null;
|
||||
return `${at.칸} = ${at.값 === null ? "—" : at.값.toLocaleString("ko-KR")}${unit ? ` ${unit}` : ""}`;
|
||||
};
|
||||
|
||||
/** 아직 안 받은 로직 입력 목록만 받음 */
|
||||
@@ -203,16 +198,18 @@ export function mountLaborDetail(
|
||||
memo: row.메모,
|
||||
seat: {
|
||||
qty: row.수량,
|
||||
linked: line && line.수량출처 !== "손" ? qtyText(line, line.분모 ?? "") || null : null,
|
||||
linked: linkedText(line?.알약, line?.분모 ?? ""),
|
||||
unit: line?.분모 ?? "",
|
||||
},
|
||||
readOnly: !edit,
|
||||
goArea: opt.goArea,
|
||||
done: (next) => {
|
||||
row.입력값 = logic ? keepKnown(logic.입력, next.입력값).kept : next.입력값;
|
||||
const values = logic ? keepKnown(logic.입력, next.입력값).kept : next.입력값;
|
||||
const qtyOnly = JSON.stringify(values) === JSON.stringify(row.입력값);
|
||||
row.입력값 = values;
|
||||
row.메모 = next.메모;
|
||||
row.수량 = next.수량;
|
||||
changed();
|
||||
changed(qtyOnly);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -224,17 +221,14 @@ export function mountLaborDetail(
|
||||
`${mat.코드} ${mat.이름 ?? ""}`.trim(),
|
||||
{
|
||||
qty: mat.수량,
|
||||
linked:
|
||||
line && line.수량출처 && line.수량출처 !== "손" && line.수량 !== null
|
||||
? `${line.수량출처} = ${line.수량}`
|
||||
: null,
|
||||
linked: linkedText(line?.알약, mat.단위 ?? ""),
|
||||
unit: mat.단위 ?? "",
|
||||
},
|
||||
!edit,
|
||||
opt.goArea,
|
||||
(qty) => {
|
||||
mat.수량 = qty;
|
||||
changed();
|
||||
changed(true);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -248,7 +242,7 @@ export function mountLaborDetail(
|
||||
if (cur.담은로직.some((r) => r.로직 === x.키))
|
||||
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
|
||||
cur.담은로직.push({ 로직: x.키, 메모: null, 입력값: {}, 수량: null });
|
||||
void loadLogics().then(changed);
|
||||
void loadLogics().then(() => changed());
|
||||
}),
|
||||
});
|
||||
const addMat = createButton({
|
||||
@@ -308,7 +302,14 @@ export function mountLaborDetail(
|
||||
}),
|
||||
);
|
||||
draw();
|
||||
void loadLogics().then(() => {
|
||||
// 로직 입력 목록 · 면적산출 이은 칸(없으면 빈 것 — 새 구조물)을 받은 뒤 첫 계산
|
||||
void Promise.all([
|
||||
loadLogics(),
|
||||
readTemplate(opt.layer, "area", opt.column, null).then(
|
||||
(got) => (links = areaLinks(got.문서)),
|
||||
() => undefined,
|
||||
),
|
||||
]).then(() => {
|
||||
if (alive) recalc();
|
||||
});
|
||||
|
||||
|
||||
@@ -105,11 +105,11 @@ export interface LaborDiagramSource {
|
||||
줄: {
|
||||
로직: string;
|
||||
기준: Money | null;
|
||||
반영: Money | null;
|
||||
결과?: number | null;
|
||||
까닭: string | null;
|
||||
반영?: Money | null;
|
||||
결과?: unknown;
|
||||
까닭?: string | null;
|
||||
}[];
|
||||
자재: { 기준단가: number | null; 반영: number | null }[];
|
||||
자재: { 기준단가: number | null; 반영?: number | null }[];
|
||||
} | null;
|
||||
/** 담은 자재 수 — 0 이면 자재 카드 없음 */
|
||||
mats: number;
|
||||
@@ -157,7 +157,7 @@ export function buildLaborDiagram(src: LaborDiagramSource): DiagramData {
|
||||
unit: "",
|
||||
inputs: [],
|
||||
out: mats ? { 노무비: 0, 재료비: sum, 경비: 0, 계: sum } : null,
|
||||
...(mats ? { basis: mats.every((m) => m.반영 !== null) ? "반영" : "기준" } : {}),
|
||||
...(mats ? { basis: mats.every((m) => typeof m.반영 === "number") ? "반영" : "기준" } : {}),
|
||||
material: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,7 +137,12 @@ export function openLaborInput(opt: LaborInputOptions): void {
|
||||
if (isBad) bad.add(input.이름);
|
||||
else bad.delete(input.이름);
|
||||
if (value === null) delete draft[input.이름];
|
||||
else draft[input.이름] = value;
|
||||
// 고르기 · 범위 없는 칸도 수로 읽히면 수로 둠(M01 넣을 값과 같게)
|
||||
else
|
||||
draft[input.이름] =
|
||||
typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))
|
||||
? Number(value)
|
||||
: value;
|
||||
});
|
||||
field.control.disabled = opt.readOnly;
|
||||
field.control.setAttribute("data-labor-input", input.이름);
|
||||
|
||||
@@ -33,16 +33,17 @@ const HEAD: Record<(typeof COSTS)[number], Parameters<typeof tc>[0]> = {
|
||||
const num = (v: number): string =>
|
||||
Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
||||
|
||||
/** 수량 글 — 「수량 손 3 개소」 · 「수량 B6 = 12.5 ㎡」 · 없으면 빈 글 */
|
||||
/** 수량 글 — 「수량 손 3 개소」 · 「수량 B6 = 12.5 ㎡」(칸 = 그 알약을 이은 면적산출 칸) · 없으면 빈 글 */
|
||||
export function qtyText(
|
||||
line: { 수량: number | null; 수량출처: string | null } | undefined,
|
||||
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
|
||||
unit: string,
|
||||
cells: ReadonlyMap<string, string> = new Map(),
|
||||
): string {
|
||||
if (!line || line.수량 === null || line.수량 === undefined) return "";
|
||||
const v = `${num(line.수량)}${unit ? ` ${unit}` : ""}`;
|
||||
if (!line.수량출처 || line.수량출처 === "손")
|
||||
return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`;
|
||||
return `${tc("Labor_Qty")} ${line.수량출처} = ${v}`;
|
||||
if (line.수량출처 === "손") return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`;
|
||||
const cell = (line.알약 && cells.get(line.알약)) || tc("In_QtyCell");
|
||||
return `${tc("Labor_Qty")} ${cell} = ${v}`;
|
||||
}
|
||||
|
||||
/** 금액 칸 하나 — 위 기준(작게) · 아래 반영 */
|
||||
@@ -100,6 +101,8 @@ export interface LogicTableData {
|
||||
/** calc 답 — null = 계산 중 · 못 셈은 `fail` */
|
||||
calc: LaborCalc | null;
|
||||
fail?: string | null;
|
||||
/** 알약 키 → 이은 면적산출 칸(「B6」) */
|
||||
cells?: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/** 담은 로직 표 — 보기 갈래는 `on` 없이 */
|
||||
@@ -114,7 +117,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
const line = lineOf(row.로직);
|
||||
const unit = logic?.결과단위 ?? "";
|
||||
const values = valueLine(logic?.입력 ?? [], row.입력값);
|
||||
const qty = qtyText(line, line?.분모 ?? "");
|
||||
const qty = qtyText(line, line?.분모 ?? "", data.cells);
|
||||
const note = [values.text, qty].filter(Boolean).join(" · ");
|
||||
const why = line?.까닭 ?? (data.fail && i === 0 ? data.fail : null);
|
||||
const small = (label: string, run: () => void): HTMLElement =>
|
||||
@@ -210,6 +213,7 @@ export interface MatTableData {
|
||||
mats: LaborMat[];
|
||||
calc: LaborCalc | null;
|
||||
fail?: string | null;
|
||||
cells?: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/** 자재목록 표 — 보기 갈래는 `on` 없이 */
|
||||
@@ -240,7 +244,7 @@ export function matTable(data: MatTableData, on?: Omit<RowHandlers, "move">): HT
|
||||
el("td", {
|
||||
className: "m02-labrow__num",
|
||||
text: qty,
|
||||
attrs: { title: line?.수량출처 ?? "" },
|
||||
attrs: { title: qtyText(line, mat.단위 ?? "", data.cells) },
|
||||
}),
|
||||
el("td", { className: "m02-labrow__num", text: show(line?.기준단가) }),
|
||||
el("td", { className: "m02-labrow__num", text: show(line?.반영) }),
|
||||
@@ -271,7 +275,7 @@ export function matTable(data: MatTableData, on?: Omit<RowHandlers, "move">): HT
|
||||
tc("Labor_Code"),
|
||||
tc("Col_Name"),
|
||||
tc("Labor_Spec"),
|
||||
tc("Col_Unit"),
|
||||
tc("Labor_Unit"),
|
||||
tc("Labor_Qty"),
|
||||
tc("Labor_BasePrice"),
|
||||
tc("Labor_AppliedSum"),
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Labor_View.ts
|
||||
* 일위대가 보기 갈래(39-3 · 고치기 없음) — 그 열 일위대가의 담은 로직 표 + 자재목록 표(기준 · 반영).
|
||||
* 면적산출 상세 오른쪽 패널(sub3) · 구조물 조합 일위대가 카드(sub1)가 씀. 표 모양은 상세와 한 벌(`Labor_Rows`).
|
||||
* · `show(층, 열, 수량?)` = 문서 · 로직 입력 목록 · calc 를 받아 그림(최신 응답만)
|
||||
* · `setQty({알약 키: 칸 값})` = 면적 칸 값이 바뀌면 서버 없이 짝 셈(`Labor_Calc.settle`)으로 바로 곱함
|
||||
* (그 열 면적 알약 값 전부를 줌 — 빠진 키는 「수량 없음」)
|
||||
* · `onRow` = 줄 누름(알약 키 · 이름 · 단위 — 칸 지정 모달 등) · 없으면 누름 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { readTemplate, type ItemLogic, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
areaLinks,
|
||||
calcLabor,
|
||||
fetchLogicInputs,
|
||||
normalizeLabor,
|
||||
type LaborCalc,
|
||||
type LaborDoc,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import { settle } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
|
||||
export interface LaborViewRow {
|
||||
kind: "logic" | "material";
|
||||
/** 로직 키 · 자재 코드 */
|
||||
key: string;
|
||||
/** 면적산출 알약 키(로직 「이름|단위」 · 자재 「자재|MT키」) — calc 전이면 null */
|
||||
pill: string | null;
|
||||
name: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface LaborViewHandle {
|
||||
root: HTMLElement;
|
||||
/** 그 열 일위대가를 읽어 그림 — `수량` = 저장 안 한 면적 칸 값(없으면 저장된 면적산출) */
|
||||
show: (layer: Layer, column: string | null, 수량?: Record<string, number>) => Promise<void>;
|
||||
/** 면적 칸 값이 바뀜(알약 값 전부) — 받은 기준에 바로 곱함(서버 안 부름) */
|
||||
setQty: (수량: Record<string, number>) => void;
|
||||
/** 지금 문서(없으면 null) */
|
||||
doc: () => LaborDoc | null;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewHandle {
|
||||
const root = el("div", { className: "m02-labview" });
|
||||
const logics = new Map<string, ItemLogic>();
|
||||
let doc: LaborDoc | null = null;
|
||||
let calc: LaborCalc | null = null;
|
||||
let fail: string | null = null;
|
||||
let cells = new Map<string, string>();
|
||||
let seq = 0;
|
||||
|
||||
const paint = (): void => {
|
||||
if (!doc) return;
|
||||
const d = doc;
|
||||
const pick = (row: LaborViewRow): (() => void) | undefined =>
|
||||
onRow ? () => onRow(row) : undefined;
|
||||
const logicOpen = (i: number): void => {
|
||||
const row = d.담은로직[i];
|
||||
const line = calc?.줄.find((l) => l.로직 === row.로직);
|
||||
pick({
|
||||
kind: "logic",
|
||||
key: row.로직,
|
||||
pill: line?.알약 ?? null,
|
||||
name: logics.get(row.로직)?.이름 ?? row.로직,
|
||||
unit: line?.분모 ?? "",
|
||||
})?.();
|
||||
};
|
||||
const matOpen = (i: number): void => {
|
||||
const mat = d.자재[i];
|
||||
const line = calc?.자재.find((m) => m.코드 === mat.코드);
|
||||
pick({
|
||||
kind: "material",
|
||||
key: mat.코드,
|
||||
pill: line?.알약 ?? null,
|
||||
name: mat.이름 ?? mat.코드,
|
||||
unit: mat.단위 ?? "",
|
||||
})?.();
|
||||
};
|
||||
root.replaceChildren(
|
||||
el("h4", { className: "m02-labview__title", text: tc("Logics_Title") }),
|
||||
logicTable(
|
||||
{ rows: d.담은로직, logics, calc, fail, cells },
|
||||
onRow ? { open: logicOpen } : undefined,
|
||||
),
|
||||
el("h4", { className: "m02-labview__title", text: tc("Labor_Mats") }),
|
||||
matTable({ mats: d.자재, calc, fail, cells }, onRow ? { open: matOpen } : undefined),
|
||||
);
|
||||
};
|
||||
|
||||
const show = async (
|
||||
layer: Layer,
|
||||
column: string | null,
|
||||
수량?: Record<string, number>,
|
||||
): Promise<void> => {
|
||||
const mine = ++seq;
|
||||
doc = null;
|
||||
calc = null;
|
||||
fail = null;
|
||||
if (!column) return void root.replaceChildren();
|
||||
root.replaceChildren(el("p", { className: "m02-labrow__muted", text: tc("Loading") }));
|
||||
try {
|
||||
const got = await readTemplate(layer, "labor", column, null);
|
||||
if (mine !== seq) return;
|
||||
doc = normalizeLabor(got.문서, column);
|
||||
const want = doc.담은로직.map((r) => r.로직).filter((k) => !logics.has(k));
|
||||
const [inputs, area] = await Promise.all([
|
||||
fetchLogicInputs(want),
|
||||
readTemplate(layer, "area", column, null).then(
|
||||
(a) => a.문서,
|
||||
() => null,
|
||||
),
|
||||
]);
|
||||
if (mine !== seq) return;
|
||||
for (const l of inputs.로직) logics.set(l.로직, l);
|
||||
cells = new Map([...areaLinks(area)].map(([k, v]) => [k, v.칸]));
|
||||
paint(); // 금액 「…」
|
||||
calc = await calcLabor(layer, column, null, 수량);
|
||||
} catch (error) {
|
||||
if (mine !== seq) return;
|
||||
if (!doc) {
|
||||
root.replaceChildren(
|
||||
el("p", {
|
||||
className: "m02-labrow__empty",
|
||||
text: error instanceof Error ? error.message : tc("Load_Failed"),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
fail = error instanceof Error ? error.message : tc("Labor_Calc_Fail");
|
||||
}
|
||||
if (mine === seq) paint();
|
||||
};
|
||||
|
||||
return {
|
||||
root,
|
||||
show,
|
||||
setQty: (수량) => {
|
||||
if (!doc || !calc) return;
|
||||
calc = settle(calc, doc, 수량);
|
||||
paint();
|
||||
},
|
||||
doc: () => doc,
|
||||
destroy: () => {
|
||||
seq += 1;
|
||||
root.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -68,6 +68,7 @@ const NUMBER_FIELD: Partial<Record<Kind, string>> = {
|
||||
structure: "도번",
|
||||
area: "면적번",
|
||||
material: "재료번",
|
||||
labor: "일위대가번",
|
||||
};
|
||||
// 표 부품(sub3) · 도면 부품(sub2) — 내보낸 이름으로 찾음
|
||||
// 표는 진입 파일만 — 폴더 통째(`*.ts`)면 분수 모듈이 따로 늦게 받는 입구가 되어 번들러 도우미를
|
||||
@@ -451,6 +452,20 @@ export function createMain(
|
||||
editor = { getDoc: () => comboMerge(loaded, shown.getDoc()), destroy: shown.destroy };
|
||||
return;
|
||||
}
|
||||
if (at.kind === "labor") {
|
||||
// 일위대가 상세(39-3) — 구조물 열마다 하나 · 고침은 이 화면 [저장]으로
|
||||
const { mountLaborDetail } = await import("./M02_MasterTemplete_Labor_Detail");
|
||||
editor = mountLaborDetail(host, doc, {
|
||||
layer: at.layer,
|
||||
column: at.name,
|
||||
readOnly,
|
||||
onChanged: (d) => (dirty = d && !readOnly),
|
||||
goArea: async () => {
|
||||
if (await confirmLeave()) void open({ ...at, kind: "area" });
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (at.kind === "area" || at.kind === "material") {
|
||||
// 면적산출 · 재료산출 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을
|
||||
// 열 때만 `import()` 로 받음(계약 `spreadsheet_types.ts` 머리)
|
||||
|
||||
Reference in New Issue
Block a user