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

290 lines
7.8 KiB
TypeScript
Raw Normal View History

"use client";
import React from "react";
/**
* 항목: 컴포넌트가
*/
export interface ConnectionMetaItem {
key: string;
label: string;
type: "filter_value" | "selected_row" | "action_trigger" | "data_refresh" | string;
category?: "event" | "filter" | "data";
description?: string;
}
/**
* 메타데이터: 디자이너가
*/
export interface ComponentConnectionMeta {
sendable: ConnectionMetaItem[];
receivable: ConnectionMetaItem[];
}
/**
* POP
*/
export interface PopComponentDefinition {
id: string;
name: string;
description: string;
category: PopComponentCategory;
icon?: string;
component: React.ComponentType<any>;
configPanel?: React.ComponentType<any>;
preview?: React.ComponentType<{ config?: any }>; // 디자이너 미리보기용
defaultProps?: Record<string, any>;
connectionMeta?: ComponentConnectionMeta;
feat(pop-scanner): 바코드/QR 스캐너 컴포넌트 + 멀티필드 파싱 + 반자동 매핑 모바일/태블릿 환경에서 바코드·QR을 카메라로 스캔하여 검색·입력 필드에 값을 자동 전달하는 pop-scanner 컴포넌트를 추가하고, JSON 형태의 멀티필드 데이터를 여러 컴포넌트에 분배하는 파싱 체계를 구현한다. [pop-scanner 신규] - 카메라 스캔 UI (BarcodeScanModal) + 아이콘 전용 버튼 - parseMode 3모드: none(단일값), auto(전역 자동매칭), json(반자동 매핑) - auto: scan_auto_fill 전역 이벤트로 fieldName 기준 자동 입력 - json: 연결된 컴포넌트 필드를 체크박스 목록으로 표시, fieldName==JSON키 자동 매칭 + 관리자 override(enabled/sourceKey) - getDynamicConnectionMeta로 parseMode별 sendable 동적 생성 [pop-field 연동] - scan_auto_fill 구독: sections.fields의 fieldName과 JSON 키 매칭 - columnMapping 키를 fieldName 기준으로 통일 (fieldId→fieldName) - targetColumn 선택 시 fieldName 자동 동기화 - 새 필드 fieldName 기본값을 빈 문자열로 변경 [pop-search 연동] - scan_auto_fill 구독: filterColumns 전체 키 매칭 - set_value 수신 시 모달 타입이면 modalDisplayText도 갱신 [BarcodeScanModal 개선] - 모달 열릴 때 상태 리셋 (scannedCode/error/isScanning) - "다시 스캔" 버튼 추가 - 스캔 가이드 영역 확대 (h-3/5 w-4/5) [getConnectedFields 필드 추출] - filterColumns(복수) > modalConfig.valueField > fieldName 우선순위 - pop-field sections.fields[].fieldName 추출
2026-03-06 19:52:18 +09:00
getDynamicConnectionMeta?: (config: Record<string, unknown>) => ComponentConnectionMeta;
// 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;