feat(M02): 사전조합 구분 · 상세구분 = 구조물군 › 종류 + 공통 · 옛 품셈 분류 = 미분류(134-5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
2026-10-09 15:06:14 +09:00
co-authored by Claude Opus 5.5
parent 2640fb4c60
commit 484c92892d
6 changed files with 107 additions and 21 deletions
+8 -2
View File
@@ -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, "구분": "", "상세구분": ""}
@@ -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,
@@ -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<void> =>
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);
});
});
@@ -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<Kind, "precombo">;
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<StructDef, "구조물군" | "types">,
): { 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;
@@ -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 }));
})();
@@ -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