"use client"; import React, { createContext, useContext, useCallback, useRef, useState } from "react"; import { logger } from "@/lib/utils/logger"; /** * 분할 패널 내 화면 위치 */ export type SplitPanelPosition = "left" | "right"; /** * 데이터 수신자 인터페이스 */ export interface SplitPanelDataReceiver { componentId: string; componentType: string; receiveData: (data: any[], mode: "append" | "replace" | "merge") => Promise; } /** * 부모 데이터 매핑 설정 * 좌측 화면에서 선택한 데이터를 우측 화면 저장 시 자동으로 포함 */ export interface ParentDataMapping { sourceColumn: string; // 좌측 화면의 컬럼명 (예: equipment_code) targetColumn: string; // 우측 화면 저장 시 사용할 컬럼명 (예: equipment_code) } /** * 연결 필터 설정 * 좌측 화면에서 선택한 데이터로 우측 화면의 테이블을 자동 필터링 */ export interface LinkedFilter { sourceColumn: string; // 좌측 화면의 컬럼명 (예: equipment_code) targetColumn: string; // 우측 화면 필터링에 사용할 컬럼명 (예: equipment_code) } /** * 분할 패널 컨텍스트 값 */ interface SplitPanelContextValue { // 분할 패널 ID splitPanelId: string; // 좌측/우측 화면 ID leftScreenId: number | null; rightScreenId: number | null; // 데이터 수신자 등록/해제 registerReceiver: (position: SplitPanelPosition, componentId: string, receiver: SplitPanelDataReceiver) => void; unregisterReceiver: (position: SplitPanelPosition, componentId: string) => void; // 반대편 화면으로 데이터 전달 transferToOtherSide: ( fromPosition: SplitPanelPosition, data: any[], targetComponentId?: string, // 특정 컴포넌트 지정 (없으면 첫 번째 수신자) mode?: "append" | "replace" | "merge" ) => Promise<{ success: boolean; message: string }>; // 반대편 화면의 수신자 목록 가져오기 getOtherSideReceivers: (fromPosition: SplitPanelPosition) => SplitPanelDataReceiver[]; // 현재 위치 확인 isInSplitPanel: boolean; // screenId로 위치 찾기 getPositionByScreenId: (screenId: number) => SplitPanelPosition | null; // 🆕 우측에 추가된 항목 ID 관리 (좌측 테이블에서 필터링용) addedItemIds: Set; addItemIds: (ids: string[]) => void; removeItemIds: (ids: string[]) => void; clearItemIds: () => void; // 🆕 좌측 선택 데이터 관리 (우측 화면 저장 시 부모 키 전달용) selectedLeftData: Record | null; setSelectedLeftData: (data: Record | null) => void; // 🆕 부모 데이터 매핑 설정 parentDataMapping: ParentDataMapping[]; // 🆕 매핑된 부모 데이터 가져오기 (우측 화면 저장 시 사용) getMappedParentData: () => Record; // 🆕 연결 필터 설정 (좌측 선택 → 우측 테이블 필터링) linkedFilters: LinkedFilter[]; // 🆕 연결 필터 값 가져오기 (우측 테이블 조회 시 사용) getLinkedFilterValues: () => Record; // 🆕 자동 데이터 전달 비활성화 여부 (버튼 클릭 시에만 데이터 전달) disableAutoDataTransfer: boolean; } const SplitPanelContext = createContext(null); interface SplitPanelProviderProps { splitPanelId: string; leftScreenId: number | null; rightScreenId: number | null; parentDataMapping?: ParentDataMapping[]; // 🆕 부모 데이터 매핑 설정 linkedFilters?: LinkedFilter[]; // 🆕 연결 필터 설정 disableAutoDataTransfer?: boolean; // 🆕 자동 데이터 전달 비활성화 (버튼 클릭 시에만 전달) children: React.ReactNode; } /** * 분할 패널 컨텍스트 프로바이더 */ export function SplitPanelProvider({ splitPanelId, leftScreenId, rightScreenId, parentDataMapping = [], linkedFilters = [], disableAutoDataTransfer = false, children, }: SplitPanelProviderProps) { // 좌측/우측 화면의 데이터 수신자 맵 const leftReceiversRef = useRef>(new Map()); const rightReceiversRef = useRef>(new Map()); // 강제 리렌더링용 상태 const [, forceUpdate] = useState(0); // 🆕 우측에 추가된 항목 ID 상태 const [addedItemIds, setAddedItemIds] = useState>(new Set()); // 🆕 좌측에서 선택된 데이터 상태 const [selectedLeftData, setSelectedLeftData] = useState | null>(null); /** * 데이터 수신자 등록 */ const registerReceiver = useCallback( (position: SplitPanelPosition, componentId: string, receiver: SplitPanelDataReceiver) => { const receiversRef = position === "left" ? leftReceiversRef : rightReceiversRef; receiversRef.current.set(componentId, receiver); logger.debug(`[SplitPanelContext] 수신자 등록: ${position} - ${componentId}`, { componentType: receiver.componentType, }); forceUpdate((n) => n + 1); }, [] ); /** * 데이터 수신자 해제 */ const unregisterReceiver = useCallback( (position: SplitPanelPosition, componentId: string) => { const receiversRef = position === "left" ? leftReceiversRef : rightReceiversRef; receiversRef.current.delete(componentId); logger.debug(`[SplitPanelContext] 수신자 해제: ${position} - ${componentId}`); forceUpdate((n) => n + 1); }, [] ); /** * 반대편 화면의 수신자 목록 가져오기 */ const getOtherSideReceivers = useCallback( (fromPosition: SplitPanelPosition): SplitPanelDataReceiver[] => { const receiversRef = fromPosition === "left" ? rightReceiversRef : leftReceiversRef; return Array.from(receiversRef.current.values()); }, [] ); /** * 반대편 화면으로 데이터 전달 */ const transferToOtherSide = useCallback( async ( fromPosition: SplitPanelPosition, data: any[], targetComponentId?: string, mode: "append" | "replace" | "merge" = "append" ): Promise<{ success: boolean; message: string }> => { const toPosition = fromPosition === "left" ? "right" : "left"; const receiversRef = fromPosition === "left" ? rightReceiversRef : leftReceiversRef; logger.info(`[SplitPanelContext] 데이터 전달 시작: ${fromPosition} → ${toPosition}`, { dataCount: data.length, targetComponentId, mode, availableReceivers: Array.from(receiversRef.current.keys()), }); if (receiversRef.current.size === 0) { const message = `${toPosition === "left" ? "좌측" : "우측"} 화면에 데이터를 받을 수 있는 컴포넌트가 없습니다.`; logger.warn(`[SplitPanelContext] ${message}`); return { success: false, message }; } try { let targetReceiver: SplitPanelDataReceiver | undefined; if (targetComponentId) { // 특정 컴포넌트 지정 targetReceiver = receiversRef.current.get(targetComponentId); if (!targetReceiver) { const message = `타겟 컴포넌트 '${targetComponentId}'를 찾을 수 없습니다.`; logger.warn(`[SplitPanelContext] ${message}`); return { success: false, message }; } } else { // 첫 번째 수신자 사용 targetReceiver = receiversRef.current.values().next().value; } if (!targetReceiver) { return { success: false, message: "데이터 수신자를 찾을 수 없습니다." }; } await targetReceiver.receiveData(data, mode); const message = `${data.length}개 항목이 ${toPosition === "left" ? "좌측" : "우측"} 화면으로 전달되었습니다.`; logger.info(`[SplitPanelContext] ${message}`); return { success: true, message }; } catch (error: any) { const message = error.message || "데이터 전달 중 오류가 발생했습니다."; logger.error(`[SplitPanelContext] 데이터 전달 실패`, error); return { success: false, message }; } }, [] ); /** * screenId로 위치 찾기 */ const getPositionByScreenId = useCallback( (screenId: number): SplitPanelPosition | null => { if (leftScreenId === screenId) return "left"; if (rightScreenId === screenId) return "right"; return null; }, [leftScreenId, rightScreenId] ); /** * 🆕 추가된 항목 ID 등록 */ const addItemIds = useCallback((ids: string[]) => { setAddedItemIds((prev) => { const newSet = new Set(prev); ids.forEach((id) => newSet.add(id)); logger.debug(`[SplitPanelContext] 항목 ID 추가: ${ids.length}개`, { ids }); return newSet; }); }, []); /** * 🆕 추가된 항목 ID 제거 */ const removeItemIds = useCallback((ids: string[]) => { setAddedItemIds((prev) => { const newSet = new Set(prev); ids.forEach((id) => newSet.delete(id)); logger.debug(`[SplitPanelContext] 항목 ID 제거: ${ids.length}개`, { ids }); return newSet; }); }, []); /** * 🆕 모든 항목 ID 초기화 */ const clearItemIds = useCallback(() => { setAddedItemIds(new Set()); logger.debug(`[SplitPanelContext] 항목 ID 초기화`); }, []); /** * 🆕 좌측 선택 데이터 설정 */ const handleSetSelectedLeftData = useCallback((data: Record | null) => { setSelectedLeftData(data); }, []); /** * 🆕 매핑된 부모 데이터 가져오기 * 우측 화면에서 저장 시 이 함수를 호출하여 부모 키 값을 가져옴 * * 동작 방식: * 1. 좌측 데이터의 모든 컬럼을 자동으로 전달 (동일한 컬럼명이면 자동 매핑) * 2. 명시적 매핑이 있으면 소스→타겟 변환 적용 (다른 컬럼명으로 매핑 시) */ const getMappedParentData = useCallback((): Record => { if (!selectedLeftData) { return {}; } const mappedData: Record = {}; // 1단계: 좌측 데이터의 모든 컬럼을 자동으로 전달 (동일 컬럼명 자동 매핑) for (const [key, value] of Object.entries(selectedLeftData)) { if (value !== undefined && value !== null) { mappedData[key] = value; } } // 2단계: 명시적 매핑이 있으면 추가 적용 (다른 컬럼명으로 변환) for (const mapping of parentDataMapping) { const value = selectedLeftData[mapping.sourceColumn]; if (value !== undefined && value !== null) { // 소스와 타겟이 다른 경우에만 추가 매핑 if (mapping.sourceColumn !== mapping.targetColumn) { mappedData[mapping.targetColumn] = value; logger.debug(`[SplitPanelContext] 명시적 매핑: ${mapping.sourceColumn} → ${mapping.targetColumn} = ${value}`); } } } return mappedData; }, [selectedLeftData, parentDataMapping]); /** * 🆕 연결 필터 값 가져오기 * 우측 화면의 테이블 조회 시 이 값으로 필터링 */ const getLinkedFilterValues = useCallback((): Record => { if (!selectedLeftData || linkedFilters.length === 0) { return {}; } const filterValues: Record = {}; for (const filter of linkedFilters) { const value = selectedLeftData[filter.sourceColumn]; if (value !== undefined && value !== null && value !== "") { filterValues[filter.targetColumn] = value; logger.debug(`[SplitPanelContext] 연결 필터: ${filter.sourceColumn} → ${filter.targetColumn} = ${value}`); } } return filterValues; }, [selectedLeftData, linkedFilters]); // 🆕 useMemo로 value 객체 메모이제이션 (무한 루프 방지) const value = React.useMemo(() => ({ splitPanelId, leftScreenId, rightScreenId, registerReceiver, unregisterReceiver, transferToOtherSide, getOtherSideReceivers, isInSplitPanel: true, getPositionByScreenId, addedItemIds, addItemIds, removeItemIds, clearItemIds, // 🆕 좌측 선택 데이터 관련 selectedLeftData, setSelectedLeftData: handleSetSelectedLeftData, parentDataMapping, getMappedParentData, // 🆕 연결 필터 관련 linkedFilters, getLinkedFilterValues, // 🆕 자동 데이터 전달 비활성화 여부 disableAutoDataTransfer, }), [ splitPanelId, leftScreenId, rightScreenId, registerReceiver, unregisterReceiver, transferToOtherSide, getOtherSideReceivers, getPositionByScreenId, addedItemIds, addItemIds, removeItemIds, clearItemIds, selectedLeftData, handleSetSelectedLeftData, parentDataMapping, getMappedParentData, linkedFilters, getLinkedFilterValues, disableAutoDataTransfer, ]); return ( {children} ); } /** * 분할 패널 컨텍스트 훅 */ export function useSplitPanelContext() { return useContext(SplitPanelContext); } /** * 분할 패널 내부인지 확인하는 훅 */ export function useIsInSplitPanel(): boolean { const context = useContext(SplitPanelContext); return context?.isInSplitPanel ?? false; }