feat(M02): 구조물 조합 고르기 모달 = M02 일위대가 카드 · 사본 셈 = 층 셈 한 벌(142-1)
- 사본 셈 = M02_Labor_Doc.calc 한 벌(사본 단가 · 굳힌 참조사본 · 사본 알약 주입 · 빈 입력 = 자동값) - 모달 일위대가 카드 = ItemCards labor(자료 자리 ItemSource.labor · 쓰는 길 없으면 읽기 전용) - 사본 문서에 사본 입력값 · 고른자재를 줄 입력값 · 고름으로 얹음 · [수정] = 넣을 값 편집 화면 - 옛 요약 카드(picker_labor) · 넣을 값 카드 · 머리 옛 일위대가 이름 줄 지움 · 처음 펼침 = 개인 - 시험: 사본 셈 = 층 셈 · 모달 일위대가 카드 읽기 전용 · 새 셈 꼴로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
@@ -245,7 +245,8 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
|
||||
for (const id of pathIds(start, labels())) opened.add(id);
|
||||
chosen = start;
|
||||
} else {
|
||||
opened.add("system"); // 지정된 게 없으면 첫 그룹만 펼침
|
||||
// 지정된 게 없으면 개인(142-1 · M02 기본 층 134-4 와 같게) — 개인 항목이 없으면(회사 없음 등) 마스터
|
||||
opened.add(rows.some((row) => layerOf(row) === "personal") ? "personal" : "system");
|
||||
}
|
||||
paintTree();
|
||||
paintDetail();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* =============================================================================
|
||||
* ui_template_item_picker_detail.ts
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b · 39-1c) — 카드 = 도면 · 일위대가 · 면적산출 ·
|
||||
* 넣을 값. 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `M02_Labor_Values`) + 일위대가 카드
|
||||
* (`ui_template_item_picker_labor`)를 늦게 받아 씀(B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`).
|
||||
* 넣을 값 = 일위대가 담은 로직 입력 전부(복사본이면 복사본 입력값).
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b · 39-1c · 142-1) — 카드 = 도면 · 일위대가 · 면적산출.
|
||||
* 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · 일위대가 = `Labor_Detail` 카드 갈래)을 늦게 받아 씀
|
||||
* (B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`). 일위대가 카드 = 자료 자리 길(`ItemSource.labor`) ·
|
||||
* 읽기 전용(쓰는 길 없음) · 복사본이면 복사본 문서 + 복사본 입력값 · 고른자재 · 셈은 복사본 셈(층 셈 한 벌).
|
||||
* 넣을 값 고침 = 일위대가 카드 [수정] → 넣을 값 편집 화면.
|
||||
*
|
||||
* 자료 자리 — **그 구조물의 복사본**(폴더 = 구조물 id · 같은 항목을 고른 구조물도 복사본은 따로)을 읽고,
|
||||
* 그 구조물에 아직 없으면 **원본 층**을 읽음(읽기만). [수정] 은 복사본만 고침 — 아직이면 「이 구조물로 가져와 고침」
|
||||
@@ -14,14 +15,12 @@
|
||||
|
||||
import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements";
|
||||
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { calcLabor, type LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||
import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
|
||||
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
|
||||
import type { InputsValueHandle } from "../M02_MasterTemplete/M02_Labor_Values";
|
||||
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
|
||||
import type { LaborCardHandle } from "./ui_template_item_picker_labor";
|
||||
import { askFields } from "./ui_template_item_picker_ask";
|
||||
import {
|
||||
folderLines,
|
||||
itemName,
|
||||
layerOf,
|
||||
pathOf,
|
||||
@@ -30,8 +29,6 @@ import {
|
||||
type PickerRow,
|
||||
} from "./ui_template_item_picker_data";
|
||||
import {
|
||||
laborOf,
|
||||
laborValueData,
|
||||
PART_OF,
|
||||
projectItems,
|
||||
sameItem,
|
||||
@@ -39,6 +36,7 @@ import {
|
||||
type CreatedItem,
|
||||
type ProjectItemInfo,
|
||||
type ProjectItems,
|
||||
withCopyValues,
|
||||
} from "./ui_template_item_picker_project";
|
||||
|
||||
/** 편집 화면을 여는 데 필요한 것 — 「넣을 값」 도 한 종류 */
|
||||
@@ -105,10 +103,9 @@ const TEXT = {
|
||||
empty: "{label} 을(를) 넣을 것",
|
||||
importFailed: "이 구조물로 가져오지 못함 — {value}",
|
||||
listFailed: "프로젝트 항목을 못 읽음 — {value}",
|
||||
calc: "복사본 계산 — 계 {total}원 · 빠진 입력 {gaps}개",
|
||||
calcStuck: " · 멈춘 로직 {stuck}개",
|
||||
calc: "복사본 계산 — 계 {total}원 · 빈 값 {gaps}개(자동값)",
|
||||
calcStuck: " · 못 돈 줄 {stuck}개",
|
||||
noCopy: "복사본에 없음",
|
||||
price: "일위대가",
|
||||
parts: {
|
||||
도면: "도면",
|
||||
면적: "면적산출",
|
||||
@@ -131,8 +128,6 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
let seq = 0;
|
||||
let infos: ProjectItemInfo[] | null = null;
|
||||
let cards: ItemCardsHandle | null = null;
|
||||
let inputs: InputsValueHandle | null = null;
|
||||
let labor: LaborCardHandle | null = null;
|
||||
let source: ItemSource = { layer: "system", projectId: null, read: () => Promise.reject() };
|
||||
/** 이 구조물에 있는 복사본 — 고른 항목의 것일 때만(다른 항목의 것이면 `other`) */
|
||||
let info: ProjectItemInfo | null = null;
|
||||
@@ -149,7 +144,6 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
const where = [opts.layerLabels[layerOf(row)], ...pathOf(row).slice(0, -1)]
|
||||
.filter(Boolean)
|
||||
.join(" › ");
|
||||
const pairs = folderLines(row).filter((line) => line.kind === "unitPrice");
|
||||
head.replaceChildren(
|
||||
el("h4", {
|
||||
className: "ui-item-picker__name",
|
||||
@@ -158,29 +152,46 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
}),
|
||||
small(where),
|
||||
...(row.단위 ? [small(`${opts.unit} ${row.단위}`)] : []),
|
||||
...pairs
|
||||
.filter((line) => line.name || line.key)
|
||||
.map((line) => small(`${TEXT.price} ${line.name || line.key}`)),
|
||||
);
|
||||
};
|
||||
|
||||
/** 이 구조물 복사본 또는 원본 층에서 열 하나의 문서를 읽음 — 없으면 던짐(카드가 「없음」) */
|
||||
const makeSource = (row: PickerRow): ItemSource =>
|
||||
info && opts.projectId
|
||||
? {
|
||||
layer: "project",
|
||||
projectId: opts.projectId,
|
||||
read: async (part) => {
|
||||
const doc = await items?.read(info!.폴더, PART_OF[part]);
|
||||
if (doc == null) throw new Error(TEXT.noCopy);
|
||||
return { 문서: doc };
|
||||
/** 이 구조물 복사본 또는 원본 층에서 열 하나의 문서를 읽음 — 없으면 던짐(카드가 「없음」) ·
|
||||
* 쓰는 길(`write`)은 없음 — 카드는 보기만(원본 층 · 복사본 고침은 편집 화면 · 142-1) */
|
||||
const makeSource = (row: PickerRow): ItemSource => {
|
||||
if (info && opts.projectId && items) {
|
||||
const at = info.폴더;
|
||||
return {
|
||||
layer: "project",
|
||||
projectId: opts.projectId,
|
||||
read: async (part) => {
|
||||
const doc = await items.read(at, PART_OF[part]);
|
||||
if (doc == null) throw new Error(TEXT.noCopy);
|
||||
return { 문서: doc };
|
||||
},
|
||||
labor: {
|
||||
read: async () => {
|
||||
const [labor, values, picks] = await Promise.all([
|
||||
items.read(at, "일위대가") as Promise<{ 줄?: LaborDoc } | null>,
|
||||
items.read(at, "입력값") as Promise<ValueDoc | null>,
|
||||
items.read(at, "고른자재") as Promise<Record<string, unknown> | null>,
|
||||
]);
|
||||
return { 문서: labor?.줄 ? withCopyValues(labor.줄, values, picks) : {} };
|
||||
},
|
||||
}
|
||||
: {
|
||||
layer: layerOf(row),
|
||||
projectId: null,
|
||||
read: (part, column) => readTemplate(layerOf(row), part, column, null),
|
||||
};
|
||||
calc: () => items.calcLabor(at),
|
||||
},
|
||||
};
|
||||
}
|
||||
const layer = layerOf(row);
|
||||
return {
|
||||
layer,
|
||||
projectId: null,
|
||||
read: (part, column) => readTemplate(layer, part, column, null),
|
||||
labor: {
|
||||
read: (column) => readTemplate(layer, "labor", column, null),
|
||||
calc: (column, doc, 수량) => calcLabor(layer, column, doc, 수량, null),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const badge = (text: string, className = ""): HTMLElement =>
|
||||
el("span", { className: `ui-item-picker__badge ${className}`.trim(), text });
|
||||
@@ -293,46 +304,17 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
opts.onEdit({ kind, row, info, items, projectId: opts.projectId });
|
||||
}
|
||||
|
||||
/** 일위대가 카드 · 넣을 값 카드 — 복사본이 있으면 복사본(값도) · 아니면 원본 층(39-1c) */
|
||||
async function paintLabor(row: PickerRow, mine: number): Promise<void> {
|
||||
if (!inputs || !labor) return;
|
||||
const stops = [labor.loading(), inputs.loading()]; // 카드 안 「불러오는 중」(M02 31-7)
|
||||
try {
|
||||
const copy = info && items ? info.폴더 : null;
|
||||
const summary = await laborOf(row, copy ? items : null, copy);
|
||||
const values = copy ? (((await items?.read(copy, "입력값")) as ValueDoc | null) ?? {}) : {};
|
||||
const data = summary ? await laborValueData(summary.로직, values) : null;
|
||||
stops.forEach((stop) => stop());
|
||||
if (mine !== seq) return;
|
||||
labor.set(summary);
|
||||
inputs.set(data);
|
||||
} catch (error) {
|
||||
stops.forEach((stop) => stop());
|
||||
if (mine !== seq) return;
|
||||
const retry = (): void => void paintLabor(row, seq);
|
||||
labor.set(null, why(error), retry);
|
||||
inputs.set(null, why(error), retry);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureCards(): Promise<void> {
|
||||
if (cards && inputs && labor) return;
|
||||
const [cardModule, inputModule, laborModule] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards"),
|
||||
import("../M02_MasterTemplete/M02_Labor_Values"),
|
||||
import("./ui_template_item_picker_labor"),
|
||||
]);
|
||||
labor = laborModule.createLaborCard();
|
||||
inputs = inputModule.createInputsValueCard({ mode: "view", onEdit: () => void edit("inputs") });
|
||||
if (cards) return;
|
||||
const cardModule = await import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards");
|
||||
// 차례 = 도면 · 일위대가 · 면적산출(M02 카드 한 벌) · 일위대가 [수정] = 넣을 값 편집 화면
|
||||
cards = cardModule.createItemCards({
|
||||
source: () => source,
|
||||
editable: () => canCopy,
|
||||
onEdit: (kind) => void edit(kind),
|
||||
warnOnFail: () => false,
|
||||
extraCards: [inputs.root],
|
||||
labor: () => void edit("inputs"),
|
||||
});
|
||||
// 차례 = 도면 · 일위대가 · 면적산출 — 일위대가 카드를 도면 카드 바로 뒤에
|
||||
cards.root.insertBefore(labor.root, cards.root.children[1] ?? null);
|
||||
cardsHost.replaceChildren(cards.root);
|
||||
}
|
||||
|
||||
@@ -344,8 +326,6 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
status.hidden = true;
|
||||
cardsHost.replaceChildren();
|
||||
cards = null;
|
||||
inputs = null;
|
||||
labor = null;
|
||||
return;
|
||||
}
|
||||
paintHead(row);
|
||||
@@ -362,14 +342,12 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
other = own && !info ? own : null;
|
||||
source = makeSource(row);
|
||||
await ensureCards();
|
||||
if (mine !== seq || !cards || !inputs || !labor) return;
|
||||
if (mine !== seq || !cards) return;
|
||||
const label = !info && canCopy ? TEXT.importEdit : null;
|
||||
cards.relabel({ structure: label, area: label });
|
||||
inputs.setEditLabel(label ?? "넣을 값 수정");
|
||||
cards.root.hidden = false;
|
||||
paintStatus(row, mine);
|
||||
void cards.load({ id: row.열, label: itemName(row) });
|
||||
void paintLabor(row, mine);
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/* =============================================================================
|
||||
* ui_template_item_picker_labor.ts
|
||||
* 항목 고르기 모달 「일위대가」 카드(계획서 39-1c) — 담은 로직 수 · 자재 수 · 기준 합 · 반영 합 글.
|
||||
* 보기만 · 고치기는 원본 층 일위대가 상세. 일위대가 보기 부품(39-3 C)이 오면 몸을 그것으로 바꿈.
|
||||
* 자료 = `ui_template_item_picker_project.laborOf`(복사본이면 복사본 계산 · 아니면 원본 층 계산).
|
||||
* 상세(`ui_template_item_picker_detail.ts`)가 카드를 받을 때 함께 `import()`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "./ui_template_elements";
|
||||
import { cardNote, cardShell } from "../M02_MasterTemplete/M02_MasterTemplete_CardShell";
|
||||
import type { LaborSummary } from "./ui_template_item_picker_project";
|
||||
|
||||
const TEXT = {
|
||||
title: "일위대가",
|
||||
none: "일위대가 없음",
|
||||
failed: "일위대가를 못 읽음 — {value}",
|
||||
logics: "단가산출 로직 {value}개",
|
||||
materials: "자재 {value}개",
|
||||
base: "기준 합 {value}원",
|
||||
reflected: "반영 합 {value}원",
|
||||
gaps: "빠진 입력 {value}개",
|
||||
stuck: "멈춘 로직 {value}개",
|
||||
};
|
||||
|
||||
const fill = (text: string, value: string | number): string =>
|
||||
text.replace("{value}", String(value));
|
||||
const won = (value: number | null): string =>
|
||||
value === null ? "—" : Math.round(value).toLocaleString();
|
||||
|
||||
export interface LaborCardHandle {
|
||||
root: HTMLElement;
|
||||
loading: () => () => void;
|
||||
/** `null` = 일위대가 없음 · `note` 가 있으면 못 읽은 까닭 */
|
||||
set: (summary: LaborSummary | null, note?: string, retry?: () => void) => void;
|
||||
}
|
||||
|
||||
export function createLaborCard(): LaborCardHandle {
|
||||
const number = el("span", { className: "m02-struct__no" });
|
||||
const body = el("div", { className: "m02-struct__sheet ui-item-picker__labor" });
|
||||
const shell = cardShell({ title: TEXT.title, sub: [number], body });
|
||||
const line = (text: string, className = ""): HTMLElement =>
|
||||
el("p", { className: `ui-item-picker__labor-line ${className}`.trim(), text });
|
||||
|
||||
return {
|
||||
root: shell.root,
|
||||
loading: () => shell.loading(),
|
||||
set: (summary, note, retry) => {
|
||||
number.textContent = summary?.번호 ?? "";
|
||||
if (note && retry) return shell.fail(fill(TEXT.failed, note), retry);
|
||||
if (!summary) return void body.replaceChildren(cardNote(TEXT.none));
|
||||
body.replaceChildren(
|
||||
line(`${fill(TEXT.logics, summary.로직.length)} · ${fill(TEXT.materials, summary.자재)}`),
|
||||
line(fill(TEXT.base, won(summary.base))),
|
||||
line(fill(TEXT.reflected, won(summary.reflected)), "is-strong"),
|
||||
...(summary.gaps ? [line(fill(TEXT.gaps, summary.gaps), "is-warn")] : []),
|
||||
...(summary.stuck ? [line(fill(TEXT.stuck, summary.stuck), "is-warn")] : []),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -9,13 +9,15 @@
|
||||
* GET|PUT /api/projects/{id}/items/{폴더}/{부분} → {종류, 판, 내용} · 고침은 {판, 내용}(판이 다르면 409)
|
||||
* POST …/{폴더}/reapply 재적용 → {폴더, 가져온날, 원본판, 덮임: [부분]}
|
||||
* POST …/{폴더}/save-as {to, 경로, 이름} 개인에 저장 → {key, 열, 이름, summary_item}
|
||||
* POST …/{폴더}/calc 복사본만으로 계산(계 · 빠진 입력 · 기준합 · 반영합)
|
||||
* 일위대가(39-1c) — 복사본 `일위대가` 부분 · 원본 층은 `calcLabor` · 넣을 값 목록은 `fetchLogicInputs`
|
||||
* POST …/{폴더}/calc 복사본만으로 계산 — 층 셈 꼴(줄 · 자재 · 기준합 · 반영합) + 알약 · 빠진입력(142-1)
|
||||
* 일위대가(39-1c · 142-1) — 모달 일위대가 카드(M02 카드 · 읽기 전용)가 `calcLabor` 로 셈 · 넣을 값 목록은 `fetchLogicInputs`
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { ItemInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { calcLabor, fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||
import { fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborCalc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Calc";
|
||||
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
|
||||
import { layerOf, ownerOf, type PickerRow } from "./ui_template_item_picker_data";
|
||||
|
||||
@@ -63,7 +65,10 @@ export interface CreatedItem {
|
||||
summary_item: { layer: string; owner: string | null; key: string; name: string };
|
||||
}
|
||||
|
||||
/** 복사본 계산 결과에서 모달이 보이는 것 — 계 · 빠진 입력 수 · 멈춘 로직 수 · 일위대가 기준합 · 반영합 계 */
|
||||
/** 복사본 셈 답(142-1) — 층 셈 꼴 + 사본 알약 · 빈 입력 목록(셈은 자동값으로 돎) */
|
||||
export type CopyCalc = LaborCalc & { 알약: unknown[]; 빠진입력?: unknown[] };
|
||||
|
||||
/** 복사본 계산 결과에서 모달이 보이는 것 — 계 · 빈 입력 수 · 못 돈 줄 수 · 일위대가 기준합 · 반영합 계 */
|
||||
export interface ProjectCalc {
|
||||
total: number;
|
||||
gaps: number;
|
||||
@@ -127,6 +132,8 @@ export interface ProjectItems {
|
||||
) => Promise<CreatedItem>;
|
||||
/** 복사본만으로 계산 — 계산할 것이 없거나 못 하면 null */
|
||||
calc: (folder: string) => Promise<ProjectCalc | null>;
|
||||
/** 복사본 셈 답 통째 — 모달 일위대가 카드(Labor_Detail)가 씀 · 못 하면 서버 까닭 그대로 던짐 */
|
||||
calcLabor: (folder: string) => Promise<CopyCalc>;
|
||||
}
|
||||
|
||||
const clients = new Map<string, ProjectItems>();
|
||||
@@ -142,6 +149,12 @@ export function projectItems(projectId: string): ProjectItems {
|
||||
for (const key of [...versions.keys()]) if (key.startsWith(`${folder}/`)) versions.delete(key);
|
||||
};
|
||||
const at = (folder: string, part: string): string => `${base}/${enc(folder)}/${enc(part)}`;
|
||||
/** 복사본 셈(142-1) — 못 하면 서버 까닭 그대로 던짐 */
|
||||
const calcCopy = async (folder: string): Promise<CopyCalc> => {
|
||||
const reply = await call(`${base}/${enc(folder)}/calc`, post({}));
|
||||
if (reply.status !== 200) throw new Error(reasonOf(reply));
|
||||
return reply.body as CopyCalc;
|
||||
};
|
||||
|
||||
const client: ProjectItems = {
|
||||
list: async () => {
|
||||
@@ -213,78 +226,23 @@ export function projectItems(projectId: string): ProjectItems {
|
||||
return reply.body as CreatedItem;
|
||||
},
|
||||
calc: async (folder) => {
|
||||
const reply = await call(`${base}/${enc(folder)}/calc`, post({}));
|
||||
if (reply.status !== 200) return null;
|
||||
const got = reply.body as {
|
||||
계?: number;
|
||||
빠진입력?: unknown[];
|
||||
멈춤?: unknown[];
|
||||
기준합?: { 계?: number };
|
||||
반영합?: { 계?: number };
|
||||
};
|
||||
const got = await calcCopy(folder).catch(() => null);
|
||||
if (!got) return null;
|
||||
return {
|
||||
total: Number(got.계 ?? 0),
|
||||
total: Number(got.반영합?.계 ?? got.기준합?.계 ?? 0),
|
||||
gaps: got.빠진입력?.length ?? 0,
|
||||
stuck: got.멈춤?.length ?? 0,
|
||||
stuck: (got.줄 ?? []).filter((line) => line.까닭).length,
|
||||
base: got.기준합?.계 ?? null,
|
||||
reflected: got.반영합?.계 ?? null,
|
||||
};
|
||||
},
|
||||
calcLabor: (folder) => calcCopy(folder),
|
||||
};
|
||||
clients.set(projectId, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/* ── 일위대가(39-1c) — 카드 요약 · 넣을 값 자료 ─────────────────────────────── */
|
||||
|
||||
/** 일위대가 카드에 보이는 것 — 복사본이면 복사본 계산 · 아니면 원본 층 계산(저장 안 함) */
|
||||
export interface LaborSummary {
|
||||
번호: string | null;
|
||||
/** 담은 로직 키(차례 그대로) — 넣을 값 목록을 받는 데도 씀 */
|
||||
로직: string[];
|
||||
자재: number;
|
||||
base: number | null;
|
||||
reflected: number | null;
|
||||
gaps: number;
|
||||
stuck: number;
|
||||
}
|
||||
|
||||
interface LaborCopy {
|
||||
줄?: { 일위대가번?: string; 담은로직?: { 로직?: string }[]; 자재?: unknown[] };
|
||||
}
|
||||
|
||||
/** 그 항목의 일위대가 — 복사본(`folder`)이 있으면 그것 · 없으면 원본 층 열 · 일위대가가 없으면 null */
|
||||
export async function laborOf(
|
||||
row: PickerRow,
|
||||
items: ProjectItems | null,
|
||||
folder: string | null,
|
||||
): Promise<LaborSummary | null> {
|
||||
if (items && folder) {
|
||||
const copy = (await items.read(folder, "일위대가")) as LaborCopy | null;
|
||||
if (!copy?.줄) return null;
|
||||
const got = await items.calc(folder);
|
||||
return {
|
||||
번호: copy.줄.일위대가번 ?? null,
|
||||
로직: (copy.줄.담은로직 ?? []).map((one) => String(one.로직 ?? "")).filter(Boolean),
|
||||
자재: copy.줄.자재?.length ?? 0,
|
||||
base: got?.base ?? null,
|
||||
reflected: got?.reflected ?? null,
|
||||
gaps: got?.gaps ?? 0,
|
||||
stuck: got?.stuck ?? 0,
|
||||
};
|
||||
}
|
||||
const got = await calcLabor(layerOf(row), row.열, null).catch(() => null);
|
||||
if (!got) return null;
|
||||
return {
|
||||
번호: null,
|
||||
로직: got.줄.map((line) => line.로직),
|
||||
자재: got.자재.length,
|
||||
base: got.기준합?.계 ?? null,
|
||||
reflected: got.반영합?.계 ?? null,
|
||||
gaps: 0,
|
||||
stuck: got.줄.filter((line) => line.까닭).length,
|
||||
};
|
||||
}
|
||||
/* ── 일위대가(39-1c) — 넣을 값 자료 ─────────────────────────────── */
|
||||
|
||||
/** 넣을 값 카드 자료 — 담은 로직 입력 전부 */
|
||||
export async function laborValueData(
|
||||
@@ -295,3 +253,29 @@ export async function laborValueData(
|
||||
const list: ItemInputs = { 일위대가: null, 로직: got.로직, 빠진로직: got.빠진로직 };
|
||||
return { list, values };
|
||||
}
|
||||
|
||||
/** 복사본 일위대가 문서에 복사본 입력값 · 고른자재를 줄 입력값 · 고름으로 얹음(보기용 · 142-1) —
|
||||
* 빈 값은 안 얹음(= 자동값) · 셈은 서버가 같은 규칙으로(`M02_Item_Prices.with_values`) */
|
||||
export function withCopyValues(
|
||||
doc: LaborDoc,
|
||||
values: ValueDoc | null,
|
||||
picks: Record<string, unknown> | null,
|
||||
): LaborDoc {
|
||||
const lines = (doc.담은로직 ?? []).map((one) => {
|
||||
const code = (one as { 로직?: string }).로직;
|
||||
if (!code) return one;
|
||||
const given = Object.fromEntries(
|
||||
Object.entries(values?.[code] ?? {}).flatMap(([name, got]) => {
|
||||
const v = (got as { 값?: unknown } | null)?.값;
|
||||
return v === null || v === undefined || v === "" ? [] : [[name, v]];
|
||||
}),
|
||||
);
|
||||
const pick = picks?.[code];
|
||||
return {
|
||||
...one,
|
||||
입력값: given,
|
||||
...(pick && typeof pick === "object" ? { 고름: pick } : {}),
|
||||
} as typeof one;
|
||||
});
|
||||
return { ...doc, 담은로직: lines };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user