웹타입 컴포넌트 분리작업

This commit is contained in:
kjs
2025-09-09 14:29:04 +09:00
parent 540d82e7e4
commit a17602c643
76 changed files with 16660 additions and 1735 deletions

View File

@@ -0,0 +1,112 @@
"use client";
import React, { useMemo } from "react";
import { WebTypeRegistry } from "./WebTypeRegistry";
import { WebTypeConfigPanelProps } from "./types";
/**
* 동적 설정 패널 렌더러 컴포넌트
* 레지스트리에서 웹타입을 조회하여 해당 설정 패널을 동적으로 렌더링합니다.
*/
export const DynamicConfigPanel: React.FC<
WebTypeConfigPanelProps & {
webType: string;
}
> = ({ webType, component, onUpdateComponent, onUpdateProperty }) => {
// 레지스트리에서 웹타입 정의 조회
const webTypeDefinition = useMemo(() => {
return WebTypeRegistry.getWebType(webType);
}, [webType]);
// 웹타입이 등록되지 않은 경우
if (!webTypeDefinition) {
console.warn(`웹타입 "${webType}"이 레지스트리에 등록되지 않았습니다.`);
return (
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4">
<div className="flex items-center gap-2 text-red-600">
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-red-500"> "{webType}" .</p>
</div>
);
}
// 설정 패널 컴포넌트가 없는 경우
if (!webTypeDefinition.configPanel) {
return (
<div className="rounded-md border border-dashed border-yellow-300 bg-yellow-50 p-4">
<div className="flex items-center gap-2 text-yellow-600">
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-yellow-500"> "{webType}" .</p>
</div>
);
}
const ConfigPanelComponent = webTypeDefinition.configPanel;
// 설정 패널 props 구성
const configPanelProps: WebTypeConfigPanelProps = {
component,
onUpdateComponent,
onUpdateProperty,
};
try {
return <ConfigPanelComponent {...configPanelProps} />;
} catch (error) {
console.error(`웹타입 "${webType}" 설정 패널 렌더링 중 오류 발생:`, error);
return (
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4">
<div className="flex items-center gap-2 text-red-600">
<span className="text-sm font-medium">💥 </span>
</div>
<p className="mt-1 text-xs text-red-500"> "{webType}" .</p>
{process.env.NODE_ENV === "development" && (
<pre className="mt-2 overflow-auto text-xs text-red-400">
{error instanceof Error ? error.stack : String(error)}
</pre>
)}
</div>
);
}
};
DynamicConfigPanel.displayName = "DynamicConfigPanel";
/**
* 웹타입별 설정 패널을 렌더링하는 헬퍼 함수
*/
export function renderConfigPanel(
webType: string,
component: any,
onUpdateComponent: (component: any) => void,
onUpdateProperty: (property: string, value: any) => void,
): React.ReactElement | null {
return (
<DynamicConfigPanel
webType={webType}
component={component}
onUpdateComponent={onUpdateComponent}
onUpdateProperty={onUpdateProperty}
/>
);
}
/**
* 웹타입이 설정 패널을 지원하는지 확인하는 헬퍼 함수
*/
export function hasConfigPanel(webType: string): boolean {
const webTypeDefinition = WebTypeRegistry.getWebType(webType);
return !!(webTypeDefinition && webTypeDefinition.configPanel);
}
/**
* 웹타입의 기본 설정을 가져오는 헬퍼 함수
*/
export function getDefaultConfig(webType: string): Record<string, any> | null {
const webTypeDefinition = WebTypeRegistry.getWebType(webType);
return webTypeDefinition ? webTypeDefinition.defaultConfig : null;
}

View File

