feat(M02 기준 목록): 로직 고르기 창 = 기준 목록 표 · 줄 누름 = 계산서 보기 → [담기] (PLAN 704-1 · 704-2)
- 고르기 창 단가산출 줄 → 공용 표(품명 · 규격 · 단위 · 금액 · 출처) · 공종 거름 · 찾기 = 공용 matchLogic(규격 · 실무 낱말 별칭까지) · GF 번호 숨김 · 창 폭 960 - 기준 목록이 오기 전 · 서버 셈 도중 = 로직 줄(금액 빈칸) · 서고 · 덧줄 목록은 그대로 - 계산서 창 `M02_MasterTemplete_Labor_Sheet` — 맨 위 출처 · 노임 판 · 「이름 | 노임 × 품 | 금액」 줄 · 비목 소계 · 계(계산 줄 부품) · 곁에 원문 띠 · [담기] = 지금 담기 동작 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,51 @@ export const fetchLibLogics = (): Promise<LogicSummary[]> =>
|
||||
export const fetchLibLogicSubs = (): Promise<SubBrief[]> =>
|
||||
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<string, string | number>;
|
||||
}
|
||||
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<StandardList> | null = null; // 페이지에 한 벌(서버 저장본 · 판 바뀌면 새로 고침)
|
||||
export const fetchStandard = (): Promise<StandardList> =>
|
||||
(standard ??= request<StandardList>("/logic-lib/standard").catch((error) => {
|
||||
standard = null;
|
||||
throw error;
|
||||
}));
|
||||
|
||||
export const fetchStandardSheet = (row: StandardRow): Promise<StandardSheet> =>
|
||||
request("/logic-lib/sheet", "POST", { key: row.키, 바꿈: row.바꿈 });
|
||||
|
||||
/* --- 일위대가 문서(39 · 구조물 열마다 `labor/<열>.json` · 틀 = sub7 39-1a) --- */
|
||||
|
||||
/** 손 입력 수량 — `null` 이면 면적산출 알약 연결(칸)을 따름 · 손이 칸보다 앞섬 */
|
||||
|
||||
@@ -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<typeof precomboClasses> = []; // 사전조합 새 분류(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"),
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user