2025-11-28 14:56:11 +09:00
|
|
|
"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<void>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 분할 패널 컨텍스트 값
|
|
|
|
|
*/
|
|
|
|
|
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;
|
2025-12-01 10:19:20 +09:00
|
|
|
|
|
|
|
|
// 🆕 우측에 추가된 항목 ID 관리 (좌측 테이블에서 필터링용)
|
|
|
|
|
addedItemIds: Set<string>;
|
|
|
|
|
addItemIds: (ids: string[]) => void;
|
|
|
|
|
removeItemIds: (ids: string[]) => void;
|
|
|
|
|
clearItemIds: () => void;
|
2025-11-28 14:56:11 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SplitPanelContext = createContext<SplitPanelContextValue | null>(null);
|
|
|
|
|
|
|
|
|
|
interface SplitPanelProviderProps {
|
|
|
|
|
splitPanelId: string;
|
|
|
|
|
leftScreenId: number | null;
|
|
|
|
|
rightScreenId: number | null;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 분할 패널 컨텍스트 프로바이더
|
|
|
|
|
*/
|
|
|
|
|
export function SplitPanelProvider({
|
|
|
|
|
splitPanelId,
|
|
|
|
|
leftScreenId,
|
|
|
|
|
rightScreenId,
|
|
|
|
|
children,
|
|
|
|
|
}: SplitPanelProviderProps) {
|
|
|
|
|
// 좌측/우측 화면의 데이터 수신자 맵
|
|
|
|
|
const leftReceiversRef = useRef<Map<string, SplitPanelDataReceiver>>(new Map());
|
|
|
|
|
const rightReceiversRef = useRef<Map<string, SplitPanelDataReceiver>>(new Map());
|
|
|
|
|
|
|
|
|
|
// 강제 리렌더링용 상태
|
|
|
|
|
const [, forceUpdate] = useState(0);
|
2025-12-01 10:19:20 +09:00
|
|
|
|
|
|
|
|
// 🆕 우측에 추가된 항목 ID 상태
|
|
|
|
|
const [addedItemIds, setAddedItemIds] = useState<Set<string>>(new Set());
|
2025-11-28 14:56:11 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 데이터 수신자 등록
|
|
|
|
|
*/
|
|
|
|
|
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]
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-01 10:19:20 +09:00
|
|
|
/**
|
|
|
|
|
* 🆕 추가된 항목 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 초기화`);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 🆕 useMemo로 value 객체 메모이제이션 (무한 루프 방지)
|
|
|
|
|
const value = React.useMemo<SplitPanelContextValue>(() => ({
|
|
|
|
|
splitPanelId,
|
|
|
|
|
leftScreenId,
|
|
|
|
|
rightScreenId,
|
|
|
|
|
registerReceiver,
|
|
|
|
|
unregisterReceiver,
|
|
|
|
|
transferToOtherSide,
|
|
|
|
|
getOtherSideReceivers,
|
|
|
|
|
isInSplitPanel: true,
|
|
|
|
|
getPositionByScreenId,
|
2025-12-01 10:19:20 +09:00
|
|
|
addedItemIds,
|
|
|
|
|
addItemIds,
|
|
|
|
|
removeItemIds,
|
|
|
|
|
clearItemIds,
|
2025-11-28 14:56:11 +09:00
|
|
|
}), [
|
|
|
|
|
splitPanelId,
|
|
|
|
|
leftScreenId,
|
|
|
|
|
rightScreenId,
|
|
|
|
|
registerReceiver,
|
|
|
|
|
unregisterReceiver,
|
|
|
|
|
transferToOtherSide,
|
|
|
|
|
getOtherSideReceivers,
|
|
|
|
|
getPositionByScreenId,
|
2025-12-01 10:19:20 +09:00
|
|
|
addedItemIds,
|
|
|
|
|
addItemIds,
|
|
|
|
|
removeItemIds,
|
|
|
|
|
clearItemIds,
|
2025-11-28 14:56:11 +09:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SplitPanelContext.Provider value={value}>
|
|
|
|
|
{children}
|
|
|
|
|
</SplitPanelContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 분할 패널 컨텍스트 훅
|
|
|
|
|
*/
|
|
|
|
|
export function useSplitPanelContext() {
|
|
|
|
|
return useContext(SplitPanelContext);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 분할 패널 내부인지 확인하는 훅
|
|
|
|
|
*/
|
|
|
|
|
export function useIsInSplitPanel(): boolean {
|
|
|
|
|
const context = useContext(SplitPanelContext);
|
|
|
|
|
return context?.isInSplitPanel ?? false;
|
|
|
|
|
}
|
|
|
|
|
|