제어관리 외부커넥션 설정기능
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ArrowLeft, CheckCircle, AlertCircle, Settings, Plus, Trash2 } from "lucide-react";
|
||||
|
||||
// 타입 import
|
||||
import { DataConnectionState, DataConnectionActions } from "../types/redesigned";
|
||||
import { ColumnInfo } from "@/lib/types/multiConnection";
|
||||
import { getColumnsFromConnection } from "@/lib/api/multiConnection";
|
||||
import { getCodesForColumn, CodeItem } from "@/lib/api/codeManagement";
|
||||
|
||||
// 컴포넌트 import
|
||||
|
||||
interface ControlConditionStepProps {
|
||||
state: DataConnectionState;
|
||||
actions: DataConnectionActions;
|
||||
onBack: () => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 4단계: 제어 조건 설정
|
||||
* - 전체 제어가 언제 실행될지 설정
|
||||
* - INSERT/UPDATE/DELETE 트리거 조건
|
||||
*/
|
||||
const ControlConditionStep: React.FC<ControlConditionStepProps> = ({ state, actions, onBack, onNext }) => {
|
||||
const { controlConditions, fromTable, toTable, fromConnection, toConnection } = state;
|
||||
|
||||
const [fromColumns, setFromColumns] = useState<ColumnInfo[]>([]);
|
||||
const [toColumns, setToColumns] = useState<ColumnInfo[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [availableCodes, setAvailableCodes] = useState<Record<string, CodeItem[]>>({});
|
||||
|
||||
// 컬럼 정보 로드
|
||||
useEffect(() => {
|
||||
const loadColumns = async () => {
|
||||
console.log("🔄 ControlConditionStep 컬럼 로드 시작");
|
||||
console.log("fromConnection:", fromConnection);
|
||||
console.log("toConnection:", toConnection);
|
||||
console.log("fromTable:", fromTable);
|
||||
console.log("toTable:", toTable);
|
||||
|
||||
if (!fromConnection || !toConnection || !fromTable || !toTable) {
|
||||
console.log("❌ 필수 정보 누락으로 컬럼 로드 중단");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log(
|
||||
`🚀 컬럼 조회 시작: FROM=${fromConnection.id}/${fromTable.tableName}, TO=${toConnection.id}/${toTable.tableName}`,
|
||||
);
|
||||
|
||||
const [fromCols, toCols] = await Promise.all([
|
||||
getColumnsFromConnection(fromConnection.id, fromTable.tableName),
|
||||
getColumnsFromConnection(toConnection.id, toTable.tableName),
|
||||
]);
|
||||
|
||||
console.log(`✅ 컬럼 조회 완료: FROM=${fromCols.length}개, TO=${toCols.length}개`);
|
||||
setFromColumns(fromCols);
|
||||
setToColumns(toCols);
|
||||
} catch (error) {
|
||||
console.error("❌ 컬럼 정보 로드 실패:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadColumns();
|
||||
}, [fromConnection, toConnection, fromTable, toTable]);
|
||||
|
||||
// 코드 타입 컬럼의 코드 로드
|
||||
useEffect(() => {
|
||||
const loadCodes = async () => {
|
||||
const allColumns = [...fromColumns, ...toColumns];
|
||||
const codeColumns = allColumns.filter(
|
||||
(col) => col.webType === "code" || col.dataType?.toLowerCase().includes("code"),
|
||||
);
|
||||
|
||||
if (codeColumns.length === 0) return;
|
||||
|
||||
console.log("🔍 코드 타입 컬럼들:", codeColumns);
|
||||
|
||||
const codePromises = codeColumns.map(async (col) => {
|
||||
try {
|
||||
const codes = await getCodesForColumn(col.columnName, col.webType, col.codeCategory);
|
||||
return { columnName: col.columnName, codes };
|
||||
} catch (error) {
|
||||
console.error(`코드 로딩 실패 (${col.columnName}):`, error);
|
||||
return { columnName: col.columnName, codes: [] };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(codePromises);
|
||||
const codeMap: Record<string, CodeItem[]> = {};
|
||||
|
||||
results.forEach(({ columnName, codes }) => {
|
||||
codeMap[columnName] = codes;
|
||||
});
|
||||
|
||||
console.log("📋 로딩된 코드들:", codeMap);
|
||||
setAvailableCodes(codeMap);
|
||||
};
|
||||
|
||||
if (fromColumns.length > 0 || toColumns.length > 0) {
|
||||
loadCodes();
|
||||
}
|
||||
}, [fromColumns, toColumns]);
|
||||
|
||||
// 완료 가능 여부 확인
|
||||
const canProceed =
|
||||
controlConditions.length === 0 ||
|
||||
controlConditions.some(
|
||||
(condition) =>
|
||||
condition.field &&
|
||||
condition.operator &&
|
||||
(condition.value !== "" || ["IS NULL", "IS NOT NULL"].includes(condition.operator)),
|
||||
);
|
||||
|
||||
const isCompleted = canProceed;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{isCompleted ? (
|
||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
||||
) : (
|
||||
<AlertCircle className="h-5 w-5 text-orange-500" />
|
||||
)}
|
||||
4단계: 제어 실행 조건
|
||||
</CardTitle>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
이 전체 제어가 언제 실행될지 설정합니다. 조건을 설정하지 않으면 항상 실행됩니다.
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex h-full flex-col overflow-hidden p-0">
|
||||
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto p-4">
|
||||
{/* 제어 실행 조건 안내 */}
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-blue-800">제어 실행 조건이란?</h4>
|
||||
<div className="space-y-1 text-sm text-blue-700">
|
||||
<p>
|
||||
• <strong>전체 제어의 트리거 조건</strong>을 설정합니다
|
||||
</p>
|
||||
<p>• 예: "상태가 '활성'이고 유형이 'A'인 경우에만 데이터 동기화 실행"</p>
|
||||
<p>• 조건을 설정하지 않으면 모든 경우에 실행됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 간단한 조건 추가 UI */}
|
||||
{!isLoading && (fromColumns.length > 0 || toColumns.length > 0 || controlConditions.length > 0) && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium">실행 조건 (WHERE)</h4>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
console.log("🔄 조건 추가 클릭");
|
||||
actions.addControlCondition();
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
조건 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{controlConditions.length === 0 ? (
|
||||
<div className="rounded-lg border-2 border-dashed p-6 text-center">
|
||||
<Settings className="text-muted-foreground mx-auto mb-2 h-6 w-6" />
|
||||
<p className="text-muted-foreground text-sm">제어 실행 조건을 설정하세요</p>
|
||||
<p className="text-muted-foreground mt-1 text-xs">"조건 추가" 버튼을 클릭하여 시작하세요</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{controlConditions.map((condition, index) => (
|
||||
<div key={`control-condition-${index}`} className="rounded-lg border p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 논리 연산자 */}
|
||||
{index > 0 && (
|
||||
<Select
|
||||
value={condition.logicalOperator || "AND"}
|
||||
onValueChange={(value) =>
|
||||
actions.updateControlCondition(index, {
|
||||
...condition,
|
||||
logicalOperator: value as "AND" | "OR",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-16">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AND">AND</SelectItem>
|
||||
<SelectItem value="OR">OR</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{/* 필드 선택 */}
|
||||
<Select
|
||||
value={condition.field || ""}
|
||||
onValueChange={(value) => {
|
||||
if (value !== "__placeholder__") {
|
||||
actions.updateControlCondition(index, {
|
||||
...condition,
|
||||
field: value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__placeholder__" disabled>
|
||||
필드 선택
|
||||
</SelectItem>
|
||||
{[...fromColumns, ...toColumns]
|
||||
.filter(
|
||||
(col, index, array) =>
|
||||
array.findIndex((c) => c.columnName === col.columnName) === index,
|
||||
)
|
||||
.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 연산자 선택 */}
|
||||
<Select
|
||||
value={condition.operator || "="}
|
||||
onValueChange={(value) =>
|
||||
actions.updateControlCondition(index, {
|
||||
...condition,
|
||||
operator: value as any,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="=">=</SelectItem>
|
||||
<SelectItem value="!=">!=</SelectItem>
|
||||
<SelectItem value=">">{">"}</SelectItem>
|
||||
<SelectItem value="<">{"<"}</SelectItem>
|
||||
<SelectItem value=">=">{">="}</SelectItem>
|
||||
<SelectItem value="<=">{`<=`}</SelectItem>
|
||||
<SelectItem value="LIKE">LIKE</SelectItem>
|
||||
<SelectItem value="IS NULL">IS NULL</SelectItem>
|
||||
<SelectItem value="IS NOT NULL">IS NOT NULL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 값 입력 */}
|
||||
{!["IS NULL", "IS NOT NULL"].includes(condition.operator || "") &&
|
||||
(() => {
|
||||
// 선택된 필드가 코드 타입인지 확인
|
||||
const selectedField = [...fromColumns, ...toColumns].find(
|
||||
(col) => col.columnName === condition.field,
|
||||
);
|
||||
const isCodeField =
|
||||
selectedField &&
|
||||
(selectedField.webType === "code" ||
|
||||
selectedField.dataType?.toLowerCase().includes("code"));
|
||||
const fieldCodes = condition.field ? availableCodes[condition.field] : [];
|
||||
|
||||
// 디버깅 정보 출력
|
||||
console.log("🔍 값 입력 필드 디버깅:", {
|
||||
conditionField: condition.field,
|
||||
selectedField: selectedField,
|
||||
webType: selectedField?.webType,
|
||||
dataType: selectedField?.dataType,
|
||||
isCodeField: isCodeField,
|
||||
fieldCodes: fieldCodes,
|
||||
availableCodesKeys: Object.keys(availableCodes),
|
||||
});
|
||||
|
||||
if (isCodeField && fieldCodes && fieldCodes.length > 0) {
|
||||
// 코드 타입 필드면 코드 선택 드롭다운
|
||||
return (
|
||||
<Select
|
||||
value={condition.value || ""}
|
||||
onValueChange={(value) => {
|
||||
if (value !== "__code_placeholder__") {
|
||||
actions.updateControlCondition(index, {
|
||||
...condition,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="코드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__code_placeholder__" disabled>
|
||||
코드 선택
|
||||
</SelectItem>
|
||||
{fieldCodes.map((code, codeIndex) => {
|
||||
console.log("🎨 코드 렌더링:", {
|
||||
index: codeIndex,
|
||||
code: code,
|
||||
codeValue: code.code,
|
||||
codeName: code.name,
|
||||
hasCode: !!code.code,
|
||||
hasName: !!code.name,
|
||||
});
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
key={`code_${condition.field}_${code.code || codeIndex}_${codeIndex}`}
|
||||
value={code.code || `unknown_${codeIndex}`}
|
||||
>
|
||||
{code.name || code.description || `코드 ${codeIndex + 1}`}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else {
|
||||
// 일반 필드면 텍스트 입력
|
||||
return (
|
||||
<Input
|
||||
placeholder="값"
|
||||
value={condition.value || ""}
|
||||
onChange={(e) =>
|
||||
actions.updateControlCondition(index, {
|
||||
...condition,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-32"
|
||||
/>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => actions.deleteControlCondition(index)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 로딩 상태 */}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">컬럼 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 조건 없음 안내 */}
|
||||
{!isLoading && controlConditions.length === 0 && (
|
||||
<div className="rounded-lg border-2 border-dashed p-8 text-center">
|
||||
<AlertCircle className="text-muted-foreground mx-auto mb-3 h-8 w-8" />
|
||||
<h3 className="mb-2 text-lg font-medium">제어 실행 조건 없음</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
현재 제어 실행 조건이 설정되지 않았습니다.
|
||||
<br />
|
||||
모든 경우에 제어가 실행됩니다.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log("제어 조건 추가 버튼 클릭");
|
||||
actions.addControlCondition();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
조건 추가하기
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 컬럼 정보 로드 실패 시 안내 */}
|
||||
{!isLoading && fromColumns.length === 0 && toColumns.length === 0 && controlConditions.length === 0 && (
|
||||
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-yellow-800">컬럼 정보를 불러올 수 없습니다</h4>
|
||||
<div className="space-y-2 text-sm text-yellow-700">
|
||||
<p>• 외부 데이터베이스 연결에 문제가 있을 수 있습니다</p>
|
||||
<p>• 조건 없이 진행하면 항상 실행됩니다</p>
|
||||
<p>• 나중에 수동으로 조건을 추가할 수 있습니다</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
console.log("🔄 수동 조건 추가");
|
||||
actions.addControlCondition();
|
||||
}}
|
||||
className="mt-3 flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
수동으로 조건 추가
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 설정 요약 */}
|
||||
{controlConditions.length > 0 && (
|
||||
<div className="bg-muted/50 rounded-lg p-4">
|
||||
<h4 className="mb-3 text-sm font-medium">설정 요약</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>제어 실행 조건:</span>
|
||||
<Badge variant={controlConditions.length > 0 ? "default" : "secondary"}>
|
||||
{controlConditions.length > 0 ? `${controlConditions.length}개 조건` : "조건 없음"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>실행 방식:</span>
|
||||
<span className="text-muted-foreground">
|
||||
{controlConditions.length === 0 ? "항상 실행" : "조건부 실행"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단 네비게이션 */}
|
||||
<div className="flex-shrink-0 border-t bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="outline" onClick={onBack} className="flex items-center gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
|
||||
<Button onClick={onNext} disabled={!canProceed} className="flex items-center gap-2">
|
||||
다음: 액션 설정
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlConditionStep;
|
||||
Reference in New Issue
Block a user