"use client"; import { useState, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { CodeFormModal } from "./CodeFormModal"; import { SortableCodeItem } from "./SortableCodeItem"; import { AlertModal } from "@/components/common/AlertModal"; import { Search, Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import { useDeleteCode, useReorderCodes } from "@/hooks/queries/useCodes"; import { useCodesInfinite } from "@/hooks/queries/useCodesInfinite"; import type { CodeInfo } from "@/types/commonCode"; // Drag and Drop import { DndContext, DragOverlay } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { useDragAndDrop } from "@/hooks/useDragAndDrop"; import { useSearchAndFilter } from "@/hooks/useSearchAndFilter"; interface CodeDetailPanelProps { categoryCode: string; } export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) { // 검색 및 필터 상태 (먼저 선언) const [searchTerm, setSearchTerm] = useState(""); const [showActiveOnly, setShowActiveOnly] = useState(false); // React Query로 코드 데이터 관리 (무한 스크롤) const { data: codes = [], isLoading, error, handleScroll, isFetchingNextPage, hasNextPage, } = useCodesInfinite(categoryCode, { search: searchTerm || undefined, active: showActiveOnly || undefined, }); const deleteCodeMutation = useDeleteCode(); const reorderCodesMutation = useReorderCodes(); // 드래그앤드롭을 위해 필터링된 코드 목록 사용 const { filteredItems: filteredCodesRaw } = useSearchAndFilter(codes, { searchFields: ["code_name", "code_value"], }); // 계층 구조로 정렬 (부모 → 자식 순서) const filteredCodes = useMemo(() => { if (!filteredCodesRaw || filteredCodesRaw.length === 0) return []; // 코드를 계층 순서로 정렬하는 함수 const sortHierarchically = (codes: CodeInfo[]): CodeInfo[] => { const result: CodeInfo[] = []; const codeMap = new Map(); const childrenMap = new Map(); // 코드 맵 생성 codes.forEach((code) => { const codeValue = code.codeValue || code.code_value || ""; const parentValue = code.parentCodeValue || code.parent_code_value; codeMap.set(codeValue, code); if (parentValue) { if (!childrenMap.has(parentValue)) { childrenMap.set(parentValue, []); } childrenMap.get(parentValue)!.push(code); } }); // 재귀적으로 트리 구조 순회 const traverse = (parentValue: string | null, depth: number) => { const children = parentValue ? childrenMap.get(parentValue) || [] : codes.filter((c) => !c.parentCodeValue && !c.parent_code_value); // 정렬 순서로 정렬 children .sort((a, b) => (a.sortOrder || a.sort_order || 0) - (b.sortOrder || b.sort_order || 0)) .forEach((code) => { result.push(code); const codeValue = code.codeValue || code.code_value || ""; traverse(codeValue, depth + 1); }); }; traverse(null, 1); // 트리에 포함되지 않은 코드들도 추가 (orphan 코드) codes.forEach((code) => { if (!result.includes(code)) { result.push(code); } }); return result; }; return sortHierarchically(filteredCodesRaw); }, [filteredCodesRaw]); // 모달 상태 const [showFormModal, setShowFormModal] = useState(false); const [editingCode, setEditingCode] = useState(null); const [showDeleteModal, setShowDeleteModal] = useState(false); const [deletingCode, setDeletingCode] = useState(null); const [defaultParentCode, setDefaultParentCode] = useState(undefined); // 트리 접기/펼치기 상태 (코드값 Set) const [collapsedCodes, setCollapsedCodes] = useState>(new Set()); // 자식 정보 계산 const childrenMap = useMemo(() => { const map = new Map(); codes.forEach((code) => { const parentValue = code.parentCodeValue || code.parent_code_value; if (parentValue) { if (!map.has(parentValue)) { map.set(parentValue, []); } map.get(parentValue)!.push(code); } }); return map; }, [codes]); // 접기/펼치기 토글 const toggleExpand = (codeValue: string) => { setCollapsedCodes((prev) => { const newSet = new Set(prev); if (newSet.has(codeValue)) { newSet.delete(codeValue); } else { newSet.add(codeValue); } return newSet; }); }; // 특정 코드가 표시되어야 하는지 확인 (부모가 접혀있으면 숨김) const isCodeVisible = (code: CodeInfo): boolean => { const parentValue = code.parentCodeValue || code.parent_code_value; if (!parentValue) return true; // 최상위 코드는 항상 표시 // 부모가 접혀있으면 숨김 if (collapsedCodes.has(parentValue)) return false; // 부모의 부모도 확인 (재귀적으로) const parentCode = codes.find((c) => (c.codeValue || c.code_value) === parentValue); if (parentCode) { return isCodeVisible(parentCode); } return true; }; // 표시할 코드 목록 (접힌 상태 반영) const visibleCodes = useMemo(() => { return filteredCodes.filter(isCodeVisible); }, [filteredCodes, collapsedCodes, codes]); // 드래그 앤 드롭 훅 사용 const dragAndDrop = useDragAndDrop({ items: filteredCodes, onReorder: async (reorderedItems) => { await reorderCodesMutation.mutateAsync({ categoryCode, codes: reorderedItems.map((item) => ({ codeValue: item.id, sortOrder: item.sortOrder, })), }); }, getItemId: (code: CodeInfo) => code.codeValue || code.code_value, }); // 새 코드 생성 const handleNewCode = () => { setEditingCode(null); setDefaultParentCode(undefined); setShowFormModal(true); }; // 코드 수정 const handleEditCode = (code: CodeInfo) => { setEditingCode(code); setDefaultParentCode(undefined); setShowFormModal(true); }; // 하위 코드 추가 const handleAddChild = (parentCode: CodeInfo) => { setEditingCode(null); setDefaultParentCode(parentCode.codeValue || parentCode.code_value || ""); setShowFormModal(true); }; // 코드 삭제 확인 const handleDeleteCode = (code: CodeInfo) => { setDeletingCode(code); setShowDeleteModal(true); }; // 코드 삭제 실행 const handleConfirmDelete = async () => { if (!deletingCode) return; try { await deleteCodeMutation.mutateAsync({ categoryCode, codeValue: deletingCode.codeValue || deletingCode.code_value, }); setShowDeleteModal(false); setDeletingCode(null); } catch (error) { console.error("코드 삭제 실패:", error); } }; // 드래그 앤 드롭 로직은 useDragAndDrop 훅에서 처리 if (!categoryCode) { return (

카테고리를 선택하세요

); } if (error) { return (

코드를 불러오는 중 오류가 발생했습니다.

); } return (
{/* 검색 및 액션 */}
{/* 검색 + 버튼 */}
setSearchTerm(e.target.value)} className="h-10 pl-10 text-sm" />
{/* 활성 필터 */}
setShowActiveOnly(e.target.checked)} className="border-input h-4 w-4 rounded" />
{/* 코드 목록 (무한 스크롤) */}
{isLoading ? (
) : visibleCodes.length === 0 ? (

{codes.length === 0 ? "코드가 없습니다." : "검색 결과가 없습니다."}

) : ( <> code.codeValue || code.code_value)} strategy={verticalListSortingStrategy} > {visibleCodes.map((code, index) => { const codeValue = code.codeValue || code.code_value || ""; const children = childrenMap.get(codeValue) || []; const hasChildren = children.length > 0; const isExpanded = !collapsedCodes.has(codeValue); return ( handleEditCode(code)} onDelete={() => handleDeleteCode(code)} onAddChild={() => handleAddChild(code)} hasChildren={hasChildren} childCount={children.length} isExpanded={isExpanded} onToggleExpand={() => toggleExpand(codeValue)} /> ); })} {dragAndDrop.activeItem ? (
{(() => { const activeCode = dragAndDrop.activeItem; if (!activeCode) return null; return (

{activeCode.codeName || activeCode.code_name}

{activeCode.isActive === "Y" || activeCode.is_active === "Y" ? "활성" : "비활성"}

{activeCode.codeValue || activeCode.code_value}

{activeCode.description && (

{activeCode.description}

)}
); })()}
) : null}
{/* 무한 스크롤 로딩 인디케이터 */} {isFetchingNextPage && (
코드를 더 불러오는 중...
)} {/* 모든 코드 로드 완료 메시지 */} {!hasNextPage && codes.length > 0 && (
모든 코드를 불러왔습니다.
)} )}
{/* 코드 폼 모달 */} {showFormModal && ( { setShowFormModal(false); setEditingCode(null); setDefaultParentCode(undefined); }} categoryCode={categoryCode} editingCode={editingCode} codes={codes} defaultParentCode={defaultParentCode} /> )} {/* 삭제 확인 모달 */} {showDeleteModal && ( setShowDeleteModal(false)} type="error" title="코드 삭제" message="정말로 이 코드를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." confirmText="삭제" onConfirm={handleConfirmDelete} /> )}
); }