360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Settings, Clock, Info, Workflow, Plus, Trash2, GripVertical, ChevronUp, ChevronDown } from "lucide-react";
|
|
import { ComponentData } from "@/types/screen";
|
|
import { getNodeFlows, NodeFlow } from "@/lib/api/nodeFlows";
|
|
|
|
interface ImprovedButtonControlConfigPanelProps {
|
|
component: ComponentData;
|
|
onUpdateProperty: (path: string, value: any) => void;
|
|
}
|
|
|
|
// 다중 제어 설정 인터페이스
|
|
interface FlowControlConfig {
|
|
id: string;
|
|
flowId: number;
|
|
flowName: string;
|
|
executionTiming: "before" | "after" | "replace";
|
|
order: number;
|
|
}
|
|
|
|
/**
|
|
* 🔥 다중 제어 지원 버튼 설정 패널
|
|
*
|
|
* 기능:
|
|
* - 여러 개의 노드 플로우 선택 및 순서 지정
|
|
* - 각 플로우별 실행 타이밍 설정
|
|
* - 드래그앤드롭 또는 버튼으로 순서 변경
|
|
*/
|
|
export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlConfigPanelProps> = ({
|
|
component,
|
|
onUpdateProperty,
|
|
}) => {
|
|
const config = component.webTypeConfig || {};
|
|
const dataflowConfig = config.dataflowConfig || {};
|
|
|
|
// 다중 제어 설정 (배열)
|
|
const flowControls: FlowControlConfig[] = dataflowConfig.flowControls || [];
|
|
|
|
// 🔥 State 관리
|
|
const [flows, setFlows] = useState<NodeFlow[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// 🔥 플로우 목록 로딩
|
|
useEffect(() => {
|
|
if (config.enableDataflowControl) {
|
|
loadFlows();
|
|
}
|
|
}, [config.enableDataflowControl]);
|
|
|
|
/**
|
|
* 🔥 플로우 목록 로드
|
|
*/
|
|
const loadFlows = async () => {
|
|
try {
|
|
setLoading(true);
|
|
console.log("🔍 플로우 목록 로딩...");
|
|
|
|
const flowList = await getNodeFlows();
|
|
setFlows(flowList);
|
|
console.log(`✅ 플로우 ${flowList.length}개 로딩 완료`);
|
|
} catch (error) {
|
|
console.error("❌ 플로우 목록 로딩 실패:", error);
|
|
setFlows([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 🔥 제어 추가
|
|
*/
|
|
const handleAddControl = useCallback(() => {
|
|
const newControl: FlowControlConfig = {
|
|
id: `control_${Date.now()}`,
|
|
flowId: 0,
|
|
flowName: "",
|
|
executionTiming: "after",
|
|
order: flowControls.length + 1,
|
|
};
|
|
|
|
const updatedControls = [...flowControls, newControl];
|
|
updateFlowControls(updatedControls);
|
|
}, [flowControls]);
|
|
|
|
/**
|
|
* 🔥 제어 삭제
|
|
*/
|
|
const handleRemoveControl = useCallback(
|
|
(controlId: string) => {
|
|
const updatedControls = flowControls
|
|
.filter((c) => c.id !== controlId)
|
|
.map((c, index) => ({ ...c, order: index + 1 }));
|
|
updateFlowControls(updatedControls);
|
|
},
|
|
[flowControls],
|
|
);
|
|
|
|
/**
|
|
* 🔥 제어 플로우 선택
|
|
*/
|
|
const handleFlowSelect = useCallback(
|
|
(controlId: string, flowId: string) => {
|
|
const selectedFlow = flows.find((f) => f.flowId.toString() === flowId);
|
|
if (selectedFlow) {
|
|
const updatedControls = flowControls.map((c) =>
|
|
c.id === controlId ? { ...c, flowId: selectedFlow.flowId, flowName: selectedFlow.flowName } : c,
|
|
);
|
|
updateFlowControls(updatedControls);
|
|
}
|
|
},
|
|
[flows, flowControls],
|
|
);
|
|
|
|
/**
|
|
* 🔥 실행 타이밍 변경
|
|
*/
|
|
const handleTimingChange = useCallback(
|
|
(controlId: string, timing: "before" | "after" | "replace") => {
|
|
const updatedControls = flowControls.map((c) => (c.id === controlId ? { ...c, executionTiming: timing } : c));
|
|
updateFlowControls(updatedControls);
|
|
},
|
|
[flowControls],
|
|
);
|
|
|
|
/**
|
|
* 🔥 순서 위로 이동
|
|
*/
|
|
const handleMoveUp = useCallback(
|
|
(controlId: string) => {
|
|
const index = flowControls.findIndex((c) => c.id === controlId);
|
|
if (index > 0) {
|
|
const updatedControls = [...flowControls];
|
|
[updatedControls[index - 1], updatedControls[index]] = [updatedControls[index], updatedControls[index - 1]];
|
|
// 순서 번호 재정렬
|
|
updatedControls.forEach((c, i) => (c.order = i + 1));
|
|
updateFlowControls(updatedControls);
|
|
}
|
|
},
|
|
[flowControls],
|
|
);
|
|
|
|
/**
|
|
* 🔥 순서 아래로 이동
|
|
*/
|
|
const handleMoveDown = useCallback(
|
|
(controlId: string) => {
|
|
const index = flowControls.findIndex((c) => c.id === controlId);
|
|
if (index < flowControls.length - 1) {
|
|
const updatedControls = [...flowControls];
|
|
[updatedControls[index], updatedControls[index + 1]] = [updatedControls[index + 1], updatedControls[index]];
|
|
// 순서 번호 재정렬
|
|
updatedControls.forEach((c, i) => (c.order = i + 1));
|
|
updateFlowControls(updatedControls);
|
|
}
|
|
},
|
|
[flowControls],
|
|
);
|
|
|
|
/**
|
|
* 🔥 제어 목록 업데이트 (백엔드 호환성 유지)
|
|
*/
|
|
const updateFlowControls = (controls: FlowControlConfig[]) => {
|
|
// 첫 번째 제어를 기존 형식으로도 저장 (하위 호환성)
|
|
const firstValidControl = controls.find((c) => c.flowId > 0);
|
|
|
|
onUpdateProperty("webTypeConfig.dataflowConfig", {
|
|
...dataflowConfig,
|
|
// 기존 형식 (하위 호환성)
|
|
selectedDiagramId: firstValidControl?.flowId || null,
|
|
selectedRelationshipId: null,
|
|
flowConfig: firstValidControl
|
|
? {
|
|
flowId: firstValidControl.flowId,
|
|
flowName: firstValidControl.flowName,
|
|
executionTiming: firstValidControl.executionTiming,
|
|
contextData: {},
|
|
}
|
|
: null,
|
|
// 새로운 다중 제어 형식
|
|
flowControls: controls,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 🔥 제어관리 활성화 스위치 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Settings className="text-primary h-4 w-4" />
|
|
<div>
|
|
<Label className="text-sm font-medium">제어 기능</Label>
|
|
<p className="text-muted-foreground mt-1 text-xs">버튼 클릭 시 추가 작업을 자동으로 실행합니다</p>
|
|
</div>
|
|
</div>
|
|
<Switch
|
|
checked={config.enableDataflowControl || false}
|
|
onCheckedChange={(checked) => onUpdateProperty("webTypeConfig.enableDataflowControl", checked)}
|
|
/>
|
|
</div>
|
|
|
|
{/* 🔥 제어관리가 활성화된 경우에만 설정 표시 */}
|
|
{config.enableDataflowControl && (
|
|
<div className="space-y-4">
|
|
{/* 제어 목록 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Workflow className="h-4 w-4 text-green-600" />
|
|
<Label>제어 목록 (순서대로 실행)</Label>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={handleAddControl} className="h-8">
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
제어 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 제어 목록 */}
|
|
{flowControls.length === 0 ? (
|
|
<div className="rounded-md border border-dashed p-6 text-center">
|
|
<Workflow className="mx-auto h-8 w-8 text-gray-400" />
|
|
<p className="mt-2 text-sm text-gray-500">등록된 제어가 없습니다</p>
|
|
<Button variant="outline" size="sm" onClick={handleAddControl} className="mt-3">
|
|
<Plus className="mr-1 h-3 w-3" />첫 번째 제어 추가
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{flowControls.map((control, index) => (
|
|
<FlowControlItem
|
|
key={control.id}
|
|
control={control}
|
|
flows={flows}
|
|
loading={loading}
|
|
isFirst={index === 0}
|
|
isLast={index === flowControls.length - 1}
|
|
onFlowSelect={(flowId) => handleFlowSelect(control.id, flowId)}
|
|
onTimingChange={(timing) => handleTimingChange(control.id, timing)}
|
|
onMoveUp={() => handleMoveUp(control.id)}
|
|
onMoveDown={() => handleMoveDown(control.id)}
|
|
onRemove={() => handleRemoveControl(control.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 안내 메시지 */}
|
|
{flowControls.length > 0 && (
|
|
<div className="rounded bg-blue-50 p-3">
|
|
<div className="flex items-start space-x-2">
|
|
<Info className="mt-0.5 h-4 w-4 text-blue-600" />
|
|
<div className="text-xs text-blue-800">
|
|
<p className="font-medium">다중 제어 실행 정보:</p>
|
|
<p className="mt-1">• 제어는 위에서 아래 순서대로 순차 실행됩니다</p>
|
|
<p>• 각 제어는 독립 트랜잭션으로 처리됩니다</p>
|
|
<p>• 이전 제어 실패 시 다음 제어는 실행되지 않습니다</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 🔥 개별 제어 아이템 컴포넌트
|
|
*/
|
|
const FlowControlItem: React.FC<{
|
|
control: FlowControlConfig;
|
|
flows: NodeFlow[];
|
|
loading: boolean;
|
|
isFirst: boolean;
|
|
isLast: boolean;
|
|
onFlowSelect: (flowId: string) => void;
|
|
onTimingChange: (timing: "before" | "after" | "replace") => void;
|
|
onMoveUp: () => void;
|
|
onMoveDown: () => void;
|
|
onRemove: () => void;
|
|
}> = ({ control, flows, loading, isFirst, isLast, onFlowSelect, onTimingChange, onMoveUp, onMoveDown, onRemove }) => {
|
|
return (
|
|
<Card className="p-3">
|
|
<div className="flex items-start gap-2">
|
|
{/* 순서 표시 및 이동 버튼 */}
|
|
<div className="flex flex-col items-center gap-1">
|
|
<Badge variant="secondary" className="h-6 w-6 justify-center rounded-full p-0 text-xs">
|
|
{control.order}
|
|
</Badge>
|
|
<div className="flex flex-col">
|
|
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={onMoveUp} disabled={isFirst}>
|
|
<ChevronUp className="h-3 w-3" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={onMoveDown} disabled={isLast}>
|
|
<ChevronDown className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 플로우 선택 및 설정 */}
|
|
<div className="flex-1 space-y-2">
|
|
{/* 플로우 선택 */}
|
|
<Select value={control.flowId > 0 ? control.flowId.toString() : ""} onValueChange={onFlowSelect}>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="플로우를 선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{loading ? (
|
|
<div className="p-2 text-center text-xs text-gray-500">로딩 중...</div>
|
|
) : flows.length === 0 ? (
|
|
<div className="p-2 text-center text-xs text-gray-500">플로우가 없습니다</div>
|
|
) : (
|
|
flows.map((flow) => (
|
|
<SelectItem key={flow.flowId} value={flow.flowId.toString()}>
|
|
<span className="text-xs">{flow.flowName}</span>
|
|
</SelectItem>
|
|
))
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 실행 타이밍 */}
|
|
<Select value={control.executionTiming} onValueChange={onTimingChange}>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="before">
|
|
<span className="text-xs">Before (사전 실행)</span>
|
|
</SelectItem>
|
|
<SelectItem value="after">
|
|
<span className="text-xs">After (사후 실행)</span>
|
|
</SelectItem>
|
|
<SelectItem value="replace">
|
|
<span className="text-xs">Replace (대체 실행)</span>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 삭제 버튼 */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-red-500 hover:bg-red-50 hover:text-red-600"
|
|
onClick={onRemove}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
);
|
|
};
|