제어관리 데이터 저장기능
This commit is contained in:
@@ -36,6 +36,7 @@ interface ActionConditionBuilderProps {
|
||||
toColumns: ColumnInfo[];
|
||||
conditions: ActionCondition[];
|
||||
fieldMappings: FieldValueMapping[];
|
||||
columnMappings?: any[]; // 컬럼 매핑 정보 (이미 매핑된 필드들)
|
||||
onConditionsChange: (conditions: ActionCondition[]) => void;
|
||||
onFieldMappingsChange: (mappings: FieldValueMapping[]) => void;
|
||||
showFieldMappings?: boolean; // 필드 매핑 섹션 표시 여부
|
||||
@@ -53,12 +54,41 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
toColumns,
|
||||
conditions,
|
||||
fieldMappings,
|
||||
columnMappings = [],
|
||||
onConditionsChange,
|
||||
onFieldMappingsChange,
|
||||
showFieldMappings = true,
|
||||
}) => {
|
||||
const [availableCodes, setAvailableCodes] = useState<Record<string, CodeItem[]>>({});
|
||||
|
||||
// 컬럼 매핑인지 필드값 매핑인지 구분하는 함수
|
||||
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: "다름 (!=)" },
|
||||
@@ -75,9 +105,25 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
// 코드 정보 로드
|
||||
useEffect(() => {
|
||||
const loadCodes = async () => {
|
||||
const codeFields = [...fromColumns, ...toColumns].filter(
|
||||
(col) => col.webType === "code" || col.dataType?.toLowerCase().includes("code"),
|
||||
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 {
|
||||
@@ -100,6 +146,23 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
}
|
||||
}, [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 = {
|
||||
@@ -129,6 +192,20 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
|
||||
// 필드 매핑 추가
|
||||
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: "",
|
||||
@@ -136,6 +213,7 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
value: "",
|
||||
};
|
||||
|
||||
console.log("✅ 새 필드 매핑 추가:", newMapping);
|
||||
onFieldMappingsChange([...fieldMappings, newMapping]);
|
||||
};
|
||||
|
||||
@@ -153,7 +231,11 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
|
||||
// 필드의 값 입력 컴포넌트 렌더링
|
||||
const renderValueInput = (mapping: FieldValueMapping, index: number, targetColumn?: ColumnInfo) => {
|
||||
if (mapping.valueType === "code" && targetColumn?.webType === "code") {
|
||||
if (
|
||||
mapping.valueType === "code" &&
|
||||
(targetColumn?.connectionId === 0 || targetColumn?.connectionId === undefined) &&
|
||||
targetColumn?.inputType === "code"
|
||||
) {
|
||||
const codes = availableCodes[targetColumn.columnName] || [];
|
||||
|
||||
return (
|
||||
@@ -164,12 +246,7 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
<SelectContent>
|
||||
{codes.map((code) => (
|
||||
<SelectItem key={code.code} value={code.code}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{code.code}
|
||||
</Badge>
|
||||
<span>{code.name}</span>
|
||||
</div>
|
||||
{code.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -227,6 +304,129 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// 날짜 타입에 대한 특별 처리
|
||||
if (
|
||||
targetColumn?.webType === "date" ||
|
||||
targetColumn?.webType === "datetime" ||
|
||||
targetColumn?.dataType?.toLowerCase().includes("date")
|
||||
) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* 날짜 타입 선택 */}
|
||||
<Select
|
||||
value={mapping.value?.startsWith("#") ? mapping.value : "#custom"}
|
||||
onValueChange={(value) => {
|
||||
if (value === "#custom") {
|
||||
updateFieldMapping(index, { value: "" });
|
||||
} else {
|
||||
updateFieldMapping(index, { value });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="날짜 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="#NOW">🕐 현재 시간 (NOW)</SelectItem>
|
||||
<SelectItem value="#TODAY">📅 오늘 날짜 (TODAY)</SelectItem>
|
||||
<SelectItem value="#YESTERDAY">📅 어제 날짜</SelectItem>
|
||||
<SelectItem value="#TOMORROW">📅 내일 날짜</SelectItem>
|
||||
<SelectItem value="#WEEK_START">📅 이번 주 시작일</SelectItem>
|
||||
<SelectItem value="#MONTH_START">📅 이번 달 시작일</SelectItem>
|
||||
<SelectItem value="#YEAR_START">📅 올해 시작일</SelectItem>
|
||||
<SelectItem value="#custom">✏️ 직접 입력</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 직접 입력이 선택된 경우 */}
|
||||
{(!mapping.value?.startsWith("#") || mapping.value === "#custom") && (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type={targetColumn?.webType === "datetime" ? "datetime-local" : "date"}
|
||||
placeholder="날짜 입력"
|
||||
value={mapping.value?.startsWith("#") ? "" : mapping.value}
|
||||
onChange={(e) => updateFieldMapping(index, { value: e.target.value })}
|
||||
/>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
상대적 날짜: +7D (7일 후), -30D (30일 전), +1M (1개월 후), +1Y (1년 후)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 선택된 날짜 타입에 대한 설명 */}
|
||||
{mapping.value?.startsWith("#") && mapping.value !== "#custom" && (
|
||||
<div className="text-muted-foreground rounded bg-blue-50 p-2 text-xs">
|
||||
{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일이 저장됩니다"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 숫자 타입에 대한 특별 처리
|
||||
if (
|
||||
targetColumn?.webType === "number" ||
|
||||
targetColumn?.webType === "decimal" ||
|
||||
targetColumn?.dataType?.toLowerCase().includes("int") ||
|
||||
targetColumn?.dataType?.toLowerCase().includes("decimal") ||
|
||||
targetColumn?.dataType?.toLowerCase().includes("numeric")
|
||||
) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* 숫자 타입 선택 */}
|
||||
<Select
|
||||
value={mapping.value?.startsWith("#") ? mapping.value : "#custom"}
|
||||
onValueChange={(value) => {
|
||||
if (value === "#custom") {
|
||||
updateFieldMapping(index, { value: "" });
|
||||
} else {
|
||||
updateFieldMapping(index, { value });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="숫자 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="#AUTO_INCREMENT">🔢 자동 증가 (AUTO_INCREMENT)</SelectItem>
|
||||
<SelectItem value="#RANDOM_INT">🎲 랜덤 정수 (1-1000)</SelectItem>
|
||||
<SelectItem value="#ZERO">0️⃣ 0</SelectItem>
|
||||
<SelectItem value="#ONE">1️⃣ 1</SelectItem>
|
||||
<SelectItem value="#SEQUENCE">📈 시퀀스값</SelectItem>
|
||||
<SelectItem value="#custom">✏️ 직접 입력</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 직접 입력이 선택된 경우 */}
|
||||
{(!mapping.value?.startsWith("#") || mapping.value === "#custom") && (
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="숫자 입력"
|
||||
value={mapping.value?.startsWith("#") ? "" : mapping.value}
|
||||
onChange={(e) => updateFieldMapping(index, { value: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 선택된 숫자 타입에 대한 설명 */}
|
||||
{mapping.value?.startsWith("#") && mapping.value !== "#custom" && (
|
||||
<div className="text-muted-foreground rounded bg-green-50 p-2 text-xs">
|
||||
{mapping.value === "#AUTO_INCREMENT" && "🔢 데이터베이스에서 자동으로 증가하는 값이 할당됩니다"}
|
||||
{mapping.value === "#RANDOM_INT" && "🎲 1부터 1000 사이의 랜덤한 정수가 생성됩니다"}
|
||||
{mapping.value === "#ZERO" && "0️⃣ 0 값이 저장됩니다"}
|
||||
{mapping.value === "#ONE" && "1️⃣ 1 값이 저장됩니다"}
|
||||
{mapping.value === "#SEQUENCE" && "📈 시퀀스에서 다음 값을 가져옵니다"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
placeholder="값 입력"
|
||||
@@ -467,8 +667,16 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center justify-between text-base">
|
||||
<span>필드 값 설정 (SET)</span>
|
||||
<Button variant="outline" size="sm" onClick={addFieldMapping}>
|
||||
<div>
|
||||
<span>필드 값 설정 (SET)</span>
|
||||
<p className="text-muted-foreground mt-1 text-xs">매핑되지 않은 필드의 기본값 설정</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addFieldMapping}
|
||||
disabled={getUnmappedToColumns().length === 0}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
필드 추가
|
||||
</Button>
|
||||
@@ -476,65 +684,98 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{fieldMappings.length === 0 ? (
|
||||
{/* 매핑되지 않은 필드가 없는 경우 */}
|
||||
{getUnmappedToColumns().length === 0 ? (
|
||||
<div className="rounded-lg border bg-green-50 p-4 text-center">
|
||||
<div className="mb-2 text-green-600">✅ 모든 필드가 매핑되었습니다</div>
|
||||
<p className="text-sm text-green-700">
|
||||
컬럼 매핑으로 모든 TO 테이블 필드가 처리되고 있어 별도의 기본값 설정이 필요하지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
) : fieldMappings.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 text-sm">매핑되지 않은 필드의 기본값을 설정하세요</p>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
컬럼 매핑으로 처리되지 않은 필드들만 여기서 설정됩니다
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
현재 {getUnmappedToColumns().length}개 필드가 매핑되지 않음
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
fieldMappings.map((mapping, index) => {
|
||||
const targetColumn = toColumns.find((col) => col.columnName === mapping.targetField);
|
||||
(() => {
|
||||
console.log("🎨 필드값 설정 렌더링:", {
|
||||
fieldMappings,
|
||||
fieldMappingsCount: fieldMappings.length,
|
||||
});
|
||||
return fieldMappings.map((mapping, index) => {
|
||||
const targetColumn = toColumns.find((col) => col.columnName === mapping.targetField);
|
||||
|
||||
return (
|
||||
<div key={mapping.id} className="flex items-center gap-3 rounded-lg border p-3">
|
||||
{/* 대상 필드 */}
|
||||
<Select
|
||||
value={mapping.targetField}
|
||||
onValueChange={(value) => updateFieldMapping(index, { targetField: value })}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="대상 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{toColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.webType || column.dataType}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
return (
|
||||
<div key={mapping.id} className="flex items-center gap-3 rounded-lg border p-3">
|
||||
{/* 대상 필드 */}
|
||||
<Select
|
||||
value={mapping.targetField}
|
||||
onValueChange={(value) =>
|
||||
updateFieldMapping(index, {
|
||||
targetField: value,
|
||||
value: "", // 필드 변경 시 값 초기화
|
||||
sourceField: "", // 소스 필드도 초기화
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="대상 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getAvailableFieldsForMapping(index).map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.webType || column.dataType}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 값 타입 */}
|
||||
<Select
|
||||
value={mapping.valueType}
|
||||
onValueChange={(value) => updateFieldMapping(index, { valueType: value as any })}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="static">고정값</SelectItem>
|
||||
<SelectItem value="source_field">소스필드</SelectItem>
|
||||
{targetColumn?.webType === "code" && <SelectItem value="code">코드선택</SelectItem>}
|
||||
<SelectItem value="calculated">계산식</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* 값 타입 */}
|
||||
<Select
|
||||
value={mapping.valueType}
|
||||
onValueChange={(value) =>
|
||||
updateFieldMapping(index, {
|
||||
valueType: value as any,
|
||||
value: "", // 값 타입 변경 시 값 초기화
|
||||
sourceField: "", // 소스 필드도 초기화
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="static">고정값</SelectItem>
|
||||
<SelectItem value="source_field">소스필드</SelectItem>
|
||||
{(targetColumn?.connectionId === 0 || targetColumn?.connectionId === undefined) &&
|
||||
targetColumn?.inputType === "code" && <SelectItem value="code">코드선택</SelectItem>}
|
||||
<SelectItem value="calculated">계산식</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 값 입력 */}
|
||||
<div className="flex-1">{renderValueInput(mapping, index, targetColumn)}</div>
|
||||
{/* 값 입력 */}
|
||||
<div className="flex-1">{renderValueInput(mapping, index, targetColumn)}</div>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button variant="ghost" size="sm" onClick={() => deleteFieldMapping(index)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
{/* 삭제 버튼 */}
|
||||
<Button variant="ghost" size="sm" onClick={() => deleteFieldMapping(index)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user