"use client" import * as React from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Loader, ArrowUpDown, ArrowUp, ArrowDown, GripVertical, RotateCcw, Info } from "lucide-react" import { toast } from "sonner" import { cn } from "@/lib/utils" import { reorderGtcClausesSchema, type ReorderGtcClausesSchema } from "@/lib/gtc-contract/gtc-clauses/validations" import { reorderGtcClauses, getGtcClausesTree } from "@/lib/gtc-contract/gtc-clauses/service" import { type GtcClauseTreeView } from "@/db/schema/gtc" import { useSession } from "next-auth/react" interface ReorderGtcClausesDialogProps extends React.ComponentPropsWithRef { documentId: number onSuccess?: () => void } interface ClauseWithOrder extends GtcClauseTreeView { newSortOrder: number hasChanges: boolean children?: ClauseWithOrder[] } export function ReorderGtcClausesDialog({ documentId, onSuccess, ...props }: ReorderGtcClausesDialogProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() const [clauses, setClauses] = React.useState([]) const [originalClauses, setOriginalClauses] = React.useState([]) const [isLoading, setIsLoading] = React.useState(false) const [draggedItem, setDraggedItem] = React.useState(null) const { data: session } = useSession() const currentUserId = React.useMemo(() => { return session?.user?.id ? Number(session.user.id) : null }, [session]) const form = useForm({ resolver: zodResolver(reorderGtcClausesSchema), defaultValues: { clauses: [], editReason: "", }, }) // 조항 데이터 로드 React.useEffect(() => { if (props.open && documentId) { loadClauses() } }, [props.open, documentId]) const loadClauses = async () => { setIsLoading(true) try { const tree = await getGtcClausesTree(documentId) const flatClauses = flattenTreeWithOrder(tree) setClauses(flatClauses) setOriginalClauses(JSON.parse(JSON.stringify(flatClauses))) // 깊은 복사 } catch (error) { console.error("Error loading clauses:", error) toast.error("조항 목록을 불러오는 중 오류가 발생했습니다.") } finally { setIsLoading(false) } } // 트리를 평면 배열로 변환하면서 순서 정보 추가 const flattenTreeWithOrder = (tree: any[]): ClauseWithOrder[] => { const result: ClauseWithOrder[] = [] function traverse(nodes: any[], parentId: number | null = null) { nodes.forEach((node, index) => { const clauseWithOrder: ClauseWithOrder = { ...node, newSortOrder: parseFloat(node.sortOrder), hasChanges: false, } result.push(clauseWithOrder) if (node.children && node.children.length > 0) { traverse(node.children, node.id) } }) } traverse(tree) return result } // 조항 순서 변경 const moveClause = (clauseId: number, direction: 'up' | 'down') => { setClauses(prev => { const newClauses = [...prev] const clauseIndex = newClauses.findIndex(c => c.id === clauseId) if (clauseIndex === -1) return prev const clause = newClauses[clauseIndex] // 같은 부모를 가진 형제 조항들 찾기 const siblings = newClauses.filter(c => c.parentId === clause.parentId) const siblingIndex = siblings.findIndex(c => c.id === clauseId) if (direction === 'up' && siblingIndex > 0) { // 위로 이동 const targetSibling = siblings[siblingIndex - 1] const tempOrder = clause.newSortOrder clause.newSortOrder = targetSibling.newSortOrder targetSibling.newSortOrder = tempOrder clause.hasChanges = true targetSibling.hasChanges = true } else if (direction === 'down' && siblingIndex < siblings.length - 1) { // 아래로 이동 const targetSibling = siblings[siblingIndex + 1] const tempOrder = clause.newSortOrder clause.newSortOrder = targetSibling.newSortOrder targetSibling.newSortOrder = tempOrder clause.hasChanges = true targetSibling.hasChanges = true } // sortOrder로 정렬 return newClauses.sort((a, b) => { if (a.parentId !== b.parentId) { // 부모가 다르면 부모 기준으로 정렬 return (a.parentId || 0) - (b.parentId || 0) } return a.newSortOrder - b.newSortOrder }) }) } // 변경사항 초기화 const resetChanges = () => { setClauses(JSON.parse(JSON.stringify(originalClauses))) toast.success("변경사항이 초기화되었습니다.") } // 드래그 앤 드롭 핸들러 const handleDragStart = (e: React.DragEvent, clause: ClauseWithOrder) => { setDraggedItem(clause) e.dataTransfer.effectAllowed = 'move' } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' } const handleDrop = (e: React.DragEvent, targetClause: ClauseWithOrder) => { e.preventDefault() if (!draggedItem || draggedItem.id === targetClause.id) { setDraggedItem(null) return } // 같은 부모를 가진 경우에만 순서 변경 허용 if (draggedItem.parentId === targetClause.parentId) { setClauses(prev => { const newClauses = [...prev] const draggedIndex = newClauses.findIndex(c => c.id === draggedItem.id) const targetIndex = newClauses.findIndex(c => c.id === targetClause.id) if (draggedIndex !== -1 && targetIndex !== -1) { const tempOrder = newClauses[draggedIndex].newSortOrder newClauses[draggedIndex].newSortOrder = newClauses[targetIndex].newSortOrder newClauses[targetIndex].newSortOrder = tempOrder newClauses[draggedIndex].hasChanges = true newClauses[targetIndex].hasChanges = true } return newClauses.sort((a, b) => { if (a.parentId !== b.parentId) { return (a.parentId || 0) - (b.parentId || 0) } return a.newSortOrder - b.newSortOrder }) }) } setDraggedItem(null) } async function onSubmit(data: ReorderGtcClausesSchema) { startUpdateTransition(async () => { if (!currentUserId) { toast.error("로그인이 필요합니다") return } // 변경된 조항들만 필터링 const changedClauses = clauses.filter(c => c.hasChanges).map(c => ({ id: c.id, sortOrder: c.newSortOrder, parentId: c.parentId, depth: c.depth, fullPath: c.fullPath, })) if (changedClauses.length === 0) { toast.error("변경된 조항이 없습니다.") return } try { const result = await reorderGtcClauses({ clauses: changedClauses, editReason: data.editReason || "조항 순서 변경", updatedById: currentUserId }) if (result.error) { toast.error(`에러: ${result.error}`) return } form.reset() props.onOpenChange?.(false) toast.success(`${changedClauses.length}개 조항의 순서가 변경되었습니다.`) onSuccess?.() } catch (error) { toast.error("조항 순서 변경 중 오류가 발생했습니다.") } }) } function handleDialogOpenChange(nextOpen: boolean) { if (!nextOpen) { form.reset() setClauses([]) setOriginalClauses([]) } props.onOpenChange?.(nextOpen) } const changedCount = clauses.filter(c => c.hasChanges).length const groupedClauses = groupClausesByParent(clauses) return ( 조항 순서 변경 드래그 앤 드롭 또는 화살표 버튼으로 조항의 순서를 변경하세요. 같은 계층 내에서만 순서 변경이 가능합니다. {/* 상태 정보 */}
총 {clauses.length}개 조항
{changedCount > 0 && ( {changedCount}개 변경됨 )}
{changedCount > 0 && ( )}
{/* 조항 목록 */} {isLoading ? (
조항을 불러오는 중...
) : (
{Object.entries(groupedClauses).map(([parentInfo, clauses]) => ( ))}
)}
{/* 편집 사유 */}
( 편집 사유 (선택사항)