feat(M02): 구조물 라이브러리 한 화면 · 뿌리 셋 · 층 권한 한 벌 · 남의 층 목록 · 해시 진입 (PLAN 27-1)

- 층 권한 한 벌 M02_Layer_Access — 나무 · 항목(Router_Items) · 양식(Router_Layers) 같은 규칙 · owner 인자 · 남의 층은 읽기만 · 읽기로 남의 층에 뼈대 안 씀
- 남의 층 목록 GET /api/m02/layers/{company|personal}/owners — 시스템 관리자 회사 목록 · 회사 관리자 같은 회사 사람
- ?layer= 페이지 분리 폐기 → 한 화면 뿌리 셋(마스터 · 회사 · 개인) · 뿌리 부품 TreePanel_Roots(펼칠 때 불러옴) · TreePanel 은 owner · api 를 받는 공용 부품
- 진입 해시 ?open=구조물조합|일위대가|옵션|도면&root=&owner= · 항목 고르기 [관리 화면에서 고치기]도 이 해시
- 「항목 나무」 → 「구조물 라이브러리」
- 시험 test_m02_layer_access 새로 · layer_screen · template_layers 새 규칙으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 13:19:29 +09:00
co-authored by Claude Opus 5.5
parent 6c09be87f8
commit 59cb28ce58
21 changed files with 754 additions and 194 deletions
@@ -1,6 +1,7 @@
/* =============================================================================
* M02_MasterTemplete_TreePanel.ts
* 관리 화면(마스터 · 회사 · 개인 공용) 좌측 「항목 나무」 — 폴더 → … → 항목(깊이 자유) · 찾기.
* 관리 화면(마스터 · 회사 · 개인 공용) 「구조물 라이브러리」 한 층의 나무 — 폴더 → … → 항목(깊이 자유) · 찾기.
* 층 + `owner`(남의 층 · PLAN 27-1c) 한 벌을 그림 — 뿌리 셋 배치는 `_Roots` · 자료 길은 `api` 로 바꿔 끼움.
* 항목을 누르면 표에서 그 열이 골라짐(카드 넷은 표가 엶) · 표에서 열을 고르면 `select` 로 나무도 그 항목.
* 오른쪽 클릭 · 키보드(F2 · Delete)로 폴더 · 항목 더하기 · 이름 바꾸기 · 옮기기 · 지우기 —
* `POST /api/m02/layers/{층}/tree`(PLAN 26-1). 읽기만 층은 메뉴 · 키 없음. 나무 자료는 `_Model`.
@@ -44,6 +45,15 @@ import {
export interface TreePanelOptions {
layer: Layer;
/** 남의 층 주인(회사 `{C}` · 개인 `{C}/{U}`) — 없으면 자기 층 */
owner?: string | null;
/** 맨 위 줄 글 — 없으면 층 이름 */
label?: string;
/** 나무 읽기 · 고치기 길 — 없으면 구조물 조합 나무(`/layers/{층}/tree`) */
api?: {
fetch: (layer: Layer, owner: string | null) => Promise<TreeView>;
edit: (layer: Layer, edit: TreeEdit, 판: string, owner: string | null) => Promise<TreeView>;
};
/** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */
canEdit: boolean;
/** 항목을 누름 — 표에서 그 열을 고름 */
@@ -67,13 +77,17 @@ const why = (error: unknown): string => (error instanceof Error ? error.message
const hint = (text: string): HTMLElement => el("p", { className: "m02-tree__hint", text });
export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
const layerLabel = L(
opt.layer === "company"
? "M02_PickerCompany"
: opt.layer === "personal"
? "M02_PickerPersonal"
: "M02_PickerSystem",
);
const layerLabel =
opt.label ??
L(
opt.layer === "company"
? "M02_PickerCompany"
: opt.layer === "personal"
? "M02_PickerPersonal"
: "M02_PickerSystem",
);
const owner = opt.owner ?? null;
const api = opt.api ?? { fetch: fetchTree, edit: editTree };
let view: TreeView | null = null;
let text = "";
let activeCol: string | null = null;
@@ -175,7 +189,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
async function refresh(): Promise<void> {
const mine = ++seq;
try {
const got = await fetchTree(opt.layer);
const got = await api.fetch(opt.layer, owner);
if (mine !== seq) return;
view = got;
} catch (error) {
@@ -217,7 +231,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
if (!view || busy) return;
busy = true;
try {
const got = await editTree(opt.layer, body_, view.판);
const got = await api.edit(opt.layer, body_, view.판, owner);
view = got;
const note: TreeNote | undefined = got.알림;
// 새 항목은 그 열을 고름 · 지운 항목이면 고름 풀림 · 나머지는 지금 고른 열 그대로