463 lines
20 KiB
TypeScript
463 lines
20 KiB
TypeScript
"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;
|