"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 { Plus, Trash2, Settings } from "lucide-react"; // 타입 import import { ColumnInfo } from "@/lib/types/multiConnection"; import { getCodesForColumn, CodeItem } from "@/lib/api/codeManagement"; interface ActionCondition { id: string; field: string; operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "IS NULL" | "IS NOT NULL"; value: string; valueType?: "static" | "field" | "calculated"; // 값 타입 (고정값, 필드값, 계산값) logicalOperator?: "AND" | "OR"; } interface FieldValueMapping { id: string; targetField: string; valueType: "static" | "source_field" | "code" | "calculated"; value: string; sourceField?: string; codeCategory?: string; } interface ActionConditionBuilderProps { actionType: "insert" | "update" | "delete" | "upsert"; fromColumns: ColumnInfo[]; toColumns: ColumnInfo[]; conditions: ActionCondition[]; fieldMappings: FieldValueMapping[]; columnMappings?: any[]; // 컬럼 매핑 정보 (이미 매핑된 필드들) onConditionsChange: (conditions: ActionCondition[]) => void; onFieldMappingsChange: (mappings: FieldValueMapping[]) => void; showFieldMappings?: boolean; // 필드 매핑 섹션 표시 여부 } /** * 🎯 액션 조건 빌더 * - 실행 조건 설정 (WHERE 절) * - 필드 값 매핑 설정 (SET 절) * - 코드 타입 필드 지원 */ const ActionConditionBuilder: React.FC = ({ actionType, fromColumns, toColumns, conditions, fieldMappings, columnMappings = [], onConditionsChange, onFieldMappingsChange, showFieldMappings = true, }) => { const [availableCodes, setAvailableCodes] = useState>({}); // 컬럼 매핑인지 필드값 매핑인지 구분하는 함수 const isColumnMapping = (mapping: any): boolean => { return mapping.fromField && mapping.toField && mapping.fromField.columnName && mapping.toField.columnName; }; // 이미 컬럼 매핑된 필드들을 가져오는 함수 const getMappedFieldNames = (): string[] => { if (!columnMappings || columnMappings.length === 0) return []; return columnMappings.filter((mapping) => isColumnMapping(mapping)).map((mapping) => mapping.toField.columnName); }; // 매핑되지 않은 필드들만 필터링하는 함수 const getUnmappedToColumns = (): ColumnInfo[] => { const mappedFieldNames = getMappedFieldNames(); return toColumns.filter((column) => !mappedFieldNames.includes(column.columnName)); }; // 필드값 설정에서 사용 가능한 필드들 (이미 필드값 설정에서 사용된 필드 제외) const getAvailableFieldsForMapping = (currentIndex?: number): ColumnInfo[] => { const unmappedColumns = getUnmappedToColumns(); const usedFieldNames = fieldMappings .filter((_, index) => index !== currentIndex) // 현재 편집 중인 항목 제외 .map((mapping) => mapping.targetField) .filter((field) => field); // 빈 값 제외 return unmappedColumns.filter((column) => !usedFieldNames.includes(column.columnName)); }; const operators = [ { value: "=", label: "같음 (=)" }, { value: "!=", label: "다름 (!=)" }, { value: ">", label: "큼 (>)" }, { value: "<", label: "작음 (<)" }, { value: ">=", label: "크거나 같음 (>=)" }, { value: "<=", label: "작거나 같음 (<=)" }, { value: "LIKE", label: "포함 (LIKE)" }, { value: "IN", label: "목록 중 하나 (IN)" }, { value: "IS NULL", label: "빈 값 (IS NULL)" }, { value: "IS NOT NULL", label: "값 있음 (IS NOT NULL)" }, ]; // 코드 정보 로드 useEffect(() => { const loadCodes = async () => { const codeFields = [...fromColumns, ...toColumns].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; }); console.log( "🔍 ActionConditionBuilder - 모든 컬럼 정보:", [...fromColumns, ...toColumns].map((col) => ({ columnName: col.columnName, connectionId: col.connectionId, inputType: col.inputType, webType: col.webType, })), ); console.log("🔍 ActionConditionBuilder - 코드 타입 컬럼들:", codeFields); for (const field of codeFields) { try { const codes = await getCodesForColumn(field.columnName, field.webType, field.codeCategory); if (codes.length > 0) { setAvailableCodes((prev) => ({ ...prev, [field.columnName]: codes, })); } } catch (error) { console.error(`코드 로드 실패: ${field.columnName}`, error); } } }; if (fromColumns.length > 0 || toColumns.length > 0) { loadCodes(); } }, [fromColumns, toColumns]); // 컬럼 매핑이 변경될 때 필드값 설정에서 이미 매핑된 필드들 제거 useEffect(() => { const mappedFieldNames = getMappedFieldNames(); if (mappedFieldNames.length > 0) { const updatedFieldMappings = fieldMappings.filter((mapping) => !mappedFieldNames.includes(mapping.targetField)); // 변경된 내용이 있으면 업데이트 if (updatedFieldMappings.length !== fieldMappings.length) { console.log("🧹 매핑된 필드들을 필드값 설정에서 제거:", { removed: fieldMappings.filter((mapping) => mappedFieldNames.includes(mapping.targetField)), remaining: updatedFieldMappings, }); onFieldMappingsChange(updatedFieldMappings); } } }, [columnMappings]); // columnMappings 변경 시에만 실행 // 조건 추가 const addCondition = () => { const newCondition: ActionCondition = { id: Date.now().toString(), field: "", operator: "=", value: "", ...(conditions.length > 0 && { logicalOperator: "AND" }), }; onConditionsChange([...conditions, newCondition]); }; // 조건 업데이트 const updateCondition = (index: number, updates: Partial) => { const updatedConditions = conditions.map((condition, i) => i === index ? { ...condition, ...updates } : condition, ); onConditionsChange(updatedConditions); }; // 조건 삭제 const deleteCondition = (index: number) => { const updatedConditions = conditions.filter((_, i) => i !== index); onConditionsChange(updatedConditions); }; // 필드 매핑 추가 const addFieldMapping = () => { // 임시로 검증을 완화 - 매핑되지 않은 필드가 있으면 추가 허용 const unmappedColumns = getUnmappedToColumns(); console.log("🔍 필드 추가 시도:", { unmappedColumns, unmappedColumnsCount: unmappedColumns.length, fieldMappings, columnMappings, }); if (unmappedColumns.length === 0) { console.warn("매핑되지 않은 필드가 없습니다."); return; } const newMapping: FieldValueMapping = { id: Date.now().toString(), targetField: "", valueType: "static", value: "", }; console.log("✅ 새 필드 매핑 추가:", newMapping); onFieldMappingsChange([...fieldMappings, newMapping]); }; // 필드 매핑 업데이트 const updateFieldMapping = (index: number, updates: Partial) => { const updatedMappings = fieldMappings.map((mapping, i) => (i === index ? { ...mapping, ...updates } : mapping)); onFieldMappingsChange(updatedMappings); }; // 필드 매핑 삭제 const deleteFieldMapping = (index: number) => { const updatedMappings = fieldMappings.filter((_, i) => i !== index); onFieldMappingsChange(updatedMappings); }; // 필드의 값 입력 컴포넌트 렌더링 const renderValueInput = (mapping: FieldValueMapping, index: number, targetColumn?: ColumnInfo) => { if ( mapping.valueType === "code" && (targetColumn?.connectionId === 0 || targetColumn?.connectionId === undefined) && targetColumn?.inputType === "code" ) { const codes = availableCodes[targetColumn.columnName] || []; return ( ); } if (mapping.valueType === "source_field") { return ( ); } // 날짜 타입에 대한 특별 처리 if ( targetColumn?.webType === "date" || targetColumn?.webType === "datetime" || targetColumn?.dataType?.toLowerCase().includes("date") ) { return (
{/* 날짜 타입 선택 */} {/* 직접 입력이 선택된 경우 */} {(!mapping.value?.startsWith("#") || mapping.value === "#custom") && (
updateFieldMapping(index, { value: e.target.value })} />
상대적 날짜: +7D (7일 후), -30D (30일 전), +1M (1개월 후), +1Y (1년 후)
)} {/* 선택된 날짜 타입에 대한 설명 */} {mapping.value?.startsWith("#") && mapping.value !== "#custom" && (
{mapping.value === "#NOW" && "⏰ 현재 날짜와 시간이 저장됩니다"} {mapping.value === "#TODAY" && "📅 현재 날짜 (00:00:00)가 저장됩니다"} {mapping.value === "#YESTERDAY" && "📅 어제 날짜가 저장됩니다"} {mapping.value === "#TOMORROW" && "📅 내일 날짜가 저장됩니다"} {mapping.value === "#WEEK_START" && "📅 이번 주 월요일이 저장됩니다"} {mapping.value === "#MONTH_START" && "📅 이번 달 1일이 저장됩니다"} {mapping.value === "#YEAR_START" && "📅 올해 1월 1일이 저장됩니다"}
)}
); } // 숫자 타입에 대한 특별 처리 if ( targetColumn?.webType === "number" || targetColumn?.webType === "decimal" || targetColumn?.dataType?.toLowerCase().includes("int") || targetColumn?.dataType?.toLowerCase().includes("decimal") || targetColumn?.dataType?.toLowerCase().includes("numeric") ) { return (
{/* 숫자 타입 선택 */} {/* 직접 입력이 선택된 경우 */} {(!mapping.value?.startsWith("#") || mapping.value === "#custom") && ( updateFieldMapping(index, { value: e.target.value })} /> )} {/* 선택된 숫자 타입에 대한 설명 */} {mapping.value?.startsWith("#") && mapping.value !== "#custom" && (
{mapping.value === "#AUTO_INCREMENT" && "🔢 데이터베이스에서 자동으로 증가하는 값이 할당됩니다"} {mapping.value === "#RANDOM_INT" && "🎲 1부터 1000 사이의 랜덤한 정수가 생성됩니다"} {mapping.value === "#ZERO" && "0️⃣ 0 값이 저장됩니다"} {mapping.value === "#ONE" && "1️⃣ 1 값이 저장됩니다"} {mapping.value === "#SEQUENCE" && "📈 시퀀스에서 다음 값을 가져옵니다"}
)}
); } return ( updateFieldMapping(index, { value: e.target.value })} /> ); }; return (
{/* 실행 조건 설정 */} {actionType !== "insert" && ( 실행 조건 (WHERE) {conditions.length === 0 ? (

{actionType.toUpperCase()} 액션의 실행 조건을 설정하세요

) : ( conditions.map((condition, index) => (
{/* 논리 연산자 */} {index > 0 && ( )} {/* 필드 선택 */} {/* 연산자 선택 */} {/* 값 입력 */} {!["IS NULL", "IS NOT NULL"].includes(condition.operator) && (() => { // FROM/TO 테이블 컬럼 구분 let fieldColumn; let actualFieldName; if (condition.field?.startsWith("from.")) { actualFieldName = condition.field.replace("from.", ""); fieldColumn = fromColumns.find((col) => col.columnName === actualFieldName); } else if (condition.field?.startsWith("to.")) { actualFieldName = condition.field.replace("to.", ""); fieldColumn = toColumns.find((col) => col.columnName === actualFieldName); } else { // 기존 호환성을 위해 TO 테이블에서 먼저 찾기 actualFieldName = condition.field; fieldColumn = toColumns.find((col) => col.columnName === condition.field) || fromColumns.find((col) => col.columnName === condition.field); } const fieldCodes = availableCodes[actualFieldName]; // 코드 타입 필드면 코드 선택 if (fieldColumn?.webType === "code" && fieldCodes?.length > 0) { return ( ); } // 값 타입 선택 (고정값, 다른 필드 값, 계산식 등) return (
{/* 값 타입 선택 */} {/* 값 입력 */} {condition.valueType === "field" ? ( ) : ( updateCondition(index, { value: e.target.value })} className="flex-1" /> )}
); })()} {/* 삭제 버튼 */}
)) )}
)} {/* 필드 값 매핑 설정 */} {showFieldMappings && actionType !== "delete" && (
필드 값 설정 (SET)

매핑되지 않은 필드의 기본값 설정

{/* 매핑되지 않은 필드가 없는 경우 */} {getUnmappedToColumns().length === 0 ? (
✅ 모든 필드가 매핑되었습니다

컬럼 매핑으로 모든 TO 테이블 필드가 처리되고 있어 별도의 기본값 설정이 필요하지 않습니다.

) : fieldMappings.length === 0 ? (

매핑되지 않은 필드의 기본값을 설정하세요

컬럼 매핑으로 처리되지 않은 필드들만 여기서 설정됩니다

현재 {getUnmappedToColumns().length}개 필드가 매핑되지 않음

) : ( (() => { console.log("🎨 필드값 설정 렌더링:", { fieldMappings, fieldMappingsCount: fieldMappings.length, }); return fieldMappings.map((mapping, index) => { const targetColumn = toColumns.find((col) => col.columnName === mapping.targetField); return (
{/* 대상 필드 */} {/* 값 타입 */} {/* 값 입력 */}
{renderValueInput(mapping, index, targetColumn)}
{/* 삭제 버튼 */}
); }); })() )}
)}
); }; export default ActionConditionBuilder;