From 1157af125b3d790b046d69e2c577e683ca7454c6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 11:45:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(m02):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=EB=AA=A9=EB=A1=9D=20=E2=80=94=20=EC=84=9C=EA=B3=A0=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EB=A7=89=ED=9E=98=20=EC=84=B8=20=EA=B3=B3?= =?UTF-8?q?=20=ED=92=82=20=C2=B7=20=EC=A0=80=EC=9E=A5=EC=9D=B4=20=EA=B3=A0?= =?UTF-8?q?=EB=A6=84=20=C2=B7=20=EC=9E=90=EC=9E=AC=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=C2=B7=20=EC=9E=90=EC=9E=AC=20=EC=82=B4=EB=A6=BC=20(175-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면: 사전조합도 구조물 일위대가와 같은 자재목록 부품(자재 추가 · 세 칸 · 운반비) · getDoc 이 자재를 안 지움 - 서버: _combo_lines 가 고름 · 자재표시 남김 · clean_doc 에 자재(labor.clean 검사 · 로직 자재와 같은 품목 400 = no_twice 한 벌) - 시험: test_175_precombo_mats(저장 왕복 · 400) · test_m02_labor_mat_marks 단정 고침 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY --- M02_MasterTemplete/M02_Labor_Doc.py | 7 ++- M02_MasterTemplete/M02_Labor_Ref.py | 20 ++++++- .../M02_MasterTemplete_Labor_Detail.ts | 11 ++-- resources/tester/test_175_precombo_mats.py | 60 +++++++++++++++++++ resources/tester/test_m02_labor_mat_marks.py | 2 +- 5 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 resources/tester/test_175_precombo_mats.py diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index 0692a0245..890c7155a 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -607,8 +607,13 @@ def server(doc: Any) -> dict[str, Any]: 사용자 자재가 로직 자재 품목과 같으면 400(103-1 · 같은 품목 두 번 셈).""" got = clean(doc) out = calc(got) + no_twice(got, out) + return {**got, "계산값": out} + + +def no_twice(got: dict, out: dict) -> None: + """사용자 자재가 로직 자재 품목과 같으면 400 — 일위대가 · 사전조합(175-1) 저장 한 벌.""" inside = {x["품목"]["키"] for one in out["줄"] for x in one.get("재료") or [] if x.get("품목")} twice = sorted({m["코드"] for m in got["자재"]} & inside) if twice: raise _bad(f"자재 「{', '.join(twice)}」 는 로직 자재에 있음 — 같은 품목 두 번 담음") - return {**got, "계산값": out} diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 126a968a0..3dbf78e2f 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -6,8 +6,9 @@ 층 문서를 읽을 때 옛 참조 줄 = 로직 줄 + `승격`(`unpromote`) · 프로젝트 사본은 그대로 셈 precombo `{층}/precombo/SC-######.json` 사전조합 `{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모} - | {로직, 입력값, 메모, 수량:{손}}]}` — 승격 · 사전조합 · 단가산출 로직(51-11) · - 수량은 손(기본 1) · 값 = Σ 안 줄 1 단위 값 × 수량(로직 줄 = 기준 ÷ 배) + | {로직, 입력값, 메모, 수량:{손}, 고름?, 자재표시?}], 자재:[일위대가 자재 꼴]}` — + 승격 · 사전조합 · 단가산출 로직(51-11) · 수량은 손(기본 1) · 자재 수량은 손 · 빈칸 = 안 셈(175) · + 값 = Σ 안 줄 1 단위 값 × 수량(로직 줄 = 기준 ÷ 배) + Σ 자재 기준단가 × 손 참조 = `{층, 주인, 키}`(마스터 주인 null · 회사 `C` · 개인 `C/U`) — 종류는 키 머리(SP · SC) · 위 층만 가리킴(개인 → 그 개인 · 그 회사 · 마스터 / 회사 → 그 회사 · 마스터 / 마스터 → 마스터) · 같은 참조 두 번 · 순환(422)은 저장 때 막음 · 계산 때도 깊이 상한(`DEPTH`)에서 멈추고 까닭. @@ -142,6 +143,7 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]: if not name: raise _bad("사전조합 이름이 빔") out.update(이름=name, 단위=unit or None, 담은로직=_combo_lines(doc, key)) + out["자재"] = _combo_mats(doc, out["담은로직"]) out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")}) out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None return out @@ -206,7 +208,7 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]: head = {"참조": one["참조"]} if "참조" in one else {"로직": one["로직"]} if "로직" in one: head["입력값"] = one["입력값"] - head.update({k: one[k] for k in ("켜쌓기율",) if k in one}) + head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one}) lines.append({**head, "수량": one["수량"] or {"손": 1}, "메모": one["메모"]}) at_ = here() me = {"층": at_[0], "주인": at_[1], "키": key} @@ -219,6 +221,18 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]: return lines +def _combo_mats(doc: dict, lines: list[dict[str, Any]]) -> list[dict[str, Any]]: + """사전조합 자재(175-1) — 일위대가 자재와 같은 검사(`labor.clean` · 개인 자재 · 세 칸) · + 로직 자재와 같은 품목 400(`labor.no_twice`).""" + from M02_MasterTemplete import M02_Labor_Doc as labor + + mats = labor.clean({"자재": doc.get("자재") or []})["자재"] + if mats: + got = {"담은로직": lines, "자재": mats} + labor.no_twice(got, labor.calc(got, {})) + return mats + + def _no_loop(ref: Ref, trail: list[Ref]) -> None: """저장 때 순환 · 깊이 — 아래로 따라가 처음 문서를 다시 만나면 422 「A → B → A」.""" if len(trail) > DEPTH: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index a37a05411..71a5e79ce 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -10,7 +10,7 @@ * · 자재목록 = 로직 자재 줄(103-1 · `Labor_LogicMats` · [바꾸기] → `담은로직[].고름` · 입력이 바뀌면 걸린 고름 풂) + * 사용자 자재 줄([삭제]) · 로직 자재와 같은 품목은 사용자 자재로 못 담음 * · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문 - * · 서고 모드(50-3 · `mode`) = 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) · + * · 서고 모드(50-3 · `mode`) = 사전조합 — 이름 칸 · 단가산출 로직 표 · 자재목록(175-1) · 원문(면적 · 다이어그램 없음) · * 정보 칸 구분 · 상세구분(51-12 · 고름) · 일위대가 상세와 같은 [로직 추가] · 표 · 동작(51-11) · 수량 손 입력(기본 1) * · 로직 줄 구분 드롭다운(134-1 · 단가산출 | 단가승격 · 서고 모드엔 없음) * ========================================================================== */ @@ -228,13 +228,13 @@ export function mountLaborDetail( cur.담은로직, calc ?? last, !calc && !fail, - // 세 칸(148-4) = 구조물 일위대가만(사전조합 줄은 `_combo_lines` 가 안 남김) + // 세 칸(148-4) — 사전조합도 같음(175-1 · `_combo_lines` 가 남김) edit ? { open: openRow, swap: swapMat, reset: resetMat, - mark: lib ? undefined : markLogicMat, + mark: markLogicMat, } : { open: openRow }, ), @@ -641,7 +641,7 @@ export function mountLaborDetail( ]), // 사전조합 = 같은 [로직 추가](51-11) section(tc("Logics_Title"), opt.card ? null : addLogic, logicHost), - ...(lib ? [] : [section(tc("Labor_Mats"), opt.card ? null : addMat, matHost)]), + section(tc("Labor_Mats"), opt.card ? null : addMat, matHost), ...(opt.card || lib ? [] : [dgBox]), ], }), @@ -668,12 +668,11 @@ export function mountLaborDetail( }); return { - // 서고 모드 = 문서 종류 그대로 · 열 · 자재 없음 + // 서고 모드 = 문서 종류 그대로 · 열 없음 getDoc: () => { if (!lib) return cur; const out: Record = { ...cur, 종류: "사전조합" }; delete out.열; - delete out.자재; return out as unknown as LaborDoc; }, saveArea: async () => { diff --git a/resources/tester/test_175_precombo_mats.py b/resources/tester/test_175_precombo_mats.py new file mode 100644 index 000000000..6d9355acf --- /dev/null +++ b/resources/tester/test_175_precombo_mats.py @@ -0,0 +1,60 @@ +"""M02 일위대가 사전조합 자재목록(PLAN 175) — 구조물 일위대가와 같은 자재목록. + +175-1 화면 · 저장 — 서고 모드 자재목록 · 로직 자재 세 칸 막힘 풂 · 저장 `_combo_lines` 가 +고름 · 자재표시 남김 · 문서 `자재`(일위대가 자재 검사 · 로직 자재와 같은 품목 400). +""" + +import json +import os + +import pytest + +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 + +HERE = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete") +NET = "GC000337" # 낙석방지망 — 재료 줄 3 · 4 · 5(철망 · 결속선 · 에폭시) +NET_ITEM = "MT006697" +WIRE = "MT000194" +MAT = "MT000008" # 자재품목 · 물가정보 83000 + + +def _save(folder, doc): + with store.at(folder): + key = refs.create("precombo", {"이름": "자재 조합", **doc})["이름"] + return key, json.loads((folder / "precombo" / f"{key}.json").read_bytes()) + + +def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # noqa: F811 + detail = open(os.path.join(SRC, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read() + assert 'section(tc("Labor_Mats"), opt.card ? null : addMat, matHost),' in detail + assert 'lib ? [] : [section(tc("Labor_Mats")' not in detail + assert "mark: markLogicMat," in detail and "lib ? undefined" not in detail + assert "delete out.자재" not in detail and "delete out.열" in detail + + +def test_저장_왕복_고름_자재표시_자재_남음(world): # noqa: F811 + line = {"로직": NET, "고름": {"4": NET_ITEM}, "자재표시": {"3": {"자재구분": "관급"}}} + mats = [ + {"코드": MAT, "수량": {"손": 2}, "운반비": "미포함", "이름": "틀린 이름"}, + {"코드": WIRE, "수량": None}, + ] + _, got = _save(world["system"], {"담은로직": [line], "자재": mats}) + one = got["담은로직"][0] + assert one["고름"] == {"4": NET_ITEM} and one["자재표시"] == {"3": {"자재구분": "관급"}} + assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, {"손": 2}), (WIRE, None)] + assert got["자재"][0]["운반비"] == "미포함" and got["자재"][0]["이름"] != "틀린 이름" + # 자재 없음 = 빈 목록(옛 문서 꼴 그대로 읽힘) + _, empty = _save(world["system"], {"담은로직": [{"로직": NET}]}) + assert empty["자재"] == [] + + +def test_로직_자재와_같은_품목_400_없는_자재_400(world): # noqa: F811 + with pytest.raises(store.StoreError) as e: + _save(world["system"], {"담은로직": [{"로직": NET}], "자재": [{"코드": WIRE}]}) + assert e.value.status == 400 and WIRE in str(e.value.detail) + with pytest.raises(store.StoreError) as e: + _save(world["system"], {"담은로직": [], "자재": [{"코드": "MT999999"}]}) + assert e.value.status == 400 diff --git a/resources/tester/test_m02_labor_mat_marks.py b/resources/tester/test_m02_labor_mat_marks.py index e10707d17..e788663a8 100644 --- a/resources/tester/test_m02_labor_mat_marks.py +++ b/resources/tester/test_m02_labor_mat_marks.py @@ -125,7 +125,7 @@ def test_화면_칸_부품_사용자_줄_로직_줄_같은_칸(): 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: markLogicMat," in detail and "lib ? undefined" not in detail # 사전조합도(175-1) assert "mark: markMat" in detail # 서버 허용 값 = 화면 허용 값 api = _src("M02_MasterTemplete_Labor_Api.ts")