제어관리 외부커넥션 설정기능

This commit is contained in:
kjs
2025-09-26 01:28:51 +09:00
parent 1a59c0cf04
commit 2a4e379dc4
43 changed files with 7129 additions and 316 deletions

View File

@@ -0,0 +1,113 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Save, Eye, TestTube, Copy, RotateCcw, Loader2 } from "lucide-react";
import { toast } from "sonner";
// 타입 import
import { DataConnectionState, DataConnectionActions } from "../types/redesigned";
interface ActionButtonsProps {
state: DataConnectionState;
actions: DataConnectionActions;
}
/**
* 🎯 액션 버튼들
* - 저장, 미리보기, 테스트 실행
* - 설정 복사, 초기화
*/
const ActionButtons: React.FC<ActionButtonsProps> = ({ state, actions }) => {
const handleSave = async () => {
try {
await actions.saveMappings();
} catch (error) {
console.error("저장 실패:", error);
}
};
const handlePreview = () => {
// TODO: 미리보기 모달 열기
toast.info("미리보기 기능은 곧 구현될 예정입니다.");
};
const handleTest = async () => {
try {
await actions.testExecution();
} catch (error) {
console.error("테스트 실패:", error);
}
};
const handleCopySettings = () => {
// TODO: 설정 복사 기능
toast.info("설정 복사 기능은 곧 구현될 예정입니다.");
};
const handleReset = () => {
if (confirm("모든 설정을 초기화하시겠습니까?")) {
// TODO: 상태 초기화
toast.success("설정이 초기화되었습니다.");
}
};
const canSave = state.fieldMappings.length > 0 && !state.isLoading;
const canTest = state.fieldMappings.length > 0 && !state.isLoading;
return (
<div className="space-y-3">
{/* 주요 액션 */}
<div className="grid grid-cols-2 gap-2">
<Button onClick={handleSave} disabled={!canSave} className="h-8 text-xs">
{state.isLoading ? <Loader2 className="mr-1 h-3 w-3 animate-spin" /> : <Save className="mr-1 h-3 w-3" />}
</Button>
<Button variant="outline" onClick={handlePreview} className="h-8 text-xs">
<Eye className="mr-1 h-3 w-3" />
</Button>
</div>
{/* 테스트 실행 */}
<Button variant="secondary" onClick={handleTest} disabled={!canTest} className="h-8 w-full text-xs">
{state.isLoading ? <Loader2 className="mr-1 h-3 w-3 animate-spin" /> : <TestTube className="mr-1 h-3 w-3" />}
</Button>
<Separator />
{/* 보조 액션 */}
<div className="grid grid-cols-2 gap-2">
<Button variant="ghost" size="sm" onClick={handleCopySettings} className="h-7 text-xs">
<Copy className="mr-1 h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleReset}
className="text-destructive hover:text-destructive h-7 text-xs"
>
<RotateCcw className="mr-1 h-3 w-3" />
</Button>
</div>
{/* 상태 정보 */}
{state.fieldMappings.length > 0 && (
<div className="text-muted-foreground border-t pt-2 text-center text-xs">
{state.fieldMappings.length}
{state.validationErrors.length > 0 && (
<span className="ml-1 text-orange-600">({state.validationErrors.length} )</span>
)}
</div>
)}
</div>
);
};
export default ActionButtons;

View File

@@ -0,0 +1,115 @@
"use client";
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Settings, CheckCircle, AlertCircle } from "lucide-react";
// 타입 import
import { DataConnectionState } from "../types/redesigned";
interface ActionSummaryPanelProps {
state: DataConnectionState;
}
/**
* 📋 액션 설정 요약 패널
* - 액션 타입 표시
* - 실행 조건 요약
* - 설정 완료 상태
*/
const ActionSummaryPanel: React.FC<ActionSummaryPanelProps> = ({ state }) => {
const { actionType, actionConditions } = state;
const isConfigured = actionType && (actionType === "insert" || actionConditions.length > 0);
const actionTypeLabels = {
insert: "INSERT",
update: "UPDATE",
delete: "DELETE",
upsert: "UPSERT",
};
const actionTypeDescriptions = {
insert: "새 데이터 삽입",
update: "기존 데이터 수정",
delete: "데이터 삭제",
upsert: "있으면 수정, 없으면 삽입",
};
return (
<Card className="shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<Settings className="h-4 w-4" />
{isConfigured ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<AlertCircle className="h-4 w-4 text-orange-500" />
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3 px-4 pt-0 pb-4">
{/* 액션 타입 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium"> </span>
{actionType ? (
<Badge variant="outline" className="text-xs">
{actionTypeLabels[actionType]}
</Badge>
) : (
<span className="text-muted-foreground text-xs"></span>
)}
</div>
{actionType && <p className="text-muted-foreground text-xs">{actionTypeDescriptions[actionType]}</p>}
</div>
{/* 실행 조건 */}
{actionType && actionType !== "insert" && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium"> </span>
<span className="text-muted-foreground text-xs">
{actionConditions.length > 0 ? `${actionConditions.length}개 조건` : "조건 없음"}
</span>
</div>
{actionConditions.length === 0 && (
<p className="text-xs text-orange-600"> {actionType.toUpperCase()} </p>
)}
</div>
)}
{/* INSERT 액션 안내 */}
{actionType === "insert" && (
<div className="rounded-md border border-green-200 bg-green-50 p-2">
<p className="text-xs text-green-700"> INSERT </p>
</div>
)}
{/* 설정 상태 */}
<div className="border-t pt-2">
<div className="flex items-center gap-2">
{isConfigured ? (
<>
<CheckCircle className="h-3 w-3 text-green-600" />
<span className="text-xs font-medium text-green-600"> </span>
</>
) : (
<>
<AlertCircle className="h-3 w-3 text-orange-500" />
<span className="text-xs font-medium text-orange-600"> </span>
</>
)}
</div>
</div>
</CardContent>
</Card>
);
};
export default ActionSummaryPanel;

