"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 { Textarea } from "@/components/ui/textarea"; 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 { Check, ChevronsUpDown, Layers, Plus, RefreshCw, Search, Pencil, Trash2, ChevronRight, ChevronDown, } from "lucide-react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { hierarchyApi, HierarchyGroup, HierarchyLevel, HIERARCHY_TYPES } from "@/lib/api/cascadingHierarchy"; import { tableManagementApi } from "@/lib/api/tableManagement"; export default function HierarchyTab() { // 목록 상태 const [groups, setGroups] = useState([]); const [tables, setTables] = useState>([]); const [loading, setLoading] = useState(true); const [searchText, setSearchText] = useState(""); // 확장된 그룹 (레벨 표시) const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [groupLevels, setGroupLevels] = useState>({}); // 모달 상태 const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [editingGroup, setEditingGroup] = useState(null); const [deletingGroupCode, setDeletingGroupCode] = useState(null); // 레벨 모달 const [isLevelModalOpen, setIsLevelModalOpen] = useState(false); const [editingLevel, setEditingLevel] = useState(null); const [currentGroupCode, setCurrentGroupCode] = useState(""); const [levelColumns, setLevelColumns] = useState>([]); // 폼 데이터 const [formData, setFormData] = useState>({ groupName: "", description: "", hierarchyType: "MULTI_TABLE", maxLevels: undefined, isFixedLevels: "Y", emptyMessage: "선택해주세요", noOptionsMessage: "옵션이 없습니다", loadingMessage: "로딩 중...", }); // 레벨 폼 데이터 const [levelFormData, setLevelFormData] = useState>({ levelOrder: 1, levelName: "", levelCode: "", tableName: "", valueColumn: "", labelColumn: "", parentKeyColumn: "", orderColumn: "", orderDirection: "ASC", placeholder: "", isRequired: "Y", isSearchable: "N", }); // 테이블 Combobox 상태 const [tableComboOpen, setTableComboOpen] = useState(false); // snake_case를 camelCase로 변환하는 함수 const transformGroup = (g: any): HierarchyGroup => ({ groupId: g.group_id || g.groupId, groupCode: g.group_code || g.groupCode, groupName: g.group_name || g.groupName, description: g.description, hierarchyType: g.hierarchy_type || g.hierarchyType, maxLevels: g.max_levels || g.maxLevels, isFixedLevels: g.is_fixed_levels || g.isFixedLevels, selfRefTable: g.self_ref_table || g.selfRefTable, selfRefIdColumn: g.self_ref_id_column || g.selfRefIdColumn, selfRefParentColumn: g.self_ref_parent_column || g.selfRefParentColumn, selfRefValueColumn: g.self_ref_value_column || g.selfRefValueColumn, selfRefLabelColumn: g.self_ref_label_column || g.selfRefLabelColumn, selfRefLevelColumn: g.self_ref_level_column || g.selfRefLevelColumn, selfRefOrderColumn: g.self_ref_order_column || g.selfRefOrderColumn, bomTable: g.bom_table || g.bomTable, bomParentColumn: g.bom_parent_column || g.bomParentColumn, bomChildColumn: g.bom_child_column || g.bomChildColumn, bomItemTable: g.bom_item_table || g.bomItemTable, bomItemIdColumn: g.bom_item_id_column || g.bomItemIdColumn, bomItemLabelColumn: g.bom_item_label_column || g.bomItemLabelColumn, bomQtyColumn: g.bom_qty_column || g.bomQtyColumn, bomLevelColumn: g.bom_level_column || g.bomLevelColumn, emptyMessage: g.empty_message || g.emptyMessage, noOptionsMessage: g.no_options_message || g.noOptionsMessage, loadingMessage: g.loading_message || g.loadingMessage, companyCode: g.company_code || g.companyCode, isActive: g.is_active || g.isActive, createdBy: g.created_by || g.createdBy, createdDate: g.created_date || g.createdDate, updatedBy: g.updated_by || g.updatedBy, updatedDate: g.updated_date || g.updatedDate, levelCount: g.level_count || g.levelCount || 0, levels: g.levels, }); // 목록 로드 const loadGroups = useCallback(async () => { setLoading(true); try { const response = await hierarchyApi.getGroups(); if (response.success && response.data) { // snake_case를 camelCase로 변환 const transformedData = response.data.map(transformGroup); setGroups(transformedData); } } 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(() => { loadGroups(); loadTables(); }, [loadGroups, loadTables]); // 그룹 레벨 로드 const loadGroupLevels = async (groupCode: string) => { try { const response = await hierarchyApi.getDetail(groupCode); if (response.success && response.data?.levels) { setGroupLevels((prev) => ({ ...prev, [groupCode]: response.data!.levels || [], })); } } catch (error) { console.error("레벨 로드 실패:", error); } }; // 그룹 확장 토글 const toggleGroupExpand = async (groupCode: string) => { const newExpanded = new Set(expandedGroups); if (newExpanded.has(groupCode)) { newExpanded.delete(groupCode); } else { newExpanded.add(groupCode); if (!groupLevels[groupCode]) { await loadGroupLevels(groupCode); } } setExpandedGroups(newExpanded); }; // 컬럼 로드 (레벨 폼용) const loadLevelColumns = async (tableName: string) => { if (!tableName) { setLevelColumns([]); return; } try { const response = await tableManagementApi.getColumnList(tableName); if (response.success && response.data?.columns) { setLevelColumns(response.data.columns); } } catch (error) { console.error("컬럼 목록 로드 실패:", error); } }; // 필터된 목록 const filteredGroups = groups.filter( (g) => g.groupName?.toLowerCase().includes(searchText.toLowerCase()) || g.groupCode?.toLowerCase().includes(searchText.toLowerCase()), ); // 모달 열기 (생성) const handleOpenCreate = () => { setEditingGroup(null); setFormData({ groupName: "", description: "", hierarchyType: "MULTI_TABLE", maxLevels: undefined, isFixedLevels: "Y", emptyMessage: "선택해주세요", noOptionsMessage: "옵션이 없습니다", loadingMessage: "로딩 중...", }); setIsModalOpen(true); }; // 모달 열기 (수정) const handleOpenEdit = (group: HierarchyGroup) => { setEditingGroup(group); setFormData({ groupCode: group.groupCode, groupName: group.groupName, description: group.description || "", hierarchyType: group.hierarchyType, maxLevels: group.maxLevels, isFixedLevels: group.isFixedLevels || "Y", emptyMessage: group.emptyMessage || "선택해주세요", noOptionsMessage: group.noOptionsMessage || "옵션이 없습니다", loadingMessage: group.loadingMessage || "로딩 중...", }); setIsModalOpen(true); }; // 삭제 확인 const handleDeleteConfirm = (groupCode: string) => { setDeletingGroupCode(groupCode); setIsDeleteDialogOpen(true); }; // 삭제 실행 const handleDelete = async () => { if (!deletingGroupCode) return; try { const response = await hierarchyApi.deleteGroup(deletingGroupCode); if (response.success) { toast.success("계층 그룹이 삭제되었습니다."); loadGroups(); } else { toast.error(response.error || "삭제에 실패했습니다."); } } catch (error) { toast.error("삭제 중 오류가 발생했습니다."); } finally { setIsDeleteDialogOpen(false); setDeletingGroupCode(null); } }; // 저장 const handleSave = async () => { if (!formData.groupName || !formData.hierarchyType) { toast.error("필수 항목을 모두 입력해주세요."); return; } try { let response; if (editingGroup) { response = await hierarchyApi.updateGroup(editingGroup.groupCode!, formData); } else { response = await hierarchyApi.createGroup(formData as HierarchyGroup); } if (response.success) { toast.success(editingGroup ? "수정되었습니다." : "생성되었습니다."); setIsModalOpen(false); loadGroups(); } else { toast.error(response.error || "저장에 실패했습니다."); } } catch (error) { toast.error("저장 중 오류가 발생했습니다."); } }; // 레벨 모달 열기 (생성) const handleOpenCreateLevel = (groupCode: string) => { setCurrentGroupCode(groupCode); setEditingLevel(null); const existingLevels = groupLevels[groupCode] || []; setLevelFormData({ levelOrder: existingLevels.length + 1, levelName: "", levelCode: "", tableName: "", valueColumn: "", labelColumn: "", parentKeyColumn: "", orderColumn: "", orderDirection: "ASC", placeholder: "", isRequired: "Y", isSearchable: "N", }); setLevelColumns([]); setIsLevelModalOpen(true); }; // 레벨 모달 열기 (수정) const handleOpenEditLevel = async (level: HierarchyLevel) => { setCurrentGroupCode(level.groupCode); setEditingLevel(level); setLevelFormData({ levelOrder: level.levelOrder, levelName: level.levelName, levelCode: level.levelCode || "", tableName: level.tableName, valueColumn: level.valueColumn, labelColumn: level.labelColumn, parentKeyColumn: level.parentKeyColumn || "", orderColumn: level.orderColumn || "", orderDirection: level.orderDirection || "ASC", placeholder: level.placeholder || "", isRequired: level.isRequired || "Y", isSearchable: level.isSearchable || "N", }); await loadLevelColumns(level.tableName); setIsLevelModalOpen(true); }; // 레벨 저장 const handleSaveLevel = async () => { if ( !levelFormData.levelName || !levelFormData.tableName || !levelFormData.valueColumn || !levelFormData.labelColumn ) { toast.error("필수 항목을 모두 입력해주세요."); return; } try { let response; if (editingLevel) { response = await hierarchyApi.updateLevel(editingLevel.levelId!, levelFormData); } else { response = await hierarchyApi.addLevel(currentGroupCode, levelFormData); } if (response.success) { toast.success(editingLevel ? "레벨이 수정되었습니다." : "레벨이 추가되었습니다."); setIsLevelModalOpen(false); await loadGroupLevels(currentGroupCode); } else { toast.error(response.error || "저장에 실패했습니다."); } } catch (error) { toast.error("저장 중 오류가 발생했습니다."); } }; // 레벨 삭제 const handleDeleteLevel = async (levelId: number, groupCode: string) => { if (!confirm("이 레벨을 삭제하시겠습니까?")) return; try { const response = await hierarchyApi.deleteLevel(levelId); if (response.success) { toast.success("레벨이 삭제되었습니다."); await loadGroupLevels(groupCode); } else { toast.error(response.error || "삭제에 실패했습니다."); } } catch (error) { toast.error("삭제 중 오류가 발생했습니다."); } }; // 계층 타입 라벨 const getHierarchyTypeLabel = (type: string) => { return HIERARCHY_TYPES.find((t) => t.value === type)?.label || type; }; return (
{/* 검색 */}
setSearchText(e.target.value)} className="pl-10" />
{/* 목록 */}
다단계 계층 국가 > 도시 > 구 같은 다단계 연쇄 드롭다운을 관리합니다. (총 {filteredGroups.length}개)
{loading ? (
로딩 중...
) : filteredGroups.length === 0 ? (
{searchText ? "검색 결과가 없습니다." : "등록된 계층 그룹이 없습니다."}
예시: 지역 계층
국가 > 시/도 > 시/군/구 > 읍/면/동
예시: 조직 계층
본부 > 팀 > 파트 (자기 참조 구조)
) : (
{filteredGroups.map((group) => (
toggleGroupExpand(group.groupCode)} >
{expandedGroups.has(group.groupCode) ? ( ) : ( )}
{group.groupName}
{group.groupCode}
{getHierarchyTypeLabel(group.hierarchyType)} {group.levelCount || 0}개 레벨 {group.isActive === "Y" ? "활성" : "비활성"}
e.stopPropagation()}>
{/* 레벨 목록 */} {expandedGroups.has(group.groupCode) && (
레벨 목록
{(groupLevels[group.groupCode] || []).length === 0 ? (
등록된 레벨이 없습니다.
) : ( 순서 레벨명 테이블 값 컬럼 부모 키 작업 {(groupLevels[group.groupCode] || []).map((level) => ( {level.levelOrder} {level.levelName} {level.tableName} {level.valueColumn} {level.parentKeyColumn || "-"} ))}
)}
)}
))}
)}
{/* 그룹 생성/수정 모달 */} {editingGroup ? "계층 그룹 수정" : "계층 그룹 생성"} 다단계 연쇄 드롭다운의 기본 정보를 설정합니다.
setFormData({ ...formData, groupName: e.target.value })} placeholder="예: 지역 계층" />