From 484c92892decb51a2652ce6e817e5579c2d9aa7a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 15:06:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EC=82=AC=EC=A0=84=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20=EA=B5=AC=EB=B6=84=20=C2=B7=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=20=3D=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=EA=B5=B0?= =?UTF-8?q?=20=E2=80=BA=20=EC=A2=85=EB=A5=98=20+=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=C2=B7=20=EC=98=9B=20=ED=92=88=EC=85=88=20=EB=B6=84=EB=A5=98=20?= =?UTF-8?q?=3D=20=EB=AF=B8=EB=B6=84=EB=A5=98(134-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY --- M02_MasterTemplete/M02_Labor_Ref.py | 10 ++- .../M02_MasterTemplete_Labor_Detail.ts | 15 ++-- .../M02_MasterTemplete_Library_List.ts | 12 +--- .../M02_MasterTemplete_UI_Side_Precombo.ts | 16 +++++ .../tester/helper_m02_precombo_scope.cjs | 6 +- resources/tester/test_m02_precombo_class.py | 69 +++++++++++++++++++ 6 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 resources/tester/test_m02_precombo_class.py diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index f9a319052..765262be8 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -147,9 +147,15 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]: return out +_OLD_CLASS = re.compile(r"품셈") # 134-5 — 옛 품셈 분류(건설품셈 공통 · 산림품셈 · 07장 …) + + def filled(kind: str, doc: Any) -> Any: - """옛 서고 문서(51-12 앞) 읽을 때 칸 채움 — 사전조합 = 빈 글(화면 몫).""" - if not isinstance(doc, dict) or kind not in PREFIX or "구분" in doc: + """옛 서고 문서 읽을 때 칸 채움 — 칸 없음(51-12 앞) · 옛 품셈 분류(134-5) = 빈 글(미분류) · + 파일은 안 고침([저장] 때 굳음) · 그 밖 지금 값 그대로(구조물정의가 바뀌어도 화면이 선택지로 둠).""" + if not isinstance(doc, dict) or kind not in PREFIX: + return doc + if "구분" in doc and not _OLD_CLASS.search(str(doc.get("구분") or "")): return doc return {**doc, "구분": "", "상세구분": ""} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 878146694..b1a4047a7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -27,7 +27,6 @@ import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side"; import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; -import type { SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; type Recalc = (typeof import("../A00_Common/spreadsheet/spreadsheet_recalc"))["recalcWorkbook"]; import { @@ -43,7 +42,6 @@ import { lineId, settle, type LaborRef } from "./M02_MasterTemplete_Labor_Calc"; import { areaLinks, calcLabor, - fetchLibLogicSubs, fetchLogicInputs, normalizeLabor, type LaborCalc, @@ -68,6 +66,8 @@ import { insideKeys, releaseTied } from "./M02_Labor_LogicMats_Model"; import { keepKnown } from "./M02_Labor_Values_Model"; import { pickMaterial } from "./M02_Material_Pick"; import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick"; +import { fetchStructureDef } from "./StructOptions/M02_SO_Api"; +import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"; import "@ui/ui_template_general_layout.css"; @@ -559,14 +559,15 @@ export function mountLaborDetail( opt.onChanged(dirty()); }); - /** 서고 구분 · 상세구분(51-12) — 로직 구분 목록(logic-lib/subs)에서 고름(구분 바꾸면 상세구분 = 그 첫 줄) */ + /** 서고 구분 · 상세구분(51-12 · 134-5) — 구조물군 › 종류 + 공통(`precomboClasses`)에서 고름 · + * 구분 바꾸면 상세구분 = 그 첫 줄(공통) · 빈 값 = 미분류 */ const meta = cur as { 구분?: string | null; 상세구분?: string | null }; const libMeta = (): HTMLElement[] => { const sub = createSelectField({ options: [], label: tc("Sub"), disabled: !edit }); const detail = createSelectField({ options: [], label: tc("Detail"), disabled: !edit }); sub.root.dataset.laborField = "구분"; detail.root.dataset.laborField = "상세구분"; - let subs: SubBrief[] = []; + let subs: { name: string; details: string[] }[] = []; // 목록에 없는 지금 값(빈 것 = 「—」 · 옛 문서)도 선택지로 둠 const opts = (names: string[], now: string | null | undefined) => [...new Set([now ?? "", ...names])] @@ -594,9 +595,9 @@ export function mountLaborDetail( opt.onChanged(dirty()); }); fill(); - void fetchLibLogicSubs().then( - (got) => { - subs = got; + void fetchStructureDef().then( + (def) => { + subs = precomboClasses(def); fill(); }, () => undefined, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts index fa4dd69c6..9774566c7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts @@ -29,7 +29,6 @@ import { type LibraryBody, type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; -import { fetchLibLogicSubs } from "./M02_MasterTemplete_Labor_Api"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo"; @@ -182,17 +181,10 @@ export function mountLibraryList( const create = async (body: LibraryBody, label: string): Promise => made((await newLibrary(opt.layer, opt.kind, body, opt.owner)).이름, label); - /** 사전조합 = 이름 + 구분 · 상세구분 처음 값 = 로직 구분 목록 첫 줄(51-12 · 상세에서 바꿈) · 빈 묶음 */ + /** 사전조합 = 이름 · 구분 · 상세구분 빈 값(미분류 · 134-5 · 상세에서 고름) · 빈 묶음 */ btnNew.addEventListener("click", () => { nameDialog(L("M02_NewTitle"), "", async (name) => { - const first = (await fetchLibLogicSubs())[0]; - const 문서 = { - 이름: name, - 구분: first?.name ?? null, - 상세구분: first?.details[0] ?? null, - 단위: null, - 담은로직: [], - }; + const 문서 = { 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] }; await create({ 문서 }, name); }); }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts index 67a77f931..b81254066 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts @@ -10,6 +10,7 @@ import { el } from "@ui/ui_template_elements"; import { createTreeRow, renderTree, type TreeNode } from "@ui/ui_template_tree"; import { listTemplates, type Kind, type Layer } from "./M02_MasterTemplete_Api_Fetch"; import type { Root } from "./M02_MasterTemplete_Layer"; +import type { StructDef } from "./StructOptions/M02_SO_Def_Model"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import type { Selection } from "./M02_MasterTemplete_UI_Main"; @@ -18,6 +19,21 @@ export type PrecomboKind = Extract; export const PRECOMBO_TITLE = "일위대가 사전조합"; export const isPrecombo = (kind: Kind): kind is PrecomboKind => kind === "precombo"; +/** 여러 구조물에 쓰는 묶음(콘크리트 · 거푸집 등) — 군마다 첫 상세구분(134-5) */ +export const COMMON = "공통"; + +/** 사전조합 구분 · 상세구분(134-5) — 구조물군 A~G › 그 군 구조물 종류 + 「공통」(원천 `구조물정의.json` · + * B05 구조물 배치와 같은 갈래 · 꺼진 종류도 넣음) */ +export const precomboClasses = ( + def: Pick, +): { name: string; details: string[] }[] => + Object.entries(def.구조물군) + .filter(([key]) => /^[A-G]$/.test(key)) + .map(([key, name]) => ({ + name, + details: [COMMON, ...new Set(def.types.filter((t) => t.group === key).map((t) => t.name))], + })); + /** 사전조합 층(134-3) — 페이지 층과 따로 · `owner` null = 자기 층 */ export interface PrecomboScope { root: Root; diff --git a/resources/tester/helper_m02_precombo_scope.cjs b/resources/tester/helper_m02_precombo_scope.cjs index aa455bbe2..58423a827 100644 --- a/resources/tester/helper_m02_precombo_scope.cjs +++ b/resources/tester/helper_m02_precombo_scope.cjs @@ -1,6 +1,7 @@ /* M02 일위대가 사전조합 층(134-3) 헬퍼 — `M02_MasterTemplete_Api_Fetch.ts` 명시 owner 길 · * `M02_MasterTemplete_UI_Side_Precombo.ts` `groupRows`(구분 › 상세구분 2단) 를 트랜스파일해 Node 로 돌림 · - * fetch 대역이 부른 주소를 모음 · stdin = {rows} · stdout = {urls, groups} JSON. */ + * `precomboClasses`(134-5 · 구조물군 › 종류 + 공통) · fetch 대역이 부른 주소를 모음 · + * stdin = {rows, def?} · stdout = {urls, groups, classes} JSON. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); @@ -48,5 +49,6 @@ global.fetch = async (url) => { 구분: g.구분, 상세: [...g.상세].map(([k, v]) => [k, v.map((r) => r.이름)]), })); - process.stdout.write(JSON.stringify({ urls, groups })); + const classes = input.def ? side.precomboClasses(input.def) : null; // 134-5 + process.stdout.write(JSON.stringify({ urls, groups, classes })); })(); diff --git a/resources/tester/test_m02_precombo_class.py b/resources/tester/test_m02_precombo_class.py new file mode 100644 index 000000000..73eb21215 --- /dev/null +++ b/resources/tester/test_m02_precombo_class.py @@ -0,0 +1,69 @@ +"""일위대가 사전조합 구분 · 상세구분 새 분류(PLAN 134-5) — 구조물군 A~G › 그 군 종류 + 「공통」. + +- 원천 = 마스터 `구조물정의.json`(구조물군 · types) — 군마다 첫 상세구분 「공통」 · 꺼진 종류도 · 「호환(기타)」 뺌 +- 옛 품셈 분류(건설품셈 공통 · 산림품셈) = 읽을 때 빈 값(미분류) · 파일 안 고침 · 새 분류 값 · 지금 값은 그대로 +- 새 문서 = 빈 값(미분류) · 상세 드롭다운 원천 = 구조물 정의(로직 구분 목록 아님) +""" + +import json +import os +import subprocess +from pathlib import Path + +from M02_MasterTemplete import M02_Labor_Ref as refs + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = Path(HERE).parents[1] +M02 = ROOT / "M02_MasterTemplete" + + +def test_분류는_구조물군_종류_공통(): + defn = json.loads((ROOT / "resources/master_template/구조물정의.json").read_text("utf-8")) + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_m02_precombo_scope.cjs")], + input=json.dumps({"rows": [], "def": defn}, ensure_ascii=False), + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + classes = json.loads(proc.stdout)["classes"] + groups = {k: v for k, v in defn["구조물군"].items() if k in "ABCDEFG"} + assert [c["name"] for c in classes] == list( + groups.values() + ) # A 횡단배수 … G 노면공 · 기타 없음 + c = next(c for c in classes if c["name"] == groups["C"]) + assert c["details"] == [ + "공통", + "옹벽", + "돌쌓기(찰)", + "돌쌓기(메)", + "흙막이", + "큰돌쌓기", + "소단", + ] + b = next(c for c in classes if c["name"] == groups["B"]) + assert "측구(옆도랑)" in b["details"] # 꺼진 종류도 넣음 + assert all( + c["details"][0] == "공통" and len(set(c["details"])) == len(c["details"]) for c in classes + ) + + +def test_옛_품셈_분류는_미분류_새_값은_그대로(): + old = {"이름": "가", "구분": "건설품셈 공통", "상세구분": "07장 돌공사", "담은로직": []} + got = refs.filled("precombo", old) + assert (got["구분"], got["상세구분"]) == ("", "") and old["구분"] == "건설품셈 공통" + assert refs.filled("precombo", {**old, "구분": "산림품셈", "상세구분": "03장"})["구분"] == "" + now = {**old, "구분": "C 사면안정", "상세구분": "옹벽"} + assert refs.filled("precombo", now) is now + gone = {**old, "구분": "Z 없어진 군", "상세구분": "x"} # 지금 값은 선택지로(화면 몫) + assert refs.filled("precombo", gone) is gone + assert refs.filled("precombo", {"담은로직": []})["구분"] == "" # 칸 없는 옛 문서 + + +def test_화면_원천은_구조물_정의_새_문서는_미분류(): + detail = (M02 / "M02_MasterTemplete_Labor_Detail.ts").read_text("utf-8") + assert "subs = precomboClasses(def);" in detail and "fetchLibLogicSubs" not in detail + assert '[...new Set([now ?? "", ...names])]' in detail # 지금 값도 선택지 그대로 + lib = (M02 / "M02_MasterTemplete_Library_List.ts").read_text("utf-8") + assert '구분: "", 상세구분: ""' in lib and "fetchLibLogicSubs" not in lib