feat(B05): 항목 고르기 모달 일위대가 카드 · 넣을 값 = 담은 로직 입력 전부 (39-1c)
- 카드 차례 도면 · 일위대가 · 면적산출 — 새 ui_template_item_picker_labor.ts(담은 로직 수 · 자재 수 · 기준 합 · 반영 합 · 빠진 입력 · 멈춘 로직) - 자료 laborOf: 복사본이면 복사본 일위대가 + calc · 아니면 원본 층 labor calc(저장 안 함) · 일위대가 없으면 「일위대가 없음」 - 넣을 값 카드 · 편집 = M02_Labor_Values 바로 · 목록 = 담은 로직 입력 전부(logic-lib/inputs) · 값은 복사본 입력값 - ProjectCalc 에 기준합 · 반영합 계 · 시험 test_item_picker_detail 손봄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
* ① 폴더 이름 · 목록 {항목} · 가져오기 POST · 읽기 {종류, 판, 내용} · 고침 PUT {판, 내용}(판 · 409 · 403)
|
||||
* ② 새로 가져오기 · 계산 한 줄 · 입력 목록(한 번만 받음)
|
||||
* ③ 카드 셈: 범위 · 고르기 원래 모양 · 자리 「선택」 만 묶음
|
||||
* ④ 일위대가(39-1c): 복사본 요약 · 원본 층 계산 요약 · 넣을 값 = 담은 로직 입력 전부
|
||||
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
@@ -66,6 +67,21 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
const method = init.method ?? "GET";
|
||||
const body = init.body ? JSON.parse(init.body) : undefined;
|
||||
state.calls.push({ method, url, body });
|
||||
if (url.startsWith("/api/m02/logic-lib/inputs?keys=")) {
|
||||
state.logicKeys = decodeURIComponent(url.split("=")[1]).split(",");
|
||||
return reply(200, { 로직: INPUTS.로직, 빠진로직: [] });
|
||||
}
|
||||
if (/^\/api\/m02\/layers\/system\/labor\/rv\/calc$/.test(url)) {
|
||||
return reply(200, {
|
||||
줄: [
|
||||
{ 로직: "GA1", 까닭: null },
|
||||
{ 로직: "GA2", 까닭: "입력 없음" },
|
||||
],
|
||||
자재: [{ 코드: "MT000008" }],
|
||||
기준합: { 계: 500 },
|
||||
반영합: { 계: 1500 },
|
||||
});
|
||||
}
|
||||
if (/^\/api\/m02\/layers\/\w+\/items\/[^/]+\/inputs$/.test(url)) {
|
||||
state.inputsHits += 1;
|
||||
return reply(200, INPUTS);
|
||||
@@ -97,7 +113,13 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
const folder = body.structure.replace("#", "n");
|
||||
state.folders[folder] = {
|
||||
info: { 폴더: folder, 원본: body, 마스터바뀜: false, 가져온날: "2026-09-30T01:00:00" },
|
||||
parts: { 면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] }, 입력값: null },
|
||||
parts: {
|
||||
면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] },
|
||||
입력값: null,
|
||||
일위대가: {
|
||||
줄: { 일위대가번: "SU-000007", 담은로직: [{ 로직: "GA1" }, { 로직: "GA2" }], 자재: [{}] },
|
||||
},
|
||||
},
|
||||
};
|
||||
return reply(200, { 폴더: folder, 만듦: true, 가져온날: "2026-09-30T01:00:00" });
|
||||
}
|
||||
@@ -132,6 +154,8 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
{ 로직: "GA2", 입력: "지역" },
|
||||
],
|
||||
멈춤: ["GA1"],
|
||||
기준합: { 계: 100 },
|
||||
반영합: { 계: 300 },
|
||||
});
|
||||
if (method === "GET") {
|
||||
const content = item.parts[part] ?? null;
|
||||
@@ -226,6 +250,18 @@ async function main() {
|
||||
out.calc = await items.calc(info.폴더);
|
||||
out.calcMissing = await items.calc("없는폴더");
|
||||
|
||||
// 일위대가 — 복사본 요약(복사본 계산) · 원본 층 요약(층 calc) · 넣을 값 = 담은 로직 입력 전부
|
||||
out.laborCopy = await project.laborOf(row(), items, info.폴더);
|
||||
out.laborOrigin = await project.laborOf(row(), null, null);
|
||||
out.laborNone = await project.laborOf(row({ 열: "없는열" }), null, null);
|
||||
const valueData = await project.laborValueData(["GA1", "GA2"], { GA1: {} });
|
||||
out.laborValues = {
|
||||
keys: state.logicKeys,
|
||||
link: valueData.link.줄.map((l) => `${l.로직}|${l.입력}|${l.자리}`),
|
||||
shown: model.pickedGroups(valueData.list, valueData.link).length,
|
||||
values: valueData.values,
|
||||
};
|
||||
|
||||
// 입력 목록 — 같은 항목은 한 번만 받음
|
||||
const first = await project.loadInputs(row());
|
||||
await project.loadInputs(row());
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_재적용은_덮임을_돌려주고_계산_한_줄(out):
|
||||
assert out["reapplyCall"][0] == "POST /api/projects/P1/items/pipe%4085.05/reapply"
|
||||
assert out["reapply"]["covered"] == ["면적"] # 덮이는 것을 알림으로
|
||||
assert out["reapply"]["info"]["마스터바뀜"] is False
|
||||
assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1}
|
||||
assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1, "base": 100, "reflected": 300}
|
||||
assert out["calcMissing"] is None # 없는 복사본 = 계산 줄 없음
|
||||
|
||||
|
||||
@@ -146,8 +146,42 @@ def test_선택_입력_카드는_sub2_파일_한_벌(out):
|
||||
assert "export function createInputsValueCard(" in values # 모달 값 받기(view · value)
|
||||
assert "createInputsValueCard," in inputs and '} from "./M02_Labor_Values";' in inputs
|
||||
for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker_edit.ts"):
|
||||
text = (UI / name).read_text(encoding="utf-8")
|
||||
assert "M02_MasterTemplete_Inputs" in text and 'InputsCard"' not in text, name
|
||||
text = (UI / name).read_text(encoding="utf-8") # 모달은 값 카드를 바로 받음(39-1c)
|
||||
assert "M02_Labor_Values" in text and 'InputsCard"' not in text, name
|
||||
|
||||
|
||||
def test_일위대가_요약_복사본_원본_넣을_값_전부(out):
|
||||
"""39-1c — 복사본이면 복사본 일위대가 · 계산 · 아니면 원본 층 calc ·
|
||||
넣을 값 = 담은 로직 입력 전부."""
|
||||
assert out["laborCopy"] == {
|
||||
"번호": "SU-000007",
|
||||
"로직": ["GA1", "GA2"],
|
||||
"자재": 1,
|
||||
"base": 100,
|
||||
"reflected": 300,
|
||||
"gaps": 2,
|
||||
"stuck": 1,
|
||||
}
|
||||
assert out["laborOrigin"] == {
|
||||
"번호": None,
|
||||
"로직": ["GA1", "GA2"],
|
||||
"자재": 1,
|
||||
"base": 500,
|
||||
"reflected": 1500,
|
||||
"gaps": 0,
|
||||
"stuck": 1,
|
||||
}
|
||||
assert out["laborNone"] is None # 일위대가 없는 열(층 calc 404)
|
||||
values = out["laborValues"]
|
||||
assert values["keys"] == ["GA1", "GA2"]
|
||||
assert values["link"] == ["GA1|지역|선택", "GA1|소량감률|선택", "GA2|지역|선택"]
|
||||
assert values["shown"] == 2 and values["values"] == {"GA1": {}}
|
||||
|
||||
|
||||
def test_모달_카드_차례_도면_일위대가_면적():
|
||||
detail = (UI / "ui_template_item_picker_detail.ts").read_text(encoding="utf-8")
|
||||
assert 'import("./ui_template_item_picker_labor")' in detail
|
||||
assert "cards.root.insertBefore(labor.root, cards.root.children[1]" in detail
|
||||
|
||||
|
||||
def test_모달은_원본_층에_쓰는_길을_안_부름():
|
||||
|
||||
@@ -269,3 +269,18 @@
|
||||
.ui-modal.ui-item-ask {
|
||||
width: min(28rem, 92vw);
|
||||
}
|
||||
|
||||
/* 일위대가 카드(39-1c) — 담은 로직 · 자재 수 · 기준 합 · 반영 합 글 */
|
||||
.ui-item-picker__labor-line {
|
||||
margin: 0 0 var(--spacing-4);
|
||||
color: var(--color-text-body);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ui-item-picker__labor-line.is-strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.ui-item-picker__labor-line.is-warn {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* =============================================================================
|
||||
* ui_template_item_picker_detail.ts
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b) — **카드 넷**(도면 · 면적산출 · 재료산출 · 선택 입력).
|
||||
* 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `_Inputs`)을 늦게 받아 씀
|
||||
* (B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`).
|
||||
* 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b · 39-1c) — 카드 = 도면 · 일위대가 · 면적산출(· 재료산출) ·
|
||||
* 넣을 값. 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `M02_Labor_Values`) + 일위대가 카드
|
||||
* (`ui_template_item_picker_labor`)를 늦게 받아 씀(B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`).
|
||||
* 넣을 값 = 일위대가 담은 로직 입력 전부(복사본이면 복사본 입력값).
|
||||
*
|
||||
* 자료 자리 — **그 구조물의 복사본**(폴더 = 구조물 id · 같은 항목을 고른 구조물도 복사본은 따로)을 읽고,
|
||||
* 그 구조물에 아직 없으면 **원본 층**을 읽음(읽기만). [수정] 은 복사본만 고침 — 아직이면 「이 구조물로 가져와 고침」
|
||||
@@ -12,15 +13,12 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements";
|
||||
import {
|
||||
readInputLinks,
|
||||
readTemplate,
|
||||
type InputLinks,
|
||||
} from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
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_MasterTemplete_Inputs";
|
||||
import type { ValueDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs_Model";
|
||||
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,
|
||||
@@ -32,7 +30,8 @@ import {
|
||||
type PickerRow,
|
||||
} from "./ui_template_item_picker_data";
|
||||
import {
|
||||
loadInputs,
|
||||
laborOf,
|
||||
laborValueData,
|
||||
PART_OF,
|
||||
projectItems,
|
||||
sameItem,
|
||||
@@ -118,6 +117,7 @@ const TEXT = {
|
||||
재료: "재료산출",
|
||||
자재조합: "자재조합",
|
||||
입력연결: "선택 입력 자리",
|
||||
일위대가: "일위대가",
|
||||
} as Record<string, string>,
|
||||
};
|
||||
|
||||
@@ -137,6 +137,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
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;
|
||||
@@ -301,34 +302,36 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
opts.onEdit({ kind, row, info, items, projectId: opts.projectId });
|
||||
}
|
||||
|
||||
async function paintInputs(row: PickerRow, mine: number): Promise<void> {
|
||||
if (!inputs) return;
|
||||
const stop = inputs.loading(); // 카드 안 「불러오는 중」(M02 31-7)
|
||||
/** 일위대가 카드 · 넣을 값 카드 — 복사본이 있으면 복사본(값도) · 아니면 원본 층(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 list = await loadInputs(row);
|
||||
let link: InputLinks | null;
|
||||
let values: ValueDoc = {};
|
||||
if (info && items) {
|
||||
// 복사본 것(가져온 때 그대로) — 값도 복사본에서
|
||||
link = (await items.read(info.폴더, "입력연결")) as InputLinks | null;
|
||||
values = ((await items.read(info.폴더, "입력값")) as ValueDoc | null) ?? {};
|
||||
} else {
|
||||
link = await readInputLinks(layerOf(row), row.key).catch(() => null);
|
||||
}
|
||||
stop();
|
||||
if (mine === seq) inputs.set({ list, link, values });
|
||||
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) {
|
||||
stop();
|
||||
if (mine === seq) inputs.set(null, why(error), () => void paintInputs(row, seq));
|
||||
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) return;
|
||||
const [cardModule, inputModule] = await Promise.all([
|
||||
if (cards && inputs && labor) return;
|
||||
const [cardModule, inputModule, laborModule] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards"),
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_Inputs"),
|
||||
import("../M02_MasterTemplete/M02_Labor_Values"),
|
||||
import("./ui_template_item_picker_labor"),
|
||||
]);
|
||||
labor = laborModule.createLaborCard();
|
||||
inputs = inputModule.createInputsValueCard({ mode: "view", onEdit: () => void edit("inputs") });
|
||||
cards = cardModule.createItemCards({
|
||||
source: () => source,
|
||||
@@ -337,6 +340,8 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
warnOnFail: () => false,
|
||||
extraCards: [inputs.root],
|
||||
});
|
||||
// 차례 = 도면 · 일위대가 · 면적산출 — 일위대가 카드를 도면 카드 바로 뒤에
|
||||
cards.root.insertBefore(labor.root, cards.root.children[1] ?? null);
|
||||
cardsHost.replaceChildren(cards.root);
|
||||
}
|
||||
|
||||
@@ -349,6 +354,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
cardsHost.replaceChildren();
|
||||
cards = null;
|
||||
inputs = null;
|
||||
labor = null;
|
||||
return;
|
||||
}
|
||||
paintHead(row);
|
||||
@@ -365,14 +371,14 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||
other = own && !info ? own : null;
|
||||
source = makeSource(row);
|
||||
await ensureCards();
|
||||
if (mine !== seq || !cards || !inputs) return;
|
||||
if (mine !== seq || !cards || !inputs || !labor) return;
|
||||
const label = !info && canCopy ? TEXT.importEdit : null;
|
||||
cards.relabel({ structure: label, area: label, material: label });
|
||||
inputs.setEditLabel(label ?? "선택 입력 수정");
|
||||
cards.root.hidden = false;
|
||||
paintStatus(row, mine);
|
||||
void cards.load({ id: row.열, label: itemName(row) });
|
||||
void paintInputs(row, mine);
|
||||
void paintLabor(row, mine);
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ import { createButton, el, showConfirmDialog, showToast } from "./ui_template_el
|
||||
import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import type { DrawingTemplateHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Drawing";
|
||||
import type { InputsValueHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs";
|
||||
import type { InputsValueHandle } from "../M02_MasterTemplete/M02_Labor_Values";
|
||||
import { pillsOf, type ValueDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs_Model";
|
||||
import type { InputLinks, StructureDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import type { EditRequest } from "./ui_template_item_picker_detail";
|
||||
import { itemName } from "./ui_template_item_picker_data";
|
||||
import { loadInputs, PART_OF } from "./ui_template_item_picker_project";
|
||||
import { laborValueData, loadInputs, PART_OF } from "./ui_template_item_picker_project";
|
||||
|
||||
export interface EditView {
|
||||
root: HTMLElement;
|
||||
@@ -146,15 +146,18 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi
|
||||
});
|
||||
}
|
||||
|
||||
/** 넣을 값 — 복사본 일위대가 담은 로직 입력 전부(39-1c) */
|
||||
async function mountInputs(): Promise<void> {
|
||||
const [{ createInputsValueCard }, list, link] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_Inputs"),
|
||||
loadInputs(row),
|
||||
linkOf(),
|
||||
const [{ createInputsValueCard }, labor, values] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_Labor_Values"),
|
||||
items.read(info.폴더, "일위대가") as Promise<{
|
||||
줄?: { 담은로직?: { 로직?: string }[] };
|
||||
} | null>,
|
||||
items.read(info.폴더, "입력값") as Promise<ValueDoc | null>,
|
||||
]);
|
||||
const values = ((await items.read(info.폴더, "입력값")) as ValueDoc | null) ?? {};
|
||||
const logics = (labor?.줄?.담은로직 ?? []).map((one) => String(one.로직 ?? "")).filter(Boolean);
|
||||
card = createInputsValueCard({ mode: "value", onChange: () => setDirty(true) });
|
||||
card.set({ list, link, values });
|
||||
card.set(await laborValueData(logics, values ?? {}));
|
||||
body.replaceChildren(
|
||||
el("div", { className: "ui-item-picker__edit-inputs", children: [card.root] }),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* =============================================================================
|
||||
* 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,20 +9,24 @@
|
||||
* GET|PUT /api/projects/{id}/items/{폴더}/{부분} → {종류, 판, 내용} · 고침은 {판, 내용}(판이 다르면 409)
|
||||
* POST …/{폴더}/reapply 재적용 → {폴더, 가져온날, 원본판, 덮임: [부분]}
|
||||
* POST …/{폴더}/save-as {to, 경로, 이름} 개인에 저장 → {key, 열, 이름, summary_item}
|
||||
* POST …/{폴더}/calc 복사본만으로 계산(계 · 빠진 입력)
|
||||
* POST …/{폴더}/calc 복사본만으로 계산(계 · 빠진 입력 · 기준합 · 반영합)
|
||||
* 입력 목록 · 항목 연결(sub4 · 25-2a) — `M02_MasterTemplete_Api_Fetch` 의 `fetchItemInputs` · `readInputLinks`
|
||||
* 일위대가(39-1c) — 복사본 `일위대가` 부분 · 원본 층은 `calcLabor` · 넣을 값 목록은 `fetchLogicInputs`
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import {
|
||||
fetchItemInputs,
|
||||
type InputLinks,
|
||||
type ItemInputs,
|
||||
} from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { calcLabor, fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
|
||||
import { layerOf, ownerOf, type PickerRow } from "./ui_template_item_picker_data";
|
||||
|
||||
/** 복사본 폴더 안 부분 — 25-1b 약속 그대로 */
|
||||
export type ProjectPart =
|
||||
"도면" | "면적" | "재료" | "자재조합" | "입력연결" | "입력값" | "고른자재";
|
||||
"도면" | "면적" | "재료" | "자재조합" | "입력연결" | "입력값" | "고른자재" | "일위대가";
|
||||
|
||||
/** 원본 종류 → 복사본 부분 */
|
||||
export const PART_OF: Record<"structure" | "area" | "material", ProjectPart> = {
|
||||
@@ -66,11 +70,13 @@ export interface CreatedItem {
|
||||
summary_item: { layer: string; owner: string | null; key: string; name: string };
|
||||
}
|
||||
|
||||
/** 복사본 계산 결과에서 모달이 보이는 것 — 계 · 빠진 입력 수 · 멈춘 로직 수 */
|
||||
/** 복사본 계산 결과에서 모달이 보이는 것 — 계 · 빠진 입력 수 · 멈춘 로직 수 · 일위대가 기준합 · 반영합 계 */
|
||||
export interface ProjectCalc {
|
||||
total: number;
|
||||
gaps: number;
|
||||
stuck: number;
|
||||
base: number | null;
|
||||
reflected: number | null;
|
||||
}
|
||||
|
||||
interface Reply {
|
||||
@@ -217,11 +223,19 @@ export function projectItems(projectId: string): ProjectItems {
|
||||
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[] };
|
||||
const got = reply.body as {
|
||||
계?: number;
|
||||
빠진입력?: unknown[];
|
||||
멈춤?: unknown[];
|
||||
기준합?: { 계?: number };
|
||||
반영합?: { 계?: number };
|
||||
};
|
||||
return {
|
||||
total: Number(got.계 ?? 0),
|
||||
gaps: got.빠진입력?.length ?? 0,
|
||||
stuck: got.멈춤?.length ?? 0,
|
||||
base: got.기준합?.계 ?? null,
|
||||
reflected: got.반영합?.계 ?? null,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -242,3 +256,67 @@ export function loadInputs(row: PickerRow): Promise<ItemInputs> {
|
||||
made.catch(() => inputsCache.delete(id));
|
||||
return made;
|
||||
}
|
||||
|
||||
/* ── 일위대가(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,
|
||||
};
|
||||
}
|
||||
|
||||
/** 넣을 값 카드 자료 — 담은 로직 입력 전부(자리 「선택」 으로 감싸 값 카드가 다 보임) */
|
||||
export async function laborValueData(
|
||||
logics: string[],
|
||||
values: ValueDoc,
|
||||
): Promise<{ list: ItemInputs; link: InputLinks; values: ValueDoc }> {
|
||||
const got = await fetchLogicInputs(logics);
|
||||
const list: ItemInputs = { 일위대가: null, 로직: got.로직, 빠진로직: got.빠진로직 };
|
||||
const 줄 = list.로직.flatMap((logic) =>
|
||||
logic.입력.map((input) => ({ 로직: logic.로직, 입력: input.이름, 자리: "선택" as const })),
|
||||
);
|
||||
return { list, link: { 판: null, 줄 }, values };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user