204 lines
5.3 KiB
TypeScript
204 lines
5.3 KiB
TypeScript
|
|
/**
|
||
|
|
* Reactive Binding Engine
|
||
|
|
* - 컴포넌트 간 동적 연동 관리
|
||
|
|
* - 이벤트 → 액션 실행 파이프라인
|
||
|
|
* - Phase A: 기본 구조만 구현 (실제 실행은 Phase B)
|
||
|
|
*/
|
||
|
|
|
||
|
|
"use client";
|
||
|
|
|
||
|
|
import { ReactiveBinding } from "@/lib/api/metaComponent";
|
||
|
|
|
||
|
|
export class ReactiveBindingEngine {
|
||
|
|
private bindings: ReactiveBinding[] = [];
|
||
|
|
private componentRegistry: Map<string, any> = new Map();
|
||
|
|
private eventHandlers: Map<string, Set<(event: any) => void>> = new Map();
|
||
|
|
|
||
|
|
constructor(bindings: ReactiveBinding[] = []) {
|
||
|
|
this.bindings = bindings.sort((a, b) => (a.priority || 100) - (b.priority || 100));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 컴포넌트 등록
|
||
|
|
* - 바인딩 대상 컴포넌트를 등록
|
||
|
|
*/
|
||
|
|
registerComponent(componentId: string, component: any) {
|
||
|
|
this.componentRegistry.set(componentId, component);
|
||
|
|
console.log(`[ReactiveBinding] 컴포넌트 등록: ${componentId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 컴포넌트 등록 해제
|
||
|
|
*/
|
||
|
|
unregisterComponent(componentId: string) {
|
||
|
|
this.componentRegistry.delete(componentId);
|
||
|
|
this.eventHandlers.delete(componentId);
|
||
|
|
console.log(`[ReactiveBinding] 컴포넌트 등록 해제: ${componentId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 이벤트 발행
|
||
|
|
* - 특정 컴포넌트에서 이벤트 발생 시 호출
|
||
|
|
*/
|
||
|
|
emit(sourceComponentId: string, event: string, data?: any) {
|
||
|
|
console.log(`[ReactiveBinding] 이벤트 발행: ${sourceComponentId}.${event}`, data);
|
||
|
|
|
||
|
|
// 해당 이벤트에 연결된 바인딩 찾기
|
||
|
|
const matchedBindings = this.bindings.filter(
|
||
|
|
(binding) =>
|
||
|
|
binding.sourceComponentId === sourceComponentId &&
|
||
|
|
binding.sourceEvent === event
|
||
|
|
);
|
||
|
|
|
||
|
|
if (matchedBindings.length === 0) {
|
||
|
|
console.log(`[ReactiveBinding] 연결된 바인딩 없음`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 바인딩 실행 (priority 순서대로)
|
||
|
|
matchedBindings.forEach((binding) => {
|
||
|
|
this.executeBinding(binding, data);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 바인딩 실행
|
||
|
|
* - 소스 컴포넌트 이벤트 → 타겟 컴포넌트 액션
|
||
|
|
*/
|
||
|
|
private executeBinding(binding: ReactiveBinding, data: any) {
|
||
|
|
console.log(
|
||
|
|
`[ReactiveBinding] 바인딩 실행: ${binding.sourceComponentId}.${binding.sourceEvent} → ${binding.targetComponentId}.${binding.targetAction}`
|
||
|
|
);
|
||
|
|
|
||
|
|
// 조건 검사
|
||
|
|
if (binding.conditionConfig && !this.evaluateCondition(binding.conditionConfig, data)) {
|
||
|
|
console.log(`[ReactiveBinding] 조건 불만족, 실행 스킵`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데이터 변환
|
||
|
|
let transformedData = data;
|
||
|
|
if (binding.transformConfig) {
|
||
|
|
transformedData = this.transformData(binding.transformConfig, data);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 타겟 컴포넌트 찾기
|
||
|
|
const targetComponent = this.componentRegistry.get(binding.targetComponentId);
|
||
|
|
if (!targetComponent) {
|
||
|
|
console.warn(
|
||
|
|
`[ReactiveBinding] 타겟 컴포넌트를 찾을 수 없음: ${binding.targetComponentId}`
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 액션 실행
|
||
|
|
this.executeAction(binding.targetAction, targetComponent, transformedData, binding.targetField);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 조건 평가
|
||
|
|
*/
|
||
|
|
private evaluateCondition(conditionConfig: any, data: any): boolean {
|
||
|
|
// TODO: Phase B에서 구현
|
||
|
|
// 예: { type: "field_value", fieldId: "status", operator: "eq", value: "active" }
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 데이터 변환
|
||
|
|
*/
|
||
|
|
private transformData(transformConfig: any, data: any): any {
|
||
|
|
// TODO: Phase B에서 구현
|
||
|
|
// 예: { type: "calculate", expression: "quantity * price" }
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 액션 실행
|
||
|
|
*/
|
||
|
|
private executeAction(
|
||
|
|
action: string,
|
||
|
|
targetComponent: any,
|
||
|
|
data: any,
|
||
|
|
targetField?: string | null
|
||
|
|
) {
|
||
|
|
console.log(`[ReactiveBinding] 액션 실행: ${action}`, { targetField, data });
|
||
|
|
|
||
|
|
switch (action) {
|
||
|
|
case "filter":
|
||
|
|
// DataView 필터링
|
||
|
|
if (targetComponent.setFilter) {
|
||
|
|
targetComponent.setFilter(data);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "setValue":
|
||
|
|
// Field 값 설정
|
||
|
|
if (targetComponent.setValue) {
|
||
|
|
targetComponent.setValue(data);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "show":
|
||
|
|
// 컴포넌트 표시
|
||
|
|
if (targetComponent.show) {
|
||
|
|
targetComponent.show();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "hide":
|
||
|
|
// 컴포넌트 숨김
|
||
|
|
if (targetComponent.hide) {
|
||
|
|
targetComponent.hide();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "enable":
|
||
|
|
// 컴포넌트 활성화
|
||
|
|
if (targetComponent.enable) {
|
||
|
|
targetComponent.enable();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "disable":
|
||
|
|
// 컴포넌트 비활성화
|
||
|
|
if (targetComponent.disable) {
|
||
|
|
targetComponent.disable();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "refresh":
|
||
|
|
// 데이터 새로고침
|
||
|
|
if (targetComponent.refresh) {
|
||
|
|
targetComponent.refresh();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
console.warn(`[ReactiveBinding] 지원하지 않는 액션: ${action}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 바인딩 추가
|
||
|
|
*/
|
||
|
|
addBinding(binding: ReactiveBinding) {
|
||
|
|
this.bindings.push(binding);
|
||
|
|
this.bindings.sort((a, b) => (a.priority || 100) - (b.priority || 100));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 바인딩 제거
|
||
|
|
*/
|
||
|
|
removeBinding(bindingId: number) {
|
||
|
|
this.bindings = this.bindings.filter((b) => b.id !== bindingId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모든 바인딩 조회
|
||
|
|
*/
|
||
|
|
getBindings(): ReactiveBinding[] {
|
||
|
|
return [...this.bindings];
|
||
|
|
}
|
||
|
|
}
|