ERP-node/frontend/contexts/SplitPanelContext.tsx

426 lines
14 KiB
TypeScript

"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>;
}
/**
* 부모 데이터 매핑 설정
* 좌측 화면에서 선택한 데이터를 우측 화면 저장 시 자동으로 포함
*/
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<string>;
addItemIds: (ids: string[]) => void;
removeItemIds: (ids: string[]) => void;
clearItemIds: () => void;
// 🆕 좌측 선택 데이터 관리 (우측 화면 저장 시 부모 키 전달용)
selectedLeftData: Record<string, any> | null;
setSelectedLeftData: (data: Record<string, any> | null) => void;
// 🆕 부모 데이터 매핑 설정
parentDataMapping: ParentDataMapping[];
// 🆕 매핑된 부모 데이터 가져오기 (우측 화면 저장 시 사용)
getMappedParentData: () => Record<string, any>;
// 🆕 연결 필터 설정 (좌측 선택 → 우측 테이블 필터링)
linkedFilters: LinkedFilter[];
// 🆕 연결 필터 값 가져오기 (우측 테이블 조회 시 사용)
getLinkedFilterValues: () => Record<string, any>;
// 🆕 자동 데이터 전달 비활성화 여부 (버튼 클릭 시에만 데이터 전달)
disableAutoDataTransfer: boolean;
}
const SplitPanelContext = createContext<SplitPanelContextValue | null>(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<Map<string, SplitPanelDataReceiver>>(new Map());
const rightReceiversRef = useRef<Map<string, SplitPanelDataReceiver>>(new Map());
// 강제 리렌더링용 상태
const [, forceUpdate] = useState(0);
// 🆕 우측에 추가된 항목 ID 상태
const [addedItemIds, setAddedItemIds] = useState<Set<string>>(new Set());
// 🆕 좌측에서 선택된 데이터 상태
const [selectedLeftData, setSelectedLeftData] = useState<Record<string, any> | 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<string, any> | null) => {
logger.info(`[SplitPanelContext] 좌측 선택 데이터 설정:`, {
hasData: !!data,
dataKeys: data ? Object.keys(data) : [],
});
setSelectedLeftData(data);
}, []);
/**
* 🆕 매핑된 부모 데이터 가져오기
* 우측 화면에서 저장 시 이 함수를 호출하여 부모 키 값을 가져옴
*
* 동작 방식:
* 1. 좌측 데이터의 모든 컬럼을 자동으로 전달 (동일한 컬럼명이면 자동 매핑)
* 2. 명시적 매핑이 있으면 소스→타겟 변환 적용 (다른 컬럼명으로 매핑 시)
*/
const getMappedParentData = useCallback((): Record<string, any> => {
if (!selectedLeftData) {
return {};
}
const mappedData: Record<string, any> = {};
// 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}`);
}
}
}
logger.info(`[SplitPanelContext] 매핑된 부모 데이터 (자동+명시적):`, {
autoMappedKeys: Object.keys(selectedLeftData),
explicitMappings: parentDataMapping.length,
finalKeys: Object.keys(mappedData),
});
return mappedData;
}, [selectedLeftData, parentDataMapping]);
/**
* 🆕 연결 필터 값 가져오기
* 우측 화면의 테이블 조회 시 이 값으로 필터링
*/
const getLinkedFilterValues = useCallback((): Record<string, any> => {
if (!selectedLeftData || linkedFilters.length === 0) {
return {};
}
const filterValues: Record<string, any> = {};
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}`);
}
}
logger.info(`[SplitPanelContext] 연결 필터 값:`, filterValues);
return filterValues;
}, [selectedLeftData, linkedFilters]);
// 🆕 useMemo로 value 객체 메모이제이션 (무한 루프 방지)
const value = React.useMemo<SplitPanelContextValue>(() => ({
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 (
<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;
}