ERP-node/frontend/lib/registry/layouts/AutoRegisteringLayoutRender...

368 lines
11 KiB
TypeScript

"use client";
import { BaseLayoutRenderer, LayoutRendererProps } from "./BaseLayoutRenderer";
import { LayoutDefinition } from "@/types/layout";
import { LayoutRegistry } from "../LayoutRegistry";
import { ComponentData, LayoutComponent } from "@/types/screen";
import { LayoutZone } from "@/types/layout";
import React from "react";
import { DynamicComponentRenderer } from "../DynamicComponentRenderer";
/**
* 자동 등록 기능을 제공하는 베이스 레이아웃 렌더러
*
* 사용 방법:
* 1. 이 클래스를 상속받습니다
* 2. static layoutDefinition을 정의합니다
* 3. 파일을 import하면 자동으로 등록됩니다
*/
export class AutoRegisteringLayoutRenderer {
protected props: LayoutRendererProps;
constructor(props: LayoutRendererProps) {
this.props = props;
}
/**
* 레이아웃 정의 - 각 구현 클래스에서 반드시 정의해야 함
*/
static readonly layoutDefinition: LayoutDefinition;
/**
* 렌더링 메서드 - 각 구현 클래스에서 오버라이드해야 함
*/
render(): React.ReactElement {
throw new Error("render() method must be implemented by subclass");
}
/**
* 레이아웃 컨테이너 스타일을 계산합니다.
*/
getLayoutContainerStyle(): React.CSSProperties {
const { layout, style: propStyle } = this.props;
const style: React.CSSProperties = {
width: layout.size.width,
height: layout.size.height,
position: "relative",
overflow: "hidden",
...propStyle,
};
// 레이아웃 커스텀 스타일 적용
if (layout.style) {
Object.assign(style, this.convertComponentStyleToCSS(layout.style));
}
return style;
}
/**
* 컴포넌트 스타일을 CSS 스타일로 변환합니다.
*/
protected convertComponentStyleToCSS(componentStyle: any): React.CSSProperties {
const cssStyle: React.CSSProperties = {};
if (componentStyle.backgroundColor) {
cssStyle.backgroundColor = componentStyle.backgroundColor;
}
if (componentStyle.borderColor) {
cssStyle.borderColor = componentStyle.borderColor;
}
if (componentStyle.borderWidth) {
cssStyle.borderWidth = `${componentStyle.borderWidth}px`;
}
if (componentStyle.borderStyle) {
cssStyle.borderStyle = componentStyle.borderStyle;
}
if (componentStyle.borderRadius) {
cssStyle.borderRadius = `${componentStyle.borderRadius}px`;
}
return cssStyle;
}
/**
* 존별 자식 컴포넌트들을 분류합니다.
*/
getZoneChildren(zoneId: string): ComponentData[] {
return this.props.allComponents.filter((comp) => comp.parentId === this.props.layout.id && comp.zoneId === zoneId);
}
/**
* 레이아웃 존을 렌더링합니다.
*/
renderZone(
zone: LayoutZone,
zoneChildren: ComponentData[] = [],
additionalProps: Record<string, any> = {},
): React.ReactElement {
const { isDesignMode, onZoneClick, onComponentDrop } = this.props;
// 존 스타일 계산 - 항상 구역 경계 표시
const zoneStyle: React.CSSProperties = {
position: "relative",
// 구역 경계 시각화 - 항상 표시
border: "1px solid #e2e8f0",
borderRadius: "6px",
backgroundColor: "rgba(248, 250, 252, 0.5)",
transition: "all 0.2s ease",
...this.getZoneStyle(zone),
...additionalProps.style,
};
// 디자인 모드일 때 더 강조된 스타일
if (isDesignMode) {
zoneStyle.border = "2px dashed #cbd5e1";
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)";
}
// 호버 효과를 위한 추가 스타일
const dropZoneStyle: React.CSSProperties = {
minHeight: isDesignMode ? "60px" : "40px",
borderRadius: "4px",
display: "flex",
flexDirection: "column",
alignItems: zoneChildren.length === 0 ? "center" : "stretch",
justifyContent: zoneChildren.length === 0 ? "flex-start" : "flex-start",
color: "#64748b",
fontSize: "12px",
transition: "all 0.2s ease",
padding: "8px",
position: "relative",
};
return (
<div
key={zone.id}
className={`layout-zone ${additionalProps.className || ""}`}
style={zoneStyle}
onClick={(e) => {
e.stopPropagation();
onZoneClick?.(zone.id, e);
}}
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}}
onDrop={(e) => {
e.preventDefault();
const componentData = e.dataTransfer.getData("application/json");
if (componentData) {
try {
const component = JSON.parse(componentData);
onComponentDrop?.(zone.id, component, e);
} catch (error) {
console.error("컴포넌트 드롭 데이터 파싱 오류:", error);
}
}
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "#3b82f6";
e.currentTarget.style.backgroundColor = "rgba(59, 130, 246, 0.05)";
e.currentTarget.style.boxShadow = "0 0 0 2px rgba(59, 130, 246, 0.1)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0";
e.currentTarget.style.backgroundColor = isDesignMode
? "rgba(241, 245, 249, 0.8)"
: "rgba(248, 250, 252, 0.5)";
e.currentTarget.style.boxShadow = "none";
}}
{...additionalProps}
>
{/* 존 라벨 */}
<div
className="zone-label"
style={{
position: "absolute",
top: "-2px",
left: "8px",
backgroundColor: isDesignMode ? "#3b82f6" : "#64748b",
color: "white",
fontSize: "10px",
padding: "2px 6px",
borderRadius: "0 0 4px 4px",
fontWeight: "500",
zIndex: 10,
opacity: isDesignMode ? 1 : 0.7,
}}
>
{zone.name || zone.id}
</div>
{/* 드롭존 */}
<div className="drop-zone" style={dropZoneStyle}>
{zoneChildren.length > 0 ? (
zoneChildren.map((child) => (
<DynamicComponentRenderer
key={child.id}
component={child}
allComponents={this.props.allComponents}
isDesignMode={isDesignMode}
/>
))
) : (
<div className="empty-zone-indicator" style={{ textAlign: "center", opacity: 0.6 }}>
{isDesignMode ? "컴포넌트를 드래그하여 추가하세요" : ""}
</div>
)}
</div>
</div>
);
}
/**
* 존의 스타일을 계산합니다.
*/
protected getZoneStyle(zone: LayoutZone): React.CSSProperties {
const style: React.CSSProperties = {};
if (zone.size) {
if (zone.size.width) {
style.width = typeof zone.size.width === "number" ? `${zone.size.width}px` : zone.size.width;
}
if (zone.size.height) {
style.height = typeof zone.size.height === "number" ? `${zone.size.height}px` : zone.size.height;
}
if (zone.size.minWidth) {
style.minWidth = typeof zone.size.minWidth === "number" ? `${zone.size.minWidth}px` : zone.size.minWidth;
}
if (zone.size.minHeight) {
style.minHeight = typeof zone.size.minHeight === "number" ? `${zone.size.minHeight}px` : zone.size.minHeight;
}
}
return style;
}
/**
* 자동 등록 상태 추적
*/
private static registeredLayouts = new Set<string>();
/**
* 클래스가 정의될 때 자동으로 레지스트리에 등록
*/
static registerSelf(): void {
const definition = this.layoutDefinition;
if (!definition) {
console.error(`${this.name}: layoutDefinition이 정의되지 않았습니다.`);
return;
}
if (this.registeredLayouts.has(definition.id)) {
console.warn(`⚠️ ${definition.id} 레이아웃이 이미 등록되어 있습니다.`);
return;
}
try {
// 레지스트리에 등록
LayoutRegistry.registerLayout(definition);
this.registeredLayouts.add(definition.id);
console.log(`✅ 자동 등록 완료: ${definition.id} (${definition.name})`);
// 개발 모드에서 추가 정보 출력
if (process.env.NODE_ENV === "development") {
console.log(`📦 ${definition.id}:`, {
name: definition.name,
category: definition.category,
zones: definition.defaultZones?.length || 0,
tags: definition.tags?.join(", ") || "none",
});
}
} catch (error) {
console.error(`${definition.id} 레이아웃 등록 실패:`, error);
}
}
/**
* 레이아웃 등록 해제 (개발 모드에서 Hot Reload용)
*/
static unregisterSelf(): void {
const definition = this.layoutDefinition;
if (definition && this.registeredLayouts.has(definition.id)) {
LayoutRegistry.unregisterLayout(definition.id);
this.registeredLayouts.delete(definition.id);
console.log(`🗑️ 등록 해제: ${definition.id}`);
}
}
/**
* Hot Reload 지원 (개발 모드)
*/
static reloadSelf(): void {
if (process.env.NODE_ENV === "development") {
this.unregisterSelf();
this.registerSelf();
console.log(`🔄 Hot Reload: ${this.layoutDefinition?.id}`);
}
}
/**
* 등록된 레이아웃 목록 조회
*/
static getRegisteredLayouts(): string[] {
return Array.from(this.registeredLayouts);
}
/**
* 레이아웃 정의 유효성 검사
*/
static validateDefinition(): { isValid: boolean; errors: string[]; warnings: string[] } {
const definition = this.layoutDefinition;
if (!definition) {
return {
isValid: false,
errors: ["layoutDefinition이 정의되지 않았습니다."],
warnings: [],
};
}
const errors: string[] = [];
const warnings: string[] = [];
// 필수 필드 검사
if (!definition.id) errors.push("ID가 필요합니다.");
if (!definition.name) errors.push("이름이 필요합니다.");
if (!definition.component) errors.push("컴포넌트가 필요합니다.");
if (!definition.category) errors.push("카테고리가 필요합니다.");
// 권장사항 검사
if (!definition.description || definition.description.length < 10) {
warnings.push("설명은 10자 이상 권장됩니다.");
}
if (!definition.defaultZones || definition.defaultZones.length === 0) {
warnings.push("기본 존 정의가 권장됩니다.");
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
}
/**
* 개발 모드에서 Hot Module Replacement 지원
*/
if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
// HMR API가 있는 경우 등록
if ((module as any).hot) {
(module as any).hot.accept();
// 글로벌 Hot Reload 함수 등록
(window as any).__reloadLayout__ = (layoutId: string) => {
const layouts = AutoRegisteringLayoutRenderer.getRegisteredLayouts();
console.log(`🔄 Available layouts for reload:`, layouts);
// TODO: 특정 레이아웃만 리로드하는 로직 구현
};
}
}