화면 분할패널 커밋
This commit is contained in:
parent
454f79caec
commit
51c49f7a3d
|
|
@ -434,6 +434,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
<SelectItem value="edit">편집</SelectItem>
|
||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||
<SelectItem value="transferData">📦 데이터 전달</SelectItem>
|
||||
<SelectItem value="openModalWithData">데이터 전달 + 모달 열기 🆕</SelectItem>
|
||||
<SelectItem value="modal">모달 열기</SelectItem>
|
||||
<SelectItem value="control">제어 흐름</SelectItem>
|
||||
|
|
@ -1601,6 +1602,166 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 데이터 전달 액션 설정 */}
|
||||
{(component.componentConfig?.action?.type || "save") === "transferData" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
|
||||
<h4 className="text-sm font-medium text-foreground">📦 데이터 전달 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="source-component-id">
|
||||
소스 컴포넌트 ID <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="source-component-id"
|
||||
placeholder="예: table-list-1"
|
||||
value={config.action?.dataTransfer?.sourceComponentId || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.sourceComponentId", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
데이터를 가져올 컴포넌트의 ID (테이블 등)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="target-type">
|
||||
타겟 타입 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={config.action?.dataTransfer?.targetType || "component"}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.dataTransfer.targetType", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="component">같은 화면의 컴포넌트</SelectItem>
|
||||
<SelectItem value="screen">다른 화면 (구현 예정)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{config.action?.dataTransfer?.targetType !== "screen" && (
|
||||
<div>
|
||||
<Label htmlFor="target-component-id">
|
||||
타겟 컴포넌트 ID <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="target-component-id"
|
||||
placeholder="예: table-list-2"
|
||||
value={config.action?.dataTransfer?.targetComponentId || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
데이터를 받을 컴포넌트의 ID
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="transfer-mode">데이터 전달 모드</Label>
|
||||
<Select
|
||||
value={config.action?.dataTransfer?.mode || "append"}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.dataTransfer.mode", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="append">추가 (Append)</SelectItem>
|
||||
<SelectItem value="replace">교체 (Replace)</SelectItem>
|
||||
<SelectItem value="merge">병합 (Merge)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
기존 데이터를 어떻게 처리할지 선택
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="clear-after-transfer">전달 후 소스 선택 초기화</Label>
|
||||
<p className="text-xs text-muted-foreground">데이터 전달 후 소스의 선택을 해제합니다</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="clear-after-transfer"
|
||||
checked={config.action?.dataTransfer?.clearAfterTransfer === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.dataTransfer.clearAfterTransfer", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="confirm-before-transfer">전달 전 확인 메시지</Label>
|
||||
<p className="text-xs text-muted-foreground">데이터 전달 전 확인 다이얼로그를 표시합니다</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="confirm-before-transfer"
|
||||
checked={config.action?.dataTransfer?.confirmBeforeTransfer === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.dataTransfer.confirmBeforeTransfer", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.action?.dataTransfer?.confirmBeforeTransfer && (
|
||||
<div>
|
||||
<Label htmlFor="confirm-message">확인 메시지</Label>
|
||||
<Input
|
||||
id="confirm-message"
|
||||
placeholder="선택한 항목을 전달하시겠습니까?"
|
||||
value={config.action?.dataTransfer?.confirmMessage || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.confirmMessage", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>검증 설정</Label>
|
||||
<div className="space-y-2 rounded-md border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="min-selection" className="text-xs">
|
||||
최소 선택 개수
|
||||
</Label>
|
||||
<Input
|
||||
id="min-selection"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={config.action?.dataTransfer?.validation?.minSelection || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.validation.minSelection", parseInt(e.target.value) || 0)}
|
||||
className="h-8 w-20 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="max-selection" className="text-xs">
|
||||
최대 선택 개수
|
||||
</Label>
|
||||
<Input
|
||||
id="max-selection"
|
||||
type="number"
|
||||
placeholder="제한없음"
|
||||
value={config.action?.dataTransfer?.validation?.maxSelection || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.validation.maxSelection", parseInt(e.target.value) || undefined)}
|
||||
className="h-8 w-20 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950">
|
||||
<p className="text-xs text-blue-900 dark:text-blue-100">
|
||||
<strong>사용 방법:</strong>
|
||||
<br />
|
||||
1. 소스 컴포넌트에서 데이터를 선택합니다
|
||||
<br />
|
||||
2. 이 버튼을 클릭하면 선택된 데이터가 타겟으로 전달됩니다
|
||||
<br />
|
||||
3. 매핑 규칙은 추후 고급 설정에서 추가 예정입니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 제어 기능 섹션 */}
|
||||
<div className="mt-8 border-t border-border pt-6">
|
||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import { toast } from "sonner";
|
|||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { applyMappingRules } from "@/lib/utils/dataMapping";
|
||||
|
||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||
config?: ButtonPrimaryConfig;
|
||||
|
|
@ -97,6 +99,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
...props
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const screenContext = useScreenContextOptional(); // 화면 컨텍스트
|
||||
|
||||
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
|
||||
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
|
||||
|
|
@ -374,6 +377,106 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
};
|
||||
|
||||
// 이벤트 핸들러
|
||||
/**
|
||||
* transferData 액션 처리
|
||||
*/
|
||||
const handleTransferDataAction = async (actionConfig: any) => {
|
||||
const dataTransferConfig = actionConfig.dataTransfer;
|
||||
|
||||
if (!dataTransferConfig) {
|
||||
toast.error("데이터 전달 설정이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!screenContext) {
|
||||
toast.error("화면 컨텍스트를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 소스 컴포넌트에서 데이터 가져오기
|
||||
const sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId);
|
||||
|
||||
if (!sourceProvider) {
|
||||
toast.error(`소스 컴포넌트를 찾을 수 없습니다: ${dataTransferConfig.sourceComponentId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = sourceProvider.getSelectedData();
|
||||
|
||||
if (!sourceData || sourceData.length === 0) {
|
||||
toast.warning("선택된 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 검증
|
||||
const validation = dataTransferConfig.validation;
|
||||
if (validation) {
|
||||
if (validation.minSelection && sourceData.length < validation.minSelection) {
|
||||
toast.error(`최소 ${validation.minSelection}개 이상 선택해야 합니다.`);
|
||||
return;
|
||||
}
|
||||
if (validation.maxSelection && sourceData.length > validation.maxSelection) {
|
||||
toast.error(`최대 ${validation.maxSelection}개까지 선택할 수 있습니다.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 확인 메시지
|
||||
if (dataTransferConfig.confirmBeforeTransfer) {
|
||||
const confirmMessage = dataTransferConfig.confirmMessage || `${sourceData.length}개 항목을 전달하시겠습니까?`;
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 매핑 규칙 적용
|
||||
const mappedData = sourceData.map((row) => {
|
||||
return applyMappingRules(row, dataTransferConfig.mappingRules || []);
|
||||
});
|
||||
|
||||
console.log("📦 데이터 전달:", {
|
||||
sourceData,
|
||||
mappedData,
|
||||
targetType: dataTransferConfig.targetType,
|
||||
targetComponentId: dataTransferConfig.targetComponentId,
|
||||
targetScreenId: dataTransferConfig.targetScreenId,
|
||||
});
|
||||
|
||||
// 5. 타겟으로 데이터 전달
|
||||
if (dataTransferConfig.targetType === "component") {
|
||||
// 같은 화면의 컴포넌트로 전달
|
||||
const targetReceiver = screenContext.getDataReceiver(dataTransferConfig.targetComponentId);
|
||||
|
||||
if (!targetReceiver) {
|
||||
toast.error(`타겟 컴포넌트를 찾을 수 없습니다: ${dataTransferConfig.targetComponentId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await targetReceiver.receiveData(mappedData, {
|
||||
targetComponentId: dataTransferConfig.targetComponentId,
|
||||
targetComponentType: targetReceiver.componentType,
|
||||
mode: dataTransferConfig.mode || "append",
|
||||
mappingRules: dataTransferConfig.mappingRules || [],
|
||||
});
|
||||
} else if (dataTransferConfig.targetType === "screen") {
|
||||
// 다른 화면으로 전달 (구현 예정)
|
||||
toast.info("다른 화면으로의 데이터 전달은 추후 구현 예정입니다.");
|
||||
}
|
||||
|
||||
toast.success(`${sourceData.length}개 항목이 전달되었습니다.`);
|
||||
|
||||
// 6. 전달 후 정리
|
||||
if (dataTransferConfig.clearAfterTransfer) {
|
||||
sourceProvider.clearSelection();
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("❌ 데이터 전달 실패:", error);
|
||||
toast.error(error.message || "데이터 전달 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
|
|
@ -390,6 +493,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
|
||||
// 인터랙티브 모드에서 액션 실행
|
||||
if (isInteractive && processedConfig.action) {
|
||||
// transferData 액션 처리 (화면 컨텍스트 필요)
|
||||
if (processedConfig.action.type === "transferData") {
|
||||
await handleTransferDataAction(processedConfig.action);
|
||||
return;
|
||||
}
|
||||
|
||||
// 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단
|
||||
const hasDataToDelete =
|
||||
(selectedRowsData && selectedRowsData.length > 0) || (flowSelectedData && flowSelectedData.length > 0);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ import { TableOptionsModal } from "@/components/common/TableOptionsModal";
|
|||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||
import { TableFilter, ColumnVisibility } from "@/types/table-options";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer";
|
||||
|
||||
// ========================================
|
||||
// 인터페이스
|
||||
|
|
@ -251,6 +253,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const { userId: authUserId } = useAuth();
|
||||
const currentUserId = userId || authUserId;
|
||||
|
||||
// 화면 컨텍스트 (데이터 제공자로 등록)
|
||||
const screenContext = useScreenContextOptional();
|
||||
|
||||
// TableOptions Context
|
||||
const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions();
|
||||
const [filters, setFilters] = useState<TableFilter[]>([]);
|
||||
|
|
@ -359,6 +364,107 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table");
|
||||
const [frozenColumns, setFrozenColumns] = useState<string[]>([]);
|
||||
|
||||
// DataProvidable 인터페이스 구현
|
||||
const dataProvider: DataProvidable = {
|
||||
componentId: component.id,
|
||||
componentType: "table-list",
|
||||
|
||||
getSelectedData: () => {
|
||||
// 선택된 행의 실제 데이터 반환
|
||||
const selectedData = data.filter((row) => {
|
||||
const rowId = String(row.id || row[tableConfig.selectedTable + "_id"] || "");
|
||||
return selectedRows.has(rowId);
|
||||
});
|
||||
return selectedData;
|
||||
},
|
||||
|
||||
getAllData: () => {
|
||||
return data;
|
||||
},
|
||||
|
||||
clearSelection: () => {
|
||||
setSelectedRows(new Set());
|
||||
setIsAllSelected(false);
|
||||
},
|
||||
};
|
||||
|
||||
// DataReceivable 인터페이스 구현
|
||||
const dataReceiver: DataReceivable = {
|
||||
componentId: component.id,
|
||||
componentType: "table",
|
||||
|
||||
receiveData: async (receivedData: any[], config: DataReceiverConfig) => {
|
||||
console.log("📥 TableList 데이터 수신:", {
|
||||
componentId: component.id,
|
||||
receivedDataCount: receivedData.length,
|
||||
mode: config.mode,
|
||||
currentDataCount: data.length,
|
||||
});
|
||||
|
||||
try {
|
||||
let newData: any[] = [];
|
||||
|
||||
switch (config.mode) {
|
||||
case "append":
|
||||
// 기존 데이터에 추가
|
||||
newData = [...data, ...receivedData];
|
||||
console.log("✅ Append 모드: 기존 데이터에 추가", { newDataCount: newData.length });
|
||||
break;
|
||||
|
||||
case "replace":
|
||||
// 기존 데이터를 완전히 교체
|
||||
newData = receivedData;
|
||||
console.log("✅ Replace 모드: 데이터 교체", { newDataCount: newData.length });
|
||||
break;
|
||||
|
||||
case "merge":
|
||||
// 기존 데이터와 병합 (ID 기반)
|
||||
const existingMap = new Map(data.map(item => [item.id, item]));
|
||||
receivedData.forEach(item => {
|
||||
if (item.id && existingMap.has(item.id)) {
|
||||
// 기존 데이터 업데이트
|
||||
existingMap.set(item.id, { ...existingMap.get(item.id), ...item });
|
||||
} else {
|
||||
// 새 데이터 추가
|
||||
existingMap.set(item.id || Date.now() + Math.random(), item);
|
||||
}
|
||||
});
|
||||
newData = Array.from(existingMap.values());
|
||||
console.log("✅ Merge 모드: 데이터 병합", { newDataCount: newData.length });
|
||||
break;
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
setData(newData);
|
||||
|
||||
// 총 아이템 수 업데이트
|
||||
setTotalItems(newData.length);
|
||||
|
||||
console.log("✅ 데이터 수신 완료:", { finalDataCount: newData.length });
|
||||
} catch (error) {
|
||||
console.error("❌ 데이터 수신 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getData: () => {
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
// 화면 컨텍스트에 데이터 제공자/수신자로 등록
|
||||
useEffect(() => {
|
||||
if (screenContext && component.id) {
|
||||
screenContext.registerDataProvider(component.id, dataProvider);
|
||||
screenContext.registerDataReceiver(component.id, dataReceiver);
|
||||
|
||||
return () => {
|
||||
screenContext.unregisterDataProvider(component.id);
|
||||
screenContext.unregisterDataReceiver(component.id);
|
||||
};
|
||||
}
|
||||
}, [screenContext, component.id, data, selectedRows]);
|
||||
|
||||
// 테이블 등록 (Context에 등록)
|
||||
const tableId = `table-list-${component.id}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -864,11 +864,14 @@ export class ImprovedButtonActionExecutor {
|
|||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
// 기존 ButtonActionExecutor 로직을 여기서 호출하거나
|
||||
// 간단한 액션들을 직접 구현
|
||||
const startTime = performance.now();
|
||||
|
||||
// 임시 구현 - 실제로는 기존 ButtonActionExecutor를 호출해야 함
|
||||
// transferData 액션 처리
|
||||
if (buttonConfig.actionType === "transferData") {
|
||||
return await this.executeTransferDataAction(buttonConfig, formData, context);
|
||||
}
|
||||
|
||||
// 기존 액션들 (임시 구현)
|
||||
const result = {
|
||||
success: true,
|
||||
message: `${buttonConfig.actionType} 액션 실행 완료`,
|
||||
|
|
@ -889,6 +892,43 @@ export class ImprovedButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 전달 액션 실행
|
||||
*/
|
||||
private static async executeTransferDataAction(
|
||||
buttonConfig: ExtendedButtonTypeConfig,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const dataTransferConfig = buttonConfig.dataTransfer;
|
||||
|
||||
if (!dataTransferConfig) {
|
||||
throw new Error("데이터 전달 설정이 없습니다.");
|
||||
}
|
||||
|
||||
console.log("📦 데이터 전달 시작:", dataTransferConfig);
|
||||
|
||||
// 1. 화면 컨텍스트에서 소스 컴포넌트 찾기
|
||||
const { ScreenContextProvider } = await import("@/contexts/ScreenContext");
|
||||
// 실제로는 현재 화면의 컨텍스트를 사용해야 하지만, 여기서는 전역적으로 접근할 수 없음
|
||||
// 대신 context에 screenContext를 전달하도록 수정 필요
|
||||
|
||||
throw new Error("데이터 전달 기능은 버튼 컴포넌트에서 직접 구현되어야 합니다.");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ 데이터 전달 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `데이터 전달 실패: ${error.message}`,
|
||||
executionTime: performance.now() - startTime,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 실행 오류 처리 및 롤백
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue