2026-02-02 15:15:01 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* POP 컴포넌트 정의 인터페이스
|
|
|
|
|
*/
|
|
|
|
|
export interface PopComponentDefinition {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description: string;
|
|
|
|
|
category: PopComponentCategory;
|
|
|
|
|
icon?: string;
|
|
|
|
|
component: React.ComponentType<any>;
|
|
|
|
|
configPanel?: React.ComponentType<any>;
|
2026-02-06 17:07:56 +09:00
|
|
|
preview?: React.ComponentType<{ config?: any }>; // 디자이너 미리보기용
|
2026-02-02 15:15:01 +09:00
|
|
|
defaultProps?: Record<string, any>;
|
|
|
|
|
// POP 전용 속성
|
|
|
|
|
touchOptimized?: boolean;
|
|
|
|
|
minTouchArea?: number;
|
|
|
|
|
supportedDevices?: ("mobile" | "tablet")[];
|
|
|
|
|
createdAt?: Date;
|
|
|
|
|
updatedAt?: Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* POP 컴포넌트 카테고리
|
|
|
|
|
*/
|
|
|
|
|
export type PopComponentCategory =
|
|
|
|
|
| "display" // 데이터 표시 (카드, 리스트, 배지)
|
|
|
|
|
| "input" // 입력 (스캐너, 터치 입력)
|
|
|
|
|
| "action" // 액션 (버튼, 스와이프)
|
|
|
|
|
| "layout" // 레이아웃 (컨테이너, 그리드)
|
|
|
|
|
| "feedback"; // 피드백 (토스트, 로딩)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* POP 컴포넌트 레지스트리 이벤트
|
|
|
|
|
*/
|
|
|
|
|
export interface PopComponentRegistryEvent {
|
|
|
|
|
type: "component_registered" | "component_unregistered";
|
|
|
|
|
data: PopComponentDefinition;
|
|
|
|
|
timestamp: Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* POP 컴포넌트 레지스트리 클래스
|
|
|
|
|
* 모바일/태블릿 전용 컴포넌트를 등록, 관리, 조회할 수 있는 중앙 레지스트리
|
|
|
|
|
*/
|
|
|
|
|
export class PopComponentRegistry {
|
|
|
|
|
private static components = new Map<string, PopComponentDefinition>();
|
|
|
|
|
private static eventListeners: Array<(event: PopComponentRegistryEvent) => void> = [];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컴포넌트 등록
|
|
|
|
|
*/
|
|
|
|
|
static registerComponent(definition: PopComponentDefinition): void {
|
|
|
|
|
// 유효성 검사
|
|
|
|
|
if (!definition.id || !definition.name || !definition.component) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`POP 컴포넌트 등록 실패 (${definition.id || "unknown"}): 필수 필드 누락`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 중복 등록 체크
|
|
|
|
|
if (this.components.has(definition.id)) {
|
|
|
|
|
console.warn(`[POP Registry] 컴포넌트 중복 등록: ${definition.id} - 기존 정의를 덮어씁니다.`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 타임스탬프 추가
|
|
|
|
|
const enhancedDefinition: PopComponentDefinition = {
|
|
|
|
|
...definition,
|
|
|
|
|
touchOptimized: definition.touchOptimized ?? true,
|
|
|
|
|
minTouchArea: definition.minTouchArea ?? 44,
|
|
|
|
|
supportedDevices: definition.supportedDevices ?? ["mobile", "tablet"],
|
|
|
|
|
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") {
|
|
|
|
|
console.log(`[POP Registry] 컴포넌트 등록: ${definition.id}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컴포넌트 등록 해제
|
|
|
|
|
*/
|
|
|
|
|
static unregisterComponent(id: string): void {
|
|
|
|
|
const definition = this.components.get(id);
|
|
|
|
|
if (!definition) {
|
|
|
|
|
console.warn(`[POP Registry] 등록되지 않은 컴포넌트 해제 시도: ${id}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.components.delete(id);
|
|
|
|
|
|
|
|
|
|
// 이벤트 발생
|
|
|
|
|
this.emitEvent({
|
|
|
|
|
type: "component_unregistered",
|
|
|
|
|
data: definition,
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`[POP Registry] 컴포넌트 해제: ${id}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 컴포넌트 조회
|
|
|
|
|
*/
|
|
|
|
|
static getComponent(id: string): PopComponentDefinition | undefined {
|
|
|
|
|
return this.components.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* URL로 컴포넌트 조회
|
|
|
|
|
*/
|
|
|
|
|
static getComponentByUrl(url: string): PopComponentDefinition | undefined {
|
|
|
|
|
// "@/lib/registry/pop-components/pop-card-list" → "pop-card-list"
|
|
|
|
|
const parts = url.split("/");
|
|
|
|
|
const componentId = parts[parts.length - 1];
|
|
|
|
|
return this.getComponent(componentId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 모든 컴포넌트 조회
|
|
|
|
|
*/
|
|
|
|
|
static getAllComponents(): PopComponentDefinition[] {
|
|
|
|
|
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 getComponentsByCategory(category: PopComponentCategory): PopComponentDefinition[] {
|
|
|
|
|
return Array.from(this.components.values())
|
|
|
|
|
.filter((def) => def.category === category)
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 디바이스별 컴포넌트 조회
|
|
|
|
|
*/
|
|
|
|
|
static getComponentsByDevice(device: "mobile" | "tablet"): PopComponentDefinition[] {
|
|
|
|
|
return Array.from(this.components.values())
|
|
|
|
|
.filter((def) => def.supportedDevices?.includes(device))
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컴포넌트 검색
|
|
|
|
|
*/
|
|
|
|
|
static searchComponents(query: string): PopComponentDefinition[] {
|
|
|
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
|
return Array.from(this.components.values()).filter(
|
|
|
|
|
(def) =>
|
|
|
|
|
def.id.toLowerCase().includes(lowerQuery) ||
|
|
|
|
|
def.name.toLowerCase().includes(lowerQuery) ||
|
|
|
|
|
def.description?.toLowerCase().includes(lowerQuery)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 등록된 컴포넌트 개수
|
|
|
|
|
*/
|
|
|
|
|
static getComponentCount(): number {
|
|
|
|
|
return this.components.size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 카테고리별 통계
|
|
|
|
|
*/
|
|
|
|
|
static getStatsByCategory(): Record<PopComponentCategory, number> {
|
|
|
|
|
const stats: Record<PopComponentCategory, number> = {
|
|
|
|
|
display: 0,
|
|
|
|
|
input: 0,
|
|
|
|
|
action: 0,
|
|
|
|
|
layout: 0,
|
|
|
|
|
feedback: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const def of this.components.values()) {
|
|
|
|
|
stats[def.category]++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return stats;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 이벤트 리스너 등록
|
|
|
|
|
*/
|
|
|
|
|
static addEventListener(callback: (event: PopComponentRegistryEvent) => void): void {
|
|
|
|
|
this.eventListeners.push(callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 이벤트 리스너 해제
|
|
|
|
|
*/
|
|
|
|
|
static removeEventListener(callback: (event: PopComponentRegistryEvent) => void): void {
|
|
|
|
|
const index = this.eventListeners.indexOf(callback);
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
this.eventListeners.splice(index, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 이벤트 발생
|
|
|
|
|
*/
|
|
|
|
|
private static emitEvent(event: PopComponentRegistryEvent): void {
|
|
|
|
|
for (const listener of this.eventListeners) {
|
|
|
|
|
try {
|
|
|
|
|
listener(event);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[POP Registry] 이벤트 리스너 오류:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 레지스트리 초기화 (테스트용)
|
|
|
|
|
*/
|
|
|
|
|
static clear(): void {
|
|
|
|
|
this.components.clear();
|
|
|
|
|
console.log("[POP Registry] 레지스트리 초기화됨");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컴포넌트 존재 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
static hasComponent(id: string): boolean {
|
|
|
|
|
return this.components.has(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 디버그 정보 출력
|
|
|
|
|
*/
|
|
|
|
|
static debug(): void {
|
|
|
|
|
console.group("[POP Registry] 등록된 컴포넌트");
|
|
|
|
|
console.log(`총 ${this.components.size}개 컴포넌트`);
|
|
|
|
|
console.table(
|
|
|
|
|
Array.from(this.components.values()).map((c) => ({
|
|
|
|
|
id: c.id,
|
|
|
|
|
name: c.name,
|
|
|
|
|
category: c.category,
|
|
|
|
|
touchOptimized: c.touchOptimized,
|
|
|
|
|
devices: c.supportedDevices?.join(", "),
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
console.groupEnd();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본 export
|
|
|
|
|
export default PopComponentRegistry;
|