제어관리 외부커넥션 설정기능
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user