"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { Plus, Pencil, Trash2, Link2, RefreshCw, Search, ChevronRight, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { cascadingRelationApi, CascadingRelation, CascadingRelationCreateInput } from "@/lib/api/cascadingRelation"; import { tableManagementApi } from "@/lib/api/tableManagement"; interface TableInfo { tableName: string; tableLabel?: string; } interface ColumnInfo { columnName: string; columnLabel?: string; } export default function CascadingRelationsPage() { // 목록 상태 const [relations, setRelations] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); // 모달 상태 const [isModalOpen, setIsModalOpen] = useState(false); const [editingRelation, setEditingRelation] = useState(null); const [saving, setSaving] = useState(false); // 테이블/컬럼 목록 const [tableList, setTableList] = useState([]); const [parentColumns, setParentColumns] = useState([]); const [childColumns, setChildColumns] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [loadingParentColumns, setLoadingParentColumns] = useState(false); const [loadingChildColumns, setLoadingChildColumns] = useState(false); // 폼 상태 const [formData, setFormData] = useState({ relationCode: "", relationName: "", description: "", parentTable: "", parentValueColumn: "", parentLabelColumn: "", childTable: "", childFilterColumn: "", childValueColumn: "", childLabelColumn: "", childOrderColumn: "", childOrderDirection: "ASC", emptyParentMessage: "상위 항목을 먼저 선택하세요", noOptionsMessage: "선택 가능한 항목이 없습니다", loadingMessage: "로딩 중...", clearOnParentChange: true, }); // 고급 설정 토글 const [showAdvanced, setShowAdvanced] = useState(false); // 목록 조회 const loadRelations = useCallback(async () => { setLoading(true); try { const response = await cascadingRelationApi.getList("Y"); if (response.success && response.data) { setRelations(response.data); } } catch (error) { toast.error("연쇄 관계 목록 조회에 실패했습니다."); } finally { setLoading(false); } }, []); // 테이블 목록 조회 const loadTableList = useCallback(async () => { setLoadingTables(true); try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setTableList( response.data.map((t: any) => ({ tableName: t.tableName || t.name, tableLabel: t.tableLabel || t.displayName || t.tableName || t.name, })), ); } } catch (error) { console.error("테이블 목록 조회 실패:", error); } finally { setLoadingTables(false); } }, []); // 컬럼 목록 조회 (수정됨) const loadColumns = useCallback(async (tableName: string, type: "parent" | "child") => { if (!tableName) return; if (type === "parent") { setLoadingParentColumns(true); setParentColumns([]); } else { setLoadingChildColumns(true); setChildColumns([]); } try { // getColumnList 사용 (getTableColumns가 아님) const response = await tableManagementApi.getColumnList(tableName); console.log(`컬럼 목록 조회 (${tableName}):`, response); if (response.success && response.data) { // 응답 구조: { data: { columns: [...] } } const columnList = response.data.columns || response.data; const columns = (Array.isArray(columnList) ? columnList : []).map((c: any) => ({ columnName: c.columnName || c.name, columnLabel: c.columnLabel || c.label || c.columnName || c.name, })); if (type === "parent") { setParentColumns(columns); // 자동 추천: id, code, _id, _code로 끝나는 컬럼 autoSelectColumn(columns, "parentValueColumn", ["id", "code", "_id", "_code"]); } else { setChildColumns(columns); // 자동 추천 autoSelectColumn(columns, "childValueColumn", ["id", "code", "_id", "_code"]); autoSelectColumn(columns, "childLabelColumn", ["name", "label", "_name", "description"]); } } } catch (error) { console.error("컬럼 목록 조회 실패:", error); toast.error(`${tableName} 테이블의 컬럼을 불러오지 못했습니다.`); } finally { if (type === "parent") { setLoadingParentColumns(false); } else { setLoadingChildColumns(false); } } }, []); // 수정 모드용 컬럼 로드 (자동 선택 없음) const loadColumnsForEdit = async (tableName: string, type: "parent" | "child") => { if (!tableName) return; if (type === "parent") { setLoadingParentColumns(true); } else { setLoadingChildColumns(true); } try { const response = await tableManagementApi.getColumnList(tableName); if (response.success && response.data) { const columnList = response.data.columns || response.data; const columns = (Array.isArray(columnList) ? columnList : []).map((c: any) => ({ columnName: c.columnName || c.name, columnLabel: c.columnLabel || c.label || c.columnName || c.name, })); if (type === "parent") { setParentColumns(columns); } else { setChildColumns(columns); } } } catch (error) { console.error("컬럼 목록 조회 실패:", error); } finally { if (type === "parent") { setLoadingParentColumns(false); } else { setLoadingChildColumns(false); } } }; // 자동 컬럼 선택 (패턴 매칭) const autoSelectColumn = (columns: ColumnInfo[], field: keyof CascadingRelationCreateInput, patterns: string[]) => { // 이미 값이 있으면 스킵 if (formData[field]) return; for (const pattern of patterns) { const found = columns.find((c) => c.columnName.toLowerCase().endsWith(pattern.toLowerCase())); if (found) { setFormData((prev) => ({ ...prev, [field]: found.columnName })); return; } } }; useEffect(() => { loadRelations(); loadTableList(); }, [loadRelations, loadTableList]); // 부모 테이블 변경 시 컬럼 로드 (수정 모드가 아닐 때만) useEffect(() => { // 수정 모드에서는 handleOpenEdit에서 직접 로드하므로 스킵 if (editingRelation) return; if (formData.parentTable) { loadColumns(formData.parentTable, "parent"); } else { setParentColumns([]); } }, [formData.parentTable, editingRelation]); // 자식 테이블 변경 시 컬럼 로드 (수정 모드가 아닐 때만) useEffect(() => { // 수정 모드에서는 handleOpenEdit에서 직접 로드하므로 스킵 if (editingRelation) return; if (formData.childTable) { loadColumns(formData.childTable, "child"); } else { setChildColumns([]); } }, [formData.childTable, editingRelation]); // 관계 코드 자동 생성 const generateRelationCode = (parentTable: string, childTable: string) => { if (!parentTable || !childTable) return ""; const parent = parentTable.replace(/_mng$|_info$|_master$/i, "").toUpperCase(); const child = childTable.replace(/_mng$|_info$|_master$/i, "").toUpperCase(); return `${parent}_${child}`; }; // 관계명 자동 생성 const generateRelationName = (parentTable: string, childTable: string) => { if (!parentTable || !childTable) return ""; const parentInfo = tableList.find((t) => t.tableName === parentTable); const childInfo = tableList.find((t) => t.tableName === childTable); const parentName = parentInfo?.tableLabel || parentTable; const childName = childInfo?.tableLabel || childTable; return `${parentName}-${childName}`; }; // 모달 열기 (신규) const handleOpenCreate = () => { setEditingRelation(null); setFormData({ relationCode: "", relationName: "", description: "", parentTable: "", parentValueColumn: "", parentLabelColumn: "", childTable: "", childFilterColumn: "", childValueColumn: "", childLabelColumn: "", childOrderColumn: "", childOrderDirection: "ASC", emptyParentMessage: "상위 항목을 먼저 선택하세요", noOptionsMessage: "선택 가능한 항목이 없습니다", loadingMessage: "로딩 중...", clearOnParentChange: true, }); setParentColumns([]); setChildColumns([]); setShowAdvanced(false); setIsModalOpen(true); }; // 모달 열기 (수정) const handleOpenEdit = async (relation: CascadingRelation) => { setEditingRelation(relation); setShowAdvanced(false); // 먼저 컬럼 목록을 로드 (모달 열기 전) const loadPromises: Promise[] = []; if (relation.parent_table) { loadPromises.push(loadColumnsForEdit(relation.parent_table, "parent")); } if (relation.child_table) { loadPromises.push(loadColumnsForEdit(relation.child_table, "child")); } // 컬럼 로드 완료 대기 await Promise.all(loadPromises); // 컬럼 로드 후 formData 설정 (이렇게 해야 Select에서 값이 제대로 표시됨) setFormData({ relationCode: relation.relation_code, relationName: relation.relation_name, description: relation.description || "", parentTable: relation.parent_table, parentValueColumn: relation.parent_value_column, parentLabelColumn: relation.parent_label_column || "", childTable: relation.child_table, childFilterColumn: relation.child_filter_column, childValueColumn: relation.child_value_column, childLabelColumn: relation.child_label_column, childOrderColumn: relation.child_order_column || "", childOrderDirection: relation.child_order_direction || "ASC", emptyParentMessage: relation.empty_parent_message || "상위 항목을 먼저 선택하세요", noOptionsMessage: relation.no_options_message || "선택 가능한 항목이 없습니다", loadingMessage: relation.loading_message || "로딩 중...", clearOnParentChange: relation.clear_on_parent_change === "Y", }); setIsModalOpen(true); }; // 부모 테이블 선택 시 자동 설정 const handleParentTableChange = async (value: string) => { // 테이블이 변경되면 컬럼 초기화 (같은 테이블이면 유지) const shouldClearColumns = value !== formData.parentTable; setFormData((prev) => ({ ...prev, parentTable: value, parentValueColumn: shouldClearColumns ? "" : prev.parentValueColumn, parentLabelColumn: shouldClearColumns ? "" : prev.parentLabelColumn, })); // 수정 모드에서 테이블 변경 시 컬럼 로드 if (editingRelation && value) { await loadColumnsForEdit(value, "parent"); } }; // 자식 테이블 선택 시 자동 설정 const handleChildTableChange = async (value: string) => { // 테이블이 변경되면 컬럼 초기화 (같은 테이블이면 유지) const shouldClearColumns = value !== formData.childTable; const newFormData = { ...formData, childTable: value, childFilterColumn: shouldClearColumns ? "" : formData.childFilterColumn, childValueColumn: shouldClearColumns ? "" : formData.childValueColumn, childLabelColumn: shouldClearColumns ? "" : formData.childLabelColumn, childOrderColumn: shouldClearColumns ? "" : formData.childOrderColumn, }; // 관계 코드/이름 자동 생성 (신규 모드에서만) if (!editingRelation) { newFormData.relationCode = generateRelationCode(formData.parentTable, value); newFormData.relationName = generateRelationName(formData.parentTable, value); } setFormData(newFormData); // 수정 모드에서 테이블 변경 시 컬럼 로드 if (editingRelation && value) { await loadColumnsForEdit(value, "child"); } }; // 저장 const handleSave = async () => { // 필수 필드 검증 if (!formData.parentTable || !formData.parentValueColumn) { toast.error("부모 테이블과 값 컬럼을 선택해주세요."); return; } if ( !formData.childTable || !formData.childFilterColumn || !formData.childValueColumn || !formData.childLabelColumn ) { toast.error("자식 테이블 설정을 완료해주세요."); return; } // 관계 코드/이름 자동 생성 (비어있으면) const finalData = { ...formData }; if (!finalData.relationCode) { finalData.relationCode = generateRelationCode(formData.parentTable, formData.childTable); } if (!finalData.relationName) { finalData.relationName = generateRelationName(formData.parentTable, formData.childTable); } setSaving(true); try { let response; if (editingRelation) { response = await cascadingRelationApi.update(editingRelation.relation_id, finalData); } else { response = await cascadingRelationApi.create(finalData); } if (response.success) { toast.success(editingRelation ? "연쇄 관계가 수정되었습니다." : "연쇄 관계가 생성되었습니다."); setIsModalOpen(false); loadRelations(); } else { toast.error(response.message || "저장에 실패했습니다."); } } catch (error) { toast.error("저장 중 오류가 발생했습니다."); } finally { setSaving(false); } }; // 삭제 const handleDelete = async (relation: CascadingRelation) => { if (!confirm(`"${relation.relation_name}" 관계를 삭제하시겠습니까?`)) { return; } try { const response = await cascadingRelationApi.delete(relation.relation_id); if (response.success) { toast.success("연쇄 관계가 삭제되었습니다."); loadRelations(); } else { toast.error(response.message || "삭제에 실패했습니다."); } } catch (error) { toast.error("삭제 중 오류가 발생했습니다."); } }; // 필터링된 목록 const filteredRelations = relations.filter( (r) => r.relation_code.toLowerCase().includes(searchTerm.toLowerCase()) || r.relation_name.toLowerCase().includes(searchTerm.toLowerCase()) || r.parent_table.toLowerCase().includes(searchTerm.toLowerCase()) || r.child_table.toLowerCase().includes(searchTerm.toLowerCase()), ); // 컬럼 셀렉트 렌더링 헬퍼 const renderColumnSelect = ( value: string, onChange: (v: string) => void, columns: ColumnInfo[], loading: boolean, placeholder: string, disabled?: boolean, ) => ( ); return (
연쇄 관계 관리 연쇄 드롭다운에서 사용할 테이블 간 관계를 정의합니다.
{/* 검색 */}
setSearchTerm(e.target.value)} className="pl-10" />
{/* 테이블 */}
관계명 연결 상태 작업 {loading ? ( ) : filteredRelations.length === 0 ? ( {searchTerm ? "검색 결과가 없습니다." : "등록된 연쇄 관계가 없습니다."} ) : ( filteredRelations.map((relation) => (
{relation.relation_name}
{relation.relation_code}
{relation.parent_table} {relation.child_table}
{relation.is_active === "Y" ? "활성" : "비활성"}
)) )}
{/* 생성/수정 모달 - 간소화된 UI */} {editingRelation ? "연쇄 관계 수정" : "새 연쇄 관계"} 부모 테이블 선택 시 자식 테이블의 옵션이 필터링됩니다.
{/* Step 1: 부모 테이블 */}

