Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -94,10 +94,6 @@ const TEXT = {
|
||||
"저장 안 한 고침이 있음 — [저장] 또는 [되돌림] 먼저",
|
||||
"Save or discard edits first",
|
||||
],
|
||||
No_Key: [
|
||||
"저장한 단가산출만 수식 문서를 엶 — [단가산출 상세] 탭",
|
||||
"Saved items only — use the detail tab",
|
||||
],
|
||||
Read_Only: ["읽기만", "Read only"],
|
||||
Tools: ["수식 도구", "Equation tools"],
|
||||
Tool_Frac: ["분수", "Fraction"],
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* ㉢ 잡품 = 재료 줄(재료 · 유가 고르기 · 수량 · 율 % · 기본 = 밑 재료 줄 · 품셈 잡재료율)
|
||||
* 고침 = 문서 칸 `기계덮음`(캐시 · [저장] 때 서버가 정본으로 다시 셈) · 원본과 같으면 지움(`putCover`)
|
||||
* 기본값 = 덮음 없이 셈한 그 기계 식 답(`defaultsOf`) · 읽기(M01 기초) = 칸 잠김 · 단추 없음
|
||||
* 기계 표 꼴(`sheet` · 840-2 기계 보기 `line_stack_machine_sheet`) = 조종원 한 줄까지 · 잡품 = 밑 줄 고정(율 하나) ·
|
||||
* `lock` = 잠긴 값 칸(품셈 방식 계수 등)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
@@ -42,6 +44,10 @@ export interface CtlHooks {
|
||||
name: (key: string) => string;
|
||||
/** 읽기(M01 기초 · 칸 잠김) */
|
||||
readOnly: boolean;
|
||||
/** 기계 표 꼴(840-2) — 기계 줄 칸에 담기는 만큼만(조종원 한 줄 · 잡품 율 하나 · 재료 고르기 없음) */
|
||||
sheet?: boolean;
|
||||
/** 잠긴 값 칸 */
|
||||
lock?: string[];
|
||||
}
|
||||
|
||||
/** 기계 칸 단위(기계 표 `단위` 와 같음) */
|
||||
@@ -116,6 +122,7 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
const { machine, sub, vals, base } = got;
|
||||
const entry: MachineCover = got.entry ?? {};
|
||||
const ro = h.readOnly;
|
||||
const table = !!h.sheet;
|
||||
const put = (next: MachineCover): void => h.change((d) => putCover(d, machine, next, base));
|
||||
const parts = partsOf(sub);
|
||||
const lines = sub.금액 ?? [];
|
||||
@@ -125,6 +132,7 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
const was = base.값[col];
|
||||
const mine = entry.값?.[col];
|
||||
const now = mine ?? was;
|
||||
const shut = ro || !!h.lock?.includes(col);
|
||||
const setVal = (v: number | string | null): void => {
|
||||
const 값 = { ...(entry.값 ?? {}) };
|
||||
if (v === null || v === "") delete 값[col];
|
||||
@@ -140,11 +148,11 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
value: String(now ?? ""),
|
||||
compact: true,
|
||||
size: 24,
|
||||
disabled: ro,
|
||||
disabled: shut,
|
||||
ariaLabel: col,
|
||||
onChange: (x) => setVal(x),
|
||||
}).root
|
||||
: numBox(col, now, UNIT[col] ?? "", ro, setVal);
|
||||
: numBox(col, now, UNIT[col] ?? "", shut, setVal);
|
||||
const row = el("div", {
|
||||
className: "lstk-frow",
|
||||
children: [
|
||||
@@ -157,7 +165,7 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
const tag = createBadge(lt("Ctl_Changed"), "info", { size: 18 });
|
||||
tag.title = `${lt("Ctl_Was")} ${was === undefined ? "—" : eqText(was)}`;
|
||||
row.append(tag);
|
||||
if (!ro)
|
||||
if (!shut)
|
||||
row.append(
|
||||
createButton({
|
||||
label: lt("Ctl_Back"),
|
||||
@@ -230,21 +238,30 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
lineRow(
|
||||
lt("K_재료"),
|
||||
miscName(r),
|
||||
ro,
|
||||
ro || table,
|
||||
() => pickMat((p) => setMisc(swapAt(misc, i, asMat(r, p))), r.키 ? miscName(r) : ""),
|
||||
[
|
||||
numBox(lt("Qty"), r.수량, r.단위 ?? "", ro, (n) => {
|
||||
const { 수량: _, ...rest } = r;
|
||||
setMisc(swapAt(misc, i, n === null ? rest : { ...r, 수량: n }));
|
||||
}, r.줄 && r.수량 === undefined ? `${baseQty(r) ?? ""} ${lt("Ctl_Follow")}` : ""), // prettier-ignore
|
||||
numBox(lt("Ctl_Rate"), r.율, "%", ro, (n) => setMisc(swapAt(misc, i, { ...r, 율: n ?? 0 }))), // prettier-ignore
|
||||
...(table
|
||||
? []
|
||||
: [numBox(lt("Qty"), r.수량, r.단위 ?? "", ro, (n) => {
|
||||
const { 수량: _, ...rest } = r;
|
||||
setMisc(swapAt(misc, i, n === null ? rest : { ...r, 수량: n }));
|
||||
}, r.줄 && r.수량 === undefined ? `${baseQty(r) ?? ""} ${lt("Ctl_Follow")}` : "")]), // prettier-ignore
|
||||
numBox(
|
||||
lt("Ctl_Rate"),
|
||||
r.율,
|
||||
"%",
|
||||
ro,
|
||||
(n) =>
|
||||
setMisc(table ? misc.map((x) => ({ ...x, 율: n ?? 0 })) : swapAt(misc, i, { ...r, 율: n ?? 0 })), // prettier-ignore
|
||||
),
|
||||
],
|
||||
"재료비",
|
||||
() => setMisc(misc.filter((_, k) => k !== i)),
|
||||
),
|
||||
);
|
||||
|
||||
const head = (label: string, covered: boolean, add: () => void, reset: () => void) =>
|
||||
const head = (label: string, covered: boolean, add: (() => void) | null, reset: () => void) =>
|
||||
el("div", {
|
||||
className: "lstk-fhead",
|
||||
children: [
|
||||
@@ -253,7 +270,7 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
...(ro
|
||||
? []
|
||||
: [
|
||||
createIconButton({ icon: "plus", title: `${label} ${lt("Ctl_Add")}`, size: 24, onClick: add }), // prettier-ignore
|
||||
...(add ? [createIconButton({ icon: "plus", title: `${label} ${lt("Ctl_Add")}`, size: 24, onClick: add })] : []), // prettier-ignore
|
||||
...(covered
|
||||
? [createButton({ label: lt("Ctl_Back"), variant: "ghost", size: 24, iconStart: "undo", onClick: reset })] // prettier-ignore
|
||||
: []),
|
||||
@@ -270,14 +287,21 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
head(
|
||||
lt("Ctl_Crew"),
|
||||
entry.조종원 !== undefined,
|
||||
() => openStackPick("인력", (p) => setCrew([...crew, { 키: p.키, 이름: p.이름, 인: 1 }])),
|
||||
table && crew.length
|
||||
? null
|
||||
: () =>
|
||||
openStackPick("인력", (p) => setCrew([...crew, { 키: p.키, 이름: p.이름, 인: 1 }])),
|
||||
() => put({ ...entry, 조종원: undefined }),
|
||||
),
|
||||
...(crewRows.length ? crewRows : [el("div", { className: "lstk-empty", text: lt("Ctl_None") })]), // prettier-ignore
|
||||
head(
|
||||
lt("Ctl_Misc"),
|
||||
entry.잡품 !== undefined,
|
||||
() => pickMat((p) => setMisc([...misc, asMat({ 율: 100 }, p)]), ""),
|
||||
!table
|
||||
? () => pickMat((p) => setMisc([...misc, asMat({ 율: 100 }, p)]), "")
|
||||
: parts.base.length && !misc.length
|
||||
? () => setMisc(parts.base.map((k) => ({ 줄: String(lines[k].이름 ?? ""), 율: 0 })))
|
||||
: null,
|
||||
() => put({ ...entry, 잡품: undefined }),
|
||||
),
|
||||
...(miscRows.length ? miscRows : [el("div", { className: "lstk-empty", text: lt("Ctl_None") })]), // prettier-ignore
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/* 기계 보기(840-2) — 비슷한 기계 찾기 목록 높이만 · 색 · 크기는 공통 값 */
|
||||
.lstk-msheet-similar .ui-find__list {
|
||||
max-height: 240px;
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
/* =============================================================================
|
||||
* line_stack_machine_sheet.ts
|
||||
* 기계 보기(PLAN 840-2 · 요구 853 · 862 「기계 보기 공용」) — 기계 한 대를 단가산출 수식 보기와 닮은 꼴로:
|
||||
* 왼쪽 = 기계 제어 구역(836-1 `line_stack_machine_ctl` 그대로 · 기계 표 꼴 `sheet` — 값 칸 · 조종원 인력 줄 · 잡품 줄) +
|
||||
* 「기본 정보 · 손료 근거」 접이 카드(이름 · 규격 · 손료 방식 · 품셈 비율 · 식이 안 읽는 값 칸) + 비슷한 기계에서 시작(새 기계)
|
||||
* 오른쪽 = 그 기계 시간당 경비 산출근거(`line_stack_out` · 노 · 재 · 경 · 계 합계 상자) — 값을 바꾸면 브라우저 계산기로 즉시
|
||||
* 서버(`POST /api/m02/machine-sheet` · `M02_MyMachine_Sheet`) = 식 고름 + 처음 줄 값을 박은 식 문서 + 연결 기록 ·
|
||||
* 고친 값은 덮음(`기계덮음` 꼴)으로 얹어 셈 · 캐시에 없는 연결 · 식이 바뀔 칸만 뒤에서 다시 물음(옛 요청 끊음)
|
||||
* 줄 = 저장 꼴(M01 기계 줄 · 내 기계 줄) — 덮음 ↔ 줄은 `common_util_machine_sheet` · 저장은 부르는 쪽(M01 상세 · 내 기계)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { createFindList } from "@ui/ui_template_find_list";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { attachPickSearch } from "@ui/ui_template_pick_search";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import type { FormulaDoc, Link } from "../common_util/common_util_formula_doc";
|
||||
import { calcDoc, type DocAnswer } from "../common_util/common_util_formula_doc_calc";
|
||||
import { createDocCache, type DocRecord } from "../common_util/common_util_formula_doc_cache";
|
||||
import {
|
||||
COVER,
|
||||
coverDoc,
|
||||
coverOf,
|
||||
defaultsOf,
|
||||
partsOf,
|
||||
putCover,
|
||||
type CoverParts,
|
||||
type MachineCover,
|
||||
} from "../common_util/common_util_formula_doc_cover";
|
||||
import type { StackDoc } from "../common_util/common_util_line_stack";
|
||||
import {
|
||||
MACHINE_COEFS,
|
||||
MACHINE_INPUTS,
|
||||
machineCoefOf,
|
||||
} from "../common_util/common_util_machine_coef";
|
||||
import {
|
||||
applyCover,
|
||||
bindDoc,
|
||||
coverFrom,
|
||||
engineVals,
|
||||
jobCode,
|
||||
modeOf,
|
||||
ownedCols,
|
||||
rangeWarnings,
|
||||
sameVal,
|
||||
slotOf,
|
||||
type MachineMode,
|
||||
type MachinePick,
|
||||
type MachineRow,
|
||||
type MachineValues,
|
||||
} from "../common_util/common_util_machine_sheet";
|
||||
import { machineCellName } from "../common_util/common_util_machine_symbol_name";
|
||||
import { docFoot } from "./formula_doc_foot";
|
||||
import { buildMachineCtl } from "./line_stack_machine_ctl";
|
||||
import { buildOut } from "./line_stack_out";
|
||||
import "./line_stack.css";
|
||||
import "./line_stack_machine_sheet.css";
|
||||
|
||||
export interface MachineSimilar {
|
||||
key: string;
|
||||
name: string;
|
||||
spec: string;
|
||||
/** 흐린 꼬리표(구분 · 내 기계 등) */
|
||||
tag?: string;
|
||||
row: MachineRow;
|
||||
}
|
||||
|
||||
export interface MachineSheetOptions {
|
||||
/** 처음 줄(저장본 · 새 기계 = 빈 줄) — 고친 칸 표지 · 되돌림 기준 */
|
||||
orig: MachineRow;
|
||||
/** 지금 줄(초안) · 없으면 처음 줄 */
|
||||
cur?: MachineRow;
|
||||
/** 내 기계(방식 바로 · 품셈 · 직접 · 그 사람 개인 층 기계 표) */
|
||||
mine: boolean;
|
||||
readOnly: boolean;
|
||||
lists: { fuels: MachinePick[]; jobs: MachinePick[] };
|
||||
findSimilar?: (q: string) => Promise<MachineSimilar[]>;
|
||||
onSimilar?: (item: MachineSimilar) => void;
|
||||
onChange?: () => void;
|
||||
}
|
||||
|
||||
export interface MachineSheetState {
|
||||
name: string;
|
||||
spec: string;
|
||||
/** 지금 줄(이름 · 규격 넣음) */
|
||||
row: MachineRow;
|
||||
mode: MachineMode;
|
||||
/** 숫자 아닌 칸 — 있으면 저장 막음 */
|
||||
errors: string[];
|
||||
changed: boolean;
|
||||
}
|
||||
|
||||
interface SheetInfo {
|
||||
키: string | null;
|
||||
이름?: string;
|
||||
까닭?: string;
|
||||
문서: FormulaDoc | null;
|
||||
/** 처음 줄 셈 꼴 값(식에 박을 값) */
|
||||
값?: MachineValues;
|
||||
연결?: DocRecord;
|
||||
직종: Record<string, string>;
|
||||
}
|
||||
|
||||
/** 늘 보이는 기계 줄 칸(값이 없어도) — 그 밖 칸은 줄에 있으면 */
|
||||
const BASE_KEYS = [
|
||||
"가격",
|
||||
"손료계수",
|
||||
"주연료",
|
||||
"주연료종류",
|
||||
"잡재료율",
|
||||
"조종원",
|
||||
"조종원직종",
|
||||
"전력",
|
||||
];
|
||||
const UNIT: Record<string, string> = {
|
||||
가격: "천원", 손료계수: "10⁻⁷/hr", 내용시간: "시간", 연간표준가동시간: "시간", 주연료: "ℓ/hr",
|
||||
잡재료율: "%", 조종원: "인", 전력: "kWh",
|
||||
}; // prettier-ignore
|
||||
const PICKS = ["주연료종류", "조종원직종"];
|
||||
/** 식이 바뀔 수 있는 칸(서버 식 고름) */
|
||||
const PICKY = ["주연료", "조종원", "조종원직종", "전력", "손료계수", ...MACHINE_COEFS];
|
||||
|
||||
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v ?? null)) as T;
|
||||
const text = (v: unknown): string => (v === null || v === undefined ? "" : String(v));
|
||||
const label = (k: string): string => machineCellName(k);
|
||||
|
||||
export function createMachineSheet(o: MachineSheetOptions) {
|
||||
const mine = o.mine;
|
||||
const ro = o.readOnly;
|
||||
let orig: MachineRow = clone(o.orig);
|
||||
let cur: MachineRow = clone(o.cur ?? o.orig);
|
||||
let name = text(cur.이름);
|
||||
let spec = text(cur.규격);
|
||||
const start = JSON.stringify([name, spec, cur]);
|
||||
const no = text(cur.원문번호) || text(cur.키) || "새 기계";
|
||||
const given = { 기계: no };
|
||||
const link: Link = { 갈래: "단가산출", 입력: { 기계: `'${no.replace(/'/g, "")}'` } };
|
||||
const holder = { 꼴: "수식문서", 변수: [], 식: [], 금액: [] } as unknown as StackDoc;
|
||||
const cache = createDocCache();
|
||||
const opened = new Set<string>();
|
||||
const errors = new Map<string, string>();
|
||||
let info: SheetInfo | null = null;
|
||||
let jobs: Record<string, string> = {};
|
||||
let parts: CoverParts | null = null;
|
||||
let base: Required<MachineCover> = { 값: {}, 조종원: [], 잡품: [] };
|
||||
|
||||
const left = el("div", { className: "lstk__left" });
|
||||
const right = el("div", { className: "lstk__right" });
|
||||
const foot = el("div", { className: "lstk__foot" });
|
||||
const notice = el("div");
|
||||
const warn = el("div");
|
||||
const ctlBox = el("div");
|
||||
const baseBox = el("div");
|
||||
const root = el("div", {
|
||||
className: "lstk",
|
||||
attrs: { "data-edit": "1", "data-machine-sheet": no },
|
||||
children: [el("div", { className: "lstk__areas", children: [left, right] }), foot],
|
||||
});
|
||||
attachCollapsible(baseBox);
|
||||
|
||||
const sub = (): FormulaDoc | null => info?.문서 ?? null;
|
||||
const entry = (): MachineCover => coverOf(holder)[no] ?? {};
|
||||
/** 값 박은 셈(덮음 얹은 뒤 · 제어 구역 기본값) */
|
||||
const plain = (d: FormulaDoc, g: Record<string, unknown>): DocAnswer =>
|
||||
raw(bindDoc(d, info?.값 ?? {}), g);
|
||||
const raw = (d: FormulaDoc, g: Record<string, unknown>): DocAnswer =>
|
||||
calcDoc(d, cache.links, g, [], new Map(), {});
|
||||
|
||||
/** 식이 바뀌거나 처음 줄 · 방식이 바뀐 뒤 — 덮음을 처음 줄 대 지금 줄로 다시 지음 */
|
||||
const rebuild = (): void => {
|
||||
delete (holder as { [COVER]?: unknown })[COVER];
|
||||
const d = sub();
|
||||
parts = d ? partsOf(d) : null;
|
||||
if (!d || !parts) return;
|
||||
base = defaultsOf(d, (plain(d, given).값 ?? {}) as Record<string, unknown>);
|
||||
putCover(holder, no, coverFrom(parts, d.금액 ?? [], orig, cur, mine), base);
|
||||
};
|
||||
|
||||
// ── 서버(뒤에서) ──
|
||||
let seq = 0;
|
||||
let timer: number | undefined;
|
||||
let ctl: AbortController | null = null;
|
||||
const ask = (now = false): void => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => void run(), now ? 0 : 300);
|
||||
};
|
||||
const run = async (): Promise<void> => {
|
||||
ctl?.abort();
|
||||
ctl = new AbortController();
|
||||
const mineSeq = ++seq;
|
||||
const body = { 줄: rowOut(), 본: orig, 덮음: coverOf(holder)[no] ?? null, 개인: mine };
|
||||
try {
|
||||
const r = await fetch(`${API_BASE_URL}/m02/machine-sheet`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: ctl.signal,
|
||||
});
|
||||
const got = (await r.json().catch(() => ({}))) as SheetInfo & { detail?: unknown };
|
||||
if (mineSeq !== seq) return;
|
||||
if (!r.ok) throw new Error(text(got.detail) || r.statusText);
|
||||
cache.merge(got.연결);
|
||||
jobs = got.직종 ?? jobs;
|
||||
if (!info || got.키 !== info.키) {
|
||||
info = got;
|
||||
rebuild();
|
||||
drawAll();
|
||||
} else {
|
||||
info.까닭 = got.까닭; // 식은 그대로 — 까닭만 지금 줄로
|
||||
drawLive();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mineSeq !== seq || (e as Error).name === "AbortError") return;
|
||||
info = { 키: null, 까닭: e instanceof Error ? e.message : "", 문서: null, 직종: jobs };
|
||||
parts = null;
|
||||
drawAll();
|
||||
}
|
||||
};
|
||||
|
||||
const rowOut = (): MachineRow => ({ ...cur, 이름: name.trim(), 규격: spec.trim() });
|
||||
const changed = (refit = false): void => {
|
||||
o.onChange?.();
|
||||
drawLive();
|
||||
ask(refit);
|
||||
};
|
||||
|
||||
// ── 품셈 비율 → 계수(같으면 처음 값 · 제어 구역 칸은 덮음으로) ──
|
||||
const recoef = (): void => {
|
||||
if (modeOf(cur, mine) !== "품셈") return;
|
||||
const v = (cur.값 ??= {});
|
||||
const same = MACHINE_INPUTS.every((k) => sameVal(v[k], orig.값?.[k]));
|
||||
const got = same ? null : machineCoefOf(v);
|
||||
if (!same && !got) return;
|
||||
const owned = parts ? ownedCols(parts) : [];
|
||||
const e = clone(entry());
|
||||
for (const k of MACHINE_COEFS) {
|
||||
const want = same ? orig.값?.[k] : got![k];
|
||||
if (owned.includes(k)) {
|
||||
e.값 = { ...(e.값 ?? {}) };
|
||||
if (want === undefined || same) delete e.값[k];
|
||||
else e.값[k] = want as number;
|
||||
} else if (want === undefined) delete v[k];
|
||||
else if (!sameVal(v[k], want)) v[k] = want;
|
||||
}
|
||||
if (parts) {
|
||||
putCover(holder, no, e, base);
|
||||
cur = applyCover(parts, orig, cur, entry(), mine, jobs);
|
||||
}
|
||||
};
|
||||
|
||||
// ── 왼쪽 ──
|
||||
const drawCtl = (): void => {
|
||||
const d = sub();
|
||||
if (!d || !info?.키) return void ctlBox.replaceChildren();
|
||||
const mode = modeOf(cur, mine);
|
||||
const lock = mode === "품셈" ? [...MACHINE_COEFS] : mine && mode === "바로" ? ["정비비계수", "관리비계수"] : []; // prettier-ignore
|
||||
const zone = buildMachineCtl(
|
||||
{
|
||||
doc: holder,
|
||||
answer: { 값: {} } as unknown as DocAnswer,
|
||||
subDoc: () => d,
|
||||
calc: plain,
|
||||
change: (fn) => {
|
||||
fn(holder);
|
||||
const e = entry();
|
||||
if (e.조종원) {
|
||||
e.조종원 = e.조종원.map((r) => ({ ...r, 키: `LB:${jobCode(r.키, jobs)}` }));
|
||||
putCover(holder, no, e, base);
|
||||
}
|
||||
if (parts) cur = applyCover(parts, orig, cur, entry(), mine, jobs);
|
||||
changed();
|
||||
},
|
||||
name: (k) => cache.names.get(k) ?? "",
|
||||
readOnly: ro,
|
||||
sheet: true,
|
||||
lock,
|
||||
},
|
||||
{ ...link, 키: info.키 },
|
||||
);
|
||||
if (zone && lock.length && mode === "품셈") zone.title = L("MS_Locked");
|
||||
ctlBox.replaceChildren(...(zone ? [zone] : []));
|
||||
};
|
||||
|
||||
/** 칸 단위 — 줄 `단위` 글(일 단위 손료 등) › 기본 */
|
||||
const unitOf = (k: string): string => {
|
||||
const u = text((cur.단위 as MachineValues | undefined)?.[k]);
|
||||
return !u || u === "10^-7/시간" ? (UNIT[k] ?? "") : u;
|
||||
};
|
||||
const numField = (k: string, read = false): HTMLElement => {
|
||||
const v = cur.값?.[k];
|
||||
const unit = unitOf(k);
|
||||
const f = createInputField({ label: label(k), value: text(v), size: 28, ...(unit ? { unit } : {}) }); // prettier-ignore
|
||||
f.input.inputMode = "decimal";
|
||||
f.input.dataset.key = k;
|
||||
f.input.readOnly = ro || read;
|
||||
f.root.classList.toggle("is-changed", !sameVal(v, orig.값?.[k]));
|
||||
f.input.addEventListener("change", () => {
|
||||
const t = f.input.value.replace(/,/g, "").trim();
|
||||
const box = (cur.값 ??= {});
|
||||
const n = Number(t);
|
||||
if (t && !Number.isFinite(n)) {
|
||||
errors.set(k, `${label(k)} · ${L("MF_NumberOnly")}`);
|
||||
f.setError(L("MF_NumberOnly"));
|
||||
return o.onChange?.();
|
||||
}
|
||||
errors.delete(k);
|
||||
f.setError();
|
||||
if (!t) {
|
||||
if (k in (orig.값 ?? {})) box[k] = null;
|
||||
else delete box[k];
|
||||
} else box[k] = n;
|
||||
if ((MACHINE_INPUTS as readonly string[]).includes(k)) recoef();
|
||||
f.root.classList.toggle("is-changed", !sameVal(box[k], orig.값?.[k]));
|
||||
changed(PICKY.includes(k));
|
||||
if ((MACHINE_INPUTS as readonly string[]).includes(k)) drawBase();
|
||||
});
|
||||
return f.root;
|
||||
};
|
||||
|
||||
const pickField = (k: string): HTMLElement => {
|
||||
const list = k === "주연료종류" ? o.lists.fuels : o.lists.jobs;
|
||||
const now = text(cur.값?.[k]);
|
||||
const options = [
|
||||
{ value: "", text: L("MF_None") },
|
||||
...list.map((p) => ({ value: p.value, text: p.text })),
|
||||
];
|
||||
if (now && !list.some((p) => p.value === now)) options.push({ value: now, text: `${now} (${L("MF_NoPrice")})` }); // prettier-ignore
|
||||
const f = createSelectField({ label: label(k), options, value: now, size: 28 });
|
||||
f.select.dataset.key = k;
|
||||
f.select.disabled = ro;
|
||||
attachPickSearch(f.select, { placeholder: L("MF_PickFind") });
|
||||
f.root.classList.toggle("is-changed", !sameVal(now, orig.값?.[k]));
|
||||
f.select.addEventListener("change", () => {
|
||||
const box = (cur.값 ??= {});
|
||||
if (f.select.value) box[k] = f.select.value;
|
||||
else if (k in (orig.값 ?? {})) box[k] = orig.값![k] === "" ? "" : null;
|
||||
else delete box[k];
|
||||
f.root.classList.toggle("is-changed", !sameVal(box[k], orig.값?.[k]));
|
||||
changed(true);
|
||||
});
|
||||
return f.root;
|
||||
};
|
||||
|
||||
const setMode = (m: MachineMode): void => {
|
||||
const v = (cur.값 ??= {});
|
||||
const was = modeOf(cur, mine);
|
||||
if (m === was) return;
|
||||
if (m === "품셈") for (const k of MACHINE_INPUTS) if (orig.값?.[k] !== undefined) v[k] = clone(orig.값[k]); // prettier-ignore
|
||||
if (m === "바로" && !mine) for (const k of MACHINE_INPUTS) delete v[k];
|
||||
if (mine) {
|
||||
if (m === "바로" && blankNum(v.손료계수)) {
|
||||
const e = engineVals(cur, true);
|
||||
v.손료계수 = ["상각비계수", "정비비계수", "관리비계수"].reduce(
|
||||
(s, k) => s + (Number(e[k]) || 0),
|
||||
0,
|
||||
);
|
||||
}
|
||||
if (m === "직접" && !cur.시간당) cur.시간당 = Object.fromEntries((["노무비", "재료비", "경비"] as const).map((k) => [k, Math.round(lastAnswer?.[k] ?? 0)])); // prettier-ignore
|
||||
cur.방식 = m;
|
||||
}
|
||||
recoef();
|
||||
rebuild();
|
||||
drawAll();
|
||||
changed(true);
|
||||
};
|
||||
const blankNum = (x: unknown): boolean => typeof x !== "number" || !x;
|
||||
|
||||
const drawBase = (): void => {
|
||||
const keep = baseBox.firstElementChild?.classList.contains("is-collapsed") ?? false;
|
||||
const mode = modeOf(cur, mine);
|
||||
const owned = new Set(
|
||||
(parts && info?.키 ? ownedCols(parts) : [])
|
||||
.map((c) => slotOf(c, mode, mine))
|
||||
.flatMap((s) => (s && s[0] === "값" ? [s[1]] : [])),
|
||||
);
|
||||
const pumsem = mode === "품셈";
|
||||
const keys =
|
||||
mode === "직접"
|
||||
? []
|
||||
: [...new Set([...BASE_KEYS, ...Object.keys(cur.값 ?? {})])].filter(
|
||||
(k) => !owned.has(k) && !(MACHINE_INPUTS as readonly string[]).includes(k) && !(pumsem && (MACHINE_COEFS as readonly string[]).includes(k)), // prettier-ignore
|
||||
);
|
||||
const textBox = (
|
||||
lab: string,
|
||||
get: string,
|
||||
was: string,
|
||||
set: (v: string) => void,
|
||||
): HTMLElement => {
|
||||
const f = createInputField({ label: lab, value: get, size: 28 });
|
||||
f.input.readOnly = ro;
|
||||
f.root.classList.toggle("is-changed", get !== was);
|
||||
f.input.addEventListener("input", () => {
|
||||
set(f.input.value);
|
||||
f.root.classList.toggle("is-changed", f.input.value !== was);
|
||||
o.onChange?.();
|
||||
});
|
||||
return f.root;
|
||||
};
|
||||
const seg = createSegment({
|
||||
options: [
|
||||
{ value: "바로", label: L("MF_CoefDirect") },
|
||||
{ value: "품셈", label: L("MF_CoefPumsem") },
|
||||
...(mine ? [{ value: "직접", label: L("MF_HourlyDirect") }] : []),
|
||||
],
|
||||
value: mode,
|
||||
size: 24,
|
||||
ariaLabel: L("MS_Mode"),
|
||||
onChange: (v) => setMode(v as MachineMode),
|
||||
});
|
||||
if (ro) seg.root.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
||||
const row = (children: HTMLElement[]): HTMLElement => el("div", { className: "lstk-frow", children }); // prettier-ignore
|
||||
const card = createPanelCard(L("MS_Base"), keep);
|
||||
card.append(
|
||||
row([
|
||||
textBox(L("MF_Name"), name, text(orig.이름), (v) => (name = v)),
|
||||
textBox(L("MF_Spec"), spec, text(orig.규격), (v) => (spec = v)),
|
||||
]),
|
||||
row([el("span", { className: "lstk-kind", text: L("MS_Mode") }), seg.root]),
|
||||
...(pumsem
|
||||
? [
|
||||
row(MACHINE_INPUTS.map((k) => numField(k))),
|
||||
row([
|
||||
el("span", { className: "lstk-kind", text: L("MS_Coefs") }),
|
||||
...MACHINE_COEFS.filter((k) => !owned.has(k)).map((k) => numField(k, true)),
|
||||
]),
|
||||
]
|
||||
: []),
|
||||
...(keys.length
|
||||
? [row(keys.map((k) => (PICKS.includes(k) ? pickField(k) : numField(k))))]
|
||||
: []),
|
||||
);
|
||||
baseBox.replaceChildren(card);
|
||||
};
|
||||
|
||||
const similarBox = (): HTMLElement[] => {
|
||||
if (!o.findSimilar || ro) return [];
|
||||
const find = createFindList({ placeholder: L("MF_SimilarFind") });
|
||||
const card = createPanelCard(L("MF_Similar"), !!(orig.이름 || orig.값 && Object.keys(orig.값).length)); // prettier-ignore
|
||||
card.classList.add("lstk-msheet-similar");
|
||||
card.append(find.root);
|
||||
attachCollapsible(card);
|
||||
let n = 0;
|
||||
let wait: number | undefined;
|
||||
const look = async (): Promise<void> => {
|
||||
const me = ++n;
|
||||
const got = await o.findSimilar!(find.search.value.trim()).catch(() => []);
|
||||
if (me !== n) return;
|
||||
find.hint.textContent = got.length ? "" : L("MF_SimilarNone");
|
||||
find.setRows(
|
||||
got.map((m) => ({
|
||||
key: m.key,
|
||||
name: `${m.name} ${m.spec}`.trim(),
|
||||
tag: m.tag,
|
||||
sub: m.key,
|
||||
onPick: () => {
|
||||
orig = clone(m.row);
|
||||
cur = clone(m.row);
|
||||
name = m.name;
|
||||
spec = m.spec;
|
||||
errors.clear();
|
||||
info = null;
|
||||
parts = null;
|
||||
delete (holder as { [COVER]?: unknown })[COVER];
|
||||
card.classList.add("is-collapsed");
|
||||
setNotice(L("MF_SimilarCopied").replace("{value}", `${m.name} ${m.spec}`.trim()));
|
||||
o.onSimilar?.(m);
|
||||
drawAll();
|
||||
changed(true);
|
||||
},
|
||||
})),
|
||||
);
|
||||
};
|
||||
find.search.addEventListener("input", () => {
|
||||
window.clearTimeout(wait);
|
||||
wait = window.setTimeout(() => void look(), 250);
|
||||
});
|
||||
void look();
|
||||
return [card];
|
||||
};
|
||||
|
||||
// ── 오른쪽 · 경고 ──
|
||||
let lastAnswer: DocAnswer | null = null;
|
||||
const drawRight = (): void => {
|
||||
const d = sub();
|
||||
const head = el("div", {
|
||||
className: "lstk__bar",
|
||||
children: [
|
||||
el("strong", { text: L("MS_Out") }),
|
||||
...(info?.키 ? [el("span", { className: "ui-text ui-text--muted", text: `${info.키} ${info.이름 ?? ""}`.trim() })] : []), // prettier-ignore
|
||||
],
|
||||
});
|
||||
if (!info) {
|
||||
lastAnswer = null;
|
||||
foot.replaceChildren();
|
||||
return void right.replaceChildren(head, createText(L("MS_Loading"), "muted"));
|
||||
}
|
||||
if (!d) {
|
||||
lastAnswer = null;
|
||||
foot.replaceChildren();
|
||||
const why = info.까닭 ? ` — ${info.까닭}` : "";
|
||||
return void right.replaceChildren(head, createText(`${L("MS_NoFormula")}${why}`, "warning"));
|
||||
}
|
||||
const shaped = coverDoc(d, given, coverOf(holder));
|
||||
const a = plain(shaped, given);
|
||||
if (cache.takeMissing()) ask();
|
||||
lastAnswer = a;
|
||||
right.replaceChildren(
|
||||
head,
|
||||
...(info.까닭 ? [createText(info.까닭, "warning")] : []),
|
||||
buildOut(bindDoc(shaped, info.값 ?? {}) as StackDoc, a, {
|
||||
cache,
|
||||
open: opened,
|
||||
toggle: (at) => {
|
||||
if (opened.has(at)) opened.delete(at);
|
||||
else opened.add(at);
|
||||
drawRight();
|
||||
},
|
||||
miss: () => ask(),
|
||||
redraw: () => drawRight(),
|
||||
}),
|
||||
);
|
||||
foot.replaceChildren(docFoot(a));
|
||||
};
|
||||
const drawWarn = (): void => {
|
||||
const vals: MachineValues = { ...engineVals(cur, mine), ...(cur.값 ?? {}) };
|
||||
if (unitOf("손료계수") !== UNIT.손료계수) delete vals.손료계수; // 일 단위 손료(임업) = 시간당 범위 아님
|
||||
warn.replaceChildren(
|
||||
...rangeWarnings(vals).map(([k, r]) =>
|
||||
createText(L("MS_Range").replace("{name}", label(k)).replace("{value}", r), "warning"),
|
||||
),
|
||||
);
|
||||
};
|
||||
const drawLive = (): void => {
|
||||
drawCtl();
|
||||
drawRight();
|
||||
drawWarn();
|
||||
};
|
||||
const drawAll = (): void => {
|
||||
drawBase();
|
||||
drawLive();
|
||||
};
|
||||
const setNotice = (t: string): void => {
|
||||
notice.replaceChildren(...(t ? [createText(t, "warning")] : []));
|
||||
};
|
||||
|
||||
left.append(...similarBox(), notice, warn, ctlBox, baseBox);
|
||||
drawAll();
|
||||
ask(true);
|
||||
|
||||
return {
|
||||
root,
|
||||
setNotice,
|
||||
state: (): MachineSheetState => ({
|
||||
name: name.trim(),
|
||||
spec: spec.trim(),
|
||||
row: rowOut(),
|
||||
mode: modeOf(cur, mine),
|
||||
errors: [...errors.values()],
|
||||
changed: JSON.stringify([name, spec, cur]) !== start,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
* 찾아 넣기 = M02 [항목 추가] 창 그대로(`openFindPicker` · 탭 M01 전체 · 찾기 칸 · 표) — 줄 갈래마다 그 종류만
|
||||
* 인력(노임 LB) · 재료(자재 MT) · 기계(EQ → 시간당 경비 로직 + 기계='원문번호') · 다른 단가산출(M01 단가산출 로직)
|
||||
* 기계 + M02 개인 층(821-3) = 품셈 기계 위 「내 기계」 묶음 · 머리 [새 기계 만들기] — 고름 = 로직 + 기계='개인 EQP…'
|
||||
* 재료 + M02 개인 층(840-4) = M01 재료 위 「내 재료」 묶음 — 고름 = 요소 PM 키(M01 재료와 같은 꼴)
|
||||
* M01 단가산출 기초 줄 쌓기 = 「M01 전체」 탭만 — M01 은 원천 기준값이라 M02 단가산출 · 일위대가를 부르지 않음(요구 680-1)
|
||||
* M02 단가산출 줄 쌓기(`take` 있음) · 다른 단가산출 = 탭 [M01 전체 | M02 단가산출 | 일위대가](위 층 · 자기 SK 뺌) —
|
||||
* 고름 = 복사해 펼침(`take(src)` · src = M01 키 | `참조|층|주인|키`) · 참조 줄 없음(요구 681)
|
||||
@@ -58,6 +59,7 @@ export function openStackPick(
|
||||
title: `${lt("Find_Title")} — ${lt(copy ? "K_복사" : (`K_${kind}` as never))}`,
|
||||
kinds: copy ? ["logic", "sk", "pre"] : [want], // M01 = M01 전체만(680-1)
|
||||
mine: kind === "기계" && m02?.layer === "personal" ? { owner: m02.owner } : undefined,
|
||||
mineMat: kind === "재료" && m02?.layer === "personal" ? { owner: m02.owner } : undefined, // 840-4 내 재료
|
||||
query: query.trim() || undefined,
|
||||
ref: copy
|
||||
? (r) =>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 고침(804-7 · 요구 687-14) = [식 고치기] 걷음 — 이름 · 식 글을 누르면 그 줄만 그 자리에서 이름 · 식 · 단위 칸 + 지움 ·
|
||||
* 바꾸면 닫힘 · 머리 [+] = 식 줄 더함 — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결
|
||||
* 만약 안 탄 갈래 기호(`hide` · 요구 687-10) = 안 보임(식 글은 그대로)
|
||||
* 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold`) — 꼴이 바뀌면 「내 틀 (틀 이름 에서 변경)」 ·
|
||||
* 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold` · 입력에 기계 있는 줄은 안 함 840-2) — 꼴이 바뀌면 「내 틀 (틀 이름 에서 변경)」 ·
|
||||
* 빈 틀 = [찾아 넣기](M01 단가산출 결과 — 작업량 등) · [틀로 저장] · 내 틀 목록 걷음(요구 685) · 표 값만 = 머리 「표 값」
|
||||
* 표 값(804-3) = [표 · 칸] 단추 → 표 고르기 창(`line_stack_pick.ts`) · 값 칸 누름 = 그 줄 · 칸(`pickCell`)
|
||||
* ========================================================================== */
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type FormulaDoc,
|
||||
} from "../common_util/common_util_formula_doc";
|
||||
import { isNum } from "../common_util/common_util_formula_doc_calc";
|
||||
import { machineKey } from "../common_util/common_util_formula_doc_cover";
|
||||
import { decide, hasIf, tryParse } from "../common_util/common_util_formula_doc_view";
|
||||
import { machineText } from "./formula_doc_options";
|
||||
import {
|
||||
@@ -262,7 +263,8 @@ export function buildBlock(
|
||||
const srcTag = src === "원파일" ? lt("Src_Native") : src === "엑셀" ? lt("Src_Excel") : src;
|
||||
const tag = expr ? null : srcTag ? clipTag(srcTag) : sourceTag(h, id, link);
|
||||
const tools: HTMLElement[] = [];
|
||||
if (sub?.결과 && link?.키)
|
||||
// 기계 줄(입력에 기계) = 풀기 안 내놓음(840-2 · 요구 864 — 기계 값 고침은 제어 구역 한 길 · 풀린 옛 줄은 그대로 열림)
|
||||
if (sub?.결과 && link?.키 && !machineKey(link.입력))
|
||||
tools.push(
|
||||
createButton({
|
||||
label: lt("Unfold"),
|
||||
|
||||
@@ -23,6 +23,7 @@ from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
FILE = "기계.json"
|
||||
OWN_SOURCE = "자체"
|
||||
HOUR_PARTS = ("시간환산", "상여계수", "휴지계수") # 조종원 노임 1/8 × 16/12 × 25/20(CC000336)
|
||||
_EQ = re.compile(r"EQ\d{6}")
|
||||
_cache: dict[str, Any] = {"ver": None, "hourly": {}, "uses": None}
|
||||
|
||||
@@ -71,12 +72,14 @@ def lists() -> dict:
|
||||
if isinstance(r.get("값"), (int, float, Decimal))
|
||||
]
|
||||
head = store.read(FILE)[0]
|
||||
hour = Decimal(1) / Decimal(8)
|
||||
hour = Decimal(5) / Decimal(24) # 1/8 × 16/12 × 25/20(계수표 없을 때)
|
||||
for t in files.get("계수_건설품셈_08장_건설기계.json", {}).get("표") or []:
|
||||
if t.get("키") == "CC000336":
|
||||
got = next((x for x in t.get("줄") or [] if x.get("구분") == "시간환산"), None)
|
||||
if got:
|
||||
hour = Decimal(got["분자"]) / Decimal(got["분모"])
|
||||
if t.get("키") == "CC000336": # 기계 식 Q_시간환산 과 같은 세 줄(840-5)
|
||||
got = {x.get("구분"): x for x in t.get("줄") or []}
|
||||
if all(p in got for p in HOUR_PARTS):
|
||||
hour = Decimal(1)
|
||||
for p in HOUR_PARTS:
|
||||
hour *= Decimal(got[p]["분자"]) / Decimal(got[p]["분모"])
|
||||
return {
|
||||
"fuels": fuels,
|
||||
"jobs": jobs,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { MachinePick } from "@ui/ui_template_machine_form";
|
||||
import type { MachinePick } from "../common_util/common_util_machine_sheet";
|
||||
import type { Row } from "./M01_MasterData_Api_Fetch";
|
||||
|
||||
export interface MachineLists {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab.ts
|
||||
* 관리자 화면 — 로직 칸. 로직을 열면 두 칸 — 왼쪽 수식(줄 쌓기 `A00_Common/line_stack_screen.ts` · 804-6) · 단가산출 상세 / 오른쪽 원문
|
||||
* 셈 = 화면 칸 없이 구동(`_Logic_Drive.ts` · 799-2) — 열 때 첫 셈 · 입력 · 시험 고침 · 재료 고름이 `drive.run`
|
||||
* 관리자 화면 — 로직 칸. 로직을 열면 두 칸 — 왼쪽 수식(줄 쌓기 `A00_Common/line_stack_screen.ts` · 804-6) / 오른쪽 원문
|
||||
* 로직 목록(원문·장 고르기 · 찾기 · 막힘 거름)은 왼쪽 패널 「단가산출 로직」 컨테이너(`listHost`) 안
|
||||
*
|
||||
* 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`.
|
||||
@@ -29,18 +28,14 @@ import {
|
||||
fetchLogicUsers,
|
||||
removeLogic,
|
||||
saveFiles,
|
||||
type CalcLine,
|
||||
type TextAnswer,
|
||||
type ElementBrief,
|
||||
type LogicRow,
|
||||
type LogicSummary,
|
||||
type SaveFile,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { createDrive } from "./M01_MasterData_UI_Logic_Drive";
|
||||
import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List";
|
||||
import type { SideHandle } from "./M01_MasterData_UI_Side";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { buildLabDetail, isOwnLogic, trial } from "./M01_MasterData_UI_LogicLab_Detail";
|
||||
import { buildLabDetail } from "./M01_MasterData_UI_LogicLab_Detail";
|
||||
import { setLogicOpener } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { openCopyScreen } from "./M01_MasterData_UI_LogicLab_Copy";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
@@ -67,19 +62,11 @@ interface Opened extends Draft {
|
||||
id: string;
|
||||
row: LogicRow | null;
|
||||
original: string | null;
|
||||
prices: Record<string, ElementBrief | null>;
|
||||
reasons: string[];
|
||||
lines: CalcLine[] | null;
|
||||
text: TextAnswer | null;
|
||||
/** 마지막으로 잘 받은 읽는 식 — 멈춘 뒤에도 수량 칸 조각을 이걸로 */
|
||||
lastText?: TextAnswer;
|
||||
ownHost: HTMLElement;
|
||||
values: Record<string, string>;
|
||||
}
|
||||
|
||||
const OWN_FILE = "로직_자체.json";
|
||||
/** 시험 계산에 넘길 줄 — 시험 고침(722-2) 덮은 사본 */
|
||||
const calcRow = (row: LogicRow | null): LogicRow | null => trial.apply(row);
|
||||
|
||||
const cacheKey = "m01_logic_drafts_lab";
|
||||
const ownHost = (): HTMLElement => el("div", { className: "m01lab__own" });
|
||||
@@ -105,12 +92,9 @@ export async function mountM01LogicLab(
|
||||
): Promise<LogicHandle> {
|
||||
let drafts = loadDrafts();
|
||||
let opened: Opened | null = null;
|
||||
let calcInputs = "";
|
||||
const admin = await fetchSessionUser()
|
||||
.then((u) => u?.role === "SYSTEM_ADMIN")
|
||||
.catch(() => false);
|
||||
let recalc: number | undefined;
|
||||
const drive = createDrive();
|
||||
// 수식 = 줄 쌓기(804-2 · 804-6 옛 수식 문서 탭 걷음) — 수식 문서 · 캐시 m01_doc_drafts
|
||||
const stack = createStackScreen({
|
||||
base: "/m01/logic/doc",
|
||||
@@ -122,7 +106,6 @@ export async function mountM01LogicLab(
|
||||
const editor = el("div", { className: "m01-logic__editor" });
|
||||
// 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」
|
||||
const panes = buildLogicSide(editor);
|
||||
editor.dataset.view = "stack"; // 보기 = 수식 하나(836-3) — 「단가산출 상세」 덩이는 CSS 로 숨김
|
||||
const errors = el("div", {
|
||||
className: "ui-banner ui-banner--danger m01-logic__reasons",
|
||||
attrs: { hidden: "" },
|
||||
@@ -181,13 +164,6 @@ export async function mountM01LogicLab(
|
||||
if (opened.origKey !== null && now === opened.original) delete drafts[opened.id];
|
||||
else drafts[opened.id] = pick(opened, clone(opened.row));
|
||||
persist();
|
||||
const inputs = JSON.stringify(calcRow(opened.row)?.입력 ?? []);
|
||||
if (inputs !== calcInputs) drawCalc();
|
||||
if (isOwnLogic(opened.row)) {
|
||||
// 자체 로직 — 고친 뒤 잠시 쉬면 다시 계산해 호표 소계와 텍스트 수식을 새로 그림
|
||||
window.clearTimeout(recalc);
|
||||
recalc = window.setTimeout(() => void drive.run(), 700);
|
||||
}
|
||||
};
|
||||
|
||||
const pick = (o: Opened, row: LogicRow | null): Draft => ({
|
||||
@@ -198,32 +174,6 @@ export async function mountM01LogicLab(
|
||||
row,
|
||||
});
|
||||
|
||||
/** 구동을 지금 로직으로 (다시) 시작 — 읽는 식 미리보기 · 첫 셈 */
|
||||
const drawCalc = (): void => {
|
||||
if (!opened?.row) {
|
||||
drive.stop();
|
||||
return;
|
||||
}
|
||||
const current = opened;
|
||||
calcInputs = JSON.stringify(calcRow(current.row)?.입력 ?? []);
|
||||
drive.start({
|
||||
savedKey: current.origKey,
|
||||
file: current.file,
|
||||
row: calcRow(current.row) as LogicRow,
|
||||
dirty: () => current.id in drafts || trial.has(current.row),
|
||||
values: current.values,
|
||||
onText: (text) => {
|
||||
current.text = text;
|
||||
if (text.ok) current.lastText = text;
|
||||
drawEditor(); // [계산] 전 미리보기 응답도 왼쪽 이름 식 칸에 바로 비치게 — onLines 와 같은 자리
|
||||
},
|
||||
onLines: (lines) => {
|
||||
current.lines = lines;
|
||||
drawEditor();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const drawEditor = (): void => {
|
||||
const listing = !opened; // 고른 로직이 없으면 목록 · 있으면 호표 표 + 시험 계산
|
||||
list.root.hidden = !listing || !loaded;
|
||||
@@ -242,37 +192,19 @@ export async function mountM01LogicLab(
|
||||
row: current.row as LogicRow,
|
||||
file: current.file,
|
||||
reasons: current.reasons,
|
||||
prices: current.prices,
|
||||
lines: current.lines,
|
||||
text: current.text,
|
||||
lastText: current.lastText,
|
||||
values: current.values,
|
||||
ownHost: current.ownHost,
|
||||
onChange: touch,
|
||||
onInput: (name, v) => syncLeft(current, { [name]: v }),
|
||||
onTrial: drawCalc,
|
||||
rerun: () => void drive.run(),
|
||||
admin,
|
||||
stack: stack.root,
|
||||
});
|
||||
stack.draw(current.origKey ?? "");
|
||||
};
|
||||
|
||||
/** 단가산출 상세 옵션 입력 → 시험 입력(이름이 있는 칸만) · 바뀌면 다시 셈 */
|
||||
const syncLeft = (o: Opened, ex: Record<string, string>): void => {
|
||||
const names = new Set((calcRow(o.row)?.입력 ?? []).map((x) => x.이름));
|
||||
const changed = Object.entries(ex).filter(([k, v]) => names.has(k) && o.values[k] !== v);
|
||||
if (!changed.length) return;
|
||||
for (const [k, v] of changed) o.values[k] = v;
|
||||
void drive.run();
|
||||
};
|
||||
|
||||
const show = (next: Opened | null): void => {
|
||||
opened = next;
|
||||
panes.showSource(next?.row?.출처 ?? "");
|
||||
errors.hidden = true;
|
||||
drawEditor();
|
||||
drawCalc();
|
||||
};
|
||||
|
||||
const open = async (id: string, sub: string, key: string): Promise<void> => {
|
||||
@@ -282,12 +214,8 @@ export async function mountM01LogicLab(
|
||||
...draft,
|
||||
id,
|
||||
original: null,
|
||||
prices: {},
|
||||
reasons: [],
|
||||
lines: null,
|
||||
text: null,
|
||||
ownHost: ownHost(),
|
||||
values: {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -301,12 +229,8 @@ export async function mountM01LogicLab(
|
||||
origKey: key,
|
||||
row: draft ? (draft.row === null ? null : clone(draft.row)) : clone(one.logic),
|
||||
original: JSON.stringify(one.logic),
|
||||
prices: one.prices,
|
||||
reasons: one.reasons,
|
||||
lines: null,
|
||||
text: null,
|
||||
ownHost: ownHost(),
|
||||
values: {},
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* M01_MasterData_UI_LogicLab_Cover.ts
|
||||
* 단가산출 상세 줄 「그 자리 고침」(PLAN 722-2 · 내역 16 · 17-2) — 줄 모달 안 수량 · 단가 칸.
|
||||
* 고침 꼴 = M02 단가산출 덮개 칸과 같음(`qty` · `labor` · `material` · `expense` — `M02_Sangeun_Doc._covered` 규칙 중 품 · 단가)
|
||||
* M01 = 시험 값만 — 고친 값은 화면 메모리(로직 줄 객체 열쇠)에만 · 셈 = 줄 사본에 덮어 서버로(`/calc` · `/text` row)
|
||||
* · 정본 · 요소(노임 · 자재) · 캐시(초안) 안 바뀜 · 로직을 다시 열면 사라짐
|
||||
* M02(724) = 같은 고침 칸 · 같은 꼴을 고침 고리(`HoEdit.set`)로 받아 덮개로 보관
|
||||
* M02(724) = 고침 칸의 값을 고침 고리(`HoEdit.set`)로 받아 덮개로 보관 — M01 시험 고침은 840-1 에 걷음
|
||||
* 서버 셈 = 줄 `단가`(손 단가) · 로직 줄 `값묶음`(노 · 재 · 경) · `수량` 글 — `master_pick._priced` · `bundled` 가 읽음
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -23,47 +21,8 @@ export interface LineCover {
|
||||
export const COVER_PARTS = { labor: "노무비", material: "재료비", expense: "경비" } as const;
|
||||
export type CoverPart = keyof typeof COVER_PARTS;
|
||||
|
||||
/** 지수 꼴 없는 수 글(엔진 식 칸) — 1e-7 → 0.0000001 */
|
||||
const plainNum = (v: number): string =>
|
||||
v.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 20 });
|
||||
|
||||
/** 호표 줄(차례 열쇠)에 고침을 덮은 줄 사본 — 원본은 안 바뀜 · 덧줄은 고침 대상 밖 */
|
||||
export function applyCovers<R extends { 호표?: object[] }>(
|
||||
row: R,
|
||||
covers: Record<number, LineCover>,
|
||||
): R {
|
||||
const ho = (row.호표 ?? []).map((raw, i) => {
|
||||
const c = covers[i];
|
||||
if (!c) return raw;
|
||||
const item = raw as Record<string, unknown>;
|
||||
const out: Record<string, unknown> = { ...item };
|
||||
if (c.qty !== undefined) out.수량 = plainNum(c.qty);
|
||||
const parts = (Object.keys(COVER_PARTS) as CoverPart[]).filter((k) => c[k] !== undefined);
|
||||
if (parts.length && item.종류 === "로직") {
|
||||
const given = parts.map((k) => [COVER_PARTS[k], c[k]]);
|
||||
out.값묶음 = { ...((item.값묶음 as object) ?? {}), ...Object.fromEntries(given) };
|
||||
} else if (parts.length) out.단가 = c[parts[0]];
|
||||
return out;
|
||||
});
|
||||
return { ...row, 호표: ho };
|
||||
}
|
||||
/* ── 순수 셈 끝 ── */
|
||||
|
||||
/** 시험 고침 — 로직 줄 객체마다(다시 열면 새 객체 = 사라짐) */
|
||||
const trials = new WeakMap<object, Record<number, LineCover>>();
|
||||
export const trial = {
|
||||
of(row: object): Record<number, LineCover> {
|
||||
let got = trials.get(row);
|
||||
if (!got) trials.set(row, (got = {}));
|
||||
return got;
|
||||
},
|
||||
has: (row: object | null): boolean => !!row && Object.keys(trials.get(row) ?? {}).length > 0,
|
||||
/** 시험 계산에 넘길 줄 — 고침 없으면 원본 그대로(같은 객체) */
|
||||
apply<R extends object | null>(row: R): R {
|
||||
return row && trial.has(row) ? (applyCovers(row, trials.get(row)!) as R) : row;
|
||||
},
|
||||
};
|
||||
|
||||
const TEXT = {
|
||||
Head: ["그 자리 고침 (시험 값)", "Edit here (trial value)"],
|
||||
Note: [
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_Detail.ts
|
||||
* 「단가산출 로직」 상세 화면 — 컨테이너를 위에서 아래로
|
||||
* ① 기본정보(정본은 읽기 전용 · 비고만 고침) ② 수식(줄 쌓기 804-2 · 804-6) ③ 단가산출 상세(공용 부품 `_LogicLab_Ho.ts` — 하위 박스 · 계산식 · 박스 옵션 · 그 자리 고침)
|
||||
* 시트 · 인력/재료/경비 · 시험 계산 · 텍스트 수식 보기는 799-2 에 걷음 — 셈은 화면 칸 없이 `_Logic_Drive.ts` 구동
|
||||
* 그 자리 고침(722-2) = 시험 값만(`_LogicLab_Cover.ts` trial — 줄 사본으로 셈 · 정본 · 요소 · 캐시 안 바뀜)
|
||||
* ① 기본정보(정본은 읽기 전용 · 비고만 고침 · 자체 로직은 이름 · 단위 · 출처도) ② 수식(줄 쌓기 804-2 · 804-6)
|
||||
* ③ M02 수식 문서 없는 줄(일위대가 줄 모달 등)만 단가산출 상세 몸(`cover` — 공용 부품 `_LogicLab_Ho.ts`)
|
||||
* 단가산출 상세 보기 · 자체 로직 시트 편집기 · 시험 고침은 840-1 에 걷음 — 자체 로직도 수식(줄 쌓기)에서 만들고 고침
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import type { LogicRow } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { buildEditor } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
|
||||
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { buildHo, lineOpener, type HoContext, type HoEdit } from "./M01_MasterData_UI_LogicLab_Ho";
|
||||
import { trial } from "./M01_MasterData_UI_LogicLab_Cover";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import "./M01_MasterData_UI_LogicLab_Style.css";
|
||||
import "./M01_MasterData_UI_LogicLab_Pill.css";
|
||||
import { block } from "./M01_MasterData_UI_LogicLab_View";
|
||||
|
||||
/** 원문 절 머리 개정 연도 글 — [2008, 2020, 2025] → 「'08 · '20 · '25」(없으면 빈 글) */
|
||||
const revisionText = (row: LogicRow): string =>
|
||||
(row.개정 ?? []).map((y) => `'${String(y).slice(-2)}`).join(" · ");
|
||||
|
||||
export { trial };
|
||||
|
||||
export interface DetailContext extends HoContext {
|
||||
export interface DetailContext {
|
||||
row: LogicRow;
|
||||
file: string;
|
||||
reasons: string[];
|
||||
/** 자체 로직 편집기 자리 — 다시 그려도 같은 칸을 씀(고치던 값이 안 날아감) */
|
||||
/** 기본정보 접힘 기억 자리 — 다시 그려도 같은 칸을 씀 */
|
||||
ownHost: HTMLElement;
|
||||
onChange: () => void;
|
||||
/** 시스템 관리자 — 기본 로직 안내 글이 다름(시트에서 고침 / 보기만) */
|
||||
admin?: boolean;
|
||||
/** 722-2 시험 고침 뒤 — 구동을 고친 줄로 다시 시작(있으면 그 자리 고침 켜짐 · 셈은 `rerun`) */
|
||||
onTrial?: () => void;
|
||||
/** 717-2b M02 단가산출 — 단가산출 상세 덩이를 이 칸으로(SK 셈 답 · 고침 표) */
|
||||
cover?: { ho: HTMLElement; open?: ReturnType<typeof lineOpener> };
|
||||
/** 717-2b M02 단가산출 — 수식 문서 없는 줄의 단가산출 상세 덩이(SK 셈 답 · 고침 표) */
|
||||
cover?: { ho: HTMLElement };
|
||||
/** 수식 탭 = 줄 쌓기 그릇(`A00_Common/line_stack_screen.ts` · 804-2 · 804-6) */
|
||||
stack?: HTMLElement;
|
||||
}
|
||||
@@ -148,88 +140,18 @@ function infoBox(ctx: DetailContext): HTMLElement[] {
|
||||
/** 자체 로직(키 GX…) — 정본은 아님 */
|
||||
export const isOwnLogic = (row: LogicRow): boolean => row.키.startsWith("GX");
|
||||
|
||||
/** 자체 로직은 정본 화면과 같은 편집기를 호표 아래에 폄(머리 칸은 기본정보에서 고치므로 숨김) */
|
||||
function ownEditor(ctx: DetailContext): HTMLElement {
|
||||
if (!ctx.ownHost.childElementCount)
|
||||
buildEditor(ctx.ownHost, {
|
||||
row: ctx.row,
|
||||
file: ctx.file,
|
||||
isNew: false,
|
||||
files: [],
|
||||
reasons: [],
|
||||
prices: ctx.prices,
|
||||
lines: ctx.lines,
|
||||
onChange: ctx.onChange,
|
||||
onFile: () => undefined,
|
||||
onPick: openPicker,
|
||||
});
|
||||
return el("details", {
|
||||
className: "m01lab__edit",
|
||||
attrs: { open: "" },
|
||||
children: [
|
||||
el("summary", { text: tl("Edit_Title") }),
|
||||
el("p", { className: "ui-text ui-text--muted", text: tl("Edit_SheetHint") }),
|
||||
ctx.ownHost,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** M01 그 자리 고침 고리 — 시험 값(줄 객체 열쇠) · 고치면 시험 계산을 다시 그리고 셈 */
|
||||
function trialEdit(ctx: DetailContext): HoEdit | undefined {
|
||||
const redraw = ctx.onTrial;
|
||||
if (!redraw) return undefined;
|
||||
const covers = trial.of(ctx.row);
|
||||
return {
|
||||
covers,
|
||||
scope: "m01",
|
||||
set: (idx, cover) => {
|
||||
if (cover) covers[idx] = cover;
|
||||
else delete covers[idx];
|
||||
redraw();
|
||||
ctx.rerun?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function hoBox(ctx: DetailContext): HTMLElement {
|
||||
const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원");
|
||||
if (!money) {
|
||||
return block(
|
||||
section(tl("Ho_Title"), [
|
||||
el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }),
|
||||
el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }),
|
||||
]),
|
||||
"ho",
|
||||
);
|
||||
}
|
||||
const body = buildHo({ ...ctx, edit: trialEdit(ctx) });
|
||||
const totalLine = body.pop()!; // 계 줄 — 자체 로직은 접힌 표 밖에 둠
|
||||
// 자체 로직 — 고치는 자리는 편집기 하나 · 읽기용 박스는 접어 둠(펼치면 소계가 보임)
|
||||
const fold = el("details", {
|
||||
className: "m01lab__fold",
|
||||
attrs: ctx.ownHost.dataset.fold ? { open: "" } : {},
|
||||
children: [el("summary", { text: tl("Ho_Fold") }), ...body],
|
||||
});
|
||||
fold.addEventListener("toggle", () => (ctx.ownHost.dataset.fold = fold.open ? "1" : ""));
|
||||
const blocks = isOwnLogic(ctx.row) ? [ownEditor(ctx), fold, totalLine] : [...body, totalLine];
|
||||
const calculated = ctx.lines !== null;
|
||||
const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`;
|
||||
return block(section(tl("Ho_Title"), blocks, hint), "ho");
|
||||
}
|
||||
|
||||
/** 상세 화면(기본정보 · 수식 · 단가산출 상세)을 `host` 에 쌓음 */
|
||||
/** 상세 화면(기본정보 · 수식 · M02 단가산출 상세 몸)을 `host` 에 쌓음 */
|
||||
export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void {
|
||||
// 다시 그리면 포커스가 빠짐 — 고치던 칸(편집기 · 기본정보)을 기억해 되돌림
|
||||
// 다시 그리면 포커스가 빠짐 — 고치던 기본정보 칸을 기억해 되돌림
|
||||
const active = document.activeElement;
|
||||
const kept =
|
||||
active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement
|
||||
? { node: active, from: active.selectionStart, to: active.selectionEnd }
|
||||
: null;
|
||||
// 보기 고르기(`_View.ts`) — 덩이마다 이름표 · 고른 덩이만 보임
|
||||
host.replaceChildren(
|
||||
...infoBox(ctx),
|
||||
...(ctx.stack ? [block(ctx.stack, "stack")] : []),
|
||||
ctx.cover ? block(section(tl("Ho_Title"), [ctx.cover.ho]), "ho") : hoBox(ctx),
|
||||
...(ctx.stack ? [ctx.stack] : []),
|
||||
...(ctx.cover ? [section(tl("Ho_Title"), [ctx.cover.ho])] : []),
|
||||
);
|
||||
const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음
|
||||
const back = own ? host.querySelector<HTMLInputElement>(`[data-own="${own}"]`) : kept?.node;
|
||||
|
||||
@@ -31,7 +31,6 @@ import { pctView } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { formatLineMoney, formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { pickMaterial } from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { coverForm, type CoverPart, type LineCover } from "./M01_MasterData_UI_LogicLab_Cover";
|
||||
import {
|
||||
condText,
|
||||
@@ -144,8 +143,6 @@ export interface HoContext {
|
||||
values: Record<string, string>;
|
||||
/** 박스 옵션 · 입력 칩 고침 — 시험 계산 입력 고침 + 다시 셈(없으면 고르기 · 칩 없음) */
|
||||
onInput?: (name: string, value: string) => void;
|
||||
/** 지금 값으로 다시 셈(구동) — 재료 후보 고르기가 씀(없으면 후보만 보임) */
|
||||
rerun?: () => void;
|
||||
/** 그 자리 고침 고리(없으면 모달은 보기만) */
|
||||
edit?: HoEdit;
|
||||
/** (M02 750-2) Ho 줄 차례 → SK 줄 cid — 읽는 식 줄을 `cid` 로 이음(이름 차례 추측 안 함) */
|
||||
@@ -279,11 +276,7 @@ function modalOf(e: Entry, ctx: HoContext, extra: { cond?: PickCond; item?: HoLi
|
||||
// 재료 줄 = 시험 계산 자재 칸과 같은 모달 — 고르면 시험 입력으로 다시 셈(정본 안 바뀜)
|
||||
kind: item?.종류,
|
||||
usedSlot: line?.품목?.값칸,
|
||||
...(item?.종류 === "재료" && ctx.pickMat
|
||||
? { onPick: ctx.pickMat(e.idx) }
|
||||
: item?.종류 === "재료" && ctx.rerun
|
||||
? { onPick: (it) => pickMaterial(ctx.values, e.idx, it, ctx.rerun!) }
|
||||
: {}),
|
||||
...(item?.종류 === "재료" && ctx.pickMat ? { onPick: ctx.pickMat(e.idx) } : {}),
|
||||
// 덧줄(%) = M02 덮개만 율 고침(M01 시험 값은 호표 줄만)
|
||||
...(edit && (e.extra ? edit.scope === "m02" : item)
|
||||
? {
|
||||
|
||||
@@ -177,12 +177,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 보기 고르기(710-2 · `_View.ts`) — 그릇 data-view 와 다른 이름표 덩이는 숨김 */
|
||||
.m01-logic__editor[data-view="stack"] [data-block]:not([data-block="stack"]),
|
||||
.m01-logic__editor[data-view="ho"] [data-block]:not([data-block="ho"]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 기본정보 — 늘 맨 위 접힌 한 줄 */
|
||||
.m01lab__infofold > summary {
|
||||
display: flex;
|
||||
|
||||
@@ -17,19 +17,11 @@ const TEXT = {
|
||||
"Base logic — view only · only the note can be edited",
|
||||
],
|
||||
Info_Own: [
|
||||
"자체 로직 — 칸을 고치면 잠시 뒤 호표 소계가 다시 계산됨",
|
||||
"Own logic — edits recalculate the subtotals shortly",
|
||||
"자체 로직 — 이름 · 단위 · 출처 · 비고는 여기서 · 식은 수식에서 고침",
|
||||
"Own logic — edit the info here · formulas in the Equation view",
|
||||
],
|
||||
Edit_Title: [
|
||||
"고치기 (호표 · 중간 값 · 덧줄 · 끝수)",
|
||||
"Edit (lines · middles · extras · rounding)",
|
||||
],
|
||||
Ho_Fold: ["묶음 소계 표 (읽기용 — 고치기는 위 편집기)", "Group subtotals (read-only)"],
|
||||
Ho_Title: ["단가산출 상세", "Unit-price detail"],
|
||||
Calc_Title: ["시험 계산", "Trial calculation"],
|
||||
View_Equation: ["수식", "Equation"],
|
||||
Edit_SheetHint: ["식은 「수식」 보기에서 고침", "Edit formulas in the Equation view"],
|
||||
View_Label: ["보기", "View"],
|
||||
G_labor: ["인력", "Labor"],
|
||||
G_material: ["재료", "Material"],
|
||||
G_cost: ["경비", "Expense"],
|
||||
@@ -46,9 +38,6 @@ const TEXT = {
|
||||
Col_Price: ["단가", "Price"],
|
||||
Col_Amount: ["금액", "Amount"],
|
||||
Empty_Group: ["줄 없음", "No lines"],
|
||||
Not_Money: ["호표가 없는 로직 — 결과 식", "No unit-cost lines — result formula"],
|
||||
Calc_First: ["금액은 셈이 끝나면 채워짐", "Amounts fill when the calculation finishes"],
|
||||
Click_Hint: ["줄을 누르면 그 줄이 쓰는 자료가 뜸", "Click a line to see the data it uses"],
|
||||
Modal_Close: ["닫기", "Close"],
|
||||
Pill_Tone: ["요소 연결", "Linked element"],
|
||||
Modal_Sum: ["한 줄 풀이", "In one line"],
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_View.ts
|
||||
* 「단가산출 로직」 상세 보기 이름표 — 보기 = 수식(줄 쌓기 `A00_Common/line_stack_screen.ts`) 하나 · 고르기 단추 없음(836-3)
|
||||
* 그릇 `data-view="stack"` 고정 — `data-block="ho"` 덩이(단가산출 상세 표)는 CSS 로 숨김
|
||||
* 덩이 = `data-block` 이름표 · 숨김 = 그릇 `data-view` + CSS(`LogicLab_Style.css`) — 다시 그려도 고름 유지
|
||||
* 셈 구동은 화면 칸 없이 `_Logic_Drive.ts` · 기본정보는 이름표 없음(늘 맨 위)
|
||||
* ========================================================================== */
|
||||
|
||||
export type LabView = "stack" | "ho";
|
||||
|
||||
/** 덩이에 보기 이름표를 붙여 돌려줌 */
|
||||
export const block = <T extends HTMLElement>(node: T, view: LabView): T => {
|
||||
node.dataset.block = view;
|
||||
return node;
|
||||
};
|
||||
@@ -1,12 +1,10 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Logic_Drive.ts
|
||||
* 셈 구동(PLAN 799-2) — 화면 없이 「입력값 → 서버 셈 → 답」을 돌림. 시험 계산 칸(`Logic_Calc`)과 로직 상세(`LogicLab`)가 같이 씀.
|
||||
* `driveRun` = 한 번 셈(`/calc` + `/text`) · `previewText` = 열 때 읽는 식 미리보기 · `autoFill` = 자동값 채움
|
||||
* `createDrive` = 로직 상세용 묶음 — 열 때 미리보기 + 첫 셈(입력 없으면 바로 · 있으면 자동값 채운 뒤) · `run` 은 칸 없이 다시 셈
|
||||
* 셈 구동(PLAN 799-2) — 화면 없이 「입력값 → 서버 셈 → 답」을 돌림. 시험 계산 칸(`Logic_Calc`)이 씀
|
||||
* `driveRun` = 한 번 셈(`/calc` + `/text`) · `autoFill` = 자동값 채움 — 로직 상세 구동은 840-1 에 걷음
|
||||
* 셈 자리 = 서버(CLAUDE.md 5 장 ③) · 화면 · sessionStorage 없음 · 고른 재료(`CalcPick`) 는 값 객체에 딸림
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchAuto,
|
||||
runCalc,
|
||||
@@ -16,8 +14,7 @@ import {
|
||||
type LogicRow,
|
||||
type TextAnswer,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { isMaterialKeyInput, pickBody } from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { pickBody } from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
|
||||
export interface DriveContext {
|
||||
/** 저장된 키 — 새 로직은 null */
|
||||
@@ -67,17 +64,6 @@ export async function driveRun(
|
||||
return { answer, draft };
|
||||
}
|
||||
|
||||
/** 열 때 읽는 식 미리보기 — [계산] 전에도 이름 식 칸이 비치게 · 늦게 온 응답은 `live()` 로 거름 */
|
||||
export function previewText(ctx: DriveContext, live: () => boolean): void {
|
||||
if (!ctx.onText) return;
|
||||
const body = { key: ctx.savedKey ?? ctx.row.키, inputs: {}, ...draftBody(ctx) };
|
||||
void runText(body).then((answer) => {
|
||||
if (!live()) return;
|
||||
(answer as TextAnswer & { 계산됨?: boolean }).계산됨 = false;
|
||||
ctx.onText?.(answer);
|
||||
});
|
||||
}
|
||||
|
||||
/** 자동값으로 빈 칸 채움 — 채웠으면 true */
|
||||
export async function autoFill(ctx: DriveContext): Promise<boolean> {
|
||||
if (ctx.savedKey === null) return false;
|
||||
@@ -88,49 +74,3 @@ export async function autoFill(ctx: DriveContext): Promise<boolean> {
|
||||
for (const n of names) ctx.values[n] = String(auto[n]);
|
||||
return names.length > 0;
|
||||
}
|
||||
|
||||
/** 로직마다 첫 셈을 한 번만(다시 그려도 안 부름) */
|
||||
const first = new WeakSet<Record<string, string>>();
|
||||
|
||||
/** 로직 상세용 구동 — `start` = 로직을 열거나 입력 칸이 바뀔 때 · `run` = 지금 값으로 다시 셈 */
|
||||
export function createDrive(): {
|
||||
start: (ctx: DriveContext) => void;
|
||||
run: () => Promise<void>;
|
||||
stop: () => void;
|
||||
} {
|
||||
let seq = 0; // 로직을 바꾸면 늦게 온 응답을 거름
|
||||
let now: DriveContext | null = null;
|
||||
let ran = false; // 셈 답이 이미 왔으면 뒤늦은 미리보기로 안 덮음
|
||||
const run = async (): Promise<void> => {
|
||||
const ctx = now;
|
||||
if (!ctx) return;
|
||||
ran = true;
|
||||
try {
|
||||
await driveRun(ctx);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
};
|
||||
const start = (ctx: DriveContext): void => {
|
||||
now = ctx;
|
||||
ran = false;
|
||||
const mine = ++seq;
|
||||
previewText(ctx, () => !ran && mine === seq);
|
||||
if (first.has(ctx.values)) return;
|
||||
if (!(ctx.row.입력 ?? []).some((spec) => !isMaterialKeyInput(spec))) {
|
||||
first.add(ctx.values); // 입력이 없으면 채울 것이 없음 — 열 때 한 번 바로 셈
|
||||
queueMicrotask(() => mine === seq && void run());
|
||||
} else if (ctx.savedKey !== null) {
|
||||
first.add(ctx.values);
|
||||
void autoFill(ctx).then((filled) => filled && mine === seq && void run());
|
||||
}
|
||||
};
|
||||
return {
|
||||
start,
|
||||
run,
|
||||
stop: () => {
|
||||
now = null;
|
||||
seq++;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_MachineDetail.ts
|
||||
* M01 기계 상세 페이지(PLAN 821-2 · 요구 790-2 · 791) — 기계 양식 부품 + 원문번호 · 원문 · 구분 ·
|
||||
* M01 기계 상세 페이지(PLAN 821-2 · 840-2 · 요구 790-2 · 791 · 862) — 기계 보기(`line_stack_machine_sheet` ·
|
||||
* 왼쪽 제어 구역 + 오른쪽 시간당 경비 산출근거 · 고침 = M01 기계 줄 정본 값) + 원문번호 · 원문 · 구분 ·
|
||||
* 상세구분 · 별칭 · 비고 · 쓰는 계산식(822-2 · 서버 식 고름) · 쓰는 곳 · 지우기(쓰는 곳 보이고 확인).
|
||||
* 고침은 초안(sessionStorage)에 바로 쌓임 → 페이지 [저장] 한 번에 서버로(자동저장 없음) ·
|
||||
* 서버가 계수를 계수 짝으로 다시 셈 · 단위 · 출처 칸을 채움(`M01_MasterData_Machine.fix`).
|
||||
@@ -16,8 +17,8 @@ import {
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createMachineForm, type MachineSimilar } from "@ui/ui_template_machine_form";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createMachineSheet, type MachineSimilar } from "../A00_Common/line_stack_machine_sheet";
|
||||
import { setLeaveGuard } from "../A00_Common/router";
|
||||
import type { Row } from "./M01_MasterData_Api_Fetch";
|
||||
import { addRow, editRow, peek, removeAdd, setAdd, toggleDelete } from "./M01_MasterData_Draft";
|
||||
@@ -96,22 +97,19 @@ async function build(host: HTMLElement, opts: MachineDetailOptions, alive: () =>
|
||||
name: text(r["이름"]),
|
||||
spec: text(r["규격"]),
|
||||
tag: text(r["상세구분"] || r["구분"]),
|
||||
values: (r["값"] ?? {}) as Row,
|
||||
row: { 이름: r["이름"], 규격: r["규격"], 값: (r["값"] ?? {}) as Row },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const notice = el("div");
|
||||
const form = createMachineForm({
|
||||
name: text(cur["이름"]),
|
||||
spec: text(cur["규격"]),
|
||||
values: (cur["값"] ?? {}) as Row,
|
||||
coefMode: "pumsem",
|
||||
fuels: lists.fuels,
|
||||
jobs: lists.jobs,
|
||||
power: lists.power,
|
||||
hour: lists.hour,
|
||||
findSimilar,
|
||||
const form = createMachineSheet({
|
||||
orig: (orig ?? cur) as Row,
|
||||
cur,
|
||||
mine: false,
|
||||
readOnly: false,
|
||||
lists,
|
||||
findSimilar: isNew ? findSimilar : undefined,
|
||||
onSimilar: (item) => {
|
||||
const r = found.get(item.key);
|
||||
if (!r || !isNew) return;
|
||||
@@ -131,7 +129,7 @@ async function build(host: HTMLElement, opts: MachineDetailOptions, alive: () =>
|
||||
];
|
||||
notice.replaceChildren(...errors.map((e) => createText(e, "error")));
|
||||
if (errors.length) return;
|
||||
const row: Row = { ...base, ...cur, 이름: s.name, 규격: s.spec, 값: s.values };
|
||||
const row: Row = { ...base, ...cur, 이름: s.name, 규격: s.spec, 값: s.row.값 ?? {} };
|
||||
for (const m of META) {
|
||||
if (orig && meta[m] === metaOrig[m]) {
|
||||
if (m in orig) row[m] = orig[m];
|
||||
@@ -305,8 +303,6 @@ async function build(host: HTMLElement, opts: MachineDetailOptions, alive: () =>
|
||||
}),
|
||||
);
|
||||
paintFormula();
|
||||
if (opts.target === "new")
|
||||
host.querySelector<HTMLDetailsElement>("details")?.setAttribute("open", "");
|
||||
}
|
||||
|
||||
/** 새 기계 바탕 — 칸 차례는 기계 줄과 같음 · 단위 · 출처는 서버가 채움 */
|
||||
|
||||
@@ -12,6 +12,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Material_Own as own
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
|
||||
@@ -46,7 +47,7 @@ def _have(copy: dict) -> set[str]:
|
||||
|
||||
|
||||
def gaps(copy: dict) -> list[str]:
|
||||
"""값 사본에 없는데 문서 셈이 읽은 M01 줄 키 + 문서의 내 기계 키(셈 틀림 = 읽은 데까지)."""
|
||||
"""값 사본에 없는데 문서 셈이 읽은 M01 줄 키 + 문서의 내 기계 · 내 재료(840-4) 키(셈 틀림 = 읽은 데까지)."""
|
||||
if "로직" not in copy or not isinstance(copy.get("문서"), dict):
|
||||
return []
|
||||
seen = _Seen(dc.over(copy))
|
||||
@@ -55,7 +56,7 @@ def gaps(copy: dict) -> list[str]:
|
||||
except dc._ERR:
|
||||
pass
|
||||
have = _have(copy)
|
||||
return sorted({*seen.keys, *mym.keys_in(copy["문서"])} - have)
|
||||
return sorted({*seen.keys, *mym.keys_in(copy["문서"]), *own.keys_in(copy["문서"])} - have)
|
||||
|
||||
|
||||
def fill(copy: dict) -> dict:
|
||||
@@ -76,7 +77,7 @@ def fill_imported(sk: dict, doc: Any) -> dict:
|
||||
return sk
|
||||
seen = _Seen(dim._index(sk, None, doc))
|
||||
dim.run(doc, sk, index=seen) # 셈 틀림 = 읽은 데까지(run 이 받음)
|
||||
keys = sorted({*seen.keys, *mym.keys_in(doc)} - _have(sk[skc.KEY]))
|
||||
keys = sorted({*seen.keys, *mym.keys_in(doc), *own.keys_in(doc)} - _have(sk[skc.KEY]))
|
||||
if not keys:
|
||||
return sk
|
||||
return {**sk, skc.KEY: skc.grow(sk[skc.KEY], skc.snap([], keys))}
|
||||
|
||||
@@ -257,9 +257,26 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
link = {"갈래": "요소", "키": key}
|
||||
if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음)
|
||||
link["단위"] = str(item.get("단위") or "")
|
||||
if key[:2] == dc.cv.mm.MARKET and (unit := str(item.get("단위") or "")):
|
||||
link["단위"] = unit # 840-3 자재품목 = 셈이 줄 단위로 환산(`master_pick.priced`)
|
||||
return link, _unit_note(key, unit)
|
||||
return link, ""
|
||||
|
||||
|
||||
def _unit_note(key: str, unit: str) -> str:
|
||||
"""840-3 자재 단위 ↔ 줄 단위 글 — 같음 · 1 : 1 = 빈 글 · 같은 성격 = 환산 · 성격 다름 = 경고(사람이 고른 줄도)."""
|
||||
mu = dc.cv.mm.mu
|
||||
try:
|
||||
have = str(imp._whole().get(key).get("단위") or "")
|
||||
except Exception: # noqa: BLE001 — 없는 키 = 셈이 끊김 표지
|
||||
return ""
|
||||
if not have or mu.factor(have, unit) == 1:
|
||||
return ""
|
||||
if mu.same_kind(have, unit):
|
||||
return f"단위 환산 {have} → {unit}"
|
||||
return f"단위 다름 {unit} ↔ {have} — 값은 자재 단위 그대로"
|
||||
|
||||
|
||||
OPTION = "_옵션" # 연결이 부르는 옵션 변수(문서에 없으면 `_attach` 가 더함)
|
||||
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[dict[str, Any]]:
|
||||
if kind in ("인력", "재료") and (named := _alias(kind, name, spec)):
|
||||
if kind in ("인력", "재료") and (named := _alias(kind, name, spec, unit)):
|
||||
return named # 815-1 K2 — M01 줄 「별칭」 칸 먼저
|
||||
if kind == "인력":
|
||||
trade = est.trade(name)
|
||||
@@ -130,17 +130,21 @@ def _found(line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
def _alias(kind: str, name: str, spec: str) -> list[dict[str, Any]]:
|
||||
def _alias(kind: str, name: str, spec: str, unit: str = "") -> list[dict[str, Any]]:
|
||||
"""M01 별칭 칸 이음(`M02_Elem_Alias`) — 인력 = 이름 · 재료 = 이름 + 규격 › 표기 정규화 정확 일치 ·
|
||||
같은 품목 지역 줄은 하나."""
|
||||
같은 품목 지역 줄은 하나(840-3 줄 단위 넘김 = 같은 단위 줄 먼저)."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
table = "LB" if kind == "인력" else ea.MARKET
|
||||
keys = ea.keys(table, name, "" if kind == "인력" else spec)
|
||||
if kind == "재료" and len(keys) > 1:
|
||||
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, keys)) else keys
|
||||
keys = [k] if (k := ea.one({"이름": name, "규격": spec, "단위": unit}, keys)) else keys
|
||||
if kind == "재료" and not keys: # 815-1 K3 — 표기 정규화 정확 일치(바탕 이름 길보다 먼저)
|
||||
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, ea.exact(name, spec))) else []
|
||||
keys = (
|
||||
[k]
|
||||
if (k := ea.one({"이름": name, "규격": spec, "단위": unit}, ea.exact(name, spec)))
|
||||
else []
|
||||
)
|
||||
rows = [(k, ea.row(table, k)) for k in keys]
|
||||
if kind == "인력":
|
||||
return [pick("인력", k, r.get("이름"), "", "인", 단가=_plain(r.get("값"))) for k, r in rows]
|
||||
|
||||
@@ -241,8 +241,12 @@ def collect(
|
||||
`extra` = 마스터 밖 로직 줄(굳힌 단가산출 719-1) — 그 줄이 닿는 것만 모음(줄은 안 담음) ·
|
||||
`over` = M02 로직 사본 줄(795-4) — 같은 키는 M01 줄 대신 · M01 에 없는 키 = `COPY_FILE`."""
|
||||
files, whole = master()
|
||||
whole = mym.whole(whole) # 지금 층 내 기계 줄(821-1) — 없으면 M01 그대로
|
||||
own = {str(r.get("키")): r for r in mym.added(whole)}
|
||||
from M02_MasterTemplete import M02_Material_Own as mat
|
||||
|
||||
whole = mat.whole(
|
||||
mym.whole(whole)
|
||||
) # 지금 층 내 기계(821-1) · 내 재료(840-4) — 없으면 M01 그대로
|
||||
own = {str(r.get("키")): r for r in (*mym.added(whole), *mat.added(whole))}
|
||||
mine = {str(r.get("키")): r for r in over or []}
|
||||
if mine:
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
|
||||
@@ -196,10 +196,13 @@ def own_materials(
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""개인 자재 목록(148-2) — 개인 층만 · 회사 · 마스터 = 빈 목록."""
|
||||
"""개인 자재 목록(148-2) — 개인 층만 · 회사 · 마스터 = 빈 목록 · 줄마다 쓰는 곳(840-4)."""
|
||||
if layer != "personal":
|
||||
return {"줄": []}
|
||||
return {"줄": own.rows(_root(session, layer, owner))}
|
||||
folder = _root(session, layer, owner)
|
||||
rows = own.rows(folder)
|
||||
used = own.usage(folder, [str(r.get("키")) for r in rows])
|
||||
return {"줄": [{**r, "쓰는곳": used[str(r.get("키"))]} for r in rows]}
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/materials")
|
||||
@@ -215,3 +218,33 @@ def new_material(
|
||||
raise HTTPException(status_code=400, detail="신규 자재는 개인 층에서만")
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, own.add, folder, body)
|
||||
|
||||
|
||||
@router.put("/layers/{layer}/materials/{key}")
|
||||
def edit_material(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: dict[str, Any],
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""내 재료 고침(840-4) — `{이름, 규격, 단위, 값}` · 키 그대로 · 개인 층만."""
|
||||
if layer != "personal":
|
||||
raise HTTPException(status_code=400, detail="내 재료는 개인 층에서만")
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, own.update, folder, key, body)
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/materials/{key}")
|
||||
def drop_material(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""내 재료 지움(840-4) — 쓰는 곳 있으면 409 · 개인 층만."""
|
||||
if layer != "personal":
|
||||
raise HTTPException(status_code=400, detail="내 재료는 개인 층에서만")
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
_call(folder, own.remove, folder, key)
|
||||
return {"ok": True}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { t as L } from "@ui/ui_template_locale";
|
||||
import { aliasNote, matchLogic } from "@ui/ui_template_logic_find";
|
||||
import { openNewMaterial } from "./M02_Material_Pick";
|
||||
import { listMyMachines, mineElement, type MyMachineRow } from "./M02_MyMachine_Api";
|
||||
import { listMyMaterials, type MyMaterial } from "./M02_MyMaterial_Api";
|
||||
import { mt } from "./M02_MyMachine_Text";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createTabs } from "@ui/ui_template_tabs";
|
||||
@@ -91,6 +92,8 @@ export interface FindPickOptions {
|
||||
oil?: (e: M01Element) => void;
|
||||
/** 821-3 개인 층 내 기계 묶음(`machine` 과 함께) — 주인(null = 자기 층) */
|
||||
mine?: { owner: string | null };
|
||||
/** 840-4 개인 층 내 재료(PM) 묶음 — 재료 고름 위 · 주인(null = 자기 층) */
|
||||
mineMat?: { owner: string | null };
|
||||
}
|
||||
|
||||
/** 727-1 기초마다 한 줄(옛 창 `Labor_Pick` 도 씀) — 규격 칸 = 「옵션 N」 */
|
||||
@@ -194,6 +197,7 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
let machines: PickItem[] = [];
|
||||
let oils: PickItem[] = []; // 785-3 유가전력 MO
|
||||
let mines: MyMachineRow[] = []; // 821-3 내 기계
|
||||
let mats: MyMaterial[] = []; // 840-4 내 재료
|
||||
const lists: Record<"sk" | "pre", SangeunRow[] | null> = { sk: null, pre: null };
|
||||
let shown: Line[] = [];
|
||||
let seq = 0;
|
||||
@@ -312,6 +316,16 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
};
|
||||
});
|
||||
};
|
||||
/** 840-4 내 재료 줄 — M01 재료처럼 고름(코드 = PM 키 · 값 = 내 목록 값) */
|
||||
const matLines = (q: string): Line[] => {
|
||||
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
return elementLines(
|
||||
"재료",
|
||||
mats
|
||||
.filter((r) => words.every((w) => `${r.이름} ${r.규격} ${r.키}`.toLowerCase().includes(w)))
|
||||
.map((r) => ({ ref: r.키, 구분: L("M02_FindMyMaterial"), ...r })),
|
||||
).map((x) => ({ ...x, 출처: x.출처.replace(/^M01 /, "") }));
|
||||
};
|
||||
const docLines = (rows: SangeunRow[], kind: string): Line[] =>
|
||||
rows.map((x) => ({
|
||||
품명: x.제목 || x.이름,
|
||||
@@ -329,6 +343,7 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
shown = [
|
||||
...(ok("logic") ? logicLines(q) : []),
|
||||
...(ok("labor") ? elementLines("인력", people) : []),
|
||||
...(opt.mineMat && ok("material") ? matLines(q) : []),
|
||||
...(ok("material") ? elementLines("재료", goods) : []),
|
||||
...(opt.oil && ok("oil") ? elementLines("유가", oils) : []),
|
||||
...(opt.machine && opt.mine && ok("machine") ? mineLines(q) : []),
|
||||
@@ -456,6 +471,13 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
draw();
|
||||
})
|
||||
.catch(fail);
|
||||
if (opt.mineMat && ok("material"))
|
||||
void listMyMaterials(opt.mineMat.owner)
|
||||
.then((rows) => {
|
||||
mats = rows;
|
||||
draw();
|
||||
})
|
||||
.catch(fail);
|
||||
void upper(opt.layer, opt.owner)
|
||||
.then((scopes) =>
|
||||
Promise.all(
|
||||
|
||||
@@ -20,7 +20,6 @@ import { el } from "@ui/ui_template_elements";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { swapUnit } from "../A00_Common/master_unit_swap";
|
||||
import {
|
||||
fetchLogic,
|
||||
setLogicApiPaths,
|
||||
@@ -31,12 +30,10 @@ import {
|
||||
type TextAnswer,
|
||||
} from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { buildLabDetail } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Detail";
|
||||
import { buildHo, lineOpener, type HoEdit } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Ho";
|
||||
import { buildHo, type HoEdit } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Ho";
|
||||
import { readTemplate, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { baseHeads } from "./M02_MasterTemplete_Sangeun_BaseHead";
|
||||
import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover";
|
||||
import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import { openRateLine } from "./M02_MasterTemplete_Sangeun_Rate";
|
||||
import { createSangeunTabs } from "./M02_MasterTemplete_Sangeun_Tabs";
|
||||
import { tl } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Text";
|
||||
import { createSangeunSide, tabOf, type SourceTab } from "./M02_MasterTemplete_Sangeun_Side";
|
||||
@@ -57,7 +54,7 @@ import {
|
||||
type SkAnswer,
|
||||
type SkDoc,
|
||||
} from "./logic/M02_Sangeun_Cover_Model";
|
||||
import { resetQty, setQty, withFirst } from "./logic/M02_Sangeun_Undo";
|
||||
import { resetQty, setQty } from "./logic/M02_Sangeun_Undo";
|
||||
|
||||
export interface SangeunDetailOptions {
|
||||
layer: Layer;
|
||||
@@ -138,74 +135,13 @@ export function mountSangeunDetail(
|
||||
}
|
||||
}
|
||||
const ownHost = el("div");
|
||||
const addButton = createButton({
|
||||
label: L("M02_SangeunAddLine"),
|
||||
variant: "ghost",
|
||||
size: 28,
|
||||
onClick: () =>
|
||||
openSangeunLinePicker(scope, (line) => {
|
||||
if ("종류" in line && line.종류 === "로직" && !doc.바탕) return setBase(line.로직);
|
||||
if ("종류" in line && line.종류 === "로직") return void addBase(line);
|
||||
doc.더한줄 = [
|
||||
...(doc.더한줄 ?? []),
|
||||
withFirst(line as unknown as NonNullable<SkDoc["더한줄"]>[number]),
|
||||
];
|
||||
touch();
|
||||
}),
|
||||
});
|
||||
addButton.hidden = !edit || !!opt.source;
|
||||
// 778-4 율 줄 — 기준 줄 후보 = 지금 셈 답 줄(세부 줄 뺌) · 새 줄은 맨 끝(앞 줄만 가리킴)
|
||||
const rateButton = createButton({
|
||||
label: L("M02_SkRateAdd"),
|
||||
variant: "ghost",
|
||||
size: 28,
|
||||
onClick: () =>
|
||||
openRateLine(
|
||||
(ans?.줄 ?? []).map((l) => ({ cid: l.cid, 이름: l.이름 })),
|
||||
(line) => ((doc.더한줄 = [...(doc.더한줄 ?? []), withFirst(line)]), touch()),
|
||||
),
|
||||
});
|
||||
rateButton.hidden = !edit || !!opt.source;
|
||||
/** 둘째 기초(728-2) — 수량 처음값 = 바탕 기준단위 1 개 = 그 기초 기준단위 몇 개(같은 성격 · 다르면 1) */
|
||||
async function addBase(line: { 로직: string; 수량: number }): Promise<void> {
|
||||
let qty = 1;
|
||||
const have = base ? baseUnitOf(base.logic.결과단위) : "";
|
||||
try {
|
||||
const swap = have
|
||||
? swapUnit(have, baseUnitOf((await fetchLogic(line.로직)).logic.결과단위))
|
||||
: null;
|
||||
if (swap?.배 != null && swap.계수 === null) qty = swap.배;
|
||||
} catch {
|
||||
/* 못 읽음 = 1 그대로 */
|
||||
}
|
||||
if (!alive) return;
|
||||
doc.더한줄 = [
|
||||
...(doc.더한줄 ?? []),
|
||||
withFirst({ ...line, 수량: qty } as unknown as NonNullable<SkDoc["더한줄"]>[number]),
|
||||
];
|
||||
touch();
|
||||
}
|
||||
/** 첫 기초 = 바탕(옛 [기본 로직 선택] 동작 · 단위는 `loadBase` 가 기초 결과단위로) */
|
||||
function setBase(key: string): void {
|
||||
doc.바탕 = { 로직: key };
|
||||
delete doc.바탕판; // 새 바탕 — 개정 견줄 판 없음
|
||||
touch(false);
|
||||
void loadBase(true);
|
||||
}
|
||||
const main = el("div", {
|
||||
className: "m02-sk",
|
||||
attrs: { "data-sangeun-detail": doc.키 ?? "" },
|
||||
children: [
|
||||
head,
|
||||
notes,
|
||||
...(opt.slot
|
||||
? []
|
||||
: [
|
||||
el("div", {
|
||||
className: "m02-sk__head",
|
||||
children: [tabs.tools, addButton, rateButton],
|
||||
}),
|
||||
]),
|
||||
...(opt.slot ? [] : [el("div", { className: "m02-sk__head", children: [tabs.tools] })]),
|
||||
tabs.host,
|
||||
editor,
|
||||
],
|
||||
@@ -420,15 +356,9 @@ export function mountSangeunDetail(
|
||||
row: base?.logic ?? row,
|
||||
file: base?.file ?? "",
|
||||
reasons: base?.reasons ?? [],
|
||||
prices: base?.prices ?? {},
|
||||
lines: null,
|
||||
text,
|
||||
lastText,
|
||||
values,
|
||||
ownHost,
|
||||
onChange: () => undefined,
|
||||
onInput,
|
||||
cover: { ho, open: lineOpener(hoCtx) },
|
||||
cover: { ho },
|
||||
});
|
||||
// prettier-ignore
|
||||
side?.set([
|
||||
|
||||
@@ -4,13 +4,10 @@
|
||||
* · `listLayerDocs` = 층 목록 길(`/layers/{층}/templates` · 종류 하나)을 마스터 › 회사 › 개인 차례로 이음 ·
|
||||
* 금액 = 서버가 붙인 1 단위 `기준금액` · `listSangeun` = 종류 `sangeun` · 찾아 넣기 창(`Find_Pick`) 탭 · B09 재료 창고 출처가 씀
|
||||
* · 고른 SK = 참조 `{층, 주인, 키: SK-…}` — 일위대가 줄은 옛 참조 줄 꼴(서버 `M02_Labor_Expand` 가 펼침)
|
||||
* · `openSangeunLinePicker` = 단가산출 [항목 추가] — 찾아 넣기 창(721-2 · M01 로직 · 인력 · 재료 · 단가산출 · 일위대가)
|
||||
* → SK 문서 `더한줄` 한 줄(PLAN 715 이음 꼴 · 인력 · 재료 = 요소 키 LB · MT · 수량 1)
|
||||
* ========================================================================== */
|
||||
|
||||
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { fixedOf, openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { call } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
|
||||
export const SANGEUN_KIND = "sangeun";
|
||||
/** 층 차례 — 위에서 아래 */
|
||||
@@ -68,29 +65,3 @@ export async function listLayerDocs(
|
||||
);
|
||||
return lists.flat();
|
||||
}
|
||||
|
||||
/** 단가산출 더한줄 한 줄(PLAN 715 이음 꼴) */
|
||||
export type SangeunLine =
|
||||
| { 종류: "로직"; 로직: string; 입력값: Record<string, never>; 수량: number }
|
||||
| { 참조: LaborRef; 수량: number }
|
||||
| { 종류: "인력" | "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number }
|
||||
| { 고정: LaborFixed; 이름: string; 단위: string | null; 수량: number };
|
||||
|
||||
/** 단가산출 [항목 추가] — `scope` = 그 SK 문서 층(고를 수 있는 서고 = 위 층만) */
|
||||
export function openSangeunLinePicker(
|
||||
scope: { layer: Layer; owner: string | null },
|
||||
done: (line: SangeunLine) => void,
|
||||
): void {
|
||||
openFindPicker({
|
||||
...scope,
|
||||
logic: (x) => done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 }),
|
||||
ref: (ref) => done({ 참조: ref, 수량: 1 }),
|
||||
element: (e) =>
|
||||
(e.종류 === "인력" || e.종류 === "재료") &&
|
||||
done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 }),
|
||||
// 785-3 유가전력 = 더한줄 재료 요소 MO
|
||||
oil: (e) => done({ 종류: "재료", 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 }),
|
||||
// 736-3 기계 = 고정 줄(이음 기계 EQ · 단가 = 한 시간 노 · 재 · 경) · 수량 1
|
||||
machine: (e) => done({ 고정: fixedOf(e), 이름: e.이름, 단위: e.단위, 수량: 1 }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Sangeun_Rate.ts
|
||||
* M02 단가산출(SK) 율 줄 넣기 창(PLAN 778-4 · 요구 623) — [율 줄 추가] → 이름 · 율 % · 기준 · 금액이 드는 칸.
|
||||
* 기준 = 묶음(인력 · 재료 · 장비 줄 합) · 비목 전체(노무비 · 재료비 · 경비) · 줄 고르기(이 SK 줄 cid 목록 ·
|
||||
* 앞 율 줄 고르면 율 위 율) → 더한줄 `{고정: {이름, 단위 %, 단가 0, 이음 없음, 비율 {기준, 비목}}, 수량 = 율}`
|
||||
* (꼴 = 서버 `M02_Sangeun_Doc` · 셈 = 서버 `M02_Sangeun_Calc._rated`) · 부품 상자만.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createFormGrid } from "@ui/ui_template_general_layout";
|
||||
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import type { SkAdded } from "./logic/M02_Sangeun_Cover_Model";
|
||||
|
||||
const LINES = "줄";
|
||||
|
||||
/** 율 줄 넣기 — `rows` = 지금 셈 답 줄(기준 줄 고르기 후보) · `done` = 새 더한줄 한 줄 */
|
||||
export function openRateLine(
|
||||
rows: { cid: string; 이름: string }[],
|
||||
done: (line: SkAdded) => void,
|
||||
): void {
|
||||
const name = createInputField({ label: L("M02_SkRateName"), value: L("M02_SkRateDefault") });
|
||||
const rate = createInputField({
|
||||
label: L("M02_SkRateRate"),
|
||||
type: "number",
|
||||
value: "2",
|
||||
unit: "%",
|
||||
min: 0,
|
||||
});
|
||||
const picks = rows.map((r) => ({
|
||||
cid: r.cid,
|
||||
box: createCheckbox(r.이름 || r.cid, { chip: true, size: 28 }),
|
||||
}));
|
||||
const list = el("div", {
|
||||
className: "m02-sk__tags",
|
||||
attrs: { "data-sk-rate-lines": "" },
|
||||
children: picks.length
|
||||
? picks.map((p) => p.box.root)
|
||||
: [el("span", { text: L("M02_SkRateNoLines") })],
|
||||
});
|
||||
list.hidden = true;
|
||||
const basis = createSelectField({
|
||||
label: L("M02_SkRateBasis"),
|
||||
value: "인력",
|
||||
options: [
|
||||
{ value: "인력", text: L("M02_SkRateGroupLabor") },
|
||||
{ value: "재료", text: L("M02_SkRateGroupMat") },
|
||||
{ value: "장비", text: L("M02_SkRateGroupMach") },
|
||||
{ value: "노무비", text: L("M02_SkRateCostLabor") },
|
||||
{ value: "재료비", text: L("M02_SkRateCostMat") },
|
||||
{ value: "경비", text: L("M02_SkRateCostExp") },
|
||||
{ value: LINES, text: L("M02_SkRateBasisLines") },
|
||||
],
|
||||
onChange: (v) => (list.hidden = v !== LINES),
|
||||
});
|
||||
const part = createSelectField({
|
||||
label: L("M02_SkRatePart"),
|
||||
value: "경비",
|
||||
options: [
|
||||
{ value: "경비", text: L("M02_SkRatePartExp") },
|
||||
{ value: "재료비", text: L("M02_SkRatePartMat") },
|
||||
{ value: "노무비", text: L("M02_SkRatePartLabor") },
|
||||
],
|
||||
});
|
||||
openModal({
|
||||
title: L("M02_SkRateTitle"),
|
||||
closeLabel: L("M02_SkRateClose"),
|
||||
width: "40rem",
|
||||
mount: (body, close, actions) => {
|
||||
const ok = createButton({
|
||||
label: L("M02_SkRateOk"),
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
const label = name.input.value.trim();
|
||||
const q = Number(rate.input.value);
|
||||
const keys = picks.filter((p) => p.box.input.checked).map((p) => p.cid);
|
||||
const lines = basis.select.value === LINES;
|
||||
name.setError(label ? undefined : L("M02_SkRateNeedName"));
|
||||
rate.setError(rate.input.value.trim() && q >= 0 ? undefined : L("M02_SkRateBadRate"));
|
||||
if (lines && !keys.length) return void rate.setError(L("M02_SkRateNeedLine"));
|
||||
if (!label || !rate.input.value.trim() || !(q >= 0)) return;
|
||||
const fx = {
|
||||
이름: label,
|
||||
단위: "%",
|
||||
단가: { 노무비: 0, 재료비: 0, 경비: 0 },
|
||||
이음: { 노임: null, 자재: null, 기계: null },
|
||||
비율: { 기준: lines ? keys : basis.select.value, 비목: part.select.value },
|
||||
};
|
||||
done({ 고정: fx, 이름: label, 단위: "%", 수량: q });
|
||||
close();
|
||||
},
|
||||
});
|
||||
ok.dataset.skRateOk = "";
|
||||
actions.append(ok);
|
||||
const grid = createFormGrid([name.root, rate.root, basis.root, part.root]);
|
||||
grid.dataset.skRate = "";
|
||||
body.append(grid, list);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -63,7 +63,6 @@ export function createSangeunTabs(
|
||||
const show = (doc: boolean): void => {
|
||||
stack.root.hidden = tools.hidden = !doc;
|
||||
editor.hidden = doc;
|
||||
if (!doc) editor.dataset.view = "ho";
|
||||
};
|
||||
show(true);
|
||||
return {
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
`master_material.element`). 품번 = `PM` + 6자리 — 모든 사람 통틀어 한 줄 번호(모든 개인 층 파일
|
||||
가장 큰 번호 + 1 · `store._LOCK` 안 · 화면이 보낸 키는 안 받음). 신규 자재는 개인 층에서만
|
||||
(회사 · 마스터 층 = 마스터 자재만).
|
||||
840-4 내 재료 = M01 재료 DB 에 없는 자재를 등록하는 자리 — M01 과 이름 · 규격 · 단위가 같으면 막음(409) ·
|
||||
고치기 · 지우기(쓰는 곳 있으면 막음) · 단가산출 셈 색인에 M01 재료처럼 얹음(`whole` · `M02_Sangeun_Copy.whole_`)
|
||||
→ 프로젝트 층 밖 SK 는 값이 바뀌면 자동 따라옴(836-2 `M02_Sk_AutoRenew`) · 프로젝트 층 = 사본 값.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -22,6 +26,7 @@ from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
FILE = "개인자재.json"
|
||||
PREFIX = "PM"
|
||||
KEY = re.compile(rf"^{PREFIX}(\d{{6}})$")
|
||||
FIND = re.compile(rf"(?<![A-Za-z0-9]){PREFIX}\d{{6}}(?!\d)")
|
||||
COLS = ("이름", "규격", "단위")
|
||||
|
||||
|
||||
@@ -70,23 +75,154 @@ def _value(v: Any) -> float | int | None:
|
||||
return int(got) if got.is_integer() else got
|
||||
|
||||
|
||||
def add(folder: str | Path, body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""새 개인 자재 한 줄 → 그 줄(키는 서버) · 품명 · 단위 빈 = 400 ·
|
||||
같은 품명 + 규격 + 단위 = 409."""
|
||||
def _norm(v: Any) -> str:
|
||||
"""빈칸 · 대소문자 · 겹낱자(㎏ = kg · ㎥ = m3) 무시."""
|
||||
return unicodedata.normalize("NFKC", " ".join(_text(v).split())).lower()
|
||||
|
||||
|
||||
def m01_same(row: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""M01 재료 DB(자재품목 MT)에서 이름 · 규격 · 단위가 같은 줄(빈칸 · 대소문자 무시)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
want = tuple(_norm(row.get(k)) for k in COLS)
|
||||
for r in (m01.loaded()[1].index.get("MT") or {}).values():
|
||||
if tuple(_norm(r.get(k)) for k in COLS) == want:
|
||||
return r
|
||||
return None
|
||||
|
||||
|
||||
def _checked(body: dict[str, Any], others: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""품명 · 단위 빈 = 400 · 내 목록 같은 줄 · M01 재료 DB 같은 줄 = 409."""
|
||||
row = {k: _text(body.get(k)) for k in COLS}
|
||||
if not row["이름"] or not row["단위"]:
|
||||
raise store.StoreError(400, "품명 · 단위는 꼭 적음")
|
||||
row["값"] = _value(body.get("값"))
|
||||
if any(all(_norm(x.get(k)) == _norm(row[k]) for k in COLS) for x in others):
|
||||
raise store.StoreError(409, f"이미 있는 자재 「{row['이름']} {row['규격']}」")
|
||||
if m01_same(row):
|
||||
raise store.StoreError(
|
||||
409,
|
||||
f"재료 DB 에 있는 자재 「{row['이름']} {row['규격']}」 — 재료 DB 자재를 고르고 "
|
||||
"값은 단가산출 안 재료 줄에서 바꿈",
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def add(folder: str | Path, body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""새 개인 자재 한 줄 → 그 줄(키는 서버) · 품명 · 단위 빈 = 400 ·
|
||||
같은 품명 + 규격 + 단위(내 목록 · M01 재료 DB) = 409."""
|
||||
with store._LOCK:
|
||||
doc = read(folder)
|
||||
if any(all(_text(x.get(k)) == row[k] for k in COLS) for x in doc["줄"]):
|
||||
raise store.StoreError(409, f"이미 있는 자재 「{row['이름']} {row['규격']}」")
|
||||
row = _checked(body, doc["줄"])
|
||||
got = {"키": f"{PREFIX}{_last() + 1:06d}", **row}
|
||||
doc["줄"].append(got)
|
||||
atomic_write_json(Path(folder) / FILE, doc)
|
||||
return got
|
||||
|
||||
|
||||
def update(folder: str | Path, key: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""내 재료 한 줄 고침(키 그대로) — 없는 키 = 404 · 검사는 새로 만들기와 같음."""
|
||||
with store._LOCK:
|
||||
doc = read(folder)
|
||||
at = next((i for i, r in enumerate(doc["줄"]) if r.get("키") == key), None)
|
||||
if at is None:
|
||||
raise store.StoreError(404, f"없는 내 재료 「{key}」")
|
||||
row = _checked(body, [r for i, r in enumerate(doc["줄"]) if i != at])
|
||||
doc["줄"][at] = got = {"키": key, **row}
|
||||
atomic_write_json(Path(folder) / FILE, doc)
|
||||
return got
|
||||
|
||||
|
||||
def _aside(path: Path, folder: str | Path) -> bool:
|
||||
"""밑줄 · 점 폴더(「_합친것」 등 파생 · 보관) — 쓰는 곳 아님."""
|
||||
return any(x.startswith(("_", ".")) for x in path.relative_to(folder).parts[:-1])
|
||||
|
||||
|
||||
def usage(folder: str | Path, keys: list[str]) -> dict[str, list[dict[str, Any]]]:
|
||||
"""그 층에서 키마다 쓰는 문서(단가산출 · 일위대가 …) {키: [{종류, 키, 이름}]} — 파일은 한 번씩 읽음."""
|
||||
out: dict[str, list[dict[str, Any]]] = {k: [] for k in keys}
|
||||
for path in sorted(Path(folder).rglob("*.json")): # 종류 폴더 · 그 밑 폴더 — 내 목록 파일 뺌
|
||||
if path.name == FILE or _aside(path, folder):
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
hit = [k for k in keys if k in text]
|
||||
if not hit:
|
||||
continue
|
||||
try:
|
||||
doc = json.loads(text)
|
||||
except ValueError:
|
||||
continue
|
||||
name = (doc.get("이름") if isinstance(doc, dict) else None) or path.stem
|
||||
for k in hit:
|
||||
out[k].append({"종류": path.parent.name, "키": path.stem, "이름": name})
|
||||
return out
|
||||
|
||||
|
||||
def uses(folder: str | Path, key: str) -> list[dict[str, Any]]:
|
||||
return usage(folder, [key])[key]
|
||||
|
||||
|
||||
def remove(folder: str | Path, key: str) -> None:
|
||||
"""내 재료 지움 — 쓰는 문서가 있으면 409(그 줄을 먼저 바꿈)."""
|
||||
with store._LOCK:
|
||||
doc = read(folder)
|
||||
if not any(r.get("키") == key for r in doc["줄"]):
|
||||
raise store.StoreError(404, f"없는 내 재료 「{key}」")
|
||||
if used := uses(folder, key):
|
||||
names = " · ".join(str(u["이름"]) for u in used[:3])
|
||||
raise store.StoreError(409, f"쓰는 곳 {len(used)} — {names} · 그 줄을 먼저 바꿈")
|
||||
doc["줄"] = [r for r in doc["줄"] if r.get("키") != key]
|
||||
atomic_write_json(Path(folder) / FILE, doc)
|
||||
|
||||
|
||||
# ── 840-4 셈 색인(단가산출이 M01 재료처럼 읽음) ─────────────────────────
|
||||
_MEMO: dict[str, tuple] = {}
|
||||
|
||||
|
||||
def whole(base: Any) -> Any:
|
||||
"""셈 색인 + 지금 층(개인) 내 재료 줄 — 파일 · 바탕이 그대로면 같은 객체 · 개인 층 아님 = 바탕 그대로."""
|
||||
from decimal import Decimal
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
|
||||
folder = store.root()
|
||||
path = folder / FILE
|
||||
try:
|
||||
st = path.stat()
|
||||
except OSError:
|
||||
return base
|
||||
if not _personal(folder):
|
||||
return base
|
||||
sig = (st.st_mtime_ns, st.st_size)
|
||||
got = _MEMO.get(str(path))
|
||||
if got and got[0] == sig and got[1] is base:
|
||||
return got[2]
|
||||
raw = json.dumps(rows(folder), ensure_ascii=False)
|
||||
mine = [
|
||||
r
|
||||
for r in json.loads(raw, parse_float=Decimal, parse_int=Decimal)
|
||||
if is_own(str(r.get("키")))
|
||||
]
|
||||
out = skc.Copy(base, mine, ("내재료", sig)) if mine else base
|
||||
if len(_MEMO) > 16: # ponytail: 통째 비움 · 층이 많아지면 LRU 로
|
||||
_MEMO.clear()
|
||||
_MEMO[str(path)] = (sig, base, out)
|
||||
return out
|
||||
|
||||
|
||||
def keys_in(doc: Any) -> list[str]:
|
||||
"""문서 안 내 재료 키(요소 이음 「PM…」 · 사본 빈틈 찍기)."""
|
||||
return sorted(set(FIND.findall(json.dumps(doc, ensure_ascii=False, default=str))))
|
||||
|
||||
|
||||
def added(index: Any) -> list[dict]:
|
||||
"""`whole` 이 얹은 내 재료 줄(사본 모으기 `collect` 가 마스터 밖 줄로 담음)."""
|
||||
return list((getattr(index, "index", {}).get(PREFIX) or {}).values())
|
||||
|
||||
|
||||
# ── 148-3 — 저장 검사 · 셈 · 사본 · 층 사이 ─────────────────────────────
|
||||
BUNDLE = "재료_개인자재.json" # 사본 `단가.json` 안 이름
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 줄 = 그 층 단가산출 줄에 값을 바로 적은 재료(우리 자재에 안 이어진 것 · 서버 `M02_MyMachine_Look.items`)
|
||||
* 칸 = 이름 · 규격 · 단위 · 값 · 쓰는 단가산출 수 · 같은 이름 수(같은 이름 묶음 = 이름 차례로 붙어 보임)
|
||||
* 줄 누름 = 아래 쓰는 단가산출 목록 → 누르면 그 단가산출로(고치기는 거기서 · 여기선 저장 없음)
|
||||
* 위 = 「내 재료」 등록 칸(840-4 · `M02_MyMaterial_View` — 재료 DB 에 없는 자재 등록) · 이 모아 보기는 그 아래 읽기
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
@@ -11,6 +12,7 @@ import { createInputField, el } from "@ui/ui_template_elements";
|
||||
import { createFindList } from "@ui/ui_template_find_list";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { listMyItems, type MyItem, type Use } from "./M02_MyMachine_Api";
|
||||
import { buildMyMaterialBlock } from "./M02_MyMaterial_View";
|
||||
import { mt } from "./M02_MyMachine_Text";
|
||||
|
||||
export interface MyItemsViewOptions {
|
||||
@@ -85,6 +87,7 @@ export function buildMyItemsView(opt: MyItemsViewOptions): HTMLElement {
|
||||
className: "m02-mymach-view",
|
||||
attrs: { "data-my-items": "" },
|
||||
children: [
|
||||
buildMyMaterialBlock({ owner: opt.owner }),
|
||||
el("div", { className: "ui-toolbar", children: [search.root, hint] }),
|
||||
createText(mt("Items_Note"), "muted"),
|
||||
fail,
|
||||
|
||||
@@ -349,6 +349,26 @@ def _user() -> str:
|
||||
return user
|
||||
|
||||
|
||||
def not_m01(raw: Any, skip: str = "") -> None:
|
||||
"""화면 만들기 · 고치기(840-2) — 이름 · 규격이 M01 기계와 같으면 409(내 기계 = M01 에 없는 기계만 ·
|
||||
불러오기 자동 생성은 이 검사 없이 `create`)."""
|
||||
raw = raw if isinstance(raw, dict) else {}
|
||||
if any(h["출처"] == "M01" for h in same(raw.get("이름"), raw.get("규격"), skip)):
|
||||
raise _bad(
|
||||
"M01 에 있는 기계 — 값은 단가산출 안에서 고침 · 내 기계는 M01 에 없는 기계만", 409
|
||||
)
|
||||
|
||||
|
||||
def create_own(raw: Any = None, base: str | None = None) -> dict[str, Any]:
|
||||
not_m01(raw)
|
||||
return create(raw, base)
|
||||
|
||||
|
||||
def update_own(key: str, raw: Any) -> dict[str, Any]:
|
||||
not_m01(raw, key)
|
||||
return update(key, raw)
|
||||
|
||||
|
||||
def create(raw: Any = None, base: str | None = None) -> dict[str, Any]:
|
||||
_user()
|
||||
seed = _seed(base) if base else {}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 양식 목록 · 비슷한 기계 찾기 · 내 품목 = `M02_MyMachine_Look`(읽기만)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MachinePick, MachineValues } from "@ui/ui_template_machine_form";
|
||||
import type { MachinePick, MachineValues } from "../common_util/common_util_machine_sheet";
|
||||
import type { DocRecord } from "../common_util/common_util_formula_doc_cache";
|
||||
import { call } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { M01Element } from "./M02_MasterTemplete_Find_Pick";
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
/* =============================================================================
|
||||
* M02_MyMachine_Form.ts
|
||||
* M02 내 기계 양식 한 장(PLAN 821-3 · 요구서 796 1 · 3 장) — 기계 양식 부품(`ui_template_machine_form` · 821-2) +
|
||||
* [저장] · [지우기] · 「개인 값」 표지 · 같은 이름 · 규격 알림 · 서버 정본 시간당 · 쓰는 곳.
|
||||
* 기본 = 손료계수 「바로 넣기」 · 「시간당 값 직접」 꼴 · 직종 · 연료 = M01 목록 고르기 · 옆 계산식 즉시(계수 짝 TS)
|
||||
* M02 내 기계 한 대(PLAN 821-3 · 840-2 · 요구 862) — 기계 보기(`A00_Common/line_stack_machine_sheet` · M01 기계 상세와
|
||||
* 같은 꼴: 왼쪽 제어 구역 + 기본 정보 · 손료 근거 · 오른쪽 시간당 경비 산출근거) + [저장] · [지우기] · 「개인 값」 표지 ·
|
||||
* 같은 이름 · 규격 알림 · 서버 정본 시간당 · 쓰는 곳.
|
||||
* 내 기계 = M01 에 없는 기계를 등록하는 자리 — 이름 · 규격이 M01 기계와 같으면 알림 + 서버가 저장 막음 ·
|
||||
* M01 기계 값 고침은 단가산출 안 기계 제어 구역(836-1) · 「복사해 내 기계로」 걷음(840-2)
|
||||
* [저장] 때만 서버(자동저장 없음) — 서버가 정본으로 다시 셈한 시간당을 받아 보임
|
||||
* 새 기계 = 「비슷한 기계에서 시작」(M01 · 내 기계 · 고른 키 = 서버 `본뜬`) 또는 빈 양식 · `seed` = 품셈 기계 복사
|
||||
* 새 기계 = 「비슷한 기계에서 시작」(M01 · 내 기계 · 고른 키 = 서버 `본뜬`) 또는 빈 줄
|
||||
* 지우기 = 쓰는 단가산출 이름 보이고 확인 + 「이미 만든 단가산출은 마지막 값 유지」
|
||||
* 부르는 곳 = 내 기계 화면(`M02_MyMachine_View` · 827-1 = 읽기 전용 · 지우기만) — 만들기 · 고치기는
|
||||
* 단가산출 기계 창(`A00_Common/line_stack_machine` · 초안 → 단가산출 [저장])
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createMachineForm,
|
||||
HOURLY_KEY,
|
||||
type MachineFormState,
|
||||
type MachineSimilar,
|
||||
type MachineValues,
|
||||
} from "@ui/ui_template_machine_form";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import {
|
||||
createMachineSheet,
|
||||
type MachineSheetState,
|
||||
type MachineSimilar,
|
||||
} from "../A00_Common/line_stack_machine_sheet";
|
||||
import type { MachineRow } from "../common_util/common_util_machine_sheet";
|
||||
import {
|
||||
createMyMachine,
|
||||
deleteMyMachine,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
readMyMachine,
|
||||
updateMyMachine,
|
||||
type MyHourly,
|
||||
type MyMachine,
|
||||
type MyMachineBody,
|
||||
type MyMachineOne,
|
||||
type SimilarRow,
|
||||
@@ -41,12 +40,6 @@ export interface MyMachineFormOptions {
|
||||
owner: string | null;
|
||||
/** 고칠 내 기계 키 — 없으면 새 기계 */
|
||||
key?: string;
|
||||
/** 새 기계 바탕(복사해 내 기계로) — 값 · 이름 · 규격 · 본뜬 키 */
|
||||
seed?: SimilarRow;
|
||||
/** 새 기계면 「비슷한 기계에서 시작」 을 펼침 */
|
||||
openSimilar?: boolean;
|
||||
/** 827-1 보기만(양식 읽기 전용 · [저장] 없음 · [지우기] 그대로) */
|
||||
readOnly?: boolean;
|
||||
onSaved: (got: MyMachineOne) => void;
|
||||
onDeleted?: () => void;
|
||||
onBack?: () => void;
|
||||
@@ -60,29 +53,32 @@ export interface MyMachineFormHandle {
|
||||
|
||||
const won = (v: number | undefined): string =>
|
||||
`${(v ?? 0).toLocaleString("ko-KR", { maximumFractionDigits: 2 })}${mt("Won")}`;
|
||||
const RATIOS = ["내용시간", "연간표준가동시간", "상각비율", "정비비율", "연간관리비율"];
|
||||
|
||||
/** 줄(저장 꼴) → 양식 값 묶음 — 직접 = 시간당 칸 하나 */
|
||||
export function valuesOf(row: Pick<MyMachine, "방식" | "값" | "시간당">): MachineValues {
|
||||
return row.방식 === "직접" ? { [HOURLY_KEY]: { ...(row.시간당 ?? {}) } } : { ...row.값 };
|
||||
/** 찾은 줄 → 내 기계 줄 꼴(M01 줄 = 비율 다섯 있으면 품셈 · 없으면 바로 — 손료계수 = 계수 셋 합) */
|
||||
export function rowOfSimilar(r: SimilarRow): MachineRow {
|
||||
if (r.출처 === "개인") return { 이름: r.이름, 규격: r.규격, 방식: r.방식 ?? "바로", 값: { ...(r.값 ?? {}) }, 시간당: r.시간당 }; // prettier-ignore
|
||||
const v = { ...(r.값 ?? {}) };
|
||||
const how = RATIOS.every((k) => typeof v[k] === "number" && v[k]) ? "품셈" : "바로";
|
||||
if (how === "바로" && typeof v.손료계수 !== "number")
|
||||
v.손료계수 = ["상각비계수", "정비비계수", "관리비계수"].reduce(
|
||||
(s, k) => s + (Number(v[k]) || 0),
|
||||
0,
|
||||
);
|
||||
return { 이름: r.이름, 규격: r.규격, 방식: how, 값: v };
|
||||
}
|
||||
|
||||
/** 양식 상태 → 보낼 몸(틀림 = 글 목록) — 내 기계 양식 · 기계 창 [확인] 한 벌 */
|
||||
export function bodyOfForm(s: MachineFormState): { body: MyMachineBody | null; errors: string[] } {
|
||||
/** 보기 상태 → 보낼 몸(틀림 = 글 목록) */
|
||||
export function bodyOfSheet(s: MachineSheetState): {
|
||||
body: MyMachineBody | null;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors = [...s.errors, ...(s.name ? [] : [mt("Name_Needed")])];
|
||||
if (errors.length) return { body: null, errors };
|
||||
if (s.hourly)
|
||||
return {
|
||||
body: {
|
||||
이름: s.name,
|
||||
규격: s.spec,
|
||||
방식: "직접",
|
||||
시간당: (s.values[HOURLY_KEY] ?? {}) as MyMachineBody["시간당"],
|
||||
},
|
||||
errors,
|
||||
};
|
||||
const { [HOURLY_KEY]: _drop, ...vals } = s.values;
|
||||
const how = s.mode === "pumsem" ? "품셈" : "바로";
|
||||
return { body: { 이름: s.name, 규격: s.spec, 방식: how, 값: vals }, errors };
|
||||
const head = { 이름: s.name, 규격: s.spec };
|
||||
if (s.mode === "직접")
|
||||
return { body: { ...head, 방식: "직접", 시간당: (s.row.시간당 ?? {}) as MyMachineBody["시간당"] }, errors }; // prettier-ignore
|
||||
return { body: { ...head, 방식: s.mode, 값: s.row.값 ?? {} }, errors };
|
||||
}
|
||||
|
||||
/** 서버 시간당 한 줄 글 */
|
||||
@@ -100,41 +96,30 @@ export async function mountMyMachineForm(
|
||||
opt.key ? readMyMachine(opt.owner, opt.key) : Promise.resolve(null),
|
||||
]);
|
||||
const row = one?.줄;
|
||||
const seed = opt.seed;
|
||||
let base = seed && seed.출처 === "M01" ? seed.키 : undefined; // 서버 `본뜬`
|
||||
const found = new Map<string, SimilarRow>();
|
||||
let base: string | undefined; // 서버 `본뜬`
|
||||
const notice = el("div", { className: "m02-mymach__notice" });
|
||||
const server = el("div", { className: "m02-mymach__server" });
|
||||
const usesBox = el("div", { className: "m02-mymach__uses" });
|
||||
const start: MachineRow = row
|
||||
? { 키: row.키, 원문번호: row.원문번호, 이름: row.이름, 규격: row.규격, 방식: row.방식, 값: row.값, ...(row.시간당 ? { 시간당: row.시간당 } : {}) } // prettier-ignore
|
||||
: { 이름: "", 규격: "", 방식: "바로", 값: {} };
|
||||
|
||||
const form = createMachineForm({
|
||||
name: row?.이름 ?? seed?.이름 ?? "",
|
||||
spec: row?.규격 ?? seed?.규격 ?? "",
|
||||
values: row
|
||||
? valuesOf(row)
|
||||
: seed
|
||||
? valuesOf({ 방식: seed.방식 ?? "바로", 값: seed.값 ?? {}, 시간당: seed.시간당 })
|
||||
: {},
|
||||
coefMode: "direct",
|
||||
allowHourly: true,
|
||||
fuels: lists.fuels,
|
||||
jobs: lists.jobs,
|
||||
power: lists.power,
|
||||
hour: lists.hour,
|
||||
readOnly: opt.readOnly,
|
||||
let m01Same = false;
|
||||
const sheet = createMachineSheet({
|
||||
orig: start,
|
||||
mine: true,
|
||||
readOnly: false,
|
||||
lists,
|
||||
findSimilar: row
|
||||
? undefined
|
||||
: async (q): Promise<MachineSimilar[]> =>
|
||||
(await findSimilar(opt.owner, q)).map((r) => {
|
||||
found.set(r.키, r);
|
||||
return {
|
||||
key: r.키,
|
||||
name: r.이름,
|
||||
spec: r.규격,
|
||||
tag: r.출처 === "개인" ? mt("Mine") : r.구분,
|
||||
values: valuesOf({ 방식: r.방식 ?? "바로", 값: r.값 ?? {}, 시간당: r.시간당 }),
|
||||
};
|
||||
}),
|
||||
(await findSimilar(opt.owner, q)).map((r) => ({
|
||||
key: r.키,
|
||||
name: r.이름,
|
||||
spec: r.규격,
|
||||
tag: r.출처 === "개인" ? mt("Mine") : r.구분,
|
||||
row: rowOfSimilar(r),
|
||||
})),
|
||||
onSimilar: (item) => {
|
||||
base = item.key;
|
||||
checkSame();
|
||||
@@ -146,42 +131,42 @@ export async function mountMyMachineForm(
|
||||
},
|
||||
});
|
||||
|
||||
let saved = !opt.key && seed ? false : true;
|
||||
const dirty = (): boolean => form.state().changed || !saved;
|
||||
let saved = true;
|
||||
const dirty = (): boolean => sheet.state().changed || !saved;
|
||||
|
||||
/* --- 같은 이름 · 규격 알림(M01 · 내 기계) --- */
|
||||
/* --- 같은 이름 · 규격 알림(M01 = 막음 · 내 기계 = 알림) --- */
|
||||
let timer: number | undefined;
|
||||
let seq = 0;
|
||||
const sameText = (hits: { 키: string; 출처: string }[]): string =>
|
||||
hits.length
|
||||
? mt("Same").replace("{value}", hits.map((h) => `${h.출처} ${h.키}`).join(" · "))
|
||||
? [
|
||||
...(hits.some((h) => h.출처 === "M01") ? [L("MS_M01Same")] : []),
|
||||
mt("Same").replace("{value}", hits.map((h) => `${h.출처} ${h.키}`).join(" · ")),
|
||||
].join(" — ")
|
||||
: "";
|
||||
const checkSame = (): void => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(async () => {
|
||||
const mine = ++seq;
|
||||
const s = form.state();
|
||||
const s = sheet.state();
|
||||
const rows = s.name ? await findSimilar(opt.owner, s.name).catch(() => []) : [];
|
||||
if (mine !== seq) return;
|
||||
const hits = rows.filter(
|
||||
(r) => r.키 !== opt.key && r.이름.trim() === s.name && (r.규격 ?? "").trim() === s.spec,
|
||||
);
|
||||
form.setNotice(sameText(hits));
|
||||
m01Same = hits.some((h) => h.출처 === "M01");
|
||||
saveBtn.disabled = m01Same;
|
||||
sheet.setNotice(sameText(hits));
|
||||
}, 300);
|
||||
};
|
||||
|
||||
/* --- 저장 --- */
|
||||
const bodyOf = (): MyMachineBody | null => {
|
||||
const { body, errors } = bodyOfForm(form.state());
|
||||
notice.replaceChildren(...errors.map((e) => createText(e, "error")));
|
||||
return body;
|
||||
};
|
||||
const saveBtn = createButton({ label: mt("Save"), variant: "filled", size: 28 });
|
||||
saveBtn.dataset.mymachSave = "";
|
||||
saveBtn.hidden = !!opt.readOnly;
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
const body = bodyOf();
|
||||
if (!body) return;
|
||||
const { body, errors } = bodyOfSheet(sheet.state());
|
||||
notice.replaceChildren(...errors.map((e) => createText(e, "error")));
|
||||
if (!body || m01Same) return;
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const got = opt.key
|
||||
@@ -196,7 +181,7 @@ export async function mountMyMachineForm(
|
||||
createText(error instanceof Error ? error.message : mt("Failed"), "error"),
|
||||
);
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.disabled = m01Same;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -238,7 +223,11 @@ export async function mountMyMachineForm(
|
||||
server.replaceChildren(
|
||||
...(one ? [createText(`${mt("Server_Hourly")} ${hourlyText(one.시간당)}`)] : []),
|
||||
);
|
||||
if (one?.같은이름.length) form.setNotice(sameText(one.같은이름));
|
||||
if (one?.같은이름.length) {
|
||||
m01Same = one.같은이름.some((h) => h.출처 === "M01");
|
||||
saveBtn.disabled = m01Same;
|
||||
sheet.setNotice(sameText(one.같은이름));
|
||||
}
|
||||
const uses = one?.쓰는곳 ?? [];
|
||||
usesBox.replaceChildren(
|
||||
el("strong", { text: `${mt("Uses")} ${uses.length}` }),
|
||||
@@ -261,19 +250,13 @@ export async function mountMyMachineForm(
|
||||
delBtn,
|
||||
],
|
||||
}),
|
||||
createText(mt(opt.readOnly ? "View_Note" : "Own_Note"), "muted"),
|
||||
createText(mt("Own_Note"), "muted"),
|
||||
notice,
|
||||
server,
|
||||
form.root,
|
||||
sheet.root,
|
||||
...(opt.key ? [usesBox] : []),
|
||||
],
|
||||
});
|
||||
host.replaceChildren(root);
|
||||
if (!row && (opt.openSimilar ?? !seed))
|
||||
root.querySelector<HTMLDetailsElement>("details")?.setAttribute("open", "");
|
||||
if (seed) {
|
||||
form.setNotice(mt("Copied").replace("{value}", `${seed.이름} ${seed.규격}`.trim()));
|
||||
opt.onDirty?.(true);
|
||||
}
|
||||
return { root, dirty };
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
GET `/api/m02/my-machine-lists` = 양식 고르기(연료 · 직종 · 전력 단가 · 시간환산) —
|
||||
M01 기계 화면과 같은 값(`M01_MasterData_Machine.lists` · 그 길은 시스템 관리자만).
|
||||
GET `/api/m02/layers/{층}/my-machine-find?q=&owner=` = 「비슷한 기계에서 시작」 ·
|
||||
「복사해 내 기계로」 — 그 층 내 기계(앞) + M01 기계(`M01_MasterData_Machine.find`) ·
|
||||
GET `/api/m02/layers/{층}/my-machine-find?q=&owner=` = 「비슷한 기계에서 시작」 · 같은 이름 알림 —
|
||||
그 층 내 기계(앞) + M01 기계(`M01_MasterData_Machine.find`) ·
|
||||
q 가 M01 원문번호와 같으면 그 기계 먼저.
|
||||
GET `/api/m02/layers/{층}/my-items?owner=` = 내 품목 — 그 층 단가산출 수식 문서 재료비 줄 가운데
|
||||
단가가 우리 자재에 안 이어진 것(단가 변수 연결 = 숫자 · 입력) ·
|
||||
(이름 · 규격 · 단위 · 값) 묶음마다 쓰는 단가산출.
|
||||
POST `/api/m02/machine-sheet?owner=` 몸 {줄, 본?, 덮음?, 개인?} = 기계 보기(840-2 · `M02_MyMachine_Sheet` · 쓰기 없음 ·
|
||||
개인 = 그 개인 층 · 아니면 M01 만).
|
||||
권한 = 내 기계 길과 같음(`M02_MyMachine_Router._folder` — 개인 층 · 본인 · 맡은 회사 관리자 읽기).
|
||||
⚠ 등록은 `main.py`(로그인만).
|
||||
"""
|
||||
@@ -18,6 +20,7 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
@@ -125,3 +128,21 @@ def get_items(
|
||||
layer: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
return {"rows": _call(_folder(session, layer, owner), items)}
|
||||
|
||||
|
||||
class SheetBody(BaseModel):
|
||||
줄: dict[str, Any]
|
||||
본: dict[str, Any] | None = None
|
||||
덮음: dict[str, Any] | None = None
|
||||
개인: bool = False
|
||||
|
||||
|
||||
@router.post("/machine-sheet")
|
||||
def post_sheet(
|
||||
body: SheetBody, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""기계 보기(840-2 · `M02_MyMachine_Sheet`) — 개인 = 내 기계(그 층) · 아니면 M01 기계만 · 쓰기 없음."""
|
||||
from M02_MasterTemplete import M02_MyMachine_Sheet as ms
|
||||
|
||||
folder = _folder(session, "personal", owner) if body.개인 else store.FOLDER
|
||||
return _call(folder, ms.sheet, body.줄, body.본, body.덮음, body.개인)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""M02 내 기계 길(PLAN 821-1 · `M02_MyMachine`) — `/api/m02/layers/personal/my-machines`(?owner=).
|
||||
|
||||
GET 목록(줄마다 시간당 · 쓰는곳수) · GET `/{키}` 하나(+시간당 · 쓰는곳 · 같은이름) ·
|
||||
POST 만들기 몸 {줄?, 본뜬?: M01 · 내 기계 키(「비슷한 기계에서 시작」 · 「M01 기계 복사해 만들기」)} ·
|
||||
POST 만들기 몸 {줄?, 본뜬?: M01 · 내 기계 키(「비슷한 기계에서 시작」)} · 만들기 · 고치기 = M01 과 같은 이름 · 규격 409(840-2) ·
|
||||
PUT `/{키}` 몸 {줄} · DELETE `/{키}`(답 = 쓰는곳 — 그 단가산출은 사본의 마지막 값 그대로) ·
|
||||
POST `/draft` 몸 {줄, 키?, 본뜬?} = 단가산출 기계 창 [확인](827-1 · 쓰기 없음 · 층 쓰기 권한) → {줄, 셈값, 시간당}.
|
||||
권한 = 층 권한 한 벌(`Router_Items._root` — 본인 개인 층 · 맡은 회사 관리자) · 개인 층만(그 밖 404).
|
||||
@@ -66,7 +66,7 @@ def my_machine_create(
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
):
|
||||
return _call(_folder(session, layer, owner, True), mym.create, body.줄, body.본뜬)
|
||||
return _call(_folder(session, layer, owner, True), mym.create_own, body.줄, body.본뜬)
|
||||
|
||||
|
||||
@router.put(PATH + "/{key}")
|
||||
@@ -77,7 +77,7 @@ def my_machine_update(
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
):
|
||||
return _call(_folder(session, layer, owner, True), mym.update, key, body.줄)
|
||||
return _call(_folder(session, layer, owner, True), mym.update_own, key, body.줄)
|
||||
|
||||
|
||||
@router.delete(PATH + "/{key}")
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""기계 보기 서버 몫(PLAN 840-2 · 요구 853 · 862) — M01 기계 상세 · 내 기계가 같이 씀 · 쓰기 없음.
|
||||
|
||||
`sheet(줄, 본, 덮음, 개인)` → {키, 이름, 까닭, 문서, 값, 연결, 직종}
|
||||
- 식 고름 = `M02_Machine_Hourly.pick`(지금 줄 · 내 기계 = 셈 꼴 `M02_MyMachine.engine`) · 못 고름 = 키 없음 + 까닭
|
||||
- 문서 = 그 식 수식 문서(그대로) · 값 = `본`(처음 줄 · 없으면 줄)의 셈 꼴 값 칸 — 셈 차례 = 덮음(`기계덮음` 꼴 ·
|
||||
836-1) 얹음 → 값 박음(`bind` — 남은 요소 `EQ:{S_기계}` 칸 → 숫자 · 저장 전 새 기계 · 내 기계도 같은 계산기로 셈)
|
||||
- 파이썬 · TS 짝 — `bind` ↔ `common_util/common_util_machine_sheet.ts` `bindDoc` · 거울 시험 test_840_2_machine_sheet.py
|
||||
- 연결 = 덮음 얹은 문서 셈이 쓴 요소 · 표 · 문서(`master_doc_record`) — 화면이 같은 계산기로 즉시 다시 셈
|
||||
- 직종 = 노임 요소 코드 → 인력 원문번호(조종원 줄 → 기계 줄 `조종원직종`)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
|
||||
|
||||
def _plain(v: Any) -> Any:
|
||||
if isinstance(v, Decimal):
|
||||
return int(v) if v == v.to_integral_value() else float(v)
|
||||
return v
|
||||
|
||||
|
||||
def bind(doc: dict, vals: dict) -> dict:
|
||||
"""식 문서 → 기계 값 칸을 숫자로 박은 새 문서(그 칸이 없는 줄은 요소 그대로)."""
|
||||
out = copy.deepcopy(doc)
|
||||
for v in out.get("변수") or []:
|
||||
link = v.get("연결") or {}
|
||||
key = str(link.get("키") or "")
|
||||
col = link.get("칸")
|
||||
if (
|
||||
link.get("갈래") == "요소"
|
||||
and key.startswith("EQ:")
|
||||
and "{S_기계}" in key
|
||||
and col in vals
|
||||
):
|
||||
v["연결"] = {"갈래": "숫자", "값": vals[col]}
|
||||
return out
|
||||
|
||||
|
||||
def _shape(row: dict, mine: bool) -> dict:
|
||||
if mine:
|
||||
return mym.engine(row)
|
||||
return {**row, "값": m01._dec(row.get("값") or {})}
|
||||
|
||||
|
||||
def _jobs() -> dict[str, str]:
|
||||
rows = (m01.loaded()[0].get("인력.json") or {}).get("줄") or []
|
||||
return {str(r["키"]): str(r.get("원문번호")) for r in rows if r.get("키")}
|
||||
|
||||
|
||||
def sheet(row: Any, base: Any = None, cover: Any = None, mine: bool = False) -> dict:
|
||||
import master_doc_calc as mc
|
||||
import master_doc_cover as mcv
|
||||
import master_doc_link as ml
|
||||
import master_doc_record as mr
|
||||
|
||||
from M01_MasterData import M01_MasterData_Doc as m01doc
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||||
|
||||
if not isinstance(row, dict):
|
||||
raise m01.StoreError(400, "기계 줄이 묶음 아님")
|
||||
cur = _shape(row, mine)
|
||||
logic = mh.pick(cur)
|
||||
out: dict[str, Any] = {"직종": _jobs()}
|
||||
doc = m01doc.docs().get(str(logic["키"])) if logic else None
|
||||
if doc is None:
|
||||
why = mh.why(cur, logic) or ("식 문서 없음" if logic else "식 없음")
|
||||
return out | {"키": None, "까닭": why, "문서": None, "값": {}, "연결": {}}
|
||||
vals = _shape(base if isinstance(base, dict) else row, mine).get("값") or {}
|
||||
no = str(row.get("원문번호") or row.get("키") or "새 기계")
|
||||
given = {"기계": no}
|
||||
entry = cover if isinstance(cover, dict) and cover else None
|
||||
try:
|
||||
covered = mcv.cover_doc(doc, given, mcv.clean({no: m01._dec(entry)})) if entry else doc
|
||||
except ValueError as e:
|
||||
raise m01.StoreError(400, str(e)) from None
|
||||
shaped = bind(covered, vals)
|
||||
links = ml.Links(mym.whole(), m01doc.docs())
|
||||
rec = mr.Rec(links)
|
||||
rec.prefetch(shaped)
|
||||
mc.calc(shaped, rec, given)
|
||||
got = {"키": logic["키"], "이름": logic.get("이름"), "까닭": mh.why(cur, logic), "문서": doc,
|
||||
"값": vals, "연결": rec.out()} # fmt: skip
|
||||
return json.loads(json.dumps(out | got, ensure_ascii=False, default=_plain))
|
||||
@@ -18,7 +18,7 @@ const TEXT = {
|
||||
Uses_Count: "쓰는 곳",
|
||||
Made: "넣은 날",
|
||||
Edited: "고친 날",
|
||||
None: "내 기계 없음 — 단가산출의 기계 줄에서 [고쳐 쓰기] 로 만듦",
|
||||
None: "내 기계 없음 — [새 기계] 로 M01 에 없는 기계를 등록",
|
||||
Save: "저장",
|
||||
Saved: "내 기계 저장 — 서버가 시간당을 다시 셈",
|
||||
Delete: "지우기",
|
||||
@@ -50,7 +50,8 @@ const TEXT = {
|
||||
// 기계 고르기 창 · 줄 쌓기
|
||||
Pick_Group: "내 기계",
|
||||
Pick_Direct: "시간당 경비 식이 없는 기계 — 줄 쌓기에 못 넣음",
|
||||
View_Note: "보기 · 쓰는 곳 · 지우기만 — 만들기 · 고치기는 단가산출의 기계 줄에서",
|
||||
View_Note:
|
||||
"M01 에 없는 기계를 등록하는 자리 · 줄 누름 = 열고 고침 · M01 기계 값은 단가산출 안 기계 제어 구역에서 고침",
|
||||
// 단가산출 기계 창(827-1)
|
||||
Win_Title: "기계",
|
||||
Win_Kind: "구분",
|
||||
@@ -74,7 +75,7 @@ const TEXT = {
|
||||
Why_Company: "개인용으로 복사해서 씀",
|
||||
// 내 품목
|
||||
Items_Note:
|
||||
"단가산출 줄에 값을 바로 적은 재료(우리 자재에 안 이어진 것) — 고치기는 그 단가산출에서 · 줄 누르면 쓰는 단가산출",
|
||||
"단가산출 줄에 값을 바로 적은 재료(재료 DB · 내 재료에 안 이어진 것 · 읽기만) — 고치기는 그 단가산출에서 · 줄 누르면 쓰는 단가산출",
|
||||
Items_None: "내 품목 없음",
|
||||
Value: "값(원)",
|
||||
Unit: "단위",
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
* M02_MyMachine_View.ts
|
||||
* M02 「내 기계」 화면(PLAN 821-3 · 요구서 796 3 장) — 좌측 「개인관리」 › [기계] 단추로 가운데에 뜸(개인 층).
|
||||
* 목록 = 이름 · 규격 · 방식 · 시간당 경비(서버 셈) · 쓰는 곳 수 · 넣은 날 · 고친 날 · 찾기
|
||||
* 줄 누름 = 양식 한 장(`M02_MyMachine_Form` · 읽기 전용) · 쓰는 곳 · [지우기] → 목록 다시
|
||||
* 만들기 · 고치기 없음(827-1) — 단가산출 기계 줄의 기계 창 [고쳐 쓰기] → 단가산출 [저장] 때 내 기계로
|
||||
* 줄 누름 = 기계 보기 한 장(`M02_MyMachine_Form` · 840-2 M01 기계 상세와 같은 꼴) · 고침 · [저장] · 쓰는 곳 · [지우기]
|
||||
* [새 기계] = M01 에 없는 기계 등록(비슷한 기계에서 시작 · M01 과 같은 이름 · 규격 = 막음) · 불러오기 자동 생성은 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { createInputField, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { listMyMachines, type MyMachineRow } from "./M02_MyMachine_Api";
|
||||
@@ -71,7 +72,20 @@ export function buildMyMachineView(opt: MyMachineViewOptions): HTMLElement {
|
||||
const list = el("div", {
|
||||
className: "m02-mymach-view__list",
|
||||
children: [
|
||||
el("div", { className: "ui-toolbar", children: [search.root, hint] }),
|
||||
el("div", {
|
||||
className: "ui-toolbar",
|
||||
children: [
|
||||
search.root,
|
||||
hint,
|
||||
createButton({
|
||||
label: mt("New"),
|
||||
variant: "filled",
|
||||
size: 28,
|
||||
iconStart: "plus",
|
||||
onClick: () => void openForm(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createText(mt("View_Note"), "muted"),
|
||||
fail,
|
||||
table.root,
|
||||
@@ -94,14 +108,13 @@ export function buildMyMachineView(opt: MyMachineViewOptions): HTMLElement {
|
||||
draw();
|
||||
}
|
||||
|
||||
async function openForm(key: string): Promise<void> {
|
||||
async function openForm(key?: string): Promise<void> {
|
||||
const slot = el("div");
|
||||
host.replaceChildren(slot);
|
||||
try {
|
||||
const h = await mountMyMachineForm(slot, {
|
||||
owner: opt.owner,
|
||||
key,
|
||||
readOnly: true,
|
||||
onSaved: (got) => void openForm(got.줄.키),
|
||||
onDeleted: () => void showList(),
|
||||
onBack: async () => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* =============================================================================
|
||||
* M02_MyMaterial_Api.ts
|
||||
* M02 내 재료(개인 자재 PM · PLAN 840-4) 서버 호출 — 개인 층만 · 서버 `M02_Labor_Router` 의 /layers/personal/materials
|
||||
* 목록(줄마다 쓰는 곳) · 만들기 · 고치기 · 지우기(쓰는 곳 있으면 409) · M01 재료 DB 와 같은 이름 · 규격 · 단위 = 409
|
||||
* ========================================================================== */
|
||||
|
||||
import { call } from "./M02_MasterTemplete_Api_Fetch";
|
||||
|
||||
export interface MyMaterialUse {
|
||||
종류: string;
|
||||
키: string;
|
||||
이름: string;
|
||||
}
|
||||
|
||||
export interface MyMaterial {
|
||||
키: string;
|
||||
이름: string;
|
||||
규격: string;
|
||||
단위: string;
|
||||
값: number | null;
|
||||
쓰는곳?: MyMaterialUse[];
|
||||
}
|
||||
|
||||
export type MyMaterialBody = Pick<MyMaterial, "이름" | "규격" | "단위" | "값">;
|
||||
|
||||
const path = (owner: string | null, key = ""): string =>
|
||||
`/layers/personal/materials${key ? `/${encodeURIComponent(key)}` : ""}${
|
||||
owner ? `?owner=${encodeURIComponent(owner)}` : ""
|
||||
}`;
|
||||
|
||||
export const listMyMaterials = (owner: string | null): Promise<MyMaterial[]> =>
|
||||
call<{ 줄: MyMaterial[] }>(path(owner)).then((d) => d.줄);
|
||||
|
||||
export const addMyMaterial = (owner: string | null, body: MyMaterialBody): Promise<MyMaterial> =>
|
||||
call<MyMaterial>(path(owner), { method: "POST", body: JSON.stringify(body) });
|
||||
|
||||
export const editMyMaterial = (
|
||||
owner: string | null,
|
||||
key: string,
|
||||
body: MyMaterialBody,
|
||||
): Promise<MyMaterial> =>
|
||||
call<MyMaterial>(path(owner, key), { method: "PUT", body: JSON.stringify(body) });
|
||||
|
||||
export const dropMyMaterial = (owner: string | null, key: string): Promise<unknown> =>
|
||||
call(path(owner, key), { method: "DELETE" });
|
||||
@@ -0,0 +1,185 @@
|
||||
/* =============================================================================
|
||||
* M02_MyMaterial_View.ts
|
||||
* M02 개인관리 › 자재 위 「내 재료」 등록 칸(PLAN 840-4 · 요구 854 · 862) — 개인 층
|
||||
* 내 재료 = 재료 DB(M01)에 없는 자재를 등록하는 곳 · M01 재료 값 바꿈은 단가산출 안 재료 줄에서
|
||||
* 표 = 이름 · 규격 · 단위 · 값 · 쓰는 곳 · 키 · 줄 누름 = 아래 칸에서 고치기 · [새 자재] = 빈 칸
|
||||
* [저장] = 서버(M01 과 이름 · 규격 · 단위 같으면 서버가 막음 — 까닭 그대로 알림) · [지우기] = 쓰는 곳 있으면 서버가 막음
|
||||
* 값을 고치면 그 재료를 쓰는 개인 층 단가산출이 따라옴(836-2 자동 따라옴 · 프로젝트 층 = 사본 값)
|
||||
* 남의 층(owner 있음) = 읽기만
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { createInputField, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
|
||||
import {
|
||||
addMyMaterial,
|
||||
dropMyMaterial,
|
||||
editMyMaterial,
|
||||
listMyMaterials,
|
||||
type MyMaterial,
|
||||
} from "./M02_MyMaterial_Api";
|
||||
|
||||
const TEXT = {
|
||||
title: "내 재료",
|
||||
note: "재료 DB(M01)에 없는 자재를 등록하는 곳 — 단가산출 재료 고르기 「내 재료」 로 씀 · M01 재료 값 바꿈은 단가산출 안 재료 줄에서",
|
||||
none: "내 재료 없음",
|
||||
add: "새 자재",
|
||||
name: "품명",
|
||||
spec: "규격",
|
||||
unit: "단위",
|
||||
value: "값(원)",
|
||||
uses: "쓰는 곳",
|
||||
key: "키",
|
||||
save: "저장",
|
||||
drop: "지우기",
|
||||
cancel: "취소",
|
||||
need: "품명 · 단위는 꼭 적음",
|
||||
saved: "저장함 — 쓰는 단가산출이 새 값으로 셈",
|
||||
dropped: "지움",
|
||||
dropAsk: "「{name}」 을 지움",
|
||||
readOnly: "남의 층 — 읽기만",
|
||||
} as const;
|
||||
|
||||
export interface MyMaterialBlockOptions {
|
||||
owner: string | null;
|
||||
}
|
||||
|
||||
export function buildMyMaterialBlock(opt: MyMaterialBlockOptions): HTMLElement {
|
||||
const canWrite = opt.owner === null;
|
||||
let rows: MyMaterial[] = [];
|
||||
let editing: MyMaterial | null = null;
|
||||
let unit = "";
|
||||
const table = createDataTable({
|
||||
caption: TEXT.title,
|
||||
columns: [
|
||||
{ key: "이름", label: TEXT.name, width: 200 },
|
||||
{ key: "규격", label: TEXT.spec, width: 140 },
|
||||
{ key: "단위", label: TEXT.unit, width: 60 },
|
||||
{ key: "값", label: TEXT.value, kind: "number", decimals: 2, trim: true, width: 100 },
|
||||
{ key: "쓰는곳", label: TEXT.uses, kind: "number", decimals: 0, width: 70 },
|
||||
{ key: "키", label: TEXT.key, width: 90 },
|
||||
],
|
||||
rows: [],
|
||||
empty: TEXT.none,
|
||||
onRowClick: (i) => canWrite && open(rows[i]),
|
||||
});
|
||||
const name = createInputField({ label: TEXT.name, required: true });
|
||||
const spec = createInputField({ label: TEXT.spec });
|
||||
const price = createInputField({ label: TEXT.value, type: "number", min: 0 });
|
||||
const unitHost = el("span");
|
||||
const drawUnit = (): void =>
|
||||
unitHost.replaceChildren(
|
||||
createUnitPick({
|
||||
value: unit || NO_UNIT,
|
||||
ariaLabel: TEXT.unit,
|
||||
onPick: (u) => {
|
||||
unit = u === NO_UNIT ? "" : u;
|
||||
drawUnit();
|
||||
},
|
||||
}),
|
||||
);
|
||||
const head = createText("");
|
||||
const saveBtn = createButton({ label: TEXT.save, variant: "filled", onClick: () => void save() });
|
||||
const dropBtn = createButton({
|
||||
label: TEXT.drop,
|
||||
variant: "danger-ghost",
|
||||
onClick: () => void drop(),
|
||||
});
|
||||
const form = el("div", {
|
||||
className: "m02-pick__new",
|
||||
attrs: { "data-my-material-form": "" },
|
||||
children: [
|
||||
head,
|
||||
name.root,
|
||||
spec.root,
|
||||
el("div", {
|
||||
className: "ui-field",
|
||||
children: [el("label", { className: "ui-field__label", text: TEXT.unit }), unitHost],
|
||||
}),
|
||||
price.root,
|
||||
el("div", {
|
||||
className: "ui-btn-group",
|
||||
children: [
|
||||
createButton({ label: TEXT.cancel, variant: "ghost", onClick: () => close() }),
|
||||
dropBtn,
|
||||
saveBtn,
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
form.hidden = true;
|
||||
|
||||
function open(row: MyMaterial | null): void {
|
||||
editing = row;
|
||||
name.input.value = row?.이름 ?? "";
|
||||
spec.input.value = row?.규격 ?? "";
|
||||
price.input.value = row?.값 === null || row?.값 === undefined ? "" : String(row.값);
|
||||
unit = row?.단위 ?? "";
|
||||
drawUnit();
|
||||
head.textContent = row ? `${row.키} ${row.이름}` : TEXT.add;
|
||||
dropBtn.hidden = !row;
|
||||
form.hidden = false;
|
||||
name.input.focus();
|
||||
}
|
||||
function close(): void {
|
||||
editing = null;
|
||||
form.hidden = true;
|
||||
}
|
||||
async function load(): Promise<void> {
|
||||
rows = await listMyMaterials(opt.owner).catch((e) => {
|
||||
showToast(e instanceof Error ? e.message : String(e), "error");
|
||||
return [];
|
||||
});
|
||||
table.setRows(rows.map((r) => ({ ...r, 쓰는곳: r.쓰는곳?.length ?? 0 })));
|
||||
}
|
||||
async function save(): Promise<void> {
|
||||
const 이름 = name.input.value.trim();
|
||||
if (!이름 || !unit) return showToast(TEXT.need, "error");
|
||||
const raw = price.input.value.trim();
|
||||
const body = {
|
||||
이름,
|
||||
규격: spec.input.value.trim(),
|
||||
단위: unit,
|
||||
값: raw === "" ? null : Number(raw),
|
||||
};
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
if (editing) await editMyMaterial(opt.owner, editing.키, body);
|
||||
else await addMyMaterial(opt.owner, body);
|
||||
showToast(TEXT.saved, "success");
|
||||
close();
|
||||
await load();
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : String(e), "error");
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
async function drop(): Promise<void> {
|
||||
if (!editing) return;
|
||||
if (!(await showConfirmDialog(TEXT.dropAsk.replace("{name}", editing.이름), TEXT.drop))) return;
|
||||
try {
|
||||
await dropMyMaterial(opt.owner, editing.키);
|
||||
showToast(TEXT.dropped, "success");
|
||||
close();
|
||||
await load();
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : String(e), "error");
|
||||
}
|
||||
}
|
||||
|
||||
const addBtn = createButton({ label: TEXT.add, variant: "filled", onClick: () => open(null) });
|
||||
addBtn.hidden = !canWrite;
|
||||
void load();
|
||||
return el("div", {
|
||||
attrs: { "data-my-materials": "" },
|
||||
children: [
|
||||
el("div", { className: "ui-toolbar", children: [addBtn] }),
|
||||
createText(canWrite ? TEXT.note : TEXT.readOnly, "muted"),
|
||||
table.root,
|
||||
form,
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -44,7 +44,7 @@ def m01(kind: str, src: dict, label: str = "") -> tuple[str | None, str | None]:
|
||||
return got["키"], mfind.mark(got, line["이름"], line["규격"])
|
||||
if kind == "재료" and src.get("종류") == "자재" and (hit := ea.est_code(src.get("코드"))):
|
||||
key = ea.one(line, hit) or hit[0] # 815-1 K1 — EST 원파일 자재 = 우리 자재 출처 코드
|
||||
return mf.done(key, "코드", src)["키"], None
|
||||
return mf.done(key, "코드", src, line["단위"])["키"], None # 840-3 단위 맞춤
|
||||
oil = ea.keys("MO", line["이름"], line["규격"]) if kind == "재료" else []
|
||||
if kind == "재료":
|
||||
if oil or elem.oil_rows(line["이름"]):
|
||||
@@ -183,6 +183,9 @@ def why(item: dict) -> str:
|
||||
return mp.apart_note(res.get("재료찾기") or {})
|
||||
if elem.oil_rows(name):
|
||||
return "유가 후보 여럿 — 고름"
|
||||
found = res.get("재료찾기") or {}
|
||||
if found.get("판정") == "단위 다름 — 고르기": # 840-3 `M02_San_MatFind.UNIT`
|
||||
return f"{found.get('방법')} — 고름"
|
||||
line = {"이름": name, "규격": res.get("규격") or "", "단위": res.get("단위") or ""}
|
||||
if picks := hrow.m01_picks("재료", line): # 817-1 ㉯ 1 순위를 글에(고르기 창은 이름 찾기)
|
||||
return f"자재 후보 여럿 — 고름 · 1 순위 {picks[0]['이름']} {picks[0]['규격']}".strip()
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
- 「규격 없음 — 고르기」 — 가져온 규격 빔 · 별도계상 · 바탕 이름 같은 후보 있음
|
||||
- 「후보 여럿」 — 규격은 있는데 하나로 못 정함
|
||||
- 「우리 표에 없음」 — 바탕 이름 같은 후보 없음 · 규격 수가 하나도 안 닮음(후보 = 닮은 줄 · 참고)
|
||||
- 「단위 다름 — 고르기」 — 이은 우리 줄 단위가 줄 단위와 성격 다름(840-3 · 방법 = 「단위 다름 ㎡ ↔ 매」 · 후보 첫째 = 그 줄)
|
||||
단위 맞춤(840-3 `_by_unit` · 이은 판정 모두) = ㉠ 이름 · 규격 같은 쌍둥이 중 줄과 같은 단위 줄 › ㉡ 같은 성격(kg ↔ 톤 등)
|
||||
= 그대로 이음 · 값은 셈이 줄 단위로 환산(`master_pick.priced`) · ㉢ 성격 다름 = 이음 풂.
|
||||
후보 = [{키, 이름, 규격, 단위, 단서?}] 차례 = 단서로 좁힌 것 › 이름 같은 묶음 › 옛 후보.
|
||||
별도계상 줄(요구 811-2 — 품목은 정해짐 · 셈만 프로젝트에서) = 같은 찾기 · 키는 판정에만(줄 이음 · 단가 0 그대로).
|
||||
|
||||
@@ -35,6 +38,7 @@ from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_MatSame as ms
|
||||
|
||||
LINKED, NO_SPEC, MANY, NONE = "이어짐", "규격 없음 — 고르기", "후보 여럿", "우리 표에 없음"
|
||||
UNIT = "단위 다름 — 고르기"
|
||||
TOP = 8
|
||||
_CTX: ContextVar[dict] = ContextVar("재료단서", default={})
|
||||
_LEAD = re.compile(r"^[\s\-·\d.)]+")
|
||||
@@ -126,16 +130,16 @@ def find(
|
||||
code = str((src or {}).get("코드") or "")
|
||||
me = ms._item(text[0], text[1])
|
||||
keys = tuple(sorted(k for k in keys if k))
|
||||
got = dict(_find(*text, keys, _texts(me, code, label), apart, id(hm.targets())))
|
||||
got = _by_unit(line, dict(_find(*text, keys, _texts(me, code, label), apart, id(hm.targets()))))
|
||||
got |= {"후보": [dict(x) for x in got["후보"]], "단서": list(got["단서"])}
|
||||
if src is not None:
|
||||
src["재료찾기"] = got
|
||||
return got
|
||||
|
||||
|
||||
def done(key: str, how: str, src: dict | None = None) -> dict:
|
||||
def done(key: str, how: str, src: dict | None = None, unit: str = "") -> dict:
|
||||
"""이미 정한 이음(코드 길) 판정."""
|
||||
got = {"판정": LINKED, "방법": how, "키": key, "후보": [], "단서": []}
|
||||
got = _by_unit({"단위": unit}, {"판정": LINKED, "방법": how, "키": key, "후보": [], "단서": []})
|
||||
if src is not None:
|
||||
src["재료찾기"] = got
|
||||
return got
|
||||
@@ -169,6 +173,33 @@ def _find(
|
||||
return _out(verdict, "", None, cands, used)
|
||||
|
||||
|
||||
def _by_unit(line: dict, got: dict) -> dict:
|
||||
"""이은 판정 단위 맞춤(840-3) — ㉠ 같은 단위 쌍둥이 › ㉡ 같은 성격 = 그대로 › ㉢ 성격 다름 = 고르기."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
key, want = got.get("키"), str(line.get("단위") or "")
|
||||
if got["판정"] != LINKED or not key or not want:
|
||||
return got
|
||||
row = ea.row(ea.MARKET, key)
|
||||
have, plain = str(row.get("단위") or ""), hm.mm.plain_unit
|
||||
if not have or plain(have) == plain(want):
|
||||
return got
|
||||
twins = [k for k in ea.exact(row.get("이름"), row.get("규격")) if plain(ea.row(ea.MARKET, k).get("단위")) == plain(want)] # fmt: skip
|
||||
if twin := ea.one({"이름": line.get("이름") or row.get("이름"), "규격": line.get("규격") or row.get("규격"), "단위": want}, twins): # fmt: skip
|
||||
return {**got, "키": twin, "방법": f"{got['방법']} · 같은 단위 줄"}
|
||||
if hm.mp.same_kind(have, want):
|
||||
return got # 값 = 셈이 줄 단위로 환산 · 표시도 셈(`단위경고`)
|
||||
first = dict(_pub({**row, "자재": key}, ""))
|
||||
rest = [dict(c) for c in got["후보"] if dict(c).get("키") != key]
|
||||
return {
|
||||
**got,
|
||||
"판정": UNIT,
|
||||
"방법": f"단위 다름 {want} ↔ {have}",
|
||||
"키": None,
|
||||
"후보": [first, *rest],
|
||||
}
|
||||
|
||||
|
||||
def _alike(me: dict, x: dict) -> bool:
|
||||
"""이름 같은 우리 줄이 이 물건일 수 있음 — 우리 규격 빔 · 가져온 수 없음 · 수 하나라도 같음(BUTTON BIT Ø38 ↔ 105mm = 아님)."""
|
||||
return (
|
||||
|
||||
@@ -31,6 +31,7 @@ DIRNAME = "재료고름"
|
||||
CENSUS = "재료판"
|
||||
KINDS = (
|
||||
"규격 없음 — 고르기",
|
||||
"단위 다름 — 고르기", # 840-3
|
||||
"후보 여럿",
|
||||
"우리 표에 없음",
|
||||
"이어짐",
|
||||
|
||||
@@ -45,10 +45,11 @@ READS: ContextVar[dict | None] = ContextVar("읽은줄", default=None)
|
||||
|
||||
|
||||
def whole_() -> Any:
|
||||
"""M01 색인 + 지금 층 내 기계 줄(821-1 · `M02_MyMachine.whole`)."""
|
||||
"""M01 색인 + 지금 층 내 기계 줄(821-1 · `M02_MyMachine.whole`) + 내 재료 줄(840-4 · `M02_Material_Own.whole`)."""
|
||||
from M02_MasterTemplete import M02_Material_Own as own
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
|
||||
return mym.whole()
|
||||
return own.whole(mym.whole())
|
||||
|
||||
|
||||
def _fp(rows: Any) -> str:
|
||||
|
||||
@@ -24,7 +24,7 @@ export const COVER = "기계덮음";
|
||||
/** 값 칸에서 빼는 기계 칸 — 줄(조종원 · 잡품)로 다룸 */
|
||||
export const NOT_VALUE = ["조종원", "조종원직종", "잡재료율"];
|
||||
const HOUR = "Q_시간환산";
|
||||
/** 조종원 줄을 새로 넣을 때(조종원 없는 식) 시간환산 — GC000267 과 같은 꼴(건설품셈 공통 1-1-3 2) */
|
||||
/** 조종원 줄을 새로 넣을 때(조종원 없는 식) 시간환산 — GC000267 과 같은 꼴(1/8 × 16/12 × 25/20 · 건협기술-2230) */
|
||||
const HOUR_VARS: DocVar[] = [
|
||||
{
|
||||
열쇠: "Q_분자^표", 기호: "Q", 아래: "분자", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분자", 단위: "",
|
||||
@@ -34,9 +34,28 @@ const HOUR_VARS: DocVar[] = [
|
||||
열쇠: "Q_분모^표", 기호: "Q", 아래: "분모", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분모", 단위: "",
|
||||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'시간환산'" }, 칸: "분모" },
|
||||
},
|
||||
{
|
||||
열쇠: "Q_분자2^표", 기호: "Q", 아래: "분자2", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분자", 단위: "",
|
||||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'상여계수'" }, 칸: "분자" },
|
||||
},
|
||||
{
|
||||
열쇠: "Q_분모2^표", 기호: "Q", 아래: "분모2", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분모", 단위: "",
|
||||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'상여계수'" }, 칸: "분모" },
|
||||
},
|
||||
{
|
||||
열쇠: "Q_분자3^표", 기호: "Q", 아래: "분자3", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분자", 단위: "",
|
||||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'휴지계수'" }, 칸: "분자" },
|
||||
},
|
||||
{
|
||||
열쇠: "Q_분모3^표", 기호: "Q", 아래: "분모3", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분모", 단위: "",
|
||||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'휴지계수'" }, 칸: "분모" },
|
||||
},
|
||||
{ 열쇠: "시간환산", 기호: "Q", 아래: "시간환산", 위: "", 이름: "시간환산", 단위: "", 연결: { 갈래: "식" } },
|
||||
]; // prettier-ignore
|
||||
const HOUR_LINE = { 기호: HOUR, 식: "Q_분자^표 ÷ Q_분모^표" };
|
||||
const HOUR_LINE = {
|
||||
기호: HOUR,
|
||||
식: "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표",
|
||||
};
|
||||
|
||||
export interface CrewRow {
|
||||
키: string;
|
||||
|
||||
@@ -20,6 +20,8 @@ export const MACHINE_COEFS = ["상각비계수", "정비비계수", "관리비
|
||||
export type MachineCoefs = Record<(typeof MACHINE_COEFS)[number], number>;
|
||||
|
||||
const E = 1e-6; // 끊는 경계 부동소수 흔들림 막음
|
||||
/** 조종원 하루 → 시간(CC000336 시간환산 · 상여 · 휴지 = 1/8 × 16/12 × 25/20 · 840-5) */
|
||||
export const MACHINE_HOUR = ((((1 / 8) * 16) / 12) * 25) / 20; // 파이썬 짝과 같은 차례
|
||||
|
||||
const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
||||
const halfUp = (x: number): number => Math.floor(x + 0.5 + E);
|
||||
@@ -69,7 +71,7 @@ export function machineHourly(
|
||||
fuel: number,
|
||||
wage: number,
|
||||
power = 0,
|
||||
hour = 1 / 8,
|
||||
hour = MACHINE_HOUR,
|
||||
): MachineHourly {
|
||||
const loss = Math.floor((num(values["가격"]) * 1000 * num(values["손료계수"])) / 1e7 + E);
|
||||
const oil = tenth(num(values["주연료"]) * num(fuel));
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/* =============================================================================
|
||||
* common_util_machine_sheet.ts
|
||||
* 기계 보기 셈 꼴(PLAN 840-2 · 요구 853 · 862) — 기계 줄(저장 꼴) ↔ 식이 읽는 값 · 덮음(836-1 `기계덮음` 꼴) ↔ 줄
|
||||
* 셈 꼴(`engineVals`) = M01 = 값 그대로 · 내 기계 = 바로 → 상각비계수 ← 손료계수 · 정비 · 관리 0 ·
|
||||
* 직접 → 노무비 · 재료비 · 경비 · 빈 연료 · 조종원 칸 = 0(서버 `M02_MyMachine.engine` 과 같은 꼴)
|
||||
* 덮음 ← 줄 차이(`coverFrom` — 처음 줄 대 지금 줄) · 지금 줄 ← 덮음(`applyCover` — 제어 구역이 맡은 칸만 ·
|
||||
* 덮음이 없는 칸 = 처음 줄 값 그대로 = 안 고치고 닫으면 줄 바이트 같음)
|
||||
* 제어 구역이 맡은 칸(`ownedCols`) = 식이 읽는 값 칸 + 조종원 · 조종원직종 + (잡품 줄 있는 식) 잡재료율
|
||||
* 값 박음(`bindDoc`) = 식 문서 요소 `EQ:{S_기계}` 칸 → 숫자(처음 줄 값) — 셈 차례 = 덮음 얹음(`coverDoc`) → 값 박음
|
||||
* 파이썬 · TS 짝(`bindDoc`) — 짝 = M02_MasterTemplete/M02_MyMachine_Sheet.py `bind` · 거울 시험 test_840_2_machine_sheet.py
|
||||
* · 화면 · DOM · fetch 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import type { FormulaDoc, MoneyLine } from "./common_util_formula_doc";
|
||||
import type { CoverParts, MachineCover } from "./common_util_formula_doc_cover";
|
||||
import { MACHINE_INPUTS } from "./common_util_machine_coef";
|
||||
|
||||
export type MachineValues = Record<string, unknown>;
|
||||
/** 고르기 목록 한 칸 — 연료(값 = 종류) · 직종(값 = 인력 원문번호) */
|
||||
export interface MachinePick {
|
||||
value: string;
|
||||
text: string;
|
||||
price: number | null;
|
||||
}
|
||||
export type MachineMode = "바로" | "품셈" | "직접";
|
||||
/** 기계 줄(M01 기계 줄 · 내 기계 줄 꼴) — 이름 · 규격 · 값 · (내 기계) 방식 · 시간당 */
|
||||
export interface MachineRow {
|
||||
이름?: unknown;
|
||||
규격?: unknown;
|
||||
값?: MachineValues;
|
||||
방식?: MachineMode;
|
||||
시간당?: MachineValues;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export const HOURLY = ["노무비", "재료비", "경비"] as const;
|
||||
const CREW = ["조종원", "조종원직종"];
|
||||
const MISC = "잡재료율";
|
||||
|
||||
/** 보통 범위(옛 기계 양식과 같음) — 밖이면 경고만 */
|
||||
export const RANGE: Record<string, [number, number]> = {
|
||||
가격: [0, 5_000_000], 손료계수: [1, 100_000], 내용시간: [100, 100_000], 연간표준가동시간: [100, 8_760],
|
||||
상각비율: [0, 1], 정비비율: [0, 3], 연간관리비율: [0, 1], 주연료: [0, 2_000], 잡재료율: [0, 100],
|
||||
조종원: [0, 20], 전력: [0, 10_000],
|
||||
}; // prettier-ignore
|
||||
|
||||
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v ?? null)) as T;
|
||||
const blank = (v: unknown): boolean => v === undefined || v === null || v === "";
|
||||
|
||||
/** 식 문서 → 기계 값 칸을 숫자로 박은 새 문서(그 칸이 없는 줄은 요소 그대로) */
|
||||
export function bindDoc(doc: FormulaDoc, vals: MachineValues): FormulaDoc {
|
||||
const out = clone(doc);
|
||||
for (const v of out.변수 ?? []) {
|
||||
const l = v.연결 ?? {};
|
||||
const key = String(l.키 ?? "");
|
||||
const col = l.칸;
|
||||
if (
|
||||
l.갈래 === "요소" &&
|
||||
key.startsWith("EQ:") &&
|
||||
key.includes("{S_기계}") &&
|
||||
col &&
|
||||
col in vals
|
||||
)
|
||||
v.연결 = { 갈래: "숫자", 값: vals[col] as number | string };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 같은 값 — 수는 작은 차이 무시 · 글은 글로 */
|
||||
export function sameVal(a: unknown, b: unknown): boolean {
|
||||
if (blank(a) && blank(b)) return true;
|
||||
if (typeof a === "number" && typeof b === "number")
|
||||
return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
|
||||
return String(a ?? "") === String(b ?? "");
|
||||
}
|
||||
|
||||
export function modeOf(row: MachineRow, mine: boolean): MachineMode {
|
||||
if (mine) return row.방식 ?? "바로";
|
||||
return MACHINE_INPUTS.some((k) => typeof row.값?.[k] === "number") ? "품셈" : "바로";
|
||||
}
|
||||
|
||||
/** 식이 읽는 값 묶음(셈 꼴) */
|
||||
export function engineVals(row: MachineRow, mine: boolean): MachineValues {
|
||||
const v: MachineValues = { ...(row.값 ?? {}) };
|
||||
if (!mine) return v;
|
||||
const how = row.방식 ?? "바로";
|
||||
if (how === "직접") return Object.fromEntries(HOURLY.map((k) => [k, row.시간당?.[k] ?? 0]));
|
||||
if (how === "바로")
|
||||
Object.assign(v, { 상각비계수: v.손료계수 || 0, 정비비계수: 0, 관리비계수: 0 });
|
||||
return { 주연료: 0, 잡재료율: 0, 조종원: 0, 주연료종류: "", 조종원직종: "", ...v };
|
||||
}
|
||||
|
||||
/** 셈 꼴 칸 → 저장 꼴 자리 · null = 저장 안 함(내 기계 바로의 정비 · 관리 = 늘 0) */
|
||||
export function slotOf(
|
||||
col: string,
|
||||
mode: MachineMode,
|
||||
mine: boolean,
|
||||
): ["값" | "시간당", string] | null {
|
||||
if (!mine) return ["값", col];
|
||||
if (mode === "직접") return (HOURLY as readonly string[]).includes(col) ? ["시간당", col] : null;
|
||||
if (mode === "바로" && col === "상각비계수") return ["값", "손료계수"];
|
||||
if (mode === "바로" && (col === "정비비계수" || col === "관리비계수")) return null;
|
||||
return ["값", col];
|
||||
}
|
||||
|
||||
/** 제어 구역이 맡은 칸(셈 꼴 이름) */
|
||||
export const ownedCols = (parts: CoverParts): string[] => [
|
||||
...parts.fields.map((f) => f.칸),
|
||||
...CREW,
|
||||
...(parts.misc !== null ? [MISC] : []),
|
||||
];
|
||||
|
||||
/** 처음 줄 대 지금 줄 → 덮음(조종원 = 「LB:직종」 한 줄 · 잡품 = 밑 줄마다 같은 율) — 정리는 `putCover` 몫 */
|
||||
export function coverFrom(
|
||||
parts: CoverParts,
|
||||
lines: MoneyLine[],
|
||||
orig: MachineRow,
|
||||
cur: MachineRow,
|
||||
mine: boolean,
|
||||
): MachineCover {
|
||||
const o = engineVals(orig, mine);
|
||||
const c = engineVals(cur, mine);
|
||||
const out: MachineCover = {};
|
||||
const 값: Record<string, number | string> = {};
|
||||
for (const { 칸 } of parts.fields)
|
||||
if (!blank(c[칸]) && !sameVal(c[칸], o[칸])) 값[칸] = c[칸] as number | string;
|
||||
if (Object.keys(값).length) out.값 = 값;
|
||||
if (CREW.some((k) => !sameVal(c[k], o[k]))) {
|
||||
const n = Number(c.조종원) || 0;
|
||||
out.조종원 = n > 0 && !blank(c.조종원직종) ? [{ 키: `LB:${String(c.조종원직종)}`, 인: n }] : [];
|
||||
}
|
||||
if (parts.misc !== null && !sameVal(c[MISC], o[MISC])) {
|
||||
const r = Number(c[MISC]) || 0;
|
||||
out.잡품 = r ? parts.base.map((i) => ({ 줄: String(lines[i]?.이름 ?? ""), 율: r })) : [];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 노임 요소 키 → 인력 원문번호(`LB:1048` → 1048 · `LB000048` → 직종 표) */
|
||||
export const jobCode = (key: string, jobs: Record<string, string>): string =>
|
||||
key.startsWith("LB:") ? key.slice(3) : (jobs[key] ?? key);
|
||||
|
||||
/** 덮음 → 지금 줄(제어 구역이 맡은 칸만 · 덮음 없는 칸 = 처음 줄 저장 값으로) — 새 줄 */
|
||||
export function applyCover(
|
||||
parts: CoverParts,
|
||||
orig: MachineRow,
|
||||
cur: MachineRow,
|
||||
entry: MachineCover,
|
||||
mine: boolean,
|
||||
jobs: Record<string, string>,
|
||||
): MachineRow {
|
||||
const mode = modeOf(cur, mine);
|
||||
const next = clone(cur);
|
||||
const put = (col: string, v: unknown): void => {
|
||||
const at = slotOf(col, mode, mine);
|
||||
if (!at) return;
|
||||
const [part, key] = at;
|
||||
const box = (next[part] ??= {}) as MachineValues;
|
||||
if (v === undefined) {
|
||||
const was = (orig[part] ?? {}) as MachineValues;
|
||||
if (key in was) box[key] = clone(was[key]);
|
||||
else delete box[key];
|
||||
} else if (!(key in box) || !sameVal(box[key], v)) box[key] = v;
|
||||
};
|
||||
for (const { 칸 } of parts.fields) put(칸, entry.값?.[칸]);
|
||||
if (entry.조종원 === undefined) CREW.forEach((k) => put(k, undefined));
|
||||
else {
|
||||
const r = entry.조종원[0];
|
||||
put("조종원", r ? r.인 : 0);
|
||||
put("조종원직종", r ? jobCode(r.키, jobs) : undefined);
|
||||
}
|
||||
if (parts.misc !== null)
|
||||
put(MISC, entry.잡품 === undefined ? undefined : (entry.잡품[0]?.율 ?? 0));
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 범위 밖 칸 — [칸, 범위 글] */
|
||||
export function rangeWarnings(values: MachineValues): [string, string][] {
|
||||
return Object.entries(RANGE).flatMap(([k, [lo, hi]]) => {
|
||||
const v = values[k];
|
||||
return typeof v === "number" && (v < lo || v > hi)
|
||||
? [[k, `${lo}~${hi}`] as [string, string]]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
@@ -34,6 +34,10 @@ const NAME: Record<string, string> = {
|
||||
Q_시간환산: "환산",
|
||||
"Q_분자^표": "환산분자",
|
||||
"Q_분모^표": "환산분모",
|
||||
"Q_분자2^표": "상여분자",
|
||||
"Q_분모2^표": "상여분모",
|
||||
"Q_분자3^표": "휴지분자",
|
||||
"Q_분모3^표": "휴지분모",
|
||||
"Q_주연료^표": "휘발유량",
|
||||
"Q_주연료2^표": "오일량",
|
||||
"Q_잡^표": "잡품",
|
||||
|
||||
Binary file not shown.
@@ -47,7 +47,7 @@ RAW_SUB = (
|
||||
"(옛 엔진 = 끝수 전 날 값 · `master_formula.run` 결과를 식에 씀)"
|
||||
)
|
||||
# 채택된 차이 — (로직, 옵션) · 요구 714-3
|
||||
ADOPTED = {("GC000100", "지역=경북")}
|
||||
ADOPTED = {("GC000091", "지역=울산"), ("GC000093", "지역=경북"), ("GC000100", "지역=전북")}
|
||||
|
||||
|
||||
def rows_of(files: dict) -> list[tuple[str, dict]]:
|
||||
|
||||
@@ -30,9 +30,20 @@ _HOUR_VARS = [
|
||||
"단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모",
|
||||
"단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자",
|
||||
"단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모",
|
||||
"단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자",
|
||||
"단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모",
|
||||
"단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "연결": {"갈래": "식"}},
|
||||
] # fmt: skip
|
||||
_HOUR_LINE = {"기호": HOUR, "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
_HOUR_LINE = {
|
||||
"기호": HOUR,
|
||||
"식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표",
|
||||
}
|
||||
MAX_ROWS = 30
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import math
|
||||
INPUTS = ("내용시간", "연간표준가동시간", "상각비율", "정비비율", "연간관리비율")
|
||||
COEFS = ("상각비계수", "정비비계수", "관리비계수", "손료계수")
|
||||
_E = 1e-6 # 끊는 경계 부동소수 흔들림 막음
|
||||
HOUR = 1 / 8 * 16 / 12 * 25 / 20 # 조종원 하루 → 시간(CC000336 시간환산 · 상여 · 휴지 · 840-5)
|
||||
|
||||
|
||||
def _num(v) -> float:
|
||||
@@ -56,9 +57,7 @@ def coef_of(values: dict) -> dict | None:
|
||||
return coef(*(values[k] for k in INPUTS))
|
||||
|
||||
|
||||
def preview(
|
||||
values: dict, fuel: float, wage: float, power: float = 0.0, hour: float = 1 / 8
|
||||
) -> dict:
|
||||
def preview(values: dict, fuel: float, wage: float, power: float = 0.0, hour: float = HOUR) -> dict:
|
||||
"""값 묶음 + 유가(원/ℓ) · 노임(원/일) · 전력(원/kWh) · 시간환산 → 시간당 줄 · 비목 · 계."""
|
||||
loss = math.floor(_num(values.get("가격")) * 1000 * _num(values.get("손료계수")) / 1e7 + _E)
|
||||
oil = _tenth(_num(values.get("주연료")) * _num(fuel))
|
||||
|
||||
@@ -12,7 +12,7 @@ from contextvars import ContextVar
|
||||
from decimal import Decimal
|
||||
|
||||
import master_material as mm
|
||||
from master_unit import UNITS, convert, kind_of, same_kind # noqa: F401 — 단위 한 벌(712-2)
|
||||
from master_unit import UNITS, convert, factor, kind_of, same_kind # noqa: F401 — 단위 한 벌(712-2)
|
||||
|
||||
REGIONS = (
|
||||
"전국평균 전국 서울 경기 강원 충북 충남 전북 전남 경북 경남 부산 대구 인천 대전 울산 세종 제주 광주"
|
||||
@@ -163,7 +163,9 @@ def _priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
price = convert(price, have, unit) if have and unit else price
|
||||
else:
|
||||
price, source = mm.element(master, ref, env, unit)
|
||||
warn = None
|
||||
warn = _direct(master, ref, unit)
|
||||
if warn and warn.startswith(SWAP): # 840-3 자재품목 바로 이음 = 줄 단위로 환산
|
||||
price = convert(price, master.get(ref).get("단위"), unit)
|
||||
except mm.mf.FormulaError as e:
|
||||
return None, None, str(e), None
|
||||
why = None if price is not None else f"「{master.label(ref)}」 값 없음 — 관리자가 채울 값"
|
||||
@@ -171,6 +173,18 @@ def _priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
|
||||
|
||||
HAND = "손 단가"
|
||||
SWAP = "단위 환산"
|
||||
|
||||
|
||||
def _direct(master, ref, unit) -> str | None:
|
||||
"""자재품목 바로 이음 줄(불러오기 · 840-3) 단위 글 — 같음 · 1 : 1 = None · 같은 성격 = 「단위 환산 톤 → kg」 ·
|
||||
성격 다름 = 검색어 길과 같은 경고(값 그대로)."""
|
||||
if not unit or not isinstance(ref, str) or ref[:2] != mm.MARKET:
|
||||
return None
|
||||
have = master.get(ref).get("단위")
|
||||
if not have or factor(have, unit) == 1:
|
||||
return None
|
||||
return f"{SWAP} {have} → {unit}" if same_kind(have, unit) else f"줄 {unit} · 자재 {have}"
|
||||
|
||||
|
||||
def bundled(item: dict, base):
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
},
|
||||
{ "이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:{연료}:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ:{기계}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1" },
|
||||
@@ -45,7 +49,7 @@
|
||||
"수량": "EQ:{기계}.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [{ "이름": "잡재료비", "식": "재료비 * EQ:{기계}.잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4 주②" }, { "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" }],
|
||||
@@ -303,7 +307,11 @@
|
||||
{ "이름": "손료", "식": "버림(EQ:{기계}.가격 * 1000 * ((EQ:{기계}.상각비계수 + EQ:{기계}.정비비계수) + EQ:{기계}.관리비계수) / 10000000, 0)", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4" },
|
||||
{ "이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:{연료}:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ:{기계}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1" },
|
||||
@@ -315,7 +323,7 @@
|
||||
"수량": "EQ:{기계}.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [{ "이름": "잡재료비", "식": "재료비 * EQ:{기계}.잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4 주②" }, { "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" }],
|
||||
@@ -496,7 +504,11 @@
|
||||
},
|
||||
{ "이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:{연료}:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ:{기계}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1" },
|
||||
@@ -508,7 +520,7 @@
|
||||
"수량": "EQ:{기계}.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [{ "이름": "잡재료비", "식": "재료비 * EQ:{기계}.잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4 주②" }, { "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" }],
|
||||
@@ -557,7 +569,11 @@
|
||||
{ "이름": "조합잡재료율", "식": "찾기(CC000074, 구분='불도저 및 굴착기 잡재료 비율').적용", "출처": "건설품셈 공통 8-4-8 주⑤" },
|
||||
{ "이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:{연료}:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ:{기계}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1" },
|
||||
@@ -569,7 +585,7 @@
|
||||
"수량": "EQ:{기계}.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [
|
||||
@@ -628,7 +644,11 @@
|
||||
{ "이름": "조합잡재료율", "식": "찾기(CC000074, 구분='불도저 및 굴착기 잡재료 비율').적용", "출처": "건설품셈 공통 8-4-8 주⑤" },
|
||||
{ "이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:{연료}:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ:{기계}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1" },
|
||||
@@ -640,7 +660,7 @@
|
||||
"수량": "EQ:{기계}.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [
|
||||
@@ -1318,7 +1338,11 @@
|
||||
"중간": [
|
||||
{ "이름": "손료", "식": "버림(EQ:{기계}.가격 * 1000 * ((EQ:{기계}.상각비계수 + EQ:{기계}.정비비계수) + EQ:{기계}.관리비계수) / 10000000, 0)", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4" },
|
||||
{ "이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:{연료}:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ:{기계}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1" },
|
||||
@@ -1330,7 +1354,7 @@
|
||||
"수량": "EQ:{기계}.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [{ "이름": "잡재료비", "식": "재료비 * EQ:{기계}.잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4 주②" }, { "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" }],
|
||||
@@ -1389,7 +1413,11 @@
|
||||
"중간": [
|
||||
{ "이름": "손료", "식": "버림(EQ:{기계}.가격 * 1000 * ((EQ:{기계}.상각비계수 + EQ:{기계}.정비비계수) + EQ:{기계}.관리비계수) / 10000000, 0)", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{
|
||||
@@ -1400,7 +1428,7 @@
|
||||
"수량": "EQ:{기계}.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [{ "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" }],
|
||||
@@ -3516,7 +3544,11 @@
|
||||
"중간": [
|
||||
{ "이름": "손료", "식": "버림(EQ:{기계}.가격 * 1000 * EQ:{기계}.손료계수 / 10000000, 0)", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{
|
||||
@@ -3671,7 +3703,11 @@
|
||||
{ "이름": "손료", "식": "버림(EQ:{기계}.가격 * 1000 * EQ:{기계}.손료계수 / 10000000, 0)", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4" },
|
||||
{ "이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
|
||||
{ "이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:{연료}:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ:{기계}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-8" },
|
||||
|
||||
@@ -534,7 +534,11 @@
|
||||
{ "이름": "손료", "식": "버림(EQ000655.가격 * 1000 * (EQ000069.상각비계수 + EQ000069.정비비계수 + EQ000069.관리비계수) / 10000000, 0)", "출처": "산림품셈 10-9 · 건설품셈 공통 8-1-5 12" },
|
||||
{ "이름": "비율", "식": "찾기(QF000330, 구분='2.5톤 덤프 주 연료 대비').비율", "출처": "산림품셈 10-9" },
|
||||
{ "이름": "직종", "식": "EQ000069.조종원직종" },
|
||||
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
|
||||
{
|
||||
"이름": "시간환산",
|
||||
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모 * 찾기(CC000336, 구분='상여계수').분자 / 찾기(CC000336, 구분='상여계수').분모 * 찾기(CC000336, 구분='휴지계수').분자 / 찾기(CC000336, 구분='휴지계수').분모",
|
||||
"출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)"
|
||||
}
|
||||
],
|
||||
"호표": [
|
||||
{ "종류": "재료", "요소": "MO:경유:{지역}", "이름": "주연료", "단위": "ℓ", "수량": "EQ000069.주연료 * 비율 / 100", "비목": "재료비", "출처": "산림품셈 10-9" },
|
||||
@@ -546,7 +550,7 @@
|
||||
"수량": "EQ000069.조종원 * 시간환산",
|
||||
"비목": "노무비",
|
||||
"출처": "건설품셈 공통 8-1-3 6",
|
||||
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2) — 질의회신 건협기술-2230 의 상여 16/12 · 휴지 25/20 뺌(내역 15 · 품셈 본문대로)"
|
||||
"비고": "하루 노임 × 1/8 × 16/12 × 25/20(1일 8시간 · 상여 300 % + 퇴직 100 % · 월 25 일 중 20 일 가동 — 질의회신 건협기술-2230 · 실무 · 경쟁사와 같음)"
|
||||
}
|
||||
],
|
||||
"덧줄": [
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4 · 8-1-7 1", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "r_가산^표", "기호": "r", "아래": "가산", "위": "표", "이름": "기계손료의 보정.가산비율", "단위": "%", "연결": {"갈래": "표", "표": "CC000034", "조건": {"기종": "'굴착기(무한궤도) 및 로더(무한궤도)'", "작업": "S_보정작업"}, "칸": "가산비율"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계상각비", "기호": "k", "아래": "기계상각비", "위": "", "이름": "EQ:{기계}.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "상각비계수"}},
|
||||
@@ -28,6 +28,10 @@
|
||||
{"열쇠": "k_기계관리비", "기호": "k", "아래": "기계관리비", "위": "", "이름": "EQ:{기계}.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_기계주연료", "기호": "P", "아래": "기계주연료", "위": "", "이름": "EQ:{기계}.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료"}},
|
||||
{"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원"}},
|
||||
@@ -37,7 +41,7 @@
|
||||
"식": [
|
||||
{"기호": "r_가산", "식": "만약(S_보정작업 == '해당 없음', 0, r_가산^표)"},
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × ((k_기계상각비 + k_기계정비비) × (1 + r_가산 ÷ 100) + k_기계관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_기계주연료", "단가": "P_자동차용경유", "출처": "건설품셈 공통 8-4-1"},
|
||||
@@ -67,13 +71,17 @@
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계상각비", "기호": "k", "아래": "기계상각비", "위": "", "이름": "EQ:{기계}.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "상각비계수"}},
|
||||
{"열쇠": "k_기계정비비", "기호": "k", "아래": "기계정비비", "위": "", "이름": "EQ:{기계}.정비비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "정비비계수"}},
|
||||
{"열쇠": "k_기계관리비", "기호": "k", "아래": "기계관리비", "위": "", "이름": "EQ:{기계}.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_기계주연료", "기호": "P", "아래": "기계주연료", "위": "", "이름": "EQ:{기계}.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료"}},
|
||||
{"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원"}},
|
||||
@@ -82,7 +90,7 @@
|
||||
],
|
||||
"식": [
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × (k_기계상각비 + k_기계정비비 + k_기계관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_기계주연료", "단가": "P_휘발유", "출처": "건설품셈 공통 8-4-1"},
|
||||
@@ -115,7 +123,7 @@
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4 · 8-1-7 1", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "r_가산^표", "기호": "r", "아래": "가산", "위": "표", "이름": "기계손료의 보정.가산비율", "단위": "%", "연결": {"갈래": "표", "표": "CC000034", "조건": {"기종": "S_보정기종", "작업": "S_보정작업"}, "칸": "가산비율"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계상각비", "기호": "k", "아래": "기계상각비", "위": "", "이름": "EQ:{기계}.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "상각비계수"}},
|
||||
@@ -123,6 +131,10 @@
|
||||
{"열쇠": "k_기계관리비", "기호": "k", "아래": "기계관리비", "위": "", "이름": "EQ:{기계}.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_기계주연료", "기호": "P", "아래": "기계주연료", "위": "", "이름": "EQ:{기계}.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료"}},
|
||||
{"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원"}},
|
||||
@@ -133,7 +145,7 @@
|
||||
{"기호": "S_보정기종", "식": "만약(S_기계 == '0101-0007' 또는 S_기계 == '0101-0010' 또는 S_기계 == '0101-0012' 또는 S_기계 == '0102-0015' 또는 S_기계 == '0102-0028' 또는 S_기계 == '0102-0033' 또는 S_기계 == '0121-0004' 또는 S_기계 == '0121-0013', '불도저(19톤이상 제외)', 만약(S_기계 == '0602-0025' 또는 S_기계 == '0602-0045' 또는 S_기계 == '0602-0060' 또는 S_기계 == '0602-0080' 또는 S_기계 == '0602-0105' 또는 S_기계 == '0602-0150', '덤프트럭', 만약(S_기계 == '0301-0057' 또는 S_기계 == '0301-0076' 또는 S_기계 == '0301-0095' 또는 S_기계 == '0301-0115' 또는 S_기계 == '0301-0134' 또는 S_기계 == '0301-0153' 또는 S_기계 == '0301-0172' 또는 S_기계 == '0301-0287', '굴착기(무한궤도) 및 로더(무한궤도)', '없음')))"},
|
||||
{"기호": "r_가산", "식": "만약(S_보정작업 == '해당 없음' 또는 S_보정기종 == '없음', 0, r_가산^표)"},
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × ((k_기계상각비 + k_기계정비비) × (1 + r_가산 ÷ 100) + k_기계관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_기계주연료", "단가": "P_휘발유", "출처": "건설품셈 공통 8-4-1"},
|
||||
@@ -168,7 +180,7 @@
|
||||
{"열쇠": "조합잡재료율", "기호": "r", "아래": "조합잡재료", "위": "", "이름": "조합잡재료율", "단위": "", "출처": "건설품셈 공통 8-4-8 주⑤", "연결": {"갈래": "표", "표": "CC000074", "조건": {"구분": "'불도저 및 굴착기 잡재료 비율'"}, "칸": "적용"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "r_가산^표", "기호": "r", "아래": "가산", "위": "표", "이름": "기계손료의 보정.가산비율", "단위": "%", "연결": {"갈래": "표", "표": "CC000034", "조건": {"기종": "'굴착기(무한궤도) 및 로더(무한궤도)'", "작업": "S_보정작업"}, "칸": "가산비율"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계상각비", "기호": "k", "아래": "기계상각비", "위": "", "이름": "EQ:{기계}.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "상각비계수"}},
|
||||
@@ -180,6 +192,10 @@
|
||||
{"열쇠": "k_집게관리비", "기호": "k", "아래": "집게관리비", "위": "", "이름": "EQ:{집게}.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_집게}", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_기계주연료", "기호": "P", "아래": "기계주연료", "위": "", "이름": "EQ:{기계}.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료"}},
|
||||
{"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원"}},
|
||||
@@ -189,7 +205,7 @@
|
||||
{"기호": "r_가산", "식": "만약(S_보정작업 == '해당 없음', 0, r_가산^표)"},
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × ((k_기계상각비 + k_기계정비비) × (1 + r_가산 ÷ 100) + k_기계관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "C_집게", "식": "버림(P_집게 × 1000 × (k_집게상각비 + k_집게정비비 + k_집게관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_기계주연료", "단가": "P_자동차용경유", "출처": "건설품셈 공통 8-4-1"},
|
||||
@@ -225,7 +241,7 @@
|
||||
{"열쇠": "조합잡재료율", "기호": "r", "아래": "조합잡재료", "위": "", "이름": "조합잡재료율", "단위": "", "출처": "건설품셈 공통 8-4-8 주⑤", "연결": {"갈래": "표", "표": "CC000074", "조건": {"구분": "'불도저 및 굴착기 잡재료 비율'"}, "칸": "적용"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "r_가산^표", "기호": "r", "아래": "가산", "위": "표", "이름": "기계손료의 보정.가산비율", "단위": "%", "연결": {"갈래": "표", "표": "CC000034", "조건": {"기종": "'굴착기(무한궤도) 및 로더(무한궤도)'", "작업": "S_보정작업"}, "칸": "가산비율"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계상각비", "기호": "k", "아래": "기계상각비", "위": "", "이름": "EQ:{기계}.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "상각비계수"}},
|
||||
@@ -237,6 +253,10 @@
|
||||
{"열쇠": "k_브레이커관리", "기호": "k", "아래": "브레이커관리", "위": "", "이름": "EQ:{브레이커}.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_브레이커}", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_기계주연료", "기호": "P", "아래": "기계주연료", "위": "", "이름": "EQ:{기계}.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료"}},
|
||||
{"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원"}},
|
||||
@@ -246,7 +266,7 @@
|
||||
{"기호": "r_가산", "식": "만약(S_보정작업 == '해당 없음', 0, r_가산^표)"},
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × ((k_기계상각비 + k_기계정비비) × (1 + r_가산 ÷ 100) + k_기계관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "C_브레이커", "식": "버림(P_브레이커 × 1000 × (k_브레이커상각 + k_브레이커정비 + k_브레이커관리) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_기계주연료", "단가": "P_자동차용경유", "출처": "건설품셈 공통 8-4-1"},
|
||||
@@ -372,13 +392,17 @@
|
||||
{"열쇠": "조종원직종", "기호": "S", "아래": "조종원직종", "위": "", "이름": "조종원직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "선택", "목록": ["1048", "1049", "1050"], "값": "1048"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계상각비", "기호": "k", "아래": "기계상각비", "위": "", "이름": "EQ:{기계}.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "상각비계수"}},
|
||||
{"열쇠": "k_기계정비비", "기호": "k", "아래": "기계정비비", "위": "", "이름": "EQ:{기계}.정비비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "정비비계수"}},
|
||||
{"열쇠": "k_기계관리비", "기호": "k", "아래": "기계관리비", "위": "", "이름": "EQ:{기계}.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_기계주연료", "기호": "P", "아래": "기계주연료", "위": "", "이름": "EQ:{기계}.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료"}},
|
||||
{"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원"}},
|
||||
@@ -387,7 +411,7 @@
|
||||
],
|
||||
"식": [
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × (k_기계상각비 + k_기계정비비 + k_기계관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_기계주연료", "단가": "P_자동차용경유", "출처": "건설품셈 공통 8-4-1"},
|
||||
@@ -416,19 +440,23 @@
|
||||
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계상각비", "기호": "k", "아래": "기계상각비", "위": "", "이름": "EQ:{기계}.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "상각비계수"}},
|
||||
{"열쇠": "k_기계정비비", "기호": "k", "아래": "기계정비비", "위": "", "이름": "EQ:{기계}.정비비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "정비비계수"}},
|
||||
{"열쇠": "k_기계관리비", "기호": "k", "아래": "기계관리비", "위": "", "이름": "EQ:{기계}.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원"}},
|
||||
{"열쇠": "w_기계운전사", "기호": "w", "아래": "기계운전사", "위": "", "이름": "건설기계운전사노임", "단위": "", "연결": {"갈래": "요소", "키": "LB:{S_직종}"}}
|
||||
],
|
||||
"식": [
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × (k_기계상각비 + k_기계정비비 + k_기계관리비) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "조종원", "단위": "인", "비목": "노무비", "수량": "Q_기계조종원 × Q_시간환산", "단가": "w_기계운전사", "출처": "건설품셈 공통 8-1-3 6"},
|
||||
@@ -2263,11 +2291,15 @@
|
||||
{"열쇠": "전력 계약종별", "기호": "S", "아래": "전력계약종별", "위": "", "이름": "전력 계약종별", "단위": "", "출처": "유가전력 한전 전기요금표 2025-01-01", "연결": {"갈래": "선택", "목록": ["일반용(갑)Ⅰ 저압", "주택용 저압"], "값": "일반용(갑)Ⅰ 저압"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "k_기계손료", "기호": "k", "아래": "기계손료", "위": "", "이름": "EQ:{기계}.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "손료계수", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_기계전력", "기호": "P", "아래": "기계전력", "위": "", "이름": "EQ:{기계}.전력", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "전력", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "P_전력", "기호": "P", "아래": "전력", "위": "", "이름": "전력단가", "단위": "", "연결": {"갈래": "요소", "재료": {"이름": "전력량요금", "계약종별": "입력"}, "단위": "㎾h", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "Q_기계조종원", "기호": "Q", "아래": "기계조종원", "위": "", "이름": "EQ:{기계}.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원", "계약종별": "S_전력계약종별"}},
|
||||
@@ -2276,7 +2308,7 @@
|
||||
],
|
||||
"식": [
|
||||
{"기호": "C_손료", "식": "버림(P_기계 × 1000 × k_기계손료 ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "전력", "단위": "㎾h", "비목": "재료비", "수량": "P_기계전력", "단가": "P_전력", "출처": "건설품셈 공통 8-1-6 3 가"},
|
||||
@@ -2409,11 +2441,15 @@
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "P_EQ기계", "기호": "P", "아래": "EQ기계", "위": "", "이름": "EQ:{기계}", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "P_EQ기계2", "기호": "P", "아래": "EQ기계2", "위": "", "이름": "EQ:{기계}", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "손료계수", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_EQ기계3", "기호": "P", "아래": "EQ기계3", "위": "", "이름": "EQ:{기계}", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "P_MO연료지역", "기호": "P", "아래": "MO연료지역", "위": "", "이름": "MO:{연료}:{지역}", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}", "계약종별": "S_전력계약종별"}},
|
||||
{"열쇠": "P_EQ기계4", "기호": "P", "아래": "EQ기계4", "위": "", "이름": "EQ:{기계}", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "전력", "계약종별": "S_전력계약종별"}},
|
||||
@@ -2424,7 +2460,7 @@
|
||||
],
|
||||
"식": [
|
||||
{"기호": "C_손료", "식": "버림(P_EQ기계 × 1000 × P_EQ기계2 ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_EQ기계3", "단가": "P_MO연료지역", "출처": "건설품셈 공통 8-4-8"},
|
||||
|
||||
@@ -481,13 +481,17 @@
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "산림품셈 10-9 · 건설품셈 공통 8-1-5 12", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "비율", "기호": "r", "아래": "비율", "위": "", "이름": "비율", "단위": "", "출처": "산림품셈 10-9", "연결": {"갈래": "표", "표": "QF000330", "조건": {"구분": "'2.5톤 덤프 주 연료 대비'"}, "칸": "비율"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "조종원직종"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "질의회신 건협기술-2230(1일 8시간 · 상여 · 휴지)", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "P_덤프트럭", "기호": "P", "아래": "덤프트럭", "위": "", "이름": "EQ000655.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000655", "칸": "가격"}},
|
||||
{"열쇠": "k_덤프트럭", "기호": "k", "아래": "덤프트럭", "위": "", "이름": "EQ000069.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "상각비계수"}},
|
||||
{"열쇠": "k_덤프트럭2", "기호": "k", "아래": "덤프트럭2", "위": "", "이름": "EQ000069.정비비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "정비비계수"}},
|
||||
{"열쇠": "k_덤프트럭3", "기호": "k", "아래": "덤프트럭3", "위": "", "이름": "EQ000069.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "관리비계수"}},
|
||||
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자2^표", "기호": "Q", "아래": "분자2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모2^표", "기호": "Q", "아래": "분모2", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'상여계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "Q_분자3^표", "기호": "Q", "아래": "분자3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분자"}},
|
||||
{"열쇠": "Q_분모3^표", "기호": "Q", "아래": "분모3", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'휴지계수'"}, "칸": "분모"}},
|
||||
{"열쇠": "P_덤프트럭2", "기호": "P", "아래": "덤프트럭2", "위": "", "이름": "EQ000069.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "주연료"}},
|
||||
{"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}},
|
||||
{"열쇠": "Q_덤프트럭", "기호": "Q", "아래": "덤프트럭", "위": "", "이름": "EQ000069.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "조종원"}},
|
||||
@@ -496,7 +500,7 @@
|
||||
],
|
||||
"식": [
|
||||
{"기호": "C_손료", "식": "버림(P_덤프트럭 × 1000 × (k_덤프트럭 + k_덤프트럭2 + k_덤프트럭3) ÷ 10000000, 0)"},
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
|
||||
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표"}
|
||||
],
|
||||
"금액": [
|
||||
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_덤프트럭2 × r_비율 ÷ 100", "단가": "P_자동차용경유", "출처": "산림품셈 10-9"},
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/* 840-2 기계 보기 셈 꼴 헬퍼 — common_util_machine_sheet.ts · common_util_formula_doc_cover.ts 를 그 자리에서 트랜스파일해 Node 로 돌림.
|
||||
* stdin = {문서들: {식 키: 식 문서}, 대상: [{식, 처음, 지금?, 덮음?, 개인, 직종?, 박음?: 값}]}
|
||||
* stdout = [{덮음: coverFrom(처음, 지금 | 처음), 줄: applyCover(처음, 지금 | 처음, 덮음 | coverFrom), 박음?: bindDoc(식, 박음)}] */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const cache = new Map();
|
||||
function load(at) {
|
||||
if (cache.has(at)) return cache.get(at);
|
||||
const js = ts.transpileModule(fs.readFileSync(at, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(at, box.exports);
|
||||
const need = (p) =>
|
||||
p.startsWith(".") ? load(path.resolve(path.dirname(at), `${p}.ts`)) : require(p);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
}
|
||||
const root = path.join(__dirname, "..", "..", "common_util");
|
||||
const cover = load(path.join(root, "common_util_formula_doc_cover.ts"));
|
||||
const sheet = load(path.join(root, "common_util_machine_sheet.ts"));
|
||||
|
||||
const ask = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const out = ask.대상.map((t) => {
|
||||
const doc = ask.문서들[t.식];
|
||||
const parts = cover.partsOf(doc);
|
||||
const cur = t.지금 ?? t.처음;
|
||||
const made = sheet.coverFrom(parts, doc.금액 ?? [], t.처음, cur, t.개인);
|
||||
const use = t.덮음 ?? made;
|
||||
const row = sheet.applyCover(parts, t.처음, cur, use, t.개인, t.직종 ?? {});
|
||||
return { 덮음: made, 줄: row, ...(t.박음 ? { 박음: sheet.bindDoc(doc, t.박음) } : {}) };
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -1,6 +1,6 @@
|
||||
"""713-4 지킴 — 정본 로직 원문 대조 정리(내역 15) 뒤 꼴.
|
||||
|
||||
조종원 = 하루 노임 × 1/8(질의회신 상여 · 휴지계수 없음) ·
|
||||
조종원 = 하루 노임 × 1/8 × 16/12 × 25/20(840-5 · 요구 864 경쟁사와 같게 — 713-4 의 1/8 만 꼴을 되돌림) ·
|
||||
지운 키(쪼갠 로직 · 쪽 표 · 터파기 작업량)를
|
||||
쓰는 곳 0 · 방제 8-3 열 여덟 모두 로직 · 플랜트 13-1-1 표 하나.
|
||||
"""
|
||||
@@ -57,11 +57,11 @@ def test_지운_키는_어디에도_없음() -> None:
|
||||
assert not pattern.search(path.read_text(encoding="utf-8", errors="ignore")), path.name
|
||||
|
||||
|
||||
def test_조종원_시간환산_본문대로() -> None:
|
||||
def test_조종원_시간환산_세_계수() -> None:
|
||||
rows = _rows(cm.load())
|
||||
for key in MACHINE:
|
||||
text = str(rows[key]["중간"])
|
||||
assert "상여계수" not in text and "휴지계수" not in text, key
|
||||
assert "'시간환산'" in text and "상여계수" in text and "휴지계수" in text, key
|
||||
|
||||
|
||||
def test_방제_열_여덟_플랜트_한_표() -> None:
|
||||
|
||||
@@ -165,9 +165,11 @@ def test_상세_부품_길_부품상자():
|
||||
cover = _text(M02 / "M02_MasterTemplete_Sangeun_Cover.ts")
|
||||
assert "mountSangeunDetail(host, doc," in view and "createEmpty" not in view
|
||||
assert 'read: "/m02/logic-lib"' in detail and "setLogicApiPaths(LIB)" in detail
|
||||
assert "buildLabDetail(editor" in detail and "cover: { ho, open: lineOpener(hoCtx) }" in detail
|
||||
assert "buildLabDetail(editor" in detail and "cover: { ho }," in detail
|
||||
assert "buildCalc" not in detail # 799-2 시험 계산 보기 걷음
|
||||
assert "openSangeunLinePicker(scope" in detail
|
||||
assert (
|
||||
"openSangeunLinePicker" not in detail and "openRateLine" not in detail
|
||||
) # 840-1 [항목 추가] · [율 줄 추가] 걷음
|
||||
assert "if (mine !== seq || !alive) return" in detail # 최신 응답만
|
||||
# 724-1 — 단가산출 상세 = 공용 상세(Ho) · 고침 고리 = SK 덮개 · 옛 고침 표 걷음
|
||||
assert "...buildHo(hoCtx)" in detail and 'scope: "m02",' in detail
|
||||
|
||||
@@ -38,10 +38,9 @@ def test_sk_list_layer_order_and_ref_shape():
|
||||
) # 일위대가 [로직 추가] → 찾아 넣기 창(785-7 `withSangeun`)
|
||||
store_ts = _text(ROOT / "B09_Estimation" / "B09_Estimation_UI_Store.ts")
|
||||
assert '["sangeun", "단가산출"],' in store_ts and "sangeun: sangeunItems," in store_ts
|
||||
# 더한줄 고르기 — 715 이음 꼴 셋(인력 · 재료 = 요소 키)
|
||||
assert 'logic: (x) => done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 })' in pick
|
||||
assert "ref: (ref) => done({ 참조: ref, 수량: 1 })" in pick
|
||||
assert "done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 })" in pick
|
||||
assert (
|
||||
"openSangeunLinePicker" not in pick
|
||||
) # 840-1 단가산출 [항목 추가] 걷음 — 줄 더하기 = 수식 [+]
|
||||
|
||||
|
||||
def _sk(folder, name):
|
||||
@@ -72,14 +71,14 @@ def test_list_amount_and_labor_line_holds_upper_sk(world): # noqa: F811
|
||||
},
|
||||
)["이름"]
|
||||
line = store.read("precombo", sc)["문서"]["담은로직"][0]
|
||||
saved, inner = line["본뜬"], [line["수량"]]
|
||||
saved, inner = line["참조"], [line["수량"]]
|
||||
outer = {"담은로직": [{"참조": {**MINE, "키": sc}, "수량": {"손": 1}}]}
|
||||
got = labor.calc(outer, {})
|
||||
fed = labor.calc(outer, {f"{refs.ref_id({**MINE, '키': sc})}#0": 3})
|
||||
direct = labor.calc({"담은로직": [{"참조": {**COMPANY, "키": sk}, "수량": {"손": 3}}]}, {})
|
||||
# 740-1 — 회사 SK = 로직 줄 + 패치로 펼쳐 담음 · 출처 = 본뜬
|
||||
assert {k: saved[k] for k in ("층", "주인", "키")} == {**COMPANY, "키": sk}
|
||||
assert line["로직"] and "참조" not in line
|
||||
# 811-1 — 일위대가 안 SK 줄 = 참조 한 행 그대로(펼치지 않음 · 본뜬 없음)
|
||||
assert saved == {**COMPANY, "키": sk}
|
||||
assert "로직" not in line and "본뜬" not in line
|
||||
# 742-1 — 일위대가 안 줄 수량 = 저장(사용자 입력) · 쓰는 자리 알약이 이어지면 그 값
|
||||
assert inner == [{"손": 3}]
|
||||
assert got["반영합"]["계"] == pytest.approx(one * 3, abs=1) # 저장 수량 3
|
||||
|
||||
@@ -27,12 +27,12 @@ def _text(path: Path) -> str:
|
||||
def test_721_1_단추_이름과_자리():
|
||||
detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert (
|
||||
"children: [tabs.picker, tabs.tools, addButton, rateButton]" in detail
|
||||
) # 750-2 안내 걷음 · 778-4 율 줄 추가 · 799-2 보기 · 808-1 [단가 갱신]
|
||||
"addButton" not in detail and "rateButton" not in detail
|
||||
) # 840-1 [항목 추가] · [율 줄 추가] 걷음
|
||||
assert "M02_SangeunPickBase" not in detail # 727-2 단추 걷음
|
||||
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
|
||||
assert "M02_SangeunPickBase" not in locale
|
||||
assert 'M02_SangeunAddLine: ["항목 추가"' in locale
|
||||
assert "M02_SangeunAddLine" not in locale and "M02_SkRateAdd" not in locale
|
||||
|
||||
|
||||
def test_721_2_부르는_곳_셋만_새_창():
|
||||
@@ -43,7 +43,7 @@ def test_721_2_부르는_곳_셋만_새_창():
|
||||
assert "findSangeun" not in labor_pick # 옛 창의 단가산출 탭 걷음
|
||||
assert "logicOnly" not in _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts") # 727-2
|
||||
assert "logicOnly" not in find
|
||||
assert "openFindPicker({" in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts")
|
||||
assert "openFindPicker" not in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts") # 840-1
|
||||
assert "sangeun: true }" in labor_pick # 일위대가(785-7 `withSangeun`)
|
||||
# 옛 창 그대로 — B09 단가 연결은 sangeun 없이 부름(736-1 불러오기 · 별칭 장부는 찾아 넣기 창으로)
|
||||
for f in (
|
||||
@@ -128,7 +128,8 @@ def test_736_2_빠진_길_둘_신규_자재_기계_탭():
|
||||
"""736-2 — 개인 층 재료 고름 머리 [신규 자재] · 엑셀 연결 = 줄 갈래대로 탭(노임 인력 · 기계 기계 · 자재 재료)."""
|
||||
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
|
||||
assert 'opt.layer === "personal" && !!opt.material && ok("material")' in find
|
||||
assert "openNewMaterial(opt.owner" in find and "actions: [newMat]" in find
|
||||
assert "openNewMaterial(opt.owner" in find and "headBtns = canNew ? [newMat] : []" in find
|
||||
assert "{ actions: headBtns }" in find # 머리 단추 있을 때만 모달 머리에
|
||||
assert "machine?: (e: M01Element) => void;" in find
|
||||
assert 'opt.machine && ok("machine")' in find
|
||||
pick = _text(M02 / "M02_Material_Pick.ts")
|
||||
@@ -171,6 +172,3 @@ def test_736_3_기계_고정_줄_금액():
|
||||
and "이음: { 노임: null, 자재: null, 기계: e.코드 }" in find
|
||||
)
|
||||
assert "machine: fixed &&" in _text(M02 / "M02_MasterTemplete_Labor_Pick.ts")
|
||||
assert "machine: (e) => done({ 고정: fixedOf(e)" in _text(
|
||||
M02 / "M02_MasterTemplete_Sangeun_Pick.ts"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,12 @@ def _node(code: str):
|
||||
|
||||
def test_상세가_공용_부품을_씀():
|
||||
detail = _text(M01 / "M01_MasterData_UI_LogicLab_Detail.ts")
|
||||
assert "buildHo({ ...ctx, edit: trialEdit(ctx) })" in detail
|
||||
assert (
|
||||
"buildHo" not in detail and "trialEdit" not in detail
|
||||
) # 840-1 M01 상세 보기 걷음 — M02 줄 모달만 씀
|
||||
assert "...buildHo(hoCtx)" in _text(
|
||||
ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Sangeun_Detail.ts"
|
||||
)
|
||||
ho = _text(HO)
|
||||
# 부품 상자만 — 표 · 카드 · 계산 줄 · 변수 칸(calc_sheet) · 입력 칩(텍스트 수식)
|
||||
for part in (
|
||||
@@ -60,13 +65,13 @@ def test_상세가_공용_부품을_씀():
|
||||
)
|
||||
|
||||
|
||||
def test_시험_고침은_캐시_밖():
|
||||
def test_시험_고침은_M01_에서_걷음():
|
||||
lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts")
|
||||
# 시험 계산에만 덮은 사본 · 고침이 있으면 줄을 보냄 · drafts(캐시)에는 원본 줄
|
||||
assert "=> trial.apply(row);" in lab
|
||||
assert "dirty: () => current.id in drafts || trial.has(current.row)," in lab
|
||||
assert "onTrial: drawCalc," in lab and "rerun: () => void drive.run()," in lab # 799-2 셈 구동
|
||||
assert (
|
||||
"trial" not in lab and "drive" not in lab
|
||||
) # 840-1 — 시험 값 · 구동 걷음(수식에서 값을 바꾸고 [되돌림])
|
||||
cover = _text(COVER)
|
||||
assert "export const trial" not in cover and "applyCovers" not in cover
|
||||
assert "drafts" not in cover and "touch" not in cover and "/save" not in cover
|
||||
modal = _text(M01 / "M01_MasterData_UI_LogicLab_Modal.ts")
|
||||
assert "...(target.edit ? [target.edit(close, head)] : [])" in modal
|
||||
@@ -91,23 +96,3 @@ console.log(JSON.stringify(got));
|
||||
assert got[3]["줄"] == [[2, 5, 2]]
|
||||
empty = _node(_pure(HO) + '\nconsole.log(JSON.stringify(boxesOf([], ["인력"])))')
|
||||
assert empty == [{"묶음": "인력", "줄": [], "소계": 0}]
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_고침_덮기():
|
||||
body = """
|
||||
const row = { 키: "GF1", 호표: [
|
||||
{ 종류: "인력", 요소: "LB1", 수량: "찾기(Q).수량" },
|
||||
{ 종류: "로직", 요소: "로직(GC1)", 수량: "1 / 작업량", 값묶음: { 경비: 3 } },
|
||||
{ 종류: "재료", 요소: "MT1", 수량: "2" } ], 덧줄: [{ 이름: "공구", 식: "노무비 * 3 / 100" }] };
|
||||
const out = applyCovers(row, { 0: { qty: 0.0000001, labor: 100000 }, 1: { labor: 50000 }, 9: { qty: 1 } });
|
||||
console.log(JSON.stringify({ out, same: row.호표[0].수량, keep: out.호표[2] === row.호표[2] }));
|
||||
"""
|
||||
got = _node(_pure(COVER) + "\n" + body)
|
||||
out = got["out"]
|
||||
assert (
|
||||
out["호표"][0]["수량"] == "0.0000001" and out["호표"][0]["단가"] == 100000
|
||||
) # 지수 꼴 없음 · 손 단가
|
||||
assert out["호표"][1]["값묶음"] == {"경비": 3, "노무비": 50000} and "단가" not in out["호표"][1]
|
||||
assert got["keep"] and got["same"] == "찾기(Q).수량" # 원본 안 바뀜 · 안 고친 줄 그대로
|
||||
assert out["덧줄"] == [{"이름": "공구", "식": "노무비 * 3 / 100"}]
|
||||
|
||||
@@ -90,7 +90,7 @@ def test_둘째_기초_수량_처음값_배(whole):
|
||||
src = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "swapUnit(" in src and "addBase(" in src and "baseUnitOf(" in src
|
||||
assert "addBase(" not in src and "baseUnitOf(" in src # 840-1 [항목 추가] 걷음
|
||||
|
||||
|
||||
def test_돈_아닌_기초_고르기_창에서_뺌(whole):
|
||||
|
||||
@@ -93,7 +93,7 @@ def test_굴착기_블록_Q_노재경():
|
||||
assert item["이름식"]["우"]["꼴"] == "분수" # 1 / 작업량
|
||||
cut = mcut.engine(whole(), EXCAVATOR, raw(EXCAVATOR))
|
||||
assert eq["합계"] == {k: cut[k] for k in mf.BUNDLE}
|
||||
assert eq["합계"]["계"] == D("2190.0")
|
||||
assert eq["합계"]["계"] == D("2791.0") # 조종원 1/8 × 16/12 × 25/20(840-5 · 옛 2190)
|
||||
|
||||
|
||||
def test_지게_N_M_42917():
|
||||
|
||||
@@ -57,27 +57,23 @@ def test_걷은_파일_없고_부르는_곳_없음():
|
||||
|
||||
|
||||
def test_보기는_수식_하나_고르기_없음():
|
||||
m01 = _text(M01 / "M01_MasterData_UI_LogicLab_View.ts")
|
||||
m02 = _text(M02 / "M02_MasterTemplete_Sangeun_Tabs.ts")
|
||||
for src in (m01, m02):
|
||||
assert not re.findall(r'\{ value: "(\w+)", label: ', src) and "createSegment" not in src
|
||||
assert "createViewPicker" not in _text(M01 / "M01_MasterData_UI_LogicLab.ts") # 836-3
|
||||
assert 'editor.dataset.view = "stack";' in _text(M01 / "M01_MasterData_UI_LogicLab.ts")
|
||||
assert 'export type LabView = "stack" | "ho";' in m01
|
||||
assert not re.findall(r'\{ value: "(\w+)", label: ', m02) and "createSegment" not in m02
|
||||
assert not (M01 / "M01_MasterData_UI_LogicLab_View.ts").exists() # 840-1 보기 이름표 걷음
|
||||
lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts")
|
||||
assert "createViewPicker" not in lab and "dataset.view" not in lab # 836-3 · 840-1
|
||||
css = _text(M01 / "M01_MasterData_UI_LogicLab_Style.css")
|
||||
assert '[data-view="ho"] [data-block]:not([data-block="ho"])' in css
|
||||
assert 'data-view="sheet"' not in css and 'data-view="calc"' not in css
|
||||
assert "data-view" not in css and "data-block" not in css # 840-1 보기 고르기 규칙 걷음
|
||||
|
||||
|
||||
def test_셈_구동은_화면_없이_상세는_구동만():
|
||||
drive = _text(M01 / "M01_MasterData_UI_Logic_Drive.ts")
|
||||
assert "document." not in drive and "sessionStorage." not in drive
|
||||
assert "export async function driveRun(" in drive and "export function createDrive(" in drive
|
||||
assert "export async function driveRun(" in drive and "createDrive" not in drive # 840-1
|
||||
lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts")
|
||||
assert "createDrive()" in lab and "buildCalc" not in lab and "data-calc-run" not in lab
|
||||
assert "rerun: () => void drive.run()," in lab
|
||||
assert "createDrive" not in lab and "buildCalc" not in lab and "data-calc-run" not in lab
|
||||
detail = _text(M01 / "M01_MasterData_UI_LogicLab_Detail.ts")
|
||||
assert "calcHost" not in detail and "data-calc-run" not in detail and "ctx.rerun?.()" in detail
|
||||
assert "calcHost" not in detail and "data-calc-run" not in detail and "rerun" not in detail
|
||||
calc = _text(
|
||||
M01 / "M01_MasterData_UI_Logic_Calc.ts"
|
||||
) # 로직 복사 화면이 쓰는 시험 계산 칸 — 구동은 공용
|
||||
|
||||
@@ -57,8 +57,8 @@ def test_옵션_표본_모든_갈래_한_번_이상(result):
|
||||
info = result["요약"]
|
||||
assert info["옵션 표본"] == want == info["옵션 같음"] + info["옵션 다름"]
|
||||
assert (
|
||||
info["옵션 같음"] >= 18400 and info["옵션 다름"] == info["채택된 차이"] == 1
|
||||
) # 813-2: 지운 6 문서 표본 빠져 18,438
|
||||
info["옵션 같음"] >= 18400 and info["옵션 다름"] == info["채택된 차이"] == 3
|
||||
) # 813-2: 지운 6 문서 표본 빠져 18,438 · 840-5 조종원 환산 뒤 채택 셋
|
||||
|
||||
|
||||
def _whole():
|
||||
@@ -72,7 +72,11 @@ def test_다름_목록_까닭(result):
|
||||
diff = [x for x in result["대조"] if not x["같음"]]
|
||||
assert all(x["까닭"] != "까닭 모름" for x in diff)
|
||||
# 채택된 차이(요구 714-3) — 식 속 단가산출 부름 값 = 끝수 뒤 · 재료비 1 원 · 저장은 함
|
||||
assert [(x["로직"], x["옵션"], x["채택"]) for x in diff] == [("GC000100", "지역=경북", True)]
|
||||
assert [(x["로직"], x["옵션"], x["채택"]) for x in diff] == [
|
||||
("GC000091", "지역=울산", True),
|
||||
("GC000093", "지역=경북", True),
|
||||
("GC000100", "지역=전북", True),
|
||||
] # 840-5 조종원 1/8 × 16/12 × 25/20 뒤 — 같은 까닭 1 원 셋(옛 GC000100 경북 = 같아짐)
|
||||
assert diff[0]["까닭"].startswith(build.RAW_SUB)
|
||||
assert diff[0]["옛"]["재료비"] != diff[0]["새"]["재료비"]
|
||||
assert abs(diff[0]["새"]["재료비"] - diff[0]["옛"]["재료비"]) == 1
|
||||
|
||||
@@ -191,9 +191,9 @@ def test_틀풀기_전수_금액같음_Cm고침(world):
|
||||
"펼침",
|
||||
[{**world["cases"]["GF000099"], "기호": "Q_작업", "고침": [{"기호": "Cm", "글": "20"}]}],
|
||||
)[0][0]
|
||||
assert one["전"]["계"] == 5286 and one["후"]["계"] == 5286
|
||||
assert one["전"]["계"] == 5651 and one["후"]["계"] == 5651 # 840-5 조종원 환산(옛 5286)
|
||||
assert one["옮김"][-1] == "Q_작업" and {"q", "K", "f", "E", "Cm"} <= set(one["옮김"])
|
||||
assert one["고친"]["계"] == 5434 # Cm 18 → 20 = Q 58.212 → 52.391
|
||||
assert one["고친"]["계"] == 5840 # Cm 18 → 20 = Q 58.212 → 52.391(840-5 · 옛 5434)
|
||||
gone = _ts("펼침", [{**world["cases"]["GF000099"], "기호": "Q_작업", "지움": ["Cm"]}])[0][0]
|
||||
assert gone["고친"]["계"] is None and "정의 필요" in gone["표지"] # 지워도 오류 없음
|
||||
|
||||
@@ -217,7 +217,7 @@ def test_저장_쌓기_다시읽어같음(client, tmp_path):
|
||||
laid = got["차림문서"]
|
||||
res = client.post("/api/m01/logic/doc/save", json={"문서": laid})
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["셈"]["계"] == read["셈"]["계"] == 5286
|
||||
assert res.json()["셈"]["계"] == read["셈"]["계"] == 5651 # 840-5(옛 5286)
|
||||
again = client.get("/api/m01/logic/doc", params={"key": "GF000099"}).json()
|
||||
assert (
|
||||
again["문서"]["쌓기"] == laid["쌓기"] and _ts("왕복", [{"문서": again["문서"]}])[0]["같음"]
|
||||
@@ -240,8 +240,8 @@ def test_화면_이음():
|
||||
'base: "/m01/logic/doc",' in lab and 'cache: "m01_doc_drafts",' in lab
|
||||
) # 804-7 page = 페이지 [저장]
|
||||
assert "stack: stack.root" in lab and "=> stack.canLeave();" in lab # 804-6 수식 탭 = 줄 쌓기
|
||||
assert 'editor.dataset.view = "stack";' in lab # 836-3 보기 고르기 없음 · 수식 하나
|
||||
assert '[data-view="stack"]' in _src("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css")
|
||||
assert "dataset.view" not in lab # 836-3 보기 고르기 없음 · 840-1 이름표 · CSS 걷음
|
||||
assert "data-view" not in _src("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css")
|
||||
tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts")
|
||||
assert 'cache: "m02_sk_doc_drafts"' in tabs and tabs.count("skDocPath(opt.scope, k, p)") == 1
|
||||
vite = _src("vite.config.ts")
|
||||
|
||||
@@ -51,10 +51,7 @@ def test_걷은_파일_없고_부르는_곳_없음():
|
||||
|
||||
|
||||
def test_보기_수식은_줄쌓기_첫탭():
|
||||
for rel in (
|
||||
"M01_MasterData/M01_MasterData_UI_LogicLab_View.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts",
|
||||
):
|
||||
for rel in ("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts",):
|
||||
src = _text(rel)
|
||||
assert "createSegment" not in src and '"equation"' not in src, rel # 836-3 고르기 없음
|
||||
lab = _text("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
|
||||
|
||||
@@ -209,7 +209,10 @@ def test_시험_페이지_걷음_화면_차림() -> None:
|
||||
def test_M02_수식탭_M01과_같은_단추_저장_한_번() -> None:
|
||||
"""804-8 — M02 단가산출 수식 탭 = M01 과 같은 공용 화면 · 페이지 [되돌림] · [저장] 한 번 · 머리 줄 한 줄."""
|
||||
screen = _src("A00_Common/line_stack_screen.ts")
|
||||
assert "...(cp && edit() && key && !mark?.자동 ? copyTools(true, (job) => void runJob(job)) : [])," in screen # 836-2 자동 층 = 단추 없음 · 프로젝트 층 = 있음
|
||||
assert (
|
||||
"...(cp && edit() && key && !mark?.자동 ? copyTools(true, (job) => void runJob(job)) : []),"
|
||||
in screen
|
||||
) # 836-2 자동 층 = 단추 없음 · 프로젝트 층 = 있음
|
||||
assert "notesOf" not in screen and "lstk__notes" not in screen # 못 고치는 칸 안내 줄 걷음
|
||||
assert "Notes:" not in _src("A00_Common/line_stack_text.ts")
|
||||
tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts")
|
||||
@@ -217,8 +220,9 @@ def test_M02_수식탭_M01과_같은_단추_저장_한_번() -> None:
|
||||
detail = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "page: opt.source ? () => report() : undefined," in detail
|
||||
assert "JSON.stringify(doc) !== saved || tabs.dirty()" in detail # 고침 알림 = 기본 + 수식
|
||||
assert "addButton.hidden = !edit || !!opt.source;" in detail # [항목 추가] · [율 줄 추가] 걷음
|
||||
assert "rateButton.hidden = !edit || !!opt.source;" in detail
|
||||
assert (
|
||||
"addButton" not in detail and "rateButton" not in detail
|
||||
) # 840-1 [항목 추가] · [율 줄 추가] 걷음
|
||||
assert "if (!(await tabs.save())) throw" in detail and "await refreshing;" in detail
|
||||
assert "if (tabs.dirty()) void tabs.discard();" in detail # 버리고 떠남 = 수식 캐시도
|
||||
view = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts")
|
||||
|
||||
@@ -133,8 +133,10 @@ def test_808_1_모달_머리_일원화():
|
||||
assert "actions: row.참조 ? [slot, ...tools] : tools," in row and 't("FromSk")' in row
|
||||
assert "slot: o.slot," in _src("M02_MasterTemplete_Labor_RowSk.ts")
|
||||
detail = _src("M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "...(opt.slot\n ? []" in detail # 머리 칸 있으면 옛 단추 줄 없음
|
||||
assert "...(opt.slot ? [] : [" in detail # 머리 칸 있으면 머리 줄 없음
|
||||
assert "children: [tabs.tools]" in detail and "tabs.picker" not in detail
|
||||
assert r"/^SK-(?!0{6})\d{6}$/" in detail
|
||||
tabs = _src("M02_MasterTemplete_Sangeun_Tabs.ts")
|
||||
assert "const doc = !!k || !!opt.page || !!opt.memory?.();" in tabs # 836-3 · 836-5 문서 없는 줄 = 상세 몸만
|
||||
assert (
|
||||
"const doc = !!k || !!opt.page || !!opt.memory?.();" in tabs
|
||||
) # 836-3 · 836-5 문서 없는 줄 = 상세 몸만
|
||||
|
||||
@@ -220,8 +220,9 @@ def test_upload_native_one_file_list_only(world) -> None:
|
||||
ans = dim.run(dim.of(sk), sk)
|
||||
assert (ans["노무비"], ans["재료비"], ans["경비"]) == (74880, 7029, 12306)
|
||||
st = fh.upload_native(folder, stc.name, stc.read_bytes(), WHO)
|
||||
assert st["원파일"] and st["호표수"] == 0
|
||||
head = hs.grid(folder, st["키"])["책"]["시트"][0]["칸"]
|
||||
assert st["원파일"] and st["호표수"] == 45 # 828-1 STmate 구성표 일위대가
|
||||
sheets = hs.grid(folder, st["키"])["책"]["시트"] # 828-1 「일위대가표」 시트가 앞
|
||||
head = next(s for s in sheets if s["id"] == "s1")["칸"]
|
||||
assert head["A1"]["값"] == "코드" and str(head["A2"]["값"]).startswith("D")
|
||||
made = fh.make(folder, st["키"], next(iter(fn.numbers(native("STC", "봉화")))))
|
||||
assert made["검산"]["같음"]
|
||||
|
||||
@@ -82,7 +82,9 @@ def test_새_기계_단위_출처_자동_계수_서버_재계산(copy: Path) ->
|
||||
client = _app()
|
||||
lists = client.get("/api/m01/machine/lists").json()
|
||||
assert {"경유", "휘발유"} <= {f["value"] for f in lists["fuels"]}
|
||||
assert any(j["value"] == "1048" for j in lists["jobs"]) and lists["hour"] == 0.125
|
||||
assert (
|
||||
any(j["value"] == "1048" for j in lists["jobs"]) and abs(lists["hour"] - 5 / 24) < 1e-12
|
||||
) # 840-5
|
||||
found = client.get("/api/m01/machine/find", params={"q": "굴착기 1.0㎥"}).json()["rows"]
|
||||
seed = next(r for r in found if r["키"] == "EQ000022")
|
||||
before = _files(copy)
|
||||
|
||||
@@ -42,7 +42,9 @@ def test_lists_same_as_m01():
|
||||
assert set(got) == {"fuels", "jobs", "power", "hour"}
|
||||
for k in got:
|
||||
assert got[k] == want[k], k
|
||||
assert got["fuels"] and got["jobs"] and got["hour"] == 0.125
|
||||
assert (
|
||||
got["fuels"] and got["jobs"] and abs(got["hour"] - 5 / 24) < 1e-12
|
||||
) # 1/8 × 16/12 × 25/20(840-5)
|
||||
|
||||
|
||||
def test_find_mine_first_and_number(world):
|
||||
@@ -111,7 +113,7 @@ def _stack_doc(key, no):
|
||||
|
||||
def test_stack_line_copy(world):
|
||||
"""⑤ 줄 쌓기로 고른 내 기계 — 미리 셈 됨 · 저장 때 값 사본에 박힘 →
|
||||
고쳐도 금액 그대로 + 단가새값 → [단가 갱신] 바뀐 자원 1 → 지워도 마지막 값 + 지운키."""
|
||||
고치면 자동 따라옴(836-2 · 갱신 할 것 없음) → 지워도 저장한 사본 값 + 지운키."""
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
@@ -131,13 +133,13 @@ def test_stack_line_copy(world):
|
||||
copy = _at(me, lambda: store.read("sangeun", sk)["문서"]["수식"])
|
||||
assert key in [r["키"] for r in copy["단가"]] # 값 사본에 박힘
|
||||
_at(me, mym.update, key, {**made["줄"], "값": {**made["줄"]["값"], "가격": 3000}})
|
||||
now = _at(me, path.read, sk)
|
||||
assert now["셈"]["계"] == was and now["사본"]["단가새값"] is True
|
||||
now = _at(me, path.read, sk) # 836-2 프로젝트 밖 SK = 자동 따라옴(새 값 배지 · 갱신 할 것 없음)
|
||||
assert now["셈"]["계"] > was and now["사본"]["자동"] and not now["사본"]["단가새값"]
|
||||
job = _at(me, path.preview, sk, {"할일": "단가갱신"})
|
||||
assert [x["키"] for x in job["바뀐자원"]] == [key] and job["후"]["계"] > was
|
||||
assert job["바뀐자원"] == [] and job["후"]["계"] == now["셈"]["계"]
|
||||
_at(me, mym.delete, key)
|
||||
gone = _at(me, path.read, sk)
|
||||
assert gone["셈"]["계"] == was and gone["사본"]["지운키"] == [key]
|
||||
assert gone["셈"]["계"] == was # 지운 키 = 저장한 사본 줄 값
|
||||
|
||||
|
||||
def _mine_row():
|
||||
|
||||
@@ -124,7 +124,8 @@ WANT = ("파워트로웰", "3.73kW")
|
||||
|
||||
|
||||
def test_generator_ours_formula(world) -> None:
|
||||
"""발전기 5.5kW 내 기계 시간당 노무비 = 우리 노임 ÷ 8 × 1 인(EST 의 ×16/12 ×25/20 안 씀)."""
|
||||
"""발전기 5.5kW 내 기계 시간당 노무비 = 우리 노임 × 1/8 × 16/12 × 25/20 × 1 인(우리 식이 한 번 · 840-5 —
|
||||
EST 구성의 환산을 가져와 또 곱하지 않음 = 0.347 아님)."""
|
||||
me = world["personal"]
|
||||
me.mkdir(parents=True, exist_ok=True)
|
||||
body = mm.body(_main()["기계구성"][GEN], 0, _line("발전기", "5.5kW"), "main.estx")
|
||||
@@ -133,7 +134,7 @@ def test_generator_ours_formula(world) -> None:
|
||||
hour = mh.hourly(key)
|
||||
lb = m01.loaded()[1].numbers["LB"][_labor("일반기계운전사")]
|
||||
assert hour["ok"] and hour["로직"] == "GC000268"
|
||||
assert abs(hour["노무비"] - float(lb["값"]) / 8) < 1
|
||||
assert abs(hour["노무비"] - float(lb["값"]) * 5 / 24) < 1
|
||||
|
||||
|
||||
def test_make_flow(world) -> None:
|
||||
|
||||
@@ -39,7 +39,10 @@ POWER, POWER_CREW, ONE_FUEL, DIRECT = "GC001196", "GC001197", "GC001198", "GC001
|
||||
FUEL_POWER, FUEL_POWER_CREW = "GC001200", "GC001201"
|
||||
NAME = "sub1임시기계822"
|
||||
# 안 옮긴 기계 — 822-2 전 금액(전국평균 · 기본 옵션)
|
||||
KEEP = {"EQ000001": 68550, "EQ000022": 108958}
|
||||
KEEP = {
|
||||
"EQ000001": 92158,
|
||||
"EQ000022": 132566,
|
||||
} # 840-5 조종원 1/8 × 16/12 × 25/20(옛 68550 · 108958)
|
||||
|
||||
|
||||
def _rows() -> dict[str, dict]:
|
||||
@@ -196,7 +199,7 @@ def test_power_line_jumbo_drill():
|
||||
elec = Decimal("135") * Decimal("114.4")
|
||||
misc = int(elec * 6) / Decimal(100)
|
||||
assert h["경비"] == loss and h["재료비"] == int(elec + misc)
|
||||
assert h["노무비"] == int(_wage("1048") / 8)
|
||||
assert h["노무비"] == int(_wage("1048") * 5 / 24) # 1/8 × 16/12 × 25/20(840-5)
|
||||
p = _preview("EQ000347")
|
||||
assert (p["노무비"], p["재료비"], p["경비"], p["계"]) == (
|
||||
h["노무비"],
|
||||
@@ -237,10 +240,10 @@ def test_power_my_machine(world):
|
||||
# ⑥ · ⑦ 옮긴 기계 견본 ─────────────────────────────────────────────────
|
||||
def test_moved_samples():
|
||||
plant = mh.hourly("EQ000188") # 아스팔트 믹싱플랜트 60t — 연료 + 조종원 2 인
|
||||
assert plant["로직"] == "GC000268" and plant["노무비"] == int(_wage("1048") * 2 / 8)
|
||||
assert plant["로직"] == "GC000268" and plant["노무비"] == int(_wage("1048") * 2 * 5 / 24)
|
||||
assert plant["재료비"] > 0
|
||||
tower = mh.hourly("EQ000165") # 타워크레인 — 건설기계운전사 조종원 줄(요구 799)
|
||||
assert tower["로직"] == "GC000276" and tower["노무비"] == int(_wage("1048") / 8)
|
||||
assert tower["로직"] == "GC000276" and tower["노무비"] == int(_wage("1048") * 5 / 24)
|
||||
spray = m01.loaded()[1].get("EQ000198") # 아스팔트 스프레어 — 직종 빈 칸 = 직종 입력 식
|
||||
assert mh.pick(spray)["키"] == "GC000275" and "조종원직종" in json.dumps(
|
||||
mh.pick(spray)["입력"], ensure_ascii=False
|
||||
|
||||
@@ -83,7 +83,8 @@ def test_pick_whole_file(world) -> None:
|
||||
conv = dim.derive(doc, None, imp._whole())
|
||||
e = _entry(conv, WIRE)
|
||||
assert e["고름"]["키"] == pick["키"] and e["처음"]["갈래"] == "숫자"
|
||||
assert _var(conv, e["기호"])["연결"] == {"갈래": "요소", "키": pick["키"]} # 우리 값
|
||||
got = _var(conv, e["기호"])["연결"] # 우리 값(엑셀 기억 없음) · 840-3 줄 단위 실음
|
||||
assert got == {"갈래": "요소", "키": pick["키"], "단위": got["단위"]}
|
||||
assert sorted(mp.made(me, fp)) == sorted(made)
|
||||
assert mp.uses(fp, WIRE, (made[0],)) == [made[1]]
|
||||
# 다른 파일 = 영향 0
|
||||
|
||||
@@ -89,7 +89,7 @@ def test_굴착기_덮음_손셈(links):
|
||||
return {"노무비": int(labor), "재료비": int(mats), "경비": int(exp)}
|
||||
|
||||
fuel_amt = _cut1(fuel * oil)
|
||||
crew1 = _cut1(D(1) / 8 * wage)
|
||||
crew1 = _cut1(D(1) / 8 * 16 / 12 * 25 / 20 * wage) # 840-5
|
||||
misc = _cut1(fuel_amt * rate / 100)
|
||||
want = total(crew1, fuel_amt + misc, loss(price))
|
||||
got = mc.calc(_one("GC000267", NO), links)
|
||||
@@ -104,7 +104,7 @@ def test_굴착기_덮음_손셈(links):
|
||||
f20 = _cut1(20 * oil)
|
||||
assert run({"값": {"주연료": 20}}) == total(crew1, f20 + _cut1(f20 * rate / 100), loss(price))
|
||||
assert run({"조종원": [{"키": "LB:1048", "인": 2}]}) == total(
|
||||
_cut1(D(2) / 8 * wage), fuel_amt + misc, loss(price)
|
||||
_cut1(D(2) / 8 * 16 / 12 * 25 / 20 * wage), fuel_amt + misc, loss(price)
|
||||
)
|
||||
assert run({"조종원": []}) == total(0, fuel_amt + misc, loss(price))
|
||||
assert run({"잡품": [{"줄": "주연료", "율": 30}]}) == total(
|
||||
|
||||
@@ -53,7 +53,9 @@ def test_16_문서_이름_전수_겹침_없음():
|
||||
assert all("_" not in v and "^" not in v for v in alias.values()), (k, alias)
|
||||
forms |= {i for i in ids if not i.startswith("P_EQ기계")}
|
||||
assert not g["틀림"], (k, g["틀림"][:3]) # 식 글 왕복 = 같은 식
|
||||
assert len(forms) == 58 # 64 꼴 = 사전 58 + 「P_EQ기계~6」 읽는 칸 이름 6
|
||||
assert (
|
||||
len(forms) == 62
|
||||
) # 68 꼴 = 사전 62(840-5 상여 · 휴지 분자 · 분모 넷 더함) + 「P_EQ기계~6」 6
|
||||
|
||||
|
||||
def test_대표_이름():
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"""840-2 기계 보기 지킴 시험 — 값 박음 · 덮음 ↔ 기계 줄 · M01 같은 이름 막음 · 쓰기 없음.
|
||||
|
||||
짝 = `M02_MasterTemplete/M02_MyMachine_Sheet.py` `bind` ↔ `common_util/common_util_machine_sheet.ts` `bindDoc`
|
||||
(+ 덮음 ↔ 줄 `coverFrom` · `applyCover` — 화면 몫 · Node 로 돌림).
|
||||
① M01 기계 655 대 전수 — 식 고름 → 값 박은 식 셈 = 기계 표 요소로 읽는 셈(노 · 재 · 경 · 계 같음) · 식 없음 = 까닭.
|
||||
② 655 대 전수(TS) — 고침 없음 = 덮음 없음 · 줄 JSON 같음(열고 닫아도 M01 기계 파일 그대로) · 박음 거울.
|
||||
③ 굴착기 1.0㎥ — 가격 · 조종원(2 인 · 직종 · 뺌) · 잡품 율 덮음 → 줄 칸 · 덮음 얹고 박은 셈 = 줄 고친 셈.
|
||||
④ 내 기계 꼴 — 바로(상각비계수 → 손료계수) · 직접(시간당) · 처음 값으로 되돌리면 줄 같음.
|
||||
⑤ M01 과 같은 이름 · 규격 = 409 · 다른 이름 = 통과(검사만 · 쓰기 없음).
|
||||
"""
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from decimal import Decimal as D
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠
|
||||
from M01_MasterData import M01_MasterData_Doc as m01doc
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
from M02_MasterTemplete import M02_MyMachine_Sheet as ms
|
||||
|
||||
mc = importlib.import_module("master_doc_calc")
|
||||
ml = importlib.import_module("master_doc_link")
|
||||
mcv = importlib.import_module("master_doc_cover")
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
COSTS = ("노무비", "재료비", "경비", "계")
|
||||
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ 0201-0100 · GC000267
|
||||
|
||||
|
||||
def _plain(v):
|
||||
return json.loads(json.dumps(v, ensure_ascii=False, default=ms._plain))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def world():
|
||||
"""M01 기계 줄 · 식 고름 · 식 문서 — 한 번만(마스터 층 · 쓰기 없음)."""
|
||||
with store.at(store.FOLDER):
|
||||
rows = m01.loaded()[0]["기계.json"]["줄"]
|
||||
docs = m01doc.docs()
|
||||
links = ml.Links(m01.loaded()[1], docs)
|
||||
picks = {}
|
||||
for r in rows:
|
||||
got = mh.pick({**r, "값": m01._dec(r.get("값") or {})})
|
||||
picks[r["키"]] = str(got["키"]) if got and str(got["키"]) in docs else None
|
||||
yield {"rows": rows, "docs": docs, "links": links, "picks": picks}
|
||||
|
||||
|
||||
def _node(payload):
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_840_2_machine_sheet.cjs")],
|
||||
input=json.dumps(_plain(payload), ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _money(a):
|
||||
return {k: a.get(k) for k in COSTS}
|
||||
|
||||
|
||||
# ── ① 655 대 금액 ──────────────────────────────────────────────────────
|
||||
def test_655대_박은_식_셈_같음(world):
|
||||
links, docs = world["links"], world["docs"]
|
||||
same = none = 0
|
||||
for r in world["rows"]:
|
||||
key = world["picks"][r["키"]]
|
||||
if key is None:
|
||||
none += 1
|
||||
continue
|
||||
given = {"기계": str(r.get("원문번호"))}
|
||||
before = mc.calc(docs[key], links, given)
|
||||
after = mc.calc(ms.bind(docs[key], m01._dec(r.get("값") or {})), links, given)
|
||||
assert _money(after) == _money(before), (r["키"], key)
|
||||
same += before["셈됨"]
|
||||
assert len(world["rows"]) == 655 and same >= 600 and none <= 44
|
||||
|
||||
|
||||
def test_식_없는_기계_까닭(world):
|
||||
with store.at(store.FOLDER):
|
||||
for r in world["rows"]:
|
||||
if world["picks"][r["키"]] is None:
|
||||
got = ms.sheet(_plain(r))
|
||||
assert got["키"] is None and got["문서"] is None and got["까닭"]
|
||||
return
|
||||
pytest.skip("식 없는 기계 없음")
|
||||
|
||||
|
||||
# ── ② 655 대 덮음 ↔ 줄(TS) ──────────────────────────────────────────────
|
||||
def test_655대_고침_없음_줄_같음_박음_거울(world):
|
||||
docs, targets, want = {}, [], []
|
||||
for r in world["rows"]:
|
||||
key = world["picks"][r["키"]]
|
||||
if key is None:
|
||||
continue
|
||||
docs[key] = docs.get(key) or _plain(world["docs"][key])
|
||||
bind = r["키"] in (DIG, "EQ000001", world["rows"][-1]["키"])
|
||||
targets.append({"식": key, "처음": _plain(r), "개인": False,
|
||||
**({"박음": _plain(r["값"])} if bind else {})}) # fmt: skip
|
||||
want.append(_plain(ms.bind(world["docs"][key], r["값"])) if bind else None)
|
||||
got = _node({"문서들": docs, "대상": targets})
|
||||
for t, g, w in zip(targets, got, want):
|
||||
assert g["덮음"] == {}, t["처음"]["키"]
|
||||
assert json.dumps(g["줄"], ensure_ascii=False) == json.dumps(t["처음"], ensure_ascii=False)
|
||||
if w is not None:
|
||||
assert g["박음"] == w
|
||||
|
||||
|
||||
# ── ③ 굴착기 덮음 → 줄 · 셈 ────────────────────────────────────────────
|
||||
def test_굴착기_덮음_줄_칸_셈(world):
|
||||
row = _plain(next(r for r in world["rows"] if r["키"] == DIG))
|
||||
doc = world["docs"]["GC000267"]
|
||||
given = {"기계": row["원문번호"]}
|
||||
jobs = ms._jobs()
|
||||
lb = next(k for k, v in jobs.items() if v == "1002")
|
||||
cases = [
|
||||
({"값": {"가격": 150000}}, {"가격": 150000}),
|
||||
({"조종원": [{"키": "LB:1048", "인": 2}]}, {"조종원": 2}),
|
||||
({"조종원": [{"키": lb, "인": 1}]}, {"조종원직종": "1002"}),
|
||||
({"조종원": []}, {"조종원": 0}),
|
||||
({"잡품": [{"줄": "주연료", "율": 30}]}, {"잡재료율": 30}),
|
||||
]
|
||||
targets = [
|
||||
{"식": "GC000267", "처음": row, "덮음": c, "개인": False, "직종": jobs} for c, _ in cases
|
||||
]
|
||||
got = _node({"문서들": {"GC000267": _plain(doc)}, "대상": targets})
|
||||
for (cover, change), g in zip(cases, got):
|
||||
want = copy.deepcopy(row)
|
||||
want["값"].update(change)
|
||||
assert g["줄"] == want, cover
|
||||
shaped = ms.bind(
|
||||
mcv.cover_doc(doc, given, {row["원문번호"]: m01._dec(cover)}), m01._dec(row["값"])
|
||||
)
|
||||
edited = ms.bind(doc, m01._dec(g["줄"]["값"]))
|
||||
a, b = mc.calc(shaped, world["links"], given), mc.calc(edited, world["links"], given)
|
||||
assert a["셈됨"] and _money(a) == _money(b), cover
|
||||
# 지금 줄(초안) → 덮음 — 처음 줄 대비 고친 칸만
|
||||
cur = copy.deepcopy(row)
|
||||
cur["값"].update({"가격": 150000, "조종원": 2, "잡재료율": 30})
|
||||
made = _node({"문서들": {"GC000267": _plain(doc)},
|
||||
"대상": [{"식": "GC000267", "처음": row, "지금": cur, "개인": False}]})[0] # fmt: skip
|
||||
assert made["덮음"] == {
|
||||
"값": {"가격": 150000},
|
||||
"조종원": [{"키": "LB:1048", "인": 2}],
|
||||
"잡품": [{"줄": "주연료", "율": 30}],
|
||||
}
|
||||
assert made["줄"] == cur
|
||||
|
||||
|
||||
# ── ④ 내 기계 꼴 ───────────────────────────────────────────────────────
|
||||
def test_내기계_바로_직접(world):
|
||||
easy = {"이름": "t", "규격": "", "방식": "바로",
|
||||
"값": {"가격": 1000, "손료계수": 2000, "주연료": 5, "주연료종류": "경유"}} # fmt: skip
|
||||
hour = {"이름": "t", "규격": "", "방식": "직접", "값": {},
|
||||
"시간당": {"노무비": 1, "재료비": 2, "경비": 3}} # fmt: skip
|
||||
with store.at(store.FOLDER):
|
||||
a, b = ms.sheet(easy, mine=True), ms.sheet(hour, mine=True)
|
||||
assert a["키"] == "GC000272" and b["키"] == "GC001199"
|
||||
assert a["값"]["상각비계수"] == 2000 and a["값"]["정비비계수"] == 0
|
||||
docs = {a["키"]: a["문서"], b["키"]: b["문서"]}
|
||||
targets = [
|
||||
{"식": a["키"], "처음": easy, "개인": True},
|
||||
{"식": a["키"], "처음": easy, "개인": True, "덮음": {"값": {"상각비계수": 2500}}},
|
||||
{"식": b["키"], "처음": hour, "개인": True},
|
||||
{"식": b["키"], "처음": hour, "개인": True, "덮음": {"값": {"경비": 10}}},
|
||||
]
|
||||
got = _node({"문서들": docs, "대상": targets})
|
||||
assert got[0]["덮음"] == {} and got[0]["줄"] == easy
|
||||
assert got[1]["줄"]["값"] == {**easy["값"], "손료계수": 2500}
|
||||
assert got[2]["덮음"] == {} and got[2]["줄"] == hour
|
||||
assert got[3]["줄"]["시간당"] == {"노무비": 1, "재료비": 2, "경비": 10}
|
||||
|
||||
|
||||
# ── ⑤ M01 같은 이름 막음 ───────────────────────────────────────────────
|
||||
def test_M01_같은_이름_막음(world):
|
||||
dig = next(r for r in world["rows"] if r["키"] == DIG)
|
||||
with store.at(store.FOLDER):
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
mym.not_m01({"이름": dig["이름"], "규격": dig["규격"]})
|
||||
assert e.value.status == 409
|
||||
mym.not_m01({"이름": "sub3임시 없는 기계", "규격": "1"})
|
||||
|
||||
|
||||
def test_서버_덮음_몸_틀림_400():
|
||||
with store.at(store.FOLDER), pytest.raises(m01.StoreError) as e:
|
||||
ms.sheet({"원문번호": "0201-0100", "값": {}}, None, {"값": {"가격": -1}})
|
||||
assert e.value.status == 400
|
||||
|
||||
|
||||
def test_수_꼴():
|
||||
assert ms._plain(D("1.50")) == 1.5 and ms._plain(D("3")) == 3
|
||||
@@ -0,0 +1,74 @@
|
||||
"""840-3 불러오기 재료 단위 — ㉠ 같은 이름 · 규격 쌍둥이 중 경쟁사 줄과 같은 단위 줄 먼저 · ㉡ 같은 성격
|
||||
단위는 이은 뒤 셈이 줄 단위로 환산(표시 = `단위경고` 「단위 환산 톤 → kg」) · ㉢ 성격 다르면 잇지 않고 고르기(요구 860 · 862).
|
||||
|
||||
판정 = `M02_San_MatFind`(`M02_San_Link.m01`) · 값 = `master_pick.priced`(자재품목 바로 이음) · 문서 연결 = `M02_Doc_Import._link_of`.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path[:0] = [str(ROOT), str(ROOT / "resources" / "master_data" / "scripts")]
|
||||
|
||||
import master_material as mm # noqa: E402
|
||||
import master_pick as mp # noqa: E402
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Link as sl # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_MatFind as mf # noqa: E402
|
||||
|
||||
|
||||
def _go(name, spec, unit, code="", kind=""):
|
||||
src = {"이름": name, "규격": spec, "단위": unit, "갈래": "재료", "코드": code, "종류": kind}
|
||||
key, _mark = sl.m01("재료", src)
|
||||
return key, src["재료찾기"]
|
||||
|
||||
|
||||
def test_twin_same_unit_first():
|
||||
"""잔디 ㎡ 줄 — 매 줄(MT006796)이 아니라 ㎡ 쌍둥이(MT006799)."""
|
||||
key, found = _go("천연잔디(한국형)", "0.18×0.18×0.01", "㎡")
|
||||
assert key == "MT006799" and found["판정"] == mf.LINKED
|
||||
key, _ = _go("천연잔디(한국형)", "0.18×0.18×0.01", "매")
|
||||
assert key == "MT006796"
|
||||
|
||||
|
||||
def test_unit_kind_differs_goes_to_pick():
|
||||
"""골재시험 회 ↔ 자재 kg · 각재 ㎥ ↔ 재 = 이음 없음 · 판정 고르기 · 까닭 글 · 후보에 그 줄."""
|
||||
for name, spec, unit, was in (
|
||||
("골재시험", "0.08mm체 통과량시험", "회", "MT009890"),
|
||||
("각재(외송)", "3.6×3.0×3.0", "㎥", "MT000393"),
|
||||
):
|
||||
key, found = _go(name, spec, unit)
|
||||
assert key is None and found["판정"] == mf.UNIT
|
||||
assert found["방법"].startswith(f"단위 다름 {unit} ↔ ")
|
||||
assert was in {c["키"] for c in found["후보"]}
|
||||
|
||||
|
||||
def test_same_meaning_units_stay_linked():
|
||||
"""본 ↔ EA · 개소 ↔ 개(개수 1 : 1) = 같은 단위로 봄 — 이음 그대로 · 표시 없음."""
|
||||
key, found = _go("시스템동바리(수직재)", "D60.5×2.6T P1725", "본")
|
||||
assert key == "MT000512" and found["판정"] == mf.LINKED and "단위" not in found["방법"]
|
||||
|
||||
|
||||
def test_ton_to_kg_price():
|
||||
"""자재 Ton 줄을 kg 줄에 이음 → 단가 ÷ 1,000 · 표시 「단위 환산 Ton → kg」 · 성격 다름 = 값 그대로 + 경고."""
|
||||
whole = m01.loaded()[1]
|
||||
base, _ = mm._lowest(whole, "MT000811")
|
||||
price, _src, _why, extra = mp.priced(whole, {"단위": "kg"}, "MT000811", {}, None)
|
||||
assert price == base * Decimal(1000) / Decimal(1000000)
|
||||
assert extra["단위경고"] == "단위 환산 Ton → kg"
|
||||
price, *_rest, extra = mp.priced(whole, {"단위": "㎡"}, "MT000811", {}, None)
|
||||
assert price == base and extra["단위경고"] == "줄 ㎡ · 자재 Ton"
|
||||
price, *_rest, extra = mp.priced(whole, {"단위": "ton"}, "MT000811", {}, None)
|
||||
assert price == base and "단위경고" not in extra
|
||||
|
||||
|
||||
def test_doc_link_carries_unit():
|
||||
"""문서 연결(호표 길) — 자재 이음에 줄 단위 · 사람이 고른 줄도 성격 다르면 설명에 경고."""
|
||||
link, note = dim._link_of({"이음": "MT000811", "단위": "kg"})
|
||||
assert link == {"갈래": "요소", "키": "MT000811", "단위": "kg"} and note == "단위 환산 Ton → kg"
|
||||
link, note = dim._link_of({"이음": "MT000811", "단위": "㎡"})
|
||||
assert link["단위"] == "㎡" and note == "단위 다름 ㎡ ↔ Ton — 값은 자재 단위 그대로"
|
||||
assert dim._link_of({"이음": "MT000811", "단위": "Ton"})[1] == ""
|
||||
@@ -0,0 +1,78 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""840-4 내 재료(개인 자재 PM) = M01 재료 DB 에 없는 자재 등록 자리 — 임시 storage.
|
||||
|
||||
① M01 재료 DB 와 이름 · 규격 · 단위가 같으면 만들기 · 고치기 막음(409) · 품명 · 단위 빈 = 400
|
||||
② 단가산출 재료 줄이 내 재료를 M01 재료처럼 읽음(미리 셈 · 저장)
|
||||
③ 내 재료 값을 고치면 개인 층 단가산출 금액이 따라옴(836-2 자동 따라옴 길)
|
||||
④ 쓰는 곳 있으면 지움 막음(409) · 쓰는 곳 없으면 지움 · 없는 키 = 404.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Material_Own as own
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
NAME = "sub4임시자재"
|
||||
|
||||
|
||||
def _at(folder, fn, *args):
|
||||
with store.at(folder):
|
||||
return fn(*args)
|
||||
|
||||
|
||||
def _code(fn, *args) -> int:
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
fn(*args)
|
||||
return e.value.status
|
||||
|
||||
|
||||
def _doc(key: str, code: str) -> dict:
|
||||
"""줄 쌓기 재료 줄 하나(화면이 저장하는 꼴) — 단가 = 요소 이음(내 재료)."""
|
||||
var = {"기호": "P", "아래": "재", "위": "", "이름": NAME, "단위": "",
|
||||
"연결": {"갈래": "요소", "키": code, "단위": "kg"}} # fmt: skip
|
||||
line = {"이름": NAME, "단위": "kg", "비목": "재료비", "수량": "2", "단가": "P_재"}
|
||||
return {"꼴": "수식문서", "판": 1, "키": key, "이름": NAME, "결과단위": "원",
|
||||
"변수": [var], "식": [], "금액": [line]} # fmt: skip
|
||||
|
||||
|
||||
def test_m01_same_blocked(world):
|
||||
me = world["personal"]
|
||||
mt = next(r for r in m01.loaded()[1].index["MT"].values() if r.get("이름") and r.get("단위"))
|
||||
same = {"이름": f" {mt['이름']} ", "규격": mt.get("규격") or "", "단위": mt["단위"], "값": 1}
|
||||
assert _at(me, _code, own.add, me, same) == 409
|
||||
assert _at(me, _code, own.add, me, {"이름": NAME, "단위": ""}) == 400
|
||||
row = _at(me, own.add, me, {"이름": NAME, "규격": "x", "단위": "kg", "값": 1000})
|
||||
assert own.is_own(row["키"]) and own.m01_same(row) is None
|
||||
assert _at(me, _code, own.update, me, row["키"], same) == 409
|
||||
assert _at(me, _code, own.update, me, "PM999999", {"이름": NAME, "단위": "kg"}) == 404
|
||||
assert (
|
||||
_at(me, _code, own.add, me, {"이름": NAME, "규격": "X", "단위": "㎏"}) == 409
|
||||
) # 내 목록 같음
|
||||
|
||||
|
||||
def test_sk_reads_and_follows(world):
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
me = world["personal"]
|
||||
code = _at(me, own.add, me, {"이름": NAME, "규격": "y", "단위": "kg", "값": 1000})["키"]
|
||||
sk = _at(me, lambda: refs.create("sangeun", {"이름": NAME, "단위": "식", "더한줄": []})["이름"])
|
||||
doc = _doc(sk, code)
|
||||
assert _at(me, path.preview, sk, {"문서": doc})["셈"]["계"] == 2000 # 저장 전 미리 셈
|
||||
_at(me, path.save, sk, {"문서": doc, "판": _at(me, path.read, sk)["판"]})
|
||||
assert _at(me, path.read, sk)["셈"]["계"] == 2000
|
||||
_at(me, own.update, me, code, {"이름": NAME, "규격": "y", "단위": "kg", "값": 1500})
|
||||
assert _at(me, path.read, sk)["셈"]["계"] == 3000 # 값 바꿈 → 따라옴
|
||||
assert [u["키"] for u in _at(me, own.uses, me, code)] == [sk]
|
||||
assert _at(me, _code, own.remove, me, code) == 409 # 쓰는 곳 있음
|
||||
_at(me, store.delete, "sangeun", sk)
|
||||
aside = me / "_합친것" / f"{sk}.json" # 파생 · 보관 폴더 = 쓰는 곳 아님
|
||||
aside.parent.mkdir(parents=True, exist_ok=True)
|
||||
aside.write_text(f'{{"이름": "x", "키": "{code}"}}', encoding="utf-8")
|
||||
_at(me, own.remove, me, code)
|
||||
assert all(r["키"] != code for r in own.rows(me))
|
||||
assert _at(me, _code, own.remove, me, code) == 404
|
||||
@@ -55,7 +55,7 @@ def test_추가하면_PM_6자리_키_목록에_보임_화면_키_무시(world):
|
||||
assert got.status_code == 200, got.text
|
||||
row = got.json()
|
||||
assert row == {"키": "PM000001", "이름": "앵커볼트", "규격": "M16", "단위": "EA", "값": 1200}
|
||||
assert c.get(URL.format("personal")).json()["줄"] == [row]
|
||||
assert c.get(URL.format("personal")).json()["줄"] == [{**row, "쓰는곳": []}] # 840-4 쓰는 곳
|
||||
path = layers.personal_dir(7, 42) / own.FILE
|
||||
assert path.is_file()
|
||||
assert own.read(path.parent)["그룹"] == "재료"
|
||||
|
||||
@@ -220,7 +220,7 @@ def test_상세_화면_돌아가기는_집계표로_그_열_고른_채():
|
||||
main = _text(MAIN)
|
||||
# M01 꼴 — 제목 앞(39-5) · 105-4 상세 이동 묶음(nav) · 49-7 [?] 칸(helpSlot)이 그 뒤
|
||||
assert re.search(
|
||||
r"children: \[\s*back,\s*title,\s*nav\.root,\s*helpSlot,\s*slot,\s*reload,\s*actions\.reset,\s*actions\.del,\s*save,",
|
||||
r"children: \[\s*back,\s*title,\s*nav\.root,\s*helpSlot,\s*slot,\s*reload,\s*actions\.reset,\s*actions\.del,\s*actions\.undo,\s*save,",
|
||||
main,
|
||||
)
|
||||
assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 일위대가 · 면적산출만
|
||||
|
||||
@@ -89,10 +89,10 @@ def test_굴착기(whole):
|
||||
whole, EXCAVATOR, {"기계": "0201-0070", "지역": "전국평균", "보정작업": "해당 없음"}
|
||||
)
|
||||
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "계")] == [
|
||||
"35412.13", # 조종원 = 하루 노임 × 1/8(713-4 · 질의회신 상여 · 휴지계수 뺌)
|
||||
"59020.21", # 조종원 = 하루 노임 × 1/8 × 16/12 × 25/20(840-5 · 옛 1/8 = 35412.13)
|
||||
"26130.11",
|
||||
"24554.00",
|
||||
"86096.24",
|
||||
"109704.32",
|
||||
]
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ def test_굴착기_암석_보정(whole):
|
||||
{"기계": "0201-0070", "지역": "전국평균", "보정작업": "암석작업(연암・보통암・경암)"},
|
||||
)
|
||||
assert got["중간"]["손료"] == 28322
|
||||
assert cents(got["계"]) == "89864.24"
|
||||
assert cents(got["계"]) == "113472.32" # 840-5 조종원 환산
|
||||
|
||||
|
||||
def test_불도저_덤프_로더_손료_보정(whole):
|
||||
@@ -148,10 +148,10 @@ def test_노체포설(whole):
|
||||
assert mf.run(whole, "GF000118", {})["결과"] == Decimal("24.255")
|
||||
got = mf.run(whole, "GF000119", {"지역": "전국평균", "보정작업": "해당 없음"})
|
||||
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "계")] == [
|
||||
"1459.99", # 조종원 = 하루 노임 × 1/8(713-4)
|
||||
"2433.32", # 조종원 = 하루 노임 × 1/8 × 16/12 × 25/20(840-5)
|
||||
"1077.31",
|
||||
"1012.33",
|
||||
"3549.63",
|
||||
"4522.96",
|
||||
]
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ def test_굴착기_부착용_집게(whole):
|
||||
{"기계": "0201-0060", "집게": "7206-0070", "지역": "전국평균", "보정작업": "해당 없음"},
|
||||
)
|
||||
assert got["중간"]["집게손료"] == 5786
|
||||
assert cents(got["계"]) == "86467.61"
|
||||
assert cents(got["계"]) == "110075.69" # 840-5 조종원 환산
|
||||
|
||||
|
||||
def test_불도저_운반_작업량(whole):
|
||||
|
||||
@@ -187,7 +187,6 @@ export const ui_locales_m2 = {
|
||||
M02_SangeunSwapCoef: ["환산 계수", "Conversion factor"],
|
||||
M02_SangeunSwapWeight: ["단위중량", "Unit weight"],
|
||||
M02_SangeunSwapPick: ["고르세요", "Choose"],
|
||||
M02_SangeunAddLine: ["항목 추가", "Add item"],
|
||||
M02_SangeunDrop: ["삭제", "Delete"],
|
||||
M02_SangeunDropBase: ["기초 빼기", "Remove base"],
|
||||
M02_SangeunBases: ["더한 기초", "Added bases"],
|
||||
@@ -236,6 +235,7 @@ export const ui_locales_m2 = {
|
||||
M02_FindClass: ["구분", "Class"],
|
||||
M02_FindLogic: ["단가산출 기초", "Unit-cost logic"],
|
||||
M02_FindNewMaterial: ["신규 자재", "New material"],
|
||||
M02_FindMyMaterial: ["내 재료", "My material"],
|
||||
M02_FindElementLater: ["일위대가 표 개편 뒤", "After the unit-cost table rework"],
|
||||
M02_FindOptions: ["옵션 {n}", "{n} options"],
|
||||
// 729-3 불러오기 — 안 이은 줄 · 일위대가표 띠
|
||||
@@ -368,7 +368,6 @@ export const ui_locales_m2 = {
|
||||
M02_ToneDanger: ["못 이음", "Not linked"],
|
||||
M02_ToneMuted: ["밖", "Outside"],
|
||||
M02_ToneMine: ["내가 고름", "My pick"],
|
||||
M02_SkRateAdd: ["율 줄 추가", "Add % line"],
|
||||
M02_SkRateTitle: ["율 줄 — 기준 × 율 %", "% line — base × rate %"],
|
||||
M02_SkRateName: ["이름", "Name"],
|
||||
M02_SkRateDefault: ["공구손료", "Tool cost"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* ui_template_locale_machine.ts
|
||||
* 기계 양식 부품(`ui_template_machine_form.ts`) · M01 기계 목록 · 상세 사전(PLAN 821-2) — 신규 문구는 최하단에.
|
||||
* 기계 보기(`A00_Common/line_stack_machine_sheet.ts` · 840-2 · 옛 기계 양식 걷음) · M01 기계 목록 · 상세 사전(PLAN 821-2) — 신규 문구는 최하단에.
|
||||
* ========================================================================== */
|
||||
|
||||
export const ui_locales_machine = {
|
||||
@@ -78,4 +78,17 @@ export const ui_locales_machine = {
|
||||
M01_MachineErrors: ["고칠 칸이 있음", "Fix the marked fields"],
|
||||
M01_MachineDetailGroup: ["상세구분", "Sub-group"],
|
||||
M01_MachineSourceNo: ["원문번호", "Source no."],
|
||||
// 기계 보기(840-2 · `A00_Common/line_stack_machine_sheet`)
|
||||
MS_Base: ["기본 정보 · 손료 근거", "Basics · ownership basis"],
|
||||
MS_Mode: ["손료 방식", "Ownership method"],
|
||||
MS_Out: ["시간당 경비 산출근거", "Hourly cost breakdown"],
|
||||
MS_NoFormula: ["시간당 경비 식 없음", "No hourly formula"],
|
||||
MS_Loading: ["식 불러오는 중", "Loading formula"],
|
||||
MS_Locked: ["품셈 방식 — 손료 근거 비율로 셈", "By standard — computed from the ratios"],
|
||||
MS_Coefs: ["계수(비율로 셈)", "Coefficients (from ratios)"],
|
||||
MS_M01Same: [
|
||||
"M01 에 있는 기계 — 값은 단가산출 안에서 고침 · 내 기계는 M01 에 없는 기계만",
|
||||
"Already in M01 — change its values inside a unit-cost sheet · my machines are for machines not in M01",
|
||||
],
|
||||
MS_Range: ["{name} 보통 범위 {value} 밖", "{name} outside usual range {value}"],
|
||||
} as const;
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
/* 기계 양식(821-2) — 왼쪽 칸 · 오른쪽 시간당 계산식 · 좁은 폭(720 아래)은 한 줄로 */
|
||||
.ui-machine-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(280px, 1fr);
|
||||
gap: var(--spacing-16);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.ui-machine-form__fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-machine-form__row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: var(--spacing-8) var(--spacing-12);
|
||||
}
|
||||
|
||||
.ui-machine-form__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ui-machine-form__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-12);
|
||||
}
|
||||
|
||||
.ui-machine-form__coefs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ui-machine-form__coefs .ui-input {
|
||||
background: var(--color-surface-sunken);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.ui-machine-form .is-changed .ui-input,
|
||||
.ui-machine-form .is-changed .ui-select {
|
||||
background: color-mix(in srgb, var(--color-warning) 28%, var(--color-canvas));
|
||||
}
|
||||
|
||||
.ui-machine-form__warn {
|
||||
min-height: 0;
|
||||
color: var(--color-warning);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-machine-form__warn:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-machine-form__notice:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-machine-form__similar {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
}
|
||||
|
||||
.ui-machine-form__similar > summary {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.ui-machine-form__similar .ui-find__list {
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ui-machine-form__preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.ui-machine-form__preview .ui-calc-row--formula {
|
||||
grid-template-columns: 64px minmax(0, 1fr) 96px;
|
||||
}
|
||||
|
||||
.ui-machine-form__rule {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin: var(--spacing-4) 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.ui-machine-form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.ui-machine-form__preview {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
/* =============================================================================
|
||||
* ui_template_machine_form.ts
|
||||
* 부품 「기계 양식」(PLAN 821-2 · 요구서 796 1 장) — M01 기계 상세 · M02 내 기계 · 불러오기가 같이 씀.
|
||||
*
|
||||
* 칸 = 이름 · 규격 / 가격(천원) / 손료계수(바로 넣기 · 품셈 방식) / 주연료(종류 고르기 + ℓ/hr) /
|
||||
* 잡품 %(주연료의) / 조종원(직종 고르기 + 인) / 전력(kWh · 선택) · 「시간당 값 직접」 꼴(allowHourly)
|
||||
* 옆 = 시간당 경비 계산식 즉시(`common_util_machine_coef` · 정본은 서버 식)
|
||||
* 찾기 = 「비슷한 기계에서 시작」(findSimilar 를 주면) — 고르면 값 · 이름 · 규격 복사
|
||||
* 상태 = 숫자만(아니면 오류) · 범위 밖 경고 · 고친 칸 노랑 · 같은 이름 알림 자리(setNotice) · readOnly
|
||||
* 자료 = 목록(연료 · 직종 · 단가) · 찾기 · 저장은 부르는 쪽이 줌 — 이 부품은 M01 · M02 에 안 묶임
|
||||
* 값 묶음 = 기계 줄 `값` 꼴 그대로(칸 이름 같음) · 안 보이는 칸(선원 등)은 손대지 않음
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
MACHINE_COEFS,
|
||||
MACHINE_INPUTS,
|
||||
machineCoefOf,
|
||||
machineHourly,
|
||||
} from "../common_util/common_util_machine_coef";
|
||||
import { createCalcFormula, createCalcFormulaRow } from "./ui_template_calc_row";
|
||||
import { el, createInputField, createSelectField } from "./ui_template_elements";
|
||||
import { createFindList } from "./ui_template_find_list";
|
||||
import { attachPickSearch } from "./ui_template_pick_search";
|
||||
import { t as L } from "./ui_template_locale";
|
||||
import { createSegment } from "./ui_template_segment";
|
||||
import { createText } from "./ui_template_text";
|
||||
import "./ui_template_machine_form.css";
|
||||
|
||||
export type MachineValues = Record<string, unknown>;
|
||||
|
||||
/** 고르기 목록 한 칸 — 연료(값 = 종류 · 값 단가 원/ℓ) · 직종(값 = 코드 · 단가 원/일) */
|
||||
export interface MachinePick {
|
||||
value: string;
|
||||
text: string;
|
||||
price: number | null;
|
||||
}
|
||||
|
||||
export interface MachineSimilar {
|
||||
key: string;
|
||||
name: string;
|
||||
spec: string;
|
||||
/** 흐린 꼬리표(구분 · 내 기계 등) */
|
||||
tag?: string;
|
||||
values: MachineValues;
|
||||
}
|
||||
|
||||
export interface MachineFormOptions {
|
||||
name?: string;
|
||||
spec?: string;
|
||||
values?: MachineValues;
|
||||
/** 손료계수 방식 기본 — 값에 품셈 입력 다섯이 있으면 늘 품셈 방식 */
|
||||
coefMode?: "direct" | "pumsem";
|
||||
/** 「시간당 값 직접」 꼴 허용(내 기계) */
|
||||
allowHourly?: boolean;
|
||||
fuels: MachinePick[];
|
||||
jobs: MachinePick[];
|
||||
/** 전력 단가(원/kWh) · 시간환산(조종원 하루 → 한 시간) */
|
||||
power?: number;
|
||||
hour?: number;
|
||||
readOnly?: boolean;
|
||||
findSimilar?: (q: string) => Promise<MachineSimilar[]>;
|
||||
onSimilar?: (item: MachineSimilar) => void;
|
||||
onChange?: () => void;
|
||||
}
|
||||
|
||||
export interface MachineFormState {
|
||||
name: string;
|
||||
spec: string;
|
||||
values: MachineValues;
|
||||
/** 숫자 아닌 칸 등 — 있으면 저장 막음 */
|
||||
errors: string[];
|
||||
changed: boolean;
|
||||
/** 손료계수 방식 · 「시간당 값 직접」 꼴인지(내 기계 저장 방식) */
|
||||
mode: "direct" | "pumsem";
|
||||
hourly: boolean;
|
||||
}
|
||||
|
||||
export interface MachineFormHandle {
|
||||
root: HTMLElement;
|
||||
state: () => MachineFormState;
|
||||
/** 같은 이름 · 규격 알림 등(빈 글 = 지움) */
|
||||
setNotice: (text: string) => void;
|
||||
}
|
||||
|
||||
/** 시간당 값 직접 칸 · 비목 */
|
||||
export const HOURLY_KEY = "시간당";
|
||||
const HOURLY_COSTS = ["노무비", "재료비", "경비"] as const;
|
||||
const HOURLY_LABEL = { 노무비: "MF_Labor", 재료비: "MF_Material", 경비: "MF_Expense" } as const;
|
||||
|
||||
interface NumSpec {
|
||||
key: string;
|
||||
label: string;
|
||||
unit: string;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
const n = (key: string, label: string, unit: string, min: number, max: number): NumSpec => ({
|
||||
key,
|
||||
label,
|
||||
unit,
|
||||
min,
|
||||
max,
|
||||
});
|
||||
const PRICE = n("가격", "MF_Price", "천원", 0, 5_000_000);
|
||||
const COEF = n("손료계수", "MF_Coef", "10⁻⁷/hr", 1, 100_000);
|
||||
const PUMSEM = [
|
||||
n("내용시간", "MF_Life", "시간", 100, 100_000),
|
||||
n("연간표준가동시간", "MF_Yearly", "시간", 100, 8_760),
|
||||
n("상각비율", "MF_Dep", "", 0, 1),
|
||||
n("정비비율", "MF_Rep", "", 0, 3),
|
||||
n("연간관리비율", "MF_Mgmt", "", 0, 1),
|
||||
];
|
||||
const FUEL = n("주연료", "MF_Fuel", "ℓ/hr", 0, 2_000);
|
||||
const MISC = n("잡재료율", "MF_Misc", "%", 0, 100);
|
||||
const CREW = n("조종원", "MF_Crew", "인", 0, 20);
|
||||
const POWER = n("전력", "MF_Power", "kWh", 0, 10_000);
|
||||
const COEF_LABEL: Record<string, string> = {
|
||||
상각비계수: "MF_DepCoef",
|
||||
정비비계수: "MF_RepCoef",
|
||||
관리비계수: "MF_MgmtCoef",
|
||||
손료계수: "MF_Coef",
|
||||
};
|
||||
|
||||
const tr = (key: string): string => L(key as "MF_Name");
|
||||
const fmt = (v: number): string => v.toLocaleString("ko-KR", { maximumFractionDigits: 4 });
|
||||
const shown = (v: unknown): string => (v === null || v === undefined ? "" : String(v));
|
||||
const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
|
||||
|
||||
/** 글 → 수 · 빈 글 = null · 수 아님 = NaN */
|
||||
function parseNum(text: string): number | null {
|
||||
const s = text.replace(/,/g, "").trim();
|
||||
if (!s) return null;
|
||||
return /^-?\d+(\.\d+)?$/.test(s) ? Number(s) : Number.NaN;
|
||||
}
|
||||
|
||||
export function createMachineForm(opts: MachineFormOptions): MachineFormHandle {
|
||||
const init: MachineValues = structuredClone(opts.values ?? {});
|
||||
let values: MachineValues = structuredClone(init);
|
||||
let name = opts.name ?? "";
|
||||
let spec = opts.spec ?? "";
|
||||
const startName = name;
|
||||
const startSpec = spec;
|
||||
const hasPumsem = MACHINE_INPUTS.some((k) => typeof init[k] === "number");
|
||||
let mode: "direct" | "pumsem" = hasPumsem ? "pumsem" : (opts.coefMode ?? "direct");
|
||||
let hourlyMode = init[HOURLY_KEY] !== undefined && Boolean(opts.allowHourly);
|
||||
const errors = new Map<string, string>();
|
||||
|
||||
const root = el("div", { className: "ui-machine-form" });
|
||||
const fields = el("div", { className: "ui-machine-form__fields" });
|
||||
const side = el("aside", { className: "ui-machine-form__preview" });
|
||||
const notice = el("div", { className: "ui-machine-form__notice" });
|
||||
|
||||
/** 칸 값을 값 묶음에 — 빈 값은 원래 칸이 있었으면 null · 없었으면 칸을 뺌 */
|
||||
const put = (key: string, value: unknown): void => {
|
||||
if (value === null || value === "") {
|
||||
if (key in init) values[key] = null;
|
||||
else delete values[key];
|
||||
} else values[key] = value;
|
||||
};
|
||||
|
||||
const changed = (): boolean => name !== startName || spec !== startSpec || !same(values, init);
|
||||
|
||||
/** 품셈 입력이 처음과 같으면 저장된 계수 그대로(원문 표 값) · 다르면 계수 짝으로 셈 */
|
||||
const syncCoefs = (): void => {
|
||||
if (mode !== "pumsem") return;
|
||||
const sameInputs = MACHINE_INPUTS.every((k) => same(values[k], init[k]));
|
||||
const got = sameInputs ? null : machineCoefOf(values);
|
||||
for (const k of MACHINE_COEFS) {
|
||||
if (sameInputs) put(k, init[k] ?? null);
|
||||
else put(k, got ? got[k] : null);
|
||||
}
|
||||
};
|
||||
|
||||
const fire = (): void => {
|
||||
syncCoefs();
|
||||
paintPreview();
|
||||
paintCoefs();
|
||||
opts.onChange?.();
|
||||
};
|
||||
|
||||
/** 숫자 칸 — 숫자만 · 범위 밖 경고 · 고친 칸 노랑 */
|
||||
const numField = (spec_: NumSpec, read = false, hold?: MachineValues): HTMLElement => {
|
||||
const bag = hold ?? values;
|
||||
const field = createInputField({
|
||||
label: tr(spec_.label),
|
||||
value: shown(bag[spec_.key]),
|
||||
unit: spec_.unit || undefined,
|
||||
size: 28,
|
||||
});
|
||||
field.input.inputMode = "decimal";
|
||||
field.input.dataset.key = spec_.key;
|
||||
const warn = el("span", { className: "ui-machine-form__warn" });
|
||||
field.root.append(warn);
|
||||
const mark = (): void => {
|
||||
const now = hold ? hold[spec_.key] : values[spec_.key];
|
||||
const was = hold
|
||||
? (init[HOURLY_KEY] as MachineValues | undefined)?.[spec_.key]
|
||||
: init[spec_.key];
|
||||
field.root.classList.toggle("is-changed", !same(now ?? null, was ?? null));
|
||||
};
|
||||
if (read || opts.readOnly) field.input.readOnly = true;
|
||||
field.input.addEventListener("input", () => {
|
||||
const v = parseNum(field.input.value);
|
||||
warn.textContent = "";
|
||||
if (Number.isNaN(v)) {
|
||||
errors.set(spec_.key, `${tr(spec_.label)} · ${L("MF_NumberOnly")}`);
|
||||
field.setError(L("MF_NumberOnly"));
|
||||
} else {
|
||||
errors.delete(spec_.key);
|
||||
field.setError();
|
||||
if (v !== null && (v < spec_.min || v > spec_.max))
|
||||
warn.textContent = L("MF_Range").replace(
|
||||
"{value}",
|
||||
`${fmt(spec_.min)}~${fmt(spec_.max)}`,
|
||||
);
|
||||
if (hold) {
|
||||
if (v === null) delete hold[spec_.key];
|
||||
else hold[spec_.key] = v;
|
||||
values[HOURLY_KEY] = { ...hold };
|
||||
} else put(spec_.key, v);
|
||||
}
|
||||
mark();
|
||||
fire();
|
||||
});
|
||||
mark();
|
||||
return field.root;
|
||||
};
|
||||
|
||||
/** 고르기 칸 — 없음 + 목록 · 목록에 없는 지금 값도 한 칸(단가 없음) */
|
||||
const pickField = (key: string, label: string, list: MachinePick[]): HTMLElement => {
|
||||
const now = shown(values[key]);
|
||||
const options = [
|
||||
{ value: "", text: L("MF_None") },
|
||||
...list.map((p) => ({ value: p.value, text: p.text })),
|
||||
];
|
||||
if (now && !list.some((p) => p.value === now))
|
||||
options.push({ value: now, text: `${now} (${L("MF_NoPrice")})` });
|
||||
const field = createSelectField({ label: tr(label), options, value: now, size: 28 });
|
||||
field.select.dataset.key = key;
|
||||
field.select.disabled = Boolean(opts.readOnly);
|
||||
attachPickSearch(field.select, { placeholder: L("MF_PickFind") }); // 항목 많은 고르기(직종) = 찾기 판(801-2)
|
||||
const mark = (): void => {
|
||||
field.root.classList.toggle("is-changed", !same(values[key] ?? null, init[key] ?? null));
|
||||
};
|
||||
field.select.addEventListener("change", () => {
|
||||
put(key, field.select.value || null);
|
||||
mark();
|
||||
fire();
|
||||
});
|
||||
mark();
|
||||
return field.root;
|
||||
};
|
||||
|
||||
const textField = (
|
||||
label: string,
|
||||
get: () => string,
|
||||
set: (v: string) => void,
|
||||
was: string,
|
||||
): HTMLElement => {
|
||||
const field = createInputField({ label: tr(label), value: get(), size: 28 });
|
||||
field.input.readOnly = Boolean(opts.readOnly);
|
||||
field.input.addEventListener("input", () => {
|
||||
set(field.input.value);
|
||||
field.root.classList.toggle("is-changed", field.input.value !== was);
|
||||
opts.onChange?.();
|
||||
});
|
||||
return field.root;
|
||||
};
|
||||
|
||||
/* --- 손료계수 꼴 --- */
|
||||
const coefBox = el("div", { className: "ui-machine-form__group" });
|
||||
const coefOut = el("div", { className: "ui-machine-form__coefs" });
|
||||
const paintCoefs = (): void => {
|
||||
if (mode !== "pumsem") return;
|
||||
coefOut.replaceChildren(
|
||||
...MACHINE_COEFS.map((k) => {
|
||||
const f = createInputField({ label: tr(COEF_LABEL[k]), value: shown(values[k]), size: 28 });
|
||||
f.input.readOnly = true;
|
||||
f.input.tabIndex = -1;
|
||||
f.root.classList.toggle("is-changed", !same(values[k] ?? null, init[k] ?? null));
|
||||
return f.root;
|
||||
}),
|
||||
);
|
||||
};
|
||||
const paintCoefBox = (): void => {
|
||||
const pumsem = mode === "pumsem";
|
||||
coefBox.replaceChildren(
|
||||
el("div", {
|
||||
className: "ui-machine-form__row",
|
||||
children: pumsem ? PUMSEM.map((s) => numField(s)) : [numField(COEF)],
|
||||
}),
|
||||
...(pumsem ? [coefOut] : []),
|
||||
);
|
||||
paintCoefs();
|
||||
};
|
||||
const coefSeg = createSegment({
|
||||
options: [
|
||||
{ value: "direct", label: L("MF_CoefDirect") },
|
||||
{ value: "pumsem", label: L("MF_CoefPumsem") },
|
||||
],
|
||||
value: mode,
|
||||
size: 24,
|
||||
ariaLabel: L("MF_Coef"),
|
||||
onChange: (v) => {
|
||||
mode = v as "direct" | "pumsem";
|
||||
if (mode === "direct") {
|
||||
for (const k of [...MACHINE_INPUTS, ...MACHINE_COEFS.slice(0, 3)]) delete values[k];
|
||||
if (values["손료계수"] === undefined && init["손료계수"] !== undefined)
|
||||
values["손료계수"] = init["손료계수"];
|
||||
} else for (const k of MACHINE_INPUTS) if (init[k] !== undefined) values[k] = init[k];
|
||||
paintCoefBox();
|
||||
fire();
|
||||
},
|
||||
});
|
||||
if (opts.readOnly) coefSeg.root.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
||||
|
||||
/* --- 구성 / 시간당 값 직접 --- */
|
||||
const body = el("div", { className: "ui-machine-form__body" });
|
||||
const paintBody = (): void => {
|
||||
if (hourlyMode) {
|
||||
const hold = { ...((values[HOURLY_KEY] as MachineValues | undefined) ?? {}) };
|
||||
body.replaceChildren(
|
||||
el("div", {
|
||||
className: "ui-machine-form__row",
|
||||
children: HOURLY_COSTS.map((k) =>
|
||||
numField(n(k, HOURLY_LABEL[k], "원/hr", 0, 10_000_000), false, hold),
|
||||
),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
paintCoefBox();
|
||||
body.replaceChildren(
|
||||
numField(PRICE),
|
||||
el("div", {
|
||||
className: "ui-machine-form__group",
|
||||
children: [
|
||||
el("div", {
|
||||
className: "ui-machine-form__head",
|
||||
children: [el("strong", { text: L("MF_Coef") }), coefSeg.root],
|
||||
}),
|
||||
coefBox,
|
||||
],
|
||||
}),
|
||||
el("div", {
|
||||
className: "ui-machine-form__row",
|
||||
children: [
|
||||
pickField("주연료종류", "MF_FuelKind", opts.fuels),
|
||||
numField(FUEL),
|
||||
numField(MISC),
|
||||
],
|
||||
}),
|
||||
el("div", {
|
||||
className: "ui-machine-form__row",
|
||||
children: [pickField("조종원직종", "MF_Job", opts.jobs), numField(CREW), numField(POWER)],
|
||||
}),
|
||||
);
|
||||
};
|
||||
const kindSeg = opts.allowHourly
|
||||
? createSegment({
|
||||
options: [
|
||||
{ value: "build", label: L("MF_Build") },
|
||||
{ value: "hourly", label: L("MF_HourlyDirect") },
|
||||
],
|
||||
value: hourlyMode ? "hourly" : "build",
|
||||
size: 24,
|
||||
onChange: (v) => {
|
||||
hourlyMode = v === "hourly";
|
||||
if (hourlyMode) values[HOURLY_KEY] ??= {};
|
||||
else if (init[HOURLY_KEY] === undefined) delete values[HOURLY_KEY];
|
||||
paintBody();
|
||||
fire();
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
/* --- 비슷한 기계에서 시작 --- */
|
||||
const similarBox = (): HTMLElement[] => {
|
||||
if (!opts.findSimilar || opts.readOnly) return [];
|
||||
const find = createFindList({ placeholder: L("MF_SimilarFind") });
|
||||
const wrap = el("details", {
|
||||
className: "ui-machine-form__similar",
|
||||
children: [el("summary", { text: L("MF_Similar") }), find.root],
|
||||
});
|
||||
let seq = 0;
|
||||
let timer: number | undefined;
|
||||
const look = async (): Promise<void> => {
|
||||
const mine = ++seq;
|
||||
const got = await opts.findSimilar!(find.search.value.trim()).catch(() => []);
|
||||
if (mine !== seq) return;
|
||||
find.hint.textContent = got.length ? "" : L("MF_SimilarNone");
|
||||
find.setRows(
|
||||
got.map((m) => ({
|
||||
key: m.key,
|
||||
name: `${m.name} ${m.spec}`.trim(),
|
||||
tag: m.tag,
|
||||
sub: m.key,
|
||||
onPick: () => {
|
||||
values = { ...structuredClone(m.values) };
|
||||
name = m.name;
|
||||
spec = m.spec;
|
||||
mode = MACHINE_INPUTS.some((k) => typeof values[k] === "number") ? "pumsem" : mode;
|
||||
coefSeg.set(mode);
|
||||
hourlyMode = values[HOURLY_KEY] !== undefined && Boolean(opts.allowHourly);
|
||||
kindSeg?.set(hourlyMode ? "hourly" : "build");
|
||||
wrap.open = false;
|
||||
paintAll();
|
||||
setNotice(L("MF_SimilarCopied").replace("{value}", `${m.name} ${m.spec}`.trim()));
|
||||
opts.onSimilar?.(m);
|
||||
fire();
|
||||
},
|
||||
})),
|
||||
);
|
||||
};
|
||||
find.search.addEventListener("input", () => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => void look(), 250);
|
||||
});
|
||||
wrap.addEventListener("toggle", () => wrap.open && void look());
|
||||
return [wrap];
|
||||
};
|
||||
|
||||
/* --- 미리보기 --- */
|
||||
const paintPreview = (): void => {
|
||||
const out = (v: number, key = false): HTMLElement =>
|
||||
el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: fmt(v) });
|
||||
const line = (label: string, text: string, v: number, key = false): HTMLElement =>
|
||||
createCalcFormulaRow(label, text ? createCalcFormula(text) : null, out(v, key));
|
||||
const head = [
|
||||
el("strong", { text: L("MF_Preview") }),
|
||||
createText(L("MF_PreviewNote"), "muted"),
|
||||
];
|
||||
if (hourlyMode) {
|
||||
const h = (values[HOURLY_KEY] as Record<string, number> | undefined) ?? {};
|
||||
const [a, b, c] = HOURLY_COSTS.map((k) => Number(h[k]) || 0);
|
||||
side.replaceChildren(
|
||||
...head,
|
||||
line(L("MF_Labor"), "", a),
|
||||
line(L("MF_Material"), "", b),
|
||||
line(L("MF_Expense"), "", c),
|
||||
line(L("MF_Total"), "", a + b + c, true),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const fuel = opts.fuels.find((f) => f.value === values["주연료종류"])?.price ?? 0;
|
||||
const wage = opts.jobs.find((j) => j.value === String(values["조종원직종"] ?? ""))?.price ?? 0;
|
||||
const hour = opts.hour ?? 1 / 8;
|
||||
const p = machineHourly(values, fuel, wage, opts.power ?? 0, hour);
|
||||
const num = (k: string): string => fmt(Number(values[k]) || 0);
|
||||
const rows = [
|
||||
line(L("MF_Loss"), `${num("가격")}천원 × 1,000 × ${num("손료계수")} × 10^-7^`, p.손료),
|
||||
line(L("MF_Fuel"), `${num("주연료")}ℓ × ${fmt(fuel)}원`, p.주연료),
|
||||
...(values["전력"]
|
||||
? [line(L("MF_Power"), `${num("전력")}kWh × ${fmt(opts.power ?? 0)}원`, p.전력)]
|
||||
: []),
|
||||
line(L("MF_Misc"), `${fmt(p.주연료 + p.전력)} × ${num("잡재료율")}%`, p.잡품),
|
||||
line(L("MF_Crew"), `${num("조종원")}인 × ${fmt(wage)}원 × ${fmt(hour)}`, p.조종원),
|
||||
];
|
||||
side.replaceChildren(
|
||||
...head,
|
||||
...rows,
|
||||
el("hr", { className: "ui-machine-form__rule" }),
|
||||
line(L("MF_Labor"), "", p.노무비),
|
||||
line(L("MF_Material"), "", p.재료비),
|
||||
line(L("MF_Expense"), "", p.경비),
|
||||
line(L("MF_Total"), "", p.계, true),
|
||||
);
|
||||
};
|
||||
|
||||
const setNotice = (text: string): void => {
|
||||
notice.replaceChildren(...(text ? [createText(text, "warning")] : []));
|
||||
};
|
||||
|
||||
const top = el("div", { className: "ui-machine-form__row" });
|
||||
const paintAll = (): void => {
|
||||
errors.clear();
|
||||
syncCoefs();
|
||||
top.replaceChildren(
|
||||
textField(
|
||||
"MF_Name",
|
||||
() => name,
|
||||
(v) => (name = v),
|
||||
startName,
|
||||
),
|
||||
textField(
|
||||
"MF_Spec",
|
||||
() => spec,
|
||||
(v) => (spec = v),
|
||||
startSpec,
|
||||
),
|
||||
);
|
||||
paintBody();
|
||||
paintPreview();
|
||||
};
|
||||
fields.append(...similarBox(), notice, top, ...(kindSeg ? [kindSeg.root] : []), body);
|
||||
root.append(fields, side);
|
||||
root.classList.toggle("is-readonly", Boolean(opts.readOnly));
|
||||
paintAll();
|
||||
|
||||
return {
|
||||
root,
|
||||
state: () => ({
|
||||
name: name.trim(),
|
||||
spec: spec.trim(),
|
||||
// 칸 차례 = 처음 값 묶음 차례(방식을 바꿨다 되돌려도 같은 줄) · 새 칸은 뒤에
|
||||
values: structuredClone(
|
||||
Object.fromEntries(
|
||||
[...Object.keys(init), ...Object.keys(values)]
|
||||
.filter((k, i, all) => all.indexOf(k) === i && k in values)
|
||||
.map((k) => [k, values[k]]),
|
||||
),
|
||||
),
|
||||
errors: [...errors.values()],
|
||||
changed: changed(),
|
||||
mode,
|
||||
hourly: hourlyMode,
|
||||
}),
|
||||
setNotice,
|
||||
};
|
||||
}
|
||||
@@ -54,7 +54,6 @@ import { createRangeBar } from "./ui_template_range_bar";
|
||||
import { createStat, statGrid } from "./ui_template_stat";
|
||||
import { toastNode } from "./ui_template_toast";
|
||||
import { FRAME_DEMOS } from "./ui_template_ui_kit_frames";
|
||||
import { createMachineForm } from "./ui_template_machine_form";
|
||||
|
||||
/** 상태 줄 다섯 칸 */
|
||||
export const KIT_STATES = ["default", "hover", "active", "disabled", "error"] as const;
|
||||
@@ -322,22 +321,6 @@ function tabsDemo(s: KitState): HTMLElement | null {
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 기계 양식 견본 — 굴착기 1.0㎥ 값(821-2) */
|
||||
const kitMachine = (readOnly: boolean): HTMLElement =>
|
||||
createMachineForm({
|
||||
name: "굴착기(무한궤도)",
|
||||
spec: "1.0㎥",
|
||||
values: {
|
||||
...{ 가격: 142069, 내용시간: 10000, 연간표준가동시간: 1250, 상각비율: 0.9, 정비비율: 0.7 },
|
||||
...{ 연간관리비율: 0.1, 상각비계수: 900, 정비비계수: 700, 관리비계수: 485, 손료계수: 2085 },
|
||||
...{ 주연료: 19.5, 주연료종류: "경유", 잡재료율: 22, 조종원: 1, 조종원직종: "1048" },
|
||||
},
|
||||
fuels: [{ value: "경유", text: "경유", price: 1846.39 }],
|
||||
jobs: [{ value: "1048", text: "건설기계운전사", price: 283297 }],
|
||||
readOnly,
|
||||
findSimilar: async () => [],
|
||||
}).root;
|
||||
|
||||
// prettier-ignore
|
||||
export const KIT_PARTS: KitPart[] = [
|
||||
// ── 552-2 기본 ─────────────────────────────────────────────
|
||||
@@ -403,8 +386,6 @@ export const KIT_PARTS: KitPart[] = [
|
||||
demo: (s) => (s === "default" ? col(createKv([{ label: "연장", value: "1,080 m", big: true }, { label: "종단 경사", value: "12 %", ok: true }, { label: "최소 곡선반경", value: "12 m", ok: false }, { label: "비고", value: null }]), createKv([{ label: kitKeys("Ctrl", "Z"), value: "되돌리기" }, { label: kitKeys("휠 끌기"), value: "화면 옮기기" }, { label: kitKeys("Esc") }], { rule: true })) : null) },
|
||||
{ key: "calc-row", name: "계산 줄", group: "compound", kinds: "값 줄(이름 · 값 112 · 덧칸) · 식 줄(이름 · 식 · 값) · 시트 줄(이름 · 식 · 값 · 꼬리) · 위 · 아래첨자 · 중요 결과 굵게", file: "ui_template_calc_row.ts",
|
||||
demo: (s) => (s === "default" ? kitCalc() : null) },
|
||||
{ key: "machine-form", name: "기계 양식", group: "compound", kinds: "이름 · 규격 · 가격 · 손료계수(바로 넣기 · 품셈 방식) · 주연료 · 잡품 · 조종원 · 전력 · 시간당 값 직접 · 옆 시간당 계산식 · 비슷한 기계에서 시작 · 고친 칸 · 숫자만", file: "ui_template_machine_form.ts",
|
||||
demo: (s) => (s === "default" || s === "disabled" ? kitMachine(s === "disabled") : null) },
|
||||
{ key: "equation", name: "수식", group: "compound", kinds: "분수(가로 줄 안 세로 쌓기) · 위 · 아래첨자 · × ÷ · 괄호 · % · 조건 칩 · 하위 로직 블록(접힘) · 한 줄 = 결과명 = 이름 식 = 값 식 = 결과 + 변수 풀이", file: "ui_template_equation.ts",
|
||||
demo: (s) => (s === "default" ? kitEquation() : null) },
|
||||
{ key: "hopyo-grid", name: "일위대가 격자", group: "compound", kinds: "호표 머리(이름 · 규격 · 단위 · 1 단위 합계) · 구성 줄(품명 · 규격 · 수량 · 단위 · 비목 단가 / 금액 · 비고) · 줄 꼬리표 품셈 · 고정 · 합계 줄 빈 줄 붉게", file: "ui_template_hopyo_grid.ts", demo: (s) => (s === "default" ? createHopyoGrid([{ key: "D", no: 17, name: "돌기슭막이 H=2.0", unit: "m", total: { sum: 121748.6, labor: 94447.5, material: 0, expense: 27301.1 }, lines: [{ name: "돌쌓기 찰쌓기", spec: "뒷길이 35cm", qty: 2.09, unit: "㎡", prices: { sum: 52995, labor: 41250, expense: 11745 }, amounts: { sum: 110759.6, labor: 86212.5, expense: 24547.1 }, note: "산근 1호표", tag: "품셈" }, { name: "야면석 채집 운반", spec: "계곡 내", qty: 0.45, unit: "㎥", prices: { sum: 24420, labor: 18300, expense: 6120 }, amounts: { sum: 10989, labor: 8235, expense: 2754 }, tag: "고정" }, { name: "잡재료", spec: "노무비의", qty: null, unit: "%", prices: {}, amounts: {}, note: "수량 없음", tag: "품셈" }] }]).root : null) },
|
||||
|
||||
Reference in New Issue
Block a user