From 8cbbe41d99b892f0ff0d0077ffd7de19e3a0eb6f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 22:27:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(723-3):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EC=A4=84=20=EB=AA=A8=EB=8B=AC=20=3D=20=EB=8B=A8?= =?UTF-8?q?=EA=B0=80=EC=82=B0=EC=B6=9C=20=EC=83=81=EC=84=B8=20=EC=A4=84=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C(=EB=B3=B4=EA=B8=B0=20=EB=84=B7)=20=E2=80=94?= =?UTF-8?q?=20[=EC=A0=81=EC=9A=A9]=20=3D=20=EA=B7=B8=20=EC=A4=84=20?= =?UTF-8?q?=EB=8D=AE=EA=B0=9C=20+=20=EC=98=B5=EC=85=98=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=EB=A1=9C=EB=8F=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5(=EA=B8=B0=EB=B3=B8=20=EA=BA=BC=EC=A7=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 `Labor_RowSk.ts` = 줄 → 이름 없는 SK 문서(lineDoc · 바탕 줄 정체) → 717-2b `mountSangeunDetail` 그대로 얹음 · take = 패치 {덮개, 뺀줄, 더한줄}(M02 변경 항목 · M01 안 바뀜) + 옵션(손댄 값만 · 자동값 안 남김) - `Labor_RowModal.ts` = 717-3 두 보기 갈음 · [적용] 뒤 닫힘 · [단가산출로도 저장] = 개인 층 새 SK(from-line) - `Sangeun_Detail.ts` handle 에 getValues 한 줄(브레인 허락 · 724 공용 부품이 살림) - `Labor_Table.ts` = 우클릭 메뉴 두 그림 뒤 엶(칸 초점 밀림에 닫히던 것) · 원문 패널 처음 접힘(표 넓게) · 머리 단추 묶음 - `Hopyo_Relink._total` = 엑셀 수량(본뜬.실무수량)으로 — 705-2 다시 잇기 전 · 후 금액 되살림(723-1 번짐) - 새 지킴 시험 test_723_labor_table(저장 수량 걷음 · 이은 알약 · 안 줄 알약 · 화면 이음) Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Hopyo_Relink.py | 6 +- .../M02_MasterTemplete_Labor_RowModal.ts | 124 ++++++++++++------ .../M02_MasterTemplete_Labor_RowSk.ts | 108 +++++++++++++++ .../M02_MasterTemplete_Labor_Table.ts | 14 +- .../M02_MasterTemplete_Sangeun_Detail.ts | 3 +- resources/tester/test_175_precombo_mats.py | 2 +- resources/tester/test_723_labor_table.py | 100 ++++++++++++++ 7 files changed, 306 insertions(+), 51 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts create mode 100644 resources/tester/test_723_labor_table.py diff --git a/M02_MasterTemplete/M02_Hopyo_Relink.py b/M02_MasterTemplete/M02_Hopyo_Relink.py index bf2054d2c..0e5ac42bc 100644 --- a/M02_MasterTemplete/M02_Hopyo_Relink.py +++ b/M02_MasterTemplete/M02_Hopyo_Relink.py @@ -111,8 +111,12 @@ def rows(keys: list[str] | None = None, doc: dict[str, Any] | None = None) -> di def _total(key: str) -> float | None: + """1 단위 계 — 수량 = 불러온 때 엑셀 수량(`본뜬.실무수량` · 723-1 · 저장 수량 없음).""" + from M02_MasterTemplete.M02_Hopyo_Build import excel_given + at = refs.here() - got = refs.Expand(m01.loaded()[1])({"층": at[0], "주인": at[1], "키": key}) + ref = {"층": at[0], "주인": at[1], "키": key} + got = refs.Expand(m01.loaded()[1], given=excel_given(ref))(ref) return _num((got.get("기준") or {}).get("계")) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts index 55e61d9eb..395279114 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts @@ -1,14 +1,17 @@ /* ============================================================================= * M02_MasterTemplete_Labor_RowModal.ts - * 일위대가표 줄 누름 모달(PLAN 723-2 · 요구서_내역16 2장 4) — 일위대가표에서 빠진 줄 동작을 모달 안으로. - * · 로직 줄 · 단가산출(SK) 참조 줄 = 머리(이름 · 「1 ㎡ 당」) · [옵션 · 메모](`Labor_Input` · 켜쌓기율) · - * 로직 자재(바꾸기 · 기본 · 세 칸 · `Labor_LogicMats`) · [단가산출 보기](717-3 `Labor_SangeunModal`) + * 일위대가표 줄 누름 모달(PLAN 723-2 · 723-3 · 요구서_내역16 2장 4 · 5) — 일위대가표에서 빠진 줄 동작을 모달 안으로. + * · 로직 줄 · 단가산출(SK) 참조 줄 = 머리(이름 · 「1 ㎡ 당」) · [옵션](메모 · 켜쌓기율 · `Labor_Input`) · + * 단가산출 상세 줄 모드(717-2b 보기 넷 · `Labor_RowSk` · 717-3 두 보기 갈음) · + * 로직 자재(바꾸기 · 기본 · 세 칸 · `Labor_LogicMats`) + * · [적용] = 그 줄 덮개(M02 변경 항목) + 옵션(시험 입력 → 그 줄 입력값 · 규격 칸) · [단가산출로도 저장](기본 꺼짐) * · 사용자 자재 줄 = 자재 모달(세 칸 · `Labor_MatTable` 한 줄) * · 고침 = 그 줄 화면 캐시(저장은 [저장]) · 금액은 표가 다시 셈 → `watch` 로 받아 다시 그림 * ========================================================================== */ +import { createCheckbox } from "@ui/ui_template_checkbox"; import { createButton, el, showToast } from "@ui/ui_template_elements"; -import { openModal } from "@ui/ui_template_modal"; +import { askConfirm, openModal } from "@ui/ui_template_modal"; import { createText } from "@ui/ui_template_text"; import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch"; import type { LaborCalc, LaborMat, LaborRow, MatMarkKey } from "./M02_MasterTemplete_Labor_Api"; @@ -16,7 +19,7 @@ import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats"; import { setLogicMark, setMark } from "./M02_MasterTemplete_Labor_MatMarks"; import { matTable } from "./M02_MasterTemplete_Labor_MatTable"; -import { openRowSangeun } from "./M02_MasterTemplete_Labor_SangeunModal"; +import { mountRowSk, saveAsSk, type RowSkHandle } from "./M02_MasterTemplete_Labor_RowSk"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import type { SkScope } from "./M02_MasterTemplete_Sangeun_Cover"; import { releaseTied } from "./M02_Labor_LogicMats_Model"; @@ -35,17 +38,23 @@ export interface RowModalOptions { unit: string; scope: SkScope; readOnly: boolean; - /** 셈 주입(프로젝트 복사본) — [단가산출 보기] 없음 */ + /** 셈 주입(프로젝트 복사본) — 단가산출 상세 없음 */ inject: boolean; /** 사용자 자재 코드 — 로직 자재 [바꾸기]로 같은 품목 막음 */ userMats: string[]; watch: CalcWatch; /** 고침 뒤 — `again` = 서버 다시 셈(입력 · 고름 · 별도계상) · 아니면 화면만 */ done: (again: boolean) => void; - /** [단가산출로 저장] 뒤 그 줄을 새 SK 참조 줄로 */ - replace?: (row: LaborRow) => void; } +const TEXT = { + Apply: ["적용", "Apply"], + AlsoSk: ["단가산출로도 저장", "Also save as build-up"], + Saved: ["{k} 저장함(내 개인 층)", "{k} saved (my layer)"], + Discard: ["적용 안 한 고침이 있음 — 버리고 닫을까?", "Discard unapplied edits?"], +} as const; +const t = (k: keyof typeof TEXT): string => TEXT[k][0]; + export function openRowModal(o: RowModalOptions): void { const row = o.row; const edit = !o.readOnly; @@ -112,56 +121,89 @@ export function openRowModal(o: RowModalOptions): void { rate: row.켜쌓기율 ?? null, readOnly: o.readOnly, done: (next) => { - const values = o.logic ? keepKnown(o.logic.입력, next.입력값).kept : next.입력값; - const same = - JSON.stringify(values) === JSON.stringify(row.입력값) && - next.켜쌓기율 === (row.켜쌓기율 ?? null); - const before = row.입력값; - row.입력값 = values; - releaseTied(row, before, line()?.재료 ?? []); + const same = setValues(next.입력값) && next.켜쌓기율 === (row.켜쌓기율 ?? null); row.메모 = next.메모; if (next.켜쌓기율 === null) delete row.켜쌓기율; else row.켜쌓기율 = next.켜쌓기율; o.done(!same); draw(); + if (!same) skMount(); // 바뀐 옵션으로 상세 다시 }, }), }); option.dataset.rowOption = ""; - const sk = createButton({ - label: tc("Labor_Sangeun"), - variant: "ghost", - onClick: () => - openRowSangeun({ - scope: o.scope, - row, - title: o.title, - head: o.head, - qty: "", - unit: o.unit, - readOnly: o.readOnly, - apply: (p) => { - if (p) row.패치 = p; - else delete row.패치; - o.done(true); - }, - ...(o.replace ? { replace: o.replace } : {}), - }), + + /** 옵션 → 그 줄 입력값(로직 줄만 · 아는 입력만) · 같으면 참 */ + function setValues(next: Record): boolean { + if (row.참조) return true; + const values = o.logic ? keepKnown(o.logic.입력, next).kept : next; + if (JSON.stringify(values) === JSON.stringify(row.입력값)) return true; + const before = row.입력값; + row.입력값 = values; + releaseTied(row, before, line()?.재료 ?? []); // 바뀐 입력에 걸린 고름 = 기본(103-1) + return false; + } + + // 단가산출 상세 줄 모드(723-3) — 셈 주입(프로젝트 복사본)엔 없음 + const skHost = el("div", { attrs: { "data-row-sk-host": "" } }); + let sk: RowSkHandle | null = null; + const skMount = (): void => { + if (o.inject) return; + sk?.destroy(); + sk = mountRowSk(skHost, { row, scope: o.scope, title: o.title, unit: o.unit, readOnly: !edit }); + }; + let close = (): void => {}; + const finish = (): void => { + stop(); + sk?.destroy(); + }; + const also = createCheckbox(t("AlsoSk"), { checked: false }); + also.input.dataset.rowAlsoSk = ""; + const apply = createButton({ + label: t("Apply"), + variant: "filled", + onClick: async () => { + const got = sk?.take() ?? null; + if (got) { + if (got.patch) row.패치 = got.patch; + else delete row.패치; + setValues(got.values); + o.done(true); + draw(); + } + if (also.input.checked) { + try { + const name = await saveAsSk(row, got?.patch ?? row.패치 ?? null, o.title, o.unit); + showToast(t("Saved").replace("{k}", name), "success"); + } catch (error) { + return showToast(error instanceof Error ? error.message : String(error), "error"); + } + } + finish(); // [적용] = 그 줄에 남기고 닫음(저장은 페이지 [저장]) · 코드 닫기는 beforeClose 안 거침 + close(); + }, }); - sk.dataset.rowSangeun = ""; + apply.dataset.rowApply = ""; const stop = o.watch((c) => { calc = c; draw(); }); - openModal({ + const tools = edit && !o.inject ? [also.root, apply] : []; + ({ close } = openModal({ title: o.title, closeLabel: tc("In_Close"), - width: "960px", - actions: [...(row.참조 ? [] : [option]), ...(o.inject ? [] : [sk])], - beforeClose: () => (stop(), true), - mount: (body) => body.append(info, mats), - }); + width: "1100px", + wide: true, + actions: [...(row.참조 || !edit ? [] : [option]), ...tools], + beforeClose: async () => { + if (sk?.take() && !(await askConfirm({ message: t("Discard"), danger: true }))) return false; + finish(); + return true; + }, + mount: (body) => body.append(info, skHost, mats), + })); draw(); + skMount(); } export interface MatModalOptions { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts new file mode 100644 index 000000000..1b8839822 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts @@ -0,0 +1,108 @@ +/* ============================================================================= + * M02_MasterTemplete_Labor_RowSk.ts + * 일위대가 줄 모달 안 단가산출 상세 = 717-2b 단가산출(SK) 상세 「줄 모드」(PLAN 723-3 · 정함 4 · 5). + * · 줄 → 이름 없는 SK 문서(`lineDoc` · 고름 · 자재표시 = 바탕 줄 정체 cid) → `mountSangeunDetail` 그대로 얹음 + * (보기 넷 · 품 · 단가 덮개 · 줄 빼기 · 더하기 · 시험 입력 칩) + * · `take()` = [적용] 때 그 줄에 남길 것 — 패치 `{덮개, 뺀줄, 더한줄}`(M02 변경 항목 · M01 안 바뀜 · 내역 17-2) + + * 옵션(시험 입력 → 그 줄 `입력값` · 정함 5) + * · [단가산출로도 저장](기본 꺼짐) = `saveAsSk` — 개인 층 새 SK(`from-line`) + * 공용 부품 갈아 끼움은 724(sub2) — 지금은 717-2b 상세를 그대로 씀. + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import { createText } from "@ui/ui_template_text"; +import { fetchAuto } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; +import type { LaborRow } from "./M02_MasterTemplete_Labor_Api"; +import { calcSangeun, sangeunFromLine, type SkScope } from "./M02_MasterTemplete_Sangeun_Cover"; +import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail"; +import { + cleanPatch, + lineDoc, + needsIds, + type LinePatch, + type SkCalc, +} from "./M02_Sangeun_Cover_Model"; + +export interface RowSkHandle { + /** 고친 것(패치 · 옵션) — 없으면 null */ + take: () => { patch: LinePatch | null; values: Record } | null; + destroy: () => void; +} + +const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b); + +/** 줄 모드 SK 상세를 `host` 에 — 바탕 줄 정체가 필요하면 먼저 셈 */ +export function mountRowSk( + host: HTMLElement, + o: { row: LaborRow; scope: SkScope; title: string; unit: string; readOnly: boolean }, +): RowSkHandle { + let handle: ReturnType | null = null; + let alive = true; + /** 자동값(상세가 빈 입력에 채움) — 손대지 않은 자동값은 줄에 안 남김(빈 값 = 자동값 그대로) */ + let auto: Record = {}; + const before = cleanPatch(o.row.패치); + const values0 = Object.fromEntries( + Object.entries(o.row.입력값 ?? {}).map(([k, v]) => [k, String(v)]), + ); + host.replaceChildren(createText("…", "muted")); + void (async () => { + let ids: string[] | null = null; + try { + if (needsIds(o.row)) + ids = (await calcSangeun(o.scope, lineDoc(o.row, null, null))).정체 ?? null; + } catch { + ids = null; // 정체 못 받음 = 고름 · 자재표시 없이(셈 까닭은 상세가 보임) + } + if (!alive) return; + const doc = { ...lineDoc(o.row, ids, before), 이름: o.title, 단위: o.unit || "식" }; + const box = el("div", { attrs: { "data-row-sk": "" } }); + host.replaceChildren(box); + handle = mountSangeunDetail(box, doc, { + layer: o.scope.layer, + owner: o.scope.owner, + readOnly: o.readOnly, + onChanged: () => undefined, + }); + if (o.row.로직 && !o.row.참조) auto = await fetchAuto(o.row.로직).catch(() => ({})); + })(); + return { + take: () => { + if (!handle) return null; + const d = handle.getDoc(); + const patch = cleanPatch({ + 덮개: d.덮개 as LinePatch["덮개"], + 뺀줄: d.뺀줄, + 더한줄: d.더한줄, + }); + const values = o.row.참조 + ? values0 + : Object.fromEntries( + Object.entries(handle.getValues()).filter( + ([k, v]) => + String(v).trim() !== "" && + (k in values0 || auto[k] == null || String(auto[k]) !== String(v)), + ), + ); + return same(patch, before) && same(values, values0) ? null : { patch, values }; + }, + destroy: () => { + alive = false; + handle?.destroy(); + }, + }; +} + +/** [단가산출로도 저장] — 그 줄(고친 패치 포함)을 개인 층 새 SK 로 · 새 이름 돌려줌 */ +export async function saveAsSk( + row: LaborRow, + patch: LinePatch | null, + name: string, + unit: string, +): Promise { + const line: Record = { ...row }; + delete line.패치; + delete line.수량; + if (patch) line.패치 = patch; + const got = await sangeunFromLine({ layer: "personal", owner: null }, line, name, unit || "식"); + return got.이름; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts index c93137a80..7fdd3e9f0 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts @@ -114,7 +114,8 @@ export function mountLaborTable( }); const sourceHost = el("div", { attrs: { "data-labor-source": "" } }); const side = createSidePanels({ - prefKey: "m02-labor-side", + prefKey: "m02-labor-table-side", // 일위대가표는 넓게 — 원문은 처음 접힘(누르면 엶) + defaultOpen: null, follow: false, panels: [{ id: "source", title: tc("Labor_Source"), body: sourceHost }], onChange: () => showSource(), @@ -175,7 +176,8 @@ export function mountLaborTable( const n = tr ? at(Number(tr.dataset.i)) : null; if (n === null || !edit) return; ev.preventDefault(); - openTreeMenu(ev, menuAt(n)); + // 칸 초점이 표를 밀면(scroll) 메뉴가 바로 닫힘 — 밀림이 끝난 다음 그림에 엶 + requestAnimationFrame(() => requestAnimationFrame(() => openTreeMenu(ev, menuAt(n)))); }); gridHost.replaceChildren(grid.root); showSource(); @@ -282,10 +284,6 @@ export function mountLaborTable( inject: !!opt.calc, userMats: cur.자재.map((m) => m.코드), done: (again) => changed(!again), - replace: - edit && opt.layer === "personal" && !library.owner - ? (next: LaborRow) => ((cur.담은로직[n] = next), changed()) - : undefined, }); } function openMat(i: number): void { @@ -452,7 +450,9 @@ export function mountLaborTable( className: "m01-logic__section-head", children: [ el("h3", { text: tc("Grid_Title") }), - ...(edit ? [addLogic, addMat] : []), + ...(edit + ? [el("div", { className: "ui-btn-group", children: [addLogic, addMat] })] + : []), ], }), createText(t("TempHint"), "hint"), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts index 3bbbde1dc..5402555a8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts @@ -58,7 +58,7 @@ export function mountSangeunDetail( host: HTMLElement, doc0: unknown, opt: SangeunDetailOptions, -): { getDoc: () => SkDoc; destroy: () => void } { +): { getDoc: () => SkDoc; getValues: () => Record; destroy: () => void } { setLogicApiPaths(LIB); // 상세 부품 길 = M02 로그인 길(717-1) const doc = clone((doc0 ?? {}) as SkDoc); let saved = JSON.stringify(doc); @@ -318,6 +318,7 @@ export function mountSangeunDetail( saved = JSON.stringify(doc); return clone(doc); }, + getValues: () => ({ ...values }), // 723-3 — 일위대가 줄 모달 [적용] = 옵션을 그 줄에 남김 destroy: () => { alive = false; host.replaceChildren(); diff --git a/resources/tester/test_175_precombo_mats.py b/resources/tester/test_175_precombo_mats.py index bfa0df359..43f350d47 100644 --- a/resources/tester/test_175_precombo_mats.py +++ b/resources/tester/test_175_precombo_mats.py @@ -43,7 +43,7 @@ def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # n ) modal = read("M02_MasterTemplete_Labor_RowModal.ts") assert 'if (opt.mode === "precombo") return mountLaborTable(host, doc, opt);' in detail - assert "...(edit ? [addLogic, addMat] : [])" in lib + assert 'className: "ui-btn-group", children: [addLogic, addMat]' in lib assert "setLogicMark(row, at, key, v);" in modal and "setMark(o.mat, key, v);" in modal assert "delete out.자재" not in lib and "delete out.열" in lib diff --git a/resources/tester/test_723_labor_table.py b/resources/tester/test_723_labor_table.py new file mode 100644 index 000000000..7dd002705 --- /dev/null +++ b/resources/tester/test_723_labor_table.py @@ -0,0 +1,100 @@ +"""PLAN 723 — M02 일위대가(사전조합) 수량 = 저장 안 함 · 일위대가표만 · 줄 모달(내역 16 · 17). + +723-1 서버 — 저장 때 안 줄 · 자재 수량 null(비율 줄 율만) · 펼침 = 쓰는 쪽 알약 값 · 안 이으면 「수량 없음」 · +구조물 조합 알약에 안 줄 알약 · 불러오기 엑셀 수량 = `본뜬.실무수량`(셈 안 씀). +723-2 · 723-3 화면 — 서고 상세 = `Labor_Table`(수량 칸 휘발 임시 입력 · 우클릭) · 줄 모달 = 717-2b 상세 줄 모드. +""" + +import os + +import pytest + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Labor_Doc as labor +from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층 fixture + +SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "M02_MasterTemplete") +SYS = {"층": "system", "주인": None} +STONE = "GF000219" # 원/㎡ +RATE = {"이름": "공구손료", "단위": "%", "단가": {"노무비": 0, "재료비": 0, "경비": 0}} + + +def _src(name: str) -> str: + return open(os.path.join(SRC, name), encoding="utf-8").read() + + +def test_저장은_안_줄_수량을_걷고_비율_줄_율만_남김(world): # noqa: F811 + rate = {"고정": {**RATE, "비율": {"기준": "노무비", "비목": "경비"}}, "수량": {"손": 3}} + lines = [{"로직": STONE, "수량": {"손": 2.5}}, rate] + with store.at(world["system"]): + key = refs.create("precombo", {"이름": "수량 없음", "단위": "m", "담은로직": lines})["이름"] + doc = store.read("precombo", key)["문서"] + assert doc["담은로직"][0]["수량"] is None + assert doc["담은로직"][1]["수량"] == {"손": 3} # 비율 = 율(기준단위 당 수량 아님) + frozen = refs.with_pills(doc) # 프로젝트 굳힘도 수량 없이 + assert frozen["담은로직"][0]["수량"] is None and frozen["알약"] + + +def test_펼침은_이은_알약만_쓰고_층_원본_옛_손은_안_씀(world): # noqa: F811 + with store.at(world["system"]): + key = refs.create("precombo", {"이름": "옛", "단위": "m", "담은로직": [{"로직": STONE}]})[ + "이름" + ] + ref = {**SYS, "키": key} + path = world["system"] / "precombo" / f"{key}.json" + import json + + old = json.loads(path.read_bytes()) + old["담은로직"][0]["수량"] = {"손": 9} # 옛 문서(723-1 앞) — 파일은 그대로 둠 + path.write_text(json.dumps(old, ensure_ascii=False), encoding="utf-8") + whole = m01.loaded()[1] + with store.at(world["system"]): + bare = refs.Expand(whole)(ref) + got = refs.Expand(whole, given={f"{refs.ref_id(ref)}#0": 2})(ref) + frozen = refs.Expand(whole, read=lambda r: old)(ref) # 굳힌 사본 = 손 그대로 + stone = labor.run_line(whole, STONE, {})["기준"]["계"] + assert bare["기준"] is None and "수량 없음" in bare["까닭"] + assert got["기준"]["계"] == pytest.approx(stone * 2) + assert frozen["기준"]["계"] == pytest.approx(stone * 9) + + +def test_구조물_조합_알약에_안_줄_알약(world): # noqa: F811 + with store.at(world["system"]): + key = refs.create( + "precombo", {"이름": "옹벽", "단위": "m", "담은로직": [{"로직": STONE, "별칭": "돌"}]} + )["이름"] + doc = labor.clean({"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}}]}) + pills = labor.pills(doc) + rid = refs.ref_id({**SYS, "키": key}) + assert [p["키"] for p in pills] == [rid, f"{rid}#0"] + assert pills[1]["이름"] == "옹벽 · 돌" and pills[1]["단위"] == "㎡" + + +def test_화면_서고_상세는_일위대가표만_임시_수량_휘발(): + table, detail = ( + _src("M02_MasterTemplete_Labor_Table.ts"), + _src("M02_MasterTemplete_Labor_Detail.ts"), + ) + assert 'if (opt.mode === "precombo") return mountLaborTable(host, doc, opt);' in detail + assert "const temp = new Map();" in table # 휘발 — 문서 밖 + assert "calc = calc && settle(calc, withTemp(), {});" in table # 짝 셈 미리 보기 + assert ( + 'const out: Record = { ...cur, 종류: "사전조합" };' in table + ) # temp 안 실음 + for label in ('tc("Up")', 'tc("Down")', 't("Alias")', 'tc("Remove")'): + assert label in table # 우클릭 메뉴 + assert "fixed: (fx: LaborFixed, name: string)" in table # 721-2 고정 줄 이음 + + +def test_화면_줄_모달은_SK_상세_줄_모드_적용은_덮개와_옵션(): + modal, sk = ( + _src("M02_MasterTemplete_Labor_RowModal.ts"), + _src("M02_MasterTemplete_Labor_RowSk.ts"), + ) + detail = _src("M02_MasterTemplete_Sangeun_Detail.ts") + assert "mountSangeunDetail(box, doc," in sk and "getValues: () => ({ ...values })" in detail + assert "if (got.patch) row.패치 = got.patch;" in modal and "setValues(got.values);" in modal + assert 'createCheckbox(t("AlsoSk"), { checked: false })' in modal # 기본 꺼짐 + assert "openRowSangeun" not in modal # 717-3 두 보기 갈음