"use client"; import { useMemo, useState } 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 { useDeleteCodeDetail, useUpdateCodeDetail, useCodeDetailTree } from "@/hooks/queries/useCodeDetail"; import type { CodeDetail } from "@/types/commonCode"; import { DndContext, DragOverlay } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; import { useDragAndDrop } from "@/hooks/useDragAndDrop"; interface CodeDetailPanelProps { codeInfo: string; } /** * 디테일 트리 패널. * * BE 가 `/api/common-codes/detail?code_info=X` 로 depth+sort_order 평탄화 리스트를 내려준다. * FE 는 parent_detail_id 로 children 맵을 만들어 indent + collapse 처리. * sort_order 변경은 형제 단위 reorder → 각 행 update PUT 으로 직렬 호출. */ export function CodeDetailPanel({ codeInfo }: CodeDetailPanelProps) { const [searchTerm, setSearchTerm] = useState(""); const [showActiveOnly, setShowActiveOnly] = useState(false); const treeQuery = useCodeDetailTree(codeInfo || undefined, { search: searchTerm || undefined, active: showActiveOnly || undefined, }); const rows: CodeDetail[] = useMemo(() => treeQuery.data || [], [treeQuery.data]); const isLoading = treeQuery.isLoading; const error = treeQuery.error; const deleteMutation = useDeleteCodeDetail(); const updateMutation = useUpdateCodeDetail(); /** parent_detail_id → children */ const childrenMap = useMemo(() => { const map = new Map(); for (const row of rows) { const key = row.parent_detail_id == null ? "ROOT" : String(row.parent_detail_id); const list = map.get(key) || []; list.push(row); map.set(key, list); } return map; }, [rows]); // 접기/펼치기 (code_detail_id Set) const [collapsed, setCollapsed] = useState>(new Set()); const toggleExpand = (id: string) => { setCollapsed((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; // 부모 행 중 하나라도 접혀있으면 자식 숨김 const isRowVisible = (row: CodeDetail): boolean => { let parentId = row.parent_detail_id; while (parentId != null) { if (collapsed.has(String(parentId))) return false; const parentRow = rows.find((r) => String(r.code_detail_id) === String(parentId)); parentId = parentRow ? parentRow.parent_detail_id : null; } return true; }; const visibleRows = useMemo(() => rows.filter(isRowVisible), [rows, collapsed]); // 모달 상태 const [showFormModal, setShowFormModal] = useState(false); const [editingRow, setEditingRow] = useState(null); const [defaultParentDetailId, setDefaultParentDetailId] = useState(null); const [showDeleteModal, setShowDeleteModal] = useState(false); const [deletingRow, setDeletingRow] = useState(null); // sort_order 재할당 (형제 단위) — 드래그 결과를 PUT 으로 직렬 호출 const dragAndDrop = useDragAndDrop({ items: visibleRows, getItemId: (row) => String(row.code_detail_id), onReorder: async (reordered) => { // visibleRows 기준으로 재정렬. 형제만 reorder 의미가 있으므로 // parent_detail_id 가 같은 그룹별로 처리. const idMap = new Map(); reordered.forEach(({ id, sortOrder }) => idMap.set(id, sortOrder)); // 같은 부모 안의 형제만 묶어서 정렬 const byParent = new Map(); for (const row of visibleRows) { const parentKey = row.parent_detail_id == null ? "ROOT" : String(row.parent_detail_id); const list = byParent.get(parentKey) || []; list.push(row); byParent.set(parentKey, list); } for (const list of byParent.values()) { const sorted = [...list].sort((a, b) => { const ao = idMap.get(String(a.code_detail_id)) ?? a.sort_order ?? 0; const bo = idMap.get(String(b.code_detail_id)) ?? b.sort_order ?? 0; return ao - bo; }); for (let i = 0; i < sorted.length; i++) { const row = sorted[i]; const nextOrder = (i + 1) * 10; if ((row.sort_order ?? 0) === nextOrder) continue; await updateMutation.mutateAsync({ codeDetailId: row.code_detail_id, data: { code_info: row.code_info, parent_detail_id: row.parent_detail_id ?? null, code_value: row.code_value, code_name: row.code_name, code_name_eng: row.code_name_eng || "", description: row.description || "", sort_order: nextOrder, is_active: row.is_active || "Y", }, }); } } }, }); const handleNew = () => { setEditingRow(null); setDefaultParentDetailId(null); setShowFormModal(true); }; const handleEdit = (row: CodeDetail) => { setEditingRow(row); setDefaultParentDetailId(null); setShowFormModal(true); }; const handleAddChild = (parent: CodeDetail) => { setEditingRow(null); setDefaultParentDetailId(Number(parent.code_detail_id)); setShowFormModal(true); }; const handleDelete = (row: CodeDetail) => { setDeletingRow(row); setShowDeleteModal(true); }; const handleConfirmDelete = async () => { if (!deletingRow) return; try { await deleteMutation.mutateAsync(deletingRow.code_detail_id); setShowDeleteModal(false); setDeletingRow(null); } catch (error) { console.error("디테일 삭제 실패:", error); } }; if (!codeInfo) { return (

그룹을 선택하세요

); } if (error) { return (

디테일을 불러오는 중 오류가 발생했습니다.

); } return (
setSearchTerm(e.target.value)} className="h-10 pl-10 text-sm" />
setShowActiveOnly(e.target.checked)} className="h-4 w-4 rounded border-input" />
{isLoading ? (
) : visibleRows.length === 0 ? (

{rows.length === 0 ? "디테일이 없습니다." : "검색 결과가 없습니다."}

) : ( String(r.code_detail_id))} strategy={verticalListSortingStrategy} > {visibleRows.map((row) => { const idStr = String(row.code_detail_id); const children = childrenMap.get(idStr) || []; const hasChildren = children.length > 0; const isExpanded = !collapsed.has(idStr); return ( handleEdit(row)} onDelete={() => handleDelete(row)} onAddChild={() => handleAddChild(row)} hasChildren={hasChildren} childCount={children.length} isExpanded={isExpanded} onToggleExpand={() => toggleExpand(idStr)} /> ); })} {dragAndDrop.activeItem ? (

{dragAndDrop.activeItem.code_name}

{dragAndDrop.activeItem.is_active === "Y" ? "활성" : "비활성"}

{dragAndDrop.activeItem.code_value}

) : null}
)}
{showFormModal && ( { setShowFormModal(false); setEditingRow(null); setDefaultParentDetailId(null); }} codeInfo={codeInfo} editingRow={editingRow} allRows={rows} defaultParentDetailId={defaultParentDetailId} /> )} {showDeleteModal && ( setShowDeleteModal(false)} type="error" title="디테일 삭제" message="정말로 이 코드를 삭제하시겠습니까? 하위 코드도 모두 함께 삭제됩니다 (CASCADE)." confirmText="삭제" onConfirm={handleConfirmDelete} /> )}
); } // `arrayMove` import 유지 위해 keep no-op (트리 reorder 로직에서 향후 활용 가능) void arrayMove;