"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(); 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(), }); console.log(`✅ 컴포넌트 등록: ${definition.id} (${definition.name})`); // 개발자 도구 등록 (개발 모드에서만) 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(); const webTypeMap = new Map(); const authorMap = new Map(); 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 () => { try { const hotReload = await import("../utils/hotReload"); return { active: hotReload.isHotReloadActive(), componentCount: this.getComponentCount(), timestamp: new Date(), }; } catch (error) { console.warn("Hot Reload 모듈 로드 실패:", error); return { active: false, componentCount: this.getComponentCount(), timestamp: new Date(), error: "Hot Reload 모듈을 로드할 수 없습니다", }; } }, force: async () => { try { // hotReload 모듈이 존재하는 경우에만 실행 const hotReload = await import("../utils/hotReload").catch(() => null); if (hotReload) { hotReload.forceReloadComponents(); console.log("✅ 강제 Hot Reload 실행 완료"); } else { console.log("⚠️ hotReload 모듈이 없어 건너뜀"); } } catch (error) { console.error("❌ 강제 Hot Reload 실행 실패:", error); } }, }, // 도움말 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") `); }, }; console.log("🛠️ 컴포넌트 레지스트리 개발자 도구가 등록되었습니다."); console.log(" 사용법: __COMPONENT_REGISTRY__.help()"); } } /** * 디버그 정보 출력 */ static debug(): void { const stats = this.getStats(); console.group("🎨 컴포넌트 레지스트리 디버그 정보"); console.log("📊 총 컴포넌트 수:", stats.total); console.log("📂 카테고리별 분포:", stats.byCategory); console.log("🏷️ 웹타입별 분포:", stats.byWebType); console.log("👨‍💻 작성자별 분포:", stats.byAuthor); console.log( "🆕 최근 추가:", stats.recentlyAdded.map((c) => `${c.id} (${c.name})`), ); console.groupEnd(); } /** * 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); } }