diff --git a/A00_Common/line_stack_edit.ts b/A00_Common/line_stack_edit.ts index 78d7a15d4..4dcba0e72 100644 --- a/A00_Common/line_stack_edit.ts +++ b/A00_Common/line_stack_edit.ts @@ -78,6 +78,8 @@ export interface EditHooks { machine?: MachineCtx; /** 836-1 기계 제어 구역 — 덮음 없이 그 기계 식 셈(원본 = 기본값) */ calc?: (doc: FormulaDoc, given: Record) => DocAnswer; + /** 842-3 그 기계 줄 인력 · 잡품 목록(기본값 바탕) */ + lists?: (given: Record) => Record | null; } const field = ( diff --git a/A00_Common/line_stack_machine_ctl.ts b/A00_Common/line_stack_machine_ctl.ts index 002bcb429..ceb50145a 100644 --- a/A00_Common/line_stack_machine_ctl.ts +++ b/A00_Common/line_stack_machine_ctl.ts @@ -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) => Record | 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; - 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 diff --git a/A00_Common/line_stack_machine_sheet.ts b/A00_Common/line_stack_machine_sheet.ts index 556c1165b..fad4d8106 100644 --- a/A00_Common/line_stack_machine_sheet.ts +++ b/A00_Common/line_stack_machine_sheet.ts @@ -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 = { - 가격: "천원", 손료계수: "10⁻⁷/hr", 내용시간: "시간", 연간표준가동시간: "시간", 주연료: "ℓ/hr", - 잡재료율: "%", 조종원: "인", 전력: "kWh", + 가격: "천원", 손료계수: "10⁻⁷/hr", 내용시간: "시간", 연간표준가동시간: "시간", 주연료: "ℓ/hr", 전력: "kWh", }; // prettier-ignore -const PICKS = ["주연료종류", "조종원직종"]; +const PICKS = ["주연료종류"]; /** 식이 바뀔 수 있는 칸(서버 식 고름) */ -const PICKY = ["주연료", "조종원", "조종원직종", "전력", "손료계수", ...MACHINE_COEFS]; +const PICKY = ["주연료", "전력", "손료계수", ...MACHINE_COEFS]; const clone = (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); - putCover(holder, no, coverFrom(parts, d.금액 ?? [], orig, cur, mine), base); + const lists = engineVals(orig, mine); + base = defaultsOf(d, (plain(d, given).값 ?? {}) as Record, 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]) => diff --git a/A00_Common/line_stack_screen.ts b/A00_Common/line_stack_screen.ts index ece4bbea8..b356aa72b 100644 --- a/A00_Common/line_stack_screen.ts +++ b/A00_Common/line_stack_screen.ts @@ -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, }), ] diff --git a/M01_MasterData/M01_MasterData_UI_Machine.ts b/M01_MasterData/M01_MasterData_UI_Machine.ts index 0f723db3b..85b8185af 100644 --- a/M01_MasterData/M01_MasterData_UI_Machine.ts +++ b/M01_MasterData/M01_MasterData_UI_Machine.ts @@ -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 => (r["값"] ?? {}) as Record; diff --git a/M02_MasterTemplete/M02_MyMachine_Sheet.py b/M02_MasterTemplete/M02_MyMachine_Sheet.py index fe0b254ea..0d9367dd5 100644 --- a/M02_MasterTemplete/M02_MyMachine_Sheet.py +++ b/M02_MasterTemplete/M02_MyMachine_Sheet.py @@ -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) diff --git a/common_util/common_util_machine_sheet.ts b/common_util/common_util_machine_sheet.ts index 8ca59f4bf..6c33c3654 100644 --- a/common_util/common_util_machine_sheet.ts +++ b/common_util/common_util_machine_sheet.ts @@ -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; /** 고르기 목록 한 칸 — 연료(값 = 종류) · 직종(값 = 인력 원문번호) */ @@ -34,8 +36,6 @@ export interface MachineRow { } export const HOURLY = ["노무비", "재료비", "경비"] as const; -const CREW = ["조종원", "조종원직종"]; -const MISC = "잡재료율"; /** 보통 범위(옛 기계 양식과 같음) — 밖이면 경고만 */ export const RANGE: Record = { @@ -48,8 +48,9 @@ const clone = (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 => key.startsWith("LB:") ? key.slice(3) : (jobs[key] ?? key); +/** 인력 줄 키 = `LB:원문번호`(빈 키 그대로) */ +export const crewKey = (key: string, jobs: Record): 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; } diff --git a/common_util/common_util_machine_symbol_name.ts b/common_util/common_util_machine_symbol_name.ts index 53830025b..6c10f1e70 100644 --- a/common_util/common_util_machine_symbol_name.ts +++ b/common_util/common_util_machine_symbol_name.ts @@ -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 = { 전력: "전력", 조종원: "조종원", 잡재료율: "잡품", + 인력: "인력", + 잡품: "잡품", 노무비: "시간노무", 재료비: "시간재료", 경비: "시간경비", @@ -105,8 +108,14 @@ export function machineAlias(doc: FormulaDoc): Record { 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(); const plain = new Set(ids); for (const id of ids) { diff --git a/resources/tester/helper_840_2_machine_sheet.cjs b/resources/tester/helper_840_2_machine_sheet.cjs index e83f17167..cecd5f61b 100644 --- a/resources/tester/helper_840_2_machine_sheet.cjs +++ b/resources/tester/helper_840_2_machine_sheet.cjs @@ -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.박음) } : {}) }; diff --git a/resources/tester/helper_842_3_machine_crew.cjs b/resources/tester/helper_842_3_machine_crew.cjs new file mode 100644 index 000000000..17fd1fe78 --- /dev/null +++ b/resources/tester/helper_842_3_machine_crew.cjs @@ -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)); diff --git a/resources/tester/test_840_2_machine_sheet.py b/resources/tester/test_840_2_machine_sheet.py index f9ae5bf88..a6b6f0ca9 100644 --- a/resources/tester/test_840_2_machine_sheet.py +++ b/resources/tester/test_840_2_machine_sheet.py @@ -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["덮음"] == { diff --git a/resources/tester/test_842_3_machine_crew.py b/resources/tester/test_842_3_machine_crew.py new file mode 100644 index 000000000..07a7bccfd --- /dev/null +++ b/resources/tester/test_842_3_machine_crew.py @@ -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