From 54ce67ceb8ad6725d227246d888d570bd2f3b977 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:39:16 +0900 Subject: [PATCH] =?UTF-8?q?refactor(M02):=20=EC=9E=90=EC=9E=AC=20=EC=A1=B0?= =?UTF-8?q?=ED=95=A9(UM)=20=EC=A7=80=EC=9B=80=20=E2=80=94=20=EC=A2=8C?= =?UTF-8?q?=EC=B8=A1=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=C2=B7=20?= =?UTF-8?q?=EC=9E=AC=EB=A3=8C=EC=82=B0=EC=B6=9C=20=EC=98=86=20=ED=8C=90=20?= =?UTF-8?q?=C2=B7=20=EA=B8=B8(material-combos=20=C2=B7=20=EA=B0=88?= =?UTF-8?q?=EB=9D=BC=20=EB=82=B4=EA=B8=B0=20=C2=B7=20=ED=82=A4=20=EC=A0=81?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=90=EC=9E=AC=EC=A1=B0=ED=95=A9)=20=C2=B7=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=86=8C=20=C2=B7=20=ED=8B=80=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=C2=B7=20=EB=B3=B5=EC=82=AC=20=EA=B8=B8=EC=9D=98=20?= =?UTF-8?q?UM=20=EC=A4=84=20=C2=B7=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=B8=B5=20=ED=91=9C=20=C2=B7=20fixture=20=C2=B7=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자재 고르기 창은 M02_Material_Pick.ts 로 옮겨 남김 - storage 층(회사 · 개인 · 프로젝트) 자재조합.json 은 지우지 않음(읽기만 끊음) - check_master 틀 검사 · _틀.md 11장 · _화면_계약.md 자재 조합 절 정리 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK --- M01_MasterData/M01_MasterData_Store_Combo.py | 2 +- M02_MasterTemplete/M02_Item_Copy.py | 3 +- M02_MasterTemplete/M02_Item_Paste.py | 2 +- M02_MasterTemplete/M02_Item_Personal.py | 22 +- M02_MasterTemplete/M02_Item_Snapshot.py | 2 +- M02_MasterTemplete/M02_Item_Source.py | 7 +- .../M02_MasterTemplete_Api_Fetch.ts | 63 +---- M02_MasterTemplete/M02_MasterTemplete_Book.py | 3 +- .../M02_MasterTemplete_Branch.py | 37 +-- .../M02_MasterTemplete_Combo.css | 42 --- .../M02_MasterTemplete_Combo.ts | 1 - .../M02_MasterTemplete_ComboPanel.ts | 55 ++-- .../M02_MasterTemplete_Combo_Api.ts | 12 +- .../M02_MasterTemplete_Items.py | 36 +-- .../M02_MasterTemplete_Materials.ts | 253 ----------------- .../M02_MasterTemplete_Matset.py | 150 ---------- .../M02_MasterTemplete_Matset.ts | 267 ------------------ .../M02_MasterTemplete_Matset_Text.ts | 48 ---- .../M02_MasterTemplete_Options.ts | 2 +- .../M02_MasterTemplete_Router.py | 57 +--- .../M02_MasterTemplete_Router_Items.py | 64 +---- .../M02_MasterTemplete_Store.py | 9 +- .../M02_MasterTemplete_Structure.ts | 56 +--- M02_MasterTemplete/M02_MasterTemplete_Tree.py | 2 +- .../M02_MasterTemplete_UI_Main.ts | 102 +------ .../M02_MasterTemplete_UI_Side.ts | 29 +- .../M02_MasterTemplete_UI_Style.css | 19 +- .../M02_Project_Items_Router.py | 4 +- resources/master_data/_틀.md | 10 - resources/master_data/_화면_계약.md | 4 - resources/master_data/scripts/check_master.py | 2 - .../scripts/master_material_combo.py | 84 ------ resources/master_template/자재조합.json | 7 - .../fixtures/m02_master_sample/자재조합.json | 7 - resources/tester/test_item_copy_complete.py | 2 +- resources/tester/test_m02_card_sheet_edit.py | 10 +- resources/tester/test_m02_combo_branch.py | 51 +--- resources/tester/test_m02_empty_master.py | 1 - resources/tester/test_m02_layer_screen.py | 4 +- resources/tester/test_m02_layers_store.py | 29 +- resources/tester/test_m02_matset.py | 140 --------- .../tester/test_m02_structure_options.py | 6 +- resources/tester/test_m02_structure_panel.py | 12 +- .../test_structures_summary_snapshot.py | 2 +- resources/tester/ui/item-picker-data.test.ts | 14 +- ui_template/sheet/ui_template_sheet_ops.ts | 8 +- ui_template/sheet/ui_template_sheet_types.ts | 2 - ui_template/ui_template_item_picker_data.ts | 16 +- ui_template/ui_template_item_picker_detail.ts | 10 +- .../ui_template_item_picker_project.ts | 3 +- ui_template/ui_template_locale_m2.ts | 1 - 51 files changed, 129 insertions(+), 1645 deletions(-) delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Materials.ts delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Matset.py delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Matset.ts delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Matset_Text.ts delete mode 100644 resources/master_data/scripts/master_material_combo.py delete mode 100644 resources/master_template/자재조합.json delete mode 100644 resources/tester/fixtures/m02_master_sample/자재조합.json delete mode 100644 resources/tester/test_m02_matset.py diff --git a/M01_MasterData/M01_MasterData_Store_Combo.py b/M01_MasterData/M01_MasterData_Store_Combo.py index 75ecedb5b..eb07d9209 100644 --- a/M01_MasterData/M01_MasterData_Store_Combo.py +++ b/M01_MasterData/M01_MasterData_Store_Combo.py @@ -225,7 +225,7 @@ def combo_edit(key: str, version: str, combo_: dict, owner: str = "") -> dict: def users_of(field: str, key: str) -> list[str]: - """집계표 · SS 장부에서 열 칸 `field`(「일위대가」 · 「자재조합」)가 `key` 인 열 이름들.""" + """집계표 · SS 장부에서 열 칸 `field`(「일위대가」)가 `key` 인 열 이름들.""" names: dict[str, str] = {} for path in USERS: if not path.is_file(): diff --git a/M02_MasterTemplete/M02_Item_Copy.py b/M02_MasterTemplete/M02_Item_Copy.py index ba8fec059..90c6e6ba7 100644 --- a/M02_MasterTemplete/M02_Item_Copy.py +++ b/M02_MasterTemplete/M02_Item_Copy.py @@ -51,7 +51,6 @@ KINDS = { "도면": "도면.json", "면적": "면적.json", "재료": "재료.json", - "자재조합": "자재조합.json", "입력연결": LINK_FILE, "입력값": VALUES_FILE, "고른자재": PICKS_FILE, @@ -245,7 +244,7 @@ def _kept_picks(picks: Any, labor: dict[str, Any]) -> dict[str, Any]: def reapply(project_root: str | Path, folder: str) -> dict[str, Any]: """재적용 — 원본(층 · 마스터)을 다시 가져옴 · 입력값 · 고른자재는 같은 키면 살림. - 원본과 달라 덮인 파일(도면 · 면적 · 재료 · 자재조합 · 입력연결)은 `덮임` 에 알림.""" + 원본과 달라 덮인 파일(도면 · 면적 · 재료 · 입력연결)은 `덮임` 에 알림.""" target = folder_of(project_root, folder) manifest = read_json(target / MANIFEST_NAME) or {} values = read_json(target / VALUES_FILE) diff --git a/M02_MasterTemplete/M02_Item_Paste.py b/M02_MasterTemplete/M02_Item_Paste.py index 29c9d1671..ea4a01fa7 100644 --- a/M02_MasterTemplete/M02_Item_Paste.py +++ b/M02_MasterTemplete/M02_Item_Paste.py @@ -2,7 +2,7 @@ 붙일 폴더(`dest`) 아래 새 항목 — 대상 층 새 키(SC · SP) · 열 `본뜬` = 원본(층 · 주인 · 키) · 같은 이름은 「이름 (2)」 · 폴더는 안의 항목 · 빈 폴더까지(폴더 이름이 겹치면 「폴더 (2)」). -파일(도면 · 면적 · 재료 · 입력 연결 · 자재 조합)은 `Items.copy_item` 이 옮김. 권한은 부르는 쪽 몫. +파일(도면 · 면적 · 재료 · 입력 연결)은 `Items.copy_item` 이 옮김. 권한은 부르는 쪽 몫. """ from __future__ import annotations diff --git a/M02_MasterTemplete/M02_Item_Personal.py b/M02_MasterTemplete/M02_Item_Personal.py index 806cf7d0d..93ec208a9 100644 --- a/M02_MasterTemplete/M02_Item_Personal.py +++ b/M02_MasterTemplete/M02_Item_Personal.py @@ -1,9 +1,9 @@ """프로젝트에서 개인 층으로 — [개인에 저장](save-as) · 프로젝트에서 새 항목(new) (PLAN 26-2b). 개인 층 새 항목 = 구조물집계표 새 열 하나 + 빈 도면 · 면적산출 · 재료산출(번호 · SS 키는 저장소가 -줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 재료 · 자재조합 · 입력연결을 담음 — +줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 재료 · 입력연결을 담음 — 단가 묶음 · 입력값 · 고른자재는 프로젝트 몫이라 안 가져감. 쓰기는 지금 층 저장소 함수(`store.at` · -`store.write` · `matset.create` · 항목 연결 `write_file`) — 나무 고침(`M02_MasterTemplete_Tree`)과 +`store.write` · 항목 연결 `write_file`) — 나무 고침(`M02_MasterTemplete_Tree`)과 같은 집계표 저장 한 줄기(경로 · 머리 줄 수 맞춤 · 번호 · SS 장부)를 탐(PLAN 26-2a). 권한(본인 개인 층 · 회사 없음 409)은 부르는 쪽 몫. """ @@ -17,11 +17,10 @@ from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store from M02_MasterTemplete import M02_MasterTemplete_Items as items_store -from M02_MasterTemplete import M02_MasterTemplete_Matset as matset from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree from M02_MasterTemplete import M02_Template_Layers as layers -from M02_MasterTemplete.M02_Item_Source import LINK_FILE, UM_FILE, read_json +from M02_MasterTemplete.M02_Item_Source import LINK_FILE, read_json #: 복사본 파일 → 층 양식 종류 PART_KINDS = (("도면.json", "structure"), ("면적.json", "area"), ("재료.json", "material")) @@ -45,17 +44,6 @@ def _free_path(doc: dict[str, Any], full: list[str]) -> list[str]: return out -def _new_matset(row: dict[str, Any], fallback: str) -> str: - """자재 조합 줄을 이 층에 새 키로 — 이름이 겹치면 「이름 (2)」.""" - taken = {str(r.get("이름") or "") for r in matset.listing()["줄"]} - name = base = str(row.get("이름") or fallback) - n = 2 - while name in taken: - name, n = f"{base} ({n})", n + 1 - got = {k: row.get(k) for k in ("담은자재", "소유", "비고")} - return matset.create({**got, "이름": name, "본뜬키": None})["키"] - - def create_item( folder: Path, path: list[str], @@ -77,8 +65,6 @@ def create_item( doc = table["문서"] used = {c.get("id") for c in doc.get("열", []) if isinstance(c, dict)} new_id = items_store._fresh_id(str(base_col.get("id") or "item").split("@")[0], used) - um_row = read_json(copy / UM_FILE) if copy else None - um_key = _new_matset(um_row, heads[0]) if isinstance(um_row, dict) else None heads = _free_path(doc, heads) column = { **json.loads(json.dumps(base_col)), @@ -88,9 +74,9 @@ def create_item( "꼴": "수", "단위": unit or base_col.get("단위") or base_row.get("단위"), "일위대가": base_col.get("일위대가") or base_row.get("일위대가"), - "자재조합": um_key, } column.pop("본뜬열", None) + column.pop("자재조합", None) # 옛 자재 조합 칸은 안 옮김 if origin: column["본뜬"] = origin tree._place(doc, column) # 같은 앞마디 형제 곁(제목행 병합이 이어지게) diff --git a/M02_MasterTemplete/M02_Item_Snapshot.py b/M02_MasterTemplete/M02_Item_Snapshot.py index 3cb25ea8d..e64d83e4a 100644 --- a/M02_MasterTemplete/M02_Item_Snapshot.py +++ b/M02_MasterTemplete/M02_Item_Snapshot.py @@ -1,6 +1,6 @@ """구조물 집계 항목 복사본 — 프로젝트 `templates/structures/{구조물 id}/` (PLAN 25-2b · 26-2b). -복사본은 **구조물마다 하나** — 같은 항목을 고른 구조물 둘도 각자 도면 · 면적 · 재료 · 자재조합 · +복사본은 **구조물마다 하나** — 같은 항목을 고른 구조물 둘도 각자 도면 · 면적 · 재료 · 일위대가 · 입력연결 · 입력값 · 고른자재 · 단가를 가짐(길이 · 소재 · 높이 · 옵션이 구조물마다 다름). 폴더 이름 = 구조물 id(`structure_id`) · 관 지점은 `pipe@측점`. diff --git a/M02_MasterTemplete/M02_Item_Source.py b/M02_MasterTemplete/M02_Item_Source.py index fbda9ce76..a03b87eba 100644 --- a/M02_MasterTemplete/M02_Item_Source.py +++ b/M02_MasterTemplete/M02_Item_Source.py @@ -2,7 +2,7 @@ 모으는 것 = `{파일 이름: 내용}`: `집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `재료.json` SM · - `자재조합.json` UM 줄 · `일위대가.json` UA 줄 + 담은 로직 줄(M01 시스템) · + `일위대가.json` UA 줄 + 담은 로직 줄(M01 시스템) · `입력연결.json` 층의 `inputs/<열id>.json`(없으면 빈 연결) · `단가.json` 계산에 닿는 마스터 줄 전부 `원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎. @@ -20,7 +20,6 @@ from typing import Any from M02_MasterTemplete import M02_Template_Layers as layers TABLE_NAME = "구조물집계표" -UM_FILE = "자재조합.json" #: 열 id → 스냅샷 파일(구조물 도면 · 면적산출 · 재료산출) PART_FILES = (("structure", "도면.json"), ("area", "면적.json"), ("material", "재료.json")) LINK_FILE = "입력연결.json" @@ -138,10 +137,6 @@ def build_source(item: dict[str, Any]) -> dict[str, Any]: part = read_json(root / kind / f"{column_id}.json") if column_id else None if part is not None: files[name] = part - if row.get("자재조합"): - um = row_of(read_json(root / UM_FILE), str(row["자재조합"])) - if um is not None: - files[UM_FILE] = um labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None if labor is not None: files[LABOR_FILE] = labor diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 0ef688765..012a60ee2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -207,72 +207,13 @@ export const branchCombo = ( ): Promise => scoped(`/structures/${enc(column)}/combo`, { method: "POST", body: JSON.stringify(body) }); -/** 있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 · 자재조합 키만 바꿈(조합은 그대로 · `새로만듦` false) */ +/** 있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 키만 바꿈(조합은 그대로 · `새로만듦` false) */ export const setColumnCombo = ( column: string, - body: { 종류: "일위대가" | "자재조합"; 키: string; 판?: string }, + body: { 종류: "일위대가"; 키: string; 판?: string }, ): Promise => scoped(`/structures/${enc(column)}/use-combo`, { method: "POST", body: JSON.stringify(body) }); -/** 자재 조합(키 `UM`) 담은 자재 한 줄 */ -export interface MaterialItem { - 코드: string; - 이름: string | null; - 규격: string | null; - 단위: string | null; -} - -/** 자재 조합 한 줄 — 본뜬키 = 갈라 낸 원래 키(없으면 null) */ -export interface MaterialCombo { - 키: string; - 이름: string; - 담은자재: MaterialItem[]; - 본뜬키: string | null; - 소유: string; - 비고: string | null; -} - -export const fetchMaterialCombo = (key: string): Promise => - scoped(`/material-combos/${enc(key)}`); - -/** 이 열의 자재 조합을 고친 담은 자재로 갈라 냄 — 규칙은 `branchCombo` 와 같음 · - * `본뜬키` 빈 글 = 이 열의 첫 자재 조합 */ -export const branchMaterialCombo = ( - column: string, - body: { 본뜬키?: string; 담은자재: MaterialItem[]; 이름: string; 판?: string }, -): Promise => - scoped(`/structures/${enc(column)}/material-combo`, { - method: "POST", - body: JSON.stringify(body), - }); - -/* --- 자재 조합 표(마스터 템플릿 `자재조합.json`) — 좌측 「자재 조합」 컨테이너 --- */ -/** 표 통째 — 판(파일 판본) · 줄(담은 자재 포함) · 쓰는곳(키 → 쓰는 구조물 열 이름들) */ -export const listMaterialCombos = (): Promise<{ - 판: string; - 줄: MaterialCombo[]; - 쓰는곳: Record; -}> => scoped("/material-combos"); - -/** 새로 · 본떠 만들기(`본뜬키`) — 키는 서버가 표 「다음」 번호로 줌 */ -export const createMaterialCombo = ( - 판: string, - 줄: Omit, -): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> => - scoped("/material-combos", { method: "POST", body: JSON.stringify({ 판, 줄 }) }); - -/** 고치기 — 이름 · 비고 · 담은자재(본뜬키 · 소유는 서버가 있던 것으로) */ -export const editMaterialCombo = ( - 키: string, - 판: string, - 줄: MaterialCombo, -): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> => - scoped(`/material-combos/${enc(키)}`, { method: "PUT", body: JSON.stringify({ 판, 줄 }) }); - -/** 지우기 — 구조물 열이 쓰면 서버가 막고 그 열 이름들을 까닭으로 줌 */ -export const deleteMaterialCombo = (키: string, 판: string): Promise => - scoped(`/material-combos/${enc(키)}?판=${enc(판)}`, { method: "DELETE" }); - /* --- 구조물 옵션 보이기(마스터 템플릿 `구조물옵션표시.json`) — 좌측 「구조물 옵션 보이기」 줄 --- */ export interface OptionKind { type_id: string; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Book.py b/M02_MasterTemplete/M02_MasterTemplete_Book.py index 7cab20740..f87b7f2b4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Book.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Book.py @@ -7,7 +7,7 @@ 개인에 저장 · 프로젝트에서 새 항목은 그 층의 새 키(본뜬 곳은 열 `본뜬`). 회사 · 개인 층에 남은 옛 `SS` 키는 맞출 때 새 모양으로 옮기고(숫자 여섯 그대로 · 겹치면 다음 번호) 옛 키는 줄 `옛키` 에 남김 — 프로젝트 복사본이 들고 있는 옛 키로도 찾음(`find`). -줄 = `{키, 열, 경로, 이름, 종류, 분류1~3(경로 앞 넷 — 옛 코드가 읽음), 단위, 일위대가, 자재조합, +줄 = `{키, 열, 경로, 이름, 종류, 분류1~3(경로 앞 넷 — 옛 코드가 읽음), 단위, 일위대가, 도번, 면적번, 재료번, 바인딩, 펼침, 옛키?}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 — 저장소는 `store.sync_combos` · `store.read_combos` · `store.find_combo` 이름으로 이 파일을 부름. """ @@ -75,7 +75,6 @@ def _row( **tree.old_fields(path), "단위": col.get("단위"), "일위대가": col.get("일위대가"), - "자재조합": col.get("자재조합"), **{field: numbers.get(field, "") for _, field, _, _ in store.PARTS}, "바인딩": col.get("바인딩"), "펼침": col.get("펼침"), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Branch.py b/M02_MasterTemplete/M02_MasterTemplete_Branch.py index 8d67f3405..a483f3163 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Branch.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Branch.py @@ -2,7 +2,6 @@ 가리키게 함(PLAN 15장 · 검토 `14_조합편집검토.md` 1장). · 일위대가 조합(UA · UC · UP) — 새 줄은 지금 층 조합 저장소가 만듦(`Combo_Store` · PLAN 27-2c: 마스터 층 = UA · 회사 층 = UC · 개인 층 = UP · 고르기는 세 층 중 이 층이 쓸 수 있는 것) - · 자재 조합(UM · 마스터 템플릿 `자재조합.json`) — M02 자재 조합 저장소가 만듦(표 「다음」 번호) 조합은 여러 열 · 모든 프로젝트가 같이 씀 — 제자리 고치지 않음. 담은 것(차례 · 메모까지)이 이미 있는 조합과 같으면 새로 안 만들고 그 키(원래 키 먼저) · 새 줄의 본뜬키 = 원래 키 · 소유 현장. @@ -13,9 +12,7 @@ from __future__ import annotations from typing import Any, Callable from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos -from M02_MasterTemplete import M02_MasterTemplete_Matset as matset from M02_MasterTemplete import M02_MasterTemplete_Store as store -from M02_MasterTemplete import M02_MasterTemplete_Tree as tree _OWNER = "현장" @@ -41,7 +38,7 @@ def _branch( name: str, ) -> dict[str, Any]: """집계표 판 검사 → 같은 내용 찾기 · 없으면 `make(이름, 줄들)` 로 새 줄 → 집계표 그 열 `field`. - `slot` = 담은 칸(담은로직 · 담은자재) · `ident` = 담은 줄에서 견주는 칸(로직 · 메모 / 코드).""" + `slot` = 담은 칸(담은로직) · `ident` = 담은 줄에서 견주는 칸(로직 · 메모).""" table = _table(column, version) if not isinstance(items, list) or not items: raise store.StoreError(400, "담은 것이 비었음") @@ -84,11 +81,9 @@ def _point(table: dict, column: str, field: str, key: str) -> str: def use_combo(column: str, field: str, key: str, version: str = "") -> dict[str, Any]: - """있는 조합을 이 열에 씀 — `field` 일위대가(UA) · 자재조합(UM) · 조합은 그대로(고르기만).""" + """있는 조합을 이 열에 씀 — `field` 일위대가(UA) · 조합은 그대로(고르기만).""" if field == "일위대가": name = _combo(key).get("이름") - elif field == "자재조합": - name = material_combo(key).get("이름") else: raise store.StoreError(400, f"모르는 조합 종류 「{field}」") 판 = _point(_table(column, version), column, field, key) @@ -133,31 +128,3 @@ def branch_combo( return _branch( column, version, "일위대가", rows, "담은로직", ("로직", "메모"), items, source, make, name ) - - -def material_combo(key: str) -> dict[str, Any]: - return matset.one(key) - - -def branch_material_combo( - column: str, source: str, items: list[dict], name: str, version: str = "" -) -> dict[str, Any]: - """자재 조합 갈라 내기 — `source` 가 빈 글이면 이 열의 첫 자재 조합(본뜬키 null).""" - was = material_combo(source) if source else {} - items = [ - {k: x.get(k) for k in ("코드", "이름", "규격", "단위")} - for x in items or [] - if isinstance(x, dict) - ] - - def make(made_name: str, _rows) -> str: - row = {"이름": made_name, "담은자재": items, "본뜬키": source or None, "소유": _OWNER} - return matset.create(row)["키"] - - heads = next((c for c in store._structure_columns() if c["id"] == column), {}) - default = tree.name_of(tree.path_of(heads)) if heads else column # 경로 이음(26-1) - name = name.strip() or str(was.get("이름") or default or column) - rows = matset.listing()["줄"] - return _branch( - column, version, "자재조합", rows, "담은자재", ("코드",), items, source, make, name - ) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo.css b/M02_MasterTemplete/M02_MasterTemplete_Combo.css index e2053362a..e51b4b0db 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Combo.css @@ -73,45 +73,3 @@ margin: 0; line-height: 1.5; } - -/* 재료산출 — 오른쪽 조합 패널 「이 열의 조합」 칸: 담은 자재 표 */ -.m02-mat { - display: flex; - flex-direction: column; - gap: var(--spacing-8); - font-size: var(--text-body-sm); -} - -/* 패널 폭이 좁아도 줄이 넘치지 않게 — 이름 · 규격 칸이 줄어듦 */ -.m02-mat .m02-mat__row { - grid-template-columns: 5.5rem minmax(0, 1.4fr) minmax(0, 1fr) 2.5rem 3rem auto; -} - -.m02-mat__title { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-4) var(--spacing-12); - color: var(--color-text); -} - -.m02-mat__row { - display: grid; - grid-template-columns: 5.5rem minmax(6rem, 1.4fr) minmax(4rem, 1fr) 3rem 4rem auto; - align-items: center; - gap: var(--spacing-4); - padding: var(--spacing-4) 0; - border-bottom: 1px solid var(--color-border); - overflow-wrap: anywhere; -} - -.m02-mat__row--head { - color: var(--color-text-secondary); - font-size: var(--text-caption); - font-weight: 600; -} - -.m02-mat__foot { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-8); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo.ts b/M02_MasterTemplete/M02_MasterTemplete_Combo.ts index c11aa148a..02b0cc559 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Combo.ts @@ -3,7 +3,6 @@ * 면적산출 편집 화면 — 오른쪽 조합 패널 「이 열의 조합」 칸의 조합 줄 + [조합 고치기](시스템 관리자만). * 일위대가 조합(UA)은 여러 열 · 모든 프로젝트가 같이 씀 → 제자리 고치지 않고 고친 내용으로 새 조합을 만들어 * 이 열만 가리킴(서버 `branchCombo`) · 원래 조합은 그대로. 담은 로직 표는 M01 조합 화면과 같은 줄 모양. - * 갈라 내기 모달(`askBranch`)은 재료산출 자재 목록(`M02_MasterTemplete_Materials.ts`)도 씀. * ========================================================================== */ import { diff --git a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts b/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts index 77b43e1d1..05c6613c2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts @@ -1,11 +1,11 @@ /* ============================================================================= * M02_MasterTemplete_ComboPanel.ts - * 면적산출 · 재료산출 편집 화면 오른쪽 안쪽 패널 — 좌측 패널과 같은 공용 틀(사이드바 카드 · 접기) · + * 면적산출 편집 화면 오른쪽 안쪽 패널 — 좌측 패널과 같은 공용 틀(사이드바 카드 · 접기) · * 공용 폭 손잡이(`ui_template_resizer` · 폭 기억 · 최소 폭) · 공용 접기 손잡이(`ui_template_overlay`). - * · 「이 열의 조합」 — 면적 = 일위대가 조합 줄 + [조합 고치기](`M02_MasterTemplete_Combo.ts`) · - * 재료 = 담은 자재 표(`M02_MasterTemplete_Materials.ts`) — 넣는 쪽이 `setMine` 으로 갈아 끼움 - * · 조합 목록 — 면적 = 일위대가 조합(이 층이 쓸 수 있는 마스터 UA · 회사 UC · 개인 UP · 27-2c) · - * 재료 = 자재 조합(UM) · 찾기 · 이 열 조합 강조 · + * · 「이 열의 조합」 — 일위대가 조합 줄 + [조합 고치기](`M02_MasterTemplete_Combo.ts`) · + * 넣는 쪽이 `setMine` 으로 갈아 끼움 + * · 조합 목록 — 일위대가 조합(이 층이 쓸 수 있는 마스터 UA · 회사 UC · 개인 UP · 27-2c) · + * 찾기 · 이 열 조합 강조 · * 하나 골라 [이 열에 쓰기] = 집계표 그 열 키만 바꿈(서버 `use-combo` · 조합은 그대로) * ========================================================================== */ @@ -21,7 +21,7 @@ import { createWorkflowPanelHandle } from "@ui/ui_template_overlay"; import { createPanelResizer } from "@ui/ui_template_resizer"; import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state"; import { fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api"; -import { getActiveLayer, listMaterialCombos, setColumnCombo } from "./M02_MasterTemplete_Api_Fetch"; +import { getActiveLayer, setColumnCombo } from "./M02_MasterTemplete_Api_Fetch"; import { readColumnBase, reasonOf } from "./M02_MasterTemplete_Combo_Api"; import "./M02_MasterTemplete_ComboPanel.css"; @@ -30,10 +30,8 @@ const TEXT = { mine: "이 열의 조합", noneArea: "이 열에 일위대가 조합 없음", listArea: "일위대가 조합 목록", - listMaterial: "자재 조합 목록", search: "키 · 이름으로 찾기", logics: "로직 {n}", - items: "자재 {n}", origin: "본뜬 {value}", current: "이 열", empty: "맞는 조합 없음", @@ -61,7 +59,7 @@ interface Row { } export interface ComboPanelOptions { - kind: "area" | "material"; + kind: "area"; /** 집계표 열 id */ column: string; isAdmin: boolean; @@ -71,7 +69,7 @@ export interface ComboPanelOptions { export interface ComboPanel { root: HTMLElement; - /** 「이 열의 조합」 부분을 갈아 끼움(면적 = 조합 줄 · 재료 = 담은 자재 표) */ + /** 「이 열의 조합」 부분을 갈아 끼움(조합 줄) */ setMine: (node: HTMLElement | null) => void; /** 목록 · 이 열 조합 키를 다시 받음(갈라 낸 뒤 등) */ refresh: () => Promise; @@ -79,7 +77,6 @@ export interface ComboPanel { } export function buildComboPanel(opts: ComboPanelOptions): ComboPanel { - const area = opts.kind === "area"; let rows: Row[] = []; let current: string | null = null; let picked: string | null = null; @@ -97,10 +94,7 @@ export function buildComboPanel(opts: ComboPanelOptions): ComboPanel { const use = createButton({ label: TEXT.use, variant: "filled", size: "sm" }); const body = el("div", { className: "m02-cpanel__body ui-sidebar-panel", - children: [ - card(TEXT.mine, mine), - card(area ? TEXT.listArea : TEXT.listMaterial, find.root, list), - ], + children: [card(TEXT.mine, mine), card(TEXT.listArea, find.root, list)], }); attachCollapsible(body); @@ -138,7 +132,7 @@ export function buildComboPanel(opts: ComboPanelOptions): ComboPanel { ...(shown.length ? shown.map((r) => { const meta = [ - fill(area ? TEXT.logics : TEXT.items, r.수), + fill(TEXT.logics, r.수), r.본뜬키 ? fill(TEXT.origin, r.본뜬키) : "", r.키 === current ? TEXT.current : "", ]; @@ -177,26 +171,17 @@ export function buildComboPanel(opts: ComboPanelOptions): ComboPanel { try { const [base, got] = await Promise.all([ readColumnBase(opts.column), - area - ? fetchUsableCombos(getActiveLayer()).then((cs) => - cs.map((c) => ({ - 키: c.키, - 이름: c.이름, - 수: c.count, // 목록 요약엔 담은로직이 없음 — 수만 옴 - 본뜬키: c.본뜬키 ?? null, - })), - ) - : listMaterialCombos().then((d) => - d.줄.map((m) => ({ - 키: m.키, - 이름: m.이름, - 수: m.담은자재.length, - 본뜬키: m.본뜬키, - })), - ), + fetchUsableCombos(getActiveLayer()).then((cs) => + cs.map((c) => ({ + 키: c.키, + 이름: c.이름, + 수: c.count, // 목록 요약엔 담은로직이 없음 — 수만 옴 + 본뜬키: c.본뜬키 ?? null, + })), + ), ]); if (mineSeq !== seq) return; - current = (area ? base.일위대가 : base.자재조합) ?? null; + current = base.일위대가 ?? null; rows = got; } catch (error) { if (mineSeq !== seq) return; @@ -213,7 +198,7 @@ export function buildComboPanel(opts: ComboPanelOptions): ComboPanel { use.disabled = true; try { const got = await setColumnCombo(opts.column, { - 종류: area ? "일위대가" : "자재조합", + 종류: "일위대가", 키: row.키, }); showToast(fill(TEXT.used, `${got.키} ${got.이름}`), "success"); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Combo_Api.ts index ffddd5d11..a352532ad 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Combo_Api.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_Combo_Api.ts - * 조합 고치기 · 자재 목록 화면이 함께 쓰는 읽기 — 집계표 열이 가리키는 조합 키 · 같이 쓰는 다른 열 수 · - * 서버 까닭 글. 갈라 내기 길(`branchCombo` · `branchMaterialCombo` · `fetchMaterialCombo`)은 `M02_MasterTemplete_Api_Fetch.ts`. + * 조합 고치기 화면이 쓰는 읽기 — 집계표 열이 가리키는 조합 키 · 같이 쓰는 다른 열 수 · + * 서버 까닭 글. 갈라 내기 길(`branchCombo`)은 `M02_MasterTemplete_Api_Fetch.ts`. * ========================================================================== */ import { getActiveLayer, readTemplate } from "./M02_MasterTemplete_Api_Fetch"; @@ -12,17 +12,15 @@ const STRUCTURE_TABLE = "구조물집계표"; /** 집계표 열이 가리키는 조합 키 + 같은 조합을 쓰는 다른 열 수 */ export interface ColumnBase { 일위대가: string | null; - 자재조합: string | null; 다른열일위대가: number; - 다른열자재조합: number; } /** 서버가 준 까닭 글 */ export const reasonOf = (error: unknown): string => (error instanceof Error ? error.message : ""); -/** 집계표(시스템 층)에서 이 열의 일위대가 · 자재조합 키와 같이 쓰는 다른 열 수 */ +/** 집계표(시스템 층)에서 이 열의 일위대가 키와 같이 쓰는 다른 열 수 */ export async function readColumnBase(column: string): Promise { - type Col = { id: string; 일위대가?: string | null; 자재조합?: string | null }; + type Col = { id: string; 일위대가?: string | null }; const table = await readTemplate(getActiveLayer(), "table", STRUCTURE_TABLE, null).catch( () => null, ); @@ -32,8 +30,6 @@ export async function readColumnBase(column: string): Promise { key ? cols.filter((c) => c.id !== column && pick(c) === key).length : 0; return { 일위대가: mine?.일위대가 || null, - 자재조합: mine?.자재조합 || null, 다른열일위대가: same(mine?.일위대가, (c) => c.일위대가), - 다른열자재조합: same(mine?.자재조합, (c) => c.자재조합), }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Items.py index 90f0dacd7..f2c02e93a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Items.py @@ -1,6 +1,6 @@ """M02 층 집계표 항목 — 회사 · 개인 층 빈 뼈대 · 항목 목록 · 항목 복사(PLAN 21-1 · 21-2). -항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) · 자재조합(UM) · +항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) · 도번 SD · 면적번 SA · 재료번 SM. 층 폴더(시스템 · 회사 · 개인)는 모양이 같아 저장소 함수를 `store.at(층 폴더)` 안에서 그대로 부름. 권한은 부르는 쪽(`M02_MasterTemplete_Router_Items.py`) 몫 — 여기는 폴더와 파일만. @@ -15,7 +15,6 @@ from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store -from M02_MasterTemplete import M02_MasterTemplete_Matset as matset from M02_MasterTemplete import M02_MasterTemplete_Store as store # 21-1 목록 칸 — SS 줄에서 그대로(키는 `key`) @@ -29,7 +28,6 @@ ITEM_FIELDS = ( "분류3", "단위", "일위대가", - "자재조합", "도번", "면적번", "재료번", @@ -38,7 +36,7 @@ ITEM_FIELDS = ( def ensure(folder: Path) -> bool: """회사 · 개인 층 첫 진입 — 빈 집계표(열 0 · 층 이름 · 줄 머리는 시스템 것) · - 조합 장부(`다음` 1) · 자재 조합 빈 표 · 종류 폴더. + 조합 장부(`다음` 1) · 종류 폴더. 이미 집계표가 있으면 아무것도 안 함(시스템 층은 늘 있음).""" table = Path(folder) / "table" / f"{store.STRUCTURE_TABLE}.json" if table.is_file(): @@ -59,28 +57,23 @@ def ensure(folder: Path) -> bool: return False atomic_write_json(table, doc) store.sync_combos() - mat = Path(folder) / matset.FILE - if not mat.is_file(): - atomic_write_json(mat, matset._load()[0]) return True def items(folder: Path) -> list[dict[str, Any]]: - """그 층 항목 — SS 줄마다 `{key, 열, 경로, 이름, 종류, 분류1~3, 단위, 일위대가, 자재조합, + """그 층 항목 — SS 줄마다 `{key, 열, 경로, 이름, 종류, 분류1~3, 단위, 일위대가, 도번, 면적번, 재료번, names}` · 경로 = 폴더 이름 배열 · 이름 = 경로 이음(26-1 · 종류 · 분류1~3 은 경로 앞 넷) · `names` = 화면 이름(키 대신 · 없으면 null) — - 일위대가 = 조합 이름 · 자재조합 = 그 층 자재 조합 이름 · + 일위대가 = 조합 이름 · 도번 · 면적번 · 재료번 = 항목(열) 이름.""" with store.at(folder): ua = combos.names() # 마스터 · 이 층이 쓸 수 있는 회사 · 개인 조합(27-2c) rows = store.read_combos().get("줄", []) - um = {str(r.get("키")): str(r.get("이름") or "") for r in matset.listing()["줄"]} out = [] for r in rows: head = r.get("이름") names = { "일위대가": ua.get(str(r.get("일위대가"))) if r.get("일위대가") else None, - "자재조합": um.get(str(r.get("자재조합"))) if r.get("자재조합") else None, **{ k: (head or r.get("열")) if r.get(k) else None for k in ("도번", "면적번", "재료번") }, @@ -104,7 +97,7 @@ def copy_item( path: list[str] | None = None, ) -> dict[str, Any]: """다른 층 항목(SS `key`) → `target` 층 새 열 하나 — 머리 · 단위 · 일위대가 · 바인딩 그대로 · - 도면 · 면적산출 · 재료산출 내용(알약 연결 포함) · 항목 연결 · 자재 조합 줄(새 UM) 복사 · + 도면 · 면적산출 · 재료산출 내용(알약 연결 포함) · 항목 연결 복사 · 새 번호 · 새 SS 키 · 열 `본뜬` = `origin`(원래 층 · 주인 · 키) · `path` = 새 자리(폴더 + 이름 · 없으면 원래 경로 · 붙여넣기 PLAN 30-2). 돌려줌 `{key, 열, 이름}`.""" @@ -120,28 +113,13 @@ def copy_item( docs[kind] = store.read(kind, col_id)["문서"] except store.StoreError: pass # 없는 파일은 새 층에서 빈 문서로 섬 - um = None - if col.get("자재조합"): - try: - um = matset.one(col["자재조합"]) - except store.StoreError: - um = None # 가리키던 줄이 없으면 비움 ensure(target) with store.at(target): - new_um = None - if um: - names = matset.listing()["줄"] - taken = {str(r.get("이름") or "") for r in names} - name, n = str(um.get("이름") or key), 2 - base = name - while name in taken: - name, n = f"{base} ({n})", n + 1 - got = {k: um.get(k) for k in ("담은자재", "소유", "비고")} - new_um = matset.create({**got, "이름": name, "본뜬키": None})["키"] table = store.read("table", store.STRUCTURE_TABLE) doc = table["문서"] new_id = _fresh_id(col_id, {c.get("id") for c in doc.get("열", []) if isinstance(c, dict)}) - made = {**json.loads(json.dumps(col)), "id": new_id, "자재조합": new_um, "본뜬": origin} + made = {**json.loads(json.dumps(col)), "id": new_id, "본뜬": origin} + made.pop("자재조합", None) # 옛 자재 조합 칸은 안 옮김 if path: made.update({"경로": list(path), "머리": list(path)}) # 머리 줄 수는 저장 맞춤이 채움 doc["열"] = [*doc.get("열", []), made] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Materials.ts b/M02_MasterTemplete/M02_MasterTemplete_Materials.ts deleted file mode 100644 index 93697e6fb..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Materials.ts +++ /dev/null @@ -1,253 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Materials.ts - * 재료산출 편집 화면 — 엑셀 옆 접히는 판: 그 열의 자재 조합(UM) 담은 자재 표 + [자재 더하기] · [빼기] · [적용]. - * 자재 조합도 여러 열이 같이 쓸 수 있어 [적용] 때 일위대가처럼 모달 후 새 조합으로 갈라 냄 - * (서버 `branchMaterialCombo`) · 원래 조합은 그대로. 빼는 자재가 칸에 이어져 있으면 확인 후 연결도 풂. - * ========================================================================== */ - -import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; -import type { LinkItem } from "../A00_Common/spreadsheet/spreadsheet_links"; -import type { CellLink } from "../A00_Common/spreadsheet/spreadsheet_types"; -import { - branchMaterialCombo, - fetchMaterialCombo, - type MaterialCombo, - type MaterialItem, -} from "./M02_MasterTemplete_Api_Fetch"; -import { pickMaterial } from "./M02_Material_Pick"; -import { askBranch, branchName, branchToast } from "./M02_MasterTemplete_Combo"; -import { readColumnBase, reasonOf } from "./M02_MasterTemplete_Combo_Api"; - -const TEXT = { - combo: "자재 조합", - branched: "본뜬 조합 {value}", - none: "자재 조합 없음 — 자재를 더하면 새로 만듦", - code: "코드", - name: "이름", - spec: "규격", - unit: "단위", - cell: "이은 칸", - remove: "빼기", - add: "자재 추가", - apply: "적용", - undo: "되돌리기", - empty: "담은 자재가 없음", - noItems: "담은 자재가 없어 적용 안 됨 — 자재를 하나 이상 담음", - already: "이미 담은 자재 — {value}", - unlinkAsk: "{value} 자재는 {cell} 칸에 이어져 있음 — 빼면 연결도 풂", - unlinkOk: "빼고 연결 풂", - loadFailed: "자재 조합을 못 읽음 — {value}", - askBody: - "이 자재 조합은 열 {n} 개와 모든 프로젝트가 같이 씀 — 고친 내용으로 새 자재 조합을 만들고 이 열만 새 조합을 가리킴 · 같은 내용이면 있는 조합을 씀 · 원래 조합은 그대로", - askFirst: - "이 열에 새 자재 조합을 만듦 — 담은 자재 목록으로 새 조합을 만들고 이 열이 가리킴 · 같은 내용의 자재 조합이 있으면 그것을 씀", - askAlone: - "이 자재 조합은 모든 프로젝트가 같이 씀 — 고친 내용으로 새 자재 조합을 만들고 이 열만 새 조합을 가리킴 · 같은 내용이면 있는 조합을 씀", - failed: "자재 조합을 못 고침 — {value}", -}; - -const fill = (text: string, values: Record): string => - Object.entries(values).reduce((s, [k, v]) => s.replace(`{${k}}`, String(v)), text); - -/** 이 열의 자재 조합 — 키(없으면 빈 글) · 같이 쓰는 다른 열 수 · 담은 자재 */ -export interface MaterialState { - key: string; - others: number; - combo: MaterialCombo | null; -} - -export async function loadMaterialState(column: string): Promise { - const base = await readColumnBase(column); - const key = base.자재조합 ?? ""; - let combo: MaterialCombo | null = null; - if (key) { - try { - combo = await fetchMaterialCombo(key); - } catch (error) { - showToast(fill(TEXT.loadFailed, { value: reasonOf(error) }), "error"); - } - } - return { key, others: base.다른열자재조합, combo }; -} - -/** 알약 막대 자재 항목 ↔ 담은 자재 — 알약 키 = 자재 코드 */ -export const toPills = (items: MaterialItem[]): LinkItem[] => - items.map((m) => { - const 자재 = { - 코드: m.코드, - 이름: m.이름 ?? "", - 규격: m.규격 ?? "", - 단위: m.단위 ?? "", - }; - return { 키: m.코드, 이름: 자재.이름, 단위: 자재.단위, 자재 }; - }); - -export interface MaterialPanel { - root: HTMLElement; - /** 알약 막대에서 목록이 바뀜 — 표만 다시 그림(알약은 이미 맞음) */ - setItems: (items: MaterialItem[]) => void; - /** 연결이 바뀜(이음 · 풂 · 되돌리기) — 이은 칸을 다시 그림 */ - refresh: () => void; -} - -export interface MaterialPanelOptions { - /** 집계표 열 id */ - column: string; - state: MaterialState; - /** 표에서 더하기 · 빼기 · 되돌리기 — 알약 막대를 이 목록으로 맞춤 */ - onChange: (items: MaterialItem[]) => void; - /** 이은 칸이 있는 줄을 누름 — 알약을 눌러 그 칸으로 감(깜빡임) */ - goTo: (code: string) => void; - /** 구조물 이름 — 새 이름 자동 미리 채움 */ - label: string; - isAdmin: boolean; - /** 지금 시트 문서의 연결 전부 */ - links: () => CellLink[]; - /** 자재 코드에 이어진 칸 연결을 풂(시트 문서를 고치고 고침 표시) */ - unlink: (codes: string[]) => void; - /** [적용]으로 이 열이 새 · 다른 자재 조합을 가리킴 — 조합 목록 다시 받기 */ - onApplied?: () => void; -} - -/** 옆 판 — 이 열에 자재 조합이 없어도 판은 뜸(더하면 새로 만듦) */ -export function buildMaterialPanel(opts: MaterialPanelOptions): MaterialPanel { - let key = opts.state.key; - let others = opts.state.others; - let combo = opts.state.combo; - let saved: MaterialItem[] = combo?.담은자재 ?? []; - let items: MaterialItem[] = [...saved]; - const sig = (list: MaterialItem[]): string => JSON.stringify(list.map((m) => m.코드)); - - const title = el("div", { className: "m02-mat__title" }); - const table = el("div", { className: "m02-mat__table" }); - const foot = el("div", { className: "m02-mat__foot" }); - // 오른쪽 조합 패널의 「이 열의 조합」 칸에 들어감(접기 · 폭은 패널이 함) - const root = el("div", { className: "m02-mat", children: [title, table, foot] }); - - const cellsOf = (code: string): string[] => - opts - .links() - .filter((l) => (l.자재?.코드 ?? l.키) === code) - .map((l) => l.칸); - - const paint = (): void => { - title.replaceChildren( - el("span", { text: key ? `${TEXT.combo} ${key} ${combo?.이름 ?? ""}`.trim() : TEXT.none }), - ...(opts.isAdmin && combo?.본뜬키 - ? [ - el("span", { - className: "m02-combo__origin", - text: fill(TEXT.branched, { value: combo.본뜬키 }), - }), - ] - : []), - ); - const head = el("div", { - className: "m02-mat__row m02-mat__row--head", - children: [TEXT.code, TEXT.name, TEXT.spec, TEXT.unit, TEXT.cell, ""].map((t) => - el("span", { text: t }), - ), - }); - const rows = items.map((m, i) => { - const cells = cellsOf(m.코드); - const acts = el("span", { className: "m02-mat__acts" }); - if (opts.isAdmin) { - const del = createButton({ label: TEXT.remove, variant: "ghost", size: "sm" }); - del.addEventListener("click", async () => { - if ( - cells.length && - !(await showConfirmDialog( - fill(TEXT.unlinkAsk, { value: m.이름 ?? m.코드, cell: cells.join(", ") }), - TEXT.unlinkOk, - )) - ) - return; - items.splice(i, 1); - if (cells.length) opts.unlink([m.코드]); // 확인 글대로 바로 풂(알약 막대 「목록에서 빼기」와 같음) - paint(); - opts.onChange(items); - }); - acts.append(del); - } - const row = el("div", { - className: `m02-mat__row${cells.length ? " is-linked" : ""}`, - attrs: { "data-material": m.코드 }, - children: [m.코드, m.이름 ?? "", m.규격 ?? "", m.단위 ?? "", cells.join(", ")] - .map((t) => el("span", { text: t })) - .concat(acts), - }); - if (cells.length) row.addEventListener("click", () => opts.goTo(m.코드)); - return row; - }); - table.replaceChildren( - head, - ...(rows.length ? rows : [el("p", { className: "m02-struct__empty", text: TEXT.empty })]), - ); - paintFoot(); - }; - - const paintFoot = (): void => { - if (!opts.isAdmin) return foot.replaceChildren(); - const add = createButton({ label: TEXT.add, variant: "ghost", size: "sm" }); - add.addEventListener("click", () => - pickMaterial((m) => { - if (items.some((x) => x.코드 === m.코드)) - return showToast(fill(TEXT.already, { value: m.코드 }), "info"); - items.push(m); - paint(); - opts.onChange(items); - }), - ); - const buttons = [add]; - if (sig(items) !== sig(saved)) { - const apply = createButton({ label: TEXT.apply, variant: "filled", size: "sm" }); - apply.addEventListener("click", async () => { - if (!items.length) return showToast(TEXT.noItems, "error"); - const message = !key - ? TEXT.askFirst - : others - ? fill(TEXT.askBody, { n: others + 1 }) - : TEXT.askAlone; - const name = await askBranch(message, branchName(combo?.이름 ?? "", opts.label)); - if (name === null) return; - apply.disabled = true; - try { - const result = await branchMaterialCombo(opts.column, { - 본뜬키: key, - 담은자재: items, - 이름: name, - }); - branchToast(result, TEXT.combo); - key = result.키; - others = 0; - combo = await fetchMaterialCombo(key).catch(() => null); - saved = combo?.담은자재 ?? [...items]; - items = [...saved]; - paint(); - opts.onApplied?.(); - } catch (error) { - apply.disabled = false; - showToast(fill(TEXT.failed, { value: reasonOf(error) }), "error"); - } - }); - const undo = createButton({ label: TEXT.undo, variant: "ghost", size: "sm" }); - undo.addEventListener("click", () => { - items = [...saved]; - paint(); - opts.onChange(items); - }); - buttons.push(apply, undo); - } - foot.replaceChildren(...buttons); - }; - - paint(); - return { - root, - setItems: (list) => { - items = [...list]; - paint(); - }, - refresh: paint, - }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Matset.py b/M02_MasterTemplete/M02_MasterTemplete_Matset.py deleted file mode 100644 index 47784a504..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Matset.py +++ /dev/null @@ -1,150 +0,0 @@ -"""M02 자재 조합 저장소 — 마스터 템플릿 표 `자재조합.json`(구조물집계표조합.json 과 같은 자리). - -줄 = 키 UM · 이름 · 담은자재(코드 · 이름 · 규격 · 단위) · 본뜬키 · 소유 · 비고 — 수량 · 단가는 안 둠 -(재료산출 몫). 키는 표 안 「다음」 번호(지운 키는 다시 안 씀) · 집계표 열 `자재조합` 칸이 가리킴. -틀 검사 = `master_material_combo.check_form`(check_master 와 같은 것) · 판 = 파일 sha256 앞 16자. -""" - -from __future__ import annotations - -import json -from typing import Any - -from common_util.common_util_json import atomic_write_json -from M01_MasterData import M01_MasterData_Store as m01 -from M02_MasterTemplete import M02_MasterTemplete_Store as store -from M02_MasterTemplete import M02_MasterTemplete_Tree as tree - -mmc = m01.cm.mmc # 자재 조합 틀(`resources/master_data/scripts/master_material_combo.py`) -FILE = mmc.FILE -OWNERS = mmc.OWNERS -_HEAD = "UM" - - -def _load() -> tuple[dict[str, Any], str]: - """(표, 판) — 파일이 없으면 빈 표 · 판 빈 글.""" - path = store.root() / FILE - if not path.is_file(): - return {"그룹": mmc.GROUP, "판": "2026", "비고": [], "다음": 1, "줄": []}, "" - raw = path.read_bytes() - return json.loads(raw), store.version_of(raw) - - -def _find(doc: dict, key: str) -> dict[str, Any]: - row = next((r for r in doc["줄"] if r.get("키") == key), None) - if row is None: - raise store.StoreError(404, f"없는 자재 조합 「{key}」") - return row - - -def listing() -> dict[str, Any]: - """표 통째 — 판 · 줄(담은 자재 포함 · 작아서 한 번에) · 쓰는곳(키 → 쓰는 구조물 열 이름들).""" - doc, version = _load() - used: dict[str, list[str]] = {} - for col in store._structure_columns(): - if key := col.get("자재조합"): - used.setdefault(key, []).append(_name(col)) - return {"판": version, "줄": doc["줄"], "쓰는곳": used} - - -def _name(col: dict) -> str: - return tree.name_of(tree.path_of(col)) or col["id"] # 경로 이음(26-1) - - -def one(key: str) -> dict[str, Any]: - return _find(_load()[0], key) - - -def users_of(key: str) -> list[str]: - """구조물집계표에서 `자재조합` 칸이 이 키인 열 — 머리 글(없으면 열 id).""" - out = [] - for col in store._structure_columns(): - if col.get("자재조합") == key: - out.append(_name(col)) - return out - - -def _clean(row: Any, key: str, keep: dict | None = None) -> dict[str, Any]: - """서버가 잡는 칸 — 키 · 열 한 벌 · 자재 줄 네 칸 · 본뜬키는 만들 때만(고칠 때는 있던 것).""" - if not isinstance(row, dict): - raise store.StoreError(400, "자재 조합 줄이 묶음 아님") - name = row.get("이름") - if not isinstance(name, str) or not name.strip(): - raise store.StoreError(400, "자재 조합 줄에 「이름」 이 없거나 글자 아님") - got = row.get("담은자재") - got = [ - {c: m.get(c) for c in mmc.ITEM_COLS} if isinstance(m, dict) else m - for m in (got if isinstance(got, list) else []) - ] - known = {str(r.get("키")) for d in m01.loaded()[0].values() for r in d.get("줄") or []} - lost = [str(m.get("코드")) for m in got if isinstance(m, dict) and m.get("코드") not in known] - if lost: - raise store.StoreError(422, {"errors": [f"마스터에 없는 자재 {' · '.join(lost)}"]}) - base = keep or row - owner = base.get("소유") or OWNERS[0] - return { - "키": key, - "이름": name.strip(), - "담은자재": got, - "본뜬키": base.get("본뜬키") or None, - "소유": owner, - "비고": row.get("비고") or None, - } - - -def _short(name: str, at: int = 30) -> str: - return name if len(name) <= at else name[:at] + "…" - - -def _check_name(rows: list, key: str, name: str) -> None: - """이름 겹침 · 긴 이름 — 틀 검사(파일명 · 키가 섞인 글) 앞에서 쉬운 말 한 줄로 막음.""" - if len(name) > mmc.MAX_이름: - raise store.StoreError(422, {"errors": [f"이름이 너무 김 — {mmc.MAX_이름}자까지"]}) - if any(r.get("키") != key and str(r.get("이름", "")).strip() == name for r in rows): - raise store.StoreError( - 422, {"errors": [f"「{_short(name)}」 이름의 자재 조합이 이미 있음 — 다른 이름으로"]} - ) - - -def _save(doc: dict, version: str) -> str: - """틀 검사 → 판이 같을 때만 씀 → 새 판.""" - if errors := mmc.check_form(FILE, doc): - raise store.StoreError(422, {"errors": errors}) - path = store.root() / FILE - with store._LOCK: - have = store.version_of(path.read_bytes()) if path.is_file() else "" - if have != (version or ""): - raise store.StoreError(409, {"stale": [FILE], "판": have}) - atomic_write_json(path, doc) - return store.version_of(path.read_bytes()) - - -def create(row: Any, version: str | None = None) -> dict[str, Any]: - """새 줄 — 키 = 「다음」 번호 · 본뜬키는 있는 다른 키만. `version` None = 지금 판(갈라 내기).""" - doc, have = _load() - key = f"{_HEAD}{int(doc.get('다음') or 1):06d}" - fixed = _clean(row, key) - if fixed["본뜬키"] and fixed["본뜬키"] not in {r.get("키") for r in doc["줄"]}: - raise store.StoreError(400, f"본뜬키 「{fixed['본뜬키']}」 — 있는 다른 자재 조합 키여야 함") - _check_name(doc["줄"], key, fixed["이름"]) - doc = {**doc, "다음": int(doc.get("다음") or 1) + 1, "줄": [*doc["줄"], fixed]} - return {"키": key, "판": _save(doc, have if version is None else version), "줄": fixed} - - -def edit(key: str, row: Any, version: str) -> dict[str, Any]: - doc, _ = _load() - fixed = _clean(row, key, keep=_find(doc, key)) - _check_name(doc["줄"], key, fixed["이름"]) - doc = {**doc, "줄": [fixed if r.get("키") == key else r for r in doc["줄"]]} - return {"키": key, "판": _save(doc, version), "줄": fixed} - - -def delete(key: str, version: str) -> dict[str, Any]: - """지우기 — 구조물 열이 쓰면 409 + 열 이름들(지우지 않음).""" - doc, _ = _load() - _find(doc, key) - if users := users_of(key): - msg = f"구조물 {len(users)} 곳이 쓰는 자재 조합이라 못 지움 — {' · '.join(users)}" - raise store.StoreError(409, {"errors": [msg], "users": users}) - doc = {**doc, "줄": [r for r in doc["줄"] if r.get("키") != key]} - return {"판": _save(doc, version)} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Matset.ts b/M02_MasterTemplete/M02_MasterTemplete_Matset.ts deleted file mode 100644 index 03f6367a6..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Matset.ts +++ /dev/null @@ -1,267 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Matset.ts - * 좌측 「자재 조합」 컨테이너 — 찾기 · 목록(키 맨 앞) · 새로 · 본떠 만들기 · 지우기(컨테이너 안) · - * 줄을 누르면 메인 칸에 상세: 이름 · 비고 · 본뜬키 · 담은 자재 표(빼기) · [자재 더하기] · [저장]. - * 표 = 마스터 템플릿 `자재조합.json`(키 UM · 서버 `M02_MasterTemplete_Matset.py`) · 지우기는 쓰는 열이 - * 있으면 서버가 막음. 재료산출 옆 판이 갈라 낸 조합도 같은 표라 여기 보임(펼칠 때 새로 받음). - * ========================================================================== */ - -import { - createButton, - createInputField, - el, - showConfirmDialog, - showToast, -} from "@ui/ui_template_elements"; -import { t as L } from "@ui/ui_template_locale"; -import { - createMaterialCombo, - deleteMaterialCombo, - editMaterialCombo, - listMaterialCombos, - StaleError, - type MaterialCombo, -} from "./M02_MasterTemplete_Api_Fetch"; -import { pickMaterial } from "./M02_Material_Pick"; -import { tm } from "./M02_MasterTemplete_Matset_Text"; - -export interface MatsetSectionOptions { - isAdmin: boolean; - /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ - confirmLeave: () => Promise; - /** 메인 칸에 상세를 띄움 — 고침 표시 손잡이를 돌려받음 */ - show: (label: string, node: HTMLElement) => (dirty: boolean) => void; - /** 메인 칸을 비움(지운 뒤) */ - close: () => void; - /** 상세를 열었음 — 좌측 다른 줄 고름 표시를 지움 */ - onOpen: () => void; -} - -export interface MatsetSection { - title: string; - body: HTMLElement; - /** 새로 · 본떠 만들기 · 지우기 줄(관리자만 · 아니면 빈 목록) */ - actions: HTMLElement[]; - /** 표를 새로 받아 목록을 다시 그림 */ - refresh: () => Promise; - /** 다른 양식을 열었음 — 고름 표시만 지움 */ - clear: () => void; -} - -const why = (error: unknown): string => (error instanceof Error ? error.message : ""); - -export function buildMatsetSection(opt: MatsetSectionOptions): MatsetSection { - let all: MaterialCombo[] = []; - let version = ""; - let used: Record = {}; - let q = ""; - /** 열린 상세의 키 — 새(저장 전)는 빈 글 · 안 열렸으면 null */ - let openKey: string | null = null; - - const find = createInputField({ type: "search", placeholder: tm("Search") }); - const list = el("div", { className: "m02-side__items ui-sidebar-list" }); - const body = el("div", { className: "m02-side__items", children: [find.root, list] }); - find.input.addEventListener("input", () => { - q = find.input.value.trim().toLowerCase(); - paintList(); - }); - - const btnNew = createButton({ label: L("M02_New"), variant: "filled", size: "sm" }); - const btnCopy = createButton({ label: L("M02_Copy"), variant: "ghost", size: "sm" }); - const btnDel = createButton({ label: L("M02_Delete"), variant: "danger", size: "sm" }); - const actions = opt.isAdmin - ? [el("div", { className: "m02-side__row", children: [btnNew, btnCopy, btnDel] })] - : []; - const paintButtons = (): void => { - btnCopy.disabled = btnDel.disabled = !openKey; - }; - - function paintList(): void { - const shown = all.filter((x) => !q || `${x.키} ${x.이름}`.toLowerCase().includes(q)); - list.replaceChildren( - ...(shown.length - ? shown.map((x) => { - const b = el("button", { - className: `m02-side__item ui-sidebar-row${x.키 === openKey ? " is-active" : ""}`, - text: `${x.키} ${x.이름}`, - attrs: { type: "button", "data-matset-key": x.키 }, - }); - b.addEventListener("click", () => void select(x.키)); - return b; - }) - : [el("p", { className: "m02-side__empty", text: tm("Empty") })]), - ); - } - - async function refresh(): Promise { - try { - ({ 판: version, 줄: all, 쓰는곳: used } = await listMaterialCombos()); - } catch (error) { - showToast(tm("Load_Failed", why(error)), "error"); - } - paintList(); - paintButtons(); - } - - async function select(key: string): Promise { - if (key === openKey || !(await opt.confirmLeave())) return; - const row = all.find((x) => x.키 === key); - if (row) draw(structuredClone(row), false); - } - - /** 메인 칸 상세 — `row` 는 고칠 사본(저장 전까지 표에 안 들어감) */ - function draw(row: MaterialCombo, isNew: boolean): void { - openKey = isNew ? "" : row.키; - const edit = opt.isAdmin; - const field = (label: string, name: "이름" | "비고") => { - const f = createInputField({ type: "text", label }); - f.input.value = row[name] ?? ""; - f.input.disabled = !edit; - f.input.setAttribute("data-matset-field", name); - f.input.addEventListener("input", () => { - if (name === "이름") row.이름 = f.input.value; - else row.비고 = f.input.value || null; - setDirty(true); - }); - return f.root; - }; - const table = el("div", { className: "m02-matset__table" }); - const paintItems = (): void => { - const head = el("div", { - className: "m02-mat__row m02-mat__row--head", - children: [tm("Col_Code"), tm("Col_Name"), tm("Col_Spec"), tm("Col_Unit"), "", ""].map( - (t) => el("span", { text: t }), - ), - }); - const rows = row.담은자재.map((m, i) => { - const acts = el("span"); - if (edit) { - const del = createButton({ label: tm("Remove"), variant: "ghost", size: "sm" }); - del.addEventListener("click", () => { - row.담은자재.splice(i, 1); - setDirty(true); - paintItems(); - }); - acts.append(del); - } - return el("div", { - className: "m02-mat__row", - attrs: { "data-matset-item": m.코드 }, - children: [m.코드, m.이름 ?? "", m.규격 ?? "", m.단위 ?? "", ""] - .map((t) => el("span", { text: t })) - .concat(acts), - }); - }); - table.replaceChildren( - head, - ...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: tm("NoItem") })]), - ); - }; - paintItems(); - - const add = createButton({ label: tm("AddItem"), variant: "ghost" }); - add.addEventListener("click", () => - pickMaterial((m) => { - if (row.담은자재.some((x) => x.코드 === m.코드)) return; - row.담은자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위 }); - setDirty(true); - paintItems(); - }), - ); - const save = createButton({ label: tm("Save"), variant: "filled" }); - save.addEventListener("click", async () => { - if (!row.이름.trim()) return showToast(tm("NeedName"), "error"); - if (!row.담은자재.length) return showToast(tm("NeedItem"), "error"); - save.disabled = true; // 연달아 눌러도 한 번만 - try { - const { 키, ...rest } = row; - const done = isNew - ? await createMaterialCombo(version, rest) - : await editMaterialCombo(키, version, row); - setDirty(false); - showToast(tm("Saved"), "success"); - await refresh(); - draw(structuredClone(done.줄), false); - } catch (error) { - if (error instanceof StaleError) { - showToast(tm("Stale"), "error"); - await refresh(); - } else showToast(tm("Failed", why(error)), "error"); - } finally { - save.disabled = false; - } - }); - - const node = el("div", { - className: "m02-matset", - children: [ - el("div", { - className: "m02-matset__fields", - children: [ - field(tm("Head_Name"), "이름"), - field(tm("Head_Note"), "비고"), - el("p", { - className: "m02-side__empty", - text: `${tm("Head_Origin")}: ${row.본뜬키 ?? tm("Origin_Self")}`, - attrs: { "data-matset-origin": row.본뜬키 ?? "" }, - }), - ], - }), - el("h3", { className: "m02-matset__head", text: tm("Items_Title") }), - table, - ...(edit ? [el("div", { className: "m02-side__row", children: [add, save] })] : []), - ], - }); - const setDirty = opt.show(isNew ? tm("New") : `${row.키} ${row.이름}`, node); - if (isNew) { - setDirty(true); - node.querySelector('[data-matset-field="이름"]')?.focus(); // 바로 이름 적기 - } - opt.onOpen(); - paintList(); - paintButtons(); - } - - btnNew.addEventListener("click", async () => { - if (!(await opt.confirmLeave())) return; - draw({ 키: "", 이름: "", 담은자재: [], 본뜬키: null, 소유: "현장", 비고: null }, true); - }); - btnCopy.addEventListener("click", async () => { - const from = all.find((x) => x.키 === openKey); - if (!from || !(await opt.confirmLeave())) return; - const copy = structuredClone(from); - draw({ ...copy, 키: "", 이름: "", 본뜬키: from.키, 소유: "현장" }, true); // 이름은 새로 적음 - }); - btnDel.addEventListener("click", async () => { - const key = openKey; - if (!key) return; - await refresh(); // 최신 판 · 쓰는 열로 - // 쓰는 열이 있으면 확인 창 없이 바로 알림(서버도 막음) - const users = used[key] ?? []; - if (users.length) return showToast(tm("InUse", users.join(" · ")), "error"); - if (!(await showConfirmDialog(tm("DeleteAsk", key)))) return; - try { - await deleteMaterialCombo(key, version); - showToast(tm("Deleted"), "success"); - openKey = null; - opt.close(); - } catch (error) { - showToast(tm("Failed", why(error)), "error"); - } - await refresh(); - }); - - paintButtons(); - return { - title: tm("Title"), - body, - actions, - refresh, - clear: () => { - if (openKey === null) return; - openKey = null; - paintList(); - paintButtons(); - }, - }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Matset_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Matset_Text.ts deleted file mode 100644 index 10bb76b78..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Matset_Text.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Matset_Text.ts - * 좌측 「자재 조합」 컨테이너 · 상세 화면 글자 — [한국어, 영어] - * ========================================================================== */ - -import { currentLanguageIndex } from "@ui/ui_template_locale"; - -const TEXT = { - Title: ["자재 조합", "Material sets"], - Search: ["키 · 이름으로 찾기", "Search by key or name"], - Empty: ["자재 조합 없음", "No material sets"], - New: ["새 자재 조합(저장 전)", "New material set (unsaved)"], - Save: ["저장", "Save"], - InUse: [ - "{value} 열이 쓰는 자재 조합이라 못 지움", - "Used by the column(s) {value} — cannot delete", - ], - DeleteAsk: ["자재 조합 {value} 을 지울까요?", "Delete material set {value}?"], - Saved: ["저장함", "Saved"], - Deleted: ["지움", "Deleted"], - Stale: [ - "그 사이 자재 조합 표가 바뀜 — 목록을 새로 받았으니 다시 고쳐 주세요", - "The table changed meanwhile — reloaded, please edit again", - ], - NeedName: ["이름을 적어야 저장됨", "A name is required"], - NeedItem: ["담은 자재가 하나는 있어야 저장됨", "Add at least one material"], - Load_Failed: ["자재 조합을 못 받음 — {value}", "Could not load material sets — {value}"], - Failed: ["자재 조합을 못 고침 — {value}", "Could not change the material set — {value}"], - Head_Name: ["이름", "Name"], - Head_Note: ["비고", "Note"], - Head_Origin: ["본뜬키", "Copied from"], - Origin_Self: ["원본", "Original"], - Items_Title: ["담은 자재", "Materials in it"], - Col_Code: ["코드", "Code"], - Col_Name: ["이름", "Name"], - Col_Spec: ["규격", "Spec"], - Col_Unit: ["단위", "Unit"], - Remove: ["빼기", "Remove"], - AddItem: ["자재 추가", "Add material"], - NoItem: ["담은 자재 없음", "No materials yet"], -} as const satisfies Record; - -export type MatsetTextKey = keyof typeof TEXT; - -export function tm(key: MatsetTextKey, value = ""): string { - const entry = TEXT[key]; - return (entry[currentLanguageIndex as 0 | 1] ?? entry[0]).replace("{value}", value); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Options.ts b/M02_MasterTemplete/M02_MasterTemplete_Options.ts index 3a8c998be..2c7074a06 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Options.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Options.ts @@ -120,7 +120,7 @@ export function buildOptionsSection(opt: OptionsSectionOptions): OptionsSection } const kind = current; optionList.replaceChildren( - el("h3", { className: "m02-matset__head", text: kind.name }), + el("h3", { className: "m02-options__head", text: kind.name }), ...kind.options.map((o) => { const box = el("input", { attrs: { type: "checkbox", "data-options-key": o.key }, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router.py b/M02_MasterTemplete/M02_MasterTemplete_Router.py index e4759adbf..d89e4f900 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router.py @@ -11,7 +11,6 @@ from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from M02_MasterTemplete import M02_MasterTemplete_Branch as branch -from M02_MasterTemplete import M02_MasterTemplete_Matset as matset from M02_MasterTemplete import M02_MasterTemplete_Options as options from M02_MasterTemplete import M02_MasterTemplete_Store as store @@ -30,18 +29,6 @@ class BranchBody(BaseModel): 판: str = "" # 집계표 판 — 주면 어긋날 때 409 -class MaterialBranchBody(BaseModel): - 본뜬키: str = "" # 빈 글 = 이 열의 첫 자재 조합 - 담은자재: list[dict[str, Any]] - 이름: str = "" - 판: str = "" - - -class MatsetBody(BaseModel): - 판: str = "" # 자재 조합 표 판 — 어긋나면 409 - 줄: dict[str, Any] - - class OptionsBody(BaseModel): 판: str = "" # 옵션 표시 표 판 — 어긋나면 409 표시: dict[str, Any] # {종류: {옵션 키: 참 · 거짓}} — 거짓만 남김 @@ -82,58 +69,18 @@ def post_structure_combo(column: str, body: BranchBody) -> dict: return _call(branch.branch_combo, column, body.본뜬키, body.담은로직, body.이름, body.판) -@router.post("/structures/{column}/material-combo") -def post_structure_material_combo(column: str, body: MaterialBranchBody) -> dict: - """자재 조합 갈라 내기 — 같은 규칙(같은 자재 집합이면 있는 키 · 아니면 새 UM).""" - return _call( - branch.branch_material_combo, column, body.본뜬키, body.담은자재, body.이름, body.판 - ) - - class UseComboBody(BaseModel): - 종류: str # 일위대가 · 자재조합 + 종류: str # 일위대가 키: str 판: str = "" # 집계표 판 — 주면 어긋날 때 409 @router.post("/structures/{column}/use-combo") def post_structure_use_combo(column: str, body: UseComboBody) -> dict: - """있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 · 자재조합 키만 바꿈(조합은 그대로).""" + """있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 키만 바꿈(조합은 그대로).""" return _call(branch.use_combo, column, body.종류, body.키, body.판) -# 자재 조합 표(마스터 템플릿 `자재조합.json` · 키 UM) — 좌측 「자재 조합」 컨테이너 -@router.get("/material-combos") -def get_material_combos() -> dict: - """표 통째 — `{판, 줄}`(줄마다 담은 자재 포함).""" - return _call(matset.listing) - - -@router.get("/material-combos/{key}") -def get_material_combo(key: str) -> dict: - """자재 조합 한 줄(키 UM) — 키 · 이름 · 담은자재 · 본뜬키 · 소유 · 비고.""" - return _call(branch.material_combo, key) - - -@router.post("/material-combos") -def post_material_combo(body: MatsetBody) -> dict: - """새로 · 본떠 만들기(`줄.본뜬키`) — 키는 표 「다음」 번호 · `{키, 판, 줄}`.""" - return _call(matset.create, body.줄, body.판) - - -@router.put("/material-combos/{key}") -def put_material_combo(key: str, body: MatsetBody) -> dict: - """고치기 — 이름 · 비고 · 담은자재(본뜬키 · 소유는 있던 것).""" - return _call(matset.edit, key, body.줄, body.판) - - -@router.delete("/material-combos/{key}") -def delete_material_combo(key: str, 판: str = "") -> dict: - """지우기 — 구조물 열이 쓰면 409 `{errors, users}`.""" - return _call(matset.delete, key, 판) - - -# 구조물 옵션 보이기 — B05/B06 폼에 보일 옵션(마스터 템플릿 `구조물옵션표시.json`) @router.get("/structure-options") def get_structure_options() -> dict: """`{판, 표시, 종류}` — 표시 = 숨긴 것만 · 종류 = 옵션 있는 종류와 그 옵션.""" diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 4321aee0f..d893d97aa 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -22,13 +22,10 @@ from M02_MasterTemplete import M02_Layer_Access as access from M02_MasterTemplete import M02_MasterTemplete_Branch as branch from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store from M02_MasterTemplete import M02_MasterTemplete_Items as items_store -from M02_MasterTemplete import M02_MasterTemplete_Matset as matset from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree from M02_MasterTemplete.M02_MasterTemplete_Router import ( BranchBody, - MaterialBranchBody, - MatsetBody, UseComboBody, ) @@ -113,7 +110,7 @@ def get_items( all: bool = False, # noqa: A002 — 21-1 약속 이름 `?all=1` session: Session = Depends(verify_session), ): - """21-1 — 한 층이면 `[{key, 열, 종류, 분류1~3, 단위, 일위대가, 자재조합, 도번, 면적번, + """21-1 — 한 층이면 `[{key, 열, 종류, 분류1~3, 단위, 일위대가, 도번, 면적번, 재료번}]` · `?all=1` 이면 세 층 `[{layer, owner, items}]`(회사 · 개인은 세션 것 · 회사 없으면 빈 목록).""" if all: @@ -123,7 +120,7 @@ def get_items( @router.post("/layers/personal/items/copy") def copy_item(body: CopyItemBody, session: Session = Depends(verify_session)) -> dict: - """회사 · 마스터 항목 → 내 개인 층 새 항목(열 · 세 파일 · 자재 조합 줄 · 새 SS 키 · `본뜬`).""" + """회사 · 마스터 항목 → 내 개인 층 새 항목(열 · 세 파일 · 새 SS 키 · `본뜬`).""" target = _root(session, "personal", None, write=True) source = _root(session, body.from_layer, body.from_owner) owner = access.grant(session, body.from_layer, body.from_owner).owner @@ -291,19 +288,6 @@ def post_combo( ) -@router.post("/layers/{layer}/structures/{column}/material-combo") -def post_material_combo_branch( - layer: Layer, - column: str, - body: MaterialBranchBody, - owner: str | None = None, - session: Session = Depends(verify_session), -): - folder = _root(session, layer, owner, write=True) - args = (column, body.본뜬키, body.담은자재, body.이름, body.판) - return _call(folder, branch.branch_material_combo, *args) - - @router.post("/layers/{layer}/structures/{column}/use-combo") def post_use_combo( layer: Layer, @@ -328,47 +312,3 @@ def post_copy( folder = _root(session, layer, owner, write=True) part = "structure" if kind == "structures" else kind return _call(folder, store.copy_from, part, column, source) - - -@router.get("/layers/{layer}/material-combos") -def get_matsets(layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)): - return _call(_root(session, layer, owner), matset.listing) - - -@router.get("/layers/{layer}/material-combos/{key}") -def get_matset( - layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) -): - return _call(_root(session, layer, owner), matset.one, key) - - -@router.post("/layers/{layer}/material-combos") -def post_matset( - layer: Layer, - body: MatsetBody, - owner: str | None = None, - session: Session = Depends(verify_session), -): - return _call(_root(session, layer, owner, write=True), matset.create, body.줄, body.판) - - -@router.put("/layers/{layer}/material-combos/{key}") -def put_matset( - layer: Layer, - key: str, - body: MatsetBody, - owner: str | None = None, - session: Session = Depends(verify_session), -): - return _call(_root(session, layer, owner, write=True), matset.edit, key, body.줄, body.판) - - -@router.delete("/layers/{layer}/material-combos/{key}") -def delete_matset( - layer: Layer, - key: str, - 판: str = "", - owner: str | None = None, - session: Session = Depends(verify_session), -): - return _call(_root(session, layer, owner, write=True), matset.delete, key, 판) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index ced3a7fc7..12a0b4497 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -26,7 +26,7 @@ FOLDER: Path = Path(__file__).resolve().parent.parent / "resources" / "master_te KINDS = layers.KINDS # 시험은 FOLDER 를 사본으로 바꿈 # 지금 다루는 층 폴더 — 층 라우터가 `at(층 폴더)` 로 바꿈 · 안 바꾸면 시스템 층(`FOLDER`). # 회사 · 개인 층도 시스템과 같은 모양(`table/구조물집계표.json` · 조합 장부 · -# structure/area/material · 자재조합.json — PLAN 21-1)이라 아래 함수는 그대로 씀 +# structure/area/material — PLAN 21-1)이라 아래 함수는 그대로 씀 _ROOT: ContextVar[Path | None] = ContextVar("m02_layer_root", default=None) @@ -158,7 +158,7 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) -> (브라우저 값 버림 · 5장 ②) · 알약 `연결` 은 문서와 같이 그대로 적힘. 번호 칸(도번 · 면적번 · 재료번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음). 표 열의 `본뜬열`(분류 더하기로 복사한 새 열)은 적지 않음 — 집계표면 아직 파일이 없는 새 열이 - 원본 열의 일위대가 · 자재조합 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로). + 원본 열의 일위대가 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로). `screen` = 화면 표 [저장](PLAN 26-7e) — 있던 항목의 경로 · 층 이름 · 빈 폴더는 저장본 그대로 (정본 = 나무 · 머리는 경로에서 다시 만듦 · `tree.keep_paths`).""" path = _path(kind, name) @@ -323,7 +323,7 @@ def _ensure(kind: str, column: str, source: str | None = None) -> bool: def _take_origins(doc: Any) -> dict[str, str]: """표 열의 `본뜬열` 을 떼어 냄 → `{새 열: 원본 열}` (구조물 파일이 아직 없는 열만) · - 그 열에 일위대가 · 자재조합 키가 비었으면 원본 열 것을 받음.""" + 그 열에 일위대가 키가 비었으면 원본 열 것을 받음.""" cols = ( [c for c in (doc.get("열") or []) if isinstance(c, dict)] if isinstance(doc, dict) else [] ) @@ -336,8 +336,7 @@ def _take_origins(doc: Any) -> dict[str, str]: if (root() / "structure" / f"{col['id']}.json").is_file(): continue out[col["id"]] = source - for field in ("일위대가", "자재조합"): - col[field] = col.get(field) or by_id[source].get(field) + col["일위대가"] = col.get("일위대가") or by_id[source].get("일위대가") return out diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 30a1b1bea..9420f9dda 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -45,7 +45,6 @@ import { type SheetEditOptions, type SheetKind, } from "./M02_MasterTemplete_ItemCards"; -import { buildMaterialPanel, loadMaterialState, toPills } from "./M02_MasterTemplete_Materials"; import { layerSource } from "./M02_MasterTemplete_ItemSource"; import { createInputsCard, inputPills, type InputsCardHandle } from "./M02_MasterTemplete_Inputs"; import "./M02_MasterTemplete_Structure.css"; @@ -302,61 +301,12 @@ export async function pillsFor( } } -/** 카드 미리보기 수정의 알약 · 자재 조합 판(31-3) — 전체 편집기(`UI_Main` `mountSheet`)와 같은 부품 · - * 면적 = 그 열 일위대가 입력값(`pillsFor`) · 재료 = 자재 조합 담은 자재 + 선택 입력 · 아래 자재 조합 판 */ +/** 카드 미리보기 수정의 알약(31-3) — 전체 편집기(`UI_Main` `mountSheet`)와 같은 부품(`pillsFor`) */ async function cardLinks( kind: SheetKind, ctl: SheetCardControl, - isAdmin: boolean, ): ReturnType { - if (kind === "area") return { links: (await pillsFor("area", ctl.column)).links }; - const [state, own] = await Promise.all([ - loadMaterialState(ctl.column), - inputPills(ctl.column, "재료"), // 「선택 입력」 카드가 재료산출로 고른 입력 - ]); - let items = state.combo?.담은자재 ?? []; - const links = (): LinkOptions => ({ - 항목: [...toPills(items), ...own], - 모드: "자재", - 자재찾기: searchMaterials, - // 알약 막대에서 추가 · 빼기 — 아래 판 표를 같은 목록으로 - onItemsChange: (list) => { - items = list.flatMap((i) => (i.자재 ? [{ ...i.자재 }] : [])); - panel.setItems(items); - }, - }); - const panel = buildMaterialPanel({ - column: ctl.column, - label: ctl.label, - isAdmin, - state, - links: () => - ctl - .handle() - ?.getDoc() - .시트.flatMap((s) => s.연결 ?? []) ?? [], - // 판 표에서 고침 — 알약 막대만 이 목록으로 다시 만듦(저장 안 한 시트 고침은 그대로) - onChange: (list) => { - items = list; - ctl.remount(links()); - }, - goTo: (code) => - ctl.host - .querySelector(`.ss-link.is-linked[data-key="${CSS.escape(code)}"]`) - ?.click(), - unlink: (codes) => { - const book = ctl.handle()?.getDoc(); - if (!book) return; - for (const sheet of book.시트) { - const rest = (sheet.연결 ?? []).filter((l) => !codes.includes(l.자재?.코드 ?? l.키)); - if (rest.length) sheet.연결 = rest; - else delete sheet.연결; - } - ctl.handle()?.setDoc(book); - ctl.touched(); - }, - }); - return { links: links(), below: panel.root, refresh: panel.refresh }; + return { links: (await pillsFor(kind, ctl.column)).links }; } export interface StructurePanelOptions { @@ -417,7 +367,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane // 면적산출 · 재료산출 카드 미리보기 수정(31-3) — 전체 편집기와 같은 알약 · 저장 길 sheetEdit: { can: () => opts.isAdmin, - links: (kind, ctl) => cardLinks(kind, ctl, opts.isAdmin), + links: (kind, ctl) => cardLinks(kind, ctl), save: async (kind, at, doc, version) => (await saveTemplate(getActiveLayer(), kind, at, null, version, doc)).판, }, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Tree.py b/M02_MasterTemplete/M02_MasterTemplete_Tree.py index d051f9488..0a0c5172b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Tree.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Tree.py @@ -247,7 +247,7 @@ def apply( _taken_item(out, made) col: dict[str, Any] = {"id": _fresh_id(out), "단위": body.get("단위"), "꼴": "수"} # 항목 추가 창에서 고른 일위대가(PLAN 31-2) — 없으면 비움 · 단위는 안 받아도 됨 - col.update({"일위대가": body.get("일위대가") or None, "자재조합": None}) + col["일위대가"] = body.get("일위대가") or None _set_path(col, made) _place(out, col) note.update({"경로": made, "열": col["id"]}) diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 480f213f2..cccb494e2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -26,21 +26,11 @@ import { type StructureColumn, type StructurePanelHandle, } from "./M02_MasterTemplete_Structure"; -import { inputPills } from "./M02_MasterTemplete_Inputs"; import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs"; import { buildComboBar } from "./M02_MasterTemplete_Combo"; import { buildComboPanel, type ComboPanel } from "./M02_MasterTemplete_ComboPanel"; -import { - buildMaterialPanel, - loadMaterialState, - toPills, - type MaterialPanel, - type MaterialState, -} from "./M02_MasterTemplete_Materials"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links"; -import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch"; -import type { MaterialItem } from "./M02_MasterTemplete_Api_Fetch"; export interface Selection { layer: Layer; projectId: string | null; @@ -189,9 +179,7 @@ export function createMain( let sheet: SheetEditorHandle | null = null; let panel: StructurePanelHandle | null = null; let sheetHandle: SpreadsheetHandle | null = null; - let matPanel: MaterialPanel | null = null; - let matState: MaterialState | null = null; - let cpanel: ComboPanel | null = null; // 면적산출 · 재료산출 오른쪽 조합 패널(프로젝트 층 아님) + let cpanel: ComboPanel | null = null; // 면적산출 오른쪽 조합 패널(프로젝트 층 아님) let seq = 0; let dirty = false; /** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 · 재료산출 31-3) */ @@ -236,8 +224,6 @@ export function createMain( editor = null; sheet = null; sheetHandle = null; - matPanel = null; - matState = null; cpanel?.destroy(); cpanel = null; panel?.destroy(); @@ -263,37 +249,22 @@ export function createMain( } /** 면적산출 · 재료산출 스프레드시트 — 위 알약 막대(sub7 `links`) · 면적 = 그 열 일위대가 입력값 · - * 재료 = 자재DB 찾기(`pillsFor`) · 시스템 층은 면적 = 조합 줄 + [조합 고치기] · - * 재료 = 그 열 자재 조합 담은 자재(알약 항목 = 옆 판 표 = 같은 목록) + 옆 판 - * (`pillItems` = 옆 판에서 고친 목록으로 알약 막대만 다시 만들 때) */ + * 재료 = 자재DB 찾기(`pillsFor`) · 시스템 층은 면적 = 조합 줄 + [조합 고치기] */ async function mountSheet( doc: unknown, kind: "area" | "material", at: Selection, readOnly: boolean, - pillItems?: MaterialItem[], ): Promise { const mine = seq; try { - const master = at.layer !== "project" && kind === "material"; - if (master && !matState) matState = await loadMaterialState(at.name); - const [{ createSpreadsheet }, pills, own] = await Promise.all([ + const [{ createSpreadsheet }, pills] = await Promise.all([ import("../A00_Common/spreadsheet/spreadsheet"), - master ? null : pillsFor(kind, at.name), - master ? inputPills(at.name, "재료") : [], // 「선택 입력」 카드가 재료산출로 고른 입력 + pillsFor(kind, at.name), ]); if (mine !== seq) return; - if (pills?.notice) showNotice([pills.notice]); - const links: LinkOptions = - pills?.links ?? - ({ - 항목: [...toPills(pillItems ?? matState?.combo?.담은자재 ?? []), ...own], - 모드: "자재", - 자재찾기: searchMaterials, - // 알약 막대에서 더하기 · 빼기 — 옆 판 표를 같은 목록으로 - onItemsChange: (list) => - matPanel?.setItems(list.flatMap((i) => (i.자재 ? [{ ...i.자재 }] : []))), - } satisfies LinkOptions); + if (pills.notice) showNotice([pills.notice]); + const links: LinkOptions = pills.links; const handle: SpreadsheetHandle = createSpreadsheet( host, doc as Parameters[1], @@ -302,90 +273,47 @@ export function createMain( tabs: false, // 시트 하나 · 탭 줄 없음 onChange: () => { dirty = !readOnly; - matPanel?.refresh(); // 알약을 이음 · 풂 — 옆 판 「이은 칸」 바로 갱신 }, links, }, ); editor = handle; sheetHandle = handle; - if (at.layer === "project") return; + if (at.layer === "project" || kind !== "area") return; // 오른쪽 조합 패널 — 한 번 만들고 시트를 다시 붙여도 그대로(늘 시트 뒤) cpanel ??= buildComboPanel({ kind, column: at.name, isAdmin, - onUsed: () => remount(kind, at, readOnly, true), + onUsed: () => remount(kind, at, readOnly), }); host.append(cpanel.root); - if (kind === "area") { - const bar = await buildComboBar({ - column: at.name, - label: at.label ?? at.name, - isAdmin, - beforeEdit: async () => true, - onApplied: async () => { - await remount(kind, at, readOnly, false); - await cpanel?.refresh(); - }, - }); - if (mine === seq) cpanel?.setMine(bar); - return; - } - if (matPanel || !matState) return; - matPanel = buildMaterialPanel({ + const bar = await buildComboBar({ column: at.name, label: at.label ?? at.name, isAdmin, - state: matState, - onApplied: () => void cpanel?.refresh(), - links: () => sheetHandle?.getDoc().시트.flatMap((s) => s.연결 ?? []) ?? [], - // 옆 판 표에서 고침 — 알약 막대만 이 목록으로 다시 만듦(저장 안 한 시트 고침은 그대로) - onChange: (items) => { - const kept = sheetHandle?.getDoc(); - if (!kept) return; - editor?.destroy(); - editor = null; - void mountSheet(kept, kind, at, readOnly, items); - }, - goTo: (code) => - host - .querySelector(`.ss-link.is-linked[data-key="${CSS.escape(code)}"]`) - ?.click(), - unlink: (codes) => { - const book = sheetHandle?.getDoc(); - if (!book) return; - for (const s of book.시트) { - const rest = (s.연결 ?? []).filter((l) => !codes.includes(l.자재?.코드 ?? l.키)); - if (rest.length) s.연결 = rest; - else delete s.연결; - } - sheetHandle?.setDoc(book); - dirty = !readOnly; + beforeEdit: async () => true, + onApplied: async () => { + await remount(kind, at, readOnly); + await cpanel?.refresh(); }, }); - cpanel.setMine(matPanel.root); + if (mine === seq) cpanel?.setMine(bar); } catch { empty(L("M02_NoSpreadsheet")); } } - /** 시트만 다시 붙임(알약을 새로 받음) — 저장 안 한 시트 고침은 그대로 옮김 · - * `fresh` = 이 열 조합이 바뀜(재료는 자재 조합 · 담은 자재 표를 새로 받음) */ + /** 시트만 다시 붙임(알약을 새로 받음) — 저장 안 한 시트 고침은 그대로 옮김 */ async function remount( kind: "area" | "material", at: Selection, readOnly: boolean, - fresh: boolean, ): Promise { const kept = sheetHandle?.getDoc(); if (sel !== at || !kept) return; editor?.destroy(); editor = null; - if (fresh && kind === "material") { - matPanel = null; - matState = null; - } await mountSheet(kept, kind, at, readOnly); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 9c64b6b19..52b42c186 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,10 +1,10 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts * 마스터 템플릿 좌측 패널 — 「구조물 조합」(구조물집계표 줄 하나 + 하위 구조물 도면 · 면적산출 · - * 재료산출 · 자재 조합) · 「도면 양식」(새로 · 본떠 · 지우기). 서버 길은 계약 `6_계약.md`. + * 재료산출) · 「도면 양식」(새로 · 본떠 · 지우기). 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 — * 서버가 미리 만들어 둠(도번 SD · 면적번 SA · 재료번 SM + 같은 8자리) · - * 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). 「자재 조합」 컨테이너는 `M02_MasterTemplete_Matset.ts`. + * 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. * 구조물 라이브러리는 좌측이 아니라 「구조물 조합」 상세 화면 왼쪽(PLAN 26-7c · 페이지가 붙임). * ========================================================================== */ @@ -38,7 +38,6 @@ import { structureLabel, type StructureColumn, } from "./M02_MasterTemplete_Structure"; -import { buildMatsetSection } from "./M02_MasterTemplete_Matset"; import type { LaborSection } from "./M02_MasterTemplete_Labor"; import { buildOptionsSection } from "./M02_MasterTemplete_Options"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; @@ -131,26 +130,14 @@ export function buildSide(opt: SideOptions): SideHandle { del.addEventListener("click", () => void deleteCurrent()); return [el("div", { className: "m02-side__row", children: [btnNew, copy, del] })]; }; - const matset = buildMatsetSection({ - isAdmin: canEdit(layer, opt.isAdmin), - confirmLeave: opt.confirmLeave, - show: opt.showView, - close: () => opt.onOpen(null), - onOpen: () => (options.clear(), labor?.clear(), paint()), - }); - const matsetSection = section(matset.title, matset.body, ...matset.actions); - // 펼칠 때마다 새로 받음 — 재료산출 옆 판이 갈라 낸 조합도 바로 보이게 - matsetSection.querySelector(".ui-collapsible__title")?.addEventListener("click", () => { - if (matsetSection.classList.contains("is-collapsed")) void matset.refresh(); - }); // 구조물 옵션 보이기 — 구조물 집계 양식 아래 별도 줄 하나(누르면 메인 칸에 종류별 옵션 체크) const options = buildOptionsSection({ isAdmin: canEdit(layer, opt.isAdmin), confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (matset.clear(), labor?.clear(), paint()), + onOpen: () => (labor?.clear(), paint()), }); - const labor = opt.labor?.({ onOpen: () => (matset.clear(), options.clear(), paint()) }); + const labor = opt.labor?.({ onOpen: () => (options.clear(), paint()) }); /** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */ const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => { sec.querySelector(".ui-collapsible__title")?.addEventListener("click", () => { @@ -164,7 +151,6 @@ export function buildSide(opt: SideOptions): SideHandle { L("M02_KindTable"), el("div", { className: "m02-side__items ui-sidebar-list", children: opt.tree ?? [] }), ...COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])), - matsetSection, ); summary.classList.remove("is-collapsed"); summary.classList.add("m02-side__summary"); @@ -255,13 +241,12 @@ export function buildSide(opt: SideOptions): SideHandle { for (const [kind, b] of editButtons) b.copy.disabled = b.del.disabled = cur?.종류 !== kind; } - /** 고름 표시는 여기서만 — 목록 · 열 줄 · 자재 조합 줄을 함께 그려야 두 줄이 동시에 안 남음 - * (양식이 열려 있으면 자재 조합 고름은 지움 · 자재 조합 상세는 열린 양식이 없음) */ + /** 고름 표시는 여기서만 — 목록 · 열 줄 · 옵션 줄을 함께 그려야 두 줄이 동시에 안 남음 + * (양식이 열려 있으면 옵션 · 일위대가 조합 고름은 지움) */ function paint(): void { paintList(); paintColumns(); paintButtons(); - if (opt.getOpen()) matset.clear(); if (opt.getOpen()) options.clear(); if (opt.getOpen()) labor?.clear(); } @@ -307,7 +292,7 @@ export function buildSide(opt: SideOptions): SideHandle { if (mine !== seq) return; paintList(); paintButtons(); - await Promise.all([refreshStructure(), matset.refresh()]); + await refreshStructure(); } /* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 0e323ca8c..02fb72880 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -108,23 +108,8 @@ gap: var(--spacing-4); } -/* 좌측 「자재 조합」 상세(메인 칸) — 이름 · 비고 · 본뜬키 + 담은 자재 표(줄 모양은 재료산출 옆 판 `m02-mat__row`) */ -.m02-matset { - display: flex; - flex-direction: column; - gap: var(--spacing-12); - max-width: 56rem; - font-size: var(--text-body-sm); -} - -.m02-matset__fields { - display: flex; - flex-wrap: wrap; - align-items: flex-end; - gap: var(--spacing-12); -} - -.m02-matset__head { +/* 구조물 옵션 보이기 — 종류 제목 */ +.m02-options__head { margin: 0; color: var(--color-text); font-size: var(--text-body); diff --git a/M02_MasterTemplete/M02_Project_Items_Router.py b/M02_MasterTemplete/M02_Project_Items_Router.py index 6214c6968..59aa24d72 100644 --- a/M02_MasterTemplete/M02_Project_Items_Router.py +++ b/M02_MasterTemplete/M02_Project_Items_Router.py @@ -9,7 +9,7 @@ POST …/items/{폴더}/reapply 재적용(입력값 · 고른자재는 같은 키면 살림 · `덮임` 알림) POST …/items/{폴더}/save-as {to, 경로, 이름} 개인 층에 새 항목으로(단가 묶음은 안 가져감) POST …/items/{폴더}/calc 복사본만으로 계산(세 비목 · 계 · 빠진 입력) -종류 = 도면 · 면적 · 재료 · 자재조합 · 입력연결 · 입력값 · 고른자재 · `refresh` = 옛 이름(재적용). +종류 = 도면 · 면적 · 재료 · 입력연결 · 입력값 · 고른자재 · `refresh` = 옛 이름(재적용). ⚠ 등록은 `main.py`(로그인만) — 프로젝트 권한(같은 회사)은 여기서 `_project` 로 봄(403 · 404). """ @@ -31,7 +31,7 @@ from M02_MasterTemplete.M02_Item_Source import item_name router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"]) -Kind = Literal["도면", "면적", "재료", "자재조합", "입력연결", "입력값", "고른자재"] +Kind = Literal["도면", "면적", "재료", "입력연결", "입력값", "고른자재"] class WriteBody(BaseModel): diff --git a/resources/master_data/_틀.md b/resources/master_data/_틀.md index e7572b3a0..8170ce235 100644 --- a/resources/master_data/_틀.md +++ b/resources/master_data/_틀.md @@ -298,13 +298,3 @@ - `이름` = 글자 · 빈 이름 안 됨 · 60자까지(`master_combo.MAX_이름`) · 같은 파일 안에서 안 겹침. - 검사(`check_master.py 조합` · 엔진 `scripts/master_combo.py`) — 없는 로직 키 · 단가산출 로직이 아닌 키 · 같은 로직 두 번 · 빈 조합 · **조합이 조합을 담음**(1단만) · 메모가 글자·`null` 아님 · 이름이 글자 아니거나 없음·너무 김·겹침. - 「이 로직을 쓰는 조합」 은 로직 줄에 칸으로 박지 않고 물을 때 훑어 찾음(`master_combo.using`) — 조합은 관리자가 그때그때 만드는 것이라 박아 두면 낡음. 소요량 「용도」 와 결은 같되 자리가 다름. - -## 11. 자재 조합 파일 - -마스터 템플릿 `resources/master_template/자재조합.json` 하나(이 폴더 밖 · `구조물집계표조합.json` 과 같은 자리) — 구조물 열 재료산출이 쓰는 **자재 목록 묶음**. 머리 `그룹` · `판` · `비고` · `다음` · `줄`(원문 없음). 검사는 `check_master.py 틀` 이 같이 봄(`master_material_combo.check_file`). - -- 열 한 벌 — `키` · `이름` · `담은자재` · `본뜬키` · `소유` · `비고`(차례까지 이대로 · 엔진 `scripts/master_material_combo.py`). -- `키` = `UM` + 6자리 — 표 머리 `다음` 번호(`_키대장` 밖) · 지운 키는 다시 안 씀 · 줄에 `원문번호` 를 두지 않음. -- `담은자재` 줄 = `{코드, 이름, 규격, 단위}` 뿐 · 빈 목록 안 됨 · 같은 코드 두 번 안 됨 · 수량 · 계산은 두지 않음(재료산출 몫). -- `본뜬키` = 갈라 낸 원래 자재 조합 키 · 없으면 `null` · `소유` = `현장` · `공용`. -- 새 줄은 M02 갈라 내기(`POST /api/m02/structures/{열}/material-combo`)가 만듦 — 같은 자재 집합이 있으면 그 키. diff --git a/resources/master_data/_화면_계약.md b/resources/master_data/_화면_계약.md index 2de40f3b1..2199db1d3 100644 --- a/resources/master_data/_화면_계약.md +++ b/resources/master_data/_화면_계약.md @@ -196,10 +196,6 @@ - 못 도는 줄은 통째로 멈추지 않고 그 줄만 `ok` false · `까닭` · `멈춤` 에 한 줄 — 나머지 줄의 합계는 그대로 냄. - 답 — 200 · 400(틀린 몸 · 이름 없음 · 모르는 소유) · 404(없는 키) · 409(낡은 판본) · 422 `{detail: {errors: […]}}`(검사 걸림 — 없는 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음). -### 자재 조합 - -M01 에 없음 — 마스터 템플릿 표(`resources/master_template/자재조합.json` · 키 `UM` · `_틀.md` 11장) · 길과 화면은 M02(`/api/m02/material-combos…` · 좌측 「자재 조합」 컨테이너). - ## 10. 재료 · 기계 고르기 재료 줄은 키 대신 기본 검색어로 품목을 찾음 — 파일 모양 · 찾기 규칙 · 자동값 차례 · 단위 환산은 `_틀.md` 7장. diff --git a/resources/master_data/scripts/check_master.py b/resources/master_data/scripts/check_master.py index 9d304d8d8..810bc54a3 100644 --- a/resources/master_data/scripts/check_master.py +++ b/resources/master_data/scripts/check_master.py @@ -30,7 +30,6 @@ sys.path.insert(0, str(ROOT / "resources/knowledge/original/_pipeline")) import master_combo as mcb # noqa: E402 import master_formula as mf # noqa: E402 import master_keys as mk # noqa: E402 -import master_material_combo as mmc # noqa: E402 # (2) 본문은 따로 모듈 — 부르는 쪽은 `cm.check_body` · `cm.DIVISIONS` 그대로 from check_master_body import DIVISIONS, check_body # noqa: E402, F401 @@ -549,7 +548,6 @@ def main(argv: list[str]) -> int: report = {} if mode in ("틀", "전부"): report["틀"] = [x for name, data in files.items() for x in check_form(name, data)] - report["틀"] += mmc.check_file() # 자재 조합은 마스터 템플릿 표 if mode in ("본문", "전부"): report["본문"] = check_body(files) if mode in ("로직", "전부"): diff --git a/resources/master_data/scripts/master_material_combo.py b/resources/master_data/scripts/master_material_combo.py deleted file mode 100644 index 39d4ed46e..000000000 --- a/resources/master_data/scripts/master_material_combo.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -"""자재 조합 — 구조물 열 재료산출이 쓰는 자재 목록 묶음(키 UM · `_틀.md` 11장). - -자리는 마스터 템플릿 `resources/master_template/자재조합.json`(구조물집계표조합.json 과 같은 결) · -키는 표 안 「다음」 번호(`_키대장` 밖). 담은 자재 목록만 지님(수량 · 계산은 재료산출 몫) · -갈라 내기(M02) 때 새 줄 · 본뜬키 = 원래 키. 저장 · 화면은 M02(`M02_MasterTemplete_Matset.py`). -""" - -from __future__ import annotations - -import json -from pathlib import Path - -import master_combo as mcb -import master_keys as mk - -GROUP = "자재조합" -FILE = "자재조합.json" -TID = "UM" -OWNERS = mcb.OWNERS -MAX_이름 = mcb.MAX_이름 -PATH = Path(__file__).resolve().parents[2] / "master_template" / FILE -COLS = ("키", "이름", "담은자재", "본뜬키", "소유", "비고") -ITEM_COLS = ("코드", "이름", "규격", "단위") - - -def items(row: dict) -> list[dict]: - got = row.get("담은자재") - return got if isinstance(got, list) else [] - - -def check_form(name: str, data: dict) -> list[str]: - """파일 틀 + 줄 — 키 모양 · 열 한 벌 · 이름 · 소유 · 담은 자재(빈 목록 · 모양 · 같은 코드 두 번).""" - out = [] if data.get("그룹") == GROUP else [f"{name} · 머리 그룹이 「{GROUP}」 아님"] - rows = data.get("줄") - if not isinstance(rows, list): - return out + [f"{name} · 머리 「줄」 목록 없음"] - after = data.get("다음") - if not isinstance(after, int) or after < 1: - out.append(f"{name} · 머리 「다음」 이 1 이상 정수 아님") - after = None - seen_names: dict[str, str] = {} - for row in rows: - key = str(row.get("키")) - where = f"{name} · {key} {row.get('이름') or ''}".strip() - if not mk.KEY.fullmatch(key) or key[:2] != TID: - out.append(f"{where} · 키 모양이 「{TID}」 + 6자리 아님") - elif after and int(key[2:]) >= after: - out.append(f"{where} · 키 번호가 머리 「다음」({after}) 보다 작지 않음") - if tuple(row) != COLS: - out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(COLS)}」") - plain = row.get("이름").strip() if isinstance(row.get("이름"), str) else "" - if not plain or len(plain) > MAX_이름: - out.append(f"{where} · 「이름」 이 없거나 {MAX_이름}자 넘음") - elif plain in seen_names: - out.append(f"{where} · 「이름」 이 같은 조합과 겹침 — {seen_names[plain]}") - seen_names.setdefault(plain, key) - if row.get("소유") not in OWNERS: - out.append(f"{where} · 소유 「{row.get('소유')}」 — {' · '.join(OWNERS)} 아님") - origin = row.get("본뜬키") - if origin is not None and not (mk.KEY.fullmatch(str(origin)) and origin[:2] == TID): - out.append(f"{where} · 본뜬키 모양 「{origin}」") - got = items(row) - if not got: - out.append(f"{where} · 빈 조합 — 담은 자재가 없음") - codes: set[str] = set() - for at, one in enumerate(got, start=1): - if not isinstance(one, dict) or set(one) - set(ITEM_COLS): - out.append(f"{where} 줄{at} · 담은 자재 줄 모양 「{one}」") - continue - code = str(one.get("코드") or "") - if not mk.KEY.fullmatch(code): - out.append(f"{where} 줄{at} · 자재 코드 모양 아님 「{code}」") - elif code in codes: - out.append(f"{where} 줄{at} · 같은 자재를 두 번 담음 「{code}」") - codes.add(code) - return out - - -def check_file(path: Path = PATH) -> list[str]: - """마스터 템플릿 자재 조합 표 — 없으면 한 줄.""" - if not path.is_file(): - return [f"{path.name} · 마스터 템플릿에 없음"] - return check_form(path.name, json.loads(path.read_text(encoding="utf-8"))) diff --git a/resources/master_template/자재조합.json b/resources/master_template/자재조합.json deleted file mode 100644 index aa84200ab..000000000 --- a/resources/master_template/자재조합.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "그룹": "자재조합", - "판": "2026", - "비고": ["자재 조합 = 구조물 열 재료산출이 쓰는 자재 목록 묶음 · 수량·계산은 두지 않음(재료산출 몫) · 키 UM 은 「다음」 번호로 매김 · 지운 키는 다시 안 씀"], - "다음": 1, - "줄": [] -} diff --git a/resources/tester/fixtures/m02_master_sample/자재조합.json b/resources/tester/fixtures/m02_master_sample/자재조합.json deleted file mode 100644 index aa84200ab..000000000 --- a/resources/tester/fixtures/m02_master_sample/자재조합.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "그룹": "자재조합", - "판": "2026", - "비고": ["자재 조합 = 구조물 열 재료산출이 쓰는 자재 목록 묶음 · 수량·계산은 두지 않음(재료산출 몫) · 키 UM 은 「다음」 번호로 매김 · 지운 키는 다시 안 씀"], - "다음": 1, - "줄": [] -} diff --git a/resources/tester/test_item_copy_complete.py b/resources/tester/test_item_copy_complete.py index 9d1265f59..3f244c1f6 100644 --- a/resources/tester/test_item_copy_complete.py +++ b/resources/tester/test_item_copy_complete.py @@ -52,7 +52,7 @@ def master_copy(tmp_path_factory) -> Path: def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]: system = tmp_path / "master" system.mkdir() - for name in (layers.COMBO_FILE, "자재조합.json"): + for name in (layers.COMBO_FILE,): shutil.copy2(REAL / name, system / name) for kind in ("table", "structure", "area", "material"): shutil.copytree(REAL / kind, system / kind) diff --git a/resources/tester/test_m02_card_sheet_edit.py b/resources/tester/test_m02_card_sheet_edit.py index a4b3e62c6..eb46f3327 100644 --- a/resources/tester/test_m02_card_sheet_edit.py +++ b/resources/tester/test_m02_card_sheet_edit.py @@ -3,7 +3,7 @@ · 스프레드시트 `preview` = 도구 모음 · 오른쪽 클릭 메뉴 없음 · 수식 입력 줄 · 칸 고침 (다른 화면 그대로) · 카드 = `sheetEdit` 가 있을 때만 고침(M02 구조물 조합) · 고르기 모달은 보기만 그대로 -· 알약 · 자재 조합 판 = 전체 편집기와 같은 부품 · 카드 [저장] = 같은 저장 길 · 저장 안 한 고침 물음 +· 알약 = 전체 편집기와 같은 부품 · 카드 [저장] = 같은 저장 길 · 저장 안 한 고침 물음 """ from __future__ import annotations @@ -55,13 +55,11 @@ def test_카드_저장은_읽은_문서에_시트를_덮음(): assert "saveTemplate(getActiveLayer(), kind, at, null, version, doc)" in struct -def test_알약_자재_조합_판은_전체_편집기와_같은_부품(): +def test_알약은_전체_편집기와_같은_부품(): struct = _text(M02 / "M02_MasterTemplete_Structure.ts") links = struct.split("async function cardLinks")[1].split("export interface")[0] - assert 'pillsFor("area", ctl.column)' in links - assert "loadMaterialState(ctl.column)" in links and 'inputPills(ctl.column, "재료")' in links - assert "toPills(items)" in links and "자재찾기: searchMaterials" in links - assert "buildMaterialPanel({" in links and "ctl.remount(links())" in links + assert "pillsFor(kind, ctl.column)" in links + assert "buildMaterialPanel" not in struct # 자재 조합 판은 없앰 assert "can: () => opts.isAdmin" in struct diff --git a/resources/tester/test_m02_combo_branch.py b/resources/tester/test_m02_combo_branch.py index e430fae03..0a8d060c4 100644 --- a/resources/tester/test_m02_combo_branch.py +++ b/resources/tester/test_m02_combo_branch.py @@ -1,11 +1,10 @@ """조합 갈라 내기 · 분류 더하기 복사 (PLAN 15장). -M02 견본 표(`write_sample_table`) + M01 마스터 사본(조합 줄은 비워 시작)으로만 봄 · 자재 조합 표는 -마스터 템플릿 사본(없이 시작). +M02 견본 표(`write_sample_table`) + M01 마스터 사본(조합 줄은 비워 시작)으로만 봄. · 일위대가(UA) — 새로 만듦(본뜬키 = 원래 키) · 이 열만 새 키 · 다른 열은 옛 키 · 원래 조합 그대로 · SS 장부 맞춤 · 담은 로직(차례 · 메모까지)이 같으면 있는 키(되돌리면 원래 키) · 차례 · 메모만 바꿔도 새로 · 또 고치면 또 새로 · 같은 이름 (2) - · 자재 조합(UM) — 같은 규칙 · 처음 만드는 열(본뜬키 없음) · 읽기 + · 자재 조합(UM)은 없앰 — 키 적기 종류 「자재조합」 = 400 · 집계표 판 어긋남 = 409 · 조합 안 만듦 · 분류 더하기로 복사한 열(`본뜬열`) — [저장] 때 원본 키 · 세 파일 내용을 새 번호로 받음 """ @@ -25,10 +24,6 @@ from resources.tester.test_m02_structure import write_sample_table REAL = m01.FOLDER A, B, C = "GF000219", "GF000220", "GF000211" # 돌쌓기 메쌓기(인력 · 장비) · 채집(인력) -M1, M2 = ( - {"코드": "MT000008", "이름": "시멘트", "규격": "40kg", "단위": "포"}, - {"코드": "MT000009", "이름": "모래", "규격": None, "단위": "㎥"}, -) TABLE = "/api/m02/templates/table/구조물집계표" @@ -156,36 +151,6 @@ def test_집계표_판_어긋나면_409_조합_안_만듦(client): _branch(client, "rv", "UA999999", [A, C], want=404) # 없는 원래 조합 -def _material(client, column, source, items, name="", want=200): - body = {"본뜬키": source, "담은자재": items, "이름": name} - res = client.post(f"/api/m02/structures/{column}/material-combo", json=body) - assert res.status_code == want, res.text - return res.json() - - -def test_자재_조합_처음_만듦_같은_내용이면_있는_키_고치면_새로(client): - first = _material(client, "rv", "", [M1, M2]) - assert first["새로만듦"] is True and first["키"].startswith("UM") - assert first["이름"] == "돌기슭막이 (형태마다) H=(높이마다)" # 이름 없으면 열 머리 - row = client.get(f"/api/m02/material-combos/{first['키']}").json() - assert tuple(row) == ("키", "이름", "담은자재", "본뜬키", "소유", "비고") - assert (row["본뜬키"], row["소유"], row["담은자재"]) == (None, "현장", [M1, M2]) - same = _material(client, "c1", "", [M1, M2]) # 같은 차례 = 있는 키 - assert (same["키"], same["새로만듦"]) == (first["키"], False) - order = _material(client, "c1", first["키"], [M2, M1], "모래 먼저") # 차례만 다름 = 새로 - assert order["새로만듦"] is True and order["키"] != first["키"] - fork = _material(client, "c1", first["키"], [M1], "시멘트만") - assert fork["새로만듦"] is True and fork["키"] != first["키"] - assert client.get(f"/api/m02/material-combos/{fork['키']}").json()["본뜬키"] == first["키"] - assert (_cols(client, "자재조합")["rv"], _cols(client, "자재조합")["c1"]) == ( - first["키"], - fork["키"], - ) - assert _ss(client, "자재조합")["c1"] == fork["키"] - assert client.get("/api/m02/material-combos/UM999999").status_code == 404 - _material(client, "rv", "", [{"코드": "없는모양"}], want=422) # 자재 조합 저장 검사 - - def _use(client, column, kind, key, 판="", want=200): body = {"종류": kind, "키": key, "판": 판} res = client.post(f"/api/m02/structures/{column}/use-combo", json=body) @@ -208,19 +173,11 @@ def test_있는_조합을_이_열에_씀_조합은_그대로(client): _use(client, "c1", "일위대가", "UA999999", want=404) _use(client, "no", "일위대가", ua, want=404) # 글 열 _use(client, "c1", "모름", ua, want=400) - um = _material(client, "rv", "", [M1, M2])["키"] - _use(client, "c1", "자재조합", um) - assert _cols(client, "자재조합")["c1"] == um and _ss(client, "자재조합")["c1"] == um - _use(client, "c1", "자재조합", "UM999999", want=404) + _use(client, "c1", "자재조합", "UM000001", want=400) # 자재 조합은 없앰 def test_분류_더하기로_복사한_열은_원본_키_세_파일_내용을_받음(client): ua = _source(client) - table = client.get(TABLE).json() - doc = table["문서"] - rv = next(c for c in doc["열"] if c["id"] == "rv") - rv["자재조합"] = "UM000001" - assert client.put(TABLE, json={"판": table["판"], "문서": doc}).status_code == 200 drawing = client.get("/api/m02/templates/structure/rv").json() marked = {**drawing["문서"], "도면": {"format": 6, "entities": [{"type": "line"}]}} put = client.put("/api/m02/templates/structure/rv", json={"판": drawing["판"], "문서": marked}) @@ -233,7 +190,7 @@ def test_분류_더하기로_복사한_열은_원본_키_세_파일_내용을_ assert client.put(TABLE, json={"판": table["판"], "문서": doc}).status_code == 200 saved = next(c for c in client.get(TABLE).json()["문서"]["열"] if c["id"] == "c9") assert "본뜬열" not in saved # 뗌 - assert (saved["일위대가"], saved["자재조합"]) == (ua, "UM000001") + assert saved["일위대가"] == ua copy = client.get("/api/m02/templates/structure/c9").json()["문서"] assert copy["도면"]["entities"] == [{"type": "line"}] and copy["열"] == "c9" numbers = client.get("/api/m02/structures/numbers").json() diff --git a/resources/tester/test_m02_empty_master.py b/resources/tester/test_m02_empty_master.py index 3b28042ca..24a96ac8f 100644 --- a/resources/tester/test_m02_empty_master.py +++ b/resources/tester/test_m02_empty_master.py @@ -82,7 +82,6 @@ def test_지운_뒤_항목_0_틀은_남음(empty): assert len(combos["지운번호"]) == 4 # 견본에 세 파일이 있던 넷(rv · c4 · pp_len · ms) for kind in ("structure", "area", "material", "inputs"): assert not list((system / kind).glob("*.json")), kind - assert (system / "자재조합.json").is_file() def test_빈_마스터에서_첫_화면_나무_새_항목_지우기(empty): diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index c7bb3f17f..a170e6516 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -151,7 +151,7 @@ def test_층_단추_셋_남의_회사_사람_나무(): def test_층_길_접두어와_고정_풀림(): api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts") assert 'activeLayer === "system" ? "" : `/layers/${activeLayer}`' in api - for path in ("/structures/numbers", "/structure-combos", "/material-combos"): + for path in ("/structures/numbers", "/structure-combos"): assert re.search(rf'scoped\(\s*[`"]{re.escape(path)}', api), path assert "/layers/personal/items/copy" in api for name in ("Structure", "Combo_Api"): @@ -189,7 +189,7 @@ def test_열_0_첫_열_만들기(): assert fresh and first, "freshId · addFirstColumn 을 못 찾음" script = ( "interface SheetColumn { id: string; 머리: (string | null)[]; 단위?: string | null;" - " 꼴?: string; 일위대가?: string | null; 자재조합?: string | null }\n" + " 꼴?: string; 일위대가?: string | null }\n" "interface SheetDoc { 열: SheetColumn[] }\n" + fresh.group(0) + "\n" diff --git a/resources/tester/test_m02_layers_store.py b/resources/tester/test_m02_layers_store.py index 1be487e98..6a15f6993 100644 --- a/resources/tester/test_m02_layers_store.py +++ b/resources/tester/test_m02_layers_store.py @@ -3,7 +3,6 @@ 임시 storage(회사 · 개인 층) + 견본 시스템 집계표(`write_sample_table`)로만 봄 — 실제 양식은 안 읽음(시스템 층 두 자리 `store.FOLDER` · `layers.SYSTEM_ROOT` 모두 사본). -자재 코드는 실제 M01 마스터에 있는 MT 키를 씀(자재 조합 저장이 마스터 대조를 함). """ from __future__ import annotations @@ -18,7 +17,6 @@ from fastapi.testclient import TestClient from common_util.common_util_auth import verify_session from config import config_system -from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layers_router @@ -29,14 +27,6 @@ from resources.tester.test_m02_structure import write_sample_table TABLE = "table/구조물집계표" -def _material() -> dict[str, Any]: - for data in m01.loaded()[0].values(): - for row in data.get("줄") or []: - if str(row.get("키", "")).startswith("MT"): - return {"코드": row["키"], "이름": row.get("이름"), "규격": None, "단위": None} - raise AssertionError("마스터에 자재 키가 없음") - - @pytest.fixture() def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: system = tmp_path / "system" @@ -78,7 +68,7 @@ def test_시스템_층은_그대로_항목_목록은_SS_줄(world): ss = store.read_combos()["줄"] assert [r["key"] for r in rows] == [r["키"] for r in ss] and rows[0]["key"] == "SS000001" assert set(rows[0]) == {"key", "names", *items_router.items_store.ITEM_FIELDS} - assert set(rows[0]["names"]) == {"일위대가", "자재조합", "도번", "면적번", "재료번"} + assert set(rows[0]["names"]) == {"일위대가", "도번", "면적번", "재료번"} assert client.get("/api/m02/layers/system/structure-combos").json() == store.read_combos() # 시스템 층 쓰기는 SYSTEM_ADMIN 만 assert client.post("/api/m02/layers/system/structures/rv").status_code == 403 @@ -102,7 +92,7 @@ def test_회사_층_첫_진입_뼈대와_쓰기_권한(world): == json.loads((world["system"] / "table/구조물집계표.json").read_text("utf-8"))["층"] ) assert json.loads((root / "구조물집계표조합.json").read_text("utf-8"))["다음"] == 1 - assert json.loads((root / "자재조합.json").read_text("utf-8"))["다음"] == 1 + assert not (root / "자재조합.json").exists() # 자재 조합은 없앰 url = f"/api/m02/layers/company/templates/{TABLE}" got = client.get(url).json() got["문서"]["열"].append(COL) @@ -134,7 +124,8 @@ def test_개인_층은_본인만_회사_없으면_409(world): assert [(g["layer"], g["items"]) for g in groups][1:] == [("company", []), ("personal", [])] assert len(groups[0]["items"]) == len(store.read_combos()["줄"]) # 마스터는 보임 made = lonely.post( - "/api/m02/layers/personal/material-combos", json={"줄": {"이름": "a", "담은자재": []}} + "/api/m02/layers/personal/structures/p1/use-combo", + json={"종류": "일위대가", "키": "UA000001"}, ) assert made.status_code == 409 and "회사부터" in made.json()["detail"] @@ -146,17 +137,12 @@ def test_회사_항목을_개인으로_복사(world): drawing = admin.get(f"{base}/templates/structure/cw").json() doc = {**drawing["문서"], "도면": {**drawing["문서"]["도면"], "entities": [{"type": "line"}]}} admin.put(f"{base}/templates/structure/cw", json={"판": drawing["판"], "문서": doc}) - 판 = admin.get(f"{base}/material-combos").json()["판"] - row = {"이름": "회사 자재", "담은자재": [_material()]} - um = admin.post(f"{base}/material-combos", json={"판": 판, "줄": row}).json()["키"] - admin.post(f"{base}/structures/cw/use-combo", json={"종류": "자재조합", "키": um}) admin.post(f"{base}/structures/cw/use-combo", json={"종류": "일위대가", "키": "UA000004"}) row = admin.get("/api/m02/items?layer=company").json()[0] ss = row["key"] ua_name = combos.find("UA000004")["이름"] assert row["names"] == { # 키 대신 화면 이름 "일위대가": ua_name, - "자재조합": "회사 자재", "도번": "돌기슭막이 찰쌓기 회사형", "면적번": "돌기슭막이 찰쌓기 회사형", "재료번": "돌기슭막이 찰쌓기 회사형", @@ -175,13 +161,10 @@ def test_회사_항목을_개인으로_복사(world): assert copy["도면"]["entities"] == [{"type": "line"}] and copy["열"] == "cw" col = next(c for c in user.get(f"{mine}/templates/{TABLE}").json()["문서"]["열"]) assert col["본뜬"] == {"layer": "company", "owner": "7", "key": ss} - mat = user.get(f"{mine}/material-combos/{col['자재조합']}").json() - assert mat["이름"] == "회사 자재" and mat["담은자재"][0]["코드"] == _material()["코드"] - # 또 복사하면 새 열 id · 새 SS 키 · 자재 조합 이름 (2) + assert "자재조합" not in col # 옛 자재 조합 칸은 안 옮김 + # 또 복사하면 새 열 id · 새 SS 키 again = user.post("/api/m02/layers/personal/items/copy", json=body).json() assert (again["열"], again["key"]) == ("cw_2", "SP42-000002") - names = [r["이름"] for r in user.get(f"{mine}/material-combos").json()["줄"]] - assert names == ["회사 자재", "회사 자재 (2)"] # 마스터 항목도 개인으로 · 없는 키 404 master = {"from_layer": "system", "key": "SS000001"} assert user.post("/api/m02/layers/personal/items/copy", json=master).status_code == 200 diff --git a/resources/tester/test_m02_matset.py b/resources/tester/test_m02_matset.py deleted file mode 100644 index c38addf76..000000000 --- a/resources/tester/test_m02_matset.py +++ /dev/null @@ -1,140 +0,0 @@ -"""M02 자재 조합(`/api/m02/material-combos…` · 마스터 템플릿 `자재조합.json` · 키 UM). - -못박는 것 - - 정본 표는 마스터 템플릿 자리 · 틀대로(머리 「다음」) · 키 대장에 UM 없음 · M01 길 없음. - - 새로 만들면 키 = 표 「다음」 번호 · 열 한 벌 · 담은 자재는 네 칸 · 지운 키는 다시 안 씀. - - 없는 자재 코드 · 같은 자재 두 번 · 이름 없음 · 이름 겹침 · 없는 본뜬키는 막음. - - 고치기 · 지우기는 낡은 판이면 409 · 본뜬키는 고쳐도 그대로 · 쓰는 열이 있으면 못 지움. - - 정본은 읽기만 함(사본 폴더 · 견본 집계표). -""" - -from __future__ import annotations - -import json -from pathlib import Path - -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from M01_MasterData import M01_MasterData_Router as m01_router -from M01_MasterData import M01_MasterData_Store as m01 -from M02_MasterTemplete import M02_MasterTemplete_Matset as matset -from M02_MasterTemplete import M02_MasterTemplete_Store as store -from M02_MasterTemplete.M02_MasterTemplete_Router import router -from resources.tester.test_m02_structure import write_sample_table - -ROOT = Path(__file__).resolve().parents[2] -M1, M2 = ( - {"코드": "MT000008", "이름": "시멘트", "규격": "40kg", "단위": "포"}, - {"코드": "MT000009", "이름": "모래", "규격": "", "단위": "㎥"}, -) -URL = "/api/m02/material-combos" -TABLE = "/api/m02/templates/table/구조물집계표" - - -@pytest.fixture -def client(tmp_path: Path, monkeypatch) -> TestClient: - write_sample_table(tmp_path) - monkeypatch.setattr(store, "FOLDER", tmp_path) - app = FastAPI() - app.include_router(router) - return TestClient(app) - - -def _new(client: TestClient, name: str, mats: list[dict], 판: str | None = None, **more): - if 판 is None: - 판 = client.get(URL).json()["판"] - return client.post(URL, json={"판": 판, "줄": {"이름": name, "담은자재": mats, **more}}) - - -def test_정본_표는_마스터_템플릿_자리_틀대로_UM_은_대장_밖() -> None: - path = ROOT / "resources/master_template/자재조합.json" - data = json.loads(path.read_text(encoding="utf-8")) - assert data["그룹"] == "자재조합" and isinstance(data["다음"], int) - assert matset.mmc.check_file(path) == [] - assert not (ROOT / "resources/master_data/자재조합.json").exists() - book = (ROOT / "resources/master_data/_키대장.json").read_text(encoding="utf-8") - assert '"UM"' not in book.splitlines()[1] # 대장 머리 「다음」 - assert not any("matset" in getattr(r, "path", "") for r in m01_router.router.routes) - - -def test_새로_만들면_다음_번호_열_한_벌_지운_키는_다시_안_씀(client) -> None: - res = _new(client, "합판 자재", [{**M1, "덤": 1}]) - assert res.status_code == 200, res.text - made = res.json() - assert made["키"] == "UM000001" and tuple(made["줄"]) == matset.mmc.COLS - assert made["줄"]["담은자재"] == [M1] and made["줄"]["본뜬키"] is None - listed = client.get(URL).json() - assert listed["판"] == made["판"] and [r["키"] for r in listed["줄"]] == ["UM000001"] - assert client.get(f"{URL}/UM000001").json()["이름"] == "합판 자재" - assert client.delete(f"{URL}/UM000001", params={"판": made["판"]}).status_code == 200 - again = _new(client, "다음 것", [M2]).json() - assert again["키"] == "UM000002" # 지운 번호는 다시 안 씀 - assert json.loads((store.FOLDER / matset.FILE).read_text(encoding="utf-8"))["다음"] == 3 - - -def test_없는_자재_두_번_이름_본뜬키는_막는다(client) -> None: - assert _new(client, "없는 코드", [{**M1, "코드": "MT999999"}]).status_code == 422 - assert _new(client, "두 번", [M1, M1]).status_code == 422 - assert _new(client, " ", [M1]).status_code == 400 - assert _new(client, "빈 것", []).status_code == 422 - assert _new(client, "없는 원본", [M1], 본뜬키="UM999999").status_code == 400 - ok = _new(client, "원본", [M1]).json()["키"] - copy = _new(client, "본뜬 것", [M1, M2], 본뜬키=ok).json() - assert copy["줄"]["본뜬키"] == ok and copy["키"] != ok - dup = _new(client, "원본", [M2]) # 이름 겹침 — 파일명 · 키 없이 쉬운 말 한 줄 - assert dup.status_code == 422 and dup.json()["detail"]["errors"] == [ - "「원본」 이름의 자재 조합이 이미 있음 — 다른 이름으로" - ] - long = "긴" * 40 - assert _new(client, long, [M1]).status_code == 200 - cut = _new(client, long, [M2]).json()["detail"]["errors"][0] - assert cut.startswith("「" + "긴" * 30 + "…」") and matset.FILE not in cut and "UM0" not in cut - assert _new(client, "긴" * 61, [M1]).json()["detail"]["errors"] == ["이름이 너무 김 — 60자까지"] - assert _new(client, "낡은 판", [M2], 판="0000").status_code == 409 - - -def test_고치기는_낡은_판이면_409_본뜬키는_그대로(client) -> None: - src = _new(client, "원본", [M1]).json()["키"] - made = _new(client, "고칠 것", [M1], 본뜬키=src).json() - row = {**made["줄"], "이름": "고침", "담은자재": [M1, M2], "본뜬키": None} - res = client.put(f"{URL}/{made['키']}", json={"판": made["판"], "줄": row}) - assert res.status_code == 200, res.text - assert res.json()["줄"]["이름"] == "고침" and res.json()["줄"]["본뜬키"] == src - assert client.put(f"{URL}/{made['키']}", json={"판": made["판"], "줄": row}).status_code == 409 - assert client.put(f"{URL}/UM999999", json={"판": "", "줄": row}).status_code == 404 - - -def test_쓰는_열이_있으면_못_지우고_없으면_지워진다(client) -> None: - made = _new(client, "쓰는 것", [M1]).json() - table = client.get(TABLE).json() - for col in table["문서"]["열"]: - if col["id"] == "rv": - col["자재조합"] = made["키"] - assert client.put(TABLE, json={"판": table["판"], "문서": table["문서"]}).status_code == 200 - res = client.delete(f"{URL}/{made['키']}", params={"판": made["판"]}) - assert client.get(URL).json()["쓰는곳"] == {made["키"]: ["돌기슭막이 (형태마다) H=(높이마다)"]} - assert res.status_code == 409 and res.json()["detail"]["users"] == [ - "돌기슭막이 (형태마다) H=(높이마다)" - ] - rows = client.get("/api/m02/structure-combos").json()["줄"] - assert next(r for r in rows if r["열"] == "rv")["자재조합"] == made["키"] # SS 장부 칸 - table = client.get(TABLE).json() - for col in table["문서"]["열"]: - col.pop("자재조합", None) - assert client.put(TABLE, json={"판": table["판"], "문서": table["문서"]}).status_code == 200 - assert client.delete(f"{URL}/{made['키']}", params={"판": "낡음"}).status_code == 409 - assert client.delete(f"{URL}/{made['키']}", params={"판": made["판"]}).status_code == 200 - assert client.get(f"{URL}/{made['키']}").status_code == 404 - - -def test_M01_마스터는_UM_을_모른다() -> None: - assert "UM" not in m01.cm.mk.IDS and "자재조합" not in m01.cm.mk.ROW_ORDER - - -def test_새로_본떠_만들기는_열자마자_이름_칸에_초점() -> None: - ui = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Matset.ts").read_text(encoding="utf-8") - assert "'[data-matset-field=\"이름\"]')?.focus()" in ui.split("if (isNew) {")[1] - copy = ui.split("btnCopy.addEventListener")[1].split("});")[0] - assert '이름: ""' in copy # 본떠 만들기는 이름 칸 비움 diff --git a/resources/tester/test_m02_structure_options.py b/resources/tester/test_m02_structure_options.py index 6df8d180b..8d37c651b 100644 --- a/resources/tester/test_m02_structure_options.py +++ b/resources/tester/test_m02_structure_options.py @@ -128,9 +128,9 @@ def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에 " ...(extras ? [extras.옵션, extras.도면] : []),", ] assert "\n".join(order) in side - # 자재 조합을 열면 이 줄 · 일위대가 고름 지움 / 이 줄을 열면 자재 조합 · 일위대가 고름 지움 - assert "onOpen: () => (options.clear(), labor?.clear(), paint())" in side - assert "onOpen: () => (matset.clear(), labor?.clear(), paint())" in side + # 일위대가를 열면 이 줄 고름 지움 / 이 줄을 열면 일위대가 고름 지움 + assert "onOpen: () => (options.clear(), paint())" in side + assert "onOpen: () => (labor?.clear(), paint())" in side paint = side.split("function paint(): void {")[1].split("\n }\n")[0] assert "if (opt.getOpen()) options.clear();" in paint # 양식을 열면 이 줄 고름 지움 api = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts").read_text("utf-8") diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index bb9b3341f..ea853da66 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -154,9 +154,9 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우 assert "createSelectField" not in side # 새로 = 종류 고르기 없이 이름만 assert "cur?.종류 !== kind" in side # 본떠 · 지우기 = 그 컨테이너에서 고른 것이 있을 때만 assert "i.이름 === STRUCTURE_TABLE" in side # 표는 구조물집계표 한 줄만 - # 구조물 도면 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘) + # 구조물 도면 · 면적산출 · 재료산출 = 구조물 집계 양식 컨테이너 안(같이 접힘) · 자재 조합 없앰 summary = side.split("const summary = section(")[1].split(");")[0] - assert "COLUMN_KINDS.map" in summary and "matsetSection" in summary + assert "COLUMN_KINDS.map" in summary and "matset" not in side assert 'M02_KindTable: ["구조물 조합"' in _text( # 화면 이름(26-7a) ROOT / "ui_template" / "ui_template_locale_m2.ts" ) @@ -170,8 +170,8 @@ def test_면적_재료_오른쪽_조합_패널은_공용_틀_손잡이(): assert "ui-sidebar-card" in cpanel and "setColumnCombo" in cpanel main = _text(MAIN) assert "comboSlot" not in main # 조합 줄은 패널 「이 열의 조합」 칸으로 - assert "cpanel.setMine(matPanel.root)" in main # 담은 자재 표도 패널 안 - assert '"aside"' not in _text(M02 / "M02_MasterTemplete_Materials.ts") + assert "matPanel" not in main # 자재 조합 판은 없앰 + assert not (M02 / "M02_MasterTemplete_Materials.ts").exists() def test_첫_화면과_닫은_뒤는_구조물집계표(): @@ -189,9 +189,9 @@ def test_좌측_고름_표시는_열린_것_한_줄뿐(): body = side.split(fn)[1].split("\n }\n")[0] assert "paint" not in body, fn # 여기서 한쪽만 그리면 두 줄이 남음 assert re.search(r"sel = next;\s*onOpened\?\.\(\);", _text(MAIN)) - # 자재 조합 상세도 같은 한 곳 — 열면 양식 · 열 줄 고름 지움 · 양식을 열면 자재 조합 고름 지움 + # 옵션 상세도 같은 한 곳 — 열면 양식 · 열 줄 고름 지움 · 양식을 열면 옵션 고름 지움 assert re.search(r"sel = null;\s*onOpened\?\.\(\);", _text(MAIN)) - assert "if (opt.getOpen()) matset.clear();" in side.split("function paint(): void {")[1] + assert "if (opt.getOpen()) options.clear();" in side.split("function paint(): void {")[1] # 편집 칸이 열 때마다 좌측 `paint`(27-2b 뒤 = 일위대가 화면 해시 맞춤도 같은 자리) assert re.search(r"\(\) => \{\s*side\.paint\(\);", _text(M02 / "M02_MasterTemplete_UI_Page.ts")) diff --git a/resources/tester/test_structures_summary_snapshot.py b/resources/tester/test_structures_summary_snapshot.py index 261206bc7..8903f7ce8 100644 --- a/resources/tester/test_structures_summary_snapshot.py +++ b/resources/tester/test_structures_summary_snapshot.py @@ -34,7 +34,7 @@ PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관 def _copy_layer(target: Path) -> None: target.mkdir(parents=True) - for name in (layers.COMBO_FILE, "자재조합.json"): + for name in (layers.COMBO_FILE,): shutil.copy2(REAL / name, target / name) for kind in ("table", "structure", "area", "material"): shutil.copytree(REAL / kind, target / kind) diff --git a/resources/tester/ui/item-picker-data.test.ts b/resources/tester/ui/item-picker-data.test.ts index 37f0c5db6..057c13c14 100644 --- a/resources/tester/ui/item-picker-data.test.ts +++ b/resources/tester/ui/item-picker-data.test.ts @@ -41,7 +41,6 @@ const row = ( 분류3: 분류[2], 단위: "m", 일위대가: "UA000004", - 자재조합: "UM000001", 도번: "SD01010100", 면적번: "SA01010100", 재료번: "SM01010100", @@ -54,7 +53,6 @@ const rows: PickerRow[] = [ row("SS000001", "rv", "돌기슭막이", ["메쌓기", "1.2m", "기초유"], "system", { names: { 일위대가: "돌기슭막이 메쌓기 일위대가", - 자재조합: null, 도번: "돌기슭막이 도면", 면적번: "돌기슭막이 면적산출", 재료번: "돌기슭막이 재료산출", @@ -221,16 +219,20 @@ assert.deepEqual(pathIds(rows[0], labels), [ "system|돌기슭막이|메쌓기|1.2m", ]); -// 폴더 모양 — 다섯 줄 · 이름은 서버 names · 없으면 키만(작은 글) · 둘 다 없으면 빈 키 +// 폴더 모양 — 네 줄 · 이름은 서버 names · 없으면 키만(작은 글) · 둘 다 없으면 빈 키 const lines = folderLines(rows[0]); assert.deepEqual( lines.map((l) => l.kind), - ["drawing", "area", "material", "combo", "unitPrice"], + ["drawing", "area", "material", "unitPrice"], ); assert.deepEqual(lines[0], { kind: "drawing", name: "돌기슭막이 도면", key: "SD01010100" }); -assert.deepEqual(lines[3], { kind: "combo", name: null, key: "UM000001" }); // names 에 없음 +assert.deepEqual(lines[3], { + kind: "unitPrice", + name: "돌기슭막이 메쌓기 일위대가", + key: "UA000004", +}); assert.deepEqual(folderLines(rows[1])[0], { kind: "drawing", name: null, key: "SD01010100" }); // names 자체가 없음 -assert.deepEqual(folderLines({ key: "SS1", 열: "x" })[4], { +assert.deepEqual(folderLines({ key: "SS1", 열: "x" })[3], { kind: "unitPrice", name: null, key: "", diff --git a/ui_template/sheet/ui_template_sheet_ops.ts b/ui_template/sheet/ui_template_sheet_ops.ts index 0347aa999..1dc2db7e8 100644 --- a/ui_template/sheet/ui_template_sheet_ops.ts +++ b/ui_template/sheet/ui_template_sheet_ops.ts @@ -120,15 +120,13 @@ export function addLevel( "c", doc.열.map((c) => c.id), ); - // 원본(`after`) 열을 따라감 — 일위대가 · 자재조합 같은 키 · 구조물 파일은 저장 때 서버가 복사 + // 원본(`after`) 열을 따라감 — 일위대가 같은 키 · 구조물 파일은 저장 때 서버가 복사 doc.열.splice(at + 1, 0, { id, 머리: head, 단위: base.단위 ?? null, 꼴: "수", - ...(base.꼴 !== "글" - ? { 본뜬열: base.id, 일위대가: base.일위대가 ?? null, 자재조합: base.자재조합 ?? null } - : {}), + ...(base.꼴 !== "글" ? { 본뜬열: base.id, 일위대가: base.일위대가 ?? null } : {}), }); return id; } @@ -141,7 +139,7 @@ export function addFirstColumn(doc: SheetDoc, depth: number, label: string): str ); const head: (string | null)[] = [label]; while (head.length < depth) head.push(""); - doc.열.push({ id, 머리: head, 단위: null, 꼴: "수", 일위대가: null, 자재조합: null }); + doc.열.push({ id, 머리: head, 단위: null, 꼴: "수", 일위대가: null }); return id; } diff --git a/ui_template/sheet/ui_template_sheet_types.ts b/ui_template/sheet/ui_template_sheet_types.ts index b89a9d725..993133a52 100644 --- a/ui_template/sheet/ui_template_sheet_types.ts +++ b/ui_template/sheet/ui_template_sheet_types.ts @@ -35,8 +35,6 @@ export interface SheetColumn { 바인딩?: SheetBinding | null; /** 일위대가 조합 키. */ 일위대가?: string | null; - /** 자재 조합 키(`UM`). */ - 자재조합?: string | null; /** 분류 더하기로 복사한 원본 열 id — 저장 때 서버가 원본 열 내용을 새 열로 복사하고 뗌. */ 본뜬열?: string | null; /** 마스터 한 칸 → 프로젝트에서 설계 값마다 열. */ diff --git a/ui_template/ui_template_item_picker_data.ts b/ui_template/ui_template_item_picker_data.ts index 771957a4f..b7224e981 100644 --- a/ui_template/ui_template_item_picker_data.ts +++ b/ui_template/ui_template_item_picker_data.ts @@ -15,7 +15,7 @@ export interface SummaryItem { } /** 항목 줄이 가리키는 키 칸 — 서버가 `names` 로 이름을 함께 주면 이름을 보임 */ -export type NameField = "일위대가" | "자재조합" | "도번" | "면적번" | "재료번"; +export type NameField = "일위대가" | "도번" | "면적번" | "재료번"; /** 목록 한 줄(21-1) — 서버는 `?all=1` 을 `[{layer, owner, items}]` 로 주고, 줄마다 `layer` · `owner` 를 붙여 폄. * `열` 은 집계표 열 id(rv …) — 보이는 이름은 종류 · 분류를 이은 `itemName`. */ @@ -28,7 +28,6 @@ export interface PickerRow { 분류3?: string; 단위?: string; 일위대가?: string; - 자재조합?: string; 도번?: string; 면적번?: string; 재료번?: string; @@ -102,13 +101,7 @@ export function filterRows(rows: PickerRow[], text: string): PickerRow[] { const words = text.trim().toLowerCase().split(/\s+/).filter(Boolean); if (!words.length) return rows; return rows.filter((row) => { - const hay = [ - itemName(row), - row.key, - row.일위대가, - row.자재조합, - ...Object.values(row.names ?? {}), - ] + const hay = [itemName(row), row.key, row.일위대가, ...Object.values(row.names ?? {})] .join(" ") .toLowerCase(); return words.every((word) => hay.includes(word)); @@ -174,7 +167,7 @@ const ancestorIds = (layer: PickerLayer, path: string[]): string[] => [ export const pathIds = (row: PickerRow, _labels?: TreeLabels): string[] => ancestorIds(layerOf(row), pathOf(row).slice(0, -1)); -export type FolderKind = "drawing" | "area" | "material" | "combo" | "unitPrice"; +export type FolderKind = "drawing" | "area" | "material" | "unitPrice"; export interface FolderLine { kind: FolderKind; @@ -188,11 +181,10 @@ const FOLDER_FIELDS: readonly [FolderKind, NameField][] = [ ["drawing", "도번"], ["area", "면적번"], ["material", "재료번"], - ["combo", "자재조합"], ["unitPrice", "일위대가"], ]; -/** 오른쪽 폴더 모양 — 다섯 줄 모두 서버의 키 칸 + `names` 의 이름 */ +/** 오른쪽 폴더 모양 — 네 줄 모두 서버의 키 칸 + `names` 의 이름 */ export function folderLines(row: PickerRow): FolderLine[] { return FOLDER_FIELDS.map(([kind, field]) => ({ kind, diff --git a/ui_template/ui_template_item_picker_detail.ts b/ui_template/ui_template_item_picker_detail.ts index 2f7ba2cf8..f5b34eaa8 100644 --- a/ui_template/ui_template_item_picker_detail.ts +++ b/ui_template/ui_template_item_picker_detail.ts @@ -110,13 +110,11 @@ const TEXT = { calc: "복사본 계산 — 계 {total}원 · 빠진 입력 {gaps}개", calcStuck: " · 멈춘 로직 {stuck}개", noCopy: "복사본에 없음", - combo: "자재조합", price: "일위대가", parts: { 도면: "도면", 면적: "면적산출", 재료: "재료산출", - 자재조합: "자재조합", 입력연결: "선택 입력 자리", } as Record, }; @@ -153,9 +151,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { const where = [opts.layerLabels[layerOf(row)], ...pathOf(row).slice(0, -1)] .filter(Boolean) .join(" › "); - const pairs = folderLines(row).filter( - (line) => line.kind === "combo" || line.kind === "unitPrice", - ); + const pairs = folderLines(row).filter((line) => line.kind === "unitPrice"); head.replaceChildren( el("h4", { className: "ui-item-picker__name", @@ -166,9 +162,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { ...(row.단위 ? [small(`${opts.unit} ${row.단위}`)] : []), ...pairs .filter((line) => line.name || line.key) - .map((line) => - small(`${line.kind === "combo" ? TEXT.combo : TEXT.price} ${line.name || line.key}`), - ), + .map((line) => small(`${TEXT.price} ${line.name || line.key}`)), ); }; diff --git a/ui_template/ui_template_item_picker_project.ts b/ui_template/ui_template_item_picker_project.ts index 27366224d..37a4186b1 100644 --- a/ui_template/ui_template_item_picker_project.ts +++ b/ui_template/ui_template_item_picker_project.ts @@ -21,8 +21,7 @@ import { import { layerOf, ownerOf, type PickerRow } from "./ui_template_item_picker_data"; /** 복사본 폴더 안 부분 — 25-1b 약속 그대로 */ -export type ProjectPart = - "도면" | "면적" | "재료" | "자재조합" | "입력연결" | "입력값" | "고른자재"; +export type ProjectPart = "도면" | "면적" | "재료" | "입력연결" | "입력값" | "고른자재"; /** 원본 종류 → 복사본 부분 */ export const PART_OF: Record<"structure" | "area" | "material", ProjectPart> = { diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index eff4717b9..37b450087 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -88,7 +88,6 @@ export const ui_locales_m2 = { M02_PickerDrawing: ["도면", "Drawing"], M02_PickerArea: ["면적산출", "Area takeoff"], M02_PickerMaterial: ["재료산출", "Material takeoff"], - M02_PickerCombo: ["자재조합", "Material combo"], M02_PickerUnitPrice: ["일위대가", "Unit price"], M02_PickerUnit: ["단위", "Unit"], M02_PickerNone: ["없음", "None"],