feat(M02): 일위대가 라이브러리 화면 — 좌측 「일위대가 조합」 컨테이너 · 뿌리 셋 나무 · 조합 편집 · M01 컨테이너 뺌(PLAN 27-2b · 27-2c 화면)

- 좌측 「일위대가 조합」(구조물 조합 위) → 메인 칸 왼쪽 뿌리 셋 + 조합 나무(폴더 [구분, 상세구분] 깊이 2 · TreePanel 공용) · 오른쪽 목록 · 상세(옛 M01 조합 화면 옮김 · Labor_Edit)
- 나무 오른쪽 클릭: 폴더 더하기(2 단까지) · 조합 더하기(그 폴더 채운 빈 조합) · 이름 · 옮기기 · 지우기 · 조합 이름 바꾸기 서버 길 더함
- 해시 open=일위대가 로 진입 · 다른 뿌리를 누르면 그 뿌리 일위대가로
- 읽기만 뿌리는 저장 · 지우기 · 로직 더하기 없음
- TreePanel 에 maxDepth · item(부르는 말 · 새 항목 화면) 선택지 더함(구조물 라이브러리는 그대로)
- 조합 고르기(면적산출 옆 목록 · 일위대가 고르기)가 이 층이 쓸 수 있는 세 층 조합 · 알약은 /api/m02/combos/key/{키}/inputs
- M01 좌측 · 페이지에서 일위대가 조합 컨테이너 뺌
- 시험 test_m02_labor_tree(새) · test_m02_combo_layers 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
This commit is contained in:
2026-09-30 13:35:38 +09:00
co-authored by Claude Opus 5.5
parent ca946ebbf1
commit c8686ee3ad
17 changed files with 572 additions and 268 deletions
@@ -23,8 +23,8 @@ import {
} from "@ui/ui_template_elements";
import { openModal } from "@ui/ui_template_modal";
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
import { fetchComboInputs, searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch";
import { type ComboSummary, fetchCombos } from "./M02_MasterTemplete_Labor_Api";
import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch";
import { fetchComboInputs, fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api";
import {
copyArea,
copyMaterial,
@@ -86,13 +86,9 @@ const fill = (text: string, value: string): string => text.replace("{value}", va
/** `{열 id: {도번, 면적번, 재료번}}` — 집계표 번호 줄(없는 열은 키 없음 = 「없음」) */
export type ColumnNumbers = Record<string, StructureNumbers>;
/** 일위대가 목록 — 처음 한 번만 받음(못 받으면 다음에 다시) */
let combos: Promise<ComboSummary[]> | null = null;
const loadCombos = (): Promise<ComboSummary[]> =>
(combos ??= fetchCombos().catch((error: unknown) => {
combos = null;
throw error;
}));
/** 일위대가 목록 — 이 층이 쓸 수 있는 세 층 조합(27-2c) · 고를 때마다 받음(층을 바꿔 다시 들어와도 맞게) */
const loadCombos = () => fetchUsableCombos(getActiveLayer());
const LAYER_WORD = { system: "마스터", company: "회사", personal: "개인" } as const;
interface Choice {
value: string;
@@ -309,7 +305,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
};
const pickPrice = async (colId: string, columns: StructureColumn[]): Promise<string | null> => {
let list: ComboSummary[];
let list: Awaited<ReturnType<typeof loadCombos>>;
try {
list = await loadCombos();
} catch (error) {
@@ -321,7 +317,12 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
value: c.키,
key: c.키,
// 이름에 단위가 이미 붙은 조합이 있음 — 겹치면 뺌
label: [c.이름, c.단위 && !c.이름.includes(`(${c.단위})`) && `(${c.단위})`, c.구분]
label: [
c.이름,
c.단위 && !c.이름.includes(`(${c.단위})`) && `(${c.단위})`,
c.구분,
`· ${LAYER_WORD[c.layer]}`,
]
.filter(Boolean)
.join(" "),
}));