From c704f18982243dd1cb0f337b48c6137238f46522 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 19:36:46 +0900 Subject: [PATCH] =?UTF-8?q?refactor(M02):=20=EC=84=A0=ED=83=9D=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=A7=80=EC=9B=80(39-7b=20G3)=20=E2=80=94=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=C2=B7=20=EC=9E=90=EB=A6=AC=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=C2=B7=20inputs/=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=C2=B7=20=EC=9E=85=EB=A0=A5=20=EB=AA=A9=EB=A1=9D=20=C2=B7=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=C2=B7=20=EC=95=8C=EC=95=BD=20=EA=B8=B8=20?= =?UTF-8?q?=C2=B7=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=EB=B3=B8=20=E3=80=8C=EC=9E=85=EB=A0=A5=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=E3=80=8D=20=C2=B7=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=B8=B5=20inputs=20=C2=B7=20=EC=8B=9C=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 넣을 값 카드(M02_Labor_Values)는 담은 로직 입력 전부를 보임(자리 「선택」 감싸기 없앰) · 이름 「넣을 값」 - 고르기 모달 면적산출 알약 = 복사본 일위대가 알약(가져온 때 그대로) - 입력값.json · 고른자재.json · /calc 는 그대로 · storage 층 inputs/ 파일은 안 건드림(읽기만 끊음) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK --- M02_MasterTemplete/M02_Item_Copy.py | 18 +- M02_MasterTemplete/M02_Item_Personal.py | 10 +- M02_MasterTemplete/M02_Item_Snapshot.py | 17 +- M02_MasterTemplete/M02_Item_Source.py | 17 - M02_MasterTemplete/M02_Labor_Inputs.py | 31 +- M02_MasterTemplete/M02_Labor_Values.css | 1 - M02_MasterTemplete/M02_Labor_Values.ts | 51 +-- M02_MasterTemplete/M02_Labor_Values_Model.ts | 5 +- .../M02_MasterTemplete_Api_Fetch.ts | 47 +-- .../M02_MasterTemplete_CardShell.ts | 2 +- .../M02_MasterTemplete_Inputs.css | 105 ------- .../M02_MasterTemplete_Inputs.py | 172 ----------- .../M02_MasterTemplete_Inputs.ts | 290 ------------------ .../M02_MasterTemplete_Inputs_Model.ts | 101 ------ .../M02_MasterTemplete_ItemCards.ts | 6 +- .../M02_MasterTemplete_Items.py | 5 +- .../M02_MasterTemplete_Router_Items.py | 50 --- .../M02_MasterTemplete_Store.py | 28 +- .../M02_MasterTemplete_Structure.ts | 16 - .../M02_MasterTemplete_UI_Main.ts | 2 - .../M02_Project_Items_Router.py | 4 +- resources/master_template/inputs/item1.json | 95 ------ .../tester/helper_item_picker_project.cjs | 43 +-- resources/tester/helper_m02_inputs.cjs | 61 ---- resources/tester/test_item_copy_complete.py | 24 +- resources/tester/test_item_picker_detail.py | 36 +-- resources/tester/test_m02_card_shell.py | 4 +- resources/tester/test_m02_combo_layers.py | 14 +- resources/tester/test_m02_empty_master.py | 2 +- resources/tester/test_m02_inputs.py | 80 ----- resources/tester/test_m02_item_inputs.py | 240 --------------- resources/tester/test_m02_item_keys.py | 3 +- resources/tester/test_m02_labor_values.py | 4 +- resources/tester/test_m02_tree.py | 6 +- ui_template/ui_template_item_picker_detail.ts | 10 +- ui_template/ui_template_item_picker_edit.ts | 28 +- .../ui_template_item_picker_project.ts | 33 +- 37 files changed, 112 insertions(+), 1549 deletions(-) delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Inputs.css delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Inputs.py delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Inputs.ts delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts delete mode 100644 resources/master_template/inputs/item1.json delete mode 100644 resources/tester/helper_m02_inputs.cjs delete mode 100644 resources/tester/test_m02_inputs.py delete mode 100644 resources/tester/test_m02_item_inputs.py diff --git a/M02_MasterTemplete/M02_Item_Copy.py b/M02_MasterTemplete/M02_Item_Copy.py index 3514c37be..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,14 +49,12 @@ from M02_MasterTemplete.M02_Item_Source import ( KINDS = { "도면": "도면.json", "면적": "면적.json", - "입력연결": LINK_FILE, "입력값": VALUES_FILE, "고른자재": PICKS_FILE, "일위대가": LABOR_FILE, # 읽기만 — 일위대가는 원본 층에서 고침(39-1c) } #: 뼈대 검사에 쓰는 층 양식 종류(도면 · 면적산출) SKELETON = {"도면": "structure", "면적": "area"} -SEATS = ("면적", "재료", "선택", None) _MT = re.compile(r"^MT\d{6}$") @@ -158,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("일위대가는 원본(마스터 · 회사 · 개인) 층에서 고침 — 프로젝트는 넣을 값만") @@ -176,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): @@ -246,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_Personal.py b/M02_MasterTemplete/M02_Item_Personal.py index 80775a8d2..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 키는 저장소가 -줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 입력연결을 담음 — +줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적을 담음 — 단가 묶음 · 입력값 · 고른자재는 프로젝트 몫이라 안 가져감. 쓰기는 지금 층 저장소 함수(`store.at` · -`store.write` · 항목 연결 `write_file`) — 나무 고침(`M02_MasterTemplete_Tree`)과 +`store.write`) — 나무 고침(`M02_MasterTemplete_Tree`)과 같은 집계표 저장 한 줄기(경로 · 머리 줄 수 맞춤 · 번호 · SS 장부)를 탐(PLAN 26-2a). 권한(본인 개인 층 · 회사 없음 409)은 부르는 쪽 몫. """ @@ -15,12 +15,11 @@ 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_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, read_json +from M02_MasterTemplete.M02_Item_Source import read_json #: 복사본 파일 → 층 양식 종류 PART_KINDS = (("도면.json", "structure"), ("면적.json", "area")) @@ -93,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 1a32321ed..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 aee15cf09..73fc41052 100644 --- a/M02_MasterTemplete/M02_Item_Source.py +++ b/M02_MasterTemplete/M02_Item_Source.py @@ -4,7 +4,6 @@ `집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄) + 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) · - `입력연결.json` 층의 `inputs/<열id>.json`(없으면 빈 연결) · `단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT) `원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎. 프로젝트가 고치는 값(`입력값.json` · `고른자재.json`)은 원본이 아니라 여기서 안 모음. @@ -24,7 +23,6 @@ TABLE_NAME = "구조물집계표" #: 열 id → 스냅샷 파일(구조물 도면 · 면적산출) PART_FILES = (("structure", "도면.json"), ("area", "면적.json")) LABOR_KIND = "labor" # 열 id → 일위대가 문서(39-1c) — 스냅샷은 `LABOR_FILE` 에 감싸서 -LINK_FILE = "입력연결.json" PRICE_FILE = "단가.json" LABOR_FILE = "일위대가.json" @@ -121,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 @@ -158,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 c5454c831..d1634da5c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -298,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 { @@ -332,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_CardShell.ts b/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts index 21fcf0f05..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_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 883f16b4c..5f80e7a33 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -1,12 +1,12 @@ /* ============================================================================= * 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`)가 그 자리. + * 덧붙는 카드는 `extraCards` 로 끼움 — 고르기 모달 넣을 값 카드(`M02_Labor_Values.ts`)가 그 자리. * `sheetEdit` 가 있으면 면적산출 카드를 미리보기에서 고침(31-3) — 스프레드시트 `preview`(메뉴 없이 * 수식 입력 줄 · 칸 고침 · 단축키) + 아래 알약 줄 + 카드 [저장] · 없으면 보기만(고르기 모달). * ========================================================================== */ @@ -104,7 +104,7 @@ export interface ItemCardsOptions { numberOf?: (kind: EditKind, column: string) => string | undefined; /** 못 읽었을 때 카드에 까닭을 보일까 — 기본 보임(M02 는 번호가 있는 열만 · 없는 열은 「없음」) */ warnOnFail?: (column: string) => boolean; - /** 세 카드 뒤에 덧붙는 카드(선택 입력 …) */ + /** 카드 뒤에 덧붙는 카드(넣을 값 …) */ extraCards?: HTMLElement[]; /** 카드 가운데 하나라도 저장 안 한 고침이 생김 · 없어짐(PLAN 31-4 · 31-7) */ onDirty?: (dirty: boolean) => void; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Items.py index 7619ac4ce..b3899ec42 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Items.py @@ -15,7 +15,6 @@ from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos -from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store from M02_MasterTemplete import M02_MasterTemplete_Store as store # 21-1 목록 칸 — SS 줄에서 그대로(키는 `key`) @@ -132,8 +131,7 @@ def copy_item( path: list[str] | None = None, ) -> dict[str, Any]: """다른 층 항목(SS `key`) → `target` 층 새 열 하나 — 머리 · 단위 · 바인딩 그대로 · - 도면 · 일위대가(새 SU · 조합 키는 안 받음 · 39-1b) · 면적산출 내용(알약 연결 포함) · - 항목 연결 복사 · + 도면 · 일위대가(새 SU · 조합 키는 안 받음 · 39-1b) · 면적산출 내용(알약 연결 포함) 복사 · 새 번호 · 새 SS 키 · 열 `본뜬` = `origin`(원래 층 · 주인 · 키) · `path` = 새 자리(폴더 + 이름 · 없으면 원래 경로 · 붙여넣기 PLAN 30-2). 돌려줌 `{key, 열, 이름}`.""" @@ -175,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_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 1c66d3f49..1c2801d1c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -27,7 +27,6 @@ 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_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree @@ -69,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, 경로, 이름?, 새경로?, 키?, 판}` (+ 항목 더하기 때 `단위?`).""" @@ -214,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) — 읽기 = 층 읽기 권한 · 고침 = 층 쓰기 권한 ── diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index 36dffd44e..5bfe9e227 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -104,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") @@ -185,7 +175,6 @@ 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() # 번호가 다 선 뒤 — 조합 표 도번 · 면적번 칸 @@ -390,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: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 5ccf1bb76..71a84493d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -46,7 +46,6 @@ import { } from "./M02_MasterTemplete_ItemCards"; import { layerSource } from "./M02_MasterTemplete_ItemSource"; import { pickHere, pickedName, type From } from "./M02_MasterTemplete_ItemPick"; -import { createInputsCard, type InputsCardHandle } from "./M02_MasterTemplete_Inputs"; import "./M02_MasterTemplete_Structure.css"; export type { EditKind, SheetKind } from "./M02_MasterTemplete_ItemCards"; @@ -311,8 +310,6 @@ export interface StructurePanelOptions { onEdit: (column: string, label: string, kind: EditKind) => void; /** 집계표에 저장 안 한 고침이 있으면 버릴지 물음 — 취소면 false */ confirmLeave: () => Promise; - /** 「선택 입력」 카드에서 자리를 골랐음 — 저장 안 한 고침 표시 */ - onDirty?: () => void; /** 카드 고침을 버리지 않고 머묾 — 표의 고른 열을 되돌림(31-3) */ keepColumn?: (colId: string) => void; } @@ -323,8 +320,6 @@ export interface StructurePanelHandle { numbers: () => ColumnNumbers; /** 번호 목록 다시 받기 */ reload: () => Promise; - /** 「선택 입력」 카드에서 고른 자리를 서버에 저장 — 집계표 [저장] 뒤에 부름(실패하면 던짐) */ - saveInputs: () => Promise; /** 카드에 저장 안 한 고침(도면 31-4 · 면적산출 31-3) — 페이지가 떠날 때 함께 물음 */ dirty: () => boolean; /** 고친 카드 저장(도면 · 엑셀) — 집계표 [저장] 이 카드를 다시 읽기 전에 부름(실패하면 던짐) */ @@ -345,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, @@ -357,7 +348,6 @@ 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) — 전체 편집기와 같은 알약 · 저장 길 @@ -374,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); } @@ -461,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, @@ -474,7 +459,6 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane }, pick, destroy: () => { - inputs.destroy(); cards.destroy(); }, }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 8bf07f719..fdc589b7f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -238,7 +238,6 @@ export function createMain( const made = createStructurePanel({ isAdmin, confirmLeave, - onDirty: () => (dirty = true), // 「선택 입력」 카드에서 자리를 고름 — 집계표 [저장]으로 함께 저장 keepColumn: (colId) => sheet?.selectColumn(colId), // 카드 고침을 안 버림 — 고른 열 되돌림 // 같은 페이지에서 그 열을 CAD·스프레드시트 전체 편집으로 엶 onEdit: (column, label, kind) => { @@ -485,7 +484,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_Project_Items_Router.py b/M02_MasterTemplete/M02_Project_Items_Router.py index ec27ca3eb..d877cda49 100644 --- a/M02_MasterTemplete/M02_Project_Items_Router.py +++ b/M02_MasterTemplete/M02_Project_Items_Router.py @@ -10,7 +10,7 @@ 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/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/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/test_item_copy_complete.py b/resources/tester/test_item_copy_complete.py index ba23d9e8a..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개를 마스터와 복사본으로 돌려 같은 값 - 마스터 단가 한 줄을 바꿔도 프로젝트 계산 그대로 · 목록에 「마스터 바뀜」 · 새로 가져오기 뒤 바뀜 (입력값 · 고른자재는 같은 키면 살림) @@ -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_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_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 571848174..ea468cf74 100644 --- a/resources/tester/test_m02_empty_master.py +++ b/resources/tester/test_m02_empty_master.py @@ -80,7 +80,7 @@ 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", "inputs"): + for kind in ("structure", "area"): assert not list((system / kind).glob("*.json")), kind 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 85006a9f7..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" 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_tree.py b/resources/tester/test_m02_tree.py index b6ec3fb80..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", "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/ui_template/ui_template_item_picker_detail.ts b/ui_template/ui_template_item_picker_detail.ts index 4e0e70360..6aea8414c 100644 --- a/ui_template/ui_template_item_picker_detail.ts +++ b/ui_template/ui_template_item_picker_detail.ts @@ -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}", @@ -113,7 +112,6 @@ const TEXT = { parts: { 도면: "도면", 면적: "면적산출", - 입력연결: "선택 입력 자리", 일위대가: "일위대가", } as Record, }; @@ -367,7 +365,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { if (mine !== seq || !cards || !inputs || !labor) return; const label = !info && canCopy ? TEXT.importEdit : null; cards.relabel({ structure: label, area: label }); - inputs.setEditLabel(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 b3474a8c8..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,11 +36,10 @@ const TEXT = { loadFailed: "복사본을 못 읽음 — {value}", discard: "저장 안 한 고침이 있음 — 버리고 돌아갈까요?", outOfRange: "범위 밖 값이 있어 저장하지 못함", - noAreaPill: - "면적산출 알약 없음 — 선택 입력에서 자리를 면적산출로 이으면 여기 뜸(마스터 · 회사 · 개인 층에서)", + noAreaPill: "면적산출 알약 없음 — 이 복사본 일위대가에 담은 로직 · 자재가 없음", drawing: "도면", area: "면적산출", - inputs: "선택 입력", + inputs: "넣을 값", suffix: "{value} 수정", }; @@ -94,13 +93,16 @@ 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; - + /** 복사본 일위대가의 알약(가져온 때 그대로) — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 */ async function sheetLinks(): Promise { - const [list, link] = await Promise.all([loadInputs(row), linkOf()]); - const 항목 = pillsOf(list, link, "면적"); + 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 { 항목, 모드: "입력" }; } diff --git a/ui_template/ui_template_item_picker_project.ts b/ui_template/ui_template_item_picker_project.ts index f0608a2d3..a719abd88 100644 --- a/ui_template/ui_template_item_picker_project.ts +++ b/ui_template/ui_template_item_picker_project.ts @@ -10,22 +10,17 @@ * 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", ProjectPart> = { @@ -185,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; @@ -241,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) — 카드 요약 · 넣을 값 자료 ─────────────────────────────── */ /** 일위대가 카드에 보이는 것 — 복사본이면 복사본 계산 · 아니면 원본 층 계산(저장 안 함) */ @@ -306,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 }; }