503 lines
20 KiB
TypeScript
503 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } 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 { Button } from "@/components/ui/button";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Check, ChevronsUpDown, Search, Info, Settings, FileText, Table, Layers } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { ComponentData, ButtonDataflowConfig } from "@/types/screen";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
interface ButtonDataflowConfigPanelProps {
|
|
component: ComponentData;
|
|
onUpdateProperty: (path: string, value: any) => void;
|
|
}
|
|
|
|
interface DiagramOption {
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
relationshipCount: number;
|
|
}
|
|
|
|
interface RelationshipOption {
|
|
id: string;
|
|
name: string;
|
|
sourceTable: string;
|
|
targetTable: string;
|
|
category: string;
|
|
}
|
|
|
|
/**
|
|
* 🔥 버튼 제어관리 설정 패널 (Phase 1: 간편 모드만)
|
|
*
|
|
* 성능 최적화를 위해 간편 모드만 구현:
|
|
* - 기존 관계도 선택
|
|
* - "after" 타이밍만 지원
|
|
* - 복잡한 고급 모드는 Phase 2에서
|
|
*/
|
|
export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps> = ({
|
|
component,
|
|
onUpdateProperty,
|
|
}) => {
|
|
const config = component.webTypeConfig || {};
|
|
const dataflowConfig = config.dataflowConfig || {};
|
|
|
|
// 🔥 State 관리
|
|
const [diagrams, setDiagrams] = useState<DiagramOption[]>([]);
|
|
const [relationships, setRelationships] = useState<RelationshipOption[]>([]);
|
|
const [diagramsLoading, setDiagramsLoading] = useState(false);
|
|
const [relationshipsLoading, setRelationshipsLoading] = useState(false);
|
|
const [diagramOpen, setDiagramOpen] = useState(false);
|
|
const [relationshipOpen, setRelationshipOpen] = useState(false);
|
|
const [previewData, setPreviewData] = useState<any>(null);
|
|
|
|
// 🔥 관계도 목록 로딩
|
|
useEffect(() => {
|
|
if (config.enableDataflowControl) {
|
|
loadDiagrams();
|
|
}
|
|
}, [config.enableDataflowControl]);
|
|
|
|
// 🔥 관계도 변경 시 관계 목록 로딩
|
|
useEffect(() => {
|
|
if (dataflowConfig.selectedDiagramId) {
|
|
loadRelationships(dataflowConfig.selectedDiagramId);
|
|
}
|
|
}, [dataflowConfig.selectedDiagramId]);
|
|
|
|
/**
|
|
* 🔥 관계도 목록 로딩 (캐시 활용)
|
|
*/
|
|
const loadDiagrams = async () => {
|
|
try {
|
|
setDiagramsLoading(true);
|
|
console.log("🔍 데이터플로우 관계도 목록 로딩...");
|
|
|
|
const response = await apiClient.get("/test-button-dataflow/diagrams");
|
|
|
|
if (response.data.success && Array.isArray(response.data.data)) {
|
|
const diagramList = response.data.data.map((diagram: any) => ({
|
|
id: diagram.diagram_id,
|
|
name: diagram.diagram_name,
|
|
description: diagram.description,
|
|
relationshipCount: diagram.relationships?.relationships?.length || 0,
|
|
}));
|
|
|
|
setDiagrams(diagramList);
|
|
console.log(`✅ 관계도 ${diagramList.length}개 로딩 완료`);
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ 관계도 목록 로딩 실패:", error);
|
|
setDiagrams([]);
|
|
} finally {
|
|
setDiagramsLoading(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 🔥 관계 목록 로딩
|
|
*/
|
|
const loadRelationships = async (diagramId: number) => {
|
|
try {
|
|
setRelationshipsLoading(true);
|
|
console.log(`🔍 관계도 ${diagramId} 관계 목록 로딩...`);
|
|
|
|
const response = await apiClient.get(`/test-button-dataflow/diagrams/${diagramId}/relationships`);
|
|
|
|
if (response.data.success && Array.isArray(response.data.data)) {
|
|
console.log("🔍 백엔드에서 받은 관계 데이터:", response.data.data);
|
|
|
|
const relationshipList = response.data.data.map((rel: any) => {
|
|
console.log("🔍 개별 관계 데이터:", rel);
|
|
|
|
// 여러 가지 가능한 필드명 시도 (백엔드 로그 기준으로 수정)
|
|
const relationshipName =
|
|
rel.relationshipName || // 백엔드에서 이 필드 사용
|
|
rel.name ||
|
|
rel.relationship_name ||
|
|
rel.label ||
|
|
rel.title ||
|
|
rel.description ||
|
|
`${rel.fromTable || rel.sourceTable || rel.source_table} → ${rel.toTable || rel.targetTable || rel.target_table}`;
|
|
|
|
const sourceTable = rel.fromTable || rel.sourceTable || rel.source_table || "Unknown"; // fromTable 우선
|
|
const targetTable = rel.toTable || rel.targetTable || rel.target_table || "Unknown"; // toTable 우선
|
|
|
|
const mappedRel = {
|
|
id: rel.id,
|
|
name: relationshipName,
|
|
sourceTable,
|
|
targetTable,
|
|
category: rel.category || rel.type || "data-save",
|
|
};
|
|
|
|
console.log("🔍 매핑된 관계 데이터:", mappedRel);
|
|
return mappedRel;
|
|
});
|
|
|
|
setRelationships(relationshipList);
|
|
console.log(`✅ 관계 ${relationshipList.length}개 로딩 완료:`, relationshipList);
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ 관계 목록 로딩 실패:", error);
|
|
setRelationships([]);
|
|
} finally {
|
|
setRelationshipsLoading(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 🔥 선택된 관계 미리보기 로딩
|
|
*/
|
|
const loadRelationshipPreview = async () => {
|
|
if (!dataflowConfig.selectedDiagramId || !dataflowConfig.selectedRelationshipId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiClient.get(
|
|
`/test-button-dataflow/diagrams/${dataflowConfig.selectedDiagramId}/relationships/${dataflowConfig.selectedRelationshipId}/preview`,
|
|
);
|
|
|
|
if (response.data.success) {
|
|
setPreviewData(response.data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ 관계 미리보기 로딩 실패:", error);
|
|
}
|
|
};
|
|
|
|
// 선택된 관계가 변경되면 미리보기 로딩
|
|
useEffect(() => {
|
|
if (dataflowConfig.selectedRelationshipId) {
|
|
loadRelationshipPreview();
|
|
}
|
|
}, [dataflowConfig.selectedRelationshipId]);
|
|
|
|
/**
|
|
* 현재 액션 타입별 표시명
|
|
*/
|
|
const getActionDisplayName = (actionType: string): string => {
|
|
const displayNames: Record<string, string> = {
|
|
save: "저장",
|
|
delete: "삭제",
|
|
edit: "수정",
|
|
add: "추가",
|
|
search: "검색",
|
|
reset: "초기화",
|
|
submit: "제출",
|
|
close: "닫기",
|
|
popup: "팝업",
|
|
navigate: "페이지 이동",
|
|
control: "제어",
|
|
};
|
|
return displayNames[actionType] || actionType;
|
|
};
|
|
|
|
/**
|
|
* 타이밍별 설명 (간소화)
|
|
*/
|
|
const getTimingDescription = (timing: string): string => {
|
|
switch (timing) {
|
|
case "before":
|
|
return "액션 실행 전 제어관리";
|
|
case "after":
|
|
return "액션 실행 후 제어관리";
|
|
case "replace":
|
|
return "제어관리로 완전 대체";
|
|
default:
|
|
return "";
|
|
}
|
|
};
|
|
|
|
// 선택된 관계도 정보
|
|
const selectedDiagram = diagrams.find((d) => d.id === dataflowConfig.selectedDiagramId);
|
|
const selectedRelationship = relationships.find((r) => r.id === dataflowConfig.selectedRelationshipId);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 🔥 제어관리 활성화 스위치 */}
|
|
<div className="flex items-center justify-between rounded-lg border bg-blue-50 p-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Settings className="h-4 w-4 text-blue-600" />
|
|
<div>
|
|
<Label className="text-sm font-medium">📊 제어관리 기능</Label>
|
|
<p className="mt-1 text-xs text-gray-600">버튼 클릭 시 데이터 흐름을 자동으로 제어합니다</p>
|
|
</div>
|
|
</div>
|
|
<Switch
|
|
checked={config.enableDataflowControl || false}
|
|
onCheckedChange={(checked) => onUpdateProperty("webTypeConfig.enableDataflowControl", checked)}
|
|
/>
|
|
</div>
|
|
|
|
{/* 🔥 제어관리가 활성화된 경우에만 설정 표시 */}
|
|
{config.enableDataflowControl && (
|
|
<>
|
|
{/* 🔥 제어 데이터 소스 선택 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-medium">📊 제어 데이터 소스</Label>
|
|
<Select
|
|
value={dataflowConfig.controlDataSource || "form"}
|
|
onValueChange={(value) => onUpdateProperty("webTypeConfig.dataflowConfig.controlDataSource", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="제어 데이터 소스 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="form">
|
|
<div className="flex items-center space-x-2">
|
|
<FileText className="h-4 w-4" />
|
|
<span>폼 데이터 기반</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="table-selection">
|
|
<div className="flex items-center space-x-2">
|
|
<Table className="h-4 w-4" />
|
|
<span>테이블 선택 기반</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="both">
|
|
<div className="flex items-center space-x-2">
|
|
<Layers className="h-4 w-4" />
|
|
<span>폼 + 테이블 선택</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-xs text-gray-500">
|
|
{dataflowConfig.controlDataSource === "form" && "현재 폼의 입력값으로 조건을 체크합니다"}
|
|
{dataflowConfig.controlDataSource === "table-selection" &&
|
|
"테이블에서 선택된 항목의 데이터로 조건을 체크합니다"}
|
|
{dataflowConfig.controlDataSource === "both" && "폼 데이터와 선택된 항목 데이터를 모두 사용합니다"}
|
|
{!dataflowConfig.controlDataSource && "폼 데이터를 기본으로 사용합니다"}
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{config.enableDataflowControl && (
|
|
<div className="space-y-6 border-l-2 border-blue-200 pl-4">
|
|
{/* 현재 액션 정보 (간소화) */}
|
|
<div className="rounded bg-gray-100 p-2">
|
|
<p className="text-xs text-gray-600">
|
|
<strong>{getActionDisplayName(config.actionType || "save")}</strong> 액션에 제어관리 연결
|
|
</p>
|
|
</div>
|
|
|
|
{/* 실행 타이밍 선택 (Phase 1: after만 지원) */}
|
|
<div>
|
|
<Label className="text-sm font-medium">실행 타이밍</Label>
|
|
<Select
|
|
value={config.dataflowTiming || "after"}
|
|
onValueChange={(value) => onUpdateProperty("webTypeConfig.dataflowTiming", value)}
|
|
>
|
|
<SelectTrigger className="mt-2">
|
|
<SelectValue placeholder="실행 타이밍을 선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="after">액션 실행 후 (권장)</SelectItem>
|
|
<SelectItem value="before" disabled>
|
|
액션 실행 전 (개발중)
|
|
</SelectItem>
|
|
<SelectItem value="replace" disabled>
|
|
액션 대신 (개발중)
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 제어 모드 선택 (Phase 1: simple만 지원) */}
|
|
<div>
|
|
<Label className="text-sm font-medium">제어 모드</Label>
|
|
<Select
|
|
value={dataflowConfig.controlMode || "simple"}
|
|
onValueChange={(value) => onUpdateProperty("webTypeConfig.dataflowConfig.controlMode", value)}
|
|
>
|
|
<SelectTrigger className="mt-2">
|
|
<SelectValue placeholder="제어 모드를 선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="simple">간편 모드 (관계도 선택)</SelectItem>
|
|
<SelectItem value="advanced" disabled>
|
|
고급 모드 (개발중)
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 간편 모드 설정 */}
|
|
{(dataflowConfig.controlMode === "simple" || !dataflowConfig.controlMode) && (
|
|
<div className="space-y-3 rounded border bg-gray-50 p-3">
|
|
<h4 className="text-sm font-medium text-gray-700">관계도 선택</h4>
|
|
|
|
{/* 관계도 선택 */}
|
|
<div>
|
|
<Label className="text-xs">관계도</Label>
|
|
<Popover open={diagramOpen} onOpenChange={setDiagramOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={diagramOpen}
|
|
className="mt-2 w-full justify-between"
|
|
disabled={diagramsLoading}
|
|
>
|
|
{selectedDiagram ? (
|
|
<div className="flex items-center space-x-2">
|
|
<span>{selectedDiagram.name}</span>
|
|
<Badge variant="secondary" className="text-xs">
|
|
{selectedDiagram.relationshipCount}개 관계
|
|
</Badge>
|
|
</div>
|
|
) : (
|
|
"관계도를 선택하세요"
|
|
)}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80 p-0">
|
|
<div className="p-2">
|
|
{diagramsLoading ? (
|
|
<div className="p-4 text-center text-sm text-gray-500">관계도 목록을 불러오는 중...</div>
|
|
) : diagrams.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-gray-500">사용 가능한 관계도가 없습니다</div>
|
|
) : (
|
|
<div className="max-h-60 overflow-y-auto">
|
|
{diagrams.map((diagram) => (
|
|
<Button
|
|
key={diagram.id}
|
|
variant="ghost"
|
|
className="h-auto w-full justify-start p-2"
|
|
onClick={() => {
|
|
onUpdateProperty("webTypeConfig.dataflowConfig.selectedDiagramId", diagram.id);
|
|
// 관계도 변경 시 기존 관계 선택 초기화
|
|
onUpdateProperty("webTypeConfig.dataflowConfig.selectedRelationshipId", null);
|
|
setDiagramOpen(false);
|
|
}}
|
|
>
|
|
<div className="flex w-full items-center space-x-2">
|
|
<Check
|
|
className={cn(
|
|
"h-4 w-4",
|
|
selectedDiagram?.id === diagram.id ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex-1 text-left">
|
|
<div className="font-medium">{diagram.name}</div>
|
|
<div className="text-xs text-gray-500">{diagram.relationshipCount}개 관계</div>
|
|
</div>
|
|
</div>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 관계 선택 */}
|
|
{dataflowConfig.selectedDiagramId && (
|
|
<div>
|
|
<Label className="text-xs">관계</Label>
|
|
<Popover open={relationshipOpen} onOpenChange={setRelationshipOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={relationshipOpen}
|
|
className="mt-2 w-full justify-between"
|
|
disabled={relationshipsLoading}
|
|
>
|
|
{selectedRelationship ? (
|
|
<div className="flex items-center space-x-2">
|
|
<span>{selectedRelationship.name}</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{selectedRelationship.category}
|
|
</Badge>
|
|
</div>
|
|
) : (
|
|
"관계를 선택하세요"
|
|
)}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-96 p-0">
|
|
<div className="p-2">
|
|
{relationshipsLoading ? (
|
|
<div className="p-4 text-center text-sm text-gray-500">관계 목록을 불러오는 중...</div>
|
|
) : relationships.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-gray-500">
|
|
이 관계도에는 사용 가능한 관계가 없습니다
|
|
</div>
|
|
) : (
|
|
<div className="max-h-60 overflow-y-auto">
|
|
{relationships.map((relationship) => (
|
|
<Button
|
|
key={relationship.id}
|
|
variant="ghost"
|
|
className="h-auto w-full justify-start p-2"
|
|
onClick={() => {
|
|
onUpdateProperty(
|
|
"webTypeConfig.dataflowConfig.selectedRelationshipId",
|
|
relationship.id,
|
|
);
|
|
setRelationshipOpen(false);
|
|
}}
|
|
>
|
|
<div className="flex w-full items-center space-x-2">
|
|
<Check
|
|
className={cn(
|
|
"h-4 w-4",
|
|
selectedRelationship?.id === relationship.id ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex-1 text-left">
|
|
<div className="font-medium">{relationship.name}</div>
|
|
<div className="text-xs text-gray-500">
|
|
{relationship.sourceTable} → {relationship.targetTable}
|
|
</div>
|
|
<Badge variant="outline" className="mt-1 text-xs">
|
|
{relationship.category}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
)}
|
|
|
|
{/* 선택된 관계 간단 정보 */}
|
|
{selectedRelationship && (
|
|
<div className="mt-2 rounded border bg-blue-50 p-2">
|
|
<p className="text-xs text-blue-700">
|
|
<strong>{selectedRelationship.sourceTable}</strong> →{" "}
|
|
<strong>{selectedRelationship.targetTable}</strong>
|
|
{previewData && (
|
|
<span className="ml-2">
|
|
(조건 {previewData.conditionsCount || 0}개, 액션 {previewData.actionsCount || 0}개)
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|