삭제 후엔 부서 선택 해제

This commit is contained in:
dohyeons 2025-11-03 17:42:46 +09:00
parent 6b53cb414c
commit c50c8d01df
3 changed files with 137 additions and 59 deletions

View File

@ -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);

View File

@ -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",
});
}
};

View File

@ -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>
);
}