@@ -0,0 +1,148 @@
"use client";
import React, { useMemo } from "react";
import { WebTypeRegistry } from "./WebTypeRegistry";
import { DynamicComponentProps } from "./types";
import { getWidgetComponentByWebType, getWidgetComponentByName } from "@/components/screen/widgets/types";
import { useWebTypes } from "@/hooks/admin/useWebTypes";
/**
* 동적 웹타입 렌더러 컴포넌트
* 레지스트리에서 웹타입을 조회하여 동적으로 렌더링합니다.
*/
export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
webType,
props = {},
config = {},
onEvent,
}) => {
// 모든 hooks를 먼저 호출 (조건부 return 이전에)
const { webTypes } = useWebTypes({ active: "Y" });
const webTypeDefinition = useMemo(() => {
return WebTypeRegistry.getWebType(webType);
}, [webType]);
// 데이터베이스에서 웹타입 정보 조회
const dbWebType = useMemo(() => {
return webTypes.find((wt) => wt.web_type === webType);
}, [webTypes, webType]);
// 기본 설정과 전달받은 설정을 병합 (조건부로 사용되지만 항상 계산)
const mergedConfig = useMemo(() => {
if (!webTypeDefinition) return config;
return {
...webTypeDefinition.defaultConfig,
...config,
};
}, [webTypeDefinition?.defaultConfig, config]);
// 최종 props 구성 (조건부로 사용되지만 항상 계산)
const finalProps = useMemo(() => {
return {
...props,
webTypeConfig: mergedConfig,
webType: webType,
onEvent: onEvent,
};
}, [props, mergedConfig, webType, onEvent]);
// 1순위: DB에서 지정된 컴포넌트 사용 (항상 우선)
if (dbWebType?.component_name) {
try {
console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`);
console.log(`DB 웹타입 정보:`, dbWebType);
console.log(`웹타입 데이터 배열:`, webTypes);
const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
return <ComponentByName {...props} />;
} catch (error) {
console.error(`DB 지정 컴포넌트 "${dbWebType.component_name}" 렌더링 실패:`, error);
}
}
// 2순위: 레지스트리에 등록된 웹타입 사용
if (webTypeDefinition) {
console.log(`웹타입 "${webType}" → 레지스트리 컴포넌트 사용`);
// 웹타입이 비활성화된 경우
if (!webTypeDefinition.isActive) {
console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
return (
<div className="rounded-md border border-dashed border-yellow-300 bg-yellow-50 p-4">
<div className="flex items-center gap-2 text-yellow-600">
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-yellow-500"> "{webType}" .</p>
</div>
);
}
const Component = webTypeDefinition.component;
try {
return <Component {...finalProps} />;
} catch (error) {
console.error(`웹타입 "${webType}" 레지스트리 컴포넌트 렌더링 실패:`, error);
}
}
// 3순위: 웹타입명으로 자동 매핑 (폴백)
try {
console.warn(`웹타입 "${webType}" → 자동 매핑 폴백 사용`);
const FallbackComponent = getWidgetComponentByWebType(webType);
return <FallbackComponent {...props} />;
} catch (error) {
console.error(`웹타입 "${webType}" 폴백 컴포넌트 렌더링 실패:`, error);
return (
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4">
<div className="flex items-center gap-2 text-red-600">
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-red-500"> "{webType}" .</p>
</div>
);
}
};
DynamicWebTypeRenderer.displayName = "DynamicWebTypeRenderer";
/**
* 웹타입 미리보기 렌더러
* 관리 페이지에서 웹타입을 미리보기할 때 사용
*/
export const WebTypePreviewRenderer: React.FC<{
webType: string;
config?: Record<string, any>;
size?: "sm" | "md" | "lg";
}> = ({ webType, config = {}, size = "md" }) => {
const webTypeDefinition = WebTypeRegistry.getWebType(webType);
if (!webTypeDefinition) {
return (
<div className="rounded border border-dashed border-gray-300 bg-gray-50 p-2 text-center">
<span className="text-xs text-gray-500"> </span>
</div>
);
}
const sizeClasses = {
sm: "text-xs p-1",
md: "text-sm p-2",
lg: "text-base p-3",
};
return (
<div className={`rounded-md border ${sizeClasses[size]}`}>
<DynamicWebTypeRenderer
webType={webType}
config={config}
props={{
placeholder: `${webTypeDefinition.name} 미리보기`,
disabled: true,
}}
/>
</div>
);
};
WebTypePreviewRenderer.displayName = "WebTypePreviewRenderer";

View File

@@ -0,0 +1,283 @@
"use client";
import {
WebTypeDefinition,
ButtonActionDefinition,
RegistryEvent,
WebTypeFilterOptions,
ButtonActionFilterOptions,
} from "./types";
/**
* 웹타입 레지스트리 클래스
* 동적으로 웹타입 컴포넌트를 등록, 관리, 조회할 수 있는 중앙 레지스트리
*/
export class WebTypeRegistry {
private static webTypes = new Map<string, WebTypeDefinition>();
private static buttonActions = new Map<string, ButtonActionDefinition>();
private static eventListeners: Array<(event: RegistryEvent) => void> = [];
/**
* 웹타입 등록
*/
static registerWebType(definition: WebTypeDefinition): void {
this.webTypes.set(definition.id, definition);
this.emitEvent({
type: "webtype_registered",
data: definition,
timestamp: new Date(),
});
console.log(`✅ 웹타입 등록: ${definition.id} (${definition.name})`);
}
/**
* 웹타입 등록 해제
*/
static unregisterWebType(id: string): void {
const definition = this.webTypes.get(id);
if (definition) {
this.webTypes.delete(id);
this.emitEvent({
type: "webtype_unregistered",
data: definition,
timestamp: new Date(),
});
console.log(`❌ 웹타입 등록 해제: ${id}`);
}
}
/**
* 웹타입 조회
*/
static getWebType(id: string): WebTypeDefinition | undefined {
return this.webTypes.get(id);
}
/**
* 모든 웹타입 조회
*/
static getAllWebTypes(): WebTypeDefinition[] {
return Array.from(this.webTypes.values());
}
/**
* 웹타입 필터링
*/
static getWebTypes(options: WebTypeFilterOptions = {}): WebTypeDefinition[] {
let webTypes = this.getAllWebTypes();
// 활성화 상태 필터
if (options.isActive !== undefined) {
webTypes = webTypes.filter((wt) => wt.isActive === options.isActive);
}
// 카테고리 필터
if (options.category) {
webTypes = webTypes.filter((wt) => wt.category === options.category);
}
// 검색어 필터
if (options.search) {
const searchLower = options.search.toLowerCase();
webTypes = webTypes.filter(
(wt) =>
wt.name.toLowerCase().includes(searchLower) ||
wt.description.toLowerCase().includes(searchLower) ||
wt.id.toLowerCase().includes(searchLower),
);
}
// 태그 필터
if (options.tags && options.tags.length > 0) {
webTypes = webTypes.filter((wt) => wt.tags && wt.tags.some((tag) => options.tags!.includes(tag)));
}
return webTypes;
}
/**
* 카테고리별 웹타입 그룹화
*/
static getWebTypesByCategory(): Record<string, WebTypeDefinition[]> {
const webTypes = this.getWebTypes({ isActive: true });
const grouped: Record<string, WebTypeDefinition[]> = {};
webTypes.forEach((webType) => {
if (!grouped[webType.category]) {
grouped[webType.category] = [];
}
grouped[webType.category].push(webType);
});
return grouped;
}
/**
* 웹타입 존재 여부 확인
*/
static hasWebType(id: string): boolean {
return this.webTypes.has(id);
}
/**
* 버튼 액션 등록
*/
static registerButtonAction(definition: ButtonActionDefinition): void {
this.buttonActions.set(definition.id, definition);
this.emitEvent({
type: "buttonaction_registered",
data: definition,
timestamp: new Date(),
});
console.log(`✅ 버튼 액션 등록: ${definition.id} (${definition.name})`);
}
/**
* 버튼 액션 등록 해제
*/
static unregisterButtonAction(id: string): void {
const definition = this.buttonActions.get(id);
if (definition) {
this.buttonActions.delete(id);
this.emitEvent({
type: "buttonaction_unregistered",
data: definition,
timestamp: new Date(),
});
console.log(`❌ 버튼 액션 등록 해제: ${id}`);
}
}
/**
* 버튼 액션 조회
*/
static getButtonAction(id: string): ButtonActionDefinition | undefined {
return this.buttonActions.get(id);
}
/**
* 모든 버튼 액션 조회
*/
static getAllButtonActions(): ButtonActionDefinition[] {
return Array.from(this.buttonActions.values());
}
/**
* 버튼 액션 필터링
*/
static getButtonActions(options: ButtonActionFilterOptions = {}): ButtonActionDefinition[] {
let buttonActions = this.getAllButtonActions();
// 활성화 상태 필터
if (options.isActive !== undefined) {
buttonActions = buttonActions.filter((ba) => ba.isActive === options.isActive);
}
// 카테고리 필터
if (options.category) {
buttonActions = buttonActions.filter((ba) => ba.category === options.category);
}
// 검색어 필터
if (options.search) {
const searchLower = options.search.toLowerCase();
buttonActions = buttonActions.filter(
(ba) =>
ba.name.toLowerCase().includes(searchLower) ||
ba.description.toLowerCase().includes(searchLower) ||
ba.id.toLowerCase().includes(searchLower),
);
}
// 확인 필요 여부 필터
if (options.requiresConfirmation !== undefined) {
buttonActions = buttonActions.filter((ba) => ba.requiresConfirmation === options.requiresConfirmation);
}
return buttonActions;
}
/**
* 이벤트 리스너 등록
*/
static subscribe(callback: (event: RegistryEvent) => void): () => void {
this.eventListeners.push(callback);
// 구독 해제 함수 반환
return () => {
const index = this.eventListeners.indexOf(callback);
if (index > -1) {
this.eventListeners.splice(index, 1);
}
};
}
/**
* 이벤트 발생
*/
private static emitEvent(event: RegistryEvent): void {
this.eventListeners.forEach((listener) => {
try {
listener(event);
} catch (error) {
console.error("레지스트리 이벤트 리스너 오류:", error);
}
});
}
/**
* 레지스트리 상태 정보
*/
static getRegistryInfo() {
return {
webTypesCount: this.webTypes.size,
buttonActionsCount: this.buttonActions.size,
activeWebTypesCount: this.getWebTypes({ isActive: true }).length,
activeButtonActionsCount: this.getButtonActions({ isActive: true }).length,
categories: [...new Set(this.getAllWebTypes().map((wt) => wt.category))],
lastUpdated: new Date(),
};
}
/**
* 레지스트리 초기화 (개발/테스트용)
*/
static clear(): void {
this.webTypes.clear();
this.buttonActions.clear();
this.eventListeners.length = 0;
console.log("🧹 레지스트리 초기화됨");
}
/**
* 레지스트리 내용을 JSON으로 내보내기
*/
static exportToJSON() {
return {
webTypes: Object.fromEntries(
Array.from(this.webTypes.entries()).map(([id, def]) => [
id,
{
...def,
// 함수/컴포넌트는 제외하고 메타데이터만 내보내기
component: def.component.name || "Unknown",
configPanel: def.configPanel.name || "Unknown",
},
]),
),
buttonActions: Object.fromEntries(
Array.from(this.buttonActions.entries()).map(([id, def]) => [
id,
{
...def,
// 함수는 제외하고 메타데이터만 내보내기
handler: def.handler.name || "Unknown",
},
]),
),
exportedAt: new Date().toISOString(),
};
}
}

View File

@@ -0,0 +1,37 @@
// Registry system exports
export { WebTypeRegistry } from "./WebTypeRegistry";
export { DynamicWebTypeRenderer, WebTypePreviewRenderer } from "./DynamicWebTypeRenderer";
export { DynamicConfigPanel, renderConfigPanel, hasConfigPanel, getDefaultConfig } from "./DynamicConfigPanel";
// Registry hooks
export {
useRegistry,
useWebTypes,
useButtonActions,
useWebTypesByCategory,
useRegistryInfo,
useWebTypeExists,
} from "./useRegistry";
// Initialization
export { initializeRegistries, initializeWebTypeRegistry } from "./init";
// Type definitions
export type {
WebTypeDefinition,
ButtonActionDefinition,
WebTypeComponentProps,
WebTypeConfigPanelProps,
ButtonActionContext,
DynamicComponentProps,
RegistryEvent,
RegistryEventType,
UseRegistryReturn,
WebTypeFilterOptions,
ButtonActionFilterOptions,
WebTypeCategory,
ButtonActionCategory,
} from "./types";
// Component registry types
export type { WidgetComponent } from "@/types/screen";

View File

@@ -0,0 +1,401 @@
"use client";
import { WebTypeRegistry } from "./WebTypeRegistry";
// 개별적으로 위젯 컴포넌트들을 import
import { TextWidget } from "@/components/screen/widgets/types/TextWidget";
import { NumberWidget } from "@/components/screen/widgets/types/NumberWidget";
import { DateWidget } from "@/components/screen/widgets/types/DateWidget";
import { SelectWidget } from "@/components/screen/widgets/types/SelectWidget";
import { TextareaWidget } from "@/components/screen/widgets/types/TextareaWidget";
import { CheckboxWidget } from "@/components/screen/widgets/types/CheckboxWidget";
import { RadioWidget } from "@/components/screen/widgets/types/RadioWidget";
import { FileWidget } from "@/components/screen/widgets/types/FileWidget";
import { CodeWidget } from "@/components/screen/widgets/types/CodeWidget";
import { EntityWidget } from "@/components/screen/widgets/types/EntityWidget";
import { ButtonWidget } from "@/components/screen/widgets/types/ButtonWidget";
// 개별적으로 설정 패널들을 import
import { TextConfigPanel } from "@/components/screen/config-panels/TextConfigPanel";
import { NumberConfigPanel } from "@/components/screen/config-panels/NumberConfigPanel";
import { DateConfigPanel } from "@/components/screen/config-panels/DateConfigPanel";
import { SelectConfigPanel } from "@/components/screen/config-panels/SelectConfigPanel";
import { TextareaConfigPanel } from "@/components/screen/config-panels/TextareaConfigPanel";
import { CheckboxConfigPanel } from "@/components/screen/config-panels/CheckboxConfigPanel";
import { RadioConfigPanel } from "@/components/screen/config-panels/RadioConfigPanel";
import { FileConfigPanel } from "@/components/screen/config-panels/FileConfigPanel";
import { CodeConfigPanel } from "@/components/screen/config-panels/CodeConfigPanel";
import { EntityConfigPanel } from "@/components/screen/config-panels/EntityConfigPanel";
import { ButtonConfigPanel } from "@/components/screen/config-panels/ButtonConfigPanel";
/**
* 웹타입 레지스트리 초기화
* 모든 기본 웹타입 컴포넌트와 설정 패널을 등록합니다.
*/
export function initializeWebTypeRegistry() {
// Text-based types
WebTypeRegistry.registerWebType({
id: "text",
name: "텍스트",
category: "input",
description: "단일 라인 텍스트 입력 필드",
component: TextWidget,
configPanel: TextConfigPanel,
defaultConfig: {
placeholder: "텍스트를 입력하세요",
maxLength: 255,
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "email",
name: "이메일",
category: "input",
description: "이메일 주소 입력 필드",
component: TextWidget,
configPanel: TextConfigPanel,
defaultConfig: {
placeholder: "이메일을 입력하세요",
inputType: "email",
validation: "email",
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "password",
name: "비밀번호",
category: "input",
description: "비밀번호 입력 필드",
component: TextWidget,
configPanel: TextConfigPanel,
defaultConfig: {
placeholder: "비밀번호를 입력하세요",
inputType: "password",
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "tel",
name: "전화번호",
category: "input",
description: "전화번호 입력 필드",
component: TextWidget,
configPanel: TextConfigPanel,
defaultConfig: {
placeholder: "전화번호를 입력하세요",
inputType: "tel",
required: false,
readonly: false,
},
isActive: true,
});
// Number types
WebTypeRegistry.registerWebType({
id: "number",
name: "숫자",
category: "input",
description: "정수 입력 필드",
component: NumberWidget,
configPanel: NumberConfigPanel,
defaultConfig: {
placeholder: "숫자를 입력하세요",
min: undefined,
max: undefined,
step: 1,
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "decimal",
name: "소수",
category: "input",
description: "소수점 숫자 입력 필드",
component: NumberWidget,
configPanel: NumberConfigPanel,
defaultConfig: {
placeholder: "소수를 입력하세요",
min: undefined,
max: undefined,
step: 0.01,
decimalPlaces: 2,
required: false,
readonly: false,
},
isActive: true,
});
// Date types
WebTypeRegistry.registerWebType({
id: "date",
name: "날짜",
category: "input",
description: "날짜 선택 필드",
component: DateWidget,
configPanel: DateConfigPanel,
defaultConfig: {
format: "YYYY-MM-DD",
showTime: false,
placeholder: "날짜를 선택하세요",
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "datetime",
name: "날짜시간",
category: "input",
description: "날짜와 시간 선택 필드",
component: DateWidget,
configPanel: DateConfigPanel,
defaultConfig: {
format: "YYYY-MM-DD HH:mm",
showTime: true,
placeholder: "날짜와 시간을 선택하세요",
required: false,
readonly: false,
},
isActive: true,
});
// Selection types
WebTypeRegistry.registerWebType({
id: "select",
name: "선택박스",
category: "input",
description: "드롭다운 선택 필드",
component: SelectWidget,
configPanel: SelectConfigPanel,
defaultConfig: {
options: [
{ label: "옵션 1", value: "option1" },
{ label: "옵션 2", value: "option2" },
],
multiple: false,
searchable: false,
placeholder: "선택하세요",
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "dropdown",
name: "드롭다운",
category: "input",
description: "검색 가능한 드롭다운 필드",
component: SelectWidget,
configPanel: SelectConfigPanel,
defaultConfig: {
options: [
{ label: "옵션 1", value: "option1" },
{ label: "옵션 2", value: "option2" },
],
multiple: false,
searchable: true,
placeholder: "검색하여 선택하세요",
required: false,
readonly: false,
},
isActive: true,
});
// Text area
WebTypeRegistry.registerWebType({
id: "textarea",
name: "텍스트영역",
category: "input",
description: "여러 줄 텍스트 입력 필드",
component: TextareaWidget,
configPanel: TextareaConfigPanel,
defaultConfig: {
rows: 4,
placeholder: "내용을 입력하세요",
resizable: true,
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "text_area",
name: "텍스트 영역",
category: "input",
description: "여러 줄 텍스트 입력 필드 (언더스코어 형식)",
component: TextareaWidget,
configPanel: TextareaConfigPanel,
defaultConfig: {
rows: 4,
placeholder: "내용을 입력하세요",
resizable: true,
required: false,
readonly: false,
},
isActive: true,
});
// Boolean/Checkbox types
WebTypeRegistry.registerWebType({
id: "boolean",
name: "불린",
category: "input",
description: "참/거짓 선택 체크박스",
component: CheckboxWidget,
configPanel: CheckboxConfigPanel,
defaultConfig: {
label: "선택",
checkedValue: true,
uncheckedValue: false,
defaultChecked: false,
required: false,
readonly: false,
},
isActive: true,
});
WebTypeRegistry.registerWebType({
id: "checkbox",
name: "체크박스",
category: "input",
description: "체크박스 입력 필드",
component: CheckboxWidget,
configPanel: CheckboxConfigPanel,
defaultConfig: {
label: "체크박스",
checkedValue: "Y",
uncheckedValue: "N",
defaultChecked: false,
required: false,
readonly: false,
},
isActive: true,
});
// Radio button
WebTypeRegistry.registerWebType({
id: "radio",
name: "라디오버튼",
category: "input",
description: "라디오버튼 그룹 선택 필드",
component: RadioWidget,
configPanel: RadioConfigPanel,
defaultConfig: {
options: [
{ label: "옵션 1", value: "option1" },
{ label: "옵션 2", value: "option2" },
],
inline: true,
required: false,
readonly: false,
},
isActive: true,
});
// File upload
WebTypeRegistry.registerWebType({
id: "file",
name: "파일 업로드",
category: "input",
description: "파일 업로드 필드",
component: FileWidget,
configPanel: FileConfigPanel,
defaultConfig: {
multiple: false,
maxFileSize: 10, // MB
acceptedTypes: [],
showPreview: true,
dragAndDrop: true,
required: false,
readonly: false,
},
isActive: true,
});
// Code editor
WebTypeRegistry.registerWebType({
id: "code",
name: "코드 에디터",
category: "input",
description: "코드 편집 필드",
component: CodeWidget,
configPanel: CodeConfigPanel,
defaultConfig: {
language: "javascript",
theme: "light",
showLineNumbers: true,
height: 300,
required: false,
readOnly: false,
},
isActive: true,
});
// Entity selection
WebTypeRegistry.registerWebType({
id: "entity",
name: "엔티티 선택",
category: "input",
description: "데이터베이스 엔티티 선택 필드",
component: EntityWidget,
configPanel: EntityConfigPanel,
defaultConfig: {
entityType: "",
valueField: "id",
labelField: "name",
multiple: false,
searchable: true,
placeholder: "엔티티를 선택하세요",
required: false,
readonly: false,
},
isActive: true,
});
// Button
WebTypeRegistry.registerWebType({
id: "button",
name: "버튼",
category: "action",
description: "클릭 가능한 버튼 컴포넌트",
component: ButtonWidget,
configPanel: ButtonConfigPanel,
defaultConfig: {
label: "버튼",
text: "",
tooltip: "",
variant: "primary",
size: "medium",
disabled: false,
fullWidth: false,
},
isActive: true,
});
console.log("웹타입 레지스트리 초기화 완료:", WebTypeRegistry.getAllWebTypes().length, "개 웹타입 등록됨");
}
/**
* 애플리케이션 시작 시 호출되어야 하는 초기화 함수
*/
export function initializeRegistries() {
initializeWebTypeRegistry();
// 필요한 경우 버튼 액션 레지스트리도 여기서 초기화
// initializeButtonActionRegistry();
}

View File

@@ -0,0 +1,198 @@
import React from "react";
/**
* 웹타입 정의 인터페이스
*/
export interface WebTypeDefinition {
/** 고유 식별자 */
id: string;
/** 표시 이름 */
name: string;
/** 카테고리 (input, display, layout 등) */
category: string;
/** 설명 */
description: string;
/** 렌더링 컴포넌트 */
component: React.ComponentType<any>;
/** 설정 패널 컴포넌트 */
configPanel: React.ComponentType<WebTypeConfigPanelProps>;
/** 기본 설정값 */
defaultConfig: Record<string, any>;
/** 활성화 상태 */
isActive: boolean;
/** 아이콘 (선택사항) */
icon?: React.ComponentType<any>;
/** 태그 (선택사항) */
tags?: string[];
}
/**
* 버튼 액션 정의 인터페이스
*/
export interface ButtonActionDefinition {
/** 고유 식별자 */
id: string;
/** 표시 이름 */
name: string;
/** 카테고리 (save, delete, navigate 등) */
category: string;
/** 설명 */
description: string;
/** 핸들러 함수 */
handler: (context: ButtonActionContext) => void | Promise<void>;
/** 기본 설정값 */
defaultConfig: Record<string, any>;
/** 활성화 상태 */
isActive: boolean;
/** 아이콘 (선택사항) */
icon?: React.ComponentType<any>;
/** 확인 메시지 필요 여부 */
requiresConfirmation?: boolean;
}
/**
* 웹타입 컴포넌트 Props
*/
export interface WebTypeComponentProps {
/** 컴포넌트 객체 */
component: any;
/** 현재 값 */
value?: any;
/** 값 변경 핸들러 */
onChange?: (value: any) => void;
/** 읽기 전용 모드 */
readonly?: boolean;
/** 추가 속성들 */
[key: string]: any;
}
/**
* 웹타입 설정 패널 Props
*/
export interface WebTypeConfigPanelProps {
/** 컴포넌트 객체 */
component: any;
/** 컴포넌트 업데이트 핸들러 */
onUpdateComponent: (component: any) => void;
/** 속성 업데이트 핸들러 */
onUpdateProperty: (property: string, value: any) => void;
}
/**
* 버튼 액션 실행 컨텍스트
*/
export interface ButtonActionContext {
/** 현재 화면 데이터 */
screenData: Record<string, any>;
/** 선택된 데이터 */
selectedData?: Record<string, any>;
/** 화면 설정 */
screenConfig: Record<string, any>;
/** 사용자 정보 */
user: any;
/** 네비게이션 함수 */
navigate: (path: string) => void;
/** 메시지 표시 함수 */
showMessage: (message: string, type?: "success" | "error" | "warning" | "info") => void;
/** API 호출 함수 */
api: {
get: (url: string, params?: any) => Promise<any>;
post: (url: string, data?: any) => Promise<any>;
put: (url: string, data?: any) => Promise<any>;
delete: (url: string) => Promise<any>;
};
}
/**
* 레지스트리 이벤트 타입
*/
export type RegistryEventType =
| "webtype_registered"
| "webtype_unregistered"
| "buttonaction_registered"
| "buttonaction_unregistered";
/**
* 레지스트리 이벤트 인터페이스
*/
export interface RegistryEvent {
type: RegistryEventType;
data: WebTypeDefinition | ButtonActionDefinition;
timestamp: Date;
}
/**
* 웹타입 카테고리
*/
export type WebTypeCategory = "input" | "display" | "layout" | "media" | "data" | "action";
/**
* 버튼 액션 카테고리
*/
export type ButtonActionCategory = "save" | "delete" | "navigate" | "export" | "import" | "custom";
/**
* 동적 컴포넌트 렌더러 Props
*/
export interface DynamicComponentProps {
/** 웹타입 ID */
webType: string;
/** 컴포넌트 속성 */
props: Record<string, any>;
/** 컴포넌트 설정 */
config?: Record<string, any>;
/** 이벤트 핸들러 */
onEvent?: (event: string, data: any) => void;
}
/**
* 레지스트리 훅 반환 타입
*/
export interface UseRegistryReturn {
/** 등록된 웹타입 목록 */
webTypes: WebTypeDefinition[];
/** 등록된 버튼 액션 목록 */
buttonActions: ButtonActionDefinition[];
/** 웹타입 등록 */
registerWebType: (definition: WebTypeDefinition) => void;
/** 웹타입 등록 해제 */
unregisterWebType: (id: string) => void;
/** 버튼 액션 등록 */
registerButtonAction: (definition: ButtonActionDefinition) => void;
/** 버튼 액션 등록 해제 */
unregisterButtonAction: (id: string) => void;
/** 웹타입 조회 */
getWebType: (id: string) => WebTypeDefinition | undefined;
/** 버튼 액션 조회 */
getButtonAction: (id: string) => ButtonActionDefinition | undefined;
/** 이벤트 구독 */
subscribe: (callback: (event: RegistryEvent) => void) => () => void;
}
/**
* 웹타입 필터 옵션
*/
export interface WebTypeFilterOptions {
/** 카테고리 필터 */
category?: WebTypeCategory;
/** 활성화 상태 필터 */
isActive?: boolean;
/** 검색어 */
search?: string;
/** 태그 필터 */
tags?: string[];
}
/**
* 버튼 액션 필터 옵션
*/
export interface ButtonActionFilterOptions {
/** 카테고리 필터 */
category?: ButtonActionCategory;
/** 활성화 상태 필터 */
isActive?: boolean;
/** 검색어 */
search?: string;
/** 확인 필요 여부 필터 */
requiresConfirmation?: boolean;
}

View File

@@ -0,0 +1,221 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import { WebTypeRegistry } from "./WebTypeRegistry";
import {
WebTypeDefinition,
ButtonActionDefinition,
UseRegistryReturn,
WebTypeFilterOptions,
ButtonActionFilterOptions,
} from "./types";
/**
* 레지스트리 관리를 위한 React 훅
*/
export function useRegistry(): UseRegistryReturn {
const [webTypes, setWebTypes] = useState<WebTypeDefinition[]>([]);
const [buttonActions, setButtonActions] = useState<ButtonActionDefinition[]>([]);
// 웹타입 목록 업데이트
const updateWebTypes = useCallback(() => {
setWebTypes(WebTypeRegistry.getAllWebTypes());
}, []);
// 버튼 액션 목록 업데이트
const updateButtonActions = useCallback(() => {
setButtonActions(WebTypeRegistry.getAllButtonActions());
}, []);
// 레지스트리 이벤트 구독
useEffect(() => {
// 초기 데이터 로드
updateWebTypes();
updateButtonActions();
// 이벤트 리스너 등록
const unsubscribe = WebTypeRegistry.subscribe((event) => {
if (event.type === "webtype_registered" || event.type === "webtype_unregistered") {
updateWebTypes();
} else if (event.type === "buttonaction_registered" || event.type === "buttonaction_unregistered") {
updateButtonActions();
}
});
return unsubscribe;
}, [updateWebTypes, updateButtonActions]);
// 웹타입 등록
const registerWebType = useCallback((definition: WebTypeDefinition) => {
WebTypeRegistry.registerWebType(definition);
}, []);
// 웹타입 등록 해제
const unregisterWebType = useCallback((id: string) => {
WebTypeRegistry.unregisterWebType(id);
}, []);
// 버튼 액션 등록
const registerButtonAction = useCallback((definition: ButtonActionDefinition) => {
WebTypeRegistry.registerButtonAction(definition);
}, []);
// 버튼 액션 등록 해제
const unregisterButtonAction = useCallback((id: string) => {
WebTypeRegistry.unregisterButtonAction(id);
}, []);
// 웹타입 조회
const getWebType = useCallback((id: string) => {
return WebTypeRegistry.getWebType(id);
}, []);
// 버튼 액션 조회
const getButtonAction = useCallback((id: string) => {
return WebTypeRegistry.getButtonAction(id);
}, []);
// 이벤트 구독
const subscribe = useCallback((callback: (event: any) => void) => {
return WebTypeRegistry.subscribe(callback);
}, []);
return {
webTypes,
buttonActions,
registerWebType,
unregisterWebType,
registerButtonAction,
unregisterButtonAction,
getWebType,
getButtonAction,
subscribe,
};
}
/**
* 필터링된 웹타입을 가져오는 훅
*/
export function useWebTypes(options: WebTypeFilterOptions = {}) {
const [webTypes, setWebTypes] = useState<WebTypeDefinition[]>([]);
const filteredWebTypes = useMemo(() => {
return WebTypeRegistry.getWebTypes(options);
}, [options]);
useEffect(() => {
const currentWebTypes = WebTypeRegistry.getWebTypes(options);
setWebTypes(currentWebTypes);
// 레지스트리 변경 감지
const unsubscribe = WebTypeRegistry.subscribe((event) => {
if (event.type === "webtype_registered" || event.type === "webtype_unregistered") {
setWebTypes(WebTypeRegistry.getWebTypes(options));
}
});
return unsubscribe;
}, [options]);
return webTypes;
}
/**
* 필터링된 버튼 액션을 가져오는 훅
*/
export function useButtonActions(options: ButtonActionFilterOptions = {}) {
const [buttonActions, setButtonActions] = useState<ButtonActionDefinition[]>([]);
const filteredButtonActions = useMemo(() => {
return WebTypeRegistry.getButtonActions(options);
}, [options]);
useEffect(() => {
setButtonActions(filteredButtonActions);
// 레지스트리 변경 감지
const unsubscribe = WebTypeRegistry.subscribe((event) => {
if (event.type === "buttonaction_registered" || event.type === "buttonaction_unregistered") {
setButtonActions(WebTypeRegistry.getButtonActions(options));
}
});
return unsubscribe;
}, [filteredButtonActions, options]);
return buttonActions;
}
/**
* 웹타입별로 그룹화된 데이터를 가져오는 훅
*/
export function useWebTypesByCategory() {
const [groupedWebTypes, setGroupedWebTypes] = useState<Record<string, WebTypeDefinition[]>>({});
useEffect(() => {
const updateGroupedWebTypes = () => {
setGroupedWebTypes(WebTypeRegistry.getWebTypesByCategory());
};
updateGroupedWebTypes();
// 레지스트리 변경 감지
const unsubscribe = WebTypeRegistry.subscribe((event) => {
if (event.type === "webtype_registered" || event.type === "webtype_unregistered") {
updateGroupedWebTypes();
}
});
return unsubscribe;
}, []);
return groupedWebTypes;
}
/**
* 레지스트리 상태 정보를 가져오는 훅
*/
export function useRegistryInfo() {
const [registryInfo, setRegistryInfo] = useState(WebTypeRegistry.getRegistryInfo());
useEffect(() => {
const updateRegistryInfo = () => {
setRegistryInfo(WebTypeRegistry.getRegistryInfo());
};
// 레지스트리 변경 감지
const unsubscribe = WebTypeRegistry.subscribe(() => {
updateRegistryInfo();
});
return unsubscribe;
}, []);
return registryInfo;
}
/**
* 특정 웹타입의 존재 여부를 확인하는 훅
*/
export function useWebTypeExists(webTypeId: string) {
const [exists, setExists] = useState(false);
useEffect(() => {
const checkExists = () => {
setExists(WebTypeRegistry.hasWebType(webTypeId));
};
checkExists();
// 레지스트리 변경 감지
const unsubscribe = WebTypeRegistry.subscribe((event) => {
if (event.type === "webtype_registered" || event.type === "webtype_unregistered") {
checkExists();
}
});
return unsubscribe;
}, [webTypeId]);
return exists;
}