View File

@@ -0,0 +1,164 @@
"use client";
import React, { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ChevronDown, Settings } from "lucide-react";
interface AdvancedSettingsProps {
connectionType: "data_save" | "external_call";
}
/**
* ⚙️ 고급 설정 패널
* - 트랜잭션 설정
* - 배치 처리 설정
* - 로깅 설정
*/
const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ connectionType }) => {
const [isOpen, setIsOpen] = useState(false);
const [settings, setSettings] = useState({
batchSize: 1000,
timeout: 30,
retryCount: 3,
logLevel: "INFO",
});
const handleSettingChange = (key: string, value: string | number) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
return (
<Card>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<Button variant="ghost" className="h-auto w-full justify-between p-4">
<div className="flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="font-medium"> </span>
</div>
<ChevronDown className={`h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="space-y-3 px-4 pt-0 pb-3">
{connectionType === "data_save" && (
<>
{/* 트랜잭션 설정 - 컴팩트 */}
<div className="space-y-2">
<h4 className="text-xs font-medium text-gray-600">🔄 </h4>
<div className="grid grid-cols-3 gap-2">
<div>
<Label htmlFor="batchSize" className="text-xs text-gray-500">
</Label>
<Input
id="batchSize"
type="number"
value={settings.batchSize}
onChange={(e) => handleSettingChange("batchSize", parseInt(e.target.value))}
className="h-7 text-xs"
/>
</div>
<div>
<Label htmlFor="timeout" className="text-xs text-gray-500">
</Label>
<Input
id="timeout"
type="number"
value={settings.timeout}
onChange={(e) => handleSettingChange("timeout", parseInt(e.target.value))}
className="h-7 text-xs"
/>
</div>
<div>
<Label htmlFor="retryCount" className="text-xs text-gray-500">
</Label>
<Input
id="retryCount"
type="number"
value={settings.retryCount}
onChange={(e) => handleSettingChange("retryCount", parseInt(e.target.value))}
className="h-7 text-xs"
/>
</div>
</div>
</div>
</>
)}
{connectionType === "external_call" && (
<>
{/* API 호출 설정 - 컴팩트 */}
<div className="space-y-2">
<h4 className="text-xs font-medium text-gray-600">🌐 API </h4>
<div className="grid grid-cols-2 gap-2">
<div>
<Label htmlFor="timeout" className="text-xs text-gray-500">
()
</Label>
<Input
id="timeout"
type="number"
value={settings.timeout}
onChange={(e) => handleSettingChange("timeout", parseInt(e.target.value))}
className="h-7 text-xs"
/>
</div>
<div>
<Label htmlFor="retryCount" className="text-xs text-gray-500">
</Label>
<Input
id="retryCount"
type="number"
value={settings.retryCount}
onChange={(e) => handleSettingChange("retryCount", parseInt(e.target.value))}
className="h-7 text-xs"
/>
</div>
</div>
</div>
</>
)}
{/* 로깅 설정 - 컴팩트 */}
<div className="space-y-2">
<h4 className="text-xs font-medium text-gray-600">📝 </h4>
<div>
<Select value={settings.logLevel} onValueChange={(value) => handleSettingChange("logLevel", value)}>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="로그 레벨 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="DEBUG">DEBUG</SelectItem>
<SelectItem value="INFO">INFO</SelectItem>
<SelectItem value="WARN">WARN</SelectItem>
<SelectItem value="ERROR">ERROR</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 설정 요약 - 더 컴팩트 */}
<div className="border-t pt-2">
<div className="text-muted-foreground text-xs">
: {settings.batchSize.toLocaleString()} | : {settings.timeout}s | :{" "}
{settings.retryCount} | : {settings.logLevel}
</div>
</div>
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
);
};
export default AdvancedSettings;

View File

@@ -0,0 +1,59 @@
"use client";
import React from "react";
import { Card, CardContent } from "@/components/ui/card";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { Database, Globe } from "lucide-react";
// 타입 import
import { ConnectionType, ConnectionTypeSelectorProps } from "../types/redesigned";
/**
* 🔘 연결 타입 선택 컴포넌트
* - 데이터 저장 (INSERT/UPDATE/DELETE)
* - 외부 호출 (API/Webhook)
*/
const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({ selectedType, onTypeChange }) => {
const connectionTypes: ConnectionType[] = [
{
id: "data_save",
label: "데이터 저장",
description: "INSERT/UPDATE/DELETE 작업",
icon: <Database className="h-4 w-4" />,
},
{
id: "external_call",
label: "외부 호출",
description: "API/Webhook 호출",
icon: <Globe className="h-4 w-4" />,
},
];
return (
<Card>
<CardContent className="p-4">
<RadioGroup
value={selectedType}
onValueChange={(value) => onTypeChange(value as "data_save" | "external_call")}
className="space-y-3"
>
{connectionTypes.map((type) => (
<div key={type.id} className="flex items-start space-x-3">
<RadioGroupItem value={type.id} id={type.id} className="mt-1" />
<div className="min-w-0 flex-1">
<Label htmlFor={type.id} className="flex cursor-pointer items-center gap-2 font-medium">
{type.icon}
{type.label}
</Label>
<p className="text-muted-foreground mt-1 text-xs">{type.description}</p>
</div>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
);
};
export default ConnectionTypeSelector;

View File

@@ -0,0 +1,81 @@
"use client";
import React from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
// 타입 import
import { LeftPanelProps } from "../types/redesigned";
// 컴포넌트 import
import ConnectionTypeSelector from "./ConnectionTypeSelector";
import MappingDetailList from "./MappingDetailList";
import ActionSummaryPanel from "./ActionSummaryPanel";
import AdvancedSettings from "./AdvancedSettings";
import ActionButtons from "./ActionButtons";
/**
* 📋 좌측 패널 (30% 너비)
* - 연결 타입 선택
* - 매핑 정보 모니터링
* - 상세 설정 목록
* - 액션 버튼들
*/
const LeftPanel: React.FC<LeftPanelProps> = ({ state, actions }) => {
return (
<div className="flex h-full flex-col overflow-hidden">
<ScrollArea className="flex-1 p-3 pb-0">
<div className="space-y-3 pb-3">
{/* 0단계: 연결 타입 선택 */}
<div>
<h3 className="text-muted-foreground mb-2 text-sm font-medium">0단계: 연결 </h3>
<ConnectionTypeSelector selectedType={state.connectionType} onTypeChange={actions.setConnectionType} />
</div>
<Separator />
{/* 매핑 상세 목록 */}
{state.fieldMappings.length > 0 && (
<>
<div>
<h3 className="text-muted-foreground mb-2 text-sm font-medium"> </h3>
<MappingDetailList
mappings={state.fieldMappings}
selectedMapping={state.selectedMapping}
onSelectMapping={(mappingId) => {
// TODO: 선택된 매핑 상태 업데이트
}}
onUpdateMapping={actions.updateMapping}
onDeleteMapping={actions.deleteMapping}
/>
</div>
<Separator />
</>
)}
{/* 액션 설정 요약 */}
<div>
<h3 className="text-muted-foreground mb-2 text-sm font-medium"> </h3>
<ActionSummaryPanel state={state} />
</div>
<Separator />
{/* 고급 설정 */}
<div>
<h3 className="text-muted-foreground mb-2 text-sm font-medium"> </h3>
<AdvancedSettings connectionType={state.connectionType} />
</div>
</div>
</ScrollArea>
{/* 하단 액션 버튼들 - 고정 위치 */}
<div className="flex-shrink-0 border-t bg-white p-3 shadow-sm">
<ActionButtons state={state} actions={actions} />
</div>
</div>
);
};
export default LeftPanel;

View File

@@ -0,0 +1,112 @@
"use client";
import React from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { CheckCircle, AlertTriangle, Edit, Trash2 } from "lucide-react";
// 타입 import
import { MappingDetailListProps } from "../types/redesigned";
/**
* 📝 매핑 상세 목록
* - 각 매핑별 상세 정보
* - 타입 변환 정보
* - 개별 수정/삭제 기능
*/
const MappingDetailList: React.FC<MappingDetailListProps> = ({
mappings,
selectedMapping,
onSelectMapping,
onUpdateMapping,
onDeleteMapping,
}) => {
return (
<Card>
<CardContent className="p-0">
<ScrollArea className="h-[300px]">
<div className="space-y-3 p-4">
{mappings.map((mapping, index) => (
<div
key={mapping.id}
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
selectedMapping === mapping.id ? "border-primary bg-primary/5" : "border-border hover:bg-muted/50"
}`}
onClick={() => onSelectMapping(mapping.id)}
>
{/* 매핑 헤더 */}
<div className="mb-2 flex items-start justify-between">
<div className="min-w-0 flex-1">
<h4 className="truncate text-sm font-medium">
{index + 1}. {mapping.fromField.displayName || mapping.fromField.columnName} {" "}
{mapping.toField.displayName || mapping.toField.columnName}
</h4>
<div className="mt-1 flex items-center gap-2">
{mapping.isValid ? (
<Badge variant="outline" className="text-xs text-green-600">
<CheckCircle className="mr-1 h-3 w-3" />
{mapping.fromField.webType} {mapping.toField.webType}
</Badge>
) : (
<Badge variant="outline" className="text-xs text-orange-600">
<AlertTriangle className="mr-1 h-3 w-3" />
</Badge>
)}
</div>
</div>
<div className="ml-2 flex gap-1">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={(e) => {
e.stopPropagation();
// TODO: 매핑 편집 모달 열기
}}
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive h-6 w-6 p-0"
onClick={(e) => {
e.stopPropagation();
onDeleteMapping(mapping.id);
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
{/* 변환 규칙 */}
{mapping.transformRule && (
<div className="text-muted-foreground text-xs">: {mapping.transformRule}</div>
)}
{/* 검증 메시지 */}
{mapping.validationMessage && (
<div className="mt-1 text-xs text-orange-600">{mapping.validationMessage}</div>
)}
</div>
))}
{mappings.length === 0 && (
<div className="text-muted-foreground py-8 text-center text-sm">
<p> .</p>
<p className="mt-1 text-xs"> .</p>
</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
);
};
export default MappingDetailList;

View File

@@ -0,0 +1,115 @@
"use client";
import React from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { CheckCircle, AlertTriangle, XCircle, Info } from "lucide-react";
// 타입 import
import { MappingInfoPanelProps } from "../types/redesigned";
/**
* 📊 매핑 정보 패널
* - 실시간 매핑 통계
* - 검증 상태 표시
* - 예상 처리량 정보
*/
const MappingInfoPanel: React.FC<MappingInfoPanelProps> = ({ stats, validationErrors }) => {
const errorCount = validationErrors.filter((e) => e.type === "error").length;
const warningCount = validationErrors.filter((e) => e.type === "warning").length;
return (
<Card>
<CardContent className="space-y-3 p-4">
{/* 매핑 통계 */}
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"> :</span>
<Badge variant="outline">{stats.totalMappings}</Badge>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"> :</span>
<Badge variant="outline" className="text-green-600">
<CheckCircle className="mr-1 h-3 w-3" />
{stats.validMappings}
</Badge>
</div>
{stats.invalidMappings > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground"> :</span>
<Badge variant="outline" className="text-orange-600">
<AlertTriangle className="mr-1 h-3 w-3" />
{stats.invalidMappings}
</Badge>
</div>
)}
{stats.missingRequiredFields > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground"> :</span>
<Badge variant="outline" className="text-red-600">
<XCircle className="mr-1 h-3 w-3" />
{stats.missingRequiredFields}
</Badge>
</div>
)}
</div>
{/* 액션 정보 */}
{stats.totalMappings > 0 && (
<div className="space-y-2 border-t pt-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">:</span>
<Badge variant="secondary">{stats.actionType}</Badge>
</div>
{stats.estimatedRows > 0 && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"> :</span>
<span className="font-medium">~{stats.estimatedRows.toLocaleString()} rows</span>
</div>
)}
</div>
)}
{/* 검증 오류 요약 */}
{validationErrors.length > 0 && (
<div className="border-t pt-2">
<div className="flex items-center gap-2 text-sm">
<Info className="h-4 w-4 text-blue-500" />
<span className="text-muted-foreground"> :</span>
</div>
<div className="mt-2 space-y-1">
{errorCount > 0 && (
<Badge variant="destructive" className="text-xs">
{errorCount}
</Badge>
)}
{warningCount > 0 && (
<Badge variant="outline" className="ml-1 text-xs text-orange-600">
{warningCount}
</Badge>
)}
</div>
</div>
)}
{/* 빈 상태 */}
{stats.totalMappings === 0 && (
<div className="text-muted-foreground py-4 text-center text-sm">
<Database className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p> .</p>
<p className="mt-1 text-xs"> .</p>
</div>
)}
</CardContent>
</Card>
);
};
// Database 아이콘 import 추가
import { Database } from "lucide-react";
export default MappingInfoPanel;