"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Ban, Check, ChevronsUpDown, Plus, RefreshCw, Search, Pencil, Trash2 } from "lucide-react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { mutualExclusionApi, MutualExclusion, EXCLUSION_TYPES } from "@/lib/api/cascadingMutualExclusion"; import { tableManagementApi } from "@/lib/api/tableManagement"; export default function MutualExclusionTab() { // 목록 상태 const [exclusions, setExclusions] = useState([]); const [tables, setTables] = useState>([]); const [columns, setColumns] = useState>([]); const [loading, setLoading] = useState(true); const [searchText, setSearchText] = useState(""); // 테이블 Combobox 상태 const [tableComboOpen, setTableComboOpen] = useState(false); // 모달 상태 const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [editingExclusion, setEditingExclusion] = useState(null); const [deletingExclusionId, setDeletingExclusionId] = useState(null); // 폼 데이터 const [formData, setFormData] = useState>({ exclusionName: "", fieldNames: "", sourceTable: "", valueColumn: "", labelColumn: "", exclusionType: "SAME_VALUE", errorMessage: "동일한 값을 선택할 수 없습니다", }); // 필드 목록 (동적 추가) const [fieldList, setFieldList] = useState(["", ""]); // 목록 로드 const loadExclusions = useCallback(async () => { setLoading(true); try { const response = await mutualExclusionApi.getList(); if (response.success && response.data) { setExclusions(response.data); } } catch (error) { console.error("상호 배제 목록 로드 실패:", error); toast.error("목록을 불러오는데 실패했습니다."); } finally { setLoading(false); } }, []); // 테이블 목록 로드 const loadTables = useCallback(async () => { try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setTables(response.data); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } }, []); useEffect(() => { loadExclusions(); loadTables(); }, [loadExclusions, loadTables]); // 테이블 선택 시 컬럼 로드 const loadColumns = async (tableName: string) => { if (!tableName) { setColumns([]); return; } try { const response = await tableManagementApi.getColumnList(tableName); if (response.success && response.data?.columns) { setColumns(response.data.columns); } } catch (error) { console.error("컬럼 목록 로드 실패:", error); } }; // 필터된 목록 const filteredExclusions = exclusions.filter( (e) => e.exclusionName?.toLowerCase().includes(searchText.toLowerCase()) || e.exclusionCode?.toLowerCase().includes(searchText.toLowerCase()), ); // 모달 열기 (생성) const handleOpenCreate = () => { setEditingExclusion(null); setFormData({ exclusionName: "", fieldNames: "", sourceTable: "", valueColumn: "", labelColumn: "", exclusionType: "SAME_VALUE", errorMessage: "동일한 값을 선택할 수 없습니다", }); setFieldList(["", ""]); setColumns([]); setIsModalOpen(true); }; // 모달 열기 (수정) const handleOpenEdit = async (exclusion: MutualExclusion) => { setEditingExclusion(exclusion); setFormData({ exclusionCode: exclusion.exclusionCode, exclusionName: exclusion.exclusionName, fieldNames: exclusion.fieldNames, sourceTable: exclusion.sourceTable, valueColumn: exclusion.valueColumn, labelColumn: exclusion.labelColumn || "", exclusionType: exclusion.exclusionType || "SAME_VALUE", errorMessage: exclusion.errorMessage || "동일한 값을 선택할 수 없습니다", }); setFieldList(exclusion.fieldNames.split(",").map((f) => f.trim())); await loadColumns(exclusion.sourceTable); setIsModalOpen(true); }; // 삭제 확인 const handleDeleteConfirm = (exclusionId: number) => { setDeletingExclusionId(exclusionId); setIsDeleteDialogOpen(true); }; // 삭제 실행 const handleDelete = async () => { if (!deletingExclusionId) return; try { const response = await mutualExclusionApi.delete(deletingExclusionId); if (response.success) { toast.success("상호 배제 규칙이 삭제되었습니다."); loadExclusions(); } else { toast.error(response.error || "삭제에 실패했습니다."); } } catch (error) { toast.error("삭제 중 오류가 발생했습니다."); } finally { setIsDeleteDialogOpen(false); setDeletingExclusionId(null); } }; // 필드 추가 const addField = () => { setFieldList([...fieldList, ""]); }; // 필드 제거 const removeField = (index: number) => { if (fieldList.length <= 2) { toast.error("최소 2개의 필드가 필요합니다."); return; } setFieldList(fieldList.filter((_, i) => i !== index)); }; // 필드 값 변경 const updateField = (index: number, value: string) => { const newFields = [...fieldList]; newFields[index] = value; setFieldList(newFields); }; // 저장 const handleSave = async () => { // 필드 목록 합치기 const cleanedFields = fieldList.filter((f) => f.trim()); if (cleanedFields.length < 2) { toast.error("최소 2개의 필드를 입력해주세요."); return; } // 유효성 검사 if (!formData.exclusionName || !formData.sourceTable || !formData.valueColumn) { toast.error("필수 항목을 모두 입력해주세요."); return; } const dataToSave = { ...formData, fieldNames: cleanedFields.join(","), }; try { let response; if (editingExclusion) { response = await mutualExclusionApi.update(editingExclusion.exclusionId!, dataToSave); } else { response = await mutualExclusionApi.create(dataToSave); } if (response.success) { toast.success(editingExclusion ? "수정되었습니다." : "생성되었습니다."); setIsModalOpen(false); loadExclusions(); } else { toast.error(response.error || "저장에 실패했습니다."); } } catch (error) { toast.error("저장 중 오류가 발생했습니다."); } }; // 테이블 선택 핸들러 const handleTableChange = async (tableName: string) => { setFormData({ ...formData, sourceTable: tableName, valueColumn: "", labelColumn: "" }); await loadColumns(tableName); }; return (
{/* 검색 */}
setSearchText(e.target.value)} className="pl-10" />
{/* 목록 */}
상호 배제 규칙 두 필드가 같은 값을 선택할 수 없도록 제한합니다. (총 {filteredExclusions.length}개)
{loading ? (
로딩 중...
) : filteredExclusions.length === 0 ? (
{searchText ? "검색 결과가 없습니다." : "등록된 상호 배제 규칙이 없습니다."}
예시: 창고 이동
"출발 창고"와 "도착 창고"가 같은 창고를 선택할 수 없도록 제한
예시: 부서 이동
"현재 부서"와 "이동 부서"가 같은 부서를 선택할 수 없도록 제한
) : ( 배제 코드 배제명 대상 필드 소스 테이블 상태 작업 {filteredExclusions.map((exclusion) => ( {exclusion.exclusionCode} {exclusion.exclusionName}
{exclusion.fieldNames.split(",").map((field, idx) => ( {field.trim()} ))}
{exclusion.sourceTable} {exclusion.isActive === "Y" ? "활성" : "비활성"}
))}
)}
{/* 생성/수정 모달 */} {editingExclusion ? "상호 배제 규칙 수정" : "상호 배제 규칙 생성"} 두 필드가 같은 값을 선택할 수 없도록 제한합니다.
{/* 배제명 */}
setFormData({ ...formData, exclusionName: e.target.value })} placeholder="예: 창고 이동 제한" />
{/* 대상 필드 */}

대상 필드 (최소 2개)

{fieldList.map((field, index) => (
updateField(index, e.target.value)} placeholder={`필드 ${index + 1} (예: source_warehouse)`} className="flex-1" /> {fieldList.length > 2 && ( )}
))}

이 필드들은 서로 같은 값을 선택할 수 없습니다.

{/* 소스 테이블 및 컬럼 */}
테이블을 찾을 수 없습니다. {tables .filter((t) => t.tableName) .map((t) => ( { setFormData({ ...formData, sourceTable: t.tableName, valueColumn: "", labelColumn: "", }); await loadColumns(t.tableName); setTableComboOpen(false); }} className="text-sm" >
{t.displayName || t.tableName} {t.displayName && t.displayName !== t.tableName && ( {t.tableName} )}
))}
{/* 에러 메시지 */}
setFormData({ ...formData, errorMessage: e.target.value })} placeholder="동일한 값을 선택할 수 없습니다" />
{/* 삭제 확인 다이얼로그 */} 상호 배제 규칙 삭제 이 상호 배제 규칙을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. 취소 삭제
); }