"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(); /** * 데이터 스토어에 데이터 저장 */ export function setTransferData(key: string, data: unknown): void { dataStore.set(key, data); } /** * 데이터 스토어에서 데이터 조회 */ export function getTransferData(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, mappings: FieldMapping[] ): Record { const result: Record = {}; 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, trigger: DataTransferTrigger) => void; // 자동 구독할 소스 (다른 화면에서 이 화면으로 전달되는 데이터) subscribeFrom?: { sourceScreenId?: number; sourceComponentId?: string; }; } interface UseScreenDataTransferReturn { /** * 데이터 전송 */ sendData: ( data: Record, config: { targetScreenId?: number; targetComponentId?: string; mappings?: FieldMapping[]; trigger?: DataTransferTrigger; } ) => void; /** * 데이터 수신 대기 (수동) */ receiveData: () => Record | undefined; /** * 스토어에서 데이터 조회 (키 기반) */ getStoredData: (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; 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, 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>(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) => { // 조건 체크 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 = {}; 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;