1. 부모 (상위 선택)

{renderColumnSelect( formData.parentValueColumn, (v) => setFormData({ ...formData, parentValueColumn: v }), parentColumns, loadingParentColumns, "컬럼 선택", !formData.parentTable, )}
{/* Step 2: 자식 테이블 */}

2. 자식 (하위 옵션)

{renderColumnSelect( formData.childFilterColumn, (v) => setFormData({ ...formData, childFilterColumn: v }), childColumns, loadingChildColumns, "컬럼 선택", !formData.childTable, )}
{renderColumnSelect( formData.childValueColumn, (v) => setFormData({ ...formData, childValueColumn: v }), childColumns, loadingChildColumns, "컬럼 선택", !formData.childTable, )}
{renderColumnSelect( formData.childLabelColumn, (v) => setFormData({ ...formData, childLabelColumn: v }), childColumns, loadingChildColumns, "컬럼 선택", !formData.childTable, )}
{/* 관계 정보 (자동 생성) */} {formData.parentTable && formData.childTable && (
setFormData({ ...formData, relationCode: e.target.value.toUpperCase() })} placeholder="자동 생성" className="h-8 text-xs" disabled={!!editingRelation} />
setFormData({ ...formData, relationName: e.target.value })} placeholder="자동 생성" className="h-8 text-xs" />
)} {/* 고급 설정 토글 */}
{showAdvanced && (