321 lines
8.7 KiB
TypeScript
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;
|
|
|