모바일/태블릿 환경에서 바코드·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 추출
290 lines
7.8 KiB
TypeScript
290 lines
7.8 KiB
TypeScript
"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;
|
|
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;
|