- registerPanelSection · createDesignPanel · createDesignDock · DesignView(24-1 약속 그대로) - 아래 단추 넷 [초기화][저장][확정][보기 전환] · 글자 키 Design_Btn_ToCross · ToProfile - 페이지 연결은 다음 커밋 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
152 lines
6.6 KiB
TypeScript
152 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* design_panel.ts
|
|
* B05 · B06 한 페이지 두 보기의 **좌측 패널 칸 등록부**(계획서 24-1 · 24-2b).
|
|
*
|
|
* 칸(구조물 배치 · 페이지 설정 · 표준 횡단면 설정 …)은 **한 번만 정의**하고 보일 보기만 적는다.
|
|
* 페이지 파일은 칸도 단추도 직접 만들지 않고 `createDesignPanel(ctx, view)` 한 줄만 부른다 —
|
|
* 시험(`test_design_view_parity.py`)이 이를 지킨다.
|
|
*
|
|
* registerPanelSection({ id, title, views, order, build }) ← 칸 정의(모듈이 읽힐 때 한 번)
|
|
* createDesignPanel(ctx, view) → { root, setView } ← 페이지가 부름
|
|
*
|
|
* 칸이 쓰는 페이지 쪽 부품(구조물 패널 · 입력 칸 묶음 …)은 `ctx.parts` 에서 이름으로 꺼낸다 —
|
|
* 칸 정의가 B05 · B06 의 구체 타입을 몰라도 되게 하려는 것이다.
|
|
* ========================================================================== */
|
|
|
|
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
|
import {
|
|
createDesignDock,
|
|
type DesignDock,
|
|
type DesignDockActions,
|
|
type DesignView,
|
|
} from "./design_dock";
|
|
|
|
export type { DesignView } from "./design_dock";
|
|
|
|
/** 좌측 패널 하나가 페이지에서 받는 것 — 값은 바뀌므로 부품은 이름으로, 단추 동작은 함수로. */
|
|
export interface DesignPanelContext {
|
|
projectId: string | null;
|
|
/** 이름 → 페이지가 만든 부품. 칸 정의가 `part()` 로 꺼낸다. */
|
|
parts: Record<string, unknown>;
|
|
/** 아래 단추 넷의 동작 — 페이지가 채운다(저장 흐름은 `design_save`). */
|
|
dock: DesignDockActions;
|
|
}
|
|
|
|
/** 칸 정의가 돌려주는 것 — 칸 뿌리 하나, 그리고 (있으면) 아래 고정 영역 위에 얹을 것. */
|
|
export type PanelSectionBuild = HTMLElement | { root: HTMLElement; dock?: HTMLElement };
|
|
|
|
export interface PanelSectionDef {
|
|
id: string;
|
|
title: string;
|
|
/** 이 칸이 보일 보기 — 둘 다 적으면 한 정의로 두 보기(구조물 배치). */
|
|
views: ReadonlyArray<DesignView>;
|
|
/** 칸 차례 — 작을수록 위. */
|
|
order: number;
|
|
build: (ctx: DesignPanelContext, view: DesignView) => PanelSectionBuild;
|
|
}
|
|
|
|
const registry = new Map<string, PanelSectionDef>();
|
|
|
|
/** 칸 정의를 등록한다. 같은 id 는 덮어쓴다(같은 모듈이 다시 읽혀도 한 벌). */
|
|
export function registerPanelSection(def: PanelSectionDef): void {
|
|
registry.set(def.id, def);
|
|
}
|
|
|
|
/** 그 보기에 보일 칸 정의 — 차례대로. 시험이 「두 보기가 같은 칸을 쓰는지」 볼 때도 쓴다. */
|
|
export function panelSectionsFor(view: DesignView): PanelSectionDef[] {
|
|
return [...registry.values()]
|
|
.filter((def) => def.views.includes(view))
|
|
.sort((a, b) => a.order - b.order);
|
|
}
|
|
|
|
/** 칸이 부품을 이름으로 꺼낸다 — 페이지가 안 건넸으면 바로 알리도록 던진다. */
|
|
export function part<T>(ctx: DesignPanelContext, name: string): T {
|
|
const found = ctx.parts[name];
|
|
if (found === undefined)
|
|
throw new Error(`좌측 패널 부품 「${name}」 을 페이지가 건네지 않았습니다.`);
|
|
return found as T;
|
|
}
|
|
|
|
/**
|
|
* 칸 틀 — 접는 제목 + 본문. 두 보기가 **다른 클래스**를 쓰던 것(`b05-route__panel-section` ·
|
|
* `b06-profile__group`)을 그대로 살린다(2단계에서 한 벌로 합침). 종단 보기 본문은 별도 div,
|
|
* 횡단 보기는 칸 뿌리에 바로 붙인다.
|
|
*/
|
|
export function createSectionFrame(
|
|
view: DesignView,
|
|
title: string,
|
|
collapsed = false,
|
|
): { root: HTMLElement; body: HTMLElement } {
|
|
const root = document.createElement("section");
|
|
const heading = document.createElement("h3");
|
|
heading.textContent = title;
|
|
if (view === "profile") {
|
|
// ui-sidebar-section: 사이드 컨테이너 공통 외곽선(진하게, 2026-08-05 사용자 지시).
|
|
// 접힘 초기값만 여기서 준다 — 펼치기·접기 토글은 ui_template_collapsible 전역 몫.
|
|
root.className = `b05-route__panel-section ui-collapsible ui-sidebar-section${collapsed ? " is-collapsed" : ""}`;
|
|
heading.className = "ui-collapsible__title";
|
|
const body = document.createElement("div");
|
|
body.className = "b05-route__panel-body";
|
|
root.append(heading, body);
|
|
return { root, body };
|
|
}
|
|
root.className = `b06-profile__group ui-collapsible ui-sidebar-section${collapsed ? " is-collapsed" : ""}`;
|
|
heading.className = "b06-profile__group-legend ui-collapsible__title";
|
|
root.append(heading);
|
|
return { root, body: root };
|
|
}
|
|
|
|
export interface DesignPanel {
|
|
root: HTMLElement;
|
|
/** 아래 고정 영역 — 단추 켬·끔을 페이지가 다시 줄 때 쓴다. */
|
|
dock: DesignDock;
|
|
/** 보기가 바뀌면 그 보기에 없는 칸은 숨기고 있는 칸은 (처음이면 만들어) 보인다. */
|
|
setView: (view: DesignView) => void;
|
|
}
|
|
|
|
/** 좌측 패널 — 그 보기의 칸을 차례대로 세우고 아래 고정 영역을 붙인다. */
|
|
export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): DesignPanel {
|
|
const root = document.createElement("div");
|
|
root.className = view === "profile" ? "b05-route__panel" : "b06-profile__form";
|
|
const built = new Map<string, HTMLElement>();
|
|
const dockParts: HTMLElement[] = [];
|
|
const dock = createDesignDock(view, dockParts, ctx.dock);
|
|
|
|
const show = (target: DesignView): void => {
|
|
const wanted = new Set(panelSectionsFor(target).map((def) => def.id));
|
|
for (const def of panelSectionsFor(target)) {
|
|
if (built.has(def.id)) continue;
|
|
const made = def.build(ctx, target);
|
|
const sectionRoot = "root" in made ? made.root : made;
|
|
if ("root" in made && made.dock) dock.root.prepend(made.dock);
|
|
built.set(def.id, sectionRoot);
|
|
root.insertBefore(sectionRoot, dock.root);
|
|
}
|
|
// 차례를 정의 순서로 다시 맞춘다 — 늦게 만들어진 칸이 맨 아래에 남지 않게.
|
|
for (const def of panelSectionsFor(target)) {
|
|
const element = built.get(def.id);
|
|
if (element) root.insertBefore(element, dock.root);
|
|
}
|
|
built.forEach((element, id) => {
|
|
// 칸이 스스로 숨긴 것(선택 포인트)은 건드리지 않고, 보기 밖 칸만 가리고 되살린다.
|
|
element.style.display = wanted.has(id) ? "" : "none";
|
|
});
|
|
};
|
|
|
|
root.append(dock.root);
|
|
show(view);
|
|
// 컨테이너 제목 행 전체 클릭 시 본문을 접거나 편다(공용 collapsible). 내부 details 등 별도
|
|
// 접힘 항목은 손대지 않는다.
|
|
attachCollapsible(root);
|
|
|
|
return {
|
|
root,
|
|
dock,
|
|
setView: (next) => {
|
|
dock.setView(next);
|
|
root.className = next === "profile" ? "b05-route__panel" : "b06-profile__form";
|
|
show(next);
|
|
},
|
|
};
|
|
}
|