/** * 화면 컨텍스트 * 같은 화면 내의 컴포넌트들이 서로 통신할 수 있도록 합니다. */ "use client"; import React, { createContext, useContext, useCallback, useRef, useState } from "react"; import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer"; import { logger } from "@/lib/utils/logger"; import type { SplitPanelPosition } from "@/contexts/SplitPanelContext"; /** * 대기 중인 데이터 전달 항목 * 타겟 컴포넌트가 아직 마운트되지 않은 경우 (조건부 레이어 등) 버퍼에 저장 */ export interface PendingTransfer { targetComponentId: string; data: any[]; config: DataReceiverConfig; timestamp: number; targetLayerId?: string; } interface ScreenContextValue { screenId?: number; tableName?: string; menuObjid?: number; splitPanelPosition?: SplitPanelPosition; formData: Record; updateFormData: (fieldName: string, value: any) => void; // 컴포넌트 등록 registerDataProvider: (componentId: string, provider: DataProvidable) => void; unregisterDataProvider: (componentId: string) => void; registerDataReceiver: (componentId: string, receiver: DataReceivable) => void; unregisterDataReceiver: (componentId: string) => void; // 컴포넌트 조회 getDataProvider: (componentId: string) => DataProvidable | undefined; getDataReceiver: (componentId: string) => DataReceivable | undefined; // 모든 컴포넌트 조회 getAllDataProviders: () => Map; getAllDataReceivers: () => Map; // 대기 중인 데이터 전달 (레이어 내부 컴포넌트 미마운트 대응) addPendingTransfer: (transfer: PendingTransfer) => void; getPendingTransfer: (componentId: string) => PendingTransfer | undefined; clearPendingTransfer: (componentId: string) => void; } const ScreenContext = createContext(null); interface ScreenContextProviderProps { screenId?: number; tableName?: string; menuObjid?: number; // 메뉴 OBJID splitPanelPosition?: SplitPanelPosition; // 🆕 분할 패널 위치 children: React.ReactNode; } /** * 화면 컨텍스트 프로바이더 */ export function ScreenContextProvider({ screenId, tableName, menuObjid, splitPanelPosition, children, }: ScreenContextProviderProps) { const dataProvidersRef = useRef>(new Map()); const dataReceiversRef = useRef>(new Map()); const pendingTransfersRef = useRef>(new Map()); const [formData, setFormData] = useState>({}); const updateFormData = useCallback((fieldName: string, value: any) => { setFormData((prev) => { const updated = { ...prev, [fieldName]: value }; logger.debug("ScreenContext formData 업데이트", { fieldName, valueType: typeof value, isArray: Array.isArray(value), }); return updated; }); }, []); const registerDataProvider = useCallback((componentId: string, provider: DataProvidable) => { dataProvidersRef.current.set(componentId, provider); logger.debug("데이터 제공자 등록", { componentId, componentType: provider.componentType }); }, []); const unregisterDataProvider = useCallback((componentId: string) => { dataProvidersRef.current.delete(componentId); logger.debug("데이터 제공자 해제", { componentId }); }, []); const registerDataReceiver = useCallback((componentId: string, receiver: DataReceivable) => { dataReceiversRef.current.set(componentId, receiver); logger.debug("데이터 수신자 등록", { componentId, componentType: receiver.componentType }); // 대기 중인 데이터 전달이 있으면 즉시 수신 처리 const pending = pendingTransfersRef.current.get(componentId); if (pending) { logger.info("대기 중인 데이터 전달 자동 수신", { componentId, dataCount: pending.data.length, waitedMs: Date.now() - pending.timestamp, }); receiver .receiveData(pending.data, pending.config) .then(() => { pendingTransfersRef.current.delete(componentId); logger.info("대기 데이터 전달 완료", { componentId }); }) .catch((err) => { logger.error("대기 데이터 전달 실패", { componentId, error: err }); }); } }, []); const unregisterDataReceiver = useCallback((componentId: string) => { dataReceiversRef.current.delete(componentId); logger.debug("데이터 수신자 해제", { componentId }); }, []); const getDataProvider = useCallback((componentId: string) => { return dataProvidersRef.current.get(componentId); }, []); const getDataReceiver = useCallback((componentId: string) => { return dataReceiversRef.current.get(componentId); }, []); const getAllDataProviders = useCallback(() => { return new Map(dataProvidersRef.current); }, []); const getAllDataReceivers = useCallback(() => { return new Map(dataReceiversRef.current); }, []); const addPendingTransfer = useCallback((transfer: PendingTransfer) => { pendingTransfersRef.current.set(transfer.targetComponentId, transfer); logger.info("데이터 전달 대기열 추가", { targetComponentId: transfer.targetComponentId, dataCount: transfer.data.length, targetLayerId: transfer.targetLayerId, }); }, []); const getPendingTransfer = useCallback((componentId: string) => { return pendingTransfersRef.current.get(componentId); }, []); const clearPendingTransfer = useCallback((componentId: string) => { pendingTransfersRef.current.delete(componentId); logger.debug("대기 데이터 전달 클리어", { componentId }); }, []); const value = React.useMemo( () => ({ screenId, tableName, menuObjid, splitPanelPosition, formData, updateFormData, registerDataProvider, unregisterDataProvider, registerDataReceiver, unregisterDataReceiver, getDataProvider, getDataReceiver, getAllDataProviders, getAllDataReceivers, addPendingTransfer, getPendingTransfer, clearPendingTransfer, }), [ screenId, tableName, menuObjid, splitPanelPosition, formData, updateFormData, registerDataProvider, unregisterDataProvider, registerDataReceiver, unregisterDataReceiver, getDataProvider, getDataReceiver, getAllDataProviders, getAllDataReceivers, addPendingTransfer, getPendingTransfer, clearPendingTransfer, ], ); return {children}; } /** * 화면 컨텍스트 훅 */ export function useScreenContext() { const context = useContext(ScreenContext); if (!context) { throw new Error("useScreenContext는 ScreenContextProvider 내부에서만 사용할 수 있습니다."); } return context; } /** * 화면 컨텍스트 훅 (선택적) * 컨텍스트가 없어도 에러를 발생시키지 않습니다. */ export function useScreenContextOptional() { return useContext(ScreenContext); }