2025-09-11 18:38:28 +09:00
|
|
|
"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(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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<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 () => {
|
|
|
|
|
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 {
|
2025-09-18 10:05:50 +09:00
|
|
|
// hotReload 모듈이 존재하는 경우에만 실행
|
|
|
|
|
const hotReload = await import("../utils/hotReload").catch(() => null);
|
|
|
|
|
if (hotReload) {
|
|
|
|
|
hotReload.forceReloadComponents();
|
|
|
|
|
console.log("✅ 강제 Hot Reload 실행 완료");
|
|
|
|
|
} else {
|
|
|
|
|
console.log("⚠️ hotReload 모듈이 없어 건너뜀");
|
|
|
|
|
}
|
2025-09-11 18:38:28 +09:00
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
}
|