feat(842-3): 기계 보기 · 제어 구역 인력 · 잡품 줄 여럿 — 기계 줄 목록 ↔ 덮음 · 기본 정보 조종원 · 잡품 % 칸 걷음
- 제어 구역 기계 표 꼴(sheet) 한 줄 막음 걷음 — 인력 더하기 · 빼기 · 직종 고르기 · 잡품 재료 고르기 · 수량 · 율 줄마다 - coverFrom · applyCover = 줄 인력 · 잡품 목록 ↔ 덮음 조종원 · 잡품 통째 · bindDoc · bind = 목록 → 조종원 수 · 직종 · 잡재료율 - 기본값 바탕 = 기계 줄 목록(lists 고리 · 단가산출 안 = 연결 기록 · 기계 보기 = 처음 줄) - M01 기계 목록 조종원 열 → 인력 줄 요약 · 기호 이름 = 인력N · 잡품N 줄마다 - 시험 test_842_3 새로(두 길 · 손셈 · 거울 · 저장 꼴) · test_840_2 ③ = 목록 꼴로 고침(옛 조종원 수 · 잡재료율 칸 기대 낡음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,8 @@ export interface EditHooks {
|
||||
machine?: MachineCtx;
|
||||
/** 836-1 기계 제어 구역 — 덮음 없이 그 기계 식 셈(원본 = 기본값) */
|
||||
calc?: (doc: FormulaDoc, given: Record<string, unknown>) => DocAnswer;
|
||||
/** 842-3 그 기계 줄 인력 · 잡품 목록(기본값 바탕) */
|
||||
lists?: (given: Record<string, unknown>) => Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const field = (
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* ㉡ 조종원 = 인력 줄(노임 고르기 · 인/일 · 더하기 · 빼기 · 기본 = 기계 값 조종원 · 직종)
|
||||
* ㉢ 잡품 = 재료 줄(재료 · 유가 고르기 · 수량 · 율 % · 기본 = 밑 재료 줄 · 품셈 잡재료율)
|
||||
* 고침 = 문서 칸 `기계덮음`(캐시 · [저장] 때 서버가 정본으로 다시 셈) · 원본과 같으면 지움(`putCover`)
|
||||
* 기본값 = 덮음 없이 셈한 그 기계 식 답(`defaultsOf`) · 읽기(M01 기초) = 칸 잠김 · 단추 없음
|
||||
* 기계 표 꼴(`sheet` · 840-2 기계 보기 `line_stack_machine_sheet`) = 조종원 한 줄까지 · 잡품 = 밑 줄 고정(율 하나) ·
|
||||
* 기본값 = 덮음 없이 셈한 그 기계 식 답 + 기계 줄 인력 · 잡품 목록(`defaultsOf` · `rowEntry` · 842-3) ·
|
||||
* 읽기(M01 기초) = 칸 잠김 · 단추 없음
|
||||
* 기계 보기(840-2 `line_stack_machine_sheet` · M01 기계 상세 · 내 기계) = 같은 구역(인력 · 잡품 여러 줄 · 842-3) ·
|
||||
* `lock` = 잠긴 값 칸(품셈 방식 계수 등)
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
machineKey,
|
||||
partsOf,
|
||||
putCover,
|
||||
rowEntry,
|
||||
type CrewRow,
|
||||
type MachineCover,
|
||||
type MiscRow,
|
||||
@@ -44,8 +46,8 @@ export interface CtlHooks {
|
||||
name: (key: string) => string;
|
||||
/** 읽기(M01 기초 · 칸 잠김) */
|
||||
readOnly: boolean;
|
||||
/** 기계 표 꼴(840-2) — 기계 줄 칸에 담기는 만큼만(조종원 한 줄 · 잡품 율 하나 · 재료 고르기 없음) */
|
||||
sheet?: boolean;
|
||||
/** 그 기계 줄 값(인력 · 잡품 목록 — 기본값 바탕 · 842-3) */
|
||||
lists?: (given: Record<string, unknown>) => Record<string, unknown> | null;
|
||||
/** 잠긴 값 칸 */
|
||||
lock?: string[];
|
||||
}
|
||||
@@ -72,7 +74,8 @@ export function ctlOf(h: CtlHooks, link: Link | undefined) {
|
||||
if (!machine || !sub) return null;
|
||||
const orig = h.calc(sub, given);
|
||||
const vals = (orig.값 ?? {}) as Record<string, unknown>;
|
||||
return { machine, sub, vals, base: defaultsOf(sub, vals), entry: coverOf(h.doc)[machine] };
|
||||
const base = defaultsOf(sub, vals, rowEntry(sub, h.lists?.(given) ?? null));
|
||||
return { machine, sub, vals, base, entry: coverOf(h.doc)[machine] };
|
||||
}
|
||||
|
||||
const num = (t: string): number | null => {
|
||||
@@ -122,7 +125,6 @@ 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.금액 ?? [];
|
||||
@@ -238,22 +240,15 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
lineRow(
|
||||
lt("K_재료"),
|
||||
miscName(r),
|
||||
ro || table,
|
||||
ro,
|
||||
() => pickMat((p) => setMisc(swapAt(misc, i, asMat(r, p))), r.키 ? miscName(r) : ""),
|
||||
[
|
||||
...(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
|
||||
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 })),
|
||||
),
|
||||
],
|
||||
"재료비",
|
||||
@@ -287,21 +282,14 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
head(
|
||||
lt("Ctl_Crew"),
|
||||
entry.조종원 !== undefined,
|
||||
table && crew.length
|
||||
? null
|
||||
: () =>
|
||||
openStackPick("인력", (p) => setCrew([...crew, { 키: p.키, 이름: p.이름, 인: 1 }])),
|
||||
() => 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,
|
||||
!table
|
||||
? () => pickMat((p) => setMisc([...misc, asMat({ 율: 100 }, p)]), "")
|
||||
: parts.base.length && !misc.length
|
||||
? () => setMisc(parts.base.map((k) => ({ 줄: String(lines[k].이름 ?? ""), 율: 0 })))
|
||||
: null,
|
||||
() => pickMat((p) => setMisc([...misc, asMat({ 율: 100 }, p)]), ""),
|
||||
() => put({ ...entry, 잡품: undefined }),
|
||||
),
|
||||
...(miscRows.length ? miscRows : [el("div", { className: "lstk-empty", text: lt("Ctl_None") })]), // prettier-ignore
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* line_stack_machine_sheet.ts
|
||||
* 기계 보기(PLAN 840-2 · 요구 853 · 862 「기계 보기 공용」) — 기계 한 대를 단가산출 수식 보기와 닮은 꼴로:
|
||||
* 왼쪽 = 기계 제어 구역(836-1 `line_stack_machine_ctl` 그대로 · 기계 표 꼴 `sheet` — 값 칸 · 조종원 인력 줄 · 잡품 줄) +
|
||||
* 왼쪽 = 기계 제어 구역(836-1 `line_stack_machine_ctl` 그대로 — 값 칸 · 인력 줄 · 잡품 재료 줄 여럿 · 842-3) +
|
||||
* 「기본 정보 · 손료 근거」 접이 카드(이름 · 규격 · 손료 방식 · 품셈 비율 · 식이 안 읽는 값 칸) + 비슷한 기계에서 시작(새 기계)
|
||||
* 오른쪽 = 그 기계 시간당 경비 산출근거(`line_stack_out` · 노 · 재 · 경 · 계 합계 상자) — 값을 바꾸면 브라우저 계산기로 즉시
|
||||
* 서버(`POST /api/m02/machine-sheet` · `M02_MyMachine_Sheet`) = 식 고름 + 처음 줄 값을 박은 식 문서 + 연결 기록 ·
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
defaultsOf,
|
||||
partsOf,
|
||||
putCover,
|
||||
rowEntry,
|
||||
type CoverParts,
|
||||
type MachineCover,
|
||||
} from "../common_util/common_util_formula_doc_cover";
|
||||
@@ -36,12 +37,13 @@ import {
|
||||
MACHINE_INPUTS,
|
||||
machineCoefOf,
|
||||
} from "../common_util/common_util_machine_coef";
|
||||
import { crewView } from "../common_util/common_util_machine_crew";
|
||||
import {
|
||||
applyCover,
|
||||
bindDoc,
|
||||
coverFrom,
|
||||
crewKey,
|
||||
engineVals,
|
||||
jobCode,
|
||||
modeOf,
|
||||
ownedCols,
|
||||
rangeWarnings,
|
||||
@@ -105,23 +107,13 @@ interface SheetInfo {
|
||||
}
|
||||
|
||||
/** 늘 보이는 기계 줄 칸(값이 없어도) — 그 밖 칸은 줄에 있으면 */
|
||||
const BASE_KEYS = [
|
||||
"가격",
|
||||
"손료계수",
|
||||
"주연료",
|
||||
"주연료종류",
|
||||
"잡재료율",
|
||||
"조종원",
|
||||
"조종원직종",
|
||||
"전력",
|
||||
];
|
||||
const BASE_KEYS = ["가격", "손료계수", "주연료", "주연료종류", "전력"];
|
||||
const UNIT: Record<string, string> = {
|
||||
가격: "천원", 손료계수: "10⁻⁷/hr", 내용시간: "시간", 연간표준가동시간: "시간", 주연료: "ℓ/hr",
|
||||
잡재료율: "%", 조종원: "인", 전력: "kWh",
|
||||
가격: "천원", 손료계수: "10⁻⁷/hr", 내용시간: "시간", 연간표준가동시간: "시간", 주연료: "ℓ/hr", 전력: "kWh",
|
||||
}; // prettier-ignore
|
||||
const PICKS = ["주연료종류", "조종원직종"];
|
||||
const PICKS = ["주연료종류"];
|
||||
/** 식이 바뀔 수 있는 칸(서버 식 고름) */
|
||||
const PICKY = ["주연료", "조종원", "조종원직종", "전력", "손료계수", ...MACHINE_COEFS];
|
||||
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));
|
||||
@@ -175,8 +167,9 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
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);
|
||||
const lists = engineVals(orig, mine);
|
||||
base = defaultsOf(d, (plain(d, given).값 ?? {}) as Record<string, unknown>, rowEntry(d, lists));
|
||||
putCover(holder, no, coverFrom(parts, orig, cur, mine), base);
|
||||
};
|
||||
|
||||
// ── 서버(뒤에서) ──
|
||||
@@ -268,7 +261,7 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
fn(holder);
|
||||
const e = entry();
|
||||
if (e.조종원) {
|
||||
e.조종원 = e.조종원.map((r) => ({ ...r, 키: `LB:${jobCode(r.키, jobs)}` }));
|
||||
e.조종원 = e.조종원.map((r) => ({ ...r, 키: crewKey(r.키, jobs) }));
|
||||
putCover(holder, no, e, base);
|
||||
}
|
||||
if (parts) cur = applyCover(parts, orig, cur, entry(), mine, jobs);
|
||||
@@ -276,7 +269,7 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
},
|
||||
name: (k) => cache.names.get(k) ?? "",
|
||||
readOnly: ro,
|
||||
sheet: true,
|
||||
lists: () => engineVals(orig, mine),
|
||||
lock,
|
||||
},
|
||||
{ ...link, 키: info.키 },
|
||||
@@ -504,7 +497,7 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
const why = info.까닭 ? ` — ${info.까닭}` : "";
|
||||
return void right.replaceChildren(head, createText(`${L("MS_NoFormula")}${why}`, "warning"));
|
||||
}
|
||||
const shaped = coverDoc(d, given, coverOf(holder));
|
||||
const shaped = coverDoc(d, given, coverOf(holder), rowEntry(d, engineVals(orig, mine)));
|
||||
const a = plain(shaped, given);
|
||||
if (cache.takeMissing()) ask();
|
||||
lastAnswer = a;
|
||||
@@ -526,7 +519,7 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
foot.replaceChildren(docFoot(a));
|
||||
};
|
||||
const drawWarn = (): void => {
|
||||
const vals: MachineValues = { ...engineVals(cur, mine), ...(cur.값 ?? {}) };
|
||||
const vals: MachineValues = crewView({ ...engineVals(cur, mine), ...(cur.값 ?? {}) });
|
||||
if (unitOf("손료계수") !== UNIT.손료계수) delete vals.손료계수; // 일 단위 손료(임업) = 시간당 범위 아님
|
||||
warn.replaceChildren(
|
||||
...rangeWarnings(vals).map(([k, r]) =>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { blankDoc, type DocVar, type FormulaDoc } from "../common_util/common_util_formula_doc";
|
||||
import { calcDoc, type DocAnswer } from "../common_util/common_util_formula_doc_calc";
|
||||
import { listsOf } from "../common_util/common_util_formula_doc_cover";
|
||||
import { importView, type Basis } from "../common_util/common_util_formula_doc_import";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import type { DocTable } from "../common_util/common_util_formula_doc_table";
|
||||
@@ -375,6 +376,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
take: takeCtx(),
|
||||
machine: machineCtx(),
|
||||
calc: calcSub,
|
||||
lists: (g) => listsOf((q) => cache.links.element(q), g),
|
||||
before: saved ? lineMap(JSON.parse(saved) as StackDoc) : undefined,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Machine.ts
|
||||
* M01 기계 목록(PLAN 821-2 · 요구 790-2) — 표는 목록용 열만(이름 · 규격 · 구분 · 가격 · 손료계수 ·
|
||||
* 주연료 · 조종원 · 시간당 경비(서버 값) · 별칭 · 비고) · 줄을 누르면 상세 페이지 · [새 기계] = 양식.
|
||||
* 주연료 · 인력 줄 요약 · 시간당 경비(서버 값) · 별칭 · 비고) · 줄을 누르면 상세 페이지 · [새 기계] = 양식.
|
||||
* 단위 · 출처 열 없음 · 고친 것 · 새 줄 · 지울 줄은 초안(노랑 · 새 · 지움 표시) — 저장은 페이지 [저장].
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { guessRows, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
|
||||
import { code, crewOf } from "../common_util/common_util_machine_crew";
|
||||
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
|
||||
import { onDraftChange, peek, same } from "./M01_MasterData_Draft";
|
||||
import { fetchMachineHourly } from "./M01_MasterData_Machine_Api";
|
||||
@@ -32,9 +33,11 @@ const COLS: [string, (r: Row) => string][] = [
|
||||
val(r)["주연료"] ? `${val(r)["주연료종류"] ?? ""} ${num(val(r)["주연료"])}ℓ`.trim() : "",
|
||||
],
|
||||
[
|
||||
"조종원",
|
||||
"인력",
|
||||
(r) =>
|
||||
val(r)["조종원"] ? `${num(val(r)["조종원"])}인 ${val(r)["조종원직종"] ?? ""}`.trim() : "",
|
||||
(crewOf(val(r)) ?? []) // 인력 줄 요약(842-3)
|
||||
.map((x) => `${num(x.인)}인 ${code(x.키) || "직종 고르기"}`)
|
||||
.join(", "),
|
||||
],
|
||||
];
|
||||
const val = (r: Row): Record<string, unknown> => (r["값"] ?? {}) as Record<string, unknown>;
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
836-1) 얹음 → 값 박음(`bind` — 남은 요소 `EQ:{S_기계}` 칸 → 숫자 · 저장 전 새 기계 · 내 기계도 같은 계산기로 셈)
|
||||
- 파이썬 · TS 짝 — `bind` ↔ `common_util/common_util_machine_sheet.ts` `bindDoc` · 거울 시험 test_840_2_machine_sheet.py
|
||||
- 연결 = 덮음 얹은 문서 셈이 쓴 요소 · 표 · 문서(`master_doc_record`) — 화면이 같은 계산기로 즉시 다시 셈
|
||||
- 직종 = 노임 요소 코드 → 인력 원문번호(조종원 줄 → 기계 줄 `조종원직종`)
|
||||
- 바탕 = 처음 줄 인력 · 잡품 목록(842-3 · `master_doc_cover.row_entry`) — 덮음(지금 줄 목록)이 그 위에 · 값 박음은
|
||||
목록 → 조종원 수 · 직종 · 잡재료율(`master_machine_crew.view`)
|
||||
- 직종 = 노임 요소 코드 → 인력 원문번호(인력 줄 키 `LB:원문번호`)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,8 +29,11 @@ def _plain(v: Any) -> Any:
|
||||
|
||||
|
||||
def bind(doc: dict, vals: dict) -> dict:
|
||||
"""식 문서 → 기계 값 칸을 숫자로 박은 새 문서(그 칸이 없는 줄은 요소 그대로)."""
|
||||
"""식 문서 → 기계 값 칸을 숫자로 박은 새 문서(그 칸이 없는 줄은 요소 그대로 · 목록 = `view`)."""
|
||||
import master_machine_crew as mcrew
|
||||
|
||||
out = copy.deepcopy(doc)
|
||||
vals = mcrew.view(vals)
|
||||
for v in out.get("변수") or []:
|
||||
link = v.get("연결") or {}
|
||||
key = str(link.get("키") or "")
|
||||
@@ -77,7 +82,8 @@ def sheet(row: Any, base: Any = None, cover: Any = None, mine: bool = False) ->
|
||||
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
|
||||
cover = mcv.clean({no: m01._dec(entry)}) if entry else None
|
||||
covered = mcv.cover_doc(doc, given, cover, mcv.row_entry(doc, vals))
|
||||
except ValueError as e:
|
||||
raise m01.StoreError(400, str(e)) from None
|
||||
shaped = bind(covered, vals)
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
* common_util_machine_sheet.ts
|
||||
* 기계 보기 셈 꼴(PLAN 840-2 · 요구 853 · 862) — 기계 줄(저장 꼴) ↔ 식이 읽는 값 · 덮음(836-1 `기계덮음` 꼴) ↔ 줄
|
||||
* 셈 꼴(`engineVals`) = M01 = 값 그대로 · 내 기계 = 바로 → 상각비계수 ← 손료계수 · 정비 · 관리 0 ·
|
||||
* 직접 → 노무비 · 재료비 · 경비 · 빈 연료 · 조종원 칸 = 0(서버 `M02_MyMachine.engine` 과 같은 꼴)
|
||||
* 직접 → 노무비 · 재료비 · 경비 · 빈 연료 = 0 · 인력 · 잡품 = 빈 목록 · 옛 꼴 = 목록으로(서버 `M02_MyMachine.engine` 과 같은 꼴)
|
||||
* 덮음 ← 줄 차이(`coverFrom` — 처음 줄 대 지금 줄) · 지금 줄 ← 덮음(`applyCover` — 제어 구역이 맡은 칸만 ·
|
||||
* 덮음이 없는 칸 = 처음 줄 값 그대로 = 안 고치고 닫으면 줄 바이트 같음)
|
||||
* 제어 구역이 맡은 칸(`ownedCols`) = 식이 읽는 값 칸 + 조종원 · 조종원직종 + (잡품 줄 있는 식) 잡재료율
|
||||
* 값 박음(`bindDoc`) = 식 문서 요소 `EQ:{S_기계}` 칸 → 숫자(처음 줄 값) — 셈 차례 = 덮음 얹음(`coverDoc`) → 값 박음
|
||||
* 제어 구역이 맡은 칸(`ownedCols`) = 식이 읽는 값 칸 + 인력 · 잡품 목록(842-3 — 덮음 조종원 · 잡품 = 줄 목록 그대로)
|
||||
* 값 박음(`bindDoc`) = 식 문서 요소 `EQ:{S_기계}` 칸 → 숫자(처음 줄 값 · 목록 → 조종원 수 · 직종 · 잡재료율 `crewView`) —
|
||||
* 셈 차례 = 덮음 얹음(`coverDoc` · 바탕 = 처음 줄 목록 `rowEntry`) → 값 박음
|
||||
* 파이썬 · 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 type { FormulaDoc } from "./common_util_formula_doc";
|
||||
import type { CoverParts, CrewRow, MachineCover, MiscRow } from "./common_util_formula_doc_cover";
|
||||
import { MACHINE_INPUTS } from "./common_util_machine_coef";
|
||||
import { CREW, MISC, crewView, fromOld } from "./common_util_machine_crew";
|
||||
|
||||
export type MachineValues = Record<string, unknown>;
|
||||
/** 고르기 목록 한 칸 — 연료(값 = 종류) · 직종(값 = 인력 원문번호) */
|
||||
@@ -34,8 +36,6 @@ export interface MachineRow {
|
||||
}
|
||||
|
||||
export const HOURLY = ["노무비", "재료비", "경비"] as const;
|
||||
const CREW = ["조종원", "조종원직종"];
|
||||
const MISC = "잡재료율";
|
||||
|
||||
/** 보통 범위(옛 기계 양식과 같음) — 밖이면 경고만 */
|
||||
export const RANGE: Record<string, [number, number]> = {
|
||||
@@ -48,8 +48,9 @@ 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 {
|
||||
export function bindDoc(doc: FormulaDoc, raw: MachineValues): FormulaDoc {
|
||||
const out = clone(doc);
|
||||
const vals = crewView(raw);
|
||||
for (const v of out.변수 ?? []) {
|
||||
const l = v.연결 ?? {};
|
||||
const key = String(l.키 ?? "");
|
||||
@@ -69,6 +70,7 @@ export function bindDoc(doc: FormulaDoc, vals: MachineValues): FormulaDoc {
|
||||
/** 같은 값 — 수는 작은 차이 무시 · 글은 글로 */
|
||||
export function sameVal(a: unknown, b: unknown): boolean {
|
||||
if (blank(a) && blank(b)) return true;
|
||||
if (Array.isArray(a) || Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b);
|
||||
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 ?? "");
|
||||
@@ -87,7 +89,7 @@ export function engineVals(row: MachineRow, mine: boolean): MachineValues {
|
||||
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 };
|
||||
return { 주연료: 0, 주연료종류: "", [CREW]: [], [MISC]: [], ...fromOld(v) };
|
||||
}
|
||||
|
||||
/** 셈 꼴 칸 → 저장 꼴 자리 · null = 저장 안 함(내 기계 바로의 정비 · 관리 = 늘 0) */
|
||||
@@ -106,14 +108,15 @@ export function slotOf(
|
||||
/** 제어 구역이 맡은 칸(셈 꼴 이름) */
|
||||
export const ownedCols = (parts: CoverParts): string[] => [
|
||||
...parts.fields.map((f) => f.칸),
|
||||
...CREW,
|
||||
...(parts.misc !== null ? [MISC] : []),
|
||||
CREW,
|
||||
MISC,
|
||||
];
|
||||
|
||||
/** 처음 줄 대 지금 줄 → 덮음(조종원 = 「LB:직종」 한 줄 · 잡품 = 밑 줄마다 같은 율) — 정리는 `putCover` 몫 */
|
||||
const listText = (v: unknown): string => JSON.stringify(v ?? null);
|
||||
|
||||
/** 처음 줄 대 지금 줄 → 덮음(값 = 고친 칸 · 조종원 · 잡품 = 지금 줄 목록 통째) — 정리는 `putCover` 몫 */
|
||||
export function coverFrom(
|
||||
parts: CoverParts,
|
||||
lines: MoneyLine[],
|
||||
orig: MachineRow,
|
||||
cur: MachineRow,
|
||||
mine: boolean,
|
||||
@@ -125,20 +128,17 @@ export function coverFrom(
|
||||
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 })) : [];
|
||||
}
|
||||
if (listText(c[CREW]) !== listText(o[CREW])) out.조종원 = clone((c[CREW] ?? []) as CrewRow[]);
|
||||
if (listText(c[MISC]) !== listText(o[MISC])) out.잡품 = clone((c[MISC] ?? []) as MiscRow[]);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 노임 요소 키 → 인력 원문번호(`LB:1048` → 1048 · `LB000048` → 직종 표) */
|
||||
/** 노임 요소 키 → 인력 원문번호(`LB:1048` → 1048 · `LB000048` → 직종 표 · 빈 글 = 직종 고르기) */
|
||||
export const jobCode = (key: string, jobs: Record<string, string>): string =>
|
||||
key.startsWith("LB:") ? key.slice(3) : (jobs[key] ?? key);
|
||||
/** 인력 줄 키 = `LB:원문번호`(빈 키 그대로) */
|
||||
export const crewKey = (key: string, jobs: Record<string, string>): string =>
|
||||
key ? `LB:${jobCode(key, jobs)}` : "";
|
||||
|
||||
/** 덮음 → 지금 줄(제어 구역이 맡은 칸만 · 덮음 없는 칸 = 처음 줄 저장 값으로) — 새 줄 */
|
||||
export function applyCover(
|
||||
@@ -163,14 +163,11 @@ export function applyCover(
|
||||
} 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));
|
||||
put(
|
||||
CREW,
|
||||
entry.조종원?.map((r) => ({ ...r, 키: crewKey(r.키, jobs) })),
|
||||
);
|
||||
put(MISC, entry.잡품 && clone(entry.잡품));
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* 화면 글만 바꿈 — 문서 자료 · 식 글 · 열쇠 · 계산 불변 · 식 글 고침 칸은 `tableAlias` 길이 이름 ↔ 기호로 오감
|
||||
* 기계경비식 밖 문서 · 사용자가 손으로 바꾼 기호(사전에 없는 꼴)는 그대로 · 이름이 겹치면 그 기호만 그대로
|
||||
* 화면 없음(시험이 Node 로 돌림) · 기계 제어 구역(836-1)도 `machineAlias` 로 같은 이름
|
||||
* 인력 · 잡품 줄 여럿(842-3 — 덮음이 더한 `w_조종원N` · `P_잡품N`) = 줄마다 「인력N」 · 「잡품N」
|
||||
* ========================================================================== */
|
||||
|
||||
import type { FormulaDoc } from "./common_util_formula_doc";
|
||||
@@ -88,6 +89,8 @@ const CELL: Record<string, string> = {
|
||||
전력: "전력",
|
||||
조종원: "조종원",
|
||||
잡재료율: "잡품",
|
||||
인력: "인력",
|
||||
잡품: "잡품",
|
||||
노무비: "시간노무",
|
||||
재료비: "시간재료",
|
||||
경비: "시간경비",
|
||||
@@ -105,8 +108,14 @@ export function machineAlias(doc: FormulaDoc): Record<string, string> {
|
||||
if (doc.부품?.갈래 !== "기계경비식") return {};
|
||||
const ids = symbols(doc);
|
||||
const slot = SLOTS[ids.filter((x) => x.startsWith("P_EQ기계")).length] ?? [];
|
||||
const row = (id: string): string | undefined => {
|
||||
const m = /^(?:w_조종원|P_잡품)(\d+)$/.exec(id); // 842-3 인력 · 잡품 줄마다
|
||||
return m ? `${id.startsWith("w_") ? "인력" : "잡품"}${m[1]}` : undefined;
|
||||
};
|
||||
const want = (id: string): string | undefined =>
|
||||
NAME[id] ?? (id.startsWith("P_EQ기계") ? slot[Number(id.slice(6) || 1) - 1] : undefined);
|
||||
NAME[id] ??
|
||||
row(id) ??
|
||||
(id.startsWith("P_EQ기계") ? slot[Number(id.slice(6) || 1) - 1] : undefined);
|
||||
const count = new Map<string, number>();
|
||||
const plain = new Set(ids);
|
||||
for (const id of ids) {
|
||||
|
||||
@@ -27,7 +27,7 @@ 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 made = sheet.coverFrom(parts, 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.박음) } : {}) };
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* 842-3 기계 인력 · 잡품 줄 헬퍼 — common_util_machine_crew.ts · common_util_formula_doc_cover.ts 를 트랜스파일해 Node 로 돌림.
|
||||
* stdin = {값들: [기계 값], 문서들: {키: 식 문서}, 대상: [{식, 값, 덮음?, 기계?}]}
|
||||
* stdout = {읽기: [crewView], 옛: [fromOld], 바탕: [rowEntry], 얹음: [coverDoc(식, {기계}, 덮음, rowEntry)]} */
|
||||
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 crew = load(path.join(root, "common_util_machine_crew.ts"));
|
||||
const cover = load(path.join(root, "common_util_formula_doc_cover.ts"));
|
||||
|
||||
const ask = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const out = {
|
||||
읽기: ask.값들.map((v) => crew.crewView(v)),
|
||||
옛: ask.값들.map((v) => crew.fromOld(v)),
|
||||
바탕: ask.대상.map((t) => cover.rowEntry(ask.문서들[t.식], t.값)),
|
||||
얹음: ask.대상.map((t) => {
|
||||
const doc = ask.문서들[t.식];
|
||||
const given = { 기계: t.기계 ?? "M" };
|
||||
return cover.coverDoc(doc, given, t.덮음 ? { [given.기계]: t.덮음 } : null, cover.rowEntry(doc, t.값));
|
||||
}),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -4,7 +4,8 @@
|
||||
(+ 덮음 ↔ 줄 `coverFrom` · `applyCover` — 화면 몫 · Node 로 돌림).
|
||||
① M01 기계 655 대 전수 — 식 고름 → 값 박은 식 셈 = 기계 표 요소로 읽는 셈(노 · 재 · 경 · 계 같음) · 식 없음 = 까닭.
|
||||
② 655 대 전수(TS) — 고침 없음 = 덮음 없음 · 줄 JSON 같음(열고 닫아도 M01 기계 파일 그대로) · 박음 거울.
|
||||
③ 굴착기 1.0㎥ — 가격 · 조종원(2 인 · 직종 · 뺌) · 잡품 율 덮음 → 줄 칸 · 덮음 얹고 박은 셈 = 줄 고친 셈.
|
||||
③ 굴착기 1.0㎥ — 가격 · 인력 줄(2 인 · 직종 · 뺌 · 둘) · 잡품 줄(율 · 재료 더함) 덮음 → 줄 목록 ·
|
||||
덮음 얹고 박은 셈 = 줄 고친 셈(842-3 — 덮음 조종원 · 잡품 = 줄 `인력` · `잡품` 목록 그대로).
|
||||
④ 내 기계 꼴 — 바로(상각비계수 → 손료계수) · 직접(시간당) · 처음 값으로 되돌리면 줄 같음.
|
||||
⑤ M01 과 같은 이름 · 규격 = 409 · 다른 이름 = 통과(검사만 · 쓰기 없음).
|
||||
"""
|
||||
@@ -28,6 +29,7 @@ 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")
|
||||
mcrew = importlib.import_module("master_machine_crew")
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
COSTS = ("노무비", "재료비", "경비", "계")
|
||||
@@ -117,18 +119,23 @@ def test_655대_고침_없음_줄_같음_박음_거울(world):
|
||||
|
||||
# ── ③ 굴착기 덮음 → 줄 · 셈 ────────────────────────────────────────────
|
||||
def test_굴착기_덮음_줄_칸_셈(world):
|
||||
row = _plain(next(r for r in world["rows"] if r["키"] == DIG))
|
||||
row = _plain(mcrew.from_old_row(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}),
|
||||
]
|
||||
({"조종원": [{"키": "LB:1048", "인": 2}]}, {"인력": [{"키": "LB:1048", "인": 2}]}),
|
||||
({"조종원": [{"키": lb, "인": 1}]}, {"인력": [{"키": "LB:1002", "인": 1}]}),
|
||||
({"조종원": []}, {"인력": []}),
|
||||
({"잡품": [{"줄": "주연료", "율": 30}]}, {"잡품": [{"줄": "주연료", "율": 30}]}),
|
||||
(two := {"조종원": [{"키": "LB:1048", "인": 1}, {"키": "LB:1002", "인": 2}]},
|
||||
{"인력": two["조종원"]}),
|
||||
(mat := {"잡품": [{"줄": "주연료", "율": 22},
|
||||
{"키": "MO:휘발유:전국평균", "이름": "휘발유", "수량": 2, "율": 50}]},
|
||||
{"잡품": mat["잡품"]}),
|
||||
] # fmt: skip
|
||||
targets = [
|
||||
{"식": "GC000267", "처음": row, "덮음": c, "개인": False, "직종": jobs} for c, _ in cases
|
||||
]
|
||||
@@ -137,15 +144,20 @@ def test_굴착기_덮음_줄_칸_셈(world):
|
||||
want = copy.deepcopy(row)
|
||||
want["값"].update(change)
|
||||
assert g["줄"] == want, cover
|
||||
first, now = m01._dec(row["값"]), m01._dec(g["줄"]["값"])
|
||||
shaped = ms.bind(
|
||||
mcv.cover_doc(doc, given, {row["원문번호"]: m01._dec(cover)}), m01._dec(row["값"])
|
||||
mcv.cover_doc(
|
||||
doc, given, {row["원문번호"]: m01._dec(cover)}, mcv.row_entry(doc, first)
|
||||
),
|
||||
first,
|
||||
)
|
||||
edited = ms.bind(doc, m01._dec(g["줄"]["값"]))
|
||||
edited = ms.bind(mcv.cover_doc(doc, given, None, mcv.row_entry(doc, now)), now)
|
||||
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})
|
||||
cur["값"].update({"가격": 150000, "인력": [{"키": "LB:1048", "인": 2}],
|
||||
"잡품": [{"줄": "주연료", "율": 30}]}) # fmt: skip
|
||||
made = _node({"문서들": {"GC000267": _plain(doc)},
|
||||
"대상": [{"식": "GC000267", "처음": row, "지금": cur, "개인": False}]})[0] # fmt: skip
|
||||
assert made["덮음"] == {
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
"""842-3 기계 인력 · 잡품 줄 지킴 시험 — 두 길 같음 · 자료 꼴 · 손셈 · 거울 · 저장 꼴 · 쓰기 없음.
|
||||
|
||||
짝 = resources/master_data/scripts/master_machine_crew.py ↔ common_util/common_util_machine_crew.ts ·
|
||||
`master_doc_cover` row_entry · merge · cover_doc(바탕) ↔ common_util_formula_doc_cover.ts rowEntry · mergeCover · coverDoc.
|
||||
① 655 대 두 길 — 로직 엔진(목록 → 조종원 수 · 직종 · 잡재료율 읽기) = 문서 엔진(식 문서 + 목록 바탕 덮음) 노 · 재 · 경 · 계.
|
||||
② M01 기계 자료 — 옛 칸(조종원 · 조종원직종 · 잡재료율) 0 · 목록 꼴 틀 0 · 선원류 칸 그대로.
|
||||
③ 굴착기 1.0㎥ — 인력 둘(직종 다름) = 한 줄씩 셈 합 · 잡품 재료 줄 = 단가 × 수량 × 율 · 단가산출 덮음이 위에 덮임.
|
||||
④ 거울 — 읽기 · 옛 꼴 바꿈 · 바탕 덮음 · 얹은 문서 파이썬 = TS.
|
||||
⑤ 내 기계 저장 꼴 — 목록 받음 · 옛 칸 → 목록 · 틀린 줄 400.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from decimal import Decimal as D
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Doc as m01doc
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠
|
||||
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_Sangeun_Copy as skc
|
||||
|
||||
mcrew = importlib.import_module("master_machine_crew")
|
||||
mcv = importlib.import_module("master_doc_cover")
|
||||
mc = importlib.import_module("master_doc_calc")
|
||||
ml = importlib.import_module("master_doc_link")
|
||||
mcut = importlib.import_module("master_cut")
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
COSTS = ("노무비", "재료비", "경비", "계")
|
||||
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ 0201-0100 · GC000267
|
||||
SEA = ("선원", "준설선선장", "준설선기관사", "고급선원")
|
||||
|
||||
|
||||
def _plain(v):
|
||||
return json.loads(json.dumps(v, ensure_ascii=False, default=float))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def world():
|
||||
with store.at(store.FOLDER):
|
||||
index = m01.loaded()[1]
|
||||
yield {"index": index, "docs": m01doc.docs(), "rows": list(index.index["EQ"].values())}
|
||||
|
||||
|
||||
def _logic(index, row, logic):
|
||||
given = {i["이름"]: (i.get("고르기") or [""])[0] for i in logic.get("입력") or []}
|
||||
if mh.takes_machine(logic):
|
||||
given["기계"] = str(row.get("원문번호"))
|
||||
mf = m01.cm.mf
|
||||
got = mcut.engine(index, logic["키"], mf.run(index, logic["키"], mf.fill_empty(logic, given)))
|
||||
return {k: round(float(got[k]), 4) for k in COSTS}
|
||||
|
||||
|
||||
# ── ① 두 길 ────────────────────────────────────────────────────────────
|
||||
def test_655대_로직_엔진_문서_엔진_같음(world):
|
||||
index, same = world["index"], 0
|
||||
with store.at(store.FOLDER):
|
||||
for row in world["rows"]:
|
||||
logic = mh.pick(row)
|
||||
if logic is None or mh.why(row, logic):
|
||||
continue
|
||||
doc = mh.doc_sums(row, logic, index)
|
||||
if doc is None or not doc["ok"]:
|
||||
continue
|
||||
assert {k: round(v, 4) for k, v in doc["합"].items()} == _logic(index, row, logic), row[
|
||||
"키"
|
||||
]
|
||||
same += 1
|
||||
assert len(world["rows"]) == 655 and same >= 600
|
||||
|
||||
|
||||
# ── ② 자료 꼴 ───────────────────────────────────────────────────────────
|
||||
def test_M01_기계_옛_칸_없음_목록_꼴(world):
|
||||
crew = misc = 0
|
||||
for row in world["rows"]:
|
||||
vals = row.get("값") or {}
|
||||
assert not any(k in vals for k in mcrew.OLD), row["키"]
|
||||
assert mcrew.check(vals) == [], row["키"]
|
||||
for part in ("단위", "출처"):
|
||||
assert set(row.get(part) or {}) == set(vals), (row["키"], part)
|
||||
crew += bool(vals.get("인력"))
|
||||
misc += bool(vals.get("잡품"))
|
||||
assert crew >= 295 and misc >= 320
|
||||
assert sum(any(k in (r.get("값") or {}) for k in SEA) for r in world["rows"]) == 79
|
||||
|
||||
|
||||
# ── ③ 손셈 ─────────────────────────────────────────────────────────────
|
||||
def _hour(world, vals, cover=None):
|
||||
"""굴착기 줄 값을 바꾼 사본 색인으로 시간당(문서 엔진 · 단가산출 덮음 얹음)."""
|
||||
row = copy.deepcopy(world["index"].get(DIG))
|
||||
row["값"] = {**mcrew.from_old(row["값"]), **vals}
|
||||
index = skc.Copy(world["index"], [row], ("시험", id(vals)))
|
||||
doc, given = world["docs"]["GC000267"], {"기계": "0201-0100"}
|
||||
shaped = mcv.cover_doc(doc, given, cover, mcv.row_entry(doc, row["값"]))
|
||||
got = mc.calc(shaped, ml.Links(index, world["docs"]), given)
|
||||
assert got["셈됨"], got["표지"]
|
||||
return got
|
||||
|
||||
|
||||
def test_굴착기_인력_둘_잡품_재료_손셈(world):
|
||||
with store.at(store.FOLDER):
|
||||
one = _hour(world, {"인력": [{"키": "LB:1048", "인": D(1)}]})
|
||||
two = _hour(world, {"인력": [{"키": "LB:1002", "인": D(2)}]})
|
||||
both = _hour(
|
||||
world, {"인력": [{"키": "LB:1048", "인": D(1)}, {"키": "LB:1002", "인": D(2)}]}
|
||||
)
|
||||
assert abs(both["노무비"] - one["노무비"] - two["노무비"]) <= 1
|
||||
assert both["재료비"] == one["재료비"] and both["경비"] == one["경비"]
|
||||
crew = [x for x in both["줄"] if x.get("이름") == "조종원"]
|
||||
assert len(crew) == 2
|
||||
none = _hour(world, {"인력": []})
|
||||
assert none["노무비"] == 0
|
||||
# 잡품 재료 줄 — 주연료 22 % + 재료 2 × 50 %
|
||||
base = _hour(world, {})
|
||||
mat = _hour(world, {"잡품": [{"줄": "주연료", "율": D(22)},
|
||||
{"키": "MO:휘발유:전국평균", "수량": D(2), "율": D(50)}]}) # fmt: skip
|
||||
price = world["index"].get("MO:휘발유:전국평균")["값"]
|
||||
assert abs(mat["재료비"] - base["재료비"] - D(str(price)) * 2 * D("0.5")) <= 1
|
||||
# 단가산출 덮음 = 목록 통째 위에(정본 2 줄 → 덮음 1 줄)
|
||||
top = _hour(world, {"인력": [{"키": "LB:1048", "인": D(1)}, {"키": "LB:1002", "인": D(2)}]},
|
||||
{"0201-0100": {"조종원": [{"키": "LB:1048", "인": D(1)}]}}) # fmt: skip
|
||||
assert top["노무비"] == one["노무비"]
|
||||
|
||||
|
||||
def test_빈_직종_줄은_식_입력_직종(world):
|
||||
doc = world["docs"]["GC000275"]
|
||||
parts = mcv.parts_of(doc)
|
||||
assert mcv.row_entry(doc, {"인력": [{"키": "", "인": 1}]}) is None # 한 줄 = 식 그대로
|
||||
got = mcv.cover_doc(doc, {"기계": "M"}, None, {"조종원": [{"키": "", "인": 2}]})
|
||||
crew = [x for x in got["금액"] if x.get("비목") == "노무비"]
|
||||
assert len(crew) == 1 and crew[0]["단가"] == doc["금액"][parts["crew"][0]]["단가"]
|
||||
|
||||
|
||||
# ── ④ 거울 ─────────────────────────────────────────────────────────────
|
||||
VALUES = [
|
||||
{"가격": 1, "주연료": 19.5, "조종원": 1, "조종원직종": "1048", "잡재료율": 22},
|
||||
{"가격": 1, "조종원": 1, "조종원직종": None, "잡재료율": 0, "주연료": 3},
|
||||
{"가격": 1, "전력": 5, "주연료": 2, "잡재료율": 7, "조종원": 0},
|
||||
{"가격": 1, "인력": [{"키": "LB:1048", "인": 1}, {"키": "LB:1002", "인": 2}], "잡품": []},
|
||||
{"가격": 1, "인력": [{"키": "", "인": 1}], "잡품": [{"키": "MA:1", "수량": 2, "율": 5}]},
|
||||
{"가격": 1},
|
||||
]
|
||||
TARGETS = [
|
||||
("GC000268", {"인력": [{"키": "LB:1048", "인": 1}], "잡품": [{"줄": "주연료", "율": 10}]}, None),
|
||||
("GC000268", {"인력": [{"키": "LB:1048", "인": 1}, {"키": "LB:1002", "인": 1}]}, None),
|
||||
("GC000268", {"인력": [], "잡품": [{"줄": "주연료", "율": 10}, {"키": "MA:1", "율": 5}]}, None),
|
||||
("GC000272", {"인력": [{"키": "LB:1048", "인": 1}], "잡품": [{"줄": "주연료", "율": 3, "수량": 1}]},
|
||||
{"값": {"가격": 9}, "잡품": [{"줄": "주연료", "율": 4}]}),
|
||||
("GC000273", {"인력": [{"키": "LB:1048", "인": 1}], "잡품": [{"키": "MA:1", "율": 5}]}, None),
|
||||
("GC000275", {"인력": [{"키": "", "인": 2}, {"키": "LB:1050", "인": 1}]}, None),
|
||||
("GC001200", {"잡품": [{"줄": "주연료", "율": 7}, {"줄": "전력", "율": 7}]}, None),
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
def test_거울_파이썬_TS_같은_답(world):
|
||||
docs = {k: _plain(world["docs"][k]) for k, _, _ in TARGETS}
|
||||
ask = {"값들": VALUES, "문서들": docs,
|
||||
"대상": [{"식": k, "값": v, **({"덮음": c} if c else {})} for k, v, c in TARGETS]} # fmt: skip
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_842_3_machine_crew.cjs")],
|
||||
input=json.dumps(ask, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
got = json.loads(proc.stdout)
|
||||
assert got["읽기"] == _plain([mcrew.view(v) for v in VALUES])
|
||||
assert got["옛"] == _plain([mcrew.from_old(v) for v in VALUES])
|
||||
base = [mcv.row_entry(docs[k], v) for k, v, _ in TARGETS]
|
||||
assert got["바탕"] == _plain(base)
|
||||
assert [b is None for b in base] == [True, False, False, False, False, False, True]
|
||||
want = [
|
||||
mcv.cover_doc(docs[k], {"기계": "M"}, {"M": c} if c else None, b)
|
||||
for (k, _, c), b in zip(TARGETS, base)
|
||||
]
|
||||
assert got["얹음"] == _plain(want)
|
||||
|
||||
|
||||
# ── ⑤ 내 기계 저장 꼴 ─────────────────────────────────────────────────
|
||||
def test_내기계_저장_목록_받음(world):
|
||||
with store.at(store.FOLDER):
|
||||
got = mym.clean({"이름": "sub3임시", "방식": "바로", "값": {
|
||||
"가격": 1000, "손료계수": 2000, "주연료": 5, "주연료종류": "경유",
|
||||
"인력": [{"키": "LB:1048", "인": 1}, {"키": "LB:1002", "인": 2}],
|
||||
"잡품": [{"줄": "주연료", "율": 10}, {"키": "MO:휘발유:전국평균", "수량": 1, "율": 50}]}}) # fmt: skip
|
||||
assert len(got["값"]["인력"]) == 2 and len(got["값"]["잡품"]) == 2
|
||||
assert got["단위"]["인력"] == "인/일" and got["출처"]["잡품"] == "개인"
|
||||
old = mym.clean({"이름": "sub3임시", "방식": "바로", "값": {
|
||||
"가격": 1000, "손료계수": 2000, "주연료": 5, "주연료종류": "경유",
|
||||
"조종원": 1, "조종원직종": "1048", "잡재료율": 10}}) # fmt: skip
|
||||
assert old["값"]["인력"] == [{"키": "LB:1048", "인": 1}]
|
||||
assert old["값"]["잡품"] == [{"줄": "주연료", "율": 10}]
|
||||
assert not any(k in old["값"] for k in mcrew.OLD)
|
||||
for bad in ({"인력": [{"키": "LB:없음", "인": 1}]}, {"인력": [{"키": "LB:1048", "인": -1}]},
|
||||
{"잡품": [{"줄": "없는줄", "율": 1}]}, {"잡품": [{"키": "MA:없음", "율": 1}]}): # fmt: skip
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
mym.clean(
|
||||
{"이름": "sub3임시", "방식": "바로", "값": {"가격": 1, "손료계수": 1, **bad}}
|
||||
)
|
||||
assert e.value.status == 400, bad
|
||||
Reference in New Issue
Block a user