473 lines
20 KiB
TypeScript
473 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 { 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,
|
|
fromColumns = [],
|
|
toColumns = [],
|
|
isLoading = false,
|
|
} = state;
|
|
const [availableCodes, setAvailableCodes] = useState<Record<string, CodeItem[]>>({});
|
|
|
|
// 컴포넌트 마운트 시 컬럼 로드
|
|
useEffect(() => {
|
|
if (
|
|
fromConnection &&
|
|
toConnection &&
|
|
fromTable &&
|
|
toTable &&
|
|
fromColumns.length === 0 &&
|
|
toColumns.length === 0 &&
|
|
!isLoading
|
|
) {
|
|
console.log("🔄 ControlConditionStep: 컬럼 로드 시작");
|
|
actions.loadColumns();
|
|
}
|
|
}, [
|
|
fromConnection?.id,
|
|
toConnection?.id,
|
|
fromTable?.tableName,
|
|
toTable?.tableName,
|
|
fromColumns.length,
|
|
toColumns.length,
|
|
isLoading,
|
|
]);
|
|
|
|
// 코드 타입 컬럼의 코드 로드
|
|
useEffect(() => {
|
|
const loadCodes = async () => {
|
|
const allColumns = [...fromColumns, ...toColumns];
|
|
const codeColumns = allColumns.filter((col) => {
|
|
// 메인 DB(connectionId === 0 또는 undefined)인 경우: column_labels의 input_type이 'code'인 경우만
|
|
if (col.connectionId === 0 || col.connectionId === undefined) {
|
|
return col.inputType === "code";
|
|
}
|
|
// 외부 DB인 경우: 코드 타입 없음
|
|
return false;
|
|
});
|
|
|
|
if (codeColumns.length === 0) return;
|
|
|
|
console.log(
|
|
"🔍 모든 컬럼 정보:",
|
|
allColumns.map((col) => ({
|
|
columnName: col.columnName,
|
|
connectionId: col.connectionId,
|
|
inputType: col.inputType,
|
|
webType: col.webType,
|
|
})),
|
|
);
|
|
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,
|
|
value: "", // 필드 변경 시 값 초기화
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-48">
|
|
<SelectValue placeholder="필드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__placeholder__" disabled>
|
|
필드 선택
|
|
</SelectItem>
|
|
{[...fromColumns, ...toColumns]
|
|
.filter(
|
|
(col, index, array) =>
|
|
col.columnName && // 빈 문자열 제외
|
|
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.connectionId === 0 || selectedField.connectionId === undefined) && // 임시: undefined도 메인 DB로 간주
|
|
selectedField.inputType === "code";
|
|
const fieldCodes = condition.field ? availableCodes[condition.field] : [];
|
|
|
|
// 디버깅 정보 출력
|
|
console.log("🔍 값 입력 필드 디버깅:", {
|
|
conditionField: condition.field,
|
|
selectedField: selectedField,
|
|
selectedFieldKeys: selectedField ? Object.keys(selectedField) : [],
|
|
webType: selectedField?.webType,
|
|
inputType: selectedField?.inputType,
|
|
connectionId: selectedField?.connectionId,
|
|
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}
|
|
</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 && fromColumns.length > 0 && toColumns.length > 0 && 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>
|
|
)}
|
|
|
|
{/* 컬럼 정보 로드 실패 시 안내 */}
|
|
{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;
|