diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index eb96ae447..2495f7a72 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -1,8 +1,10 @@ """M02 일위대가 문서 `labor/<열id>.json` — 정리 · 검사 · 계산 · 알약(PLAN 39-1a · 설계_정함 1장). 문서 = `{종류, 판, 열, 일위대가번, 단위, 본뜬, -담은로직: [{로직, 별칭, 메모, 입력값, 수량, 켜쌓기율?, 고름?, 승격?} | {참조: {층, 주인, 키}, 별칭, 메모, 수량}], -자재: [{코드, 이름, 규격, 단위, 수량}], 계산값}` — 이름은 구조물 열 이름(따로 안 둠). +담은로직: [{로직, 별칭, 메모, 입력값, 수량, 켜쌓기율?, 고름?, 승격?, 자재표시?} | +{참조: {층, 주인, 키}, 별칭, 메모, 수량}], +자재: [{코드, 이름, 규격, 단위, 수량, 자재구분?, 별도계상?, 운반비?}], 계산값}` — +이름은 구조물 열 이름(따로 안 둠). 수량 = `null`(면적산출 알약 칸 값 · 로직 알약 「이름|단위」 · 자재 알약 「자재|MT키」) | `{손: 수}` (손이 칸보다 앞섬) · 같은 로직 두 번 금지 · 입력값 = 그 로직 입력 이름만(빈 값 = 자동값). 금액: 기준 = 결과단위 그대로(서버 단독 · M01 엔진) · 반영 = 기준 × 수량 ÷ 배 · 돈 아닌 로직은 @@ -18,6 +20,8 @@ 고름 = `{호표 차례: MT키}`(엔진 `choosing` 그대로) · 금액은 로직 재료비 안이라 합 · 알약에 안 더함(보기만) · 사용자 자재와 같은 품목 두 번은 [저장] 때 400. 승격(134-1) = 로직 줄 「단가승격」 표시(`true` 만 · 없음 = 단가산출) · 금액 무관 · B09 호표 몫. +자재 세 칸(148-4) = `MARKS` 허용 값만(그 밖 · 빈 것 버림 · 금액 무관) — 사용자 자재 = 줄 덧칸 · +로직 자재 = 로직 줄 `자재표시 {호표 차례: {세 칸}}`(차례가 재료 줄인 것만 · 틀리면 버림). """ from __future__ import annotations @@ -38,6 +42,11 @@ mm = mf.mp.mm COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비 MATERIAL_HEAD = "자재|" _MAT_COLS = ("코드", "이름", "규격", "단위") +MARKS = { + "자재구분": ("일반", "관급", "사급"), + "별도계상": ("별도계상", "직접계산"), + "운반비": ("포함", "미포함"), +} ALIAS_MAX = 40 # 별칭(44-6) 글자 수 상한 — 화면 칸과 같음 _BRACE = re.compile(r"\{([^{}]+)\}") # 품목 조건의 {입력} @@ -126,6 +135,9 @@ def clean(doc: Any) -> dict[str, Any]: **({"켜쌓기율": rate} if rate is not None else {}), **({"고름": picks} if picks else {}), **({"승격": True} if one.get("승격") is True else {}), + **( + {"자재표시": shown} if (shown := _mat_marks(one.get("자재표시"), logic)) else {} + ), } ) mats, codes = [], set() @@ -146,6 +158,7 @@ def clean(doc: Any) -> dict[str, Any]: "코드": code, **{k: row.get(k) or "" for k in _MAT_COLS[1:]}, "수량": _qty(one.get("수량"), where), + **marks(one), } ) out = { @@ -193,6 +206,24 @@ def _picks(value: Any, logic: dict, whole: Any, where: str) -> dict[str, str] | return out +def marks(one: Any) -> dict[str, str]: + """자재 세 칸(148-4) — 허용 값만.""" + got = one if isinstance(one, dict) else {} + return {k: got[k] for k, allowed in MARKS.items() if got.get(k) in allowed} + + +def _mat_marks(value: Any, logic: dict) -> dict[str, dict[str, str]] | None: + """로직 자재 세 칸(148-4) `{호표 차례: {세 칸}}` — 맨 위 층 재료 줄 차례 · 허용 값만 · + 빈 것 = None.""" + rows = logic.get("호표") or [] + out = {} + for at, got in (value if isinstance(value, dict) else {}).items(): + n = int(at) if str(at).isdigit() else -1 + if 0 <= n < len(rows) and rows[n].get("종류") == "재료" and (kept := marks(got)): + out[str(n)] = kept + return out or None + + def _rate(value: Any, code: str, where: str) -> int | float | None: """켜쌓기율(85-2) — 0 초과 200 이하 · 소수 한 자리 · 100 · 대상 밖 로직은 None(안 둠).""" if value is None or value == "": diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 550e2ec7b..78eb720f0 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -205,15 +205,22 @@ } /* 구분 드롭다운(134-1) — 칸 너비만 · 품셈 책 글은 아래 */ -.m02-labrow__kind .ui-field { +.m02-labrow__kind .ui-field, +.m02-labrow__mark .ui-field { display: block; } -.m02-labrow__kind .ui-select { +.m02-labrow__kind .ui-select, +.m02-labrow__mark .ui-select { width: auto; min-width: 0; } +/* 자재 세 칸(148-4) — 글 한 줄 */ +.m02-labrow__mark { + white-space: nowrap; +} + /* 줄 접고 폄 표(51-2) · 고른 줄 하위 행 — 이름 들여쓰기 · 흐린 바탕 · 위 줄보다 촘촘(51-7) */ .m02-labrow .m02-labrow__sub td { padding: var(--spacing-4) var(--spacing-8); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 6f1fa2a76..341987933 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -135,8 +135,35 @@ export interface LaborRow { 고름?: Record; /** 단가승격(134-1 · 로직 줄 구분 드롭다운) — 없음 = 단가산출 · 금액 무관 · B09 호표 몫 */ 승격?: boolean; + /** 로직 자재 세 칸(148-4) — {호표 차례: 세 칸} · 금액 무관 */ + 자재표시?: Record; } -export interface LaborMat { +/** 자재 세 칸(148-4 · 서버 `M02_Labor_Doc.MARKS` 와 같음) — 처음 값 없음(「—」) · 금액 무관 */ +export const MAT_MARKS = { + 자재구분: ["일반", "관급", "사급"], + 별도계상: ["별도계상", "직접계산"], + 운반비: ["포함", "미포함"], +} as const; +export type MatMarkKey = keyof typeof MAT_MARKS; +export type MatMarks = { [K in MatMarkKey]?: (typeof MAT_MARKS)[K][number] }; +/** 허용 값만 남김(서버 `marks` 와 같은 규칙) */ +export function cleanMarks(v: unknown): MatMarks { + const got = (v && typeof v === "object" ? v : {}) as Record; + const out: Record = {}; + for (const [k, allowed] of Object.entries(MAT_MARKS)) + if ((allowed as readonly unknown[]).includes(got[k])) out[k] = got[k]; + return out as MatMarks; +} +/** 로직 줄 `자재표시` 정리 — 빈 차례는 뺌 · 다 비면 칸 없음 */ +function cleanShown(v: unknown): { 자재표시?: Record } { + const out: Record = {}; + for (const [at, one] of Object.entries(v && typeof v === "object" ? v : {})) { + const kept = cleanMarks(one); + if (Object.keys(kept).length) out[at] = kept; + } + return Object.keys(out).length ? { 자재표시: out } : {}; +} +export interface LaborMat extends MatMarks { 코드: string; 이름: string | null; 규격: string | null; @@ -173,6 +200,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc { ...(r.켜쌓기율 != null ? { 켜쌓기율: r.켜쌓기율 } : {}), ...(r.고름 && Object.keys(r.고름).length ? { 고름: { ...r.고름 } } : {}), ...(r.승격 === true && !r.참조 ? { 승격: true } : {}), + ...(r.참조 ? {} : cleanShown(r.자재표시)), })), 자재: (d.자재 ?? []).map((m) => ({ 코드: m.코드, @@ -180,6 +208,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc { 규격: m.규격 ?? null, 단위: m.단위 ?? null, 수량: m.수량 ?? null, + ...cleanMarks(m), })), }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 70f5bb5f6..f95134d12 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -48,6 +48,7 @@ import { type LaborDoc, type LaborQty, type LaborRow, + type MatMarkKey, } from "./M02_MasterTemplete_Labor_Api"; import { buildLaborDiagram, @@ -60,11 +61,12 @@ import { openRefEdit } from "./M02_MasterTemplete_Labor_RefEdit"; import { afterRowDrag } from "./M02_MasterTemplete_Labor_RowDrag"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats"; +import { setLogicMark, setMark } from "./M02_MasterTemplete_Labor_MatMarks"; import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import { insideKeys, releaseTied } from "./M02_Labor_LogicMats_Model"; import { keepKnown } from "./M02_Labor_Values_Model"; -import { pickMaterial } from "./M02_Material_Pick"; +import { addMaterialButton } from "./M02_Material_Pick"; import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick"; import { fetchStructureDef } from "./StructOptions/M02_SO_Api"; import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo"; @@ -228,10 +230,18 @@ export function mountLaborDetail( cur.담은로직, calc ?? last, !calc && !fail, - edit ? { open: openRow, swap: swapMat, reset: resetMat } : { open: openRow }, + // 세 칸(148-4) = 구조물 일위대가만(사전조합 줄은 `_combo_lines` 가 안 남김) + edit + ? { + open: openRow, + swap: swapMat, + reset: resetMat, + mark: lib ? undefined : markLogicMat, + } + : { open: openRow }, ), }, - edit ? { open: openMat, remove: removeMat } : { open: openMat }, + edit ? { open: openMat, remove: removeMat, mark: markMat } : { open: openMat }, ), ); drawDg(); @@ -376,6 +386,15 @@ export function mountLaborDetail( else delete cur.담은로직[i].승격; opt.onChanged(dirty()); } + // 자재 세 칸(148-4) — 화면 캐시만 · 금액 무관이라 다시 셈 없음 + function markMat(i: number, key: MatMarkKey, v: string): void { + setMark(cur.자재[i], key, v); + opt.onChanged(dirty()); + } + function markLogicMat(i: number, at: number, key: MatMarkKey, v: string): void { + setLogicMark(cur.담은로직[i], at, key, v); + opt.onChanged(dirty()); + } function removeMat(i: number): void { cur.자재.splice(i, 1); changed(); @@ -492,24 +511,13 @@ export function mountLaborDetail( library, ), }); - const addMat = createButton({ - label: tc("Labor_AddMat"), - variant: "filled", - onClick: () => - pickMaterial((m) => { - if (insideKeys((calc ?? last)?.줄).has(m.코드)) - return showToast( - tc("Labor_InLogicMat").replace("{value}", `${m.코드} ${m.이름 ?? ""}`), - "error", - ); - if (cur.자재.some((x) => x.코드 === m.코드)) - return showToast( - tc("Labor_AlreadyMat").replace("{value}", `${m.코드} ${m.이름 ?? ""}`), - "error", - ); - cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null }); - changed(); - }), + const addMat = addMaterialButton(tc("Labor_AddMat"), (m) => { + const why = insideKeys((calc ?? last)?.줄).has(m.코드) + ? "Labor_InLogicMat" + : cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat"; + if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error"); + cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null }); + changed(); }); // 단위 = 표준 단위 목록에서 고르기(지금 값이 목록에 없어도 선택지로 둠) · 저장은 [저장] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_LogicMats.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_LogicMats.ts index 924640cac..2fc6db140 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_LogicMats.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_LogicMats.ts @@ -12,8 +12,9 @@ import { createButton, el } from "@ui/ui_template_elements"; import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money"; import { openPickTable } from "../M01_MasterData/M01_MasterData_UI_Logic_PickTable"; import { headWord } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Modal"; -import type { LaborCalc, LaborRow } from "./M02_MasterTemplete_Labor_Api"; +import type { LaborCalc, LaborRow, MatMarkKey } from "./M02_MasterTemplete_Labor_Api"; import { lineId } from "./M02_MasterTemplete_Labor_Calc"; +import { markCells } from "./M02_MasterTemplete_Labor_MatMarks"; import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import type { LogicMat } from "./M02_Labor_LogicMats_Model"; @@ -25,6 +26,8 @@ export interface LogicMatHandlers { swap?: (row: number, at: number, key: string) => void; /** [기본] — 그 줄 고름 풂 */ reset?: (row: number, at: number) => void; + /** 세 칸(148-4) — 로직 줄 `자재표시` · 없으면 글만 */ + mark?: (row: number, at: number, key: MatMarkKey, v: string) => void; } const num = (v: number | null): string => @@ -141,6 +144,10 @@ function matRow( text: mat.단위, attrs: { "data-label": tc("Labor_Unit") }, }), + ...markCells( + row.자재표시?.[String(mat.차례)] ?? {}, + on?.mark ? (k, v) => on.mark?.(r, mat.차례, k, v) : undefined, + ), ...PRICE_SLOTS.map(priceCell), money, ...tools, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_MatMarks.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_MatMarks.ts new file mode 100644 index 000000000..c9dcf8a0a --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_MatMarks.ts @@ -0,0 +1,78 @@ +/* ============================================================================= + * M02_MasterTemplete_Labor_MatMarks.ts + * 자재목록 세 칸(148-4) — 자재구분(일반 · 관급 · 사급) · 별도계상(별도계상 · 직접계산) · 운반비(포함 · 미포함) + * · 고칠 수 있으면 드롭다운(처음 값 빈 「—」 · 줄 누름과 따로) · 아니면 글 · 금액 무관 + * · 사용자 자재 = 줄 덧칸 · 로직 자재 = 로직 줄 `자재표시 {호표 차례: 세 칸}`(`Labor_Api` 정리 · 서버 `clean`) + * ========================================================================== */ + +import { createSelectField, el } from "@ui/ui_template_elements"; +import { + MAT_MARKS, + type LaborRow, + type MatMarkKey, + type MatMarks, +} from "./M02_MasterTemplete_Labor_Api"; +import { tc, type ComboTextKey } from "./M02_MasterTemplete_Labor_Text"; + +const HEAD: Record = { + 자재구분: "Mark_Kind", + 별도계상: "Mark_Separate", + 운반비: "Mark_Haul", +}; +/** 선택지 글(저장 값은 한국어 그대로) */ +const WORD: Record = { + 일반: "Mark_General", + 관급: "Mark_Gov", + 사급: "Mark_Private", + 별도계상: "Mark_Separate", + 직접계산: "Mark_Direct", + 포함: "Mark_In", + 미포함: "Mark_Out", +}; +const KEYS = Object.keys(MAT_MARKS) as MatMarkKey[]; + +export const markHeads = (): string[] => KEYS.map((k) => tc(HEAD[k])); + +/** 세 칸 — `set` 없으면 글만(빈 = 「—」) */ +export function markCells( + value: MatMarks, + set?: (key: MatMarkKey, v: string) => void, +): HTMLElement[] { + return KEYS.map((key) => { + const now = value[key] ?? ""; + const attrs = { "data-label": tc(HEAD[key]), "data-mat-mark": key }; + if (!set) + return el("td", { + className: "m02-labrow__mark", + text: now ? tc(WORD[now]) : "—", + attrs, + }); + const pick = createSelectField({ + options: [ + { value: "", text: "—" }, + ...MAT_MARKS[key].map((v) => ({ value: v, text: tc(WORD[v]) })), + ], + value: now, + compact: true, + ariaLabel: tc(HEAD[key]), + onChange: (v) => set(key, v), + }); + pick.root.addEventListener("click", (e) => e.stopPropagation()); // 줄 누름과 따로 + return el("td", { className: "m02-labrow__mark", attrs, children: [pick.root] }); + }); +} + +/** 한 칸 바꿈 — 빈 값 = 칸 지움 */ +export function setMark(target: MatMarks, key: MatMarkKey, v: string): void { + if (v) (target as Record)[key] = v; + else delete target[key]; +} + +/** 로직 줄 `자재표시` 한 차례 한 칸 — 빈 차례 · 빈 묶음은 뺌 */ +export function setLogicMark(row: LaborRow, at: number, key: MatMarkKey, v: string): void { + const all = (row.자재표시 ??= {}); + const one = (all[String(at)] ??= {}); + setMark(one, key, v); + if (!Object.keys(one).length) delete all[String(at)]; + if (!Object.keys(all).length) delete row.자재표시; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index adb93289c..35895e24c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -22,8 +22,10 @@ import type { LaborCost, LaborMat, LaborRow, + MatMarkKey, } from "./M02_MasterTemplete_Labor_Api"; import { lineId } from "./M02_MasterTemplete_Labor_Calc"; +import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks"; import { attachRowDrag } from "./M02_MasterTemplete_Labor_RowDrag"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import "@ui/ui_template_general_layout.css"; @@ -213,6 +215,8 @@ export interface RowHandlers { alias?: (i: number, value: string | null) => void; /** 구분 드롭다운(134-1 · 로직 줄 단가산출 ↔ 단가승격) — 없으면 글만 */ promote?: (i: number, on: boolean) => void; + /** 자재 세 칸(148-4) — 없으면 글만 */ + mark?: (i: number, key: MatMarkKey, v: string) => void; } /** 별칭 칸(44-6) — 고칠 수 있으면 칸 안 글상자(누르면 바로 고침 · 줄 누름과 따로) · 아니면 글 · 비면 빈 칸 */ @@ -590,6 +594,7 @@ export function matTable(data: MatTableData, on?: Omit): HT text: mat.단위 ?? "", attrs: { "data-label": tc("Labor_Unit") }, }), + ...markCells(mat, on?.mark ? (k, v) => on.mark?.(i, k, v) : undefined), ...PRICE_SLOTS.map(priceCell), moneyCell(line?.기준단가, line?.반영, wait, tc("Labor_AppliedSum")), ...(remove @@ -620,6 +625,7 @@ export function matTable(data: MatTableData, on?: Omit): HT tc("Labor_Spec"), tc("Labor_QtyCol"), tc("Labor_Unit"), + ...markHeads(), ...PRICE_SLOTS, `${tc("Labor_BasePrice")} · ${tc("Labor_AppliedSum")}`, ...(remove ? [tc("Col_Move")] : []), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 8088e75fb..460a20598 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -120,6 +120,16 @@ const TEXT = { "Inside the logic price — not added again", ], LogicMat_Picked: ["바꾼 품목", "Changed item"], + // 자재목록 세 칸(148-4) + Mark_Kind: ["자재구분", "Material type"], + Mark_Separate: ["별도계상", "Separate"], + Mark_Haul: ["운반비", "Haulage"], + Mark_General: ["일반", "General"], + Mark_Gov: ["관급", "Gov-supplied"], + Mark_Private: ["사급", "Contractor-supplied"], + Mark_Direct: ["직접계산", "Direct"], + Mark_In: ["포함", "Included"], + Mark_Out: ["미포함", "Excluded"], In_Title: ["넣을 값 — {value}", "Inputs — {value}"], In_None: ["이 로직은 넣을 값이 없음", "This logic has no inputs"], In_Memo: ["메모", "Memo"], diff --git a/M02_MasterTemplete/M02_Material_Pick.ts b/M02_MasterTemplete/M02_Material_Pick.ts index 8b0e604b5..fa5e9f1bf 100644 --- a/M02_MasterTemplete/M02_Material_Pick.ts +++ b/M02_MasterTemplete/M02_Material_Pick.ts @@ -1,10 +1,11 @@ /* ============================================================================= * M02_Material_Pick.ts * M01 자재 하나 고르기 창 — 이름 · 규격 · 코드 낱말로 찾아 하나 고름(일위대가 자재목록 · 자재 추가가 씀). + * · 일위대가 [자재 추가] 단추(`addMaterialButton` · 148 에서 `Labor_Detail` 이 덜어 냄) * 모양(`m02-pick*`)은 `M02_MasterTemplete_Structure.css`. * ========================================================================== */ -import { createInputField, el, showToast } from "@ui/ui_template_elements"; +import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch"; @@ -75,3 +76,7 @@ export function pickMaterial(done: (item: MaterialItem) => void): void { }, }); } + +/** 일위대가 [자재 추가] 단추 — 누르면 고르기 창 · 고른 줄로 `done`(막을지는 부르는 쪽) */ +export const addMaterialButton = (label: string, done: (item: MaterialItem) => void): HTMLElement => + createButton({ label, variant: "filled", onClick: () => pickMaterial(done) }); diff --git a/resources/tester/test_m02_labor_logic_mats.py b/resources/tester/test_m02_labor_logic_mats.py index 293b2ee7e..4a26f251a 100644 --- a/resources/tester/test_m02_labor_logic_mats.py +++ b/resources/tester/test_m02_labor_logic_mats.py @@ -170,8 +170,9 @@ def _src(name): def test_로직_자재_줄_누름은_그_로직_옵션_모달_사용자_줄은_그대로(): """148-1 — 고칠 때 · 읽기만 둘 다 `open: openRow` · 바꾸기 단추 칸은 swap 있을 때만 · 사용자 줄 = openMat.""" detail = _src("M02_MasterTemplete_Labor_Detail.ts") - assert "edit ? { open: openRow, swap: swapMat, reset: resetMat } : { open: openRow }" in detail - assert "edit ? { open: openMat, remove: removeMat } : { open: openMat }" in detail + assert "open: openRow,\n swap: swapMat," in detail + assert ": { open: openRow }," in detail + assert "{ open: openMat, remove: removeMat, mark: markMat } : { open: openMat }" in detail mats = _src("M02_MasterTemplete_Labor_LogicMats.ts") assert "const tools = swap" in mats assert 'tr.addEventListener("click", () => on.open(r))' in mats diff --git a/resources/tester/test_m02_labor_mat_marks.py b/resources/tester/test_m02_labor_mat_marks.py new file mode 100644 index 000000000..eb1752081 --- /dev/null +++ b/resources/tester/test_m02_labor_mat_marks.py @@ -0,0 +1,130 @@ +"""일위대가 자재목록 세 칸(PLAN 148-4) — 자재구분 · 별도계상 · 운반비 · 금액 무관. + +- 서버 `clean` = 허용 값만(그 밖 · 빈 것 버림) · 사용자 자재 = 줄 덧칸 · + 로직 자재 = 로직 줄 `자재표시 {호표 차례: 세 칸}`(차례가 재료 줄인 것만 · 틀려도 400 아님) +- 기준합 · 반영합 그대로 · 저장 왕복에 남음 · 화면 `normalizeLabor` 도 같은 규칙(Node 헬퍼) +- 화면 칸 부품 = 새 파일 · 사용자 줄 · 로직 줄 같은 칸 · 사전조합 줄엔 안 넘김(소스 검사) +""" + +import json +import os +import subprocess + +import pytest + +from M02_MasterTemplete import M02_Labor_Doc as labor +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from resources.tester.test_m02_structure import write_sample_table + +HERE = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete") +NET = "GC000337" # 낙석방지망 — 재료 줄 3 · 4 · 5 +WIRE = "MT000194" +MARKS = {"자재구분": "관급", "별도계상": "별도계상", "운반비": "미포함"} + + +def _doc(**extra): + return {"열": "rv", "담은로직": [], "자재": [], **extra} + + +def _first_non_material(code): + from M01_MasterData import M01_MasterData_Store as m01 + + rows = m01.loaded()[1].logic(code).get("호표") or [] + return next(n for n, r in enumerate(rows) if r.get("종류") != "재료") + + +def test_사용자_자재_허용_값만(): + got = labor.clean( + _doc(자재=[{"코드": WIRE, **MARKS}, {"코드": "MT000008", "자재구분": "아무", "운반비": ""}]) + )["자재"] + assert {k: got[0][k] for k in MARKS} == MARKS + assert not set(MARKS) & set(got[1]) + + +def test_로직_자재_자재표시_재료_줄_차례만_허용_값만(): + other = _first_non_material(NET) + shown = {"3": MARKS, "4": {"자재구분": "엉뚱"}, str(other): MARKS, "99": MARKS, "x": MARKS} + line = labor.clean(_doc(담은로직=[{"로직": NET, "자재표시": shown}]))["담은로직"][0] + assert line["자재표시"] == {"3": MARKS} + bare = labor.clean(_doc(담은로직=[{"로직": NET, "자재표시": {"4": {"운반비": "?"}}}])) + assert "자재표시" not in bare["담은로직"][0] + + +def test_금액_그대로(): + plain = _doc( + 담은로직=[{"로직": NET, "수량": {"손": 2}}], 자재=[{"코드": WIRE, "수량": {"손": 3}}] + ) + marked = _doc( + 담은로직=[{"로직": NET, "수량": {"손": 2}, "자재표시": {"3": MARKS}}], + 자재=[{"코드": WIRE, "수량": {"손": 3}, **MARKS}], + ) + a, b = (labor.calc(labor.clean(d), {}) for d in (plain, marked)) + assert a["기준합"] == b["기준합"] and a["반영합"] == b["반영합"] + + +@pytest.fixture() +def system(tmp_path, monkeypatch): + write_sample_table(tmp_path) + monkeypatch.setattr(store, "FOLDER", tmp_path) + store.create_structure("rv") + return tmp_path + + +def test_저장_왕복에_남음(system): + have = store.read("labor", "rv") + sent = { + **have["문서"], + "담은로직": [{"로직": NET, "수량": None, "자재표시": {"3": MARKS}}], + "자재": [{"코드": "MT000008", "수량": None, "자재구분": "사급"}], + } + store.write("labor", "rv", have["판"], sent) + doc = store.read("labor", "rv")["문서"] + assert doc["담은로직"][0]["자재표시"] == {"3": MARKS} + assert doc["자재"][0]["자재구분"] == "사급" and "운반비" not in doc["자재"][0] + + +def test_화면_정리도_허용_값만(): + doc = { + "담은로직": [ + {"로직": NET, "자재표시": {"3": MARKS, "4": {"운반비": "?"}}}, + { + "로직": "", + "참조": {"층": "system", "주인": None, "키": "SC-1"}, + "자재표시": {"3": MARKS}, + }, + ], + "자재": [{"코드": WIRE, "별도계상": "직접계산", "자재구분": "?"}], + } + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_m02_labor_normalize.cjs")], + input=json.dumps({"doc": doc, "column": "rv"}, ensure_ascii=False), + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + got = json.loads(proc.stdout) + assert [r.get("자재표시") for r in got["담은로직"]] == [{"3": MARKS}, None] + assert got["자재"][0]["별도계상"] == "직접계산" and "자재구분" not in got["자재"][0] + + +def _src(name): + with open(os.path.join(SRC, name), encoding="utf-8") as f: + return f.read() + + +def test_화면_칸_부품_사용자_줄_로직_줄_같은_칸(): + marks = _src("M02_MasterTemplete_Labor_MatMarks.ts") + assert '{ value: "", text: "—" }' in marks + assert "e.stopPropagation()" in marks # 줄 누름과 따로 + rows = _src("M02_MasterTemplete_Labor_Rows.ts") + assert "...markCells(mat, on?.mark" in rows and "...markHeads()," in rows + assert "...markCells(" in _src("M02_MasterTemplete_Labor_LogicMats.ts") + detail = _src("M02_MasterTemplete_Labor_Detail.ts") + assert "mark: lib ? undefined : markLogicMat" in detail + assert "mark: markMat" in detail + # 서버 허용 값 = 화면 허용 값 + api = _src("M02_MasterTemplete_Labor_Api.ts") + for key, allowed in labor.MARKS.items(): + assert f"{key}: [{', '.join(json.dumps(v, ensure_ascii=False) for v in allowed)}]" in api