ERP-node/frontend/lib/registry/ComponentRegistry.ts

432 lines
13 KiB
TypeScript

"use client";
import React from "react";
import {
ComponentDefinition,
ComponentCategory,
ComponentRegistryEvent,
ComponentSearchOptions,
ComponentStats,
ComponentAutoDiscoveryOptions,
ComponentDiscoveryResult,
} from "@/types/component";
import type { WebType } from "@/types/screen";
/**
* 컴포넌트 레지스트리 클래스
* 동적으로 컴포넌트를 등록, 관리, 조회할 수 있는 중앙 레지스트리
* 레이아웃 시스템과 동일한 패턴으로 설계
*/
export class ComponentRegistry {
private static components = new Map<string, ComponentDefinition>();
private static eventListeners: Array<(event: ComponentRegistryEvent) => void> = [];
/**
* 컴포넌트 등록
*/
static registerComponent(definition: ComponentDefinition): void {
// 유효성 검사
const validation = this.validateComponentDefinition(definition);
if (!validation.isValid) {
throw new Error(`컴포넌트 등록 실패 (${definition.id}): ${validation.errors.join(", ")}`);
}
// 중복 등록 체크
if (this.components.has(definition.id)) {
console.warn(`⚠️ 컴포넌트 중복 등록: ${definition.id} - 기존 정의를 덮어씁니다.`);
}
// 타임스탬프 추가
const enhancedDefinition = {
...definition,
createdAt: definition.createdAt || new Date(),
updatedAt: new Date(),
};
this.components.set(definition.id, enhancedDefinition);
// 이벤트 발생
this.emitEvent({
type: "component_registered",
data: enhancedDefinition,
timestamp: new Date(),
});
// 개발자 도구 등록 (개발 모드에서만)
if (process.env.NODE_ENV === "development") {
this.registerGlobalDevTools();
}
}
/**
* 컴포넌트 등록 해제
*/
static unregisterComponent(id: string): void {
const definition = this.components.get(id);
if (!definition) {
console.warn(`⚠️ 등록되지 않은 컴포넌트 해제 시도: ${id}`);
return;
}
this.components.delete(id);
// 이벤트 발생
this.emitEvent({
type: "component_unregistered",
data: definition,
timestamp: new Date(),
});
console.log(`🗑️ 컴포넌트 해제: ${id}`);
}
/**
* 특정 컴포넌트 조회
*/
static getComponent(id: string): ComponentDefinition | undefined {
return this.components.get(id);
}
/**
* 모든 컴포넌트 조회
*/
static getAllComponents(): ComponentDefinition[] {
return Array.from(this.components.values()).sort((a, b) => {
// 카테고리별 정렬, 그 다음 이름순
if (a.category !== b.category) {
return a.category.localeCompare(b.category);
}
return a.name.localeCompare(b.name);
});
}
/**
* 카테고리별 컴포넌트 조회
*/
static getByCategory(category: ComponentCategory): ComponentDefinition[] {
return this.getAllComponents().filter((comp) => comp.category === category);
}
/**
* 웹타입별 컴포넌트 조회
*/
static getByWebType(webType: WebType): ComponentDefinition[] {
return this.getAllComponents().filter((comp) => comp.webType === webType);
}
/**
* 컴포넌트 검색
*/
static search(options: ComponentSearchOptions = {}): ComponentDefinition[] {
let results = this.getAllComponents();
// 검색어 필터
if (options.query) {
const lowercaseQuery = options.query.toLowerCase();
results = results.filter(
(comp) =>
comp.name.toLowerCase().includes(lowercaseQuery) ||
comp.nameEng?.toLowerCase().includes(lowercaseQuery) ||
comp.description.toLowerCase().includes(lowercaseQuery) ||
comp.tags?.some((tag) => tag.toLowerCase().includes(lowercaseQuery)) ||
comp.id.toLowerCase().includes(lowercaseQuery),
);
}
// 카테고리 필터
if (options.category) {
results = results.filter((comp) => comp.category === options.category);
}
// 웹타입 필터
if (options.webType) {
results = results.filter((comp) => comp.webType === options.webType);
}
// 태그 필터
if (options.tags && options.tags.length > 0) {
results = results.filter((comp) => comp.tags?.some((tag) => options.tags!.includes(tag)));
}
// 작성자 필터
if (options.author) {
results = results.filter((comp) => comp.author === options.author);
}
// 페이징
if (options.offset !== undefined || options.limit !== undefined) {
const start = options.offset || 0;
const end = options.limit ? start + options.limit : undefined;
results = results.slice(start, end);
}
return results;
}
/**
* 컴포넌트 존재 여부 확인
*/
static hasComponent(id: string): boolean {
return this.components.has(id);
}
/**
* 컴포넌트 수 조회
*/
static getComponentCount(): number {
return this.components.size;
}
/**
* 통계 정보 조회
*/
static getStats(): ComponentStats {
const components = this.getAllComponents();
// 카테고리별 통계
const categoryMap = new Map<ComponentCategory, number>();
const webTypeMap = new Map<WebType, number>();
const authorMap = new Map<string, number>();
components.forEach((comp) => {
// 카테고리별 집계
categoryMap.set(comp.category, (categoryMap.get(comp.category) || 0) + 1);
// 웹타입별 집계
webTypeMap.set(comp.webType, (webTypeMap.get(comp.webType) || 0) + 1);
// 작성자별 집계
if (comp.author) {
authorMap.set(comp.author, (authorMap.get(comp.author) || 0) + 1);
}
});
// 최근 추가된 컴포넌트 (7개)
const recentlyAdded = components
.filter((comp) => comp.createdAt)
.sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime())
.slice(0, 7);
return {
total: components.length,
byCategory: Array.from(categoryMap.entries()).map(([category, count]) => ({
category,
count,
})),
byWebType: Array.from(webTypeMap.entries()).map(([webType, count]) => ({
webType,
count,
})),
byAuthor: Array.from(authorMap.entries()).map(([author, count]) => ({
author,
count,
})),
recentlyAdded,
};
}
/**
* 컴포넌트 정의 유효성 검사
*/
private static validateComponentDefinition(definition: ComponentDefinition): {
isValid: boolean;
errors: string[];
warnings: string[];
} {
const errors: string[] = [];
const warnings: string[] = [];
// 필수 필드 검사
if (!definition.id) errors.push("id는 필수입니다");
if (!definition.name) errors.push("name은 필수입니다");
if (!definition.description) errors.push("description은 필수입니다");
if (!definition.category) errors.push("category는 필수입니다");
if (!definition.webType) errors.push("webType은 필수입니다");
if (!definition.component) errors.push("component는 필수입니다");
if (!definition.defaultSize) errors.push("defaultSize는 필수입니다");
// ID 형식 검사 (kebab-case)
if (definition.id && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(definition.id)) {
errors.push("id는 kebab-case 형식이어야 합니다 (예: button-primary)");
}
// 카테고리 유효성 검사
if (definition.category && !Object.values(ComponentCategory).includes(definition.category)) {
errors.push(`유효하지 않은 카테고리: ${definition.category}`);
}
// 크기 유효성 검사
if (definition.defaultSize) {
if (definition.defaultSize.width <= 0) {
errors.push("defaultSize.width는 0보다 커야 합니다");
}
if (definition.defaultSize.height <= 0) {
errors.push("defaultSize.height는 0보다 커야 합니다");
}
}
// 경고: 권장사항 검사
if (!definition.icon) warnings.push("아이콘이 설정되지 않았습니다");
if (!definition.tags || definition.tags.length === 0) {
warnings.push("검색을 위한 태그가 설정되지 않았습니다");
}
if (!definition.author) warnings.push("작성자가 설정되지 않았습니다");
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* 이벤트 리스너 등록
*/
static addEventListener(listener: (event: ComponentRegistryEvent) => void): void {
this.eventListeners.push(listener);
}
/**
* 이벤트 리스너 제거
*/
static removeEventListener(listener: (event: ComponentRegistryEvent) => void): void {
const index = this.eventListeners.indexOf(listener);
if (index !== -1) {
this.eventListeners.splice(index, 1);
}
}
/**
* 이벤트 발생
*/
private static emitEvent(event: ComponentRegistryEvent): void {
this.eventListeners.forEach((listener) => {
try {
listener(event);
} catch (error) {
console.error("컴포넌트 레지스트리 이벤트 리스너 오류:", error);
}
});
}
/**
* 레지스트리 초기화 (테스트용)
*/
static clear(): void {
this.components.clear();
this.eventListeners.length = 0;
}
/**
* 브라우저 개발자 도구 등록
*/
private static registerGlobalDevTools(): void {
if (typeof window !== "undefined") {
(window as any).__COMPONENT_REGISTRY__ = {
// 기본 조회 기능
list: () => this.getAllComponents(),
get: (id: string) => this.getComponent(id),
has: (id: string) => this.hasComponent(id),
count: () => this.getComponentCount(),
// 검색 및 필터링
search: (query: string) => this.search({ query }),
byCategory: (category: ComponentCategory) => this.getByCategory(category),
byWebType: (webType: WebType) => this.getByWebType(webType),
// 통계 및 분석
stats: () => this.getStats(),
categories: () => Object.values(ComponentCategory),
webTypes: () => Object.values(WebType),
// 개발자 유틸리티
validate: (definition: ComponentDefinition) => this.validateComponentDefinition(definition),
clear: () => this.clear(),
// Hot Reload 제어
hotReload: {
status: async () => {
// hotReload 기능 제거 (불필요)
return {
active: false,
componentCount: this.getComponentCount(),
timestamp: new Date(),
};
},
force: async () => {
// hotReload 기능 비활성화 (불필요)
console.log("⚠️ 강제 Hot Reload는 더 이상 필요하지 않습니다");
},
},
// 도움말
help: () => {
console.log(`
🎨 컴포넌트 레지스트리 개발자 도구
기본 명령어:
__COMPONENT_REGISTRY__.list() - 모든 컴포넌트 목록
__COMPONENT_REGISTRY__.get("button-primary") - 특정 컴포넌트 조회
__COMPONENT_REGISTRY__.count() - 등록된 컴포넌트 수
검색 및 필터링:
__COMPONENT_REGISTRY__.search("버튼") - 컴포넌트 검색
__COMPONENT_REGISTRY__.byCategory("input") - 카테고리별 조회
__COMPONENT_REGISTRY__.byWebType("button") - 웹타입별 조회
통계 및 분석:
__COMPONENT_REGISTRY__.stats() - 통계 정보
__COMPONENT_REGISTRY__.categories() - 사용 가능한 카테고리
__COMPONENT_REGISTRY__.webTypes() - 사용 가능한 웹타입
Hot Reload 제어 (비동기):
await __COMPONENT_REGISTRY__.hotReload.status() - Hot Reload 상태 확인
await __COMPONENT_REGISTRY__.hotReload.force() - 강제 컴포넌트 재로드
개발자 도구:
__COMPONENT_REGISTRY__.validate(def) - 컴포넌트 정의 검증
__COMPONENT_REGISTRY__.clear() - 레지스트리 초기화
__COMPONENT_REGISTRY__.debug() - 디버그 정보 출력
__COMPONENT_REGISTRY__.export() - JSON으로 내보내기
__COMPONENT_REGISTRY__.help() - 이 도움말
💡 사용 예시:
__COMPONENT_REGISTRY__.search("input")
__COMPONENT_REGISTRY__.byCategory("input")
__COMPONENT_REGISTRY__.get("text-input")
`);
},
};
}
}
/**
* 디버그 정보 출력
*/
static debug(): void {
// 디버그 로그 제거 (필요시 브라우저 콘솔에서 ComponentRegistry.getStats() 사용)
}
/**
* JSON으로 내보내기
*/
static export(): string {
const data = {
timestamp: new Date().toISOString(),
version: "1.0.0",
components: Array.from(this.components.entries()).map(([id, definition]) => ({
id,
definition: {
...definition,
// React 컴포넌트는 직렬화할 수 없으므로 제외
component: definition.component.name,
renderer: definition.renderer?.name,
configPanel: definition.configPanel?.name,
},
})),
};
return JSON.stringify(data, null, 2);
}
}