From 508e1ec34d5182763cd5c226c9de9a796d8e0f02 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 13:02:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02=20=EA=B8=B0=EC=A4=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D):=20=EB=A1=9C=EC=A7=81=20=EA=B3=A0=EB=A5=B4=EA=B8=B0?= =?UTF-8?q?=20=EC=B0=BD=20=3D=20=EA=B8=B0=EC=A4=80=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=ED=91=9C=20=C2=B7=20=EC=A4=84=20=EB=88=84=EB=A6=84=20=3D=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=84=9C=20=EB=B3=B4=EA=B8=B0=20=E2=86=92=20?= =?UTF-8?q?[=EB=8B=B4=EA=B8=B0]=20(PLAN=20704-1=20=C2=B7=20704-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 고르기 창 단가산출 줄 → 공용 표(품명 · 규격 · 단위 · 금액 · 출처) · 공종 거름 · 찾기 = 공용 matchLogic(규격 · 실무 낱말 별칭까지) · GF 번호 숨김 · 창 폭 960 - 기준 목록이 오기 전 · 서버 셈 도중 = 로직 줄(금액 빈칸) · 서고 · 덧줄 목록은 그대로 - 계산서 창 `M02_MasterTemplete_Labor_Sheet` — 맨 위 출처 · 노임 판 · 「이름 | 노임 × 품 | 금액」 줄 · 비목 소계 · 계(계산 줄 부품) · 곁에 원문 띠 · [담기] = 지금 담기 동작 그대로 Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_Labor.css | 27 ++++++ .../M02_MasterTemplete_Labor_Api.ts | 45 +++++++++ .../M02_MasterTemplete_Labor_Pick.ts | 95 +++++++++++++++---- .../M02_MasterTemplete_Labor_Sheet.ts | 76 +++++++++++++++ .../M02_MasterTemplete_Labor_Text.ts | 11 +++ 5 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor_Sheet.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 1c32aadc0..dd3399808 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -481,3 +481,30 @@ font-weight: 600; font-variant-numeric: tabular-nums; } + +/* 기준 목록(704-1) — 찾기 · 거름 위 · 기준 목록 표 아래(표 높이 = 창 몸에 맞춤) */ +.m02-std-pick { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-height: 0; +} + +.m02-std-pick .ui-find__list { + max-height: 10rem; +} + +/* 계산서 보기(704-2) — 계산서 | 원문 띠 */ +.m02-std-sheet { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); + gap: var(--spacing-16); + min-height: 0; +} + +.m02-std-sheet__body { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + min-width: 0; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 10e55f950..82d64ec84 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -115,6 +115,51 @@ export const fetchLibLogics = (): Promise => export const fetchLibLogicSubs = (): Promise => request<{ subs: SubBrief[] }>("/logic-lib/subs").then((d) => d.subs); +/* --- 기준 목록 · 계산서 보기(704 · 서버 M01 엔진이 셈 · `M02_Logic_Standard`) --- */ +/** 기준 목록 한 줄 — 로직 하나를 대표 규격 하나로 펼친 줄 · `바꿈` = 바탕에서 바꾼 입력 */ +export interface StandardRow { + 키: string; + 이름: string; + 규격: string; + 단위: string; + 금액: number | null; + 출처: string; + 공종: string; + 별칭: string; + 구분: string; + 상세구분: string; + 막힘: boolean; + 바꿈: Record; +} +export interface StandardList { + 판: string; + 공종: string[]; + 줄: StandardRow[]; +} +export interface StandardSheet { + 이름: string; + 규격: string; + 단위: string; + 출처: string; + 판: string; + 묶음: { + 비목: string; + 소계: number | null; + 끝수: string; + 줄: { 이름: string; 글: string; 금액: number | null }[]; + }[]; + 계: number | null; +} +let standard: Promise | null = null; // 페이지에 한 벌(서버 저장본 · 판 바뀌면 새로 고침) +export const fetchStandard = (): Promise => + (standard ??= request("/logic-lib/standard").catch((error) => { + standard = null; + throw error; + })); + +export const fetchStandardSheet = (row: StandardRow): Promise => + request("/logic-lib/sheet", "POST", { key: row.키, 바꿈: row.바꿈 }); + /* --- 일위대가 문서(39 · 구조물 열마다 `labor/<열>.json` · 틀 = sub7 39-1a) --- */ /** 손 입력 수량 — `null` 이면 면적산출 알약 연결(칸)을 따름 · 손이 칸보다 앞섬 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index d03b5c56a..a92505ab3 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -6,9 +6,13 @@ * 단가연결(251 · B08 거리이정표 · B09 설계내역서) = 종류 고르기(단가산출 · 사전조합 · 덧붙인 줄 종류) + * 덧붙일 줄(구조물 조합 · B09 호표) — `lib.extra` · `lib.kindFilter` · 늦게 받는 덧줄 `lib.more` * (324-4 실무 호표 · `withHopyo`) + * 704-1 단가산출 줄 = 기준 목록 표(품명 · 규격 · 단위 · 금액 · 출처 · 대표 규격 펼침 · 공종 거름 · + * 실무 낱말 = 서버가 별칭에 덧붙임) · 금액 = 서버 M01 엔진(`/logic-lib/standard`) · 오기 전엔 로직 줄만 · + * 줄 누름 = 계산서 보기(704-2 · `Labor_Sheet`) → [담기] = 지금 담기 동작(`done(로직)`) * ========================================================================== */ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; +import { createDataTable } from "@ui/ui_template_data_table"; import { createFindList } from "@ui/ui_template_find_list"; import { aliasNote, matchLogic } from "@ui/ui_template_logic_find"; import { t as L } from "@ui/ui_template_locale"; @@ -19,8 +23,11 @@ import { fetchLibLogics as fetchLogics, fetchLibLogicSubs as fetchLogicSubs, fetchLibrary, + fetchStandard, type LibraryRow, + type StandardRow, } from "./M02_MasterTemplete_Labor_Api"; +import { openStandardSheet } from "./M02_MasterTemplete_Labor_Sheet"; import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc"; import { ownOwner } from "./M02_MasterTemplete_Layer"; import { tc } from "./M02_MasterTemplete_Labor_Text"; @@ -29,6 +36,7 @@ import { fetchStructureDef } from "./StructOptions/M02_SO_Api"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀 import "./M02_MasterTemplete_Labor.css"; import { openModal } from "@ui/ui_template_modal"; +import { createText } from "@ui/ui_template_text"; export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick"; @@ -90,6 +98,9 @@ export function openLogicPicker( const subs: SubBrief[] = []; let classes: ReturnType = []; // 사전조합 새 분류(149-2) — 서고 줄이 있을 때만 let all: LogicSummary[] = []; + let std: StandardRow[] | null = null; // 기준 목록(서버 셈) — 오기 전 = 로직 줄 그대로 + let listed: StandardRow[] = []; + const work = createSelectField({ options: opts([]), compact: true, label: tc("Std_Kind") }); const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") }); const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") }); detail.select.disabled = true; @@ -115,19 +126,56 @@ export function openLogicPicker( /* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */ const finder = createFindList({ placeholder: tc("Pick_Search"), - filters: [...(kindSel ? [kindSel.root] : []), sub.root, detail.root], + filters: [...(kindSel ? [kindSel.root] : []), work.root, sub.root, detail.root], extra: [layerBar], }); + const table = createDataTable({ + caption: tc("Pick_Title"), + columns: [ + { key: "품명", label: tc("Std_Name"), width: 220 }, + { key: "규격", label: tc("Std_Spec"), width: 220 }, + { key: "단위", label: tc("Std_Unit"), width: 56 }, + { key: "금액", label: tc("Std_Money"), kind: "number", width: 96 }, + { key: "출처", label: tc("Std_Source"), width: 150 }, + ], + rows: [], + onRowClick: (i) => { + const row = listed[i]; + const logic = all.find((x) => x.키 === row?.키); + if (row && logic) + openStandardSheet(row, () => { + close(); + done(logic); + }); + }, + }); + /** 기준 목록이 오기 전 · 못 받으면 — 로직 하나 = 줄 하나(금액 빈칸) */ + const plain = (x: LogicSummary): StandardRow => ({ + 키: x.키, + 이름: x.이름, + 규격: "", + 단위: x.결과단위.replace(/^원\/?/, ""), + 금액: null, + 출처: x.출처 ?? "", + 공종: "", + 별칭: x.별칭 ?? "", + 구분: x.구분, + 상세구분: x.상세구분, + 막힘: x.blocked, + 바꿈: {}, + }); if (pre?.query) finder.search.value = pre.query; const find = (): void => { const q = finder.search.value.trim().toLowerCase(); - const found = all.filter( + const rows = std ?? all.map(plain); + const found = rows.filter( (x) => kindOk("logic") && + (!work.select.value || x.공종 === work.select.value) && (!sub.select.value || x.구분 === sub.select.value) && (!detail.select.value || x.상세구분 === detail.select.value) && - matchLogic(finder.search.value, x).ok, // 이름 · 키 · 별칭 …(공용 · 208-1) + matchLogic(finder.search.value, { ...x, 이름: `${x.이름} ${x.규격}` }).ok, // 공용 · 208-1 ); const libFound = libRows.filter( (x) => @@ -142,27 +190,25 @@ export function openLogicPicker( !sub.select.value && (!q || `${x.tag} ${x.name} ${x.sub}`.toLowerCase().includes(q)), ); - const total = all.length + libRows.length + (lib?.extra?.length ?? 0) + later.length; + const total = rows.length + libRows.length + (lib?.extra?.length ?? 0) + later.length; const shown = found.length + libFound.length + extra.length; finder.hint.textContent = loading ? tc("Loading") : shown ? `${shown} / ${total}` : tc("Pick_None"); - finder.setRows([ - ...found.slice(0, 100).map((x) => ({ - key: x.키, - tag: tc("Kind_Logic"), - name: x.이름, - sub: [`${x.키} · ${x.구분} ${x.상세구분}`.trim(), aliasNote(matchLogic(q, x).alias)] - .filter(Boolean) - .join(" · "), - unit: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}`, - onPick: () => { - close(); - done(x); - }, + listed = found; + table.root.hidden = !!lib?.libOnly; + table.setRows( + found.map((x) => ({ + 품명: [x.이름, aliasNote(matchLogic(q, x).alias)].filter(Boolean).join(" · "), + 규격: x.규격, + 단위: `${x.단위}${x.막힘 ? ` · ${tc("Pick_Blocked")}` : ""}`, + 금액: x.금액, + 출처: x.출처, })), + ); + finder.setRows([ ...libFound.map((x) => { const at = scope as Scope; return { @@ -246,6 +292,7 @@ export function openLogicPicker( find(); }); detail.select.addEventListener("change", find); + work.select.addEventListener("change", find); kindSel?.select.addEventListener("change", find); finder.search.addEventListener("input", find); @@ -272,8 +319,15 @@ export function openLogicPicker( title: lib?.title ?? tc("Pick_Title"), closeLabel: tc("Pick_Close"), dialogClass: "m01-logic__pick m01-combo", + width: "960px", // 기준 목록 표 다섯 칸(704-1) backdropClass: "m01-logic__backdrop", - mount: (body) => body.append(finder.root), + mount: (body) => + body.append( + el("div", { + className: "m02-std-pick", + children: [finder.root, createText(tc("Std_Hint")), table.root], + }), + ), })); finder.search.focus(); @@ -288,6 +342,11 @@ export function openLogicPicker( all = l; drawSubs(); find(); + return fetchStandard().then((got) => { + std = got.줄.length ? got.줄 : null; // 빈 줄 = 서버가 아직 셈 — 로직 줄 그대로 + work.setOptions(opts(got.공종), ""); + find(); + }); }) .catch((error) => showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Sheet.ts new file mode 100644 index 000000000..1c22a7caa --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Sheet.ts @@ -0,0 +1,76 @@ +/* ============================================================================= + * M02_MasterTemplete_Labor_Sheet.ts + * 계산서 보기(704-2) — 기준 목록 줄을 누르면 담기 전에 한 장: 맨 위 「출처 · 판」 · 비목마다 + * 「이름 | 노임 × 품 | 금액」 줄 · 소계 · 계(서버 M01 엔진 값 · `/logic-lib/sheet`) · 곁에 원문 띠 · + * [담기] = 부르는 쪽 담기 동작 그대로(`put`). + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { createCalcFormula, createCalcFormulaRow } from "@ui/ui_template_calc_row"; +import { openModal } from "@ui/ui_template_modal"; +import { createText } from "@ui/ui_template_text"; +import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; +import { fetchStandardSheet, type StandardRow } from "./M02_MasterTemplete_Labor_Api"; +import { tc } from "./M02_MasterTemplete_Labor_Text"; + +const won = (v: number | null, digits = 0): string => + v === null ? "-" : v.toLocaleString("ko-KR", { maximumFractionDigits: digits }); + +const out = (v: number | null, key = false, digits = 0): HTMLElement => + el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: won(v, digits) }); + +export function openStandardSheet(row: StandardRow, put: () => void): void { + const sheet = el("div", { className: "m02-std-sheet__body", attrs: { "data-std-sheet": "" } }); + sheet.append(createText(tc("Loading"))); + const source = createSourceView({ + path: "/m02/logic-lib/source", + pdf: "/m02/logic-lib/source-pdf", + }); + let close = (): void => {}; + const add = createButton({ + label: tc("Std_Put"), + variant: "filled", + onClick: () => { + close(); + put(); + }, + }); + ({ close } = openModal({ + title: `${tc("Std_Sheet")} — ${row.이름}`, + closeLabel: tc("Pick_Close"), + width: "1100px", + wide: true, + actions: [add], + mount: (body) => + body.append(el("div", { className: "m02-std-sheet", children: [sheet, source.root] })), + })); + void fetchStandardSheet(row).then( + (got) => { + const head = [got.이름, got.규격, got.단위].filter(Boolean).join(" · "); + const lines: HTMLElement[] = [ + createText(head, "muted", 14), + createText([got.출처, got.판].filter(Boolean).join(" · "), "hint"), + ]; + for (const g of got.묶음) { + for (const line of g.줄) { + const text = line.글.replace(/ = [^=]*$/, "").replaceAll(" * ", " × "); + lines.push( + createCalcFormulaRow(line.이름, createCalcFormula(text), out(line.금액, false, 1)), + ); + } + lines.push( + createCalcFormulaRow(`${g.비목}${g.끝수 ? ` (${g.끝수})` : ""}`, null, out(g.소계, true)), + ); + } + lines.push( + createCalcFormulaRow(`${tc("Std_Sum")} (원/${got.단위 || "식"})`, null, out(got.계, true)), + ); + sheet.replaceChildren(...lines); + source.show(got.출처); + }, + (error) => { + sheet.replaceChildren(createText(tc("Load_Failed"), "error")); + showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); + }, + ); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index e5b3b3ce6..b379755f3 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -57,6 +57,17 @@ const TEXT = { Pick_None: ["찾은 로직 없음", "No logics found"], Pick_Close: ["닫기", "Close"], Pick_Blocked: ["막힘", "Blocked"], + // 기준 목록 · 계산서 보기(704) + Std_Name: ["품명", "Item"], + Std_Spec: ["규격", "Spec"], + Std_Unit: ["단위", "Unit"], + Std_Money: ["금액", "Amount"], + Std_Source: ["출처", "Source"], + Std_Kind: ["공종", "Work type"], + Std_Sheet: ["계산서", "Cost sheet"], + Std_Put: ["담기", "Add"], + Std_Sum: ["계", "Total"], + Std_Hint: ["줄을 누르면 계산서 · [담기]", "Click a row for its sheet · [Add]"], Prev_Labor: ["노무비", "Labor"], Prev_Material: ["재료비", "Material"], Prev_Expense: ["경비", "Expense"],