diff --git a/M01_MasterData/M01_MasterData_Store_Combo.py b/M01_MasterData/M01_MasterData_Store_Combo.py index cb147ba2a..c8eed5a3d 100644 --- a/M01_MasterData/M01_MasterData_Store_Combo.py +++ b/M01_MasterData/M01_MasterData_Store_Combo.py @@ -227,7 +227,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 9924f16c1..d0b50ab51 100644 --- a/M02_MasterTemplete/M02_Item_Copy.py +++ b/M02_MasterTemplete/M02_Item_Copy.py @@ -39,7 +39,6 @@ from M02_MasterTemplete.M02_Item_Snapshot import ( ) from M02_MasterTemplete.M02_Item_Source import ( LABOR_FILE, - LINK_FILE, PRICE_FILE, build_source, read_json, @@ -50,16 +49,12 @@ from M02_MasterTemplete.M02_Item_Source import ( KINDS = { "도면": "도면.json", "면적": "면적.json", - "재료": "재료.json", - "자재조합": "자재조합.json", - "입력연결": LINK_FILE, "입력값": VALUES_FILE, "고른자재": PICKS_FILE, "일위대가": LABOR_FILE, # 읽기만 — 일위대가는 원본 층에서 고침(39-1c) } -#: 뼈대 검사에 쓰는 층 양식 종류(도면 · 면적산출 · 재료산출) -SKELETON = {"도면": "structure", "면적": "area", "재료": "material"} -SEATS = ("면적", "재료", "선택", None) +#: 뼈대 검사에 쓰는 층 양식 종류(도면 · 면적산출) +SKELETON = {"도면": "structure", "면적": "area"} _MT = re.compile(r"^MT\d{6}$") @@ -160,17 +155,6 @@ def _check_picks(content: Any) -> None: raise ValueError(f"고른자재 「{logic}」 {at} → 「{key}」 — 차례는 수 · 값은 MT 키") -def _check_links(content: Any) -> None: - rows = content.get("줄") if isinstance(content, dict) else None - if not isinstance(rows, list): - raise ValueError("입력연결은 {판, 줄: [...]} 이어야 합니다.") - for row in rows: - if not isinstance(row, dict) or not row.get("로직") or not row.get("입력"): - raise ValueError("입력연결 줄은 {로직, 입력, 자리, 알약?} 이어야 합니다.") - if row.get("자리") not in SEATS: - raise ValueError(f"입력연결 자리 「{row.get('자리')}」 — 면적 · 재료 · 선택 · 빈 값") - - def check(kind: str, content: Any) -> None: if kind == "일위대가": raise ValueError("일위대가는 원본(마스터 · 회사 · 개인) 층에서 고침 — 프로젝트는 넣을 값만") @@ -178,8 +162,6 @@ def check(kind: str, content: Any) -> None: _check_values(content) elif kind == "고른자재": _check_picks(content) - elif kind == "입력연결": - _check_links(content) elif kind in SKELETON: layers.check_skeleton(SKELETON[kind], content) elif not isinstance(content, dict): @@ -194,13 +176,13 @@ def write_file( content: Any, recalc: Callable[[dict], dict] | None = None, ) -> dict[str, Any]: - """판이 같을 때만 씀 — 면적 · 재료는 `recalc`(서버 계산값)를 거친 것을 씀.""" + """판이 같을 때만 씀 — 면적은 `recalc`(서버 계산값)를 거친 것을 씀.""" target = folder_of(project_root, folder) check(kind, content) current = version_of(read_json(target / KINDS[kind])) if version != current: raise Stale(current) - if recalc is not None and kind in ("면적", "재료"): + if recalc is not None and kind == "면적": content = recalc(content) atomic_write_json(target / KINDS[kind], content) manifest = read_json(target / MANIFEST_NAME) or {} @@ -248,7 +230,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..453dce6fb 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..2ffd79765 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 키는 저장소가 -줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 재료 · 자재조합 · 입력연결을 담음 — +개인 층 새 항목 = 구조물집계표 새 열 하나 + 빈 도면 · 면적산출(번호 · SS 키는 저장소가 +줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적을 담음 — 단가 묶음 · 입력값 · 고른자재는 프로젝트 몫이라 안 가져감. 쓰기는 지금 층 저장소 함수(`store.at` · -`store.write` · `matset.create` · 항목 연결 `write_file`) — 나무 고침(`M02_MasterTemplete_Tree`)과 +`store.write`) — 나무 고침(`M02_MasterTemplete_Tree`)과 같은 집계표 저장 한 줄기(경로 · 머리 줄 수 맞춤 · 번호 · SS 장부)를 탐(PLAN 26-2a). 권한(본인 개인 층 · 회사 없음 409)은 부르는 쪽 몫. """ @@ -15,16 +15,14 @@ from pathlib import Path 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 read_json #: 복사본 파일 → 층 양식 종류 -PART_KINDS = (("도면.json", "structure"), ("면적.json", "area"), ("재료.json", "material")) +PART_KINDS = (("도면.json", "structure"), ("면적.json", "area")) def heads_of(path: list[str], name: str | None) -> list[str]: @@ -45,17 +43,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 +64,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 +73,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) # 같은 앞마디 형제 곁(제목행 병합이 이어지게) @@ -107,9 +92,6 @@ def create_item( body = store._server_values(body) with store._LOCK: atomic_write_json(store._path(kind, new_id), body) - links = read_json(copy / LINK_FILE) if copy else None - if isinstance(links, dict) and links.get("줄"): - inputs_store.write_file(new_id, {"판": 1, "줄": links["줄"]}) ss = next(r for r in store.read_combos()["줄"] if r.get("열") == new_id) return {"key": ss["키"], "열": new_id, "이름": tree.name_of(heads)} diff --git a/M02_MasterTemplete/M02_Item_Snapshot.py b/M02_MasterTemplete/M02_Item_Snapshot.py index 6d52b6989..44e7eeaa6 100644 --- a/M02_MasterTemplete/M02_Item_Snapshot.py +++ b/M02_MasterTemplete/M02_Item_Snapshot.py @@ -1,7 +1,7 @@ """구조물 집계 항목 복사본 — 프로젝트 `templates/structures/{구조물 id}/` (PLAN 25-2b · 26-2b). -복사본은 **구조물마다 하나** — 같은 항목을 고른 구조물 둘도 각자 도면 · 면적 · 재료 · 자재조합 · -일위대가 · 입력연결 · 입력값 · 고른자재 · 단가를 가짐(길이 · 소재 · 높이 · 옵션이 구조물마다 다름). +복사본은 **구조물마다 하나** — 같은 항목을 고른 구조물 둘도 각자 도면 · 면적 · +일위대가 · 입력값 · 고른자재 · 단가를 가짐(길이 · 소재 · 높이 · 옵션이 구조물마다 다름). 폴더 이름 = 구조물 id(`structure_id`) · 관 지점은 `pipe@측점`. [저장](`PUT /route/structures` · `PUT /drainage/pipe-points`) 뒤 서버가 맞춤: @@ -32,7 +32,6 @@ from common_util.common_util_summary_item import snapshot_folder_name from M02_MasterTemplete import M02_Template_Layers as layers from M02_MasterTemplete.M02_Item_Source import ( LABOR_FILE, - LINK_FILE, PRICE_FILE, build_source, item_path, @@ -236,28 +235,24 @@ def safe_sync(project_root: str | Path) -> None: def complete_old(folder: Path) -> bool: - """옛 복사본(21-3 · 새 파일 없음)을 채움 — 입력연결 · 단가는 지금 원본에서 · 값은 빈 것. + """옛 복사본(21-3 · 새 파일 없음)을 채움 — 단가는 지금 원본에서 · 값은 빈 것. 옛 내용 파일(집계 · 도면 …)은 그대로 둠. 채웠으면 True(manifest 에 `옮겨받음` 날).""" manifest = read_json(folder / MANIFEST_NAME) if not isinstance(manifest, dict): return False lacking = [ - name - for name in (LINK_FILE, PRICE_FILE, VALUES_FILE, PICKS_FILE) - if not (folder / name).is_file() + name for name in (PRICE_FILE, VALUES_FILE, PICKS_FILE) if not (folder / name).is_file() ] if not lacking and "원본판" in manifest: return False - need_source = LINK_FILE in lacking or PRICE_FILE in lacking + need_source = PRICE_FILE in lacking try: source = build_source(manifest["원본"]) if need_source else {} except LookupError: - source = {} # 원본이 지워짐 — 빈 연결 · 빈 단가로(계산은 멈춤으로 알림) + source = {} # 원본이 지워짐 — 빈 단가로(계산은 멈춤으로 알림) for name in lacking: - atomic_write_json( - folder / name, source.get(name, {"판": None, "줄": []} if name == LINK_FILE else {}) - ) + atomic_write_json(folder / name, source.get(name, {})) kept = { name: read_json(folder / name) for name in sorted(p.name for p in folder.glob("*.json")) diff --git a/M02_MasterTemplete/M02_Item_Source.py b/M02_MasterTemplete/M02_Item_Source.py index 961100a1f..73fc41052 100644 --- a/M02_MasterTemplete/M02_Item_Source.py +++ b/M02_MasterTemplete/M02_Item_Source.py @@ -1,11 +1,9 @@ """집계 항목 원본 — 층(시스템 · 회사 · 개인)에서 항목 내용을 모음 (PLAN 21-1 · 25-1b · 25-2b). 모으는 것 = `{파일 이름: 내용}`: - `집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `재료.json` SM · - `자재조합.json` UM 줄 · + `집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄) + 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) · - `입력연결.json` 층의 `inputs/<열id>.json`(없으면 빈 연결) · `단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT) `원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎. 프로젝트가 고치는 값(`입력값.json` · `고른자재.json`)은 원본이 아니라 여기서 안 모음. @@ -22,11 +20,9 @@ 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")) +#: 열 id → 스냅샷 파일(구조물 도면 · 면적산출) +PART_FILES = (("structure", "도면.json"), ("area", "면적.json")) LABOR_KIND = "labor" # 열 id → 일위대가 문서(39-1c) — 스냅샷은 `LABOR_FILE` 에 감싸서 -LINK_FILE = "입력연결.json" PRICE_FILE = "단가.json" LABOR_FILE = "일위대가.json" @@ -123,20 +119,6 @@ def logic_rows(row: dict[str, Any]) -> dict[str, Any]: } -def input_links(root: Path, column_id: str) -> dict[str, Any]: - """층의 항목 연결 `inputs/<열id>.json`(sub4 `_Inputs.read_file`) · 없으면 빈 연결.""" - from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs - from M02_MasterTemplete import M02_MasterTemplete_Store as store - - if not column_id: - return {"판": None, "줄": []} - with store.at(root): - found = inputs.read_file(column_id) - if isinstance(found, dict) and isinstance(found.get("줄"), list): - return found - return {"판": None, "줄": []} - - def build_source(item: dict[str, Any]) -> dict[str, Any]: """원본 층 · 마스터에서 항목 내용을 모음 — SS 줄이 없으면 LookupError.""" from M02_MasterTemplete import M02_Item_Prices as prices @@ -153,10 +135,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 doc = read_json(root / LABOR_KIND / f"{column_id}.json") if column_id else None if isinstance(doc, dict): labor = labor_doc_rows(doc) @@ -164,7 +142,6 @@ def build_source(item: dict[str, Any]) -> dict[str, Any]: labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None if labor is not None: files[LABOR_FILE] = labor - files[LINK_FILE] = input_links(root, column_id) logic_keys = [str(one.get("키")) for one in (labor or {}).get("로직", [])] materials = [str(m.get("코드")) for m in ((labor or {}).get("줄") or {}).get("자재") or []] wanted = logic_keys + [code for code in materials if code] diff --git a/M02_MasterTemplete/M02_Labor_Inputs.py b/M02_MasterTemplete/M02_Labor_Inputs.py index 068c64b33..ccde25e9c 100644 --- a/M02_MasterTemplete/M02_Labor_Inputs.py +++ b/M02_MasterTemplete/M02_Labor_Inputs.py @@ -1,9 +1,9 @@ -"""M02 일위대가 입력 목록(39-3 · 옛 선택 입력 `M02_MasterTemplete_Inputs` 에서 옮김). +"""M02 일위대가 입력 목록(39-3). 담은 단가산출 로직마다 입력 전부 · 로직마다 갈라 보이고 이름이 같아도 합치지 않음 · 입력 목록은 그 로직 줄의 `입력` 그대로(하위 로직으로 넘겨지는 입력은 맨 위 로직 입력으로만) · 자동값 = -`master_auto.values`(보기용 · 저장 안 함). 일위대가 입력 모달(`GET /api/m02/logic-lib/inputs`)과 -옛 선택 입력 길(`inputs`)이 같이 씀. M01 은 읽기만 · 권한은 부르는 쪽 몫. +`master_auto.values`(보기용 · 저장 안 함). 일위대가 입력 모달(`GET /api/m02/logic-lib/inputs`)이 +씀. M01 은 읽기만 · 권한은 부르는 쪽 몫. """ from __future__ import annotations @@ -13,7 +13,6 @@ from typing import Any from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Combo as combo_store -from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos _SPEC = ("이름", "설명", "단위", "고르기", "범위") @@ -41,17 +40,6 @@ def _logics(codes: list[str]) -> tuple[list[dict], list[str]]: return found, missing -def combo_logics(ua: str | None) -> tuple[dict[str, Any] | None, list[dict], list[str]]: - """(조합 줄, [로직 줄 …], 빠진 로직 키) — 조합이 없거나 못 찾으면 (None, [], []).""" - if not ua: - return None, [], [] - row = combos.find(str(ua)) # 마스터 UA · 회사 UC · 개인 UP(27-2c) - if row is None: - return None, [], [] - found, missing = _logics(combo_store.mcb.logic_keys(row)) - return row, found, missing - - def _listed(logics: list[dict]) -> list[dict[str, Any]]: """로직 줄마다 `{로직, 이름, 결과단위, 입력: [{이름, 설명, 단위, 고르기?, 범위?, 자동값}]}`.""" whole = m01.loaded()[1] @@ -79,16 +67,3 @@ def logic_inputs(codes: list[str]) -> dict[str, Any]: """일위대가 입력 모달 — 로직 키들의 입력 목록 `{로직: [...], 빠진로직}`(키 차례 · 겹침 한 번)""" found, missing = _logics(list(dict.fromkeys(c for c in codes if c))) return _num({"로직": _listed(found), "빠진로직": missing}) - - -def inputs_of(ua: str | None) -> dict[str, Any]: - """옛 선택 입력 목록 — `{일위대가, 이름, 로직: [...], 빠진로직}` · 일위대가 없으면 로직 없음.""" - row, logics, missing = combo_logics(ua) - return _num( - { - "일위대가": ua or None, - "이름": row.get("이름") if row else None, - "로직": _listed(logics), - "빠진로직": missing, - } - ) diff --git a/M02_MasterTemplete/M02_Labor_Values.css b/M02_MasterTemplete/M02_Labor_Values.css index dca291790..02d9361ac 100644 --- a/M02_MasterTemplete/M02_Labor_Values.css +++ b/M02_MasterTemplete/M02_Labor_Values.css @@ -19,7 +19,6 @@ min-width: 0; } -.m02-inputval__places, .m02-inputval__missing { margin: 0; color: var(--color-text-secondary); diff --git a/M02_MasterTemplete/M02_Labor_Values.ts b/M02_MasterTemplete/M02_Labor_Values.ts index a7453b93a..1171ce553 100644 --- a/M02_MasterTemplete/M02_Labor_Values.ts +++ b/M02_MasterTemplete/M02_Labor_Values.ts @@ -1,16 +1,22 @@ /* ============================================================================= * M02_Labor_Values.ts - * 넣을 값 칸(39-3 · 옛 선택 입력 `M02_MasterTemplete_Inputs.ts` 에서 옮김) — 값 칸 규칙 한 벌: + * 넣을 값 칸(39-3) — 값 칸 규칙 한 벌: * 고르기 = 목록 · 범위 = 숫자(밖이면 붉은 글) · 빈 칸 = 자동값(흐리게 · placeholder). * · `createValueControl` — 칸 하나(일위대가 입력 모달 · 프로젝트 값 카드가 같이 씀) - * · `createInputsValueCard` — 프로젝트 고르기 모달 값 카드(옛 자리 `M02_MasterTemplete_Inputs.ts` 가 다시 내보냄) + * · `createInputsValueCard` — 프로젝트 고르기 모달 값 카드(담은 로직 입력 전부) * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; -import type { ItemInput, ItemInputs, ItemLogic, InputLinks } from "./M02_MasterTemplete_Api_Fetch"; +import type { ItemInput, ItemInputs, ItemLogic } from "./M02_MasterTemplete_Api_Fetch"; import { cardNote, cardShell } from "./M02_MasterTemplete_CardShell"; -import { inputKey, pickedGroups, seatsOf } from "./M02_MasterTemplete_Inputs_Model"; -import { choicesOf, isOutside, rangeOf, storedOf, type ValueDoc } from "./M02_Labor_Values_Model"; +import { + choicesOf, + inputKey, + isOutside, + rangeOf, + storedOf, + type ValueDoc, +} from "./M02_Labor_Values_Model"; import "./M02_Labor_Values.css"; const fill = (text: string, values: Record): string => @@ -82,20 +88,19 @@ export function createValueControl( } /* ── 프로젝트 복사본에서 값 받기(PLAN 25-2d) — 고르기 모달 카드 ───────────────────────────── - * 같은 카드 틀 · 같은 입력 목록인데 여기서는 자리 고르기 대신 **값**을 받음: 자리가 「선택」 인 입력만 + * 같은 카드 틀 · 같은 입력 목록에서 **값**을 받음: 담은 로직 입력 전부를 * 로직마다 묶어, 고르기 = 목록 · 범위 = 숫자 · 자동값은 흐리게 · 「확인함」 표시. 자동값은 저장값이 아님 * (확인 안 한 입력은 계산 때 「빠진 입력」). 값은 로직마다 따로 — 이름이 같아도 합치지 않음. - * mode "view" 보기만 + [선택 입력 수정] · mode "value" 값 받기(범위 밖은 저장 막음) */ + * mode "view" 보기만 + [넣을 값 수정] · mode "value" 값 받기(범위 밖은 저장 막음) */ export interface InputsValueData { list: ItemInputs; - link: InputLinks | null; values: ValueDoc; } export interface InputsValueOptions { mode: "view" | "value"; - /** mode "view" — [선택 입력 수정] */ + /** mode "view" — [넣을 값 수정] */ onEdit?: () => void; /** mode "value" — 값이 바뀔 때마다 */ onChange?: (values: ValueDoc) => void; @@ -111,21 +116,20 @@ export interface InputsValueHandle { getValues: () => ValueDoc; /** 범위 밖 값이 없나 */ isValid: () => boolean; - /** [선택 입력 수정] 글 — 아직 안 가져온 항목이면 「프로젝트로 가져와 고침」 */ + /** [넣을 값 수정] 글 — 아직 안 가져온 항목이면 「프로젝트로 가져와 고침」 */ setEditLabel: (label: string) => void; } const VALUE_TEXT = { - title: "선택 입력", + title: "넣을 값", missing: "빠진 로직 {value}", - edit: "선택 입력 수정", - none: "선택으로 이은 입력이 없음", + edit: "넣을 값 수정", + none: "넣을 값이 없음 — 담은 로직에 입력이 없음", auto: "자동값", unconfirmed: "확인 안 함", confirmed: "✓ 확인함", confirmBox: "확인", - summary: "선택 {value}", - places: "면적산출 알약 {area} · 재료산출 알약 {material}", + summary: "넣을 값 {value}", noList: "입력 목록을 못 읽음 — {value}", }; @@ -140,20 +144,22 @@ export function createInputsValueCard(opts: InputsValueOptions): InputsValueHand const edit = createButton({ label: VALUE_TEXT.edit, variant: "ghost", size: "sm" }); edit.hidden = opts.mode !== "view"; if (opts.onEdit) edit.addEventListener("click", opts.onEdit); - const places = el("p", { className: "m02-inputval__places" }); const body = el("div", { className: "m02-struct__sheet m02-inputval" }); - // 공용 카드 틀(31-7) — 부제 = 확인 n/m · 제목 줄 오른쪽 = [선택 입력 수정] · 상태 글은 목록 칸에 + // 공용 카드 틀(31-7) — 부제 = 확인 n/m · 제목 줄 오른쪽 = [넣을 값 수정] · 상태 글은 목록 칸에 const shell = cardShell({ title: VALUE_TEXT.title, sub: [summary], - body: el("div", { className: "m02-inputval__wrap", children: [places, body] }), + body: el("div", { className: "m02-inputval__wrap", children: [body] }), stateHost: body, }); shell.tools.append(edit); const root = shell.root; - const groups = (): ReturnType => - data ? pickedGroups(data.list, data.link) : []; + /** 로직마다 묶은 입력 — 입력이 없는 로직은 뺌 */ + const groups = (): { logic: ItemLogic; inputs: ItemInput[] }[] => + (data?.list.로직 ?? []) + .map((logic) => ({ logic, inputs: logic.입력 })) + .filter((group) => group.inputs.length > 0); const one = (logic: string, name: string): ValueEntry => values[logic]?.[name] ?? { 값: null, 확인: false }; const isEmpty = (value: string | number | null): boolean => value === null || value === ""; @@ -239,14 +245,9 @@ export function createInputsValueCard(opts: InputsValueOptions): InputsValueHand invalid = 0; paintSummary(); if (!data) { - places.textContent = ""; if (note && retry) return shell.fail(fill(VALUE_TEXT.noList, { value: note }), retry); return shell.note(note ? fill(VALUE_TEXT.noList, { value: note }) : VALUE_TEXT.none); } - const seats = [...seatsOf(data.link).values()]; - const area = seats.filter((seat) => seat === "면적").length; - const material = seats.filter((seat) => seat === "재료").length; - places.textContent = area + material ? fill(VALUE_TEXT.places, { area, material }) : ""; const shown = groups(); const missing = data.list.빠진로직.length ? [ diff --git a/M02_MasterTemplete/M02_Labor_Values_Model.ts b/M02_MasterTemplete/M02_Labor_Values_Model.ts index 046ee7cc8..b49ea4771 100644 --- a/M02_MasterTemplete/M02_Labor_Values_Model.ts +++ b/M02_MasterTemplete/M02_Labor_Values_Model.ts @@ -1,6 +1,6 @@ /* ============================================================================= * M02_Labor_Values_Model.ts - * 넣을 값 셈(화면 없음 · 39-3) — 옛 선택 입력 `M02_MasterTemplete_Inputs_Model.ts` 의 값 함수를 옮김. + * 넣을 값 셈(화면 없음 · 39-3). * 일위대가 입력 모달 · 프로젝트 고르기 모달 값 카드가 같은 값 칸 규칙(고르기 = 목록 · 범위 = 숫자 · * 빈 값 = 자동값)을 씀. 일위대가 입력값 = `{입력 이름: 값}`(빈 값은 안 둠 = 자동값 · 모르는 이름 버림). * ========================================================================== */ @@ -32,6 +32,9 @@ export function valueHint(input: ItemInput): string { export const autoText = (input: ItemInput): string => text(input.자동값); /** `입력값.json` — `{로직키: {입력이름: {값, 확인}}}` (서버 검사 모양 · 값은 수 · 글 · 빈 값) */ +/** 입력 키 `{로직키}|{입력이름}` — 같은 이름의 입력도 로직마다 따로 */ +export const inputKey = (logic: string, name: string): string => `${logic}|${name}`; + export type ValueDoc = Record< string, Record diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index ac1649609..d1634da5c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -8,9 +8,9 @@ import { API_BASE_URL } from "@config/config_frontend"; /** 층 이름 — 서버와 같은 글 */ export type Layer = "system" | "company" | "personal" | "project"; /** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure · - * 「면적산출」 = area · 「재료산출」 = material(둘 다 구조물집계표 열마다 한 통합문서) · + * 「면적산출」 = area(구조물집계표 열마다 한 통합문서) · * 「일위대가」 = labor(열마다 문서 · 39장) */ -export type Kind = "table" | "drawing" | "structure" | "area" | "material" | "labor"; +export type Kind = "table" | "drawing" | "structure" | "area" | "labor"; export interface TemplateInfo { 종류: Kind; @@ -130,7 +130,7 @@ export const deleteLayerTemplate = ( /* --- 구조물 도면(종류 structure · 이름 = 구조물집계표 열 id) --- * 읽기 · 저장 · 지우기는 위 readTemplate · saveTemplate · deleteTemplate 에 "structure" */ -/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 면적산출 · 재료산출은 따로(같은 열 id) */ +/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 면적산출은 따로(같은 열 id) */ export interface StructureDoc { 양식: "구조물도면"; 종류: "structure"; @@ -144,21 +144,20 @@ export interface StructureDoc { export const listStructures = async (): Promise => (await listTemplates("system", null)).filter((t) => t.종류 === "structure"); -/** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 면적산출 · 재료산출 통합문서) · 이미 있으면 StaleError */ +/** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 면적산출 통합문서) · 이미 있으면 StaleError */ export const createStructure = (column: string): Promise => scoped(`/structures/${enc(column)}`, { method: "POST" }); -/** 한 열의 번호 — 도번 `SD` · 면적번 `SA` · 재료번 `SM` + 같은 8자리 +/** 한 열의 번호 — 도번 `SD` · 면적번 `SA` + 같은 8자리 * [종류][분류1][분류2][분류3](없는 쪽은 빈 글) */ export interface StructureNumbers { 도번: string; 면적번: string; - 재료번: string; /** 일위대가 `SU-######`(39장) */ 일위대가번: string; } -/** `{열 id: {도번, 면적번, 재료번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */ +/** `{열 id: {도번, 면적번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */ export const fetchStructureNumbers = (): Promise> => scoped("/structures/numbers"); @@ -172,8 +171,6 @@ export interface StructureCombo extends StructureNumbers { 분류3: string | null; 단위: string | null; 일위대가: string | null; - /** 자재 조합 키 `UM` */ - 자재조합: string | null; 바인딩: unknown; 펼침: unknown; } @@ -210,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; @@ -323,12 +261,7 @@ export const copyArea = async (column: string, from: string): Promise => { await scoped(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); }; -/** 다른 열(`from`) 재료산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 재료번 · 열은 그대로 */ -export const copyMaterial = async (column: string, from: string): Promise => { - await scoped(`/material/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); -}; - -/** 회사 · 개인 층의 집계표 항목(열 하나 + 도면 · 면적 · 재료 파일 + 자재조합 줄)을 내 개인 층으로 복사 — +/** 회사 · 개인 층의 집계표 항목(열 하나 + 도면 · 면적 파일)을 내 개인 층으로 복사 — * 새 SS 키로 만들고 `본뜬` 표시 · `fromOwner` = 회사 층 회사 · 개인 층 「회사/사용자」 */ export const copyItemToPersonal = (body: { fromLayer: Layer; @@ -365,15 +298,8 @@ export const fetchSources = (id: string): Promise => call(`/projects/${ export const readFilled = (id: string, name: string): Promise => call(`/projects/${enc(id)}/tables/${enc(name)}/filled`); -/* --- 선택 입력 (PLAN 25-1b) — 항목(집계표 열 하나) 일위대가의 로직별 입력 목록 · 입력 자리 연결 --- - * 길은 층 항목 길(`/layers/{층}/items/{SS 키}/…`) · 층 폴더 저장은 연결만(값은 안 받음). */ - -/** 층 항목 목록 한 줄 — 열 id ↔ SS 키 */ -export interface ItemRow { - key: string; - 열: string; - 일위대가: string | null; -} +/* --- 넣을 값 입력 목록(39-3) — 일위대가 담은 로직마다 입력(고르기 · 범위 · 자동값) --- + * 길은 `M02_MasterTemplete_Labor_Api` 의 `fetchLogicInputs` · 모양만 여기 */ /** 로직의 입력 하나 — 고르기(선택지) · 범위 · 자동값은 보이기만 */ export interface ItemInput { @@ -399,42 +325,6 @@ export interface ItemInputs { 빠진로직: string[]; } -/** 입력 자리 — 면적산출 알약 · 재료산출 알약 · 선택 컨테이너 · 안 씀(null) */ -export type InputSeat = "면적" | "재료" | "선택"; - -export interface InputLine { - 로직: string; - 입력: string; - 자리: InputSeat | null; - 알약?: string; -} - -export interface InputLinks { - 판: string | null; - 줄: InputLine[]; -} - -const itemPath = (layer: Layer, key: string, tail: string): string => - owned(layer, `/layers/${layer}/items/${enc(key)}/${tail}`); - -export const fetchItemRows = (layer: Layer): Promise => - call(owned(layer, `/items?layer=${layer}`)); - -export const fetchItemInputs = (layer: Layer, key: string): Promise => - call(itemPath(layer, key, "inputs")); - -export const readInputLinks = (layer: Layer, key: string): Promise => - call(itemPath(layer, key, "input-links")); - -/** 판이 다르면 409(`StaleError`) · 없는 키는 422 */ -export const saveInputLinks = ( - layer: Layer, - key: string, - 판: string | null, - 줄: InputLine[], -): Promise<{ 판: string }> => - call(itemPath(layer, key, "input-links"), { method: "PUT", body: JSON.stringify({ 판, 줄 }) }); - /* --- 구조물 라이브러리(PLAN 26-1 · 26-3a) — 폴더 경로 깊이 자유 · 고침은 층 쓰기 권한 --- */ export interface TreeItem { key: string; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Book.py b/M02_MasterTemplete/M02_MasterTemplete_Book.py index 7cab20740..319c4be00 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Book.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Book.py @@ -7,8 +7,8 @@ 개인에 저장 · 프로젝트에서 새 항목은 그 층의 새 키(본뜬 곳은 열 `본뜬`). 회사 · 개인 층에 남은 옛 `SS` 키는 맞출 때 새 모양으로 옮기고(숫자 여섯 그대로 · 겹치면 다음 번호) 옛 키는 줄 `옛키` 에 남김 — 프로젝트 복사본이 들고 있는 옛 키로도 찾음(`find`). -줄 = `{키, 열, 경로, 이름, 종류, 분류1~3(경로 앞 넷 — 옛 코드가 읽음), 단위, 일위대가, 자재조합, -도번, 면적번, 재료번, 바인딩, 펼침, 옛키?}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 — +줄 = `{키, 열, 경로, 이름, 종류, 분류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_CardShell.ts b/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts index a62a07a1e..383533d94 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts @@ -1,6 +1,6 @@ /* ============================================================================= * M02_MasterTemplete_CardShell.ts - * 항목 카드 공용 틀(PLAN 31-7) — 도면 정보 · 면적산출 · 재료산출 · 선택 입력(관리 화면 · 고르기 모달)이 + * 항목 카드 공용 틀(PLAN 31-7) — 도면 정보 · 면적산출 · 넣을 값(관리 화면 · 고르기 모달)이 * 내용만 넣음. 모양 · 높이 · 스크롤은 `M02_MasterTemplete_Structure.css`. * 머리 = 제목 줄(h3 · 제목 · 저장 안 한 점 ● · 오른쪽 단추 자리) + 한 줄 부제(번호 · 이름 · 넘치면 말줄임) * 본문 = 카드 높이를 채움 · 넘치면 카드 안 스크롤 · 상태 = 불러오는 중 · 실패 + [다시 불러오기] · 비었음 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_Inputs.css b/M02_MasterTemplete/M02_MasterTemplete_Inputs.css deleted file mode 100644 index fff107c95..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.css +++ /dev/null @@ -1,105 +0,0 @@ -/* 「선택 입력」 카드 — 로직마다 묶은 입력 줄 + 줄마다 자리 고르기. 색은 테마 토큰. */ - -.m02-inputs__summary { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-4) var(--spacing-12); - align-items: baseline; - color: var(--color-text-muted); - font-size: var(--text-body-sm); -} - -.m02-inputs__count { - color: var(--color-text); - font-weight: 600; -} - -/* 카드 본문이 남는 높이를 채우고 넘치면 안에서 스크롤 */ -.m02-inputs__body { - display: flex; - flex: 1 1 0; - flex-direction: column; - gap: var(--spacing-12); - min-height: var(--m02-struct-body-min); - min-width: 0; - overflow: auto; - padding: var(--spacing-8) 0; -} - -.m02-inputs__logic { - display: flex; - flex-direction: column; - gap: var(--spacing-4); -} - -.m02-inputs__logic-title { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-4) var(--spacing-8); - align-items: baseline; - margin: 0; - padding-bottom: var(--spacing-4); - border-bottom: 1px solid var(--color-border); - color: var(--color-text); - font-size: var(--text-body-sm); - overflow-wrap: anywhere; -} - -.m02-inputs__logic-title small, -.m02-inputs__row small { - color: var(--color-text-muted); - font-size: var(--text-caption); - font-weight: 400; -} - -.m02-inputs__row { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-4) var(--spacing-8); - align-items: center; - justify-content: space-between; - padding: var(--spacing-4) 0; -} - -.m02-inputs__info { - display: flex; - flex: 1 1 12rem; - flex-wrap: wrap; - gap: 2px var(--spacing-8); - align-items: baseline; - min-width: 0; - overflow-wrap: anywhere; -} - -.m02-inputs__name { - color: var(--color-text); - font-size: var(--text-body-sm); -} - -.m02-inputs__desc, -.m02-inputs__hint { - flex-basis: 100%; -} - -/* 자동값 — 흐리게(입력 값이 아니라 보기용 첫 선택지) */ -.m02-inputs__auto { - flex-basis: 100%; - opacity: 0.6; - font-style: italic; -} - -.m02-inputs__seat { - flex: none; - max-width: 100%; - padding: 4px var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm, 4px); - background: var(--color-surface); - color: var(--color-text); - font: inherit; - font-size: var(--text-body-sm); -} - -.m02-inputs__seat:disabled { - opacity: 0.7; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py deleted file mode 100644 index d6c130984..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py +++ /dev/null @@ -1,172 +0,0 @@ -"""M02 항목 선택 입력(PLAN 25-1b · 25-2a) — 입력 목록 · 항목 연결 파일 · 알약. - -항목 = 구조물집계표조합(SS) 한 줄 → 그 열의 일위대가(UA) 조합 → 담은 단가산출 로직마다 입력 전부. -로직마다 갈라 보이고 이름이 같아도 합치지 않음(로직마다 값이 다를 수 있음) · 입력 목록은 그 로직 -줄의 `입력` 그대로(하위 로직으로 넘겨지는 입력은 맨 위 로직 입력으로만 · M01 조합 미리 보기와 -같음) · 자동값 = `master_auto.values`(보기용 · 저장 안 함). - -항목 연결 파일 = 층 폴더 `inputs/<열id>.json` = `{판, 줄: [{로직, 입력, 자리, 알약?}]}` — -판은 저장마다 1 씩 늘고, 연결만 적음(값은 프로젝트 몫). 자리 면적 · 재료 = 그 시트의 알약 -(키 `입력|{로직}|{입력}`) · 선택 = 「선택 입력」 카드 · null = 안 씀. -M01 은 읽기만(`M01_MasterData_Store` · `_Store_Combo` · `master_auto`) · 권한은 부르는 쪽 몫. -""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -from common_util.common_util_json import atomic_write_json -from M01_MasterData import M01_MasterData_Store_Combo as combo_store -from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs -from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos -from M02_MasterTemplete import M02_MasterTemplete_Store as store -from M02_MasterTemplete.M02_Labor_Inputs import _num, combo_logics - -PLACES = ("면적", "재료", "선택", None) -PILL_HEAD = "입력" - - -def pill_key(logic: str, name: str) -> str: - return f"{PILL_HEAD}|{logic}|{name}" - - -def _item(key: str) -> dict[str, Any]: - """지금 층(`store.at`)의 항목 줄 — 없으면 404.""" - row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도 - if row is None: - raise store.StoreError(404, f"없는 항목 「{key}」") - return row - - -def _combo_row(ua: str) -> dict[str, Any] | None: - return combos.find(ua) # 마스터 UA · 회사 UC · 개인 UP(27-2c) - - -def inputs(key: str) -> dict[str, Any]: - """25-1b 입력 목록 — `{일위대가, 이름, 로직: [{로직, 이름, 결과단위, 입력: [...]}], 빠진로직}`. - 일위대가 없는 항목은 로직 빈 목록 · 몸은 `M02_Labor_Inputs`(일위대가 입력 모달과 한 벌).""" - return labor_inputs.inputs_of(_item(key).get("일위대가")) - - -# ── 항목 연결 파일 ───────────────────────────────────── - - -def read_file(column: str) -> dict[str, Any] | None: - """지금 층의 `inputs/<열id>.json` 그대로 — 없으면 None(열 지우기 · 복사 · 스냅샷이 씀).""" - path = store.inputs_path(column) - return json.loads(path.read_bytes()) if path.is_file() else None - - -def write_file(column: str, doc: dict[str, Any]) -> None: - """지금 층에 연결 파일을 그대로 씀(복사용 — 검사 · 판 비교 없음).""" - path = store.inputs_path(column) - path.parent.mkdir(parents=True, exist_ok=True) - atomic_write_json(path, doc) - - -def _known(key: str) -> dict[str, set[str]]: - """{로직 키: {입력 이름}} — 그 항목 조합이 지금 담은 것.""" - _, logics, _ = combo_logics(_item(key).get("일위대가")) - return {str(lg.get("키")): {str(s.get("이름")) for s in lg.get("입력") or []} for lg in logics} - - -def read_links(key: str) -> dict[str, Any]: - """항목 연결 — 없으면 `{판: null, 줄: []}` · 조합이 바뀌어 사라진 로직 · 입력 줄은 - `사라짐: true`.""" - doc = read_file(_item(key)["열"]) or {"판": None, "줄": []} - known = _known(key) - rows = [] - for row in doc.get("줄") or []: - gone = str(row.get("입력")) not in known.get(str(row.get("로직")), set()) - rows.append({**row, "사라짐": True} if gone else row) - return {"판": doc.get("판"), "줄": rows} - - -def _clean_rows(key: str, rows: Any) -> list[dict[str, Any]]: - """없는 로직 · 입력 · 모르는 자리 · 같은 입력 두 번은 422 · 알약 키는 서버가 붙임.""" - if not isinstance(rows, list): - raise store.StoreError(422, "「줄」 이 목록 아님") - known = _known(key) - out, seen, errors = [], set(), [] - for at, row in enumerate(rows, start=1): - if not isinstance(row, dict): - errors.append(f"줄{at} · 묶음 아님") - continue - logic, name, place = str(row.get("로직") or ""), str(row.get("입력") or ""), row.get("자리") - if logic not in known: - errors.append(f"줄{at} · 이 항목 일위대가에 없는 로직 「{logic}」") - elif name not in known[logic]: - errors.append(f"줄{at} · 로직 {logic} 에 없는 입력 「{name}」") - elif place not in PLACES: - errors.append(f"줄{at} · 모르는 자리 「{place}」 — 면적 · 재료 · 선택 · null") - elif (logic, name) in seen: - errors.append(f"줄{at} · 같은 입력 두 번 「{logic}|{name}」") - else: - seen.add((logic, name)) - one: dict[str, Any] = {"로직": logic, "입력": name, "자리": place} - if place in ("면적", "재료"): - one["알약"] = pill_key(logic, name) - out.append(one) - if errors: - raise store.StoreError(422, {"errors": errors}) - return out - - -def write_links(key: str, version: Any, rows: Any) -> dict[str, Any]: - """항목 연결 저장 — 받은 `판` 이 지금 판(없으면 null)과 다르면 409 · 저장마다 판 + 1.""" - column = _item(key)["열"] - clean = _clean_rows(key, rows) - with store._LOCK: - have = (read_file(column) or {}).get("판") - if version != have: - raise store.StoreError(409, {"stale": [f"inputs/{column}"], "판": have}) - doc = {"판": int(have or 0) + 1, "줄": clean} - write_file(column, doc) - return doc - - -# ── 알약 — 면적산출 · 재료산출 시트 ─────────────────────── - - -def pills(key: str, place: str) -> dict[str, Any]: - """그 시트 알약 — 면적이면 조합 물량 알약(M01 `combo_pills` · 권한 무관 읽기) + 자리를 - 그 시트로 이은 입력 알약 · 재료면 이은 입력 알약만. 입력 알약 = `{키, 이름, 단위, 로직, 입력}` — - 이름은 로직마다 갈라 「입력 · 로직 이름」(같은 이름 입력이 여러 로직에 있어도 안 합침).""" - if place not in ("면적", "재료"): - raise store.StoreError(422, f"모르는 자리 「{place}」 — 면적 · 재료") - ua = _item(key).get("일위대가") - quantity: list[dict[str, Any]] = [] - if place == "면적" and ua and (combo := _combo_row(str(ua))): - quantity = combo_store.combo_pills(combo)["물량"] - _, logics, _ = combo_logics(ua) - by_key = {str(lg.get("키")): lg for lg in logics} - linked = [] - for row in read_links(key)["줄"]: - if row.get("자리") != place or row.get("사라짐"): - continue - logic = by_key[str(row["로직"])] - spec = next(s for s in logic.get("입력") or [] if s.get("이름") == row["입력"]) - short = combo_store._TAIL.sub("", str(logic.get("이름") or "")) or row["로직"] - linked.append( - { - "키": pill_key(row["로직"], row["입력"]), - "이름": f"{row['입력']} · {short}", - "단위": spec.get("단위"), - "로직": row["로직"], - "입력": row["입력"], - } - ) - return _num({"물량": quantity, "입력": linked}) - - -def copy_between(source: Path, target: Path, source_column: str, target_column: str) -> bool: - """다른 층(또는 같은 층) 열의 연결 파일을 새 열로 — 판은 1 부터 · 없으면 아무것도 안 함.""" - with store.at(source): - doc = read_file(source_column) - if doc is None: - return False - with store.at(target): - write_file(target_column, {"판": 1, "줄": doc.get("줄") or []}) - return True diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts b/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts deleted file mode 100644 index 2b9b8a143..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Inputs.ts - * 구조물집계표 아래 넷째 카드 「선택 입력」(PLAN 25-2c) — 고른 열(항목) 일위대가의 로직마다 묶어 - * 입력 목록을 보이고, 입력마다 자리(면적산출 알약 · 재료산출 알약 · 선택 · 안 씀)를 사용자가 고름. - * - * · 처음은 모두 「안 씀」 — 프로그램이 추측해 나누지 않음 · 이름이 같은 입력도 로직마다 따로. - * · 마스터 층에서는 값을 받지 않음 — 자리 연결만(값은 프로젝트 복사본 몫 · 25-2b). - * · 저장은 집계표 [저장]과 같은 단추 흐름 — 여기는 고른 것을 초안으로 들고 있다가 `save()` 가 서버로 보냄 - * (자동저장 없음). 고름이 바뀌면 `onDirty` 로 메인에 알림(떠날 때 물음). - * · 면적 · 재료로 고른 입력은 그 시트 편집 화면 알약 줄에 뜸(`inputPills` → `pillsFor`). - * ========================================================================== */ - -import { el } from "@ui/ui_template_elements"; -import type { LinkItem } from "../A00_Common/spreadsheet/spreadsheet_links"; -import { - fetchItemInputs, - fetchItemRows, - getActiveLayer, - readInputLinks, - saveInputLinks, - type InputSeat, - type ItemInputs, - type ItemRow, - type Layer, -} from "./M02_MasterTemplete_Api_Fetch"; -import { - autoText, - countOf, - inputKey, - linesOf, - pillsOf, - seatsOf, - SEAT_OPTIONS, - valueHint, - type SeatMap, -} from "./M02_MasterTemplete_Inputs_Model"; -import { cardNote, cardShell } from "./M02_MasterTemplete_CardShell"; -import "./M02_MasterTemplete_Inputs.css"; - -const TEXT = { - title: "선택 입력", - count: "로직 {logics} · 입력 {inputs}", - chosen: "면적산출 {area} · 재료산출 {material} · 선택 {pick}", - auto: "자동값", - noItem: "구조물 조합을 먼저 저장하면 입력 목록이 나옴", - noCombo: "일위대가를 먼저 고름 — 구조물 조합 일위대가 줄에서 고름", - unsavedCombo: "고른 일위대가는 구조물 조합을 저장하면 입력 목록이 나옴", - noLogic: "이 일위대가에 담긴 로직이 없음", - missing: "빠진 로직 {value}", - failed: "입력 목록을 못 받음 — {value}", - seat: "자리", -}; - -const why = (error: unknown): string => (error instanceof Error ? error.message : ""); -const fill = (text: string, values: Record): string => - text.replace(/\{(\w+)\}/g, (_, key: string) => String(values[key] ?? "")); - -/** 열 하나의 초안 — 서버 목록 · 판 · 고른 자리 · 바뀜 */ -interface Draft { - key: string; - 판: string | null; - list: ItemInputs; - seats: SeatMap; - dirty: boolean; -} - -export interface InputsCardOptions { - /** 이 층에서 고칠 수 있나 — 아니면 자리 고르기가 잠김 */ - isAdmin: boolean; - /** 고름이 바뀜 — 메인의 「저장 안 한 고침」 표시 */ - onDirty: () => void; -} - -export interface InputsCardHandle { - root: HTMLElement; - /** 고른 열 — `combo` = 집계표에 지금 고른 일위대가(저장 전일 수 있음) · null 이면 카드 비움 */ - show: (colId: string | null, combo: string | null) => Promise; - /** 고른 게 저장 안 됐나 */ - dirty: () => boolean; - /** 바뀐 열마다 서버에 저장 — 실패하면 던짐(판이 다르면 `StaleError`) */ - save: () => Promise; - /** 항목 목록(열 ↔ 키)을 다시 받게 함 — 집계표 [저장] 뒤 */ - invalidate: () => void; - destroy: () => void; -} - -export function createInputsCard(opts: InputsCardOptions): InputsCardHandle { - const drafts = new Map(); - let rows: Promise | null = null; - let seq = 0; - - const summary = el("div", { className: "m02-inputs__summary" }); - const body = el("div", { className: "m02-inputs__body" }); - // 공용 카드 틀(31-7) — 부제 = 요약 · [저장]은 없음(집계표 [저장]과 함께) · 점(●) = 고른 것 저장 전 - const shell = cardShell({ title: TEXT.title, className: "m02-inputs", sub: [summary], body }); - const root = shell.root; - const clear = (): void => { - summary.replaceChildren(); - body.replaceChildren(); - }; - - const itemRows = (): Promise => - (rows ??= fetchItemRows(getActiveLayer()).catch((error: unknown) => { - rows = null; - throw error; - })); - - function paintSummary(draft: Draft): void { - const count = countOf(draft.list); - // 목록에 있는 입력의 자리만 셈 — 서버 목록에서 빠진 입력의 옛 고름은 안 셈(저장 때도 빠짐) - const listed = new Set( - draft.list.로직.flatMap((l) => l.입력.map((i) => inputKey(l.로직, i.이름))), - ); - const seats = [...draft.seats].filter(([key]) => listed.has(key)).map(([, seat]) => seat); - const by = (seat: InputSeat): number => seats.filter((s) => s === seat).length; - summary.replaceChildren( - el("span", { className: "m02-inputs__count", text: fill(TEXT.count, count) }), - el("span", { - className: "m02-inputs__chosen", - text: fill(TEXT.chosen, { area: by("면적"), material: by("재료"), pick: by("선택") }), - }), - ); - } - - function paint(draft: Draft): void { - paintSummary(draft); - const groups = draft.list.로직.map((logic) => - el("div", { - className: "m02-inputs__logic", - attrs: { "data-logic": logic.로직 }, - children: [ - el("h4", { - className: "m02-inputs__logic-title", - children: [ - el("span", { text: logic.이름 }), - ...(logic.결과단위 ? [el("small", { text: logic.결과단위 })] : []), - el("small", { className: "m02-inputs__logic-key", text: logic.로직 }), - ], - }), - ...logic.입력.map((input) => { - const key = inputKey(logic.로직, input.이름); - const select = el("select", { - className: "m02-inputs__seat", - attrs: { "aria-label": `${logic.이름} ${input.이름} ${TEXT.seat}` }, - children: SEAT_OPTIONS.map((o) => - el("option", { text: o.label, attrs: { value: o.value } }), - ), - }) as HTMLSelectElement; - select.value = draft.seats.get(key) ?? ""; - select.disabled = !opts.isAdmin; - select.addEventListener("change", () => { - if (select.value) draft.seats.set(key, select.value as InputSeat); - else draft.seats.delete(key); - draft.dirty = true; - paintSummary(draft); - shell.setDirty(true); - opts.onDirty(); - }); - const hint = valueHint(input); - const auto = autoText(input); - return el("div", { - className: "m02-inputs__row", - attrs: { "data-input": input.이름 }, - children: [ - el("div", { - className: "m02-inputs__info", - children: [ - el("span", { className: "m02-inputs__name", text: input.이름 }), - ...(input.단위 ? [el("small", { text: input.단위 })] : []), - ...(input.설명 - ? [el("small", { className: "m02-inputs__desc", text: input.설명 })] - : []), - ...(hint ? [el("small", { className: "m02-inputs__hint", text: hint })] : []), - ...(auto - ? [ - el("small", { - className: "m02-inputs__auto", - text: `${TEXT.auto} ${auto}`, - }), - ] - : []), - ], - }), - select, - ], - }); - }), - ], - }), - ); - const missing = draft.list.빠진로직; - body.replaceChildren( - ...(groups.length ? groups : [cardNote(TEXT.noLogic)]), - ...(missing.length ? [cardNote(fill(TEXT.missing, { value: missing.join(", ") }))] : []), - ); - } - - async function show(colId: string | null, combo: string | null): Promise { - const mine = ++seq; - clear(); - shell.setDirty(!!colId && !!drafts.get(colId)?.dirty); - if (!colId) return; - const kept = drafts.get(colId); - if (kept) return paint(kept); - const stop = shell.loading(); // 0.3초 넘으면 「불러오는 중」(31-7 · 네 카드 같게) - try { - const layer: Layer = getActiveLayer(); - const row = (await itemRows()).find((r) => r.열 === colId); - if (mine !== seq) return; - stop(); - if (!row) return shell.note(combo ? TEXT.unsavedCombo : TEXT.noItem); - if (!row.일위대가) return shell.note(combo ? TEXT.unsavedCombo : TEXT.noCombo); - const [list, links] = await Promise.all([ - fetchItemInputs(layer, row.key), - readInputLinks(layer, row.key), - ]); - if (mine !== seq) return; - if (!list.일위대가) return shell.note(TEXT.noCombo); - const draft: Draft = { - key: row.key, - 판: links.판, - list, - seats: seatsOf(links), - dirty: false, - }; - drafts.set(colId, draft); - paint(draft); - } catch (error) { - // 실패 = 카드 안 까닭 + [다시 불러오기](31-7) - if (mine === seq) - shell.fail(fill(TEXT.failed, { value: why(error) }), () => void show(colId, combo)); - } finally { - stop(); - } - } - - return { - root, - show, - dirty: () => [...drafts.values()].some((d) => d.dirty), - save: async () => { - const layer = getActiveLayer(); - for (const draft of drafts.values()) { - if (!draft.dirty) continue; - const got = await saveInputLinks( - layer, - draft.key, - draft.판, - linesOf(draft.list, draft.seats), - ); - draft.판 = got.판; - draft.dirty = false; - } - shell.setDirty(false); - }, - invalidate: () => { - rows = null; - for (const [id, draft] of drafts) if (!draft.dirty) drafts.delete(id); // 서버 것으로 다시 읽음 - }, - destroy: () => { - seq += 1; - root.remove(); - }, - }; -} - -/** 그 열의 면적산출 · 재료산출 편집 화면 알약 — 자리를 그 시트로 고른 입력(프로젝트 층은 25-2b 몫이라 없음) */ -export async function inputPills(column: string, seat: "면적" | "재료"): Promise { - const layer = getActiveLayer(); - if (layer === "project") return []; - try { - const row = (await fetchItemRows(layer)).find((r) => r.열 === column); - if (!row?.일위대가) return []; - const [list, links] = await Promise.all([ - fetchItemInputs(layer, row.key), - readInputLinks(layer, row.key), - ]); - return pillsOf(list, links, seat); - } catch { - return []; // 서버 길이 아직 없거나 못 읽어도 시트 편집은 그대로 - } -} - -/* ── 프로젝트 복사본에서 값 받기(PLAN 25-2d) — 39-3 에 `M02_Labor_Values.ts` 로 옮김 · 옛 이름 그대로 내보냄 ── */ -export { - createInputsValueCard, - type InputsValueData, - type InputsValueHandle, - type InputsValueOptions, -} from "./M02_Labor_Values"; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts b/M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts deleted file mode 100644 index bbdb8fc53..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Inputs_Model.ts - * 「선택 입력」 카드의 셈(화면 없음) — 입력 키 · 알약 키 · 자리 고름 · 줄 만들기 · 알약 목록. - * 값 글 · 값 칸 규칙은 `M02_Labor_Values_Model.ts`(일위대가 입력 모달과 한 벌 · 여기서 다시 내보냄). - * 약속 = PLAN 25-1b: 입력 키 `{로직키}|{입력이름}` · 알약 키 `입력|{로직키}|{입력이름}` · - * 같은 이름의 입력도 로직마다 따로. 처음은 모두 「안 씀」 — 프로그램이 추측해 나누지 않음. - * ========================================================================== */ - -import type { LinkItem } from "../A00_Common/spreadsheet/spreadsheet_links"; -import type { - InputLine, - InputLinks, - InputSeat, - ItemInput, - ItemInputs, -} from "./M02_MasterTemplete_Api_Fetch"; - -/** 화면 글 ↔ 자리 — 「안 씀」 = 빈 글 */ -export const SEAT_OPTIONS: { value: "" | InputSeat; label: string }[] = [ - { value: "", label: "안 씀" }, - { value: "면적", label: "면적산출 알약" }, - { value: "재료", label: "재료산출 알약" }, - { value: "선택", label: "선택" }, -]; - -export const inputKey = (logic: string, name: string): string => `${logic}|${name}`; -export const pillKey = (logic: string, name: string): string => `입력|${logic}|${name}`; - -/** 자리 고름 표 — 키 = 입력 키 · 값 = 자리(안 쓰면 키 없음) */ -export type SeatMap = Map; - -export function seatsOf(links: InputLinks | null): SeatMap { - const out: SeatMap = new Map(); - for (const line of links?.줄 ?? []) - if (line.자리) out.set(inputKey(line.로직, line.입력), line.자리); - return out; -} - -/** 저장할 줄 — 자리를 고른 것만(없는 것 = 안 씀) · 면적 · 재료는 알약 키를 같이 */ -export function linesOf(list: ItemInputs, seats: SeatMap): InputLine[] { - const out: InputLine[] = []; - for (const logic of list.로직) - for (const input of logic.입력) { - const seat = seats.get(inputKey(logic.로직, input.이름)); - if (!seat) continue; - out.push({ - 로직: logic.로직, - 입력: input.이름, - 자리: seat, - ...(seat === "선택" ? {} : { 알약: pillKey(logic.로직, input.이름) }), - }); - } - return out; -} - -export const countOf = (list: ItemInputs): { logics: number; inputs: number } => ({ - logics: list.로직.length, - inputs: list.로직.reduce((sum, logic) => sum + logic.입력.length, 0), -}); - -/** 그 시트(면적산출 · 재료산출)의 알약 — 고른 자리가 그 시트인 입력만 · 「로직 이름 · 입력 이름」 */ -export function pillsOf( - list: ItemInputs, - links: InputLinks | null, - seat: "면적" | "재료", -): LinkItem[] { - const seats = seatsOf(links); - const out: LinkItem[] = []; - for (const logic of list.로직) - for (const input of logic.입력) { - if (seats.get(inputKey(logic.로직, input.이름)) !== seat) continue; - out.push({ - 키: pillKey(logic.로직, input.이름), - 이름: `${logic.이름} · ${input.이름}`, - ...(input.단위 ? { 단위: input.단위 } : {}), - 고정: true, - }); - } - return out; -} - -/* ── 값 함수 — 일위대가 입력 모달과 한 벌(39-3 · `M02_Labor_Values_Model.ts` 로 옮김 · 옛 이름 그대로 내보냄) ── */ -export { autoText, choicesOf, rangeOf, valueHint, type ValueDoc } from "./M02_Labor_Values_Model"; - -/* ── 프로젝트에서 값 받기(PLAN 25-2d) — 자리 「선택」 인 입력만 로직마다 묶어 값 · 확인 표시 ── */ - -/** 자리가 「선택」 인 입력만 로직 차례로 묶음 — 값 받기 카드가 보이는 입력 */ -export function pickedGroups( - list: ItemInputs, - links: InputLinks | null, -): { logic: ItemInputs["로직"][number]; inputs: ItemInput[] }[] { - const picked = new Set( - [...seatsOf(links)].filter(([, seat]) => seat === "선택").map(([key]) => key), - ); - return list.로직 - .map((logic) => ({ - logic, - inputs: logic.입력.filter((input) => picked.has(inputKey(logic.로직, input.이름))), - })) - .filter((group) => group.inputs.length > 0); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index d00f83160..5f80e7a33 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -1,13 +1,13 @@ /* ============================================================================= * M02_MasterTemplete_ItemCards.ts - * 항목 카드 — 도면 정보 · 면적산출 · 재료산출(보기만 + [수정]) + 덧붙는 카드(선택 입력 …). + * 항목 카드 — 도면 정보 · 면적산출(보기만 + [수정]) + 덧붙는 카드(넣을 값 …). * M02 구조물 설계 컨테이너(`M02_MasterTemplete_Structure.ts`)에서 떼어 냄(계획서 25-2d) — 항목 고르기 * 모달(`ui_template_item_picker_detail.ts`)이 **같은 카드**를 쓰려고. 문서를 어디서 읽는지는 * 자료 자리(`ItemSource`)가 정함 · 카드는 몰라도 됨. * 카드 겉 틀 · 제목 줄 · 부제 · [저장] · ? · 점(●) · 상태 글은 공용 틀(`M02_MasterTemplete_CardShell` · 31-7). - * 면적산출 · 재료산출 = 스프레드시트 · 부품은 카드를 열 때만 `import()`(다른 페이지 번들에 안 섞임). - * 덧붙는 카드는 `extraCards` 로 끼움 — 선택 입력 카드(`M02_MasterTemplete_Inputs.ts`)가 그 자리. - * `sheetEdit` 가 있으면 면적산출 · 재료산출 카드를 미리보기에서 고침(31-3) — 스프레드시트 `preview`(메뉴 없이 + * 면적산출 = 스프레드시트 · 부품은 카드를 열 때만 `import()`(다른 페이지 번들에 안 섞임). + * 덧붙는 카드는 `extraCards` 로 끼움 — 고르기 모달 넣을 값 카드(`M02_Labor_Values.ts`)가 그 자리. + * `sheetEdit` 가 있으면 면적산출 카드를 미리보기에서 고침(31-3) — 스프레드시트 `preview`(메뉴 없이 * 수식 입력 줄 · 칸 고침 · 단축키) + 아래 알약 줄 + 카드 [저장] · 없으면 보기만(고르기 모달). * ========================================================================== */ @@ -43,12 +43,10 @@ export type SheetKind = Exclude; export const PART_FIELD: Record = { structure: "도번", area: "면적번", - material: "재료번", }; export const PART_WORD: Record = { structure: "도면", area: "면적산출", - material: "재료산출", }; const TEXT = { @@ -92,7 +90,7 @@ const emptyBook = (book: Workbook): boolean => (s) => !s.연결?.length && Object.values(s.칸 ?? {}).every((c) => (c.값 ?? "") === "" && !c.식), ); const fill = (text: string, value: string): string => text.replace("{value}", value); -/** 스프레드시트는 면적산출 · 재료산출 카드를 열 때만 받음 */ +/** 스프레드시트는 면적산출 카드를 열 때만 받음 */ const loadSpreadsheet = () => import("../A00_Common/spreadsheet/spreadsheet"); export interface ItemCardsOptions { @@ -100,23 +98,23 @@ export interface ItemCardsOptions { source: () => ItemSource; /** [수정] 단추를 보일까 — 부를 때마다 다시 봄 */ editable: () => boolean; - /** [도면 수정] · [면적산출 수정] · [재료산출 수정] */ + /** [도면 수정] · [면적산출 수정] */ onEdit: (kind: EditKind) => void; /** 문서에 번호가 없을 때 쓸 번호(집계표 번호 줄) */ numberOf?: (kind: EditKind, column: string) => string | undefined; /** 못 읽었을 때 카드에 까닭을 보일까 — 기본 보임(M02 는 번호가 있는 열만 · 없는 열은 「없음」) */ warnOnFail?: (column: string) => boolean; - /** 세 카드 뒤에 덧붙는 카드(선택 입력 …) */ + /** 카드 뒤에 덧붙는 카드(넣을 값 …) */ extraCards?: HTMLElement[]; /** 카드 가운데 하나라도 저장 안 한 고침이 생김 · 없어짐(PLAN 31-4 · 31-7) */ onDirty?: (dirty: boolean) => void; - /** 면적산출 · 재료산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */ + /** 면적산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */ sheetEdit?: SheetEditOptions; /** 「복사해 가져오기」 · 빈 카드 [복사 생성](39-5) — 다른 항목의 이 칸을 골라 덮어씀 · 없으면 단추 없음 */ onImport?: (kind: EditKind) => void; } -/** 카드 스프레드시트를 다루는 손잡이 — 알약 · 자재 조합 판이 씀 */ +/** 카드 스프레드시트를 다루는 손잡이 — 알약이 씀 */ export interface SheetCardControl { column: string; label: string; @@ -131,7 +129,7 @@ export interface SheetCardControl { export interface SheetEditOptions { /** 지금 고칠 수 있나 — 부를 때마다 다시 봄 */ can: () => boolean; - /** 아래 알약 줄(입력 연결) · 카드 아래 덧붙는 판(재료산출 = 자재 조합) — 전체 편집기와 같은 부품 */ + /** 아래 알약 줄(입력 연결) · 카드 아래 덧붙는 판 — 전체 편집기와 같은 부품 */ links: ( kind: SheetKind, control: SheetCardControl, @@ -150,7 +148,7 @@ export interface ItemCardsHandle { relabel: (labels: Partial>) => void; /** 지금 보이는 열 id */ column: () => string | null; - /** 카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */ + /** 카드(도면 · 면적산출)에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */ dirty: () => boolean; /** 고친 카드를 모두 저장(도면 · 엑셀) — 실패하면 던짐 */ saveAll: () => Promise; @@ -253,7 +251,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { preview ? preview.write() : Promise.resolve(), ); - /** 면적산출 · 재료산출 카드 — 스프레드시트 미리보기 · 카드 고침(31-3) */ + /** 면적산출 카드 — 스프레드시트 미리보기 · 카드 고침(31-3) */ interface SheetCard extends Card { kind: SheetKind; host: HTMLElement; @@ -275,7 +273,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { } as SheetCard; return card; }; - const sheets = [sheetCard("area"), sheetCard("material")]; + const sheets = [sheetCard("area")]; // 면적산출 카드 [알약] — 아래 알약 줄 보임 · 숨김(기본 숨김 · 계정 취향 · 상세는 늘 보임) const pills = createButton({ label: TEXT.pills, variant: "ghost", size: "sm" }); pills.title = TEXT.pillsTip; @@ -294,7 +292,6 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { const defaults: Record = { structure: TEXT.edit, area: fill(TEXT.editSheet, PART_WORD.area), - material: fill(TEXT.editSheet, PART_WORD.material), }; const root = el("div", { @@ -335,7 +332,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { c.doc = next as Workbook; } - /** 미리보기 수정 — 메뉴 없이 수식 입력 줄 · 칸 고침 · 아래 알약 줄 · 덧붙는 판(재료산출 = 자재 조합) */ + /** 미리보기 수정 — 메뉴 없이 수식 입력 줄 · 칸 고침 · 아래 알약 줄 · 덧붙는 판 */ const paintEditable = async ( c: SheetCard, mine: number, diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemSource.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemSource.ts index 7a325c9b8..c8903d81c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemSource.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemSource.ts @@ -12,8 +12,8 @@ import { type Layer, } from "./M02_MasterTemplete_Api_Fetch"; -/** 집계표 열 하나에 딸린 문서 셋 — 구조물 도면 · 면적산출 · 재료산출 */ -export type ItemPart = "structure" | "area" | "material"; +/** 집계표 열 하나에 딸린 문서 — 구조물 도면 · 면적산출 */ +export type ItemPart = "structure" | "area"; export interface ItemSource { /** CAD 자동백업 칸을 가르는 층 — 프로젝트 복사본이면 `project` */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Items.py index 121ce2776..b3899ec42 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Items.py @@ -1,7 +1,7 @@ """M02 층 집계표 항목 — 회사 · 개인 층 빈 뼈대 · 항목 목록 · 항목 복사(PLAN 21-1 · 21-2). -항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) · 자재조합(UM) · -도번 SD · 면적번 SA · 재료번 SM. +항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) · +도번 SD · 면적번 SA. 층 폴더(시스템 · 회사 · 개인)는 모양이 같아 저장소 함수를 `store.at(층 폴더)` 안에서 그대로 부름. 권한은 부르는 쪽(`M02_MasterTemplete_Router_Items.py`) 몫 — 여기는 폴더와 파일만. """ @@ -15,8 +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`) @@ -30,16 +28,14 @@ ITEM_FIELDS = ( "분류3", "단위", "일위대가", - "자재조합", "도번", "면적번", - "재료번", ) def ensure(folder: Path) -> bool: """회사 · 개인 층 첫 진입 — 빈 집계표(열 0 · 층 이름 · 줄 머리는 시스템 것) · - 조합 장부(`다음` 1) · 자재 조합 빈 표 · 종류 폴더. + 조합 장부(`다음` 1) · 종류 폴더. 이미 집계표가 있으면 아무것도 안 함(시스템 층은 늘 있음).""" table = Path(folder) / "table" / f"{store.STRUCTURE_TABLE}.json" if table.is_file(): @@ -60,31 +56,24 @@ 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, 단위, 일위대가, 자재조합, - 도번, 면적번, 재료번, names}` · 경로 = 폴더 이름 배열 · 이름 = 경로 이음(26-1 · 종류 · + """그 층 항목 — 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 ("도번", "면적번", "재료번") - }, + **{k: (head or r.get("열")) if r.get(k) else None for k in ("도번", "면적번")}, } out.append( { @@ -142,8 +131,7 @@ def copy_item( path: list[str] | None = None, ) -> dict[str, Any]: """다른 층 항목(SS `key`) → `target` 층 새 열 하나 — 머리 · 단위 · 바인딩 그대로 · - 도면 · 일위대가(새 SU · 조합 키는 안 받음 · 39-1b) · 면적산출 · 재료산출 내용(알약 연결 포함) · - 항목 연결 · 자재 조합 줄(새 UM) 복사 · + 도면 · 일위대가(새 SU · 조합 키는 안 받음 · 39-1b) · 면적산출 내용(알약 연결 포함) 복사 · 새 번호 · 새 SS 키 · 열 `본뜬` = `origin`(원래 층 · 주인 · 키) · `path` = 새 자리(폴더 + 이름 · 없으면 원래 경로 · 붙여넣기 PLAN 30-2). 돌려줌 `{key, 열, 이름}`.""" @@ -159,24 +147,8 @@ 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)}) @@ -184,9 +156,9 @@ def copy_item( **json.loads(json.dumps(col)), "id": new_id, "일위대가": None, # 정본 = 일위대가 문서(아래 복사) - "자재조합": new_um, "본뜬": origin, } + made.pop("자재조합", None) # 옛 자재 조합 칸은 안 옮김 if path: made.update({"경로": list(path), "머리": list(path)}) # 머리 줄 수는 저장 맞춤이 채움 doc["열"] = [*doc.get("열", []), made] @@ -201,5 +173,4 @@ def copy_item( atomic_write_json(store._path(kind, new_id), body) store._refresh_labor(new_id) # 면적이 일위대가 뒤에 서므로 칸 값으로 다시 ss = next(r for r in store.read_combos()["줄"] if r.get("열") == new_id) - inputs_store.copy_between(source, target, col_id, new_id) # 항목 연결도 같이(판 1) return {"key": ss["키"], "열": new_id, "이름": ss["이름"]} 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..42d171b9c 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] # {종류: {옵션 키: 참 · 거짓}} — 거짓만 남김 @@ -72,7 +59,7 @@ def get_structure_combos() -> dict: @router.post("/structures/{column}") def post_structure(column: str) -> dict: - """구조물 도면 새로 — 열 id 하나 · 도번 자동 · 빈 면적산출 · 재료산출도 · 이미 있으면 409.""" + """구조물 도면 새로 — 열 id 하나 · 도번 자동 · 빈 면적산출도 · 이미 있으면 409.""" return _call(store.create_structure, column) @@ -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: """`{판, 표시, 종류}` — 표시 = 숨긴 것만 · 종류 = 옵션 있는 종류와 그 옵션.""" @@ -157,11 +104,6 @@ def post_area_copy(column: str, source: str = Query(alias="from")) -> dict: return _call(store.copy_from, "area", column, source) -@router.post("/material/{column}/copy") -def post_material_copy(column: str, source: str = Query(alias="from")) -> dict: - return _call(store.copy_from, "material", column, source) - - @router.get("/templates/{kind}/{name}") def get_template(kind: str, name: str) -> dict: return _call(store.read, kind, name) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 3151acf2e..1c2801d1c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -27,15 +27,11 @@ from config.config_db import get_db_pool from M02_MasterTemplete import M02_Item_Paste as paste_store 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, ) @@ -72,11 +68,6 @@ Session = dict[str, Any] NO_COMPANY = access.NO_COMPANY -class InputLinksBody(BaseModel): - 판: int | None = None - 줄: list[Any] = [] - - class TreeBody(BaseModel): """26-1 나무 고침 — `{op, 경로, 이름?, 새경로?, 키?, 판}` (+ 항목 더하기 때 `단위?`).""" @@ -197,8 +188,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: return [_listing(session, one, None) for one in ("system", "company", "personal")] @@ -207,7 +197,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 @@ -218,50 +208,6 @@ def copy_item(body: CopyItemBody, session: Session = Depends(verify_session)) -> raise HTTPException(status_code=e.status, detail=e.detail) from e -# ── 항목 선택 입력(PLAN 25-1b · 25-2a) — 읽기 = 층 읽기 권한(시스템 층 = 로그인 누구나) ── - - -@router.get("/layers/{layer}/items/{key}/inputs") -def get_item_inputs( - layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) -) -> dict: - """그 항목 일위대가가 담은 로직마다 입력 전부(합치지 않음) · 자동값 — M01 길은 관리자 전용이라 - 일반 사용자는 이 길로 읽음.""" - return _call(_root(session, layer, owner), inputs_store.inputs, key) - - -@router.get("/layers/{layer}/items/{key}/input-links") -def get_input_links( - layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) -) -> dict: - return _call(_root(session, layer, owner), inputs_store.read_links, key) - - -@router.put("/layers/{layer}/items/{key}/input-links") -def put_input_links( - layer: Layer, - key: str, - body: InputLinksBody, - owner: str | None = None, - session: Session = Depends(verify_session), -) -> dict: - """본문 `{판, 줄}` — 판 다르면 409 · 없는 로직 · 입력 키 422 · 층 쓰기 권한.""" - folder = _root(session, layer, owner, write=True) - return _call(folder, inputs_store.write_links, key, body.판, body.줄) - - -@router.get("/layers/{layer}/items/{key}/pills") -def get_item_pills( - layer: Layer, - key: str, - place: Literal["면적", "재료"], - owner: str | None = None, - session: Session = Depends(verify_session), -) -> dict: - """면적산출 · 재료산출 시트 알약 — `{물량: [조합 물량], 입력: [그 시트로 이은 입력]}`.""" - return _call(_root(session, layer, owner), inputs_store.pills, key, place) - - # ── 폴더 구조(PLAN 26-1 · 26-2a) — 읽기 = 층 읽기 권한 · 고침 = 층 쓰기 권한 ── @@ -375,19 +321,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, @@ -412,7 +345,7 @@ def _copy_practice(column: str, key: str) -> dict[str, Any]: @router.post("/layers/{layer}/{kind}/{column}/copy") def post_copy( layer: Layer, - kind: Literal["structures", "labor", "area", "material"], + kind: Literal["structures", "labor", "area"], column: str, source: str = Query(alias="from"), owner: str | None = None, @@ -430,47 +363,3 @@ def post_copy( return _call(folder, _copy_practice, column, source) origin = _root(session, from_layer, from_owner) if from_layer else None return _call(folder, store.copy_from, part, column, source, origin) - - -@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_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index a2e2aa95d..52667b2fe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -336,8 +336,8 @@ async def save_layer_template( return await asyncio.to_thread( _store_save, folder, kind, name, body.판 or "", document, layer ) - if kind in ("area", "material"): - # 면적산출 · 재료산출 계산값은 서버가 같은 TS 엔진을 Node 로 돌려 새로 적음 + if kind == "area": + # 면적산출 계산값은 서버가 같은 TS 엔진을 Node 로 돌려 새로 적음 # (브라우저 값 버림 · 5장 ②) try: document = await asyncio.to_thread(spreadsheet.with_server_values, document) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index e8bedb6c2..5bfe9e227 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 — PLAN 21-1)이라 아래 함수는 그대로 씀 _ROOT: ContextVar[Path | None] = ContextVar("m02_layer_root", default=None) @@ -45,9 +45,8 @@ def at(folder: str | Path) -> Iterator[Path]: # 구조물집계표 구조물 열(꼴=수) 마다 세 파일(파일 이름 = 열 id) — -# 구조물 도면 `structure/` (도각 없는 전용 화면) · 면적산출 `area/` · 재료산출 `material/` -# (둘 다 스프레드시트 통합문서) -# 번호 = 도번 SD · 면적번 SA · 재료번 SM + 뒷자리 — 한 열의 세 번호는 같은 뒷자리 · 한 번 준 번호는 +# 구조물 도면 `structure/` (도각 없는 전용 화면) · 면적산출 `area/`(스프레드시트 통합문서) +# 번호 = 도번 SD · 면적번 SA + 뒷자리 — 한 열의 번호는 같은 뒷자리 · 한 번 준 번호는 # 파일에 남아 안 바뀜(PLAN 13장). 뒷자리 두 모양: # 옛 8자리 `SD01020300` = [종류][분류1][분류2][분류3] 두 자리씩(그대로 유효 · 더는 새로 안 줌) # 새 일련 `SD-000001` = 붙임표 + 6자리 · 자리 뜻 없음(PLAN 26-1 · 26-4a) — 옛 8자리 · @@ -57,7 +56,7 @@ def at(folder: str | Path) -> Iterator[Path]: STRUCTURE_TABLE = "구조물집계표" # 프로그램이 이름으로 부르는 양식 — 지우면 그 기능이 죽음(B07 이름은 `B07_DesignDetail_Engine_*`) IN_USE = { - ("table", STRUCTURE_TABLE): "구조물 도면 · 면적산출 · 재료산출 · 조합 장부가 씀", + ("table", STRUCTURE_TABLE): "구조물 도면 · 면적산출 · 조합 장부가 씀", ("drawing", "00_template_A1"): "B07 도면 도각", ("drawing", "00_template_compass"): "B07 방위표", ("drawing", "00_template_cover"): "B07 표지", @@ -67,11 +66,10 @@ PARTS = ( ("structure", "도번", "SD", None), ("labor", "일위대가번", "SU", None), # 일위대가 — `M02_Labor_Doc`(39장) ("area", "면적번", "SA", "면적산출"), - ("material", "재료번", "SM", "재료산출"), ) _KEY = {kind: key for kind, key, _, _ in PARTS} -SHEETS = ("area", "material") # 통합문서 종류 — [저장] 때 서버가 계산값을 새로 적음 -_CODE = re.compile(r"^S[DAMU](\d{8}|-\d{6})$") +SHEETS = ("area",) # 통합문서 종류 — [저장] 때 서버가 계산값을 새로 적음 +_CODE = re.compile(r"^S[DAU](\d{8}|-\d{6})$") _OLD_CODE = re.compile(r"^S[DCA](\d{6})$") # PLAN 12장 6자리 — 끝에 00 을 붙여 읽음 _BAD_NAME = re.compile(r'[\\/:*?"<>|\x00-\x1f]|\.\.') # 도각 없는 빈 CAD 문서의 기본 도면층 — 없으면 B07 CAD 가 못 엶("Cannot read properties @@ -106,16 +104,6 @@ def _path(kind: str, name: str) -> Path: return root() / kind / f"{name}.json" -# 항목 연결(PLAN 25-1b) — 열 id 마다 `inputs/<열id>.json` · 종류 폴더(KINDS) 밖 · 읽기 · 쓰기는 -# `M02_MasterTemplete_Inputs` 몫 · 열 지우기 · 분류 더하기 복사는 여기서 같이 -INPUTS = "inputs" - - -def inputs_path(column: str) -> Path: - """그 열의 항목 연결 파일 자리 — 이름 규칙은 종류 파일과 같음.""" - return _path("structure", column).parent.parent / INPUTS / f"{column}.json" - - def _info(kind: str, path: Path) -> dict[str, Any]: raw = path.read_bytes() stamp = datetime.fromtimestamp(path.stat().st_mtime).isoformat(timespec="seconds") @@ -153,14 +141,14 @@ def _check_skeleton(kind: str, doc: Any) -> None: def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) -> dict[str, Any]: """`version` 이 빈 글이면 새로 만듦(이미 있으면 409) · 아니면 그 판일 때만 덮어씀. - 구조물집계표를 저장하면 — 새 구조물 열의 빈 구조물 도면 · 면적산출 · 재료산출을 번호와 같이 + 구조물집계표를 저장하면 — 새 구조물 열의 빈 구조물 도면 · 면적산출을 번호와 같이 만들고 지운 열(또는 구조물이 아니게 된 열)의 세 파일을 지움(잠금 밖에서 · 안에서 부르면 죽음). - 통합문서(area · material)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음 + 통합문서(area)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음 (브라우저 값 버림 · 5장 ②) · 알약 `연결` 은 문서와 같이 그대로 적힘 · 일위대가(labor)는 `M02_Labor_Doc.server` 가 정리 · `계산값` 을 새로 적음(면적산출 저장 때 그 열 일위대가도). - 번호 칸(도번 · 면적번 · 재료번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음). + 번호 칸(도번 · 일위대가번 · 면적번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음). 표 열의 `본뜬열`(분류 더하기로 복사한 새 열)은 적지 않음 — 집계표면 아직 파일이 없는 새 열이 - 원본 열의 일위대가 · 자재조합 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로). + 원본 열의 일위대가 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로). `screen` = 화면 표 [저장](PLAN 26-7e) — 있던 항목의 경로 · 층 이름 · 빈 폴더는 저장본 그대로 (정본 = 나무 · 머리는 경로에서 다시 만듦 · `tree.keep_paths`).""" path = _path(kind, name) @@ -187,10 +175,9 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) -> for part in _KEY: _ensure(part, column, source) _refresh_labor(column) # 면적이 일위대가 뒤에 서므로 칸 값으로 다시 - _copy_inputs(source, column) backfill_missing_structures() # 지우기 앞 — 같은 저장의 새 열이 지운 번호를 안 받게 prune_removed_structures() - sync_combos() # 번호가 다 선 뒤 — 조합 표 도번 · 면적번 · 재료번 칸 + sync_combos() # 번호가 다 선 뒤 — 조합 표 도번 · 면적번 칸 return info @@ -206,7 +193,7 @@ def delete(kind: str, name: str, version: str | None = None) -> None: path.unlink() -# ── 구조물 도면 · 면적산출 · 재료산출 ───────────────────── +# ── 구조물 도면 · 면적산출 ───────────────────── def _docs(kind: str) -> list[tuple[str, Any]]: @@ -225,7 +212,7 @@ def _code(doc: Any, key: str) -> str | None: def _codes() -> dict[str, str]: - """`{열 id: 뒷자리}` — 구조물 도면 도번 먼저 · 없으면 면적번 · 재료번.""" + """`{열 id: 뒷자리}` — 구조물 도면 도번 먼저 · 없으면 면적번.""" codes: dict[str, str] = {} for kind, key, _, _ in reversed(PARTS): codes.update({name: c for name, doc in _docs(kind) if (c := _code(doc, key))}) @@ -233,7 +220,7 @@ def _codes() -> dict[str, str]: def structure_numbers() -> dict[str, dict[str, str]]: - """`{열 id: {도번, 면적번, 재료번}}` — 집계표 줄 · 좌측 줄이 한 번에 받음(없는 쪽은 빈 글).""" + """`{열 id: {도번, 면적번}}` — 집계표 줄 · 좌측 줄이 한 번에 받음(없는 쪽은 빈 글).""" out: dict[str, dict[str, str]] = {} for kind, key, _, _ in PARTS: for name, doc in _docs(kind): @@ -285,7 +272,7 @@ def _retired() -> list[str]: def _blank(kind: str, column: str) -> dict[str, Any]: """빈 문서(번호는 부르는 쪽이 붙임) — 구조물 도면 = 도각 없음 · 기본 도면층 하나 / - 면적산출 · 재료산출 = 진짜 빈 시트 하나(식 없어 `계산값` 없음 · 첫 [저장] 때 서버가 적음).""" + 면적산출 = 진짜 빈 시트 하나(식 없어 `계산값` 없음 · 첫 [저장] 때 서버가 적음).""" if kind == "labor": return { "종류": "일위대가", @@ -336,8 +323,7 @@ def _ensure(kind: str, column: str, source: str | None = None) -> bool: def _take_origins(doc: Any) -> dict[str, str]: """표 열의 `본뜬열` 을 떼어 냄 → `{새 열: 원본 열}` (구조물 파일이 아직 없는 열만) · - 그 열에 자재조합 키가 비었으면 원본 열 것을 받음 · 일위대가는 원본 열 문서를 새 번호로 복사 - (`_ensure` · 39-1b — 조합 키는 안 받음).""" + 일위대가는 원본 열 문서를 새 번호로 복사(`_ensure` · 39-1b — 조합 키는 안 받음).""" cols = ( [c for c in (doc.get("열") or []) if isinstance(c, dict)] if isinstance(doc, dict) else [] ) @@ -350,13 +336,12 @@ def _take_origins(doc: Any) -> dict[str, str]: if (root() / "structure" / f"{col['id']}.json").is_file(): continue out[col["id"]] = source - col["자재조합"] = col.get("자재조합") or by_id[source].get("자재조합") return out def create_structure(column: str) -> dict[str, Any]: """구조물 열 하나의 구조물 도면 새로 — 도번 SD + 새 일련(`-` + 6자리) · 이미 있으면 409. - 일위대가 · 면적산출 · 재료산출 파일이 없으면 빈 문서도 같이 만듦(같은 뒷자리).""" + 일위대가 · 면적산출 파일이 없으면 빈 문서도 같이 만듦(같은 뒷자리).""" if column not in _column_ids(): raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 구조물 열 「{column}」") if not _ensure("structure", column): @@ -394,26 +379,13 @@ def copy_from( return info -def _copy_inputs(source: str, column: str) -> None: - """분류 더하기로 복사한 새 열 — 원본 열의 항목 연결을 판 1 로(이미 있으면 그대로).""" - have, got = inputs_path(column), inputs_path(source) - if have.is_file() or not got.is_file(): - return - have.parent.mkdir(parents=True, exist_ok=True) - atomic_write_json(have, {"판": 1, "줄": json.loads(got.read_bytes()).get("줄") or []}) - - def prune_removed_structures() -> list[str]: - """집계표에서 지운 열(또는 구조물이 아니게 된 열)의 세 파일 · 항목 연결을 같이 지움 — - 번호는 비워 둠(다른 열 번호를 안 당김).""" + """집계표에서 지운 열(또는 구조물이 아니게 된 열)의 세 파일을 같이 지움 — + 번호는 비워 둠(다른 열 번호를 안 당김) · 옛 `inputs/` 는 안 건드림.""" keep = set(_column_ids()) gone: list[str] = [] codes: set[str] = set() with _LOCK: - for path in sorted((root() / INPUTS).glob("[!.]*.json")): - if path.stem not in keep: - path.unlink() - gone.append(f"{INPUTS}/{path.stem}") for kind, key, _, _ in PARTS: for path in sorted((root() / kind).glob("[!.]*.json")): if path.stem not in keep: @@ -456,7 +428,7 @@ def _move_basis() -> list[str]: def renumber() -> dict[str, list[str]]: """한 번 — 옛 산출근거를 면적산출로 옮김 · 옛 번호(6자리 · 구-NN · 표-NN · 없음)를 8자리로 (6자리는 끝에 00 · 번호 없는 열은 열 차례로 일련번호) · - 구조물 아닌 열 파일 지움 · 빠진 파일(재료산출 등) 만듦. 내용은 그대로 · 두 번 돌려도 같음.""" + 구조물 아닌 열 파일 지움 · 빠진 파일(면적산출 등) 만듦. 내용은 그대로 · 두 번 돌려도 같음.""" moved = _move_basis() gone = prune_removed_structures() changed: list[str] = [] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 77bf194ff..71a84493d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -1,15 +1,15 @@ /* ============================================================================= * M02_MasterTemplete_Structure.ts - * 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 · 재료산출 카드(보기만 + [수정]). + * 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 카드(보기만 + [수정]). * 카드 셋은 좌측 패널과 같은 공용 ui-sidebar 틀 — 고침은 [수정] 이 여는 M02 메인 칸 전체 편집 화면 * (`M02_MasterTemplete_UI_Main.ts`)이 맡음. - * 면적산출 · 재료산출 = 스프레드시트(`A00_Common/spreadsheet/`) · 열마다 따로 파일(`area|material/<열 id>.json`) · + * 면적산출 = 스프레드시트(`A00_Common/spreadsheet/`) · 열마다 따로 파일(`area/<열 id>.json`) · * 부품은 카드를 열 때만 `import()` 로 받음(다른 페이지 번들에 안 섞임). * * 구조물 도면은 집계표 열마다 하나(`resources/master_template/structure/<열 id>.json`) · * 서버가 열마다 미리 만들어 둠(번호 자동 · 새 열도 집계표 저장 때 서버가 같이 만듦 — `M02_MasterTemplete_Store.py`). * 이 칸은 표 부품 host 밖 형제 자리에 붙음 — 집계표는 그대로 두고 여기만 열마다 다시 그림. - * 집계표 일위대가 · 도면 · 면적산출 · 재료산출 줄의 조회 후 선택(`pick`)도 여기 — 일위대가 = M01 일위대가 + * 집계표 일위대가 · 도면 · 면적산출 줄의 조회 후 선택(`pick`)도 여기 — 일위대가 = M01 일위대가 * 목록에서 고름 · 나머지 = 다른 열 것을 골라 내용만 복사해 가져옴(번호 · 열은 그대로 · 서버가 복사). * 글자는 이 파일에 둠(도면 부품 `M02_MasterTemplete_Drawing.ts` 와 같은 방식). * ========================================================================== */ @@ -23,11 +23,9 @@ import { } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links"; -import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch"; import { fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api"; import { copyArea, - copyMaterial, copyStructure, fetchLaborPills, fetchStructureNumbers, @@ -46,10 +44,8 @@ import { type SheetEditOptions, type SheetKind, } from "./M02_MasterTemplete_ItemCards"; -import { buildMaterialPanel, loadMaterialState, toPills } from "./M02_MasterTemplete_Materials"; import { layerSource } from "./M02_MasterTemplete_ItemSource"; import { pickHere, pickedName, type From } from "./M02_MasterTemplete_ItemPick"; -import { createInputsCard, inputPills, type InputsCardHandle } from "./M02_MasterTemplete_Inputs"; import "./M02_MasterTemplete_Structure.css"; export type { EditKind, SheetKind } from "./M02_MasterTemplete_ItemCards"; @@ -72,26 +68,24 @@ const TEXT = { copyFailed: "복사 실패 — {value}", noPills: "이 열 일위대가에 담은 로직 · 자재가 없어 알약 없음 — 일위대가 상세에서 담음", pillsFailed: "일위대가 알약을 못 받음 — {value}", - discardCards: "카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?", + discardCards: "카드(도면 · 면적산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?", }; /** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 · 복사 */ const COPY: Record Promise> = { structure: copyStructure, area: copyArea, - material: copyMaterial, }; /** 집계표 줄 열쇠 → 문서 */ const ROW_KIND: Record = { "s:drawing": "structure", "s:area": "area", - "s:material": "material", }; const why = (error: unknown): string => (error instanceof Error ? error.message : ""); const fill = (text: string, value: string): string => text.replace("{value}", value); -/** `{열 id: {도번, 면적번, 재료번}}` — 집계표 번호 줄(없는 열은 키 없음 = 「없음」) */ +/** `{열 id: {도번, 면적번}}` — 집계표 번호 줄(없는 열은 키 없음 = 「없음」) */ export type ColumnNumbers = Record; /** 일위대가 목록 — 이 층이 쓸 수 있는 세 층 조합(27-2c) · 고를 때마다 받음(층을 바꿔 다시 들어와도 맞게) */ @@ -209,7 +203,7 @@ export interface StructureColumn { 꼴?: string | null; } -/** 구조물 열 = 꼴=수 — NO · 측점 · 비고 같은 글 열은 도면 · 면적산출 · 재료산출 · 번호가 없음(서버와 같은 규칙) */ +/** 구조물 열 = 꼴=수 — NO · 측점 · 비고 같은 글 열은 도면 · 면적산출 · 번호가 없음(서버와 같은 규칙) */ export const isStructureColumn = (col: StructureColumn): boolean => col.꼴 === "수"; /** 관리 화면 「구조물 조합」에서 숨기는 것(PLAN 26-7b · 30-7) — NO · 측점 · 비고 열 · 전구간 줄(본문 줄) · @@ -277,17 +271,13 @@ export const columnHead = (col: StructureColumn): string => export const structureLabel = (number: string, col: StructureColumn): string => [number, columnHead(col)].filter(Boolean).join(" "); -/** 면적산출 · 재료산출 편집 화면 아래 알약 막대 — 면적 = 그 열 일위대가의 로직 물량 「이름|단위」 + 자재 「자재|MT키」 - * (서버 pills 길 · 39-4 · 다 `고정` — 더하기 · 빼기는 일위대가 쪽) · 재료 = 자재DB 찾기. +/** 면적산출 편집 화면 아래 알약 막대 — 그 열 일위대가의 로직 물량 「이름|단위」 + 자재 「자재|MT키」 + * (서버 pills 길 · 39-4 · 다 `고정` — 더하기 · 빼기는 일위대가 쪽). * 알약이 없으면 까닭 글(`notice`)을 같이 돌려줌 */ export async function pillsFor( - kind: SheetKind, + _kind: SheetKind, // 종류는 면적산출 하나 — 부르는 쪽 모양 그대로 column: string, ): Promise<{ links: LinkOptions; notice?: string }> { - if (kind === "material") { - const own = await inputPills(column, "재료"); - return { links: { 항목: own, 모드: "자재", 자재찾기: searchMaterials } }; - } const none = (notice: string) => ({ links: { 항목: [], 모드: "입력" as const }, notice }); try { const { 알약 } = await fetchLaborPills(column); @@ -304,86 +294,33 @@ 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 { /** 시스템 관리자만 저장함(구조물 도면은 서버가 미리 만들어 둠) */ isAdmin: boolean; - /** [도면 수정] · [면적산출 수정] · [재료산출 수정] — 그 열을 CAD · 스프레드시트 전체 편집으로 엶 + /** [도면 수정] · [면적산출 수정] — 그 열을 CAD · 스프레드시트 전체 편집으로 엶 * (페이지의 `open`) · label 은 화면 제목에 씀 */ onEdit: (column: string, label: string, kind: EditKind) => void; /** 집계표에 저장 안 한 고침이 있으면 버릴지 물음 — 취소면 false */ confirmLeave: () => Promise; - /** 「선택 입력」 카드에서 자리를 골랐음 — 저장 안 한 고침 표시 */ - onDirty?: () => void; /** 카드 고침을 버리지 않고 머묾 — 표의 고른 열을 되돌림(31-3) */ keepColumn?: (colId: string) => void; } export interface StructurePanelHandle { root: HTMLElement; - /** `{열 id: {도번, 면적번, 재료번}}` — 집계표 `drawingLabel` · `areaLabel` · `materialLabel` 이 씀 */ + /** `{열 id: {도번, 면적번}}` — 집계표 `drawingLabel` · `areaLabel` 이 씀 */ numbers: () => ColumnNumbers; /** 번호 목록 다시 받기 */ reload: () => Promise; - /** 「선택 입력」 카드에서 고른 자리를 서버에 저장 — 집계표 [저장] 뒤에 부름(실패하면 던짐) */ - saveInputs: () => Promise; - /** 카드에 저장 안 한 고침(도면 31-4 · 면적산출 · 재료산출 31-3) — 페이지가 떠날 때 함께 물음 */ + /** 카드에 저장 안 한 고침(도면 31-4 · 면적산출 31-3) — 페이지가 떠날 때 함께 물음 */ dirty: () => boolean; /** 고친 카드 저장(도면 · 엑셀) — 집계표 [저장] 이 카드를 다시 읽기 전에 부름(실패하면 던짐) */ saveCards: () => Promise; @@ -392,7 +329,7 @@ export interface StructurePanelHandle { /** 보던 열을 다시 읽음 — 집계표 [저장] 뒤(서버가 새 열 파일 · 번호를 만듦) */ refresh: (columns: StructureColumn[]) => void; /** 집계표 조회 후 선택(`onPick`) — 일위대가 = 고른 키(「비움」 = 빈 글 · 닫으면 null) · - * 도면 · 면적산출 · 재료산출 = 다른 열 것을 복사해 가져옴(돌려주는 글 없음) */ + * 도면 · 면적산출 = 다른 열 것을 복사해 가져옴(돌려주는 글 없음) */ pick: (row: string, colId: string | null, columns: StructureColumn[]) => Promise; destroy: () => void; } @@ -403,10 +340,6 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane let column: string | null = null; let columnLabel = ""; - const inputs: InputsCardHandle = createInputsCard({ - isAdmin: opts.isAdmin, - onDirty: () => opts.onDirty?.(), - }); // 카드 셋은 항목 카드 한 벌(`ItemCards`) — 여기서는 지금 층에서 열 하나를 읽어 넘기기만. const cards = createItemCards({ source: layerSource, @@ -415,13 +348,12 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane numberOf: (kind, id) => numbers[id]?.[PART_FIELD[kind]], // 도번 목록에 있는 열인데 못 읽으면 알림 — 없는 열은 그냥 「도면 없음」 warnOnFail: (id) => !!numbers[id], - extraCards: [inputs.root], // 「복사해 가져오기」 · 빈 카드 [복사 생성](39-5) — 집계표 줄 「조회 후 선택」 과 같은 길 onImport: (kind) => void pickCopy(kind, shown ?? undefined), - // 면적산출 · 재료산출 카드 미리보기 수정(31-3) — 전체 편집기와 같은 알약 · 저장 길 + // 면적산출 카드 미리보기 수정(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)).판, }, @@ -432,9 +364,6 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane shown = col; column = col?.id ?? null; columnLabel = col ? columnHead(col) : ""; - if (!col) void inputs.show(null, null); - // 집계표에 지금 고른 일위대가(저장 전일 수 있음) — 카드가 「먼저 저장」 안내를 고름 - else void inputs.show(col.id, (col as { 일위대가?: string | null }).일위대가 ?? null); return cards.load(col ? { id: col.id, label: columnLabel } : null); } @@ -472,7 +401,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane const same = (from: From): boolean => from.layer === here.layer && from.owner === here.owner; const picked = await pickHere({ title: `${fill(TEXT.pickCopy, word)} — ${target}`, - kind: kind === "material" ? "item" : kind, + kind, skip: (from, row) => same(from) && row.열 === to.id, ask: (p) => TEXT.overwrite @@ -519,9 +448,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane numbers: () => numbers, reload: async () => { numbers = await fetchStructureNumbers().catch(() => ({})); - inputs.invalidate(); // 새 열 · 바뀐 일위대가를 서버 항목 목록에서 다시 찾게 }, - saveInputs: () => inputs.save(), dirty: () => cards.dirty(), saveCards: () => cards.saveAll(), show, @@ -532,7 +459,6 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane }, pick, destroy: () => { - inputs.destroy(); cards.destroy(); }, }; 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 821a1f8db..7637780c2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -26,21 +26,10 @@ import { type StructureColumn, type StructurePanelHandle, } from "./M02_MasterTemplete_Structure"; -import { inputPills } from "./M02_MasterTemplete_Inputs"; import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs"; import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel"; -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; @@ -67,7 +56,6 @@ type Mods = Record Promise>; const NUMBER_FIELD: Partial> = { structure: "도번", area: "면적번", - material: "재료번", labor: "일위대가번", }; // 표 부품(sub3) · 도면 부품(sub2) — 내보낸 이름으로 찾음 @@ -141,7 +129,7 @@ export function createMain( const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") }); const reload = createButton({ label: L("M02_Reload"), variant: "ghost" }); const save = createButton({ label: L("M02_Save"), variant: "filled" }); - // 구조물 도면 · 면적산출 · 재료산출 상세 화면 → 구조물집계표(그 열 고른 채) + // 구조물 도면 · 면적산출 상세 화면 → 구조물집계표(그 열 고른 채) const back = createButton({ label: L("M02_Back"), variant: "ghost", size: "sm" }); const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴 const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } }); @@ -173,7 +161,7 @@ export function createMain( title.classList.toggle("m02-main__title--fold", combo); title.title = combo ? L("M02_TableFold") : ""; if (!combo) { - host.hidden = false; // 다른 상세(도면 · 면적 · 재료)는 늘 펼침 + host.hidden = false; // 다른 상세(도면 · 면적)는 늘 펼침 title.classList.remove("is-folded"); } }; @@ -191,14 +179,11 @@ 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 apanel: AreaPanel | null = null; // 면적산출 오른쪽 일위대가 패널(프로젝트 층 아님 · 39-4) let picked = "A1"; // 면적산출 시트에서 지금 고른 칸 — 칸 지정 모달 [지금 고른 칸으로] let seq = 0; let dirty = false; - /** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 · 재료산출 31-3) */ + /** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 31-3) */ const busy = (): boolean => dirty || !!panel?.dirty(); const guard = (event: BeforeUnloadEvent): void => { @@ -240,10 +225,6 @@ export function createMain( editor = null; sheet = null; sheetHandle = null; - matPanel = null; - matState = null; - cpanel?.destroy(); - cpanel = null; apanel?.destroy(); apanel = null; panel?.destroy(); @@ -252,12 +233,11 @@ export function createMain( slot.replaceChildren(); }; - /** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 · 재료산출 카드 */ + /** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 카드 */ function buildPanel(layer: Layer): StructurePanelHandle { const made = createStructurePanel({ isAdmin, confirmLeave, - onDirty: () => (dirty = true), // 「선택 입력」 카드에서 자리를 고름 — 집계표 [저장]으로 함께 저장 keepColumn: (colId) => sheet?.selectColumn(colId), // 카드 고침을 안 버림 — 고른 열 되돌림 // 같은 페이지에서 그 열을 CAD·스프레드시트 전체 편집으로 엶 onEdit: (column, label, kind) => { @@ -268,38 +248,23 @@ export function createMain( return made; } - /** 면적산출 · 재료산출 스프레드시트 — 아래 알약 막대 · 면적 = 그 열 일위대가 알약(`pillsFor`) · - * 재료 = 자재DB 찾기 · 프로젝트 층 아니면 면적 = 오른쪽 일위대가 패널(담은 로직 · 자재목록 · 칸 지정) · - * 재료 = 그 열 자재 조합 담은 자재(알약 항목 = 옆 판 표 = 같은 목록) + 옆 판 - * (`pillItems` = 옆 판에서 고친 목록으로 알약 막대만 다시 만들 때) */ + /** 면적산출 스프레드시트 — 아래 알약 막대 = 그 열 일위대가 알약(`pillsFor`) · + * 프로젝트 층 아니면 오른쪽 일위대가 패널(담은 로직 · 자재목록 · 칸 지정) */ async function mountSheet( doc: unknown, - kind: "area" | "material", + kind: "area", 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], @@ -308,7 +273,6 @@ export function createMain( tabs: false, // 시트 하나 · 탭 줄 없음 onChange: () => { dirty = !readOnly; - matPanel?.refresh(); // 알약을 이음 · 풂 — 옆 판 「이은 칸」 바로 갱신 apanel?.refresh(); // 칸 값 · 연결이 바뀜 — 반영 금액을 바로 다시 곱함 }, onSelect: (s) => (picked = s.칸), @@ -332,71 +296,11 @@ export function createMain( host.append(apanel.root); return; } - // 오른쪽 조합 패널 — 한 번 만들고 시트를 다시 붙여도 그대로(늘 시트 뒤) - cpanel ??= buildComboPanel({ - kind, - column: at.name, - isAdmin, - onUsed: () => remount(kind, at, readOnly, true), - }); - host.append(cpanel.root); - if (matPanel || !matState) return; - matPanel = buildMaterialPanel({ - 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; - }, - }); - cpanel.setMine(matPanel.root); } 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); - } - async function mount(doc: unknown, at: Selection): Promise { const readOnly = !canEdit(at.layer, isAdmin); host.replaceChildren(); @@ -406,8 +310,8 @@ export function createMain( "createSheet", ); if (!create) return empty(L("M02_NoSheet")); - // 구조물집계표(프로젝트 층 아님)만 — 도면 · 면적산출 · 재료산출 줄에 번호 · 칸을 고르면 아래 컨테이너 · - // 고칠 수 있으면 일위대가 · 도면 · 면적산출 · 재료산출 칸은 조회 후 선택 + // 구조물집계표(프로젝트 층 아님)만 — 도면 · 면적산출 줄에 번호 · 칸을 고르면 아래 컨테이너 · + // 고칠 수 있으면 일위대가 · 도면 · 면적산출 칸은 조회 후 선택 const struct = at.layer !== "project" && at.name === STRUCTURE_TABLE ? buildPanel(at.layer) : null; if (struct) await struct.reload(); @@ -427,7 +331,6 @@ export function createMain( ? { drawingLabel: (colId: string) => struct.numbers()[colId]?.도번 ?? "", areaLabel: (colId: string) => struct.numbers()[colId]?.면적번 ?? "", - materialLabel: (colId: string) => struct.numbers()[colId]?.재료번 ?? "", combo: { ...(readOnly || !extras.onHeadRename ? {} @@ -472,8 +375,8 @@ export function createMain( }); return; } - if (at.kind === "area" || at.kind === "material") { - // 면적산출 · 재료산출 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을 + if (at.kind === "area") { + // 면적산출 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을 // 열 때만 `import()` 로 받음(계약 `spreadsheet_types.ts` 머리) return mountSheet(doc, at.kind, at, readOnly); } @@ -529,7 +432,7 @@ export function createMain( ) : got.문서; if (mine !== seq) return; - // 구조물 도면 · 면적산출 · 재료산출은 열 id 로 열리니 머리 맨 앞에 번호(「SD01010100 돌기슭막이 …」) + // 구조물 도면 · 면적산출은 열 id 로 열리니 머리 맨 앞에 번호(「SD01010100 돌기슭막이 …」) const field = NUMBER_FIELD[next.kind]; const number = field && (got.문서 as Record | null)?.[field]; if (number) title.textContent = `${String(number)} ${next.label ?? next.name}`; @@ -551,11 +454,11 @@ export function createMain( try { const edited = editor ? await editor.getDoc() : loaded; // 구조물 도면 저장은 `도면` 칸만 갈아 끼움 — 도번은 읽은 그대로. - // 면적산출 · 재료산출은 스프레드시트 문서로 통째 갈아 끼우되 번호는 계약 밖(엔진이 모름)이라 옮겨 붙임. + // 면적산출은 스프레드시트 문서로 통째 갈아 끼우되 번호는 계약 밖(엔진이 모름)이라 옮겨 붙임. const doc = at.kind === "structure" && loaded ? { ...(loaded as StructureDoc), 도면: edited as StructureDoc["도면"] } - : (at.kind === "area" || at.kind === "material") && loaded + : at.kind === "area" && loaded ? { ...(loaded as Record), ...(edited as Record) } : edited; const info = await saveTemplate(at.layer, at.kind, at.name, at.projectId, version, doc); @@ -569,7 +472,6 @@ export function createMain( await panel.reload(); sheet.recalc(); panel.refresh(sheet.getDoc()?.열 ?? []); - await panel.saveInputs(); // 「선택 입력」 카드의 자리 고름 — 같은 [저장]에서 } showNotice([]); showToast(L("M02_Saved"), "success"); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 51c9400c1..35b18e7b9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,11 +1,11 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts * 마스터 템플릿 좌측 패널 — 「구조물 조합」(층 단추 · 나무 → 수평선 → 하위 구조물 도면 · 일위대가 · - * 면적산출 · 재료산출 · 자재 조합 · 하위마다 맨 위 「전체 항목」) · 「도면 양식」(새로 · 본떠 · 지우기). + * 면적산출 · 하위마다 맨 위 「전체 항목」) · 「도면 양식」(새로 · 본떠 · 지우기). * 서버 길은 계약 `6_계약.md`. - * 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 — - * 서버가 미리 만들어 둠(도번 SD · 면적번 SA · 재료번 SM + 같은 8자리) · - * 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). 「자재 조합」 컨테이너는 `M02_MasterTemplete_Matset.ts`. + * 구조물 도면 · 일위대가 · 면적산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 — + * 서버가 미리 만들어 둠(도번 SD · 일위대가번 SU · 면적번 SA + 같은 뒷자리) · + * 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. * 구조물 라이브러리는 좌측이 아니라 「구조물 조합」 상세 화면 왼쪽(PLAN 26-7c · 페이지가 붙임). * ========================================================================== */ @@ -39,7 +39,6 @@ import { structureLabel, type StructureColumn, } from "./M02_MasterTemplete_Structure"; -import { buildMatsetSection } from "./M02_MasterTemplete_Matset"; import { buildOptionsSection } from "./M02_MasterTemplete_Options"; import { buildPracticeSection } from "./M02_MasterTemplete_Practice"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; @@ -50,7 +49,7 @@ const KINDS: { kind: Kind; label: () => string }[] = [ ]; /** 구조물 열마다 한 줄씩인 하위 목록 — 종류(편집 칸이 여는 `Kind`) · 번호 칸 · 제목 · * `all` = 맨 위 「전체 항목」 줄(PLAN 39-2) · 차례 = 구조물 도면 → 일위대가(SU) → 면적산출 */ -type ColumnKind = "structure" | "labor" | "area" | "material"; +type ColumnKind = "structure" | "labor" | "area"; const COLUMN_KINDS: { kind: ColumnKind; key: keyof StructureNumbers; @@ -60,7 +59,6 @@ const COLUMN_KINDS: { { kind: "structure", key: "도번", label: () => L("M02_KindStructure"), all: true }, { kind: "labor", key: "일위대가번", label: () => L("M02_KindLabor"), all: true }, { kind: "area", key: "면적번", label: () => L("M02_KindArea"), all: true }, - { kind: "material", key: "재료번", label: () => L("M02_KindMaterial"), all: false }, ]; /** 「전체 항목」 줄이 있는 하위 — 누르면 H 목록 화면(`onAll`) */ export type AllKind = "structure" | "labor" | "area"; @@ -143,24 +141,12 @@ 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(), practice?.clear(), (allPicked = null), 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(), practice?.clear(), (allPicked = null), paint()), + onOpen: () => (practice?.clear(), (allPicked = null), paint()), }); /** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */ const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => { @@ -177,7 +163,6 @@ export function buildSide(opt: SideOptions): SideHandle { el("div", { className: "m02-side__items ui-sidebar-list", children: opt.tree ?? [] }), el("hr", { className: "m02-side__rule" }), ...columnSections, - matsetSection, ); summary.classList.remove("is-collapsed"); summary.classList.add("m02-side__summary"); @@ -198,7 +183,7 @@ export function buildSide(opt: SideOptions): SideHandle { ? buildPracticeSection({ confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (matset.clear(), options.clear(), (allPicked = null), paint()), + onOpen: () => (options.clear(), (allPicked = null), paint()), }) : null; const listHost = el("div", { @@ -288,21 +273,19 @@ export function buildSide(opt: SideOptions): SideHandle { for (const [kind, b] of editButtons) b.copy.disabled = b.del.disabled = cur?.종류 !== kind; } - /** 고름 표시는 여기서만 — 목록 · 열 줄 · 자재 조합 줄을 함께 그려야 두 줄이 동시에 안 남음 - * (양식이 열려 있으면 자재 조합 고름은 지움 · 자재 조합 상세는 열린 양식이 없음) */ + /** 고름 표시는 여기서만 — 목록 · 열 줄 · 옵션 줄을 함께 그려야 두 줄이 동시에 안 남음 + * (양식이 열려 있으면 옵션 · 일위대가 조합 고름은 지움) */ function paint(): void { if (opt.getOpen()) allPicked = null; paintList(); paintColumns(); paintButtons(); - if (opt.getOpen()) matset.clear(); if (opt.getOpen()) options.clear(); if (opt.getOpen()) practice?.clear(); } async function selectAll(kind: AllKind): Promise { if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return; - matset.clear(); options.clear(); practice?.clear(); allPicked = kind; @@ -351,7 +334,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 70139f1d7..63caa709f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -155,23 +155,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 1cac7ac39..d877cda49 100644 --- a/M02_MasterTemplete/M02_Project_Items_Router.py +++ b/M02_MasterTemplete/M02_Project_Items_Router.py @@ -5,12 +5,12 @@ POST …/items {structure, layer, owner, key} 미리 가져오기 → {폴더, 만듦, 가져온날} POST …/items/new {structure, 경로, 이름, 단위} 새 항목(개인 층에도) · 회사 없음 409 GET …/items/{폴더}/{종류} {종류, 판, 내용} - PUT …/items/{폴더}/{종류} {판, 내용} — 판이 다르면 409 · 면적 · 재료는 서버 계산값 + PUT …/items/{폴더}/{종류} {판, 내용} — 판이 다르면 409 · 면적은 서버 계산값 POST …/items/{폴더}/reapply 재적용(입력값 · 고른자재는 같은 키면 살림 · `덮임` 알림) POST …/items/{폴더}/save-as {to, 경로, 이름} 개인 층에 새 항목으로(단가 묶음은 안 가져감) POST …/items/{폴더}/calc 복사본만으로 계산(세 비목 · 계 · 빠진 입력) 일위대가 문서면 줄마다 수량 · 반영 + 자재 · 기준합 · 반영합 -종류 = 도면 · 면적 · 재료 · 자재조합 · 입력연결 · 입력값 · 고른자재 · 일위대가(읽기만 · 쓰면 400) · +종류 = 도면 · 면적 · 입력값 · 고른자재 · 일위대가(읽기만 · 쓰면 400) · `refresh` = 옛 이름(재적용). ⚠ 등록은 `main.py`(로그인만) — 프로젝트 권한(같은 회사)은 여기서 `_project` 로 봄(403 · 404). """ @@ -33,7 +33,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/M02_MasterTemplete/M02_Template_Layers.py b/M02_MasterTemplete/M02_Template_Layers.py index 6080bb5b8..8fc899387 100644 --- a/M02_MasterTemplete/M02_Template_Layers.py +++ b/M02_MasterTemplete/M02_Template_Layers.py @@ -1,9 +1,9 @@ """M02 양식 층 — 네 층의 자리 · 읽기 · 쓰기 · 복사 · manifest. 층 넷 (PLAN 10-5): - system `resources/master_template/{table,drawing,structure,area,material}/<이름>.json` (git) - company `storage/{회사}/templates/{table,drawing,structure,area,material}/` - personal `storage/{회사}/{사용자}/templates/{table,drawing,structure,area,material}/` + system `resources/master_template/{table,drawing,structure,area}/<이름>.json` (git) + company `storage/{회사}/templates/{table,drawing,structure,area}/` + personal `storage/{회사}/{사용자}/templates/{table,drawing,structure,area}/` project `storage/{회사}/{사용자}/{프로젝트}/templates/{…같은 다섯}/` + 초기 사본 `templates/_initial/` @@ -26,9 +26,9 @@ from common_util.common_util_json import atomic_write_json from config import config_system LAYERS = ("system", "company", "personal", "project") -# area = 면적산출 · material = 재료산출 — 통합문서(구조물집계표 열마다 · `A00_Common/spreadsheet/`) +# area = 면적산출 — 통합문서(구조물집계표 열마다 · `A00_Common/spreadsheet/`) # labor = 일위대가(구조물 열마다 · `M02_Labor_Doc`) -KINDS = ("table", "drawing", "structure", "labor", "area", "material") +KINDS = ("table", "drawing", "structure", "labor", "area") TEMPLATES_DIRNAME = "templates" INITIAL_DIRNAME = "_initial" MANIFEST_NAME = "manifest.json" @@ -171,7 +171,6 @@ _SKELETON = { "structure": ("도면.entities",), "labor": ("담은로직",), "area": ("시트",), - "material": ("시트",), } 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/inputs/item1.json b/resources/master_template/inputs/item1.json deleted file mode 100644 index 88862116f..000000000 --- a/resources/master_template/inputs/item1.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "판": 3, - "줄": [ - { - "로직": "GC000297", - "입력": "형식", - "자리": "선택" - }, - { - "로직": "GC000297", - "입력": "두께", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "구분", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "믹서", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "지역", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "재료배합", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "골재최대치수", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "수중", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "모래상태", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "자갈상태", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "모래단위중량", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "자갈단위중량", - "자리": "선택" - }, - { - "로직": "GF000156", - "입력": "자재지역", - "자리": "선택" - }, - { - "로직": "GF000162", - "입력": "포장두께", - "자리": "선택" - }, - { - "로직": "GF000162", - "입력": "운반", - "자리": "선택" - }, - { - "로직": "GF000162", - "입력": "운반감률", - "자리": "선택" - }, - { - "로직": "GF000162", - "입력": "공급감률", - "자리": "선택" - }, - { - "로직": "GF000162", - "입력": "종단경사", - "자리": "선택" - } - ] -} diff --git a/resources/master_template/material/item1.json b/resources/master_template/material/item1.json deleted file mode 100644 index 56b1753fb..000000000 --- a/resources/master_template/material/item1.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "item1", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM-000001" -} diff --git a/resources/master_template/material/item2.json b/resources/master_template/material/item2.json deleted file mode 100644 index 070827ebc..000000000 --- a/resources/master_template/material/item2.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "item2", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM-000002" -} diff --git a/resources/master_template/material/item3.json b/resources/master_template/material/item3.json deleted file mode 100644 index a75069ccd..000000000 --- a/resources/master_template/material/item3.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "item3", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM-000003" -} 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/material/c4.json b/resources/tester/fixtures/m02_master_sample/material/c4.json deleted file mode 100644 index c07721f53..000000000 --- a/resources/tester/fixtures/m02_master_sample/material/c4.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c4", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": { - "I5": { - "값": 6 - } - }, - "연결": [ - { - "키": "MT003810", - "이름": "모 래, 서울, 도착도 강모래", - "칸": "I5", - "단위": "㎥", - "자재": { - "코드": "MT003810", - "이름": "모 래, 서울, 도착도", - "규격": "강모래", - "단위": "㎥" - } - } - ] - } - ], - "활성": "s1", - "재료번": "SM01010101", - "계산값": {} -} diff --git a/resources/tester/fixtures/m02_master_sample/material/ms.json b/resources/tester/fixtures/m02_master_sample/material/ms.json deleted file mode 100644 index 72d3af7f4..000000000 --- a/resources/tester/fixtures/m02_master_sample/material/ms.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "ms", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM10010100" -} diff --git a/resources/tester/fixtures/m02_master_sample/material/pp_len.json b/resources/tester/fixtures/m02_master_sample/material/pp_len.json deleted file mode 100644 index 648bedb86..000000000 --- a/resources/tester/fixtures/m02_master_sample/material/pp_len.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pp_len", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM03010100" -} diff --git a/resources/tester/fixtures/m02_master_sample/material/rv.json b/resources/tester/fixtures/m02_master_sample/material/rv.json deleted file mode 100644 index 2709b17b8..000000000 --- a/resources/tester/fixtures/m02_master_sample/material/rv.json +++ /dev/null @@ -1,1425 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "rv", - "서식": [ - {}, - { - "가로": "center", - "테두리": { - "위": { - "선": "hair" - }, - "왼": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "Fixed", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0.0", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "_ * #,##0.00_ ", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "_-* #,##0.00_-", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0%", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0.00_)", - "테두리": { - "위": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - }, - "왼": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "Fixed", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "_-* #,##0_-", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "_-* #,##0.0_-", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0.0", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "_-* #,##0.00_-", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "형식": "_-* #,##0.00_-", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "left", - "형식": "0.0", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "형식": "0.0", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0.00_)", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "아래": { - "선": "hair" - }, - "왼": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "Fixed", - "테두리": { - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0.0", - "테두리": { - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0", - "테두리": { - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "_ * #,##0.00_ ", - "테두리": { - "아래": { - "선": "hair" - } - } - }, - { - "테두리": { - "아래": { - "선": "hair" - } - } - }, - { - "가로": "center", - "형식": "0.00_)", - "테두리": { - "아래": { - "선": "hair" - } - } - }, - { - "가로": "left", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - }, - "왼": { - "선": "thin" - }, - "오른": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "위": { - "선": "hair" - }, - "오른": { - "선": "hair" - } - } - }, - { - "테두리": { - "위": { - "선": "hair" - }, - "오른": { - "선": "thin" - } - } - }, - { - "가로": "center", - "테두리": { - "아래": { - "선": "hair" - }, - "왼": { - "선": "thin" - }, - "오른": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - }, - "왼": { - "선": "hair" - }, - "오른": { - "선": "hair" - } - } - }, - { - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "hair" - }, - "오른": { - "선": "thin" - } - } - }, - { - "테두리": { - "아래": { - "선": "hair" - }, - "오른": { - "선": "thin" - } - } - }, - { - "가로": "center" - }, - { - "가로": "center", - "형식": "Fixed" - }, - { - "가로": "center", - "형식": "0.0" - }, - { - "가로": "center", - "형식": "0" - }, - { - "가로": "center", - "형식": "_ * #,##0.00_ " - }, - { - "가로": "center", - "형식": "_-* #,##0.00_-" - }, - { - "테두리": { - "오른": { - "선": "thin" - } - } - }, - { - "가로": "left", - "테두리": { - "아래": { - "선": "thin" - }, - "왼": { - "선": "thin" - }, - "오른": { - "선": "hair" - } - } - }, - { - "가로": "center", - "테두리": { - "아래": { - "선": "thin" - }, - "오른": { - "선": "hair" - } - } - }, - { - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "thin" - } - } - }, - { - "가로": "center", - "형식": "Fixed", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "thin" - } - } - }, - { - "가로": "center", - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "thin" - } - } - }, - { - "가로": "center", - "테두리": { - "아래": { - "선": "thin" - } - } - }, - { - "가로": "center", - "형식": "0.00_)", - "테두리": { - "아래": { - "선": "thin" - } - } - }, - { - "테두리": { - "위": { - "선": "hair" - }, - "아래": { - "선": "thin" - }, - "오른": { - "선": "thin" - } - } - } - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": { - "C9": { - "값": "{ (", - "서식": 1 - }, - "D9": { - "값": 2, - "서식": 2 - }, - "E9": { - "값": "+", - "서식": 3 - }, - "F9": { - "값": 4.2, - "서식": 2 - }, - "G9": { - "값": ")", - "서식": 3 - }, - "H9": { - "값": "÷", - "서식": 4 - }, - "I9": { - "값": 2, - "서식": 5 - }, - "J9": { - "값": "}", - "서식": 3 - }, - "K9": { - "값": " × ", - "서식": 6 - }, - "L9": { - "값": 2.2, - "서식": 7 - }, - "N9": { - "값": "×", - "서식": 2 - }, - "O9": { - "값": 1, - "서식": 4 - }, - "P9": { - "값": "×", - "서식": 3 - }, - "Q9": { - "값": "100%", - "서식": 8 - }, - "S9": { - "값": " ", - "서식": 9 - }, - "T9": { - "값": " ", - "서식": 9 - }, - "U9": { - "값": " =", - "서식": 3 - }, - "V9": { - "값": 6.82, - "서식": 10 - }, - "W9": { - "값": "㎥", - "서식": 3 - }, - "C10": { - "값": "{ (", - "서식": 1 - }, - "D10": { - "값": 2, - "서식": 2 - }, - "E10": { - "값": "+", - "서식": 3 - }, - "F10": { - "값": 4.2, - "서식": 2 - }, - "G10": { - "값": ")", - "서식": 3 - }, - "H10": { - "값": "÷", - "서식": 4 - }, - "I10": { - "값": 2, - "서식": 5 - }, - "J10": { - "값": "}", - "서식": 3 - }, - "K10": { - "값": " × ", - "서식": 6 - }, - "L10": { - "값": 2.2, - "서식": 7 - }, - "N10": { - "값": "×", - "서식": 2 - }, - "O10": { - "값": 1, - "서식": 4 - }, - "P10": { - "값": "×", - "서식": 3 - }, - "Q10": { - "값": "0%", - "서식": 8 - }, - "S10": { - "값": " ", - "서식": 9 - }, - "T10": { - "값": " ", - "서식": 9 - }, - "U10": { - "값": " =", - "서식": 3 - }, - "V10": { - "값": 0, - "서식": 10 - }, - "W10": { - "값": "㎥", - "서식": 3 - }, - "C11": { - "값": "[ { (", - "서식": 11 - }, - "D11": { - "값": 2, - "서식": 12 - }, - "E11": { - "값": "+", - "서식": 13 - }, - "F11": { - "값": 3.7, - "서식": 12 - }, - "G11": { - "값": ")", - "서식": 13 - }, - "H11": { - "값": "÷", - "서식": 13 - }, - "I11": { - "값": 2, - "서식": 13 - }, - "J11": { - "값": " × ", - "서식": 14 - }, - "K11": { - "값": 1.7, - "서식": 15 - }, - "L11": { - "값": "} - {", - "서식": 16 - }, - "M11": { - "값": 3.14, - "서식": 17 - }, - "N11": { - "값": "×", - "서식": 16 - }, - "O11": { - "값": 0.6, - "서식": 18 - }, - "P11": { - "값": "²} ", - "서식": 19 - }, - "Q11": { - "값": "]", - "서식": 20 - }, - "R11": { - "값": "×", - "서식": 13 - }, - "S11": { - "값": 1, - "서식": 16 - }, - "T11": { - "값": " ", - "서식": 21 - }, - "U11": { - "값": " =", - "서식": 13 - }, - "V11": { - "값": 3.71, - "서식": 22 - }, - "W11": { - "값": "㎥", - "서식": 13 - }, - "C12": { - "값": "{ (", - "서식": 23 - }, - "D12": { - "값": 3.7, - "서식": 24 - }, - "E12": { - "값": "+", - "서식": 25 - }, - "F12": { - "값": 4.2, - "서식": 24 - }, - "G12": { - "값": ")", - "서식": 25 - }, - "H12": { - "값": "÷", - "서식": 26 - }, - "I12": { - "값": 2, - "서식": 27 - }, - "J12": { - "값": "}", - "서식": 25 - }, - "K12": { - "값": " × ", - "서식": 28 - }, - "L12": { - "값": 0.5, - "서식": 17 - }, - "N12": { - "값": "×", - "서식": 24 - }, - "O12": { - "값": 1, - "서식": 26 - }, - "P12": { - "값": " ", - "서식": 29 - }, - "Q12": { - "값": " ", - "서식": 29 - }, - "R12": { - "값": " ", - "서식": 26 - }, - "S12": { - "값": " ", - "서식": 29 - }, - "T12": { - "값": " ", - "서식": 29 - }, - "U12": { - "값": " =", - "서식": 25 - }, - "V12": { - "값": 1.97, - "서식": 30 - }, - "W12": { - "값": "㎥", - "서식": 25 - }, - "C17": { - "값": " 1.터파기", - "서식": 31 - }, - "D17": { - "값": "토사(100%)", - "서식": 32 - }, - "E17": { - "값": "{ (", - "서식": 3 - }, - "F17": { - "식": "K22/1000", - "서식": 2 - }, - "G17": { - "값": "+", - "서식": 3 - }, - "H17": { - "식": "K5/1000", - "서식": 2 - }, - "I17": { - "값": ")", - "서식": 3 - }, - "J17": { - "값": "÷", - "서식": 4 - }, - "K17": { - "값": 2, - "서식": 5 - }, - "L17": { - "값": "}", - "서식": 3 - }, - "M17": { - "값": "×", - "서식": 6 - }, - "N17": { - "식": "R15/1000", - "서식": 7 - }, - "P17": { - "값": "×", - "서식": 2 - }, - "Q17": { - "값": 1, - "서식": 4 - }, - "R17": { - "값": "×", - "서식": 3 - }, - "S17": { - "값": 1, - "서식": 8 - }, - "U17": { - "값": " ", - "서식": 9 - }, - "V17": { - "값": " ", - "서식": 9 - }, - "W17": { - "값": " =", - "서식": 3 - }, - "X17": { - "식": "ROUNDDOWN((D26+F26)/2*L26*Q26,2)", - "서식": 10 - }, - "Y17": { - "값": "㎥", - "서식": 3 - }, - "Z17": { - "값": " ", - "서식": 33 - }, - "D18": { - "값": "암(0%)", - "서식": 32 - }, - "E18": { - "값": "{ (", - "서식": 3 - }, - "F18": { - "식": "D26", - "서식": 2 - }, - "G18": { - "값": "+", - "서식": 3 - }, - "H18": { - "식": "F26", - "서식": 2 - }, - "I18": { - "값": ")", - "서식": 3 - }, - "J18": { - "값": "÷", - "서식": 4 - }, - "K18": { - "값": 2, - "서식": 5 - }, - "L18": { - "값": "}", - "서식": 3 - }, - "M18": { - "값": "×", - "서식": 6 - }, - "N18": { - "식": "L26", - "서식": 7 - }, - "P18": { - "값": "×", - "서식": 2 - }, - "Q18": { - "값": 1, - "서식": 4 - }, - "R18": { - "값": "×", - "서식": 3 - }, - "S18": { - "값": 0, - "서식": 8 - }, - "U18": { - "값": " ", - "서식": 9 - }, - "V18": { - "값": " ", - "서식": 9 - }, - "W18": { - "값": " =", - "서식": 3 - }, - "X18": { - "식": "ROUNDDOWN((D27+F27)/2*L27*Q27,2)", - "서식": 10 - }, - "Y18": { - "값": "㎥", - "서식": 3 - }, - "Z18": { - "값": " ", - "서식": 33 - }, - "C19": { - "값": "2.되메우기", - "서식": 34 - }, - "D19": { - "값": "일반토사", - "서식": 35 - }, - "E19": { - "값": "[ { (", - "서식": 13 - }, - "F19": { - "식": "D26", - "서식": 12 - }, - "G19": { - "값": "+", - "서식": 13 - }, - "H19": { - "식": "K7/1000", - "서식": 12 - }, - "I19": { - "값": ")", - "서식": 13 - }, - "J19": { - "값": "÷", - "서식": 13 - }, - "K19": { - "값": 2, - "서식": 13 - }, - "L19": { - "값": "×", - "서식": 14 - }, - "M19": { - "식": "D16/1000", - "서식": 15 - }, - "N19": { - "값": "} - {", - "서식": 16 - }, - "O19": { - "값": 3.14, - "서식": 17 - }, - "P19": { - "값": "×", - "서식": 16 - }, - "Q19": { - "식": "K20/2000", - "서식": 18 - }, - "R19": { - "값": "²} ", - "서식": 19 - }, - "S19": { - "값": "]", - "서식": 20 - }, - "T19": { - "값": "×", - "서식": 13 - }, - "U19": { - "값": 1, - "서식": 16 - }, - "V19": { - "값": " ", - "서식": 21 - }, - "W19": { - "값": " =", - "서식": 13 - }, - "X19": { - "식": "ROUNDDOWN(((D28+F28)/2*K28-(3.14*O28*O28))*1,2)", - "서식": 22 - }, - "Y19": { - "값": "㎥", - "서식": 13 - }, - "Z19": { - "값": " ", - "서식": 36 - }, - "E20": { - "값": "{ (", - "서식": 25 - }, - "F20": { - "식": "K7/1000", - "서식": 24 - }, - "G20": { - "값": "+", - "서식": 25 - }, - "H20": { - "식": "F26", - "서식": 24 - }, - "I20": { - "값": ")", - "서식": 25 - }, - "J20": { - "값": "÷", - "서식": 26 - }, - "K20": { - "값": 2, - "서식": 27 - }, - "L20": { - "값": "}", - "서식": 25 - }, - "M20": { - "값": "×", - "서식": 28 - }, - "N20": { - "식": "D13/1000", - "서식": 17 - }, - "P20": { - "값": "×", - "서식": 24 - }, - "Q20": { - "값": 1, - "서식": 26 - }, - "R20": { - "값": " ", - "서식": 29 - }, - "S20": { - "값": " ", - "서식": 29 - }, - "T20": { - "값": " ", - "서식": 26 - }, - "U20": { - "값": " ", - "서식": 29 - }, - "V20": { - "값": " ", - "서식": 29 - }, - "W20": { - "값": " =", - "서식": 25 - }, - "X20": { - "식": "ROUNDDOWN((D29+F29)/2*L29,2)", - "서식": 30 - }, - "Y20": { - "값": "㎥", - "서식": 25 - }, - "Z20": { - "값": " ", - "서식": 37 - }, - "E21": { - "값": "계 : ", - "서식": 38 - }, - "W21": { - "값": " =", - "서식": 25 - }, - "X21": { - "식": "V28+V29", - "서식": 30 - }, - "Y21": { - "값": "㎥", - "서식": 25 - }, - "Z21": { - "값": " ", - "서식": 44 - }, - "C22": { - "값": " 4.잔 토", - "서식": 45 - }, - "D22": { - "값": " ", - "서식": 46 - }, - "E22": { - "값": " ", - "서식": 47 - }, - "F22": { - "값": "1식", - "서식": 48 - }, - "G22": { - "값": " -", - "서식": 49 - }, - "H22": { - "값": "3식", - "서식": 48 - }, - "I22": { - "값": " ", - "서식": 49 - }, - "J22": { - "값": " ", - "서식": 48 - }, - "L22": { - "값": " ", - "서식": 49 - }, - "M22": { - "값": " ", - "서식": 49 - }, - "N22": { - "값": " ", - "서식": 49 - }, - "O22": { - "값": " ", - "서식": 49 - }, - "P22": { - "값": " ", - "서식": 49 - }, - "Q22": { - "값": " ", - "서식": 49 - }, - "R22": { - "값": " ", - "서식": 49 - }, - "S22": { - "값": " ", - "서식": 47 - }, - "T22": { - "값": " ", - "서식": 47 - }, - "U22": { - "값": " ", - "서식": 47 - }, - "V22": { - "값": " ", - "서식": 47 - }, - "W22": { - "값": " =", - "서식": 50 - }, - "X22": { - "식": "(V26+V27)-V29", - "서식": 51 - }, - "Y22": { - "값": "㎥", - "서식": 50 - }, - "Z22": { - "값": " ", - "서식": 52 - }, - "F21": { - "서식": 39 - }, - "G21": { - "서식": 38 - }, - "H21": { - "서식": 39 - }, - "I21": { - "서식": 38 - }, - "J21": { - "서식": 40 - }, - "K21": { - "서식": 41 - }, - "L21": { - "서식": 38 - }, - "M21": { - "서식": 42 - }, - "N21": { - "서식": 43 - }, - "O21": { - "서식": 43 - }, - "P21": { - "서식": 39 - }, - "Q21": { - "서식": 40 - }, - "T21": { - "서식": 40 - } - }, - "병합": [ - "L12:M12", - "Q10:R10", - "L10:M10", - "Q9:R9", - "L9:M9", - "C17:C18", - "N17:O17", - "S17:T17", - "N18:O18", - "S18:T18", - "C19:C21", - "D19:D21", - "N20:O20", - "J22:K22" - ] - } - ], - "활성": "s1", - "재료번": "SM01010100", - "계산값": { - "s1": { - "F17": { - "수": "0" - }, - "H17": { - "수": "0" - }, - "N17": { - "수": "0" - }, - "X17": { - "수": "0" - }, - "F18": { - "수": "0" - }, - "H18": { - "수": "0" - }, - "N18": { - "수": "0" - }, - "X18": { - "수": "0" - }, - "F19": { - "수": "0" - }, - "H19": { - "수": "0" - }, - "M19": { - "수": "0" - }, - "Q19": { - "수": "0.001" - }, - "X19": { - "수": "0" - }, - "F20": { - "수": "0" - }, - "H20": { - "수": "0" - }, - "N20": { - "수": "0" - }, - "X20": { - "수": "0" - }, - "X21": { - "수": "0" - }, - "X22": { - "수": "0" - } - } - } -} 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/helper_item_picker_project.cjs b/resources/tester/helper_item_picker_project.cjs index fcdf04a53..a54bc1ac5 100644 --- a/resources/tester/helper_item_picker_project.cjs +++ b/resources/tester/helper_item_picker_project.cjs @@ -1,9 +1,9 @@ /* 항목 고르기 모달 자료 창구 시험 헬퍼(계획서 25-2d) — `ui_template_item_picker_project.ts` 와 - * 선택 입력 카드 셈(`M02_MasterTemplete_Inputs_Model.ts`)을 트랜스파일해 가짜 서버(fetch 대역) 위에서 돌린다. + * 넣을 값 셈(`M02_Labor_Values_Model.ts`)을 트랜스파일해 가짜 서버(fetch 대역) 위에서 돌린다. * 서버 모양은 sub6 복사본 길(`M02_Project_Items_Router.py`) · sub4 입력 길 그대로. 결과는 JSON 한 벌. * ① 폴더 이름 · 목록 {항목} · 가져오기 POST · 읽기 {종류, 판, 내용} · 고침 PUT {판, 내용}(판 · 409 · 403) - * ② 새로 가져오기 · 계산 한 줄 · 입력 목록(한 번만 받음) - * ③ 카드 셈: 범위 · 고르기 원래 모양 · 자리 「선택」 만 묶음 + * ② 새로 가져오기 · 계산 한 줄 + * ③ 값 셈: 범위 · 고르기 원래 모양 * ④ 일위대가(39-1c): 복사본 요약 · 원본 층 계산 요약 · 넣을 값 = 담은 로직 입력 전부 * ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다. */ @@ -42,7 +42,7 @@ const reply = (status, body) => ({ }); /** 가짜 서버 — 복사본 폴더마다 부분 파일 · 판(글자) */ -const state = { readOnly: false, calls: [], folders: {}, inputsHits: 0 }; +const state = { readOnly: false, calls: [], folders: {} }; const versionOf = (content) => `v${JSON.stringify(content ?? null).length}`; const INPUTS = { @@ -82,10 +82,6 @@ globalThis.fetch = async (url, init = {}) => { 반영합: { 계: 1500 }, }); } - if (/^\/api\/m02\/layers\/\w+\/items\/[^/]+\/inputs$/.test(url)) { - state.inputsHits += 1; - return reply(200, INPUTS); - } const m = /^\/api\/projects\/([^/]+)\/items(?:\/([^/]+)(?:\/([^/]+))?)?$/.exec(url); if (!m) return reply(404, { detail: "Not Found" }); const [, , folderEnc, partEnc] = m; @@ -173,7 +169,7 @@ globalThis.fetch = async (url, init = {}) => { }; const project = loadTs(path.join(ROOT, "ui_template", "ui_template_item_picker_project.ts")); -const model = loadTs(path.join(ROOT, "M02_MasterTemplete", "M02_MasterTemplete_Inputs_Model.ts")); +const model = loadTs(path.join(ROOT, "M02_MasterTemplete", "M02_Labor_Values_Model.ts")); const row = (over = {}) => ({ key: "SS000001", 열: "rv", layer: "system", owner: null, ...over }); @@ -257,18 +253,12 @@ async function main() { const valueData = await project.laborValueData(["GA1", "GA2"], { GA1: {} }); out.laborValues = { keys: state.logicKeys, - link: valueData.link.줄.map((l) => `${l.로직}|${l.입력}|${l.자리}`), - shown: model.pickedGroups(valueData.list, valueData.link).length, + logics: valueData.list.로직.map((l) => [l.로직, l.입력.map((i) => i.이름)]), + hasLink: "link" in valueData, values: valueData.values, }; - // 입력 목록 — 같은 항목은 한 번만 받음 - const first = await project.loadInputs(row()); - await project.loadInputs(row()); - out.inputsHits = state.inputsHits; - out.inputsLogics = first.로직.map((l) => [l.로직, l.입력.map((i) => i.이름)]); - - // 카드 셈 + // 값 셈 out.model = { range: [ model.rangeOf({ 이름: "a", 범위: [0, 50] }), @@ -276,22 +266,7 @@ async function main() { model.rangeOf({ 이름: "c" }), ], choices: model.choicesOf({ 이름: "d", 고르기: [15, "16", { 값: 18, 이름: "열여덟" }] }), - groups: model - .pickedGroups(first, { - 판: "x", - 줄: [ - { 로직: "GA1", 입력: "지역", 자리: "선택" }, - { 로직: "GA1", 입력: "소량감률", 자리: "면적" }, - { 로직: "GA2", 입력: "지역", 자리: "선택" }, - ], - }) - .map((g) => [g.logic.로직, g.inputs.map((i) => i.이름)]), - noLink: model.pickedGroups(first, null).length, - pills: model.pillsOf( - first, - { 판: "x", 줄: [{ 로직: "GA1", 입력: "소량감률", 자리: "면적" }] }, - "면적", - ), + key: model.inputKey("GA1", "지역"), }; return out; } diff --git a/resources/tester/helper_m02_inputs.cjs b/resources/tester/helper_m02_inputs.cjs deleted file mode 100644 index 1e14e5c27..000000000 --- a/resources/tester/helper_m02_inputs.cjs +++ /dev/null @@ -1,61 +0,0 @@ -/* M02 「선택 입력」 카드 셈(`M02_MasterTemplete_Inputs_Model.ts`) 단위검증 헬퍼 — - * 그 자리에서 트랜스파일해 Node 로 돌리고 값을 JSON 으로 낸다. */ -const fs = require("fs"); -const path = require("path"); -const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); - -// 상대 경로 TS(값 함수 = `M02_Labor_Values_Model.ts` 다시 내보냄)는 이어 트랜스파일 -function loadTs(file) { - const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, - }).outputText; - const box = { exports: {} }; - const local = (request) => - request.startsWith(".") ? loadTs(`${path.resolve(path.dirname(file), request)}.ts`) : require(request); - new Function("exports", "module", "require", js)(box.exports, box, local); - return box.exports; -} -const m = loadTs(path.join(__dirname, "..", "..", "M02_MasterTemplete", "M02_MasterTemplete_Inputs_Model.ts")); - -// 같은 이름(높이)의 입력이 두 로직에 있음 -const list = { - 일위대가: "UA000001", - 로직: [ - { - 로직: "GC1", - 이름: "돌쌓기", - 입력: [ - { 이름: "높이", 단위: "m", 범위: [0.5, 5], 자동값: 1.2 }, - { 이름: "종류", 고르기: ["막돌", "견치돌"] }, - ], - }, - { 로직: "GC2", 이름: "타설", 입력: [{ 이름: "높이", 범위: { 최소: 0, 최대: 3 } }, { 이름: "배합" }] }, - ], - 빠진로직: [], -}; -const seats = m.seatsOf({ - 판: "v1", - 줄: [ - { 로직: "GC1", 입력: "높이", 자리: "면적", 알약: "입력|GC1|높이" }, - { 로직: "GC2", 입력: "높이", 자리: "면적" }, - { 로직: "GC2", 입력: "배합", 자리: "재료" }, - { 로직: "GC1", 입력: "종류", 자리: null }, - { 로직: "GC9", 입력: "없는것", 자리: "선택" }, - ], -}); -const links = { 판: "v1", 줄: [...[...seats].map(([k, 자리]) => ({ 로직: k.split("|")[0], 입력: k.split("|")[1], 자리 }))] }; -const pick = new Map(seats); -pick.set(m.inputKey("GC1", "종류"), "선택"); -process.stdout.write( - JSON.stringify({ - keys: [m.inputKey("GC1", "높이"), m.pillKey("GC2", "높이")], - seatKeys: [...seats.keys()], - count: m.countOf(list), - area: m.pillsOf(list, links, "면적").map((p) => [p.키, p.이름, p.단위 ?? null, p.고정]), - material: m.pillsOf(list, links, "재료").map((p) => [p.키, p.이름]), - lines: m.linesOf(list, pick), - none: m.linesOf(list, new Map()), - hints: list.로직.flatMap((l) => l.입력).map((i) => [m.valueHint(i), m.autoText(i)]), - seatOptions: m.SEAT_OPTIONS.map((o) => o.label), - }), -); diff --git a/resources/tester/helper_m02_master_sample.py b/resources/tester/helper_m02_master_sample.py index a12185196..12e8807fa 100644 --- a/resources/tester/helper_m02_master_sample.py +++ b/resources/tester/helper_m02_master_sample.py @@ -1,7 +1,7 @@ """시험 안 마스터 견본(PLAN 26-5a) — 초기화 전 마스터 층을 줄여 얼린 것 (`fixtures/m02_master_sample`). -집계표(바인딩 열 54) · SS 장부(51 줄) · 자재조합 · 세 파일은 rv · c4 · pp_len · ms 넷(도면 줄임) — +집계표(바인딩 열 54) · SS 장부(51 줄) · 도면 · 면적 파일은 rv · c4 · pp_len · ms 넷(도면 줄임) — 시험은 실제 `resources/master_template` 을 안 읽고 이 견본을 임시 폴더에 복사해 씀. """ diff --git a/resources/tester/spreadsheet/test_spreadsheet_saved_values.py b/resources/tester/spreadsheet/test_spreadsheet_saved_values.py index 0d165bb75..38baa3f46 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_saved_values.py +++ b/resources/tester/spreadsheet/test_spreadsheet_saved_values.py @@ -1,4 +1,4 @@ -"""저장 뒤 식 · 값 유지 — M02 면적산출 · 재료산출 저장본을 서버 엔진으로 다시 풀면 저장된 `계산값` 과 같음. +"""저장 뒤 식 · 값 유지 — M02 면적산출 저장본을 서버 엔진으로 다시 풀면 저장된 `계산값` 과 같음. 시험 안 견본 저장본(`fixtures/m02_master_sample` · PLAN 26-5a)을 읽기만 함. 저장 때 서버가 적은 값과 지금 엔진 풀이가 어긋나면 화면을 다시 열 때 @@ -18,7 +18,7 @@ pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 def _books() -> list[tuple[str, dict]]: out = [] - for kind in ("area", "material"): + for kind in ("area",): for path in sorted((FOLDER / kind).glob("[!.]*.json")): book = json.loads(path.read_text(encoding="utf-8")) if isinstance(book, dict) and book.get("시트"): diff --git a/resources/tester/spreadsheet/test_spreadsheet_store.py b/resources/tester/spreadsheet/test_spreadsheet_store.py index edac0a183..f1d81d820 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_store.py +++ b/resources/tester/spreadsheet/test_spreadsheet_store.py @@ -1,4 +1,4 @@ -"""F — 면적산출(area) · 재료산출(material) 저장 · 옛 산출근거(basis) 옮김 · [저장] 때 서버 계산값 · +"""F — 면적산출(area) 저장 · 옛 산출근거(basis) 옮김 · [저장] 때 서버 계산값 · 알약 연결(`연결`)은 저장 · 복사 때 문서와 같이. 서버 Node 풀이는 `test_spreadsheet_node.py` 가 따로 봄 — 여기는 `with_server_values` 를 바꿔 끼워 @@ -42,7 +42,7 @@ def _write(folder, kind, column, doc): def test_옛_산출근거를_면적산출로_옮기고_번호_8자리(client, tmp_path): - """12장 파일(SD · SC 6자리 · basis/) → SA 로 옮김 · 끝에 00 · 재료산출 새로 · 내용 그대로.""" + """12장 파일(SD · SC 6자리 · basis/) → SA 로 옮김 · 끝에 00 · 재료산출은 안 만듦 · 내용 그대로.""" structure = {"양식": "구조물도면", "종류": "structure", "판": 1, "열": "rv", "도번": "SD010101"} _write(tmp_path, "structure", "rv", {**structure, "도면": {"entities": [{"type": "line"}]}}) old = {**store._blank("area", "rv"), "표번": "SC010101", "연결": LINKS} @@ -56,30 +56,30 @@ def test_옛_산출근거를_면적산출로_옮기고_번호_8자리(client, tm "A2": {"값": "터파기"} } assert store.read("structure", "rv")["문서"]["도번"] == "SD01010100" - material = store.read("material", "rv")["문서"] - assert (material["재료번"], material["시트"][0]["이름"]) == ("SM01010100", "재료산출") - assert material["시트"][0]["칸"] == {} and material["서식"] == [{}] # 진짜 빈 시트 + assert not (tmp_path / "material").exists() # 재료산출은 없앰 before = {p: p.read_bytes() for p in tmp_path.rglob("*.json")} assert store.renumber() == {"옮김": [], "지움": [], "바꿈": [], "새로": []} assert before == {p: p.read_bytes() for p in tmp_path.rglob("*.json")} # 두 번 돌려도 같음 -def test_새_구조물_도면은_면적_재료_진짜_빈_시트_같은_8자리(client): +def test_새_구조물_도면은_면적_진짜_빈_시트_같은_8자리(client): made1 = client.post("/api/m02/structures/rv").json()["문서"] made2 = client.post("/api/m02/structures/pv_b").json()["문서"] # 새 항목 번호 = 자리 뜻 없는 일련번호(PLAN 26-1) assert made1["도번"] == "SD-000001" and made2["도번"] == "SD-000002" - for kind, key, head in (("area", "면적번", "SA"), ("material", "재료번", "SM")): + for kind, key, head in (("area", "면적번", "SA"),): doc = client.get(f"/api/m02/templates/{kind}/pv_b").json()["문서"] assert doc[key] == f"{head}-000002" assert doc["시트"][0]["칸"] == {} and doc["서식"] == [{}] # 머리 줄 없음 numbers = client.get("/api/m02/structures/numbers").json() - assert numbers["pv_b"] == {"도번": "SD-000002", "면적번": "SA-000002", "재료번": "SM-000002"} + assert numbers["pv_b"] == { + "도번": "SD-000002", + "일위대가번": "SU-000002", + "면적번": "SA-000002", + } -@pytest.mark.parametrize( - ("kind", "key", "head"), [("area", "면적번", "SA"), ("material", "재료번", "SM")] -) +@pytest.mark.parametrize(("kind", "key", "head"), [("area", "면적번", "SA")]) def test_다른_열_내용을_복사해_가져옴_연결도_같이(client, kind, key, head): store.renumber() got = client.get(f"/api/m02/templates/{kind}/rv").json() @@ -117,21 +117,21 @@ def test_저장은_서버_계산값만_적음_연결_두고_번호는_서버_것 def test_못_풀면_저장을_막음(client, monkeypatch): - store._ensure("material", "rv") - got = client.get("/api/m02/templates/material/rv").json() + store._ensure("area", "rv") + got = client.get("/api/m02/templates/area/rv").json() def broken(_book): raise spreadsheet.RecalcError("스프레드시트 계산 실패 — 시험") monkeypatch.setattr(spreadsheet, "with_server_values", broken) - r = client.put("/api/m02/templates/material/rv", json={"판": got["판"], "문서": got["문서"]}) + r = client.put("/api/m02/templates/area/rv", json={"판": got["판"], "문서": got["문서"]}) assert r.status_code == 503 - assert client.get("/api/m02/templates/material/rv").json()["판"] == got["판"] + assert client.get("/api/m02/templates/area/rv").json()["판"] == got["판"] def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch): client.post("/api/m02/structures/rv") - for kind in ("area", "material"): + for kind in ("area",): doc = store.read(kind, "rv")["문서"] layers.check_skeleton(kind, doc) for bad in ({}, {**doc, "시트": {}}): @@ -140,5 +140,6 @@ def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch): layers.check_skeleton("structure", {"도면": {"entities": []}}) # 산출근거 없어도 됨 monkeypatch.setattr(layers, "SYSTEM_ROOT", tmp_path) copied = layers.seed_project(tmp_path / "project") - for kind in ("area", "material"): + assert not any(k.startswith("material/") for k in copied["작업본"]) # 재료산출 없앰 + for kind in ("area",): assert f"{kind}/rv" in copied["작업본"] and f"{kind}/rv" in copied["초기"] diff --git a/resources/tester/test_item_copy_complete.py b/resources/tester/test_item_copy_complete.py index 9d1265f59..fc5be79a1 100644 --- a/resources/tester/test_item_copy_complete.py +++ b/resources/tester/test_item_copy_complete.py @@ -2,7 +2,7 @@ 복사본은 구조물마다(`templates/structures/{구조물 id}/` · 관은 `pipe@측점`). -- 지정 → [저장] → 폴더에 새 파일(입력연결 · 입력값 · 고른자재 · 단가) · manifest 가져온날 · 원본판 +- 지정 → [저장] → 폴더에 새 파일(입력값 · 고른자재 · 단가) · manifest 가져온날 · 원본판 - 단가 묶음이 닫혀 있음 — 조합 37개 · 로직 170개를 마스터와 복사본으로 돌려 같은 값 - 마스터 단가 한 줄을 바꿔도 프로젝트 계산 그대로 · 목록에 「마스터 바뀜」 · 새로 가져오기 뒤 바뀜 (입력값 · 고른자재는 같은 키면 살림) @@ -52,9 +52,9 @@ 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"): + for kind in ("table", "structure", "area"): shutil.copytree(REAL / kind, system / kind) monkeypatch.setattr(layers, "SYSTEM_ROOT", system) monkeypatch.setattr(m01, "FOLDER", master_copy) @@ -109,7 +109,7 @@ def test_지정하면_새_파일까지_복사(world): root = world["root"] assert _save(root, [_pipe()])["만듦"] == [FOLDER] folder = root / "templates/structures" / FOLDER - assert _read(folder / "입력연결.json") == {"판": None, "줄": []} # 층 연결이 아직 없음 + assert not (folder / "입력연결.json").exists() # 선택 입력 연결은 없앰 assert _read(folder / "입력값.json") == {} and _read(folder / "고른자재.json") == {} price = _read(folder / "단가.json") rows = {k: len(v.get("줄") or v.get("표")) for k, v in price.items()} @@ -134,18 +134,6 @@ def test_지정하면_새_파일까지_복사(world): ] -def test_층_입력연결을_복사(world): - root, system = world["root"], world["system"] - (system / "inputs").mkdir() - link = {"판": "a1", "줄": [{"로직": "GF000176", "입력": "관경", "자리": "면적", "알약": "x"}]} - column = next( - r["열"] for r in _read(system / layers.COMBO_FILE)["줄"] if r["키"] == PIPE["key"] - ) - (system / "inputs" / f"{column}.json").write_text(json.dumps(link, ensure_ascii=False), "utf-8") - _save(root, [_pipe()]) - assert copies.read_file(root, FOLDER, "입력연결")["내용"] == link - - def test_단가_묶음은_닫혀_있음(world): """조합마다 복사본 단가 묶음만으로 돌린 값 = 마스터로 돌린 값(자동값 · 고르기 끝값).""" files, whole = prices.master() @@ -263,7 +251,6 @@ def test_쓰기_뒤_다시_읽기_같음_판_다르면_409(world): ("입력값", {"GF000176": {"관경": 600}}), ("입력값", {"GF000176": {"관경": {"값": 1, "확인": "네"}}}), ("고른자재", {"GF000176": {"첫": "MT000001"}}), - ("입력연결", {"줄": [{"로직": "GF000176", "입력": "관경", "자리": "어딘가"}]}), ("도면", {}), ): with pytest.raises(ValueError): @@ -303,7 +290,7 @@ def test_옛_복사본은_읽을_때_채움(world): root = world["root"] _save(root, [_pipe()]) folder = root / "templates/structures" / FOLDER - for name in ("입력연결.json", "입력값.json", "고른자재.json", "단가.json"): + for name in ("입력값.json", "고른자재.json", "단가.json"): (folder / name).unlink() manifest = _read(folder / "manifest.json") for key in ("원본판", "가져온날"): @@ -437,9 +424,6 @@ def test_재적용은_시트_고침을_덮고_알림_입력값은_살림(world): def test_개인에_저장하면_개인_층에_항목(world): root = world["root"] _save(root, [_pipe()]) - rows = [{"로직": "GF000176", "입력": "관경", "자리": "선택"}] - current = copies.read_file(root, FOLDER, "입력연결") - copies.write_file(root, FOLDER, "입력연결", current["판"], {"판": 3, "줄": rows}) made = copies.save_as(root, FOLDER, 3, 12, ["관공", "우리 현장"], "파형강관 매설") assert made["summary_item"] == { "layer": "personal", @@ -454,7 +438,7 @@ def test_개인에_저장하면_개인_층에_항목(world): copy = root / "templates/structures" / FOLDER drawing = _read(folder / "structure" / f"{made['열']}.json") assert drawing["도면"] == _read(copy / "도면.json")["도면"] - assert _read(folder / "inputs" / f"{made['열']}.json") == {"판": 1, "줄": rows} + assert not (folder / "inputs").exists() # 선택 입력 연결은 안 옮김 assert not list(folder.rglob("단가.json")) # 단가 묶음은 안 가져감 # 개인 층 항목을 다른 구조물에 지정하면 그 내용으로 복사본이 섬 assert _save_all(root, [_wall("b1", made["summary_item"])], [])["만듦"] == ["b1"] diff --git a/resources/tester/test_item_picker_detail.py b/resources/tester/test_item_picker_detail.py index a5b0bf6fa..a034a6b7e 100644 --- a/resources/tester/test_item_picker_detail.py +++ b/resources/tester/test_item_picker_detail.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""항목 고르기 모달 상세(계획서 25-2d) — 복사본 창구 · 선택 입력 카드 셈 · 한 벌 지킴. +"""항목 고르기 모달 상세(계획서 25-2d) — 복사본 창구 · 넣을 값 셈 · 한 벌 지킴. ① 프로젝트 복사본 창구(`ui_template_item_picker_project.ts`)를 Node 로 돌려(sub6 길 모양의 가짜 서버 fetch 대역) 본다: 목록 `{항목}` · 가져오기 POST · 읽기 `{종류, 판, 내용}` · 고침 PUT `{판, 내용}`(판 · 409 · 403 까닭 그대로) · - 새로 가져오기 · 계산 한 줄 · 입력 목록 한 번만 받음 · 카드 셈(범위 · 고르기 원래 모양 · 자리 「선택」). + 새로 가져오기 · 계산 한 줄 · 값 셈(범위 · 고르기 원래 모양). ② 모달이 카드 · 편집기를 M02 와 같은 한 벌로 쓰는지, 원본 층에 쓰는 길을 안 부르는지 소스로 지킨다. """ @@ -103,21 +103,11 @@ def test_회사_없음_409_는_서버_까닭_그대로(out): assert out[key].startswith("회사부터 등록"), key -def test_입력_목록은_한_번만_받고_로직마다_갈라_둠(out): - assert out["inputsHits"] == 1 - assert out["inputsLogics"] == [["GA1", ["지역", "소량감률"]], ["GA2", ["지역"]]] - - -def test_선택_입력_카드_셈(out): +def test_넣을_값_셈(out): model = out["model"] assert model["range"] == [{"min": 0, "max": 50}, {"min": 1, "max": 2}, None] assert [c["raw"] for c in model["choices"]] == [15, "16", 18] # 저장은 원래 모양 - assert model["groups"] == [ - ["GA1", ["지역"]], - ["GA2", ["지역"]], - ] # 같은 이름도 로직마다 · 「선택」 만 - assert model["noLink"] == 0 # 연결이 없으면 아무것도 안 보임 - assert model["pills"][0]["키"] == "입력|GA1|소량감률" # 25-1b 알약 키 + assert model["key"] == "GA1|지역" # 같은 이름도 로직마다 따로 def test_모달은_M02_와_같은_카드_한_벌을_씀(): @@ -136,15 +126,15 @@ def test_모달은_M02_와_같은_카드_한_벌을_씀(): assert drawing_card.count("mountDrawingTemplate(") == 1 -def test_선택_입력_카드는_sub2_파일_한_벌(out): +def test_넣을_값_카드는_sub2_파일_한_벌(out): """두 벌 금지 — 모달의 값 받기 카드는 `M02_Labor_Values.ts`(39-3 · 일위대가 입력 모달과 같은 - 값 칸) · 옛 자리 `M02_MasterTemplete_Inputs.ts` 가 다시 내보냄.""" - assert not (M02 / "M02_MasterTemplete_InputsCard.ts").exists() - inputs = (M02 / "M02_MasterTemplete_Inputs.ts").read_text(encoding="utf-8") + 값 칸) · 옛 선택 입력(자리 고르기)은 없앰(39-7b).""" + for gone in ("M02_MasterTemplete_InputsCard.ts", "M02_MasterTemplete_Inputs.ts", + "M02_MasterTemplete_Inputs_Model.ts", "M02_MasterTemplete_Inputs.py"): # fmt: skip + assert not (M02 / gone).exists(), gone values = (M02 / "M02_Labor_Values.ts").read_text(encoding="utf-8") - assert "export function createInputsCard(" in inputs # M02 자리 고르기(link) assert "export function createInputsValueCard(" in values # 모달 값 받기(view · value) - assert "createInputsValueCard," in inputs and '} from "./M02_Labor_Values";' in inputs + assert "seatsOf" not in values and "pickedGroups" not in values for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker_edit.ts"): text = (UI / name).read_text(encoding="utf-8") # 모달은 값 카드를 바로 받음(39-1c) assert "M02_Labor_Values" in text and 'InputsCard"' not in text, name @@ -174,8 +164,8 @@ def test_일위대가_요약_복사본_원본_넣을_값_전부(out): assert out["laborNone"] is None # 일위대가 없는 열(층 calc 404) values = out["laborValues"] assert values["keys"] == ["GA1", "GA2"] - assert values["link"] == ["GA1|지역|선택", "GA1|소량감률|선택", "GA2|지역|선택"] - assert values["shown"] == 2 and values["values"] == {"GA1": {}} + assert values["logics"] == [["GA1", ["지역", "소량감률"]], ["GA2", ["지역"]]] + assert values["hasLink"] is False and values["values"] == {"GA1": {}} def test_모달_카드_차례_도면_일위대가_면적(): @@ -218,8 +208,6 @@ def test_소스가_700줄을_넘지_않음(): *sorted(UI.glob("ui_template_item_picker*.ts")), M02 / "M02_MasterTemplete_ItemCards.ts", M02 / "M02_MasterTemplete_ItemSource.ts", - M02 / "M02_MasterTemplete_Inputs.ts", - M02 / "M02_MasterTemplete_Inputs_Model.ts", M02 / "M02_Labor_Values.ts", M02 / "M02_Labor_Values_Model.ts", ): diff --git a/resources/tester/test_m02_card_sheet_edit.py b/resources/tester/test_m02_card_sheet_edit.py index 8c450a75d..5f74cbd3b 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 @@ -95,7 +93,8 @@ def test_면적산출_알약은_일위대가_pills_길(): """39-4 D7 — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 · 선택 입력 알약 빠짐.""" struct = _text(M02 / "M02_MasterTemplete_Structure.ts") pills = struct.split("export async function pillsFor")[1].split("async function cardLinks")[0] - area = pills.split('if (kind === "material")')[1].split("\n }\n", 1)[1] + area = pills # 재료산출 갈래는 없앰 — 몸통이 면적산출 알약 + assert 'kind === "material"' not in pills assert "await fetchLaborPills(column)" in area assert "고정: true" in area and "...(p.자재 ? { 자재: p.자재 } : {})" in area assert "inputPills" not in area and "fetchComboInputs" not in struct diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 4972c8711..0a8a59709 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -44,8 +44,8 @@ def test_네_카드가_틀을_씀_수정_단추_한_모양(): assert 'variant: "filled"' not in cards # [수정] 은 모두 빈 단추(ghost) assert "c.saver.button.hidden = c.help.hidden = !can;" in cards # [저장] · ? 늘 보임 assert "DRAWING_KEYS" in cards and "SHEET_KEYS" in cards - inputs = _text("M02_MasterTemplete_Inputs.ts") + _text("M02_Labor_Values.ts") - assert inputs.count("cardShell({") == 2 # 선택 입력 · 값 카드(39-3 에 `M02_Labor_Values.ts` 로) + inputs = _text("M02_Labor_Values.ts") + assert inputs.count("cardShell({") == 1 # 넣을 값 카드(선택 입력 카드는 없앰 · 39-7b) assert "shell.fail(" in inputs and "shell.loading()" in inputs assert "m02-struct__card ui-collapsible" not in inputs # 손으로 만든 겉 틀 없음 drawing = _text("M02_MasterTemplete_DrawingCard.ts") diff --git a/resources/tester/test_m02_combo_branch.py b/resources/tester/test_m02_combo_branch.py index e430fae03..2dbb38789 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,13 +190,13 @@ 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 not saved.get("일위대가") # 열 키는 안 받음 — 일위대가는 labor 문서로 복사(39-1b) 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() assert numbers["c9"]["도번"] != numbers["rv"]["도번"] # 새 번호 assert numbers["c9"]["면적번"][2:] == numbers["c9"]["도번"][2:] - for kind in ("area", "material"): + for kind in ("area",): assert client.get(f"/api/m02/templates/{kind}/c9").json()["문서"]["열"] == "c9" # 그 뒤로는 열마다 따로 — 원본을 고쳐도 복사본은 그대로 drawing = client.get("/api/m02/templates/structure/rv").json() diff --git a/resources/tester/test_m02_combo_layers.py b/resources/tester/test_m02_combo_layers.py index f9bdf8e14..0053c5cf9 100644 --- a/resources/tester/test_m02_combo_layers.py +++ b/resources/tester/test_m02_combo_layers.py @@ -227,19 +227,7 @@ def test_면적_알약이_개인_조합을_읽음(world): got = me.get(f"{URL.format('personal')}/{up}/inputs").json() assert [p["키"] for p in got["물량"]] == ["돌쌓기 찰쌓기|㎡"] assert me.get(f"/api/m02/combos/key/{up}/inputs").json() == got - tree = me.post( - "/api/m02/layers/personal/tree", - json={"op": "add-item", "경로": [], "이름": "찰쌓기 옹벽", "단위": "m", "판": _판(me)}, - ).json() - column = tree["알림"]["열"] - me.post( - f"/api/m02/layers/personal/structures/{column}/use-combo", - json={"종류": "일위대가", "키": up}, - ) - key = next(i["key"] for i in tree["items"] if i["열"] == column) - pills = me.get(f"/api/m02/layers/personal/items/{key}/pills", params={"place": "면적"}) - assert pills.status_code == 200, pills.text - assert [p["키"] for p in pills.json()["물량"]] == ["돌쌓기 찰쌓기|㎡"] + # 항목 알약 길(`/items/{key}/pills`)은 선택 입력과 같이 없앰(39-7b) def test_담을_로직_읽기는_로그인이면_됨(world): diff --git a/resources/tester/test_m02_empty_master.py b/resources/tester/test_m02_empty_master.py index 3b28042ca..ea468cf74 100644 --- a/resources/tester/test_m02_empty_master.py +++ b/resources/tester/test_m02_empty_master.py @@ -80,9 +80,8 @@ def test_지운_뒤_항목_0_틀은_남음(empty): combos = client.get("/api/m02/structure-combos").json() assert combos["줄"] == [] and combos["다음"] == 52 assert len(combos["지운번호"]) == 4 # 견본에 세 파일이 있던 넷(rv · c4 · pp_len · ms) - for kind in ("structure", "area", "material", "inputs"): + for kind in ("structure", "area"): assert not list((system / kind).glob("*.json")), kind - assert (system / "자재조합.json").is_file() def test_빈_마스터에서_첫_화면_나무_새_항목_지우기(empty): diff --git a/resources/tester/test_m02_inputs.py b/resources/tester/test_m02_inputs.py deleted file mode 100644 index a6b875491..000000000 --- a/resources/tester/test_m02_inputs.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -"""M02 「선택 입력」 카드(PLAN 25-2c) 셈 회귀검증 — 입력 키 · 알약 키 · 자리 고름 · 줄 만들기 · 알약 목록. - -같은 이름의 입력도 로직마다 따로(키 `{로직}|{이름}`) · 처음은 모두 「안 씀」(저장 줄 없음) · -면적 · 재료로 고른 입력만 그 시트 알약(`입력|{로직}|{이름}` · 「로직 이름 · 입력 이름」). -Node 헬퍼(`helper_m02_inputs.cjs`)가 TS 를 트랜스파일해 돌리고, 여기서 값을 판정한다. -""" - -import json -import os -import subprocess - -import pytest - -HERE = os.path.dirname(os.path.abspath(__file__)) -ROOT = os.path.join(HERE, "..", "..") - - -@pytest.fixture(scope="module") -def results(): - proc = subprocess.run( - ["node", os.path.join(HERE, "helper_m02_inputs.cjs")], - capture_output=True, - text=True, - encoding="utf-8", - timeout=60, - ) - assert proc.returncode == 0, proc.stderr - return json.loads(proc.stdout) - - -def test_키_모양(results): - assert results["keys"] == ["GC1|높이", "입력|GC2|높이"] - - -def test_자리는_고른_것만_안_쓰면_없음(results): - assert results["seatKeys"] == ["GC1|높이", "GC2|높이", "GC2|배합", "GC9|없는것"] - assert results["none"] == [] # 처음은 모두 안 씀 — 저장할 줄 없음 - - -def test_로직_수_입력_수(results): - assert results["count"] == {"logics": 2, "inputs": 4} - - -def test_같은_이름_입력도_로직마다_따로_알약(results): - # 높이가 두 로직에 있어도 알약 둘 · 이름은 「로직 이름 · 입력 이름」 · 목록에서 못 뺌(고정) - assert results["area"] == [ - ["입력|GC1|높이", "돌쌓기 · 높이", "m", True], - ["입력|GC2|높이", "타설 · 높이", None, True], - ] - assert results["material"] == [["입력|GC2|배합", "타설 · 배합"]] - - -def test_저장_줄은_고른_것만_알약_키_함께(results): - assert results["lines"] == [ - {"로직": "GC1", "입력": "높이", "자리": "면적", "알약": "입력|GC1|높이"}, - {"로직": "GC1", "입력": "종류", "자리": "선택"}, # 선택은 알약 없음 - {"로직": "GC2", "입력": "높이", "자리": "면적", "알약": "입력|GC2|높이"}, - {"로직": "GC2", "입력": "배합", "자리": "재료", "알약": "입력|GC2|배합"}, - ] - - -def test_고르기_범위_자동값_글(results): - assert results["hints"] == [ - ["0.5 ~ 5", "1.2"], - ["막돌 / 견치돌", ""], - ["0 ~ 3", ""], - ["", ""], - ] - assert results["seatOptions"] == ["안 씀", "면적산출 알약", "재료산출 알약", "선택"] - - -def test_카드는_집계표_카드_옆_넷째_그리고_700줄_안(): - page = os.path.join(ROOT, "M02_MasterTemplete") - src = open(os.path.join(page, "M02_MasterTemplete_Structure.ts"), encoding="utf-8").read() - assert "inputs.root" in src and "createInputsCard" in src - for name in os.listdir(page): - if name.endswith((".ts", ".py")): - n = len(open(os.path.join(page, name), encoding="utf-8").read().splitlines()) - assert n <= 700, (name, n) diff --git a/resources/tester/test_m02_item_inputs.py b/resources/tester/test_m02_item_inputs.py deleted file mode 100644 index 9e71112fe..000000000 --- a/resources/tester/test_m02_item_inputs.py +++ /dev/null @@ -1,240 +0,0 @@ -"""M02 항목 선택 입력(PLAN 25-1b · 25-2a) — 입력 목록 · 항목 연결 파일 · 알약 · 권한. - -① 입력 목록은 시험 안 마스터 견본(`fixtures/m02_master_sample` 사본 · PLAN 26-5a)으로 — - 일위대가 있는 항목 셋에서 로직 수 · 입력 수 · 자동값이 M01 조합 미리 보기가 쓰는 것(로직 줄 - `입력` · `master_auto`)과 같은지 · 로직마다 갈라 합치지 않는지 · 일위대가 없는 항목은 빈 목록. -② 연결 파일 쓰기는 임시 storage(회사 · 개인 층) + 견본 시스템 집계표로만 — 판 409 · 없는 키 422 · - 사라짐 표시 · 알약 · 층 권한 · 열 지우기 · 항목 복사 · 분류 더하기 복사가 파일을 같이 다룸. -""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -import pytest -from fastapi import FastAPI -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 M01_MasterData import M01_MasterData_Store_Combo as combo_store -from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store -from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router -from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layers_router -from M02_MasterTemplete import M02_MasterTemplete_Store as store -from M02_MasterTemplete import M02_Template_Layers as layers -from resources.tester.helper_m02_master_sample import copy_master_sample -from resources.tester.test_m02_structure import write_sample_table - -TABLE = "table/구조물집계표" -UA = "UA000004" # 견본 항목 셋(rv · c4 · c1)이 쓰는 조합 — 담은 로직 여럿 -OTHER_UA = "UA000007" - - -def _app(session: dict[str, Any]) -> TestClient: - app = FastAPI() - app.include_router(layers_router.router) - app.include_router(items_router.router) - app.dependency_overrides[verify_session] = lambda: session - return TestClient(app) - - -def _m01_view(ua: str) -> list[tuple[str, list[str], dict]]: - """M01 조합 미리 보기가 그리는 것 — 담은 로직마다 (키, 입력 이름들, 자동값).""" - row = next(r for r in m01.read(combo_store.FILE)[0]["줄"] if r["키"] == ua) - whole = m01.loaded()[1] - out = [] - for one in row["담은로직"]: - try: - logic = whole.logic(one["로직"]) - except combo_store.cm.mf.FormulaError: - continue # 화면도 못 읽은 로직은 건너뜀 - auto = inputs_store._num(m01.auto(one["로직"])["값"]) - out.append((one["로직"], [s["이름"] for s in logic.get("입력") or []], auto)) - return out - - -# ── ① 입력 목록 — 마스터 견본 사본 ───────────────────── - - -def test_입력_목록은_M01_조합_미리_보기와_같음(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(store, "FOLDER", copy_master_sample(tmp_path / "master")) - client = _app({"user_id": 1, "company_id": 1, "role": "USER", "is_master": False}) - rows = [r for r in json.loads((store.FOLDER / store.COMBO_FILE).read_bytes())["줄"]] - with_ua = [r for r in rows if r.get("일위대가")] - picked, seen = [], set() - for r in with_ua: # 서로 다른 조합 셋 - if r["일위대가"] not in seen: - picked.append(r) - seen.add(r["일위대가"]) - if len(picked) == 3: - break - assert len(picked) == 3 - for r in picked: - res = client.get(f"/api/m02/layers/system/items/{r['키']}/inputs") - assert res.status_code == 200, res.text # 일반 사용자 읽기 - got = res.json() - want = _m01_view(r["일위대가"]) - assert got["일위대가"] == r["일위대가"] and got["이름"] - assert [lg["로직"] for lg in got["로직"]] == [k for k, _, _ in want] - for logic, (_, names, auto) in zip(got["로직"], want): - assert [i["이름"] for i in logic["입력"]] == names # 로직마다 · 합치지 않음 - assert {i["이름"]: i["자동값"] for i in logic["입력"]} == auto - assert all({"이름", "설명", "단위", "자동값"} <= set(i) for i in logic["입력"]) - # 이름이 같은 입력이 두 로직에 있어도 두 번 나옴 - names = [ - i["이름"] - for r in with_ua - for lg in client.get(f"/api/m02/layers/system/items/{r['키']}/inputs").json()["로직"] - for i in lg["입력"] - ] - assert len(names) > len(set(names)) - no_ua = next(r for r in rows if not r.get("일위대가")) - empty = client.get(f"/api/m02/layers/system/items/{no_ua['키']}/inputs").json() - assert empty == {"일위대가": None, "이름": None, "로직": [], "빠진로직": []} - assert client.get("/api/m02/layers/system/items/SS999999/inputs").status_code == 404 - - -# ── ② 연결 파일 — 임시 storage ───────────────────────── - - -@pytest.fixture() -def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: - system = tmp_path / "system" - system.mkdir() - write_sample_table(system) - monkeypatch.setattr(store, "FOLDER", system) - monkeypatch.setattr(layers, "SYSTEM_ROOT", system) - store.renumber() - storage = tmp_path / "storage" - storage.mkdir() - monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) - session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False} - return {"client": _app(session), "session": session, "storage": storage} - - -def _as(world: dict, role: str = "USER", user: int = 42) -> TestClient: - world["session"].update(role=role, user_id=user) - return world["client"] - - -def _put_table(client: TestClient, layer: str, edit) -> None: - url = f"/api/m02/layers/{layer}/templates/{TABLE}" - got = client.get(url).json() - edit(got["문서"]["열"]) - res = client.put(url, json={"판": got["판"], "문서": got["문서"]}) - assert res.status_code == 200, res.text - - -def _col(cid: str, ua: str | None = UA) -> dict[str, Any]: - return { - "id": cid, - "머리": ["sub4 돌쌓기", cid, None, None], - "단위": "㎡", - "꼴": "수", - "일위대가": ua, - } - - -def _key(client: TestClient, layer: str, cid: str) -> str: - items = client.get(f"/api/m02/items?layer={layer}").json() - return next(r["key"] for r in items if r["열"] == cid) - - -def test_연결_저장_다시_읽기_판_422_사라짐_알약(world): - client = _as(world) - _put_table(client, "personal", lambda cols: cols.append(_col("sub4_a"))) - key = _key(client, "personal", "sub4_a") - base = f"/api/m02/layers/personal/items/{key}" - listing = client.get(f"{base}/inputs").json() - first, second = listing["로직"][0], listing["로직"][1] - assert client.get(f"{base}/input-links").json() == {"판": None, "줄": []} - rows = [ - {"로직": first["로직"], "입력": first["입력"][0]["이름"], "자리": "면적"}, - {"로직": second["로직"], "입력": second["입력"][0]["이름"], "자리": "재료"}, - {"로직": first["로직"], "입력": first["입력"][1]["이름"], "자리": "선택"}, - ] - saved = client.put(f"{base}/input-links", json={"판": None, "줄": rows}) - assert saved.status_code == 200, saved.text - assert saved.json()["판"] == 1 - again = client.get(f"{base}/input-links").json() - assert again == saved.json() - assert again["줄"][0]["알약"] == f"입력|{first['로직']}|{first['입력'][0]['이름']}" - assert "알약" not in again["줄"][2] # 선택은 시트 알약이 아님 - file = world["storage"] / "7" / "42" / "templates" / "inputs" / "sub4_a.json" - assert json.loads(file.read_text("utf-8")) == saved.json() - # 판 다르면 409 · 없는 키 · 모르는 자리 · 두 번은 422 - assert client.put(f"{base}/input-links", json={"판": None, "줄": rows}).status_code == 409 - for bad in ( - [{"로직": "GF999999", "입력": "높이", "자리": "면적"}], - [{"로직": first["로직"], "입력": "없는입력", "자리": "면적"}], - [{"로직": first["로직"], "입력": first["입력"][0]["이름"], "자리": "옆"}], - [rows[0], rows[0]], - ): - res = client.put(f"{base}/input-links", json={"판": 1, "줄": bad}) - assert res.status_code == 422, res.text - # 알약 — 면적 = 조합 물량 + 면적으로 이은 입력 · 재료 = 재료로 이은 입력만 - area = client.get(f"{base}/pills?place=면적").json() - assert area["물량"] and [p["키"] for p in area["입력"]] == [again["줄"][0]["알약"]] - mat = client.get(f"{base}/pills?place=재료").json() - assert mat["물량"] == [] and [p["입력"] for p in mat["입력"]] == [second["입력"][0]["이름"]] - assert " · " in mat["입력"][0]["이름"] # 입력 · 로직 이름(합치지 않음) - # 일위대가가 바뀌면 옛 로직 연결은 읽을 때 사라짐 · 알약에서 빠짐 - _put_table(client, "personal", lambda cols: cols[-1].update(일위대가=OTHER_UA)) - gone = client.get(f"{base}/input-links").json()["줄"] - assert all(r.get("사라짐") for r in gone) - assert client.get(f"{base}/pills?place=면적").json()["입력"] == [] - - -def test_권한_일반_사용자_읽기_쓰기(world): - admin = _as(world, "SYSTEM_ADMIN") - _put_table(admin, "system", lambda cols: cols.append(_col("sub4_sys"))) - key = _key(admin, "system", "sub4_sys") - base = f"/api/m02/layers/system/items/{key}" - user = _as(world) - assert user.get(f"{base}/inputs").status_code == 200 - assert user.get(f"{base}/input-links").status_code == 200 - assert user.put(f"{base}/input-links", json={"판": None, "줄": []}).status_code == 403 - assert _as(world, "SYSTEM_ADMIN").put(f"{base}/input-links", json={"줄": []}).status_code == 200 - # 회사 층 — 같은 회사 일반 사용자 읽기 · 쓰기 403 · 회사 관리자 쓰기 - _put_table(_as(world, "ADMIN"), "company", lambda cols: cols.append(_col("sub4_c"))) - ckey = _key(_as(world), "company", "sub4_c") - cbase = f"/api/m02/layers/company/items/{ckey}" - assert _as(world).get(f"{cbase}/inputs").status_code == 200 - assert _as(world).put(f"{cbase}/input-links", json={"줄": []}).status_code == 403 - assert _as(world, "ADMIN").put(f"{cbase}/input-links", json={"줄": []}).status_code == 200 - # 남의 개인 층은 못 읽음 - assert _as(world).get(f"{cbase}/inputs?owner=9").status_code == 403 - - -def test_열_지우기_항목_복사_분류_복사가_연결을_같이(world): - admin = _as(world, "ADMIN") - _put_table(admin, "company", lambda cols: cols.append(_col("sub4_c"))) - key = _key(admin, "company", "sub4_c") - base = f"/api/m02/layers/company/items/{key}" - logic = admin.get(f"{base}/inputs").json()["로직"][0] - rows = [{"로직": logic["로직"], "입력": logic["입력"][0]["이름"], "자리": "면적"}] - assert admin.put(f"{base}/input-links", json={"판": None, "줄": rows}).status_code == 200 - # 항목 개인 복사 — 새 열에 연결이 판 1 로 - user = _as(world) - made = user.post( - "/api/m02/layers/personal/items/copy", json={"from_layer": "company", "key": key} - ).json() - mine = user.get(f"/api/m02/layers/personal/items/{made['key']}/input-links").json() - assert mine["판"] == 1 and [r["입력"] for r in mine["줄"]] == [rows[0]["입력"]] - # 분류 더하기(본뜬열) — 새 열이 원본 열 연결을 받음 - admin = _as(world, "ADMIN") - twin = {**_col("sub4_c2", None), "본뜬열": "sub4_c"} - _put_table(admin, "company", lambda cols: cols.append(twin)) - folder = world["storage"] / "7" / "templates" - with store.at(folder): - assert inputs_store.read_file("sub4_c2")["줄"] == inputs_store.read_file("sub4_c")["줄"] - # 열 지우기 — 연결 파일도 같이 지움 - _put_table( - admin, "company", lambda cols: cols.remove(next(c for c in cols if c["id"] == "sub4_c")) - ) - assert not (folder / "inputs" / "sub4_c.json").is_file() - assert (folder / "inputs" / "sub4_c2.json").is_file() diff --git a/resources/tester/test_m02_item_keys.py b/resources/tester/test_m02_item_keys.py index 7ac0949b3..c157388ad 100644 --- a/resources/tester/test_m02_item_keys.py +++ b/resources/tester/test_m02_item_keys.py @@ -109,8 +109,7 @@ def test_옛_SS_키는_새_모양으로_옮기고_옛_키로도_찾음(world): migrated = json.loads((folder / store.COMBO_FILE).read_bytes()) assert [r["옛키"] for r in migrated["줄"]] == [["SS000001"], ["SS000003"]] assert migrated["다음"] == 5 - # 옛 키로도 찾음 — 입력 목록 · 나무 고침 · 복사본 원본 찾기(M02_Item_Source) - assert client.get("/api/m02/layers/personal/items/SS000003/inputs").status_code == 200 + # 옛 키로도 찾음 — 나무 고침 · 복사본 원본 찾기(M02_Item_Source) tree_ = Tree(client, "personal") tree_.do(op="rename", 키="SS000001", 이름="a2") assert tree_.item(["a2"])["key"] == "SP42-000001" @@ -164,7 +163,7 @@ def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world): doc = json.loads(path.read_bytes()) path.write_text(json.dumps({**doc, key: f"{head}{code}"}), encoding="utf-8") old = {cid: _numbers(system, cid) for cid in store._column_ids()} - assert old["rv"] == {"SD01010100", "SU01010100", "SA01010100", "SM01010100"} + assert old["rv"] == {"SD01010100", "SU01010100", "SA01010100"} first = max(int(n[3:]) for nums in old.values() for n in nums if NEW.match(n)) + 1 with store.at(system): got = store.read("table", store.STRUCTURE_TABLE) @@ -191,7 +190,7 @@ def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world): got["문서"]["열"].append({"id": "y", "머리": ["y"], "꼴": "수"}) store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"]) last = f"-{first + 120:06d}" # 지운 x119 번호(first + 119) 다음 - assert _numbers(system, "y") == {f"SD{last}", f"SU{last}", f"SA{last}", f"SM{last}"} + assert _numbers(system, "y") == {f"SD{last}", f"SU{last}", f"SA{last}"} def test_층마다_번호는_따로_파일은_열_id_로(world): diff --git a/resources/tester/test_m02_item_labor.py b/resources/tester/test_m02_item_labor.py index 80321bdcc..ca3f2b4cb 100644 --- a/resources/tester/test_m02_item_labor.py +++ b/resources/tester/test_m02_item_labor.py @@ -65,9 +65,9 @@ def _first_input(code: str) -> tuple[str, Any]: 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(SAMPLE / name, system / name) - for kind in ("table", "structure", "area", "material"): + for kind in ("table", "structure", "area"): shutil.copytree(SAMPLE / kind, system / kind) monkeypatch.setattr(layers, "SYSTEM_ROOT", system) monkeypatch.setattr(m01, "FOLDER", master_copy) diff --git a/resources/tester/test_m02_labor_values.py b/resources/tester/test_m02_labor_values.py index 1757129cb..66c7a77cf 100644 --- a/resources/tester/test_m02_labor_values.py +++ b/resources/tester/test_m02_labor_values.py @@ -2,7 +2,7 @@ 못박는 것 - 입력 목록 = 키 차례 · 겹친 키 한 번 · 입력마다 자동값 · 없는 키는 「빠진로직」. - - 옛 선택 입력 길(`M02_MasterTemplete_Inputs.inputs`)과 몸 한 벌(`M02_Labor_Inputs`). + - 입력 목록 몸 한 벌(`M02_Labor_Inputs`) · 옛 선택 입력은 없앰(39-7b). - 값 셈 = 칸 글 → 저장 값(빈 글 = 자동값 · 범위 = 수 · 고르기 = 원래 모양) · 범위 밖 · 모르는 이름 버림(사라진 값 알림) · 줄 아래 작은 글 · 빈 값 수(자동값도 없는 입력만). 화면 TS 는 그 자리에서 트랜스파일해 node 로 돌림 · 정본은 읽기만. @@ -44,7 +44,7 @@ def test_입력_목록은_키_차례_겹침_한_번_자동값_빠진_로직(clie one = got["로직"][1] assert one["이름"] and one["결과단위"] assert one["입력"] and all("자동값" in i and "이름" in i for i in one["입력"]) - # 옛 선택 입력 목록과 같은 몸 + # 입력 목록 몸 한 벌(M02_Labor_Inputs) assert got["로직"] == labor_inputs.logic_inputs([B, A])["로직"] diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index ce9c04253..1f95ee5fe 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -207,7 +207,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"): @@ -245,7 +245,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 9c954b3c0..1bd3308f5 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" @@ -84,7 +74,7 @@ def test_시스템_층은_그대로_항목_목록은_SS_줄(world): "labor", *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 @@ -108,7 +98,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) @@ -140,7 +130,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"] @@ -152,20 +143,14 @@ 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, - "자재조합": "회사 자재", "도번": "돌기슭막이 찰쌓기 회사형", "면적번": "돌기슭막이 찰쌓기 회사형", - "재료번": "돌기슭막이 찰쌓기 회사형", } user = _as(world) @@ -182,13 +167,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.py b/resources/tester/test_m02_structure.py index af31c66f7..403c78937 100644 --- a/resources/tester/test_m02_structure.py +++ b/resources/tester/test_m02_structure.py @@ -1,7 +1,7 @@ """M02 구조물 도면(structure) — 새로 · 도번 SD+8자리 · 409 · 저장 · 지운 뒤 번호 · 번호 매김(새 항목 = 자리 뜻 없는 일련번호 · PLAN 26-1) · 집계표 저장 때 만들기/지우기 · 복사 가져오기 · 프로젝트 복사. -면적산출(area) · 재료산출(material)은 따로 파일 — +면적산출(area)은 따로 파일(재료산출은 없앰) — `resources/tester/spreadsheet/test_spreadsheet_store.py`.""" import json @@ -69,7 +69,6 @@ def _num(code: str) -> dict: "도번": f"SD{code}", "일위대가번": f"SU{code}", "면적번": f"SA{code}", - "재료번": f"SM{code}", } @@ -87,10 +86,10 @@ def test_새로_도번_자동_409_없는_열(client): assert doc["도면"]["format"] == 6 assert doc["도면"]["entities"] == [] assert len(doc["도면"]["layers"]) == 1 - # 면적산출 · 재료산출은 구조물 문서 밖 — 같은 열 id 로 따로 한 통합문서씩 + # 면적산출은 구조물 문서 밖 — 같은 열 id 로 따로 한 통합문서 · 재료산출은 안 만듦 assert "산출근거" not in doc - for kind in ("area", "material"): - assert client.get(f"/api/m02/templates/{kind}/pp_len").json()["문서"]["종류"] == "통합문서" + assert client.get("/api/m02/templates/area/pp_len").json()["문서"]["종류"] == "통합문서" + assert client.get("/api/m02/templates/material/pp_len").status_code == 404 assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD-000002" assert client.post("/api/m02/structures/pp_len").status_code == 409 @@ -132,7 +131,7 @@ def test_지워도_번호를_당기지_않음_짝은_같은_번호(client): client.post(f"/api/m02/structures/{column}") assert client.delete("/api/m02/templates/structure/rv").status_code == 200 numbers = client.get("/api/m02/structures/numbers").json() - assert numbers["rv"] == {**_num("-000002"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠 + assert numbers["rv"] == {**_num("-000002"), "도번": ""} # 면적은 남아 번호를 쥠 assert numbers["ms"] == _num("-000003") # 다른 열 번호는 그대로 # 새 열 = 다음 일련번호(자리 뜻 없음) · 다시 만든 도면은 짝(면적산출) 번호를 이어 받음 assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD-000004" @@ -186,7 +185,7 @@ def test_집계표_저장_때_새_열은_만들고_지운_열은_같이_지움(c assert saved.status_code == 200 numbers = client.get("/api/m02/structures/numbers").json() assert "pv_a" not in numbers and "note2" not in numbers - for kind_ in ("structure", "area", "material"): + for kind_ in ("structure", "area"): assert client.get(f"/api/m02/templates/{kind_}/pv_a").status_code == 404 # 새 열 = 열 차례로 다음 일련번호 · 지운 pv_a(5) 는 비워 둠(다른 번호를 안 당김) assert numbers["pv_x"] == _num("-000014") diff --git a/resources/tester/test_m02_structure_combo.py b/resources/tester/test_m02_structure_combo.py index 68022a738..87246122b 100644 --- a/resources/tester/test_m02_structure_combo.py +++ b/resources/tester/test_m02_structure_combo.py @@ -84,11 +84,9 @@ def test_집계표_저장_때_새_열_지운_열_고친_열을_맞춤_키_유지 assert (rv["분류1"], rv["분류3"]) == (old_rv["분류1"], old_rv["분류3"]) assert (rv["단위"], rv["일위대가"]) == ("m", "UA000004") assert (rv["바인딩"], rv["펼침"]) == ({"종류": "revetment"}, True) - # 번호는 세 파일과 같음 — 새 열도 [저장] 한 번에 SD · SA · SM 이 섬 + # 번호는 파일과 같음 — 새 열도 [저장] 한 번에 SD · SA 가 섬 numbers = client.get("/api/m02/structures/numbers").json() - assert {k: rows["new1"][k] for k in ("도번", "일위대가번", "면적번", "재료번")} == numbers[ - "new1" - ] + assert {k: rows["new1"][k] for k in ("도번", "일위대가번", "면적번")} == numbers["new1"] assert rows["new1"]["도번"].startswith("SD") # 또 저장해도(바뀐 것 없음) 키 · 다음 그대로 diff --git a/resources/tester/test_m02_structure_options.py b/resources/tester/test_m02_structure_options.py index c8f27428d..943b45132 100644 --- a/resources/tester/test_m02_structure_options.py +++ b/resources/tester/test_m02_structure_options.py @@ -125,9 +125,10 @@ def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에 assert ( order.index("summary") < order.index("extras.옵션, extras.도면") < order.index("practice") ) - # 자재 조합을 열면 이 줄 · 실무문서 · 「전체 항목」 고름 지움 / 이 줄을 열면 자재 조합 · … 지움 - assert "onOpen: () => (options.clear(), practice?.clear(), (allPicked = null), paint())" in side - assert "onOpen: () => (matset.clear(), practice?.clear(), (allPicked = null), paint())" in side + # 실무문서를 열면 이 줄 · 「전체 항목」 고름 지움 / 이 줄을 열면 실무문서 · … 지움(자재 조합 없앰) + assert "onOpen: () => (options.clear(), (allPicked = null), paint())" in side + assert "onOpen: () => (practice?.clear(), (allPicked = null), paint())" in side + assert "matset" not 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 1df781508..99585c6a5 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -51,8 +51,8 @@ def test_소스가_700줄을_넘지_않음(): ), # 산출근거 저장은 컨테이너가 안 함 — [산출근거 수정] 이 여는 메인 칸 전체 편집이 함 ("M02_MasterTemplete_Api_Fetch.ts", ["saveTemplate"], MAIN), - # 좌측 구조물 도면 · 면적산출 · 재료산출 줄 번호는 한 번에 받음 - # (`{열 id: {도번, 면적번, 재료번}}`) + # 좌측 구조물 도면 · 면적산출 줄 번호는 한 번에 받음 + # (`{열 id: {도번, 면적번}}`) ("M02_MasterTemplete_Api_Fetch.ts", ["fetchStructureNumbers"], SIDE), ("M02_MasterTemplete_Structure.ts", ["structureLabel", "isStructureColumn"], SIDE), # 도면 카드(31-4)는 카드 파일에서 뗀 `DrawingCard` 가 부름 @@ -72,15 +72,16 @@ def test_컨테이너가_쓰는_이름이_내보내진_것(source: str, names: l def test_sub2_가_부르는_이름과_모양(): """브레인 확정 이름 · 모양 — sub2 가 이대로 부름(바꾸면 브레인에게).""" body = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts") - for name in ("copyStructure", "copyArea", "copyMaterial"): + for name in ("copyStructure", "copyArea"): assert re.search( rf"export const {name} = async \(column: string, from: string\): Promise", body ), name assert "fetchStructureNumbers = (): Promise>" in body assert re.search( - r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*재료번: string;", body + r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*/\*\* 일위대가[^*]*\*/\s*일위대가번: string;\s*\}", + body, ) - assert re.search(r'export type Kind = [^;]*"area" \| "material"', body) + assert re.search(r'export type Kind = [^;]*"area" \| "labor";', body) # 재료산출 없앰 def test_집계표만_컨테이너를_붙임(): @@ -154,7 +155,7 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우 assert "createSelectField" not in side # 새로 = 종류 고르기 없이 이름만 assert "cur?.종류 !== kind" in side # 본떠 · 지우기 = 그 컨테이너에서 고른 것이 있을 때만 assert "i.이름 === STRUCTURE_TABLE" in side # 표는 구조물집계표 한 줄만 - # 구조물 도면 · 일위대가 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘) · + # 구조물 도면 · 일위대가 · 면적산출 = 구조물 집계 양식 컨테이너 안(같이 접힘) · 자재 조합 없앰 · # 층 단추 · 나무와 하위 사이 수평선(39-2) summary = side.split("const summary = section(")[1].split(");")[0] assert ( @@ -162,7 +163,7 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우 < summary.index("m02-side__rule") < summary.index("...columnSections") ) - assert "matsetSection" in summary + assert "matset" not in side assert "const columnSections = COLUMN_KINDS.map(" in side assert 'M02_KindTable: ["구조물 조합"' in _text( # 화면 이름(26-7a) ROOT / "ui_template" / "ui_template_locale_m2.ts" @@ -177,8 +178,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_첫_화면과_닫은_뒤는_구조물집계표(): @@ -200,9 +201,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`(일위대가 화면 해시 맞춤은 39-2 에서 지움) assert "() => side.paint()," in _text(M02 / "M02_MasterTemplete_UI_Page.ts") @@ -225,7 +226,7 @@ def test_표_도구_막대에_줄_더하기_줄_지우기_손_입력_열_없음( def test_상세_화면_돌아가기는_집계표로_그_열_고른_채(): main = _text(MAIN) assert "children: [back, title, slot, reload, save, " in main # M01 꼴 — 제목 앞(39-5) - assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 면적산출 · 재료산출만 + assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 일위대가 · 면적산출만 body = main.split('back.addEventListener("click"')[1] assert "confirmLeave()" in body and "STRUCTURE_TABLE" in body assert "sheet?.selectColumn(column)" in body diff --git a/resources/tester/test_m02_tree.py b/resources/tester/test_m02_tree.py index 90bce3ee5..af43e062d 100644 --- a/resources/tester/test_m02_tree.py +++ b/resources/tester/test_m02_tree.py @@ -182,13 +182,11 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor t.do(op="move", 경로=["A", "b"], 새경로=["돌쌓기"]) assert t.paths() == [["돌쌓기", "메쌓기"], ["돌쌓기", "소단"], ["돌쌓기", "b", "c", "d", "e"]] t.do(422, op="move", 경로=["돌쌓기"], 새경로=["돌쌓기", "b"]) - # 지우기 — 항목의 세 파일 · 입력 연결 파일도 같이 · 마지막 항목까지 + # 지우기 — 항목의 파일 · 마지막 항목까지 gone = t.item(["돌쌓기", "소단"]) - (root / "inputs").mkdir(exist_ok=True) - (root / "inputs" / f"{gone['열']}.json").write_text('{"판": 1, "줄": []}', encoding="utf-8") note = t.do(op="delete", 키=gone["key"])["알림"] assert note["지운항목"] == [gone["열"]] - for part in ("structure", "area", "material", "inputs"): + for part in ("structure", "area"): assert not (root / part / f"{gone['열']}.json").is_file() t.do(op="add-item", 경로=["돌쌓기"], 이름="새것") # 지운 번호는 다시 안 줌 assert _numbers(root, t.item(["돌쌓기", "새것"])["열"]) == {"-000004"} diff --git a/resources/tester/test_m02_tree_paste.py b/resources/tester/test_m02_tree_paste.py index 46ee0bb58..56a3d2a72 100644 --- a/resources/tester/test_m02_tree_paste.py +++ b/resources/tester/test_m02_tree_paste.py @@ -50,7 +50,7 @@ def test_회사_항목_폴더를_개인에_붙임_새_키_파일_이름2(world): table = json.loads((root / "table/구조물집계표.json").read_bytes()) made = next(c for c in table["열"] if c.get("경로") == ["옹벽"]) assert made["본뜬"] == {"layer": "company", "owner": "7", "key": wall} - for kind in ("structure", "area", "material"): + for kind in ("structure", "area"): assert (root / kind / f"{made['id']}.json").is_file() # 다시 붙이면 같은 이름 「(2)」 · 붙일 폴더 안으로 again = client.post(PASTE.format("personal"), json={**body, "경로": []}).json() diff --git a/resources/tester/test_sheet_drawing_select.py b/resources/tester/test_sheet_drawing_select.py index 957e4915f..2909841c4 100644 --- a/resources/tester/test_sheet_drawing_select.py +++ b/resources/tester/test_sheet_drawing_select.py @@ -1,6 +1,6 @@ """표 도면 줄 · 칸 고르기 신호 — `ui_template_sheet_ops.ts` 를 Node 로 바로 돌려 봄. -마스터 줄 = 일위대가 · 도면 `s:drawing` · 면적산출 `s:area` · 재료산출 `s:material`(식 · 들어갈 것 줄 없음) · +마스터 줄 = 일위대가 · 도면 `s:drawing` · 면적산출 `s:area`(식 · 들어갈 것 줄 없음) · master 만 · 단위 줄 바로 아래 차례(키보드) · project 에는 없음. `selectionOf` = 격자가 `onSelect` 로 넘기는 모양({row, col, colId} · 모르는 열은 col -1 · colId null). """ @@ -26,7 +26,7 @@ console.log(JSON.stringify({ master: navRows(doc, "master"), project: navRows(doc, "project"), pick: [selectionOf(doc, "s:drawing", "c2"), selectionOf(doc, "d:r1", "c1"), selectionOf(doc, "s:unit", "zz")], - blank: ["s:price", "s:drawing", "s:area", "s:material", "s:unit", "d:r1"].map((k) => [isBlankPick(k, { id: "t", 머리: [], 꼴: "글" }), isBlankPick(k, { id: "n", 머리: [], 꼴: "수" })]), + blank: ["s:price", "s:drawing", "s:area", "s:unit", "d:r1"].map((k) => [isBlankPick(k, { id: "t", 머리: [], 꼴: "글" }), isBlankPick(k, { id: "n", 머리: [], 꼴: "수" })]), })); """ @@ -53,7 +53,7 @@ def _run() -> dict: def test_drawing_row_under_price_master_only(): got = _run() assert got["master"] == [ - "s:unit", "s:price", "s:drawing", "s:area", "s:material", "d:r1", "t:t1", + "s:unit", "s:price", "s:drawing", "s:area", "d:r1", "t:t1", ] # fmt: skip assert got["project"] == ["s:unit", "d:r1", "t:t1"] @@ -68,5 +68,5 @@ def test_selection_shape(): def test_text_column_pick_rows_blank(): assert _run()["blank"] == [ - [True, False], [True, False], [True, False], [True, False], [False, False], [False, False], + [True, False], [True, False], [True, False], [False, False], [False, False], ] # fmt: skip diff --git a/resources/tester/test_structures_summary_snapshot.py b/resources/tester/test_structures_summary_snapshot.py index 261206bc7..188d46d24 100644 --- a/resources/tester/test_structures_summary_snapshot.py +++ b/resources/tester/test_structures_summary_snapshot.py @@ -34,9 +34,9 @@ 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"): + for kind in ("table", "structure", "area"): shutil.copytree(REAL / kind, target / kind) @@ -131,7 +131,12 @@ def test_지정하면_폴더가_생기고_내용이_원본과_같음(world): assert result == {"만듦": ["pipe@40.0", "st0"], "지움": [], "못만듦": [], "옮겨받음": []} folder = _items(root) / "st0" got = {path.name for path in folder.iterdir()} - assert {"집계.json", "도면.json", "면적.json", "재료.json", "manifest.json"} <= got + assert { + "집계.json", + "도면.json", + "면적.json", + "manifest.json", + } <= got and "재료.json" not in got total = json.loads((folder / "집계.json").read_text(encoding="utf-8")) assert total["줄"]["키"] == "SS000022" and total["열"]["id"] == "ms" drawing = json.loads((world["system"] / "structure/ms.json").read_text(encoding="utf-8")) diff --git a/resources/tester/ui/item-picker-data.test.ts b/resources/tester/ui/item-picker-data.test.ts index 37f0c5db6..256912023 100644 --- a/resources/tester/ui/item-picker-data.test.ts +++ b/resources/tester/ui/item-picker-data.test.ts @@ -41,10 +41,8 @@ const row = ( 분류3: 분류[2], 단위: "m", 일위대가: "UA000004", - 자재조합: "UM000001", 도번: "SD01010100", 면적번: "SA01010100", - 재료번: "SM01010100", layer, owner: layer === "company" ? "샘플회사" : layer === "personal" ? "샘플회사/사용자" : null, ...extra, @@ -54,10 +52,8 @@ const rows: PickerRow[] = [ row("SS000001", "rv", "돌기슭막이", ["메쌓기", "1.2m", "기초유"], "system", { names: { 일위대가: "돌기슭막이 메쌓기 일위대가", - 자재조합: null, 도번: "돌기슭막이 도면", 면적번: "돌기슭막이 면적산출", - 재료번: "돌기슭막이 재료산출", }, }), row("SS000002", "rv2", "돌기슭막이", ["메쌓기", "1.5m", "기초유"], "system"), @@ -221,16 +217,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", "unitPrice"], ); assert.deepEqual(lines[0], { kind: "drawing", name: "돌기슭막이 도면", key: "SD01010100" }); -assert.deepEqual(lines[3], { kind: "combo", name: null, key: "UM000001" }); // names 에 없음 +assert.deepEqual(lines[2], { + 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" })[2], { kind: "unitPrice", name: null, key: "", diff --git a/ui_template/sheet/ui_template_sheet.ts b/ui_template/sheet/ui_template_sheet.ts index 34435f640..35372ff8a 100644 --- a/ui_template/sheet/ui_template_sheet.ts +++ b/ui_template/sheet/ui_template_sheet.ts @@ -1,12 +1,12 @@ /* ============================================================================= * ui_template_sheet.ts - * 엑셀처럼 도는 표 부품 — `createSheet(칸, 문서, {mode, onChange, drawingLabel, areaLabel, materialLabel, onSelect, onPick})` + * 엑셀처럼 도는 표 부품 — `createSheet(칸, 문서, {mode, onChange, drawingLabel, areaLabel, onSelect, onPick})` * → `{getDoc, setDoc, recalc, selectColumn, destroy}`. * * master = 시스템 관리자 양식 고치기 — 머리 · 층 이름 · 단위 · 일위대가 · * 층마다 열 더하기(「분류1 더하기」 … 층 이름 + 더하기) · 열 지우기. - * 도면 · 면적산출 · 재료산출 줄 = `drawingLabel` · `areaLabel` · `materialLabel` 글 · - * 칸을 고르면 `onSelect` · `onPick` 을 주면 일위대가 · 도면 · 면적산출 · 재료산출 칸은 + * 도면 · 면적산출 줄 = `drawingLabel` · `areaLabel` 글 · + * 칸을 고르면 `onSelect` · `onPick` 을 주면 일위대가 · 도면 · 면적산출 칸은 * 치지 않고 조회 후 선택. * `readOnly` = 볼 수만 있음 — 단추 줄 · 안내 글 숨김 · 칸 고치기 · 머리 고치기 · 열 너비 끌기 막음 · * 열이 하나도 없으면 [열 더하기] 가 켜져 첫 열을 만듦(머리 글 바로 고치기). @@ -56,12 +56,11 @@ export interface SheetOptions { onChange?: (doc: SheetDoc) => void; /** 도면 줄 칸 글(마스터 첫 쪽 · 열 id) — 없거나 빈 글이면 「없음」 · 글이 바뀌면 `recalc()` 로 다시 그림. */ drawingLabel?: (colId: string) => string; - /** 면적산출 · 재료산출 줄 칸 글(번호) — `drawingLabel` 과 같음. */ + /** 면적산출 줄 칸 글(번호) — `drawingLabel` 과 같음. */ areaLabel?: (colId: string) => string; - materialLabel?: (colId: string) => string; /** 칸을 고름 — 누르기 · 방향키 · Tab. */ onSelect?: (sel: SheetSelection) => void; - /** 조회 후 선택 — master 일위대가 · 도면 · 면적산출 · 재료산출 칸을 두 번 누르기 · Enter · F2 로 부름. + /** 조회 후 선택 — master 일위대가 · 도면 · 면적산출 칸을 두 번 누르기 · Enter · F2 로 부름. * 글을 돌려주면 일위대가 칸에 넣음 · 번호 글 줄은 부른 쪽이 가져온 뒤 `recalc()`. * 없으면 일위대가는 글로 고침 · 번호 글 줄은 읽기만. */ onPick?: (sel: SheetSelection) => Promise | void; @@ -90,7 +89,6 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio sel: null, drawingLabel: opts.drawingLabel, areaLabel: opts.areaLabel, - materialLabel: opts.materialLabel, pick: !!opts.onPick, headLocked: !!opts.lockHead, combo: !!opts.combo, diff --git a/ui_template/sheet/ui_template_sheet_ops.ts b/ui_template/sheet/ui_template_sheet_ops.ts index 0347aa999..a3d807da6 100644 --- a/ui_template/sheet/ui_template_sheet_ops.ts +++ b/ui_template/sheet/ui_template_sheet_ops.ts @@ -22,16 +22,15 @@ export const KEY_UNIT = "s:unit"; export const KEY_PRICE = "s:price"; export const KEY_DRAWING = "s:drawing"; export const KEY_AREA = "s:area"; -export const KEY_MATERIAL = "s:material"; -/** 마스터 전용 줄(첫 쪽 단위 줄 아래 차례) — 도면 · 면적산출 · 재료산출 줄은 손으로 못 침(번호 글). */ -export const MASTER_ROWS = [KEY_PRICE, KEY_DRAWING, KEY_AREA, KEY_MATERIAL] as const; +/** 마스터 전용 줄(첫 쪽 단위 줄 아래 차례) — 도면 · 면적산출 줄은 손으로 못 침(번호 글). */ +export const MASTER_ROWS = [KEY_PRICE, KEY_DRAWING, KEY_AREA] as const; export type MasterRowKey = (typeof MASTER_ROWS)[number]; /** 번호 글 줄 — 부른 쪽이 준 글(`refLabel`)만 보임. */ -export const REF_ROWS: readonly string[] = [KEY_DRAWING, KEY_AREA, KEY_MATERIAL]; +export const REF_ROWS: readonly string[] = [KEY_DRAWING, KEY_AREA]; /** 조회 후 선택 줄 — `onPick` 을 주면 이 줄 칸은 치지 않고 고름. */ export const PICK_ROWS: readonly string[] = MASTER_ROWS; -/** 글 열(NO · 측점 · 비고 같은 꼴=글)의 일위대가 · 도면 · 면적산출 · 재료산출 칸 — 빈칸 · 잠김. */ +/** 글 열(NO · 측점 · 비고 같은 꼴=글)의 일위대가 · 도면 · 면적산출 칸 — 빈칸 · 잠김. */ export const isBlankPick = (key: string, col: SheetColumn): boolean => PICK_ROWS.includes(key) && col.꼴 === "글"; @@ -120,15 +119,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 +138,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_render.ts b/ui_template/sheet/ui_template_sheet_render.ts index f42b007a1..3b5113938 100644 --- a/ui_template/sheet/ui_template_sheet_render.ts +++ b/ui_template/sheet/ui_template_sheet_render.ts @@ -3,8 +3,8 @@ * 표 그리기 — 문서 + 풀이 결과 → 쪽마다 ``(머리 층 · 단위 줄 · 본문 · 합계 줄). * * 칸마다 `data-r`(줄 열쇠) · `data-c`(열 id) — 고치기 · 키보드는 격자(`ui_template_sheet.ts`)가 위임으로 받음. - * 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|price|drawing|area|material`. - * master = 본문 위에 열마다 일위대가 · 도면 · 면적산출 · 재료산출 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽). + * 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|price|drawing|area`. + * master = 본문 위에 열마다 일위대가 · 도면 · 면적산출 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽). * 머리 층 줄 머리 칸은 `data-layer`(층 차례) — master 에서 층 이름을 고침. * ========================================================================== */ @@ -17,7 +17,6 @@ import { isBlankPick, isCalcColumn, KEY_AREA, - KEY_MATERIAL, KEY_UNIT, MASTER_ROWS, type MasterRowKey, @@ -39,10 +38,9 @@ export interface RenderState { mode: SheetMode; result: SheetResult; sel: { r: string; c: string } | null; - /** 도면 · 면적산출 · 재료산출 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */ + /** 도면 · 면적산출 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */ drawingLabel?: (colId: string) => string; areaLabel?: (colId: string) => string; - materialLabel?: (colId: string) => string; /** 조회 후 선택 줄(`PICK_ROWS`)을 고름으로 받나 — 칸 겉모양만. */ pick?: boolean; /** 머리 · 층 이름을 못 고침(`lockHead`) — 칸 겉모양만. */ @@ -53,13 +51,9 @@ export interface RenderState { headEditable?: boolean; } -/** 도면 · 면적산출 · 재료산출 줄 칸 글. */ +/** 도면 · 면적산출 줄 칸 글. */ export const refText = (state: RenderState, key: string, colId: string): string => - (key === KEY_AREA - ? state.areaLabel - : key === KEY_MATERIAL - ? state.materialLabel - : state.drawingLabel)?.(colId)?.trim() || st("Drawing_None"); + (key === KEY_AREA ? state.areaLabel : state.drawingLabel)?.(colId)?.trim() || st("Drawing_None"); /** 열 폭 — 저장 폭(없으면 기본) · 머리 글이 안 잘리는 최소 폭보다 좁지 않게. */ const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map): number => @@ -68,7 +62,7 @@ const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map) mins.get(col.id) ?? 0, ); -/** 구조물 조합 — 일위대가 · 도면 · 면적산출 · 재료산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘 +/** 구조물 조합 — 일위대가 · 도면 · 면적산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘 * (`.ui-sheet__meta td` 글꼴과 같게 잼). */ function fitMeta(state: RenderState, cols: SheetColumn[], mins: Map): void { const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; @@ -115,7 +109,7 @@ export function renderSheet(state: RenderState): HTMLElement { const mins = headMinWidths(cols, depth, font); if (state.combo) fitMeta(state, cols, mins); const widthDoc = state.combo ? { ...doc, 보기: { ...doc.보기, 열너비: {} } } : doc; - // 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 재료산출 · 합계 이름이 안 잘리게 + // 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 합계 이름이 안 잘리게 const GUTTER = Math.min( GUTTER_MAX, Math.max( @@ -126,7 +120,6 @@ export function renderSheet(state: RenderState): HTMLElement { st("Row_Unit_Price"), st("Row_Drawing"), st("Row_Area"), - st("Row_Material"), ...(doc.합계줄 ?? []).map((t) => t.이름), ].map((label) => Math.ceil(textWidth(label, font)) + 16), ), @@ -193,7 +186,6 @@ export function renderSheet(state: RenderState): HTMLElement { "s:price": "Row_Unit_Price", "s:drawing": "Row_Drawing", "s:area": "Row_Area", - "s:material": "Row_Material", } as const )[key]; const tr = el("tr", { className: "ui-sheet__meta", children: [gutter("th", st(label))] }); diff --git a/ui_template/sheet/ui_template_sheet_text.ts b/ui_template/sheet/ui_template_sheet_text.ts index 164722376..3ed27f142 100644 --- a/ui_template/sheet/ui_template_sheet_text.ts +++ b/ui_template/sheet/ui_template_sheet_text.ts @@ -13,7 +13,6 @@ const TEXT = { Row_Unit_Price: ["일위대가", "Unit price"], Row_Drawing: ["도면", "Drawing"], Row_Area: ["면적산출", "Area"], - Row_Material: ["재료산출", "Material"], Add_Suffix: [" 추가", " +"], New_Prefix: ["새 ", "New "], Drawing_None: ["없음", "None"], diff --git a/ui_template/sheet/ui_template_sheet_types.ts b/ui_template/sheet/ui_template_sheet_types.ts index b89a9d725..ae5ca98b8 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; /** 마스터 한 칸 → 프로젝트에서 설계 값마다 열. */ @@ -82,7 +80,7 @@ export interface SheetDoc { 보기?: SheetView; } -/** 고른 칸 — `row` = 줄 열쇠(`d:<줄id>` · `t:<합계id>` · `s:unit|price|drawing|area|material`) · +/** 고른 칸 — `row` = 줄 열쇠(`d:<줄id>` · `t:<합계id>` · `s:unit|price|drawing|area`) · * `col` = 열 차례(0부터 · 없으면 -1) · `colId` = 열 id(없으면 null). */ export interface SheetSelection { row: string; diff --git a/ui_template/ui_template_item_picker_data.ts b/ui_template/ui_template_item_picker_data.ts index 771957a4f..598364652 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,10 +28,8 @@ export interface PickerRow { 분류3?: string; 단위?: string; 일위대가?: string; - 자재조합?: string; 도번?: string; 면적번?: string; - 재료번?: string; /** 폴더 이름 배열(깊이 자유 · 26-1) — 없으면 종류 · 분류1~3 중 있는 것으로 이음 */ 경로?: string[] | null; /** 위 키 칸의 이름(21-9 서버가 더함) — 없으면 키를 작은 글로 보임 */ @@ -102,13 +100,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 +166,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" | "unitPrice"; export interface FolderLine { kind: FolderKind; @@ -187,12 +179,10 @@ export interface FolderLine { 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 681fdc461..6aea8414c 100644 --- a/ui_template/ui_template_item_picker_detail.ts +++ b/ui_template/ui_template_item_picker_detail.ts @@ -1,6 +1,6 @@ /* ============================================================================= * ui_template_item_picker_detail.ts - * 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b · 39-1c) — 카드 = 도면 · 일위대가 · 면적산출(· 재료산출) · + * 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b · 39-1c) — 카드 = 도면 · 일위대가 · 면적산출 · * 넣을 값. 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `M02_Labor_Values`) + 일위대가 카드 * (`ui_template_item_picker_labor`)를 늦게 받아 씀(B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`). * 넣을 값 = 일위대가 담은 로직 입력 전부(복사본이면 복사본 입력값). @@ -41,7 +41,7 @@ import { type ProjectItems, } from "./ui_template_item_picker_project"; -/** 편집 화면을 여는 데 필요한 것 — 「선택 입력」 도 한 종류 */ +/** 편집 화면을 여는 데 필요한 것 — 「넣을 값」 도 한 종류 */ export interface EditRequest { kind: EditKind | "inputs"; row: PickerRow; @@ -85,10 +85,9 @@ const TEXT = { changed: "마스터가 바뀜 — 복사본은 가져온 때 그대로", gone: "원본이 없어짐 — 복사본은 그대로 씀", reapply: "재적용", - reapplyTip: - "원본을 다시 가져옴 — 이 구조물 복사본에서 고친 것(도면 · 시트 · 선택 입력 자리)이 원본 것으로 덮임", + reapplyTip: "원본을 다시 가져옴 — 이 구조물 복사본에서 고친 것(도면 · 시트)이 원본 것으로 덮임", reapplyAsk: - "원본을 다시 가져옴 — 이 구조물 복사본에서 고친 도면 · 면적산출 · 재료산출 · 선택 입력 자리가 원본 것으로 덮임(넣은 값은 같은 키면 남음). 재적용할까요?", + "원본을 다시 가져옴 — 이 구조물 복사본에서 고친 도면 · 면적산출이 원본 것으로 덮임(넣은 값은 같은 키면 남음). 재적용할까요?", reapplied: "재적용했음 — 덮인 것: {value}", reappliedNone: "재적용했음 — 덮인 것 없음(원본과 같았음)", reapplyFailed: "재적용하지 못함 — {value}", @@ -109,14 +108,10 @@ const TEXT = { calc: "복사본 계산 — 계 {total}원 · 빠진 입력 {gaps}개", calcStuck: " · 멈춘 로직 {stuck}개", noCopy: "복사본에 없음", - combo: "자재조합", price: "일위대가", parts: { 도면: "도면", 면적: "면적산출", - 재료: "재료산출", - 자재조합: "자재조합", - 입력연결: "선택 입력 자리", 일위대가: "일위대가", } as Record, }; @@ -154,9 +149,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", @@ -167,9 +160,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}`)), ); }; @@ -373,8 +364,8 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { await ensureCards(); if (mine !== seq || !cards || !inputs || !labor) return; const label = !info && canCopy ? TEXT.importEdit : null; - cards.relabel({ structure: label, area: label, material: label }); - inputs.setEditLabel(label ?? "선택 입력 수정"); + cards.relabel({ structure: label, area: label }); + inputs.setEditLabel(label ?? "넣을 값 수정"); cards.root.hidden = false; paintStatus(row, mine); void cards.load({ id: row.열, label: itemName(row) }); diff --git a/ui_template/ui_template_item_picker_edit.ts b/ui_template/ui_template_item_picker_edit.ts index 0140c9b1f..892c5a84e 100644 --- a/ui_template/ui_template_item_picker_edit.ts +++ b/ui_template/ui_template_item_picker_edit.ts @@ -1,7 +1,7 @@ /* ============================================================================= * ui_template_item_picker_edit.ts * 항목 고르기 모달의 **편집 화면**(계획서 25-2d) — 카드 [수정] 이 모달 안에서 이 화면으로 바뀜. - * 도면 = M02 CAD 부품 · 면적산출 · 재료산출 = M02 스프레드시트 · 선택 입력 = 값 받기 카드. + * 도면 = M02 CAD 부품 · 면적산출 = M02 스프레드시트 · 넣을 값 = 값 받기 카드. * 고침은 **프로젝트 복사본**에만 씀(`ProjectItems.write`) — 마스터 · 회사 · 개인 원본은 안 바뀜. * [뒤로] = 선택지(나무 + 카드)로 · 저장 안 한 고침이 있으면 버릴지 물음. * 편집 부품은 이 화면을 열 때만 `import()`(B05 · B06 번들에 안 섞임). @@ -12,11 +12,11 @@ import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spre import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links"; import type { DrawingTemplateHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Drawing"; import type { InputsValueHandle } from "../M02_MasterTemplete/M02_Labor_Values"; -import { pillsOf, type ValueDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs_Model"; -import type { InputLinks, StructureDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch"; +import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model"; +import type { LaborPill, StructureDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch"; import type { EditRequest } from "./ui_template_item_picker_detail"; import { itemName } from "./ui_template_item_picker_data"; -import { laborValueData, loadInputs, PART_OF } from "./ui_template_item_picker_project"; +import { laborValueData, PART_OF } from "./ui_template_item_picker_project"; export interface EditView { root: HTMLElement; @@ -36,12 +36,10 @@ const TEXT = { loadFailed: "복사본을 못 읽음 — {value}", discard: "저장 안 한 고침이 있음 — 버리고 돌아갈까요?", outOfRange: "범위 밖 값이 있어 저장하지 못함", - noAreaPill: - "면적산출 알약 없음 — 선택 입력에서 자리를 면적산출로 이으면 여기 뜸(마스터 · 회사 · 개인 층에서)", + noAreaPill: "면적산출 알약 없음 — 이 복사본 일위대가에 담은 로직 · 자재가 없음", drawing: "도면", area: "면적산출", - material: "재료산출", - inputs: "선택 입력", + inputs: "넣을 값", suffix: "{value} 수정", }; @@ -61,7 +59,6 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi const word = { structure: TEXT.drawing, area: TEXT.area, - material: TEXT.material, inputs: TEXT.inputs, }[kind]; const back = createButton({ label: TEXT.back, variant: "ghost", size: "sm" }); @@ -96,28 +93,24 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi body.replaceChildren(el("p", { className: "ui-item-picker__hint", text })); }; - /** 복사본의 항목 연결 — 자리가 면적 · 재료인 입력이 그 시트 알약(25-1b · 키 모양은 `pillsOf`) */ - const linkOf = async (): Promise => - (await items.read(info.폴더, "입력연결")) as InputLinks | null; - - async function sheetLinks(kindOf: "area" | "material"): Promise { - const [list, link, { searchMaterials }] = await Promise.all([ - loadInputs(row), - linkOf(), - import("../M01_MasterData/M01_MasterData_Api_Fetch"), - ]); - const 항목 = pillsOf(list, link, kindOf === "area" ? "면적" : "재료"); - if (kindOf === "area") { - if (!항목.length) note.textContent = TEXT.noAreaPill; - return { 항목, 모드: "입력" }; - } - return { 항목, 모드: "자재", 자재찾기: searchMaterials }; + /** 복사본 일위대가의 알약(가져온 때 그대로) — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 */ + async function sheetLinks(): Promise { + const labor = (await items.read(info.폴더, "일위대가")) as { 알약?: LaborPill[] } | null; + const 항목 = (labor?.알약 ?? []).map((p) => ({ + 키: p.키, + 이름: p.이름, + 단위: p.단위, + ...(p.자재 ? { 자재: p.자재 } : {}), + 고정: true, + })); + if (!항목.length) note.textContent = TEXT.noAreaPill; + return { 항목, 모드: "입력" }; } - async function mountSheet(kindOf: "area" | "material"): Promise { + async function mountSheet(): Promise { const [{ createSpreadsheet }, links] = await Promise.all([ import("../A00_Common/spreadsheet/spreadsheet"), - sheetLinks(kindOf), + sheetLinks(), ]); const host = el("div", { className: "ui-item-picker__edit-sheet" }); body.replaceChildren(host); @@ -171,7 +164,7 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi } if (kind === "structure") await mountDrawing(); else if (kind === "inputs") await mountInputs(); - else await mountSheet(kind); + else await mountSheet(); } catch (error) { fail(fill(TEXT.loadFailed, why(error))); } diff --git a/ui_template/ui_template_item_picker_project.ts b/ui_template/ui_template_item_picker_project.ts index ef9a68c3e..a719abd88 100644 --- a/ui_template/ui_template_item_picker_project.ts +++ b/ui_template/ui_template_item_picker_project.ts @@ -10,29 +10,22 @@ * POST …/{폴더}/reapply 재적용 → {폴더, 가져온날, 원본판, 덮임: [부분]} * POST …/{폴더}/save-as {to, 경로, 이름} 개인에 저장 → {key, 열, 이름, summary_item} * POST …/{폴더}/calc 복사본만으로 계산(계 · 빠진 입력 · 기준합 · 반영합) - * 입력 목록 · 항목 연결(sub4 · 25-2a) — `M02_MasterTemplete_Api_Fetch` 의 `fetchItemInputs` · `readInputLinks` * 일위대가(39-1c) — 복사본 `일위대가` 부분 · 원본 층은 `calcLabor` · 넣을 값 목록은 `fetchLogicInputs` * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; -import { - fetchItemInputs, - type InputLinks, - type ItemInputs, -} from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch"; +import type { ItemInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch"; import { calcLabor, fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api"; import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model"; 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> = { +export const PART_OF: Record<"structure" | "area", ProjectPart> = { structure: "도면", area: "면적", - material: "재료", }; export interface ProjectItemInfo { @@ -187,7 +180,6 @@ export function projectItems(projectId: string): ProjectItems { const reply = await call(`${base}/${enc(info.폴더)}/reapply`, post({})); if (reply.status >= 300) throw new Error(reasonOf(reply)); forget(info.폴더); - inputsCache.clear(); // 원본이 바뀌었을 수 있어 입력 목록도 다시 받음 const got = reply.body as { 가져온날?: string | null; 원본판?: string | null; @@ -243,20 +235,6 @@ export function projectItems(projectId: string): ProjectItems { return client; } -/* ── 입력 목록(원본 층 기준) — 같은 항목은 한 번만 받음 ───────────────────────── */ -const inputsCache = new Map>(); - -/** 원본 항목의 입력 목록(로직마다 갈라 · 합치지 않음) — 못 받으면 다음에 다시 */ -export function loadInputs(row: PickerRow): Promise { - const id = `${layerOf(row)}|${ownerOf(row) ?? ""}|${row.key}`; - const kept = inputsCache.get(id); - if (kept) return kept; - const made = fetchItemInputs(layerOf(row), row.key); - inputsCache.set(id, made); - made.catch(() => inputsCache.delete(id)); - return made; -} - /* ── 일위대가(39-1c) — 카드 요약 · 넣을 값 자료 ─────────────────────────────── */ /** 일위대가 카드에 보이는 것 — 복사본이면 복사본 계산 · 아니면 원본 층 계산(저장 안 함) */ @@ -308,15 +286,12 @@ export async function laborOf( }; } -/** 넣을 값 카드 자료 — 담은 로직 입력 전부(자리 「선택」 으로 감싸 값 카드가 다 보임) */ +/** 넣을 값 카드 자료 — 담은 로직 입력 전부 */ export async function laborValueData( logics: string[], values: ValueDoc, -): Promise<{ list: ItemInputs; link: InputLinks; values: ValueDoc }> { +): Promise<{ list: ItemInputs; values: ValueDoc }> { const got = await fetchLogicInputs(logics); const list: ItemInputs = { 일위대가: null, 로직: got.로직, 빠진로직: got.빠진로직 }; - const 줄 = list.로직.flatMap((logic) => - logic.입력.map((input) => ({ 로직: logic.로직, 입력: input.이름, 자리: "선택" as const })), - ); - return { list, link: { 판: null, 줄 }, values }; + return { list, values }; } diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index b5595c878..3eba5f311 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -13,7 +13,6 @@ export const ui_locales_m2 = { M02_KindDrawing: ["도면 양식", "Drawing templates"], M02_KindStructure: ["구조물 도면", "Structure drawings"], M02_KindArea: ["면적산출", "Area takeoff"], - M02_KindMaterial: ["재료산출", "Material takeoff"], M02_NoTemplates: ["양식 없음", "None"], M02_New: ["새로", "New"], M02_Copy: ["본떠 만들기", "Copy as new"], @@ -87,8 +86,6 @@ export const ui_locales_m2 = { M02_PickerLoadFailed: ["항목을 못 읽음 — {value}", "Failed to load items — {value}"], 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"],