화면 분할 패널 기능

This commit is contained in:
kjs
2025-11-28 14:56:11 +09:00
parent 30dac204c0
commit f15846fd10
27 changed files with 2455 additions and 207 deletions

View File

@@ -24,6 +24,7 @@ import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { useCurrentFlowStep } from "@/stores/flowStepStore";
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
import { applyMappingRules } from "@/lib/utils/dataMapping";
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
@@ -100,6 +101,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const screenContext = useScreenContextOptional(); // 화면 컨텍스트
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
// 🆕 ScreenContext에서 splitPanelPosition 가져오기 (중첩 화면에서도 작동)
const splitPanelPosition = screenContext?.splitPanelPosition;
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
@@ -395,20 +399,128 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
try {
// 1. 소스 컴포넌트에서 데이터 가져오기
const sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId);
let sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId);
// 🆕 소스 컴포넌트를 찾을 수 없으면, 현재 화면에서 테이블 리스트 자동 탐색
// (조건부 컨테이너의 다른 섹션으로 전환했을 때 이전 컴포넌트 ID가 남아있는 경우 대응)
if (!sourceProvider) {
toast.error(`소스 컴포넌트를 찾을 수 없습니다: ${dataTransferConfig.sourceComponentId}`);
return;
console.log(`⚠️ [ButtonPrimary] 지정된 소스 컴포넌트를 찾을 수 없: ${dataTransferConfig.sourceComponentId}`);
console.log(`🔍 [ButtonPrimary] 현재 화면에서 DataProvider 자동 탐색...`);
const allProviders = screenContext.getAllDataProviders();
// 테이블 리스트 우선 탐색
for (const [id, provider] of allProviders) {
if (provider.componentType === "table-list") {
sourceProvider = provider;
console.log(`✅ [ButtonPrimary] 테이블 리스트 자동 발견: ${id}`);
break;
}
}
// 테이블 리스트가 없으면 첫 번째 DataProvider 사용
if (!sourceProvider && allProviders.size > 0) {
const firstEntry = allProviders.entries().next().value;
if (firstEntry) {
sourceProvider = firstEntry[1];
console.log(`✅ [ButtonPrimary] 첫 번째 DataProvider 사용: ${firstEntry[0]} (${sourceProvider.componentType})`);
}
}
if (!sourceProvider) {
toast.error("데이터를 제공할 수 있는 컴포넌트를 찾을 수 없습니다.");
return;
}
}
const sourceData = sourceProvider.getSelectedData();
const rawSourceData = sourceProvider.getSelectedData();
// 🆕 배열이 아닌 경우 배열로 변환
const sourceData = Array.isArray(rawSourceData) ? rawSourceData : (rawSourceData ? [rawSourceData] : []);
console.log("📦 소스 데이터:", { rawSourceData, sourceData, isArray: Array.isArray(rawSourceData) });
if (!sourceData || sourceData.length === 0) {
toast.warning("선택된 데이터가 없습니다.");
return;
}
// 1.5. 추가 데이터 소스 처리 (예: 조건부 컨테이너의 카테고리 값)
let additionalData: Record<string, any> = {};
// 방법 1: additionalSources 설정에서 가져오기
if (dataTransferConfig.additionalSources && Array.isArray(dataTransferConfig.additionalSources)) {
for (const additionalSource of dataTransferConfig.additionalSources) {
const additionalProvider = screenContext.getDataProvider(additionalSource.componentId);
if (additionalProvider) {
const additionalValues = additionalProvider.getSelectedData();
if (additionalValues && additionalValues.length > 0) {
// 첫 번째 값 사용 (조건부 컨테이너는 항상 1개)
const firstValue = additionalValues[0];
// fieldName이 지정되어 있으면 그 필드만 추출
if (additionalSource.fieldName) {
additionalData[additionalSource.fieldName] = firstValue[additionalSource.fieldName] || firstValue.condition || firstValue;
} else {
// fieldName이 없으면 전체 객체 병합
additionalData = { ...additionalData, ...firstValue };
}
console.log("📦 추가 데이터 수집 (additionalSources):", {
sourceId: additionalSource.componentId,
fieldName: additionalSource.fieldName,
value: additionalData[additionalSource.fieldName || 'all'],
});
}
}
}
}
// 방법 2: formData에서 조건부 컨테이너 값 가져오기 (자동)
// ConditionalSectionViewer가 __conditionalContainerValue, __conditionalContainerControlField를 formData에 포함시킴
if (formData && formData.__conditionalContainerValue) {
// includeConditionalValue 설정이 true이거나 설정이 없으면 자동 포함
if (dataTransferConfig.includeConditionalValue !== false) {
const conditionalValue = formData.__conditionalContainerValue;
const conditionalLabel = formData.__conditionalContainerLabel;
const controlField = formData.__conditionalContainerControlField; // 🆕 제어 필드명 직접 사용
// 🆕 controlField가 있으면 그것을 필드명으로 사용 (자동 매핑!)
if (controlField) {
additionalData[controlField] = conditionalValue;
console.log("📦 조건부 컨테이너 값 자동 매핑:", {
controlField,
value: conditionalValue,
label: conditionalLabel,
});
} else {
// controlField가 없으면 기존 방식: formData에서 같은 값을 가진 키 찾기
for (const [key, value] of Object.entries(formData)) {
if (value === conditionalValue && !key.startsWith('__')) {
additionalData[key] = conditionalValue;
console.log("📦 조건부 컨테이너 값 자동 포함:", {
fieldName: key,
value: conditionalValue,
label: conditionalLabel,
});
break;
}
}
// 못 찾았으면 기본 필드명 사용
if (!Object.keys(additionalData).some(k => !k.startsWith('__'))) {
additionalData['condition_type'] = conditionalValue;
console.log("📦 조건부 컨테이너 값 (기본 필드명):", {
fieldName: 'condition_type',
value: conditionalValue,
});
}
}
}
}
// 2. 검증
const validation = dataTransferConfig.validation;
if (validation) {
@@ -430,9 +542,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}
}
// 4. 매핑 규칙 적용
// 4. 매핑 규칙 적용 + 추가 데이터 병합
const mappedData = sourceData.map((row) => {
return applyMappingRules(row, dataTransferConfig.mappingRules || []);
const mappedRow = applyMappingRules(row, dataTransferConfig.mappingRules || []);
// 추가 데이터를 모든 행에 포함
return {
...mappedRow,
...additionalData,
};
});
console.log("📦 데이터 전달:", {
@@ -459,13 +577,54 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
mode: dataTransferConfig.mode || "append",
mappingRules: dataTransferConfig.mappingRules || [],
});
toast.success(`${sourceData.length}개 항목이 전달되었습니다.`);
} else if (dataTransferConfig.targetType === "splitPanel") {
// 🆕 분할 패널의 반대편 화면으로 전달
if (!splitPanelContext) {
toast.error("분할 패널 컨텍스트를 찾을 수 없습니다. 이 버튼이 분할 패널 내부에 있는지 확인하세요.");
return;
}
// 🆕 useSplitPanelPosition 훅으로 위치 가져오기 (중첩된 화면에서도 작동)
// screenId로 찾는 것은 직접 임베드된 화면에서만 작동하므로,
// SplitPanelPositionProvider로 전달된 위치를 우선 사용
const currentPosition = splitPanelPosition || (screenId ? splitPanelContext.getPositionByScreenId(screenId) : null);
if (!currentPosition) {
toast.error("분할 패널 내 위치를 확인할 수 없습니다. screenId: " + screenId);
return;
}
console.log("📦 분할 패널 데이터 전달:", {
currentPosition,
splitPanelPositionFromHook: splitPanelPosition,
screenId,
leftScreenId: splitPanelContext.leftScreenId,
rightScreenId: splitPanelContext.rightScreenId,
});
const result = await splitPanelContext.transferToOtherSide(
currentPosition,
mappedData,
dataTransferConfig.targetComponentId, // 특정 컴포넌트 지정 (선택사항)
dataTransferConfig.mode || "append"
);
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
return;
}
} else if (dataTransferConfig.targetType === "screen") {
// 다른 화면으로 전달 (구현 예정)
toast.info("다른 화면으로의 데이터 전달은 추후 구현 예정입니다.");
return;
} else {
toast.success(`${sourceData.length}개 항목이 전달되었습니다.`);
}
toast.success(`${sourceData.length}개 항목이 전달되었습니다.`);
// 6. 전달 후 정리
if (dataTransferConfig.clearAfterTransfer) {
sourceProvider.clearSelection();