feat(M01): 자재 조합 화면 — 목록 · 상세 · 자재 더하기 · 지우기 알림

- 좌측 「자재 조합」 컨테이너(찾기) + 우측 목록(키 · 이름 · 자재 수 · 본뜬키 · 소유) · 상세(이름 · 비고 · 본뜬키 · 담은 자재 표 · 빼기)
- [자재 더하기] = 자재DB 낱말 찾기 → 하나씩 고름(이미 담은 자재는 잠김) · 저장 · 지우기는 M01 길 /api/m01/matset…
- 쓰는 구조물 열이 있어 지울 수 없으면 서버가 준 구조물 이름들을 알림으로 보임
- 알약 키 「이름|단위」 주석 갱신

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoyUwuTFiDJfvCfHpvKKzp
This commit is contained in:
2026-09-28 19:14:10 +09:00
co-authored by Claude Sonnet 5
parent d35269e32a
commit 4c8ee6828f
6 changed files with 525 additions and 6 deletions
+15 -2
View File
@@ -11,6 +11,7 @@ import { t as L } from "@ui/ui_template_locale";
import { fetchFiles, fetchSubsAll, type FileInfo, type SubInfo } from "./M01_MasterData_Api_Fetch";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { tc } from "./M01_MasterData_UI_Combo_Text";
import { tm } from "./M01_MasterData_UI_Matset_Text";
import { renderTree, type MakeRow } from "./M01_MasterData_UI_Tree";
/** 컨테이너마다 기억하는 화면 상태 — 찾기 글 · 쪽 · 소요량·계수 표의 용도 대상 거름 (거름은 `m01.filter.*`) */
@@ -88,6 +89,8 @@ export interface SideHandle {
logicHost: HTMLElement;
/** 일위대가 조합 컨테이너 안(구분 · 상세구분 거름이 들어갈 자리) */
comboHost: HTMLElement;
/** 자재 조합 컨테이너 안(찾기 칸이 들어갈 자리) */
matsetHost: HTMLElement;
/** 그룹의 파일 판본을 다시 받음(저장 뒤) — 돌려받는 것 = 새 목록 */
refresh: (group: string) => Promise<FileInfo[]>;
/** 그룹의 파일 목록(이미 받은 것) */
@@ -146,11 +149,13 @@ const LEAF: Group[] = ["환율", "요율"];
const FILTERED: Group[] = ["인력", "기계", "소요량", "계수"];
const LOGIC_ID = "로직|";
const COMBO_ID = "조합|";
const MATSET_ID = "자재조합|";
export function buildSide(
onOpen: (pick: Pick) => void,
onLogic: () => void,
onCombo: () => void = () => {},
onMatset: () => void = () => {},
): SideHandle {
const lists = new Map<Group, FileInfo[]>();
const bodies = new Map<Group, HTMLElement>();
@@ -460,9 +465,17 @@ ${first.file}`
onCombo();
});
const matsetHost = el("div", { className: "m01-side__items ui-sidebar-list" });
const matset = section(tm("Title"), matsetHost);
matset.querySelector(".ui-collapsible__title")?.addEventListener("click", () => {
if (!matset.classList.contains("is-collapsed")) return;
setActive(buttons.has(MATSET_ID) ? MATSET_ID : null);
onMatset();
});
const root = el("div", {
className: "m01-side ui-sidebar-panel",
children: [...groups, logic, combo],
children: [...groups, logic, combo, matset],
});
attachCollapsible(root);
void Promise.all(GROUPS.map(refresh)).catch((error) =>
@@ -472,5 +485,5 @@ ${first.file}`
onPicked = fn;
};
const files = (group: string): FileInfo[] => lists.get(group as Group) ?? [];
return { root, logicHost, comboHost, refresh, files, setActive, setOnPicked, filter };
return { root, logicHost, comboHost, matsetHost, refresh, files, setActive, setOnPicked, filter };
}