삭제 후엔 부서 선택 해제
This commit is contained in:
parent
6b53cb414c
commit
c50c8d01df
|
|
@ -250,25 +250,19 @@ export async function deleteDepartment(req: AuthenticatedRequest, res: Response)
|
|||
if (parseInt(hasChildren?.count || "0") > 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "하위 부서가 있는 부서는 삭제할 수 없습니다.",
|
||||
message: "하위 부서가 있는 부서는 삭제할 수 없습니다. 먼저 하위 부서를 삭제해주세요.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 부서원 확인
|
||||
const hasMembers = await queryOne<any>(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM user_dept
|
||||
// 부서원 삭제 (부서 삭제 전에 먼저 삭제)
|
||||
const deletedMembers = await query<any>(`
|
||||
DELETE FROM user_dept
|
||||
WHERE dept_code = $1
|
||||
RETURNING user_id
|
||||
`, [deptCode]);
|
||||
|
||||
if (parseInt(hasMembers?.count || "0") > 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "부서원이 있는 부서는 삭제할 수 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const memberCount = deletedMembers.length;
|
||||
|
||||
// 부서 삭제
|
||||
const result = await query<any>(`
|
||||
|
|
@ -285,11 +279,17 @@ export async function deleteDepartment(req: AuthenticatedRequest, res: Response)
|
|||
return;
|
||||
}
|
||||
|
||||
logger.info("부서 삭제 성공", { deptCode });
|
||||
logger.info("부서 삭제 성공", {
|
||||
deptCode,
|
||||
deptName: result[0].dept_name,
|
||||
deletedMemberCount: memberCount
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "부서가 삭제되었습니다.",
|
||||
message: memberCount > 0
|
||||
? `부서가 삭제되었습니다. (부서원 ${memberCount}명 제외됨)`
|
||||
: "부서가 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("부서 삭제 실패", error);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import type { Department, DepartmentMember } from "@/types/department";
|
||||
import * as departmentAPI from "@/lib/api/department";
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ export function DepartmentMembers({
|
|||
selectedDepartment,
|
||||
onMemberChange,
|
||||
}: DepartmentMembersProps) {
|
||||
const { toast } = useToast();
|
||||
const [members, setMembers] = useState<DepartmentMember[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
|
@ -108,16 +110,31 @@ export function DepartmentMembers({
|
|||
setSearchResults([]);
|
||||
loadMembers();
|
||||
onMemberChange?.(); // 부서 구조 새로고침
|
||||
|
||||
// 성공 Toast 표시
|
||||
toast({
|
||||
title: "부서원 추가 완료",
|
||||
description: "부서원이 추가되었습니다.",
|
||||
variant: "default",
|
||||
});
|
||||
} else {
|
||||
if ((response as any).isDuplicate) {
|
||||
setDuplicateMessage(response.error || "이미 해당 부서의 부서원입니다.");
|
||||
} else {
|
||||
alert(response.error || "부서원 추가에 실패했습니다.");
|
||||
toast({
|
||||
title: "부서원 추가 실패",
|
||||
description: response.error || "부서원 추가에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("부서원 추가 실패:", error);
|
||||
alert("부서원 추가 중 오류가 발생했습니다.");
|
||||
toast({
|
||||
title: "부서원 추가 실패",
|
||||
description: "부서원 추가 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -142,12 +159,27 @@ export function DepartmentMembers({
|
|||
setMemberToRemove(null);
|
||||
loadMembers();
|
||||
onMemberChange?.(); // 부서 구조 새로고침
|
||||
|
||||
// 성공 Toast 표시
|
||||
toast({
|
||||
title: "부서원 제거 완료",
|
||||
description: `${memberToRemove.name} 님이 부서에서 제외되었습니다.`,
|
||||
variant: "default",
|
||||
});
|
||||
} else {
|
||||
alert(response.error || "부서원 제거에 실패했습니다.");
|
||||
toast({
|
||||
title: "부서원 제거 실패",
|
||||
description: response.error || "부서원 제거에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("부서원 제거 실패:", error);
|
||||
alert("부서원 제거 중 오류가 발생했습니다.");
|
||||
toast({
|
||||
title: "부서원 제거 실패",
|
||||
description: "부서원 제거 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -163,12 +195,27 @@ export function DepartmentMembers({
|
|||
|
||||
if (response.success) {
|
||||
loadMembers();
|
||||
|
||||
// 성공 Toast 표시
|
||||
toast({
|
||||
title: "주 부서 설정 완료",
|
||||
description: "주 부서가 변경되었습니다.",
|
||||
variant: "default",
|
||||
});
|
||||
} else {
|
||||
alert(response.error || "주 부서 설정에 실패했습니다.");
|
||||
toast({
|
||||
title: "주 부서 설정 실패",
|
||||
description: response.error || "주 부서 설정에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("주 부서 설정 실패:", error);
|
||||
alert("주 부서 설정 중 오류가 발생했습니다.");
|
||||
toast({
|
||||
title: "주 부서 설정 실패",
|
||||
description: "주 부서 설정 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,11 @@
|
|||
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 { 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 { useToast } from "@/hooks/use-toast";
|
||||
import type { Department } from "@/types/department";
|
||||
import * as departmentAPI from "@/lib/api/department";
|
||||
|
||||
interface DepartmentStructureProps {
|
||||
|
|
@ -31,6 +26,7 @@ export function DepartmentStructure({
|
|||
onSelectDepartment,
|
||||
refreshTrigger,
|
||||
}: DepartmentStructureProps) {
|
||||
const { toast } = useToast();
|
||||
const [departments, setDepartments] = useState<Department[]>([]);
|
||||
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -40,10 +36,11 @@ export function DepartmentStructure({
|
|||
const [parentDeptForAdd, setParentDeptForAdd] = useState<string | null>(null);
|
||||
const [newDeptName, setNewDeptName] = useState("");
|
||||
const [duplicateMessage, setDuplicateMessage] = useState<string | null>(null);
|
||||
|
||||
|
||||
// 부서 삭제 확인 모달
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [deptToDelete, setDeptToDelete] = useState<{ code: string; name: string } | null>(null);
|
||||
const [deleteErrorMessage, setDeleteErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// 부서 목록 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -97,16 +94,31 @@ export function DepartmentStructure({
|
|||
setNewDeptName("");
|
||||
setParentDeptForAdd(null);
|
||||
loadDepartments();
|
||||
|
||||
// 성공 Toast 표시
|
||||
toast({
|
||||
title: "부서 생성 완료",
|
||||
description: `"${newDeptName}" 부서가 생성되었습니다.`,
|
||||
variant: "default",
|
||||
});
|
||||
} else {
|
||||
if ((response as any).isDuplicate) {
|
||||
setDuplicateMessage(response.error || "이미 존재하는 부서명입니다.");
|
||||
} else {
|
||||
alert(response.error || "부서 추가에 실패했습니다.");
|
||||
toast({
|
||||
title: "부서 생성 실패",
|
||||
description: response.error || "부서 추가에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("부서 추가 실패:", error);
|
||||
alert("부서 추가 중 오류가 발생했습니다.");
|
||||
toast({
|
||||
title: "부서 생성 실패",
|
||||
description: "부서 추가 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -124,15 +136,32 @@ export function DepartmentStructure({
|
|||
const response = await departmentAPI.deleteDepartment(deptToDelete.code);
|
||||
|
||||
if (response.success) {
|
||||
// 삭제된 부서가 선택되어 있었다면 선택 해제
|
||||
if (selectedDepartment?.dept_code === deptToDelete.code) {
|
||||
onSelectDepartment(null);
|
||||
}
|
||||
|
||||
setDeleteConfirmOpen(false);
|
||||
setDeptToDelete(null);
|
||||
loadDepartments();
|
||||
|
||||
// 성공 메시지 Toast로 표시 (부서원 수 포함)
|
||||
toast({
|
||||
title: "부서 삭제 완료",
|
||||
description: response.message || "부서가 삭제되었습니다.",
|
||||
variant: "default",
|
||||
});
|
||||
} else {
|
||||
alert(response.error || "부서 삭제에 실패했습니다.");
|
||||
// 삭제 확인 모달을 닫고 에러 모달을 표시
|
||||
setDeleteConfirmOpen(false);
|
||||
setDeptToDelete(null);
|
||||
setDeleteErrorMessage(response.error || "부서 삭제에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("부서 삭제 실패:", error);
|
||||
alert("부서 삭제 중 오류가 발생했습니다.");
|
||||
setDeleteConfirmOpen(false);
|
||||
setDeptToDelete(null);
|
||||
setDeleteErrorMessage("부서 삭제 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -160,15 +189,12 @@ export function DepartmentStructure({
|
|||
<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 ${
|
||||
className={`hover:bg-muted flex cursor-pointer items-center justify-between rounded-lg p-2 text-sm transition-colors ${
|
||||
isSelected ? "bg-primary/10 text-primary" : ""
|
||||
}`}
|
||||
style={{ marginLeft: `${level * 16}px` }}
|
||||
>
|
||||
<div
|
||||
className="flex flex-1 items-center gap-2"
|
||||
onClick={() => onSelectDepartment(dept)}
|
||||
>
|
||||
<div className="flex flex-1 items-center gap-2" onClick={() => onSelectDepartment(dept)}>
|
||||
{/* 확장/축소 아이콘 */}
|
||||
{hasChildren ? (
|
||||
<button
|
||||
|
|
@ -178,11 +204,7 @@ export function DepartmentStructure({
|
|||
}}
|
||||
className="h-4 w-4"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight 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" />
|
||||
|
|
@ -192,7 +214,7 @@ export function DepartmentStructure({
|
|||
<span className="font-medium">{dept.dept_name}</span>
|
||||
|
||||
{/* 인원수 */}
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-xs">
|
||||
<Users className="h-3 w-3" />
|
||||
<span>{dept.memberCount || 0}</span>
|
||||
</div>
|
||||
|
|
@ -214,7 +236,7 @@ export function DepartmentStructure({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-destructive"
|
||||
className="text-destructive h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteDepartmentRequest(dept.dept_code, dept.dept_name);
|
||||
|
|
@ -244,11 +266,11 @@ export function DepartmentStructure({
|
|||
</div>
|
||||
|
||||
{/* 부서 트리 */}
|
||||
<div className="space-y-1 rounded-lg border bg-card p-4 shadow-sm">
|
||||
<div className="bg-card space-y-1 rounded-lg border p-4 shadow-sm">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">로딩 중...</div>
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">로딩 중...</div>
|
||||
) : departments.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||
부서가 없습니다. 최상위 부서를 추가해주세요.
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -260,9 +282,7 @@ export function DepartmentStructure({
|
|||
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{parentDeptForAdd ? "하위 부서 추가" : "최상위 부서 추가"}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{parentDeptForAdd ? "하위 부서 추가" : "최상위 부서 추가"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
|
|
@ -301,10 +321,7 @@ export function DepartmentStructure({
|
|||
<p className="text-sm">{duplicateMessage}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => setDuplicateMessage(null)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
<Button onClick={() => setDuplicateMessage(null)} className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
@ -321,9 +338,7 @@ export function DepartmentStructure({
|
|||
<p className="text-sm">
|
||||
<span className="font-semibold">{deptToDelete?.name}</span> 부서를 삭제하시겠습니까?
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-xs">이 작업은 되돌릴 수 없습니다.</p>
|
||||
</div>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
|
|
@ -346,7 +361,23 @@ export function DepartmentStructure({
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 부서 삭제 에러 모달 */}
|
||||
<Dialog open={!!deleteErrorMessage} onOpenChange={() => setDeleteErrorMessage(null)}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">삭제 불가</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-sm">{deleteErrorMessage}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setDeleteErrorMessage(null)} className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue