ERP-node/frontend/hooks/useScreenDataTransfer.ts

321 lines
8.7 KiB
TypeScript

"use client";
/**
* 화면 간 데이터 전달 훅
*
* 화면 간, 컴포넌트 간 데이터 전달을 통합된 방식으로 처리합니다.
*
* 사용 시나리오:
* 1. 마스터-디테일 패턴: 목록에서 선택 → 상세 화면에 데이터 전달
* 2. 모달 오픈: 버튼 클릭 → 모달에 선택된 데이터 전달
* 3. 화면 임베딩: 부모 화면 → 자식 화면에 필터 조건 전달
*/
import { useCallback, useEffect, useRef } from "react";
import type {
ScreenDataTransferConfig,
FieldMapping,
DataTransferTrigger,
} from "@/types/unified-form";
// ===== 이벤트 이름 상수 =====
export const SCREEN_DATA_TRANSFER_EVENT = "screenDataTransfer";
// ===== 전역 데이터 스토어 (간단한 인메모리 저장소) =====
const dataStore = new Map<string, unknown>();
/**
* 데이터 스토어에 데이터 저장
*/
export function setTransferData(key: string, data: unknown): void {
dataStore.set(key, data);
}
/**
* 데이터 스토어에서 데이터 조회
*/
export function getTransferData<T = unknown>(key: string): T | undefined {
return dataStore.get(key) as T | undefined;
}
/**
* 데이터 스토어에서 데이터 삭제
*/
export function clearTransferData(key: string): void {
dataStore.delete(key);
}
// ===== 데이터 변환 유틸 =====
/**
* 필드 매핑 적용
*/
export function applyFieldMappings(
data: Record<string, unknown>,
mappings: FieldMapping[]
): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const mapping of mappings) {
const sourceValue = data[mapping.sourceField];
// 변환 적용
let targetValue = sourceValue;
switch (mapping.transform) {
case "copy":
// 그대로 복사
targetValue = sourceValue;
break;
case "lookup":
// TODO: 다른 테이블에서 조회
targetValue = sourceValue;
break;
case "calculate":
// TODO: 계산식 적용
targetValue = sourceValue;
break;
case "format":
// TODO: 포맷팅 적용
if (typeof sourceValue === "string" && mapping.transformConfig?.format) {
// 간단한 포맷 적용 (확장 가능)
targetValue = sourceValue;
}
break;
default:
targetValue = sourceValue;
}
result[mapping.targetField] = targetValue;
}
return result;
}
// ===== 훅 =====
interface UseScreenDataTransferOptions {
// 이 컴포넌트/화면의 ID
screenId?: number;
componentId?: string;
// 데이터 수신 시 콜백
onReceiveData?: (data: Record<string, unknown>, trigger: DataTransferTrigger) => void;
// 자동 구독할 소스 (다른 화면에서 이 화면으로 전달되는 데이터)
subscribeFrom?: {
sourceScreenId?: number;
sourceComponentId?: string;
};
}
interface UseScreenDataTransferReturn {
/**
* 데이터 전송
*/
sendData: (
data: Record<string, unknown>,
config: {
targetScreenId?: number;
targetComponentId?: string;
mappings?: FieldMapping[];
trigger?: DataTransferTrigger;
}
) => void;
/**
* 데이터 수신 대기 (수동)
*/
receiveData: () => Record<string, unknown> | undefined;
/**
* 스토어에서 데이터 조회 (키 기반)
*/
getStoredData: <T = unknown>(key: string) => T | undefined;
/**
* 스토어에 데이터 저장 (키 기반)
*/
setStoredData: (key: string, data: unknown) => void;
}
/**
* 화면 간 데이터 전달 훅
*/
export function useScreenDataTransfer(
options: UseScreenDataTransferOptions = {}
): UseScreenDataTransferReturn {
const { screenId, componentId, onReceiveData, subscribeFrom } = options;
const receiveCallbackRef = useRef(onReceiveData);
receiveCallbackRef.current = onReceiveData;
// 이벤트 리스너 등록 (데이터 수신)
useEffect(() => {
if (!subscribeFrom && !screenId && !componentId) return;
const handleDataTransfer = (event: Event) => {
const customEvent = event as CustomEvent<{
sourceScreenId?: number;
sourceComponentId?: string;
targetScreenId?: number;
targetComponentId?: string;
data: Record<string, unknown>;
trigger: DataTransferTrigger;
}>;
const detail = customEvent.detail;
// 이 화면/컴포넌트를 대상으로 하는지 확인
const isTargetMatch =
(detail.targetScreenId && detail.targetScreenId === screenId) ||
(detail.targetComponentId && detail.targetComponentId === componentId);
// 구독 중인 소스에서 온 데이터인지 확인
const isSourceMatch = subscribeFrom && (
(subscribeFrom.sourceScreenId && subscribeFrom.sourceScreenId === detail.sourceScreenId) ||
(subscribeFrom.sourceComponentId && subscribeFrom.sourceComponentId === detail.sourceComponentId)
);
if (isTargetMatch || isSourceMatch) {
receiveCallbackRef.current?.(detail.data, detail.trigger);
}
};
window.addEventListener(SCREEN_DATA_TRANSFER_EVENT, handleDataTransfer);
return () => {
window.removeEventListener(SCREEN_DATA_TRANSFER_EVENT, handleDataTransfer);
};
}, [screenId, componentId, subscribeFrom]);
/**
* 데이터 전송
*/
const sendData = useCallback((
data: Record<string, unknown>,
config: {
targetScreenId?: number;
targetComponentId?: string;
mappings?: FieldMapping[];
trigger?: DataTransferTrigger;
}
) => {
// 매핑 적용
const mappedData = config.mappings
? applyFieldMappings(data, config.mappings)
: data;
// 이벤트 발생
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent(SCREEN_DATA_TRANSFER_EVENT, {
detail: {
sourceScreenId: screenId,
sourceComponentId: componentId,
targetScreenId: config.targetScreenId,
targetComponentId: config.targetComponentId,
data: mappedData,
trigger: config.trigger || "manual",
}
}));
}
// 스토어에도 저장 (비동기 조회용)
const storeKey = config.targetScreenId
? `screen_${config.targetScreenId}`
: config.targetComponentId
? `component_${config.targetComponentId}`
: "default";
setTransferData(storeKey, mappedData);
}, [screenId, componentId]);
/**
* 데이터 수신 (스토어에서 조회)
*/
const receiveData = useCallback(() => {
const storeKey = screenId
? `screen_${screenId}`
: componentId
? `component_${componentId}`
: "default";
return getTransferData<Record<string, unknown>>(storeKey);
}, [screenId, componentId]);
return {
sendData,
receiveData,
getStoredData: getTransferData,
setStoredData: setTransferData,
};
}
/**
* 간편한 데이터 전달 훅 (설정 기반)
*/
export function useConfiguredDataTransfer(config: ScreenDataTransferConfig) {
const { source, target, trigger, condition } = config;
const { sendData } = useScreenDataTransfer({
screenId: source.screenId,
componentId: source.componentId,
});
/**
* 설정된 대로 데이터 전달
*/
const transfer = useCallback((data: Record<string, unknown>) => {
// 조건 체크
if (condition) {
const fieldValue = data[condition.field];
let conditionMet = false;
switch (condition.operator) {
case "=":
conditionMet = fieldValue === condition.value;
break;
case "!=":
conditionMet = fieldValue !== condition.value;
break;
case ">":
conditionMet = Number(fieldValue) > Number(condition.value);
break;
case "<":
conditionMet = Number(fieldValue) < Number(condition.value);
break;
case "in":
conditionMet = Array.isArray(condition.value) && condition.value.includes(fieldValue);
break;
case "notIn":
conditionMet = Array.isArray(condition.value) && !condition.value.includes(fieldValue);
break;
}
if (!conditionMet) {
return; // 조건 불충족 시 전달 안 함
}
}
// 소스 필드만 추출
const sourceData: Record<string, unknown> = {};
for (const field of source.fields) {
sourceData[field] = data[field];
}
// 전달
sendData(sourceData, {
targetScreenId: target.screenId,
mappings: target.mappings,
trigger,
});
}, [source.fields, target.screenId, target.mappings, trigger, condition, sendData]);
return { transfer };
}
export default useScreenDataTransfer;