삭제를 alert에서 modal로 변경
This commit is contained in:
parent
0d6b018ec4
commit
6b53cb414c
|
|
@ -114,6 +114,22 @@ export async function createDepartment(req: AuthenticatedRequest, res: Response)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 같은 회사 내 중복 부서명 확인
|
||||||
|
const duplicate = await queryOne<any>(`
|
||||||
|
SELECT dept_code, dept_name
|
||||||
|
FROM dept_info
|
||||||
|
WHERE company_code = $1 AND dept_name = $2
|
||||||
|
`, [companyCode, dept_name.trim()]);
|
||||||
|
|
||||||
|
if (duplicate) {
|
||||||
|
res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
message: `"${dept_name}" 부서가 이미 존재합니다.`,
|
||||||
|
isDuplicate: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 회사 이름 조회
|
// 회사 이름 조회
|
||||||
const company = await queryOne<any>(`
|
const company = await queryOne<any>(`
|
||||||
SELECT company_name FROM company_mng WHERE company_code = $1
|
SELECT company_name FROM company_mng WHERE company_code = $1
|
||||||
|
|
@ -322,6 +338,53 @@ export async function getDepartmentMembers(req: AuthenticatedRequest, res: Respo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 검색 (부서원 추가용)
|
||||||
|
*/
|
||||||
|
export async function searchUsers(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { companyCode } = req.params;
|
||||||
|
const { search } = req.query;
|
||||||
|
|
||||||
|
if (!search || typeof search !== 'string') {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "검색어를 입력해주세요.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 검색 (ID 또는 이름)
|
||||||
|
const users = await query<any>(`
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
user_name,
|
||||||
|
email,
|
||||||
|
position_name,
|
||||||
|
company_code
|
||||||
|
FROM user_info
|
||||||
|
WHERE company_code = $1
|
||||||
|
AND (
|
||||||
|
user_id ILIKE $2 OR
|
||||||
|
user_name ILIKE $2
|
||||||
|
)
|
||||||
|
ORDER BY user_name
|
||||||
|
LIMIT 20
|
||||||
|
`, [companyCode, `%${search}%`]);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: users,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("사용자 검색 실패", error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "사용자 검색 중 오류가 발생했습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 부서원 추가
|
* 부서원 추가
|
||||||
*/
|
*/
|
||||||
|
|
@ -361,9 +424,10 @@ export async function addDepartmentMember(req: AuthenticatedRequest, res: Respon
|
||||||
`, [user_id, deptCode]);
|
`, [user_id, deptCode]);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
res.status(400).json({
|
res.status(409).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "이미 해당 부서의 부서원입니다.",
|
message: "이미 해당 부서의 부서원입니다.",
|
||||||
|
isDuplicate: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ router.delete("/:deptCode", departmentController.deleteDepartment);
|
||||||
// 부서원 목록 조회
|
// 부서원 목록 조회
|
||||||
router.get("/:deptCode/members", departmentController.getDepartmentMembers);
|
router.get("/:deptCode/members", departmentController.getDepartmentMembers);
|
||||||
|
|
||||||
|
// 사용자 검색 (부서원 추가용)
|
||||||
|
router.get("/companies/:companyCode/users/search", departmentController.searchUsers);
|
||||||
|
|
||||||
// 부서원 추가
|
// 부서원 추가
|
||||||
router.post("/:deptCode/members", departmentController.addDepartmentMember);
|
router.post("/:deptCode/members", departmentController.addDepartmentMember);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ export function DepartmentManagement({ companyCode }: DepartmentManagementProps)
|
||||||
const [selectedDepartment, setSelectedDepartment] = useState<Department | null>(null);
|
const [selectedDepartment, setSelectedDepartment] = useState<Department | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState<string>("structure");
|
const [activeTab, setActiveTab] = useState<string>("structure");
|
||||||
const [companyName, setCompanyName] = useState<string>("");
|
const [companyName, setCompanyName] = useState<string>("");
|
||||||
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
|
||||||
|
// 부서원 변경 시 부서 구조 새로고침
|
||||||
|
const handleMemberChange = () => {
|
||||||
|
setRefreshTrigger((prev) => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
// 회사 정보 로드
|
// 회사 정보 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -71,11 +77,16 @@ export function DepartmentManagement({ companyCode }: DepartmentManagementProps)
|
||||||
companyCode={companyCode}
|
companyCode={companyCode}
|
||||||
selectedDepartment={selectedDepartment}
|
selectedDepartment={selectedDepartment}
|
||||||
onSelectDepartment={setSelectedDepartment}
|
onSelectDepartment={setSelectedDepartment}
|
||||||
|
refreshTrigger={refreshTrigger}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="members" className="mt-4">
|
<TabsContent value="members" className="mt-4">
|
||||||
<DepartmentMembers companyCode={companyCode} selectedDepartment={selectedDepartment} />
|
<DepartmentMembers
|
||||||
|
companyCode={companyCode}
|
||||||
|
selectedDepartment={selectedDepartment}
|
||||||
|
onMemberChange={handleMemberChange}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,12 +99,17 @@ export function DepartmentManagement({ companyCode }: DepartmentManagementProps)
|
||||||
companyCode={companyCode}
|
companyCode={companyCode}
|
||||||
selectedDepartment={selectedDepartment}
|
selectedDepartment={selectedDepartment}
|
||||||
onSelectDepartment={setSelectedDepartment}
|
onSelectDepartment={setSelectedDepartment}
|
||||||
|
refreshTrigger={refreshTrigger}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측: 부서 인원 (80%) */}
|
{/* 우측: 부서 인원 (80%) */}
|
||||||
<div className="w-[80%] pl-0">
|
<div className="w-[80%] pl-0">
|
||||||
<DepartmentMembers companyCode={companyCode} selectedDepartment={selectedDepartment} />
|
<DepartmentMembers
|
||||||
|
companyCode={companyCode}
|
||||||
|
selectedDepartment={selectedDepartment}
|
||||||
|
onMemberChange={handleMemberChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,16 +19,28 @@ import * as departmentAPI from "@/lib/api/department";
|
||||||
interface DepartmentMembersProps {
|
interface DepartmentMembersProps {
|
||||||
companyCode: string;
|
companyCode: string;
|
||||||
selectedDepartment: Department | null;
|
selectedDepartment: Department | null;
|
||||||
|
onMemberChange?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 부서 인원 관리 컴포넌트
|
* 부서 인원 관리 컴포넌트
|
||||||
*/
|
*/
|
||||||
export function DepartmentMembers({ companyCode, selectedDepartment }: DepartmentMembersProps) {
|
export function DepartmentMembers({
|
||||||
|
companyCode,
|
||||||
|
selectedDepartment,
|
||||||
|
onMemberChange,
|
||||||
|
}: DepartmentMembersProps) {
|
||||||
const [members, setMembers] = useState<DepartmentMember[]>([]);
|
const [members, setMembers] = useState<DepartmentMember[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [searchUserId, setSearchUserId] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const [duplicateMessage, setDuplicateMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 부서원 삭제 확인 모달
|
||||||
|
const [removeConfirmOpen, setRemoveConfirmOpen] = useState(false);
|
||||||
|
const [memberToRemove, setMemberToRemove] = useState<{ userId: string; name: string } | null>(null);
|
||||||
|
|
||||||
// 부서원 목록 로드
|
// 부서원 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -57,42 +69,79 @@ export function DepartmentMembers({ companyCode, selectedDepartment }: Departmen
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 사용자 검색
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!searchQuery.trim()) {
|
||||||
|
setSearchResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSearching(true);
|
||||||
|
try {
|
||||||
|
const response = await departmentAPI.searchUsers(companyCode, searchQuery);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setSearchResults(response.data);
|
||||||
|
} else {
|
||||||
|
setSearchResults([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("사용자 검색 실패:", error);
|
||||||
|
setSearchResults([]);
|
||||||
|
} finally {
|
||||||
|
setIsSearching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 부서원 추가
|
// 부서원 추가
|
||||||
const handleAddMember = async () => {
|
const handleAddMember = async (userId: string) => {
|
||||||
if (!searchUserId.trim() || !selectedDepartment) return;
|
if (!selectedDepartment) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await departmentAPI.addDepartmentMember(
|
const response = await departmentAPI.addDepartmentMember(
|
||||||
selectedDepartment.dept_code,
|
selectedDepartment.dept_code,
|
||||||
searchUserId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
setSearchUserId("");
|
setSearchQuery("");
|
||||||
|
setSearchResults([]);
|
||||||
loadMembers();
|
loadMembers();
|
||||||
|
onMemberChange?.(); // 부서 구조 새로고침
|
||||||
|
} else {
|
||||||
|
if ((response as any).isDuplicate) {
|
||||||
|
setDuplicateMessage(response.error || "이미 해당 부서의 부서원입니다.");
|
||||||
} else {
|
} else {
|
||||||
alert(response.error || "부서원 추가에 실패했습니다.");
|
alert(response.error || "부서원 추가에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("부서원 추가 실패:", error);
|
console.error("부서원 추가 실패:", error);
|
||||||
alert("부서원 추가 중 오류가 발생했습니다.");
|
alert("부서원 추가 중 오류가 발생했습니다.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 부서원 제거
|
// 부서원 제거 확인 요청
|
||||||
const handleRemoveMember = async (userId: string) => {
|
const handleRemoveMemberRequest = (userId: string, userName: string) => {
|
||||||
if (!selectedDepartment) return;
|
setMemberToRemove({ userId, name: userName });
|
||||||
if (!confirm("이 부서에서 사용자를 제외하시겠습니까?")) return;
|
setRemoveConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 부서원 제거 실행
|
||||||
|
const handleRemoveMemberConfirm = async () => {
|
||||||
|
if (!selectedDepartment || !memberToRemove) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await departmentAPI.removeDepartmentMember(
|
const response = await departmentAPI.removeDepartmentMember(
|
||||||
selectedDepartment.dept_code,
|
selectedDepartment.dept_code,
|
||||||
userId
|
memberToRemove.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
setRemoveConfirmOpen(false);
|
||||||
|
setMemberToRemove(null);
|
||||||
loadMembers();
|
loadMembers();
|
||||||
|
onMemberChange?.(); // 부서 구조 새로고침
|
||||||
} else {
|
} else {
|
||||||
alert(response.error || "부서원 제거에 실패했습니다.");
|
alert(response.error || "부서원 제거에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +244,7 @@ export function DepartmentMembers({ companyCode, selectedDepartment }: Departmen
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-destructive"
|
className="h-8 w-8 text-destructive"
|
||||||
onClick={() => handleRemoveMember(member.user_id)}
|
onClick={() => handleRemoveMemberRequest(member.user_id, member.user_name)}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -208,35 +257,139 @@ export function DepartmentMembers({ companyCode, selectedDepartment }: Departmen
|
||||||
|
|
||||||
{/* 부서원 추가 모달 */}
|
{/* 부서원 추가 모달 */}
|
||||||
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>부서원 추가</DialogTitle>
|
<DialogTitle className="text-base sm:text-lg">부서원 추가</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4">
|
||||||
|
{/* 검색 입력 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="user_id">
|
<Label htmlFor="search" className="text-xs sm:text-sm">
|
||||||
사용자 ID <span className="text-destructive">*</span>
|
사용자 검색 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="user_id"
|
id="search"
|
||||||
value={searchUserId}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchUserId(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="사용자 ID를 입력하세요"
|
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||||||
|
placeholder="이름 또는 ID로 검색"
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={!searchQuery.trim() || isSearching}
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
>
|
||||||
|
검색
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
겸직이 가능합니다. 한 사용자를 여러 부서에 추가할 수 있습니다.
|
겸직이 가능합니다. 한 사용자를 여러 부서에 추가할 수 있습니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 검색 결과 */}
|
||||||
|
{isSearching ? (
|
||||||
|
<div className="py-8 text-center text-sm text-muted-foreground">검색 중...</div>
|
||||||
|
) : searchResults.length > 0 ? (
|
||||||
|
<div className="max-h-64 space-y-2 overflow-y-auto rounded-lg border p-2">
|
||||||
|
{searchResults.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.user_id}
|
||||||
|
className="flex cursor-pointer items-center justify-between rounded-lg border bg-background p-3 transition-colors hover:bg-muted/50"
|
||||||
|
onClick={() => handleAddMember(user.user_id)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">{user.user_name}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">({user.user_id})</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex gap-3 text-xs text-muted-foreground">
|
||||||
|
{user.position_name && <span>{user.position_name}</span>}
|
||||||
|
{user.email && <span>{user.email}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : searchQuery && !isSearching ? (
|
||||||
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||||
|
검색 결과가 없습니다.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddModalOpen(false);
|
||||||
|
setSearchQuery("");
|
||||||
|
setSearchResults([]);
|
||||||
|
}}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
닫기
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 중복 알림 모달 */}
|
||||||
|
<Dialog open={!!duplicateMessage} onOpenChange={() => setDuplicateMessage(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">{duplicateMessage}</p>
|
||||||
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsAddModalOpen(false)}>
|
<Button
|
||||||
|
onClick={() => setDuplicateMessage(null)}
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 부서원 제거 확인 모달 */}
|
||||||
|
<Dialog open={removeConfirmOpen} onOpenChange={setRemoveConfirmOpen}>
|
||||||
|
<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">
|
||||||
|
<span className="font-semibold">{memberToRemove?.name}</span> 님을 이 부서에서 제외하시겠습니까?
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
다른 부서에는 영향을 주지 않습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setRemoveConfirmOpen(false);
|
||||||
|
setMemberToRemove(null);
|
||||||
|
}}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
취소
|
취소
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAddMember} disabled={!searchUserId.trim()}>
|
<Button
|
||||||
추가
|
variant="destructive"
|
||||||
|
onClick={handleRemoveMemberConfirm}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
제거
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ interface DepartmentStructureProps {
|
||||||
companyCode: string;
|
companyCode: string;
|
||||||
selectedDepartment: Department | null;
|
selectedDepartment: Department | null;
|
||||||
onSelectDepartment: (department: Department | null) => void;
|
onSelectDepartment: (department: Department | null) => void;
|
||||||
|
refreshTrigger?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,6 +29,7 @@ export function DepartmentStructure({
|
||||||
companyCode,
|
companyCode,
|
||||||
selectedDepartment,
|
selectedDepartment,
|
||||||
onSelectDepartment,
|
onSelectDepartment,
|
||||||
|
refreshTrigger,
|
||||||
}: DepartmentStructureProps) {
|
}: DepartmentStructureProps) {
|
||||||
const [departments, setDepartments] = useState<Department[]>([]);
|
const [departments, setDepartments] = useState<Department[]>([]);
|
||||||
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set());
|
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set());
|
||||||
|
|
@ -37,11 +39,16 @@ export function DepartmentStructure({
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [parentDeptForAdd, setParentDeptForAdd] = useState<string | null>(null);
|
const [parentDeptForAdd, setParentDeptForAdd] = useState<string | null>(null);
|
||||||
const [newDeptName, setNewDeptName] = useState("");
|
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);
|
||||||
|
|
||||||
// 부서 목록 로드
|
// 부서 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDepartments();
|
loadDepartments();
|
||||||
}, [companyCode]);
|
}, [companyCode, refreshTrigger]);
|
||||||
|
|
||||||
const loadDepartments = async () => {
|
const loadDepartments = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -87,24 +94,38 @@ export function DepartmentStructure({
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
|
setNewDeptName("");
|
||||||
|
setParentDeptForAdd(null);
|
||||||
loadDepartments();
|
loadDepartments();
|
||||||
|
} else {
|
||||||
|
if ((response as any).isDuplicate) {
|
||||||
|
setDuplicateMessage(response.error || "이미 존재하는 부서명입니다.");
|
||||||
} else {
|
} else {
|
||||||
alert(response.error || "부서 추가에 실패했습니다.");
|
alert(response.error || "부서 추가에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("부서 추가 실패:", error);
|
console.error("부서 추가 실패:", error);
|
||||||
alert("부서 추가 중 오류가 발생했습니다.");
|
alert("부서 추가 중 오류가 발생했습니다.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 부서 삭제
|
// 부서 삭제 확인 요청
|
||||||
const handleDeleteDepartment = async (deptCode: string) => {
|
const handleDeleteDepartmentRequest = (deptCode: string, deptName: string) => {
|
||||||
if (!confirm("이 부서를 삭제하시겠습니까?")) return;
|
setDeptToDelete({ code: deptCode, name: deptName });
|
||||||
|
setDeleteConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 부서 삭제 실행
|
||||||
|
const handleDeleteDepartmentConfirm = async () => {
|
||||||
|
if (!deptToDelete) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await departmentAPI.deleteDepartment(deptCode);
|
const response = await departmentAPI.deleteDepartment(deptToDelete.code);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
setDeptToDelete(null);
|
||||||
loadDepartments();
|
loadDepartments();
|
||||||
} else {
|
} else {
|
||||||
alert(response.error || "부서 삭제에 실패했습니다.");
|
alert(response.error || "부서 삭제에 실패했습니다.");
|
||||||
|
|
@ -196,7 +217,7 @@ export function DepartmentStructure({
|
||||||
className="h-6 w-6 text-destructive"
|
className="h-6 w-6 text-destructive"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteDepartment(dept.dept_code);
|
handleDeleteDepartmentRequest(dept.dept_code, dept.dept_name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
|
|
@ -269,6 +290,62 @@ export function DepartmentStructure({
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 중복 알림 모달 */}
|
||||||
|
<Dialog open={!!duplicateMessage} onOpenChange={() => setDuplicateMessage(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">{duplicateMessage}</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => setDuplicateMessage(null)}
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 부서 삭제 확인 모달 */}
|
||||||
|
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||||
|
<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">
|
||||||
|
<span className="font-semibold">{deptToDelete?.name}</span> 부서를 삭제하시겠습니까?
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
이 작업은 되돌릴 수 없습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
setDeptToDelete(null);
|
||||||
|
}}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteDepartmentConfirm}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,12 @@ export async function createDepartment(companyCode: string, data: DepartmentForm
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("부서 생성 실패:", error);
|
console.error("부서 생성 실패:", error);
|
||||||
return { success: false, error: error.message };
|
const isDuplicate = error.response?.status === 409;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.response?.data?.message || error.message,
|
||||||
|
isDuplicate,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,18 +94,39 @@ export async function getDepartmentMembers(deptCode: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 검색 (부서원 추가용)
|
||||||
|
*/
|
||||||
|
export async function searchUsers(companyCode: string, search: string) {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<{ success: boolean; data: any[] }>(
|
||||||
|
`/departments/companies/${companyCode}/users/search`,
|
||||||
|
{ params: { search } },
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("사용자 검색 실패:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 부서원 추가
|
* 부서원 추가
|
||||||
*/
|
*/
|
||||||
export async function addDepartmentMember(deptCode: string, userId: string) {
|
export async function addDepartmentMember(deptCode: string, userId: string) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<{ success: boolean }>(`/departments/${deptCode}/members`, {
|
const response = await apiClient.post<{ success: boolean; message?: string }>(`/departments/${deptCode}/members`, {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("부서원 추가 실패:", error);
|
console.error("부서원 추가 실패:", error);
|
||||||
return { success: false, error: error.message };
|
const isDuplicate = error.response?.status === 409;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.response?.data?.message || error.message,
|
||||||
|
isDuplicate,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue