diff --git a/resources/tester/helper_item_picker_project.cjs b/resources/tester/helper_item_picker_project.cjs index 55ad7a213..fcdf04a53 100644 --- a/resources/tester/helper_item_picker_project.cjs +++ b/resources/tester/helper_item_picker_project.cjs @@ -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()); diff --git a/resources/tester/test_item_picker_detail.py b/resources/tester/test_item_picker_detail.py index e0b785b12..a5b0bf6fa 100644 --- a/resources/tester/test_item_picker_detail.py +++ b/resources/tester/test_item_picker_detail.py @@ -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_모달은_원본_층에_쓰는_길을_안_부름(): diff --git a/ui_template/ui_template_item_picker.css b/ui_template/ui_template_item_picker.css index 805e228a6..efe0fa5d9 100644 --- a/ui_template/ui_template_item_picker.css +++ b/ui_template/ui_template_item_picker.css @@ -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); +} diff --git a/ui_template/ui_template_item_picker_detail.ts b/ui_template/ui_template_item_picker_detail.ts index 2f7ba2cf8..681fdc461 100644 --- a/ui_template/ui_template_item_picker_detail.ts +++ b/ui_template/ui_template_item_picker_detail.ts @@ -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, }; @@ -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 { - if (!inputs) return; - const stop = inputs.loading(); // 카드 안 「불러오는 중」(M02 31-7) + /** 일위대가 카드 · 넣을 값 카드 — 복사본이 있으면 복사본(값도) · 아니면 원본 층(39-1c) */ + async function paintLabor(row: PickerRow, mine: number): Promise { + 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 { - 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); })(); } diff --git a/ui_template/ui_template_item_picker_edit.ts b/ui_template/ui_template_item_picker_edit.ts index 9aa0b4b62..0140c9b1f 100644 --- a/ui_template/ui_template_item_picker_edit.ts +++ b/ui_template/ui_template_item_picker_edit.ts @@ -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 { - 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, ]); - 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] }), ); diff --git a/ui_template/ui_template_item_picker_labor.ts b/ui_template/ui_template_item_picker_labor.ts new file mode 100644 index 000000000..69f14eaf1 --- /dev/null +++ b/ui_template/ui_template_item_picker_labor.ts @@ -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")] : []), + ); + }, + }; +} diff --git a/ui_template/ui_template_item_picker_project.ts b/ui_template/ui_template_item_picker_project.ts index 27366224d..b3f46bb7a 100644 --- a/ui_template/ui_template_item_picker_project.ts +++ b/ui_template/ui_template_item_picker_project.ts @@ -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 { 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 { + 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 }; +}