ERP-node/frontend/components/admin/department/DepartmentStructure.tsx

276 lines
8.6 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Plus, ChevronDown, ChevronRight, Users, Trash2 } 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 type { Department, DepartmentFormData } from "@/types/department";
import * as departmentAPI from "@/lib/api/department";
interface DepartmentStructureProps {
companyCode: string;
selectedDepartment: Department | null;
onSelectDepartment: (department: Department | null) => void;
}
/**
* 부서 구조 컴포넌트 (트리 형태)
*/
export function DepartmentStructure({
companyCode,
selectedDepartment,
onSelectDepartment,
}: DepartmentStructureProps) {
const [departments, setDepartments] = useState<Department[]>([]);
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set());
const [isLoading, setIsLoading] = useState(false);
// 부서 추가 모달
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [parentDeptForAdd, setParentDeptForAdd] = useState<string | null>(null);
const [newDeptName, setNewDeptName] = useState("");
// 부서 목록 로드
useEffect(() => {
loadDepartments();
}, [companyCode]);
const loadDepartments = async () => {
setIsLoading(true);
try {
const response = await departmentAPI.getDepartments(companyCode);
if (response.success && response.data) {
setDepartments(response.data);
} else {
console.error("부서 목록 로드 실패:", response.error);
setDepartments([]);
}
} catch (error) {
console.error("부서 목록 로드 실패:", error);
setDepartments([]);
} finally {
setIsLoading(false);
}
};
// 부서 트리 구조 생성
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);
loadDepartments();
} else {
alert(response.error || "부서 추가에 실패했습니다.");
}
} catch (error) {
console.error("부서 추가 실패:", error);
alert("부서 추가 중 오류가 발생했습니다.");
}
};
// 부서 삭제
const handleDeleteDepartment = async (deptCode: string) => {
if (!confirm("이 부서를 삭제하시겠습니까?")) return;
try {
const response = await departmentAPI.deleteDepartment(deptCode);
if (response.success) {
loadDepartments();
} else {
alert(response.error || "부서 삭제에 실패했습니다.");
}
} catch (error) {
console.error("부서 삭제 실패:", error);
alert("부서 삭제 중 오류가 발생했습니다.");
}
};
// 확장/축소 토글
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;
return (
<div key={dept.dept_code}>
{/* 부서 항목 */}
<div
className={`flex cursor-pointer items-center justify-between rounded-lg p-2 text-sm transition-colors hover:bg-muted ${
isSelected ? "bg-primary/10 text-primary" : ""
}`}
style={{ marginLeft: `${level * 16}px` }}
>
<div
className="flex flex-1 items-center gap-2"
onClick={() => onSelectDepartment(dept)}
>
{/* 확장/축소 아이콘 */}
{hasChildren ? (
<button
onClick={(e) => {
e.stopPropagation();
toggleExpand(dept.dept_code);
}}
className="h-4 w-4"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
) : (
<div className="h-4 w-4" />
)}
{/* 부서명 */}
<span className="font-medium">{dept.dept_name}</span>
{/* 인원수 */}
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Users className="h-3 w-3" />
<span>{dept.memberCount || 0}</span>
</div>
</div>
{/* 액션 버튼 */}
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
handleAddDepartment(dept.dept_code);
}}
>
<Plus className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive"
onClick={(e) => {
e.stopPropagation();
handleDeleteDepartment(dept.dept_code);
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
{/* 하위 부서 (재귀) */}
{hasChildren && isExpanded && renderDepartmentTree(dept.dept_code, level + 1)}
</div>
);
});
};
return (
<div className="space-y-4">
{/* 헤더 */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold"> </h3>
<Button size="sm" className="h-9 gap-2 text-sm" onClick={() => handleAddDepartment(null)}>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 부서 트리 */}
<div className="space-y-1 rounded-lg border bg-card p-4 shadow-sm">
{isLoading ? (
<div className="py-8 text-center text-sm text-muted-foreground"> ...</div>
) : departments.length === 0 ? (
<div className="py-8 text-center text-sm text-muted-foreground">
. .
</div>
) : (
renderDepartmentTree(null)
)}
</div>
{/* 부서 추가 모달 */}
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
{parentDeptForAdd ? "하위 부서 추가" : "최상위 부서 추가"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="dept_name">
<span className="text-destructive">*</span>
</Label>
<Input
id="dept_name"
value={newDeptName}
onChange={(e) => setNewDeptName(e.target.value)}
placeholder="부서명을 입력하세요"
autoFocus
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddModalOpen(false)}>
</Button>
<Button onClick={handleSaveDepartment} disabled={!newDeptName.trim()}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}