"use client"; import { useState, useEffect } from "react"; import { Plus, ChevronDown, ChevronRight, Users, Trash2, Eye, EyeOff, Undo2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/use-toast"; import type { Department } from "@/types/department"; import * as departmentAPI from "@/lib/api/department"; interface DepartmentStructureProps { companyCode: string; selectedDepartment: Department | null; onSelectDepartment: (department: Department | null) => void; refreshTrigger?: number; } /** * 부서 구조 컴포넌트 (트리 형태) */ export function DepartmentStructure({ companyCode, selectedDepartment, onSelectDepartment, refreshTrigger, }: DepartmentStructureProps) { const { toast } = useToast(); const [departments, setDepartments] = useState([]); const [expandedDepts, setExpandedDepts] = useState>(new Set()); const [isLoading, setIsLoading] = useState(false); // V1: soft-delete 된 부서 표시 토글 const [showDeleted, setShowDeleted] = useState(false); // 부서 추가 모달 const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [parentDeptForAdd, setParentDeptForAdd] = useState(null); const [newDeptName, setNewDeptName] = useState(""); const [duplicateMessage, setDuplicateMessage] = useState(null); // 부서 삭제 확인 모달 const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deptToDelete, setDeptToDelete] = useState<{ code: string; name: string } | null>(null); const [deleteErrorMessage, setDeleteErrorMessage] = useState(null); // 부서 목록 로드 — showDeleted 도 의존성에 포함 useEffect(() => { loadDepartments(); }, [companyCode, refreshTrigger, showDeleted]); const loadDepartments = async () => { setIsLoading(true); try { const response = await departmentAPI.getDepartments(companyCode, { includeDeleted: showDeleted }); if (response.success && (response as any).data) { setDepartments((response as any).data); } else { console.error("부서 목록 로드 실패:", (response as any).error); setDepartments([]); } } catch (error) { console.error("부서 목록 로드 실패:", error); setDepartments([]); } finally { setIsLoading(false); } }; // V1: 부서 복구 핸들러 (soft-delete 된 부서 되살리기) const handleRestoreDepartment = async (deptCode: string, deptName: string) => { try { const response = await departmentAPI.restoreDepartment(deptCode); if (response.success) { loadDepartments(); toast({ title: "부서 복구 완료", description: `"${deptName}" 부서가 복구되었습니다.`, variant: "default", }); } else { toast({ title: "복구 불가", description: (response as any).error || "부서 복구에 실패했습니다.", variant: "destructive", }); } } catch (error) { console.error("부서 복구 실패:", error); toast({ title: "부서 복구 실패", description: "복구 중 오류가 발생했습니다.", variant: "destructive", }); } }; // 부서 트리 구조 생성 const buildTree = (parentCode: string | null): Department[] => { return departments .filter((dept) => dept.parent_dept_code === parentCode) .sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); }; // 부서 추가 핸들러 const handleAddDepartment = (parentDeptCode: string | null = null) => { setParentDeptForAdd(parentDeptCode); setNewDeptName(""); setIsAddModalOpen(true); }; // 부서 저장 const handleSaveDepartment = async () => { if (!newDeptName.trim()) return; try { const response = await departmentAPI.createDepartment(companyCode, { dept_name: newDeptName, parent_dept_code: parentDeptForAdd, }); if (response.success) { setIsAddModalOpen(false); setNewDeptName(""); setParentDeptForAdd(null); loadDepartments(); // 성공 Toast 표시 toast({ title: "부서 생성 완료", description: `"${newDeptName}" 부서가 생성되었습니다.`, variant: "default", }); } else { if ((response as any).is_duplicate) { setDuplicateMessage((response as any).error || "이미 존재하는 부서명입니다."); } else { toast({ title: "부서 생성 실패", description: (response as any).error || "부서 추가에 실패했습니다.", variant: "destructive", }); } } } catch (error) { console.error("부서 추가 실패:", error); toast({ title: "부서 생성 실패", description: "부서 추가 중 오류가 발생했습니다.", variant: "destructive", }); } }; // 부서 삭제 확인 요청 const handleDeleteDepartmentRequest = (deptCode: string, deptName: string) => { setDeptToDelete({ code: deptCode, name: deptName }); setDeleteConfirmOpen(true); }; // 부서 삭제 실행 const handleDeleteDepartmentConfirm = async () => { if (!deptToDelete) return; try { const response = await departmentAPI.deleteDepartment(deptToDelete.code); if (response.success) { // 삭제된 부서가 선택되어 있었다면 선택 해제 if (selectedDepartment?.dept_code === deptToDelete.code) { onSelectDepartment(null); } setDeleteConfirmOpen(false); setDeptToDelete(null); loadDepartments(); // V1 soft-delete: 복구 가능 안내 추가 const isSoft = (response as any)?.data?.soft_deleted === true; toast({ title: isSoft ? "부서 삭제됨 (복구 가능)" : "부서 삭제 완료", description: isSoft ? `"${deptToDelete.name}" 부서를 휴지통으로 보냈습니다. '삭제 부서 보기' 토글로 복구할 수 있습니다.` : (response as any).message || "부서가 삭제되었습니다.", variant: "default", }); } else { // 삭제 확인 모달을 닫고 에러 모달을 표시 setDeleteConfirmOpen(false); setDeptToDelete(null); setDeleteErrorMessage((response as any).error || "부서 삭제에 실패했습니다."); } } catch (error) { console.error("부서 삭제 실패:", error); setDeleteConfirmOpen(false); setDeptToDelete(null); setDeleteErrorMessage("부서 삭제 중 오류가 발생했습니다."); } }; // 확장/축소 토글 const toggleExpand = (deptCode: string) => { const newExpanded = new Set(expandedDepts); if (newExpanded.has(deptCode)) { newExpanded.delete(deptCode); } else { newExpanded.add(deptCode); } setExpandedDepts(newExpanded); }; // 부서 트리 렌더링 (재귀) const renderDepartmentTree = (parentCode: string | null, level: number = 0) => { const children = buildTree(parentCode); return children.map((dept) => { const hasChildren = departments.some((d) => d.parent_dept_code === dept.dept_code); const isExpanded = expandedDepts.has(dept.dept_code); const isSelected = selectedDepartment?.dept_code === dept.dept_code; const isDeleted = !!(dept as any).deleted_at; return (
{/* 부서 항목 — soft-delete 시 회색+취소선 */}
!isDeleted && onSelectDepartment(dept)}> {/* 확장/축소 아이콘 */} {hasChildren ? ( ) : (
)} {/* 부서명 */} {dept.dept_name} {/* 인원수 */}
{dept.member_count || 0}
{/* deleted 배지 */} {isDeleted && 삭제됨}
{/* 액션 버튼 — deleted 면 복구 버튼만, 아니면 추가/삭제 버튼 */}
{isDeleted ? ( ) : ( <> )}
{/* 하위 부서 (재귀) */} {hasChildren && isExpanded && renderDepartmentTree(dept.dept_code, level + 1)}
); }); }; return (
{/* 헤더 */}

부서 구조

{/* V1: soft-delete 부서 표시 토글 */}
{/* 부서 트리 */}
{isLoading ? (
로딩 중...
) : departments.length === 0 ? (
부서가 없습니다. 최상위 부서를 추가해주세요.
) : ( renderDepartmentTree(null) )}
{/* 부서 추가 모달 */} {parentDeptForAdd ? "하위 부서 추가" : "최상위 부서 추가"}
setNewDeptName(e.target.value)} placeholder="부서명을 입력하세요" autoFocus />
{/* 중복 알림 모달 */} setDuplicateMessage(null)}> 중복 알림

{duplicateMessage}

{/* 부서 삭제 확인 모달 */} 부서 삭제 확인

{deptToDelete?.name} 부서를 삭제하시겠습니까?

부서원은 보존됩니다. 휴지통(상단 '삭제 부서 보기' 토글)에서 복구할 수 있습니다.

{/* 부서 삭제 에러 모달 */} setDeleteErrorMessage(null)}> 삭제 불가

{deleteErrorMessage}

); }