2025-08-21 09:41:46 +09:00
|
|
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
|
|
|
import { User, UserSearchFilter } from "@/types/user";
|
|
|
|
|
import { userAPI } from "@/lib/api/user"; // 백엔드 연동 활성화
|
|
|
|
|
import { PaginationInfo } from "@/components/common/Pagination";
|
|
|
|
|
import { useDebounce } from "./useDebounce";
|
|
|
|
|
// import { MOCK_USERS } from "@/constants/user"; // 목 데이터 비활성화
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 관리 비즈니스 로직을 담당하는 커스텀 훅
|
|
|
|
|
*/
|
|
|
|
|
export const useUserManagement = () => {
|
|
|
|
|
// 사용자 목록 상태
|
|
|
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
|
|
|
|
|
|
|
|
// 검색 필터 상태
|
|
|
|
|
const [searchFilter, setSearchFilter] = useState<UserSearchFilter>({});
|
|
|
|
|
|
2025-08-26 14:23:22 +09:00
|
|
|
// 통합 검색어 디바운싱 (500ms 지연)
|
2025-08-21 09:41:46 +09:00
|
|
|
const debouncedSearchValue = useDebounce(searchFilter.searchValue || "", 500);
|
|
|
|
|
|
2025-08-26 14:23:22 +09:00
|
|
|
// 고급 검색 필드들 디바운싱
|
|
|
|
|
const debouncedSabun = useDebounce(searchFilter.search_sabun || "", 500);
|
|
|
|
|
const debouncedCompanyName = useDebounce(searchFilter.search_companyName || "", 500);
|
|
|
|
|
const debouncedDeptName = useDebounce(searchFilter.search_deptName || "", 500);
|
|
|
|
|
const debouncedPositionName = useDebounce(searchFilter.search_positionName || "", 500);
|
|
|
|
|
const debouncedUserId = useDebounce(searchFilter.search_userId || "", 500);
|
|
|
|
|
const debouncedUserName = useDebounce(searchFilter.search_userName || "", 500);
|
|
|
|
|
const debouncedTel = useDebounce(searchFilter.search_tel || "", 500);
|
|
|
|
|
const debouncedEmail = useDebounce(searchFilter.search_email || "", 500);
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
// 디바운싱된 검색 필터 (useMemo로 최적화)
|
|
|
|
|
const debouncedSearchFilter = useMemo(
|
|
|
|
|
() => ({
|
2025-08-26 14:23:22 +09:00
|
|
|
// 통합 검색
|
2025-08-21 09:41:46 +09:00
|
|
|
searchValue: debouncedSearchValue,
|
2025-08-26 14:23:22 +09:00
|
|
|
|
|
|
|
|
// 고급 검색
|
|
|
|
|
search_sabun: debouncedSabun,
|
|
|
|
|
search_companyName: debouncedCompanyName,
|
|
|
|
|
search_deptName: debouncedDeptName,
|
|
|
|
|
search_positionName: debouncedPositionName,
|
|
|
|
|
search_userId: debouncedUserId,
|
|
|
|
|
search_userName: debouncedUserName,
|
|
|
|
|
search_tel: debouncedTel,
|
|
|
|
|
search_email: debouncedEmail,
|
|
|
|
|
|
|
|
|
|
// 하위 호환성
|
2025-08-21 09:41:46 +09:00
|
|
|
searchType: searchFilter.searchType || "all",
|
|
|
|
|
}),
|
2025-08-26 14:23:22 +09:00
|
|
|
[
|
|
|
|
|
debouncedSearchValue,
|
|
|
|
|
debouncedSabun,
|
|
|
|
|
debouncedCompanyName,
|
|
|
|
|
debouncedDeptName,
|
|
|
|
|
debouncedPositionName,
|
|
|
|
|
debouncedUserId,
|
|
|
|
|
debouncedUserName,
|
|
|
|
|
debouncedTel,
|
|
|
|
|
debouncedEmail,
|
|
|
|
|
searchFilter.searchType,
|
|
|
|
|
],
|
2025-08-21 09:41:46 +09:00
|
|
|
);
|
|
|
|
|
|
2025-08-26 14:23:22 +09:00
|
|
|
// 검색 중인지 확인 (모든 검색 필드를 고려)
|
|
|
|
|
const isSearching = useMemo(() => {
|
|
|
|
|
return (
|
|
|
|
|
(searchFilter.searchValue || "") !== debouncedSearchValue ||
|
|
|
|
|
(searchFilter.search_sabun || "") !== debouncedSabun ||
|
|
|
|
|
(searchFilter.search_companyName || "") !== debouncedCompanyName ||
|
|
|
|
|
(searchFilter.search_deptName || "") !== debouncedDeptName ||
|
|
|
|
|
(searchFilter.search_positionName || "") !== debouncedPositionName ||
|
|
|
|
|
(searchFilter.search_userId || "") !== debouncedUserId ||
|
|
|
|
|
(searchFilter.search_userName || "") !== debouncedUserName ||
|
|
|
|
|
(searchFilter.search_tel || "") !== debouncedTel ||
|
|
|
|
|
(searchFilter.search_email || "") !== debouncedEmail
|
|
|
|
|
);
|
|
|
|
|
}, [
|
|
|
|
|
searchFilter.searchValue,
|
|
|
|
|
debouncedSearchValue,
|
|
|
|
|
searchFilter.search_sabun,
|
|
|
|
|
debouncedSabun,
|
|
|
|
|
searchFilter.search_companyName,
|
|
|
|
|
debouncedCompanyName,
|
|
|
|
|
searchFilter.search_deptName,
|
|
|
|
|
debouncedDeptName,
|
|
|
|
|
searchFilter.search_positionName,
|
|
|
|
|
debouncedPositionName,
|
|
|
|
|
searchFilter.search_userId,
|
|
|
|
|
debouncedUserId,
|
|
|
|
|
searchFilter.search_userName,
|
|
|
|
|
debouncedUserName,
|
|
|
|
|
searchFilter.search_tel,
|
|
|
|
|
debouncedTel,
|
|
|
|
|
searchFilter.search_email,
|
|
|
|
|
debouncedEmail,
|
|
|
|
|
]);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 로딩 및 에러 상태
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// 페이지네이션 상태
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
const [pageSize, setPageSize] = useState(20);
|
|
|
|
|
const [totalItems, setTotalItems] = useState(0);
|
|
|
|
|
|
2025-08-26 14:23:22 +09:00
|
|
|
// 사용자 목록 로드 (새로운 통합 검색 방식)
|
2025-08-21 09:41:46 +09:00
|
|
|
const loadUsers = useCallback(
|
2025-08-26 14:23:22 +09:00
|
|
|
async (filter?: UserSearchFilter) => {
|
2025-08-21 09:41:46 +09:00
|
|
|
setIsLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-26 14:23:22 +09:00
|
|
|
// 검색 파라미터 구성
|
2025-08-21 09:41:46 +09:00
|
|
|
const searchParams: Record<string, string | number | undefined> = {
|
|
|
|
|
page: currentPage,
|
|
|
|
|
countPerPage: pageSize,
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-26 14:23:22 +09:00
|
|
|
// 검색 조건 추가
|
|
|
|
|
if (filter) {
|
|
|
|
|
// 통합 검색 (우선순위 최고)
|
|
|
|
|
if (filter.searchValue && filter.searchValue.trim()) {
|
|
|
|
|
searchParams.search = filter.searchValue.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 고급 검색 (개별 필드별)
|
|
|
|
|
if (filter.search_sabun && filter.search_sabun.trim()) {
|
|
|
|
|
searchParams.search_sabun = filter.search_sabun.trim();
|
|
|
|
|
}
|
|
|
|
|
if (filter.search_companyName && filter.search_companyName.trim()) {
|
|
|
|
|
searchParams.search_companyName = filter.search_companyName.trim();
|
|
|
|
|
}
|
|
|
|
|
if (filter.search_deptName && filter.search_deptName.trim()) {
|
|
|
|
|
searchParams.search_deptName = filter.search_deptName.trim();
|
|
|
|
|
}
|
|
|
|
|
if (filter.search_positionName && filter.search_positionName.trim()) {
|
|
|
|
|
searchParams.search_positionName = filter.search_positionName.trim();
|
|
|
|
|
}
|
|
|
|
|
if (filter.search_userId && filter.search_userId.trim()) {
|
|
|
|
|
searchParams.search_userId = filter.search_userId.trim();
|
|
|
|
|
}
|
|
|
|
|
if (filter.search_userName && filter.search_userName.trim()) {
|
|
|
|
|
searchParams.search_userName = filter.search_userName.trim();
|
|
|
|
|
}
|
|
|
|
|
if (filter.search_tel && filter.search_tel.trim()) {
|
|
|
|
|
searchParams.search_tel = filter.search_tel.trim();
|
|
|
|
|
}
|
|
|
|
|
if (filter.search_email && filter.search_email.trim()) {
|
|
|
|
|
searchParams.search_email = filter.search_email.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 하위 호환성: 기존 searchType/searchValue 방식 지원
|
|
|
|
|
if (!filter.searchValue && filter.searchType && filter.searchType !== "all" && searchParams.searchValue) {
|
|
|
|
|
// 기존 방식 변환은 일단 제거 (통합 검색과 고급 검색만 지원)
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("🔍 검색 파라미터:", searchParams);
|
|
|
|
|
const response = await userAPI.getList(searchParams);
|
|
|
|
|
|
|
|
|
|
// 백엔드 응답 구조에 맞게 처리 { success, data, total }
|
2025-08-21 13:28:49 +09:00
|
|
|
if (response && response.success && response.data) {
|
|
|
|
|
// 새로운 API 응답 구조: { success, data: { users, pagination } }
|
|
|
|
|
if (response.data.users && Array.isArray(response.data.users)) {
|
|
|
|
|
setUsers(response.data.users);
|
|
|
|
|
setTotalItems(response.data.pagination?.totalCount || response.data.users.length);
|
|
|
|
|
} else if (Array.isArray(response.data)) {
|
|
|
|
|
// 기존 구조: { success, data: User[] }
|
|
|
|
|
setUsers(response.data);
|
|
|
|
|
setTotalItems(response.total || response.data.length);
|
|
|
|
|
} else {
|
|
|
|
|
setUsers([]);
|
|
|
|
|
setTotalItems(0);
|
|
|
|
|
}
|
2025-08-21 09:41:46 +09:00
|
|
|
} else {
|
|
|
|
|
setUsers([]);
|
|
|
|
|
setTotalItems(0);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "사용자 목록 조회에 실패했습니다.");
|
|
|
|
|
setUsers([]);
|
|
|
|
|
setTotalItems(0);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[currentPage, pageSize],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 초기 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadUsers();
|
|
|
|
|
}, [loadUsers]);
|
|
|
|
|
|
2025-08-26 14:23:22 +09:00
|
|
|
// 디바운싱된 검색 조건이 변경될 때마다 API 호출
|
2025-08-21 09:41:46 +09:00
|
|
|
useEffect(() => {
|
2025-08-26 14:23:22 +09:00
|
|
|
loadUsers(debouncedSearchFilter);
|
|
|
|
|
}, [
|
|
|
|
|
debouncedSearchFilter.searchValue,
|
|
|
|
|
debouncedSearchFilter.search_sabun,
|
|
|
|
|
debouncedSearchFilter.search_companyName,
|
|
|
|
|
debouncedSearchFilter.search_deptName,
|
|
|
|
|
debouncedSearchFilter.search_positionName,
|
|
|
|
|
debouncedSearchFilter.search_userId,
|
|
|
|
|
debouncedSearchFilter.search_userName,
|
|
|
|
|
debouncedSearchFilter.search_tel,
|
|
|
|
|
debouncedSearchFilter.search_email,
|
|
|
|
|
loadUsers,
|
|
|
|
|
]);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 검색 필터 업데이트
|
|
|
|
|
const updateSearchFilter = useCallback((newFilter: Partial<UserSearchFilter>) => {
|
|
|
|
|
setSearchFilter((prev) => ({ ...prev, ...newFilter }));
|
|
|
|
|
|
2025-08-26 14:23:22 +09:00
|
|
|
// 검색 조건이 변경될 때마다 첫 페이지로 이동
|
|
|
|
|
const hasSearchChange = !!(
|
|
|
|
|
newFilter.searchValue !== undefined ||
|
|
|
|
|
newFilter.search_sabun !== undefined ||
|
|
|
|
|
newFilter.search_companyName !== undefined ||
|
|
|
|
|
newFilter.search_deptName !== undefined ||
|
|
|
|
|
newFilter.search_positionName !== undefined ||
|
|
|
|
|
newFilter.search_userId !== undefined ||
|
|
|
|
|
newFilter.search_userName !== undefined ||
|
|
|
|
|
newFilter.search_tel !== undefined ||
|
|
|
|
|
newFilter.search_email !== undefined ||
|
|
|
|
|
newFilter.searchType !== undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (hasSearchChange) {
|
2025-08-21 09:41:46 +09:00
|
|
|
setCurrentPage(1);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 페이지 변경 핸들러
|
|
|
|
|
const handlePageChange = useCallback((page: number) => {
|
|
|
|
|
setCurrentPage(page);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 페이지 크기 변경 핸들러
|
|
|
|
|
const handlePageSizeChange = useCallback((newPageSize: number) => {
|
|
|
|
|
setPageSize(newPageSize);
|
|
|
|
|
setCurrentPage(1); // 페이지 크기 변경 시 첫 페이지로 이동
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 페이지네이션 정보 계산
|
|
|
|
|
const paginationInfo: PaginationInfo = {
|
|
|
|
|
currentPage,
|
|
|
|
|
totalPages: Math.ceil(totalItems / pageSize),
|
|
|
|
|
totalItems,
|
|
|
|
|
itemsPerPage: pageSize,
|
|
|
|
|
startItem: totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0,
|
|
|
|
|
endItem: Math.min(currentPage * pageSize, totalItems),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 사용자 편집 기능 제거됨
|
|
|
|
|
|
|
|
|
|
// 사용자 삭제 기능 제거됨
|
|
|
|
|
|
|
|
|
|
// 사용자 상태 토글 핸들러
|
|
|
|
|
const handleStatusToggle = useCallback(async (user: User, newStatus: string) => {
|
|
|
|
|
try {
|
2025-08-26 09:56:45 +09:00
|
|
|
console.log(`🎛️ 상태 변경: ${user.userName} (${user.userId}) → ${newStatus}`);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 백엔드 API 호출
|
2025-08-26 09:56:45 +09:00
|
|
|
const response = await userAPI.updateStatus(user.userId, newStatus);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 백엔드 응답 구조: { result: boolean, msg: string }
|
|
|
|
|
if (response && typeof response === "object" && "result" in response) {
|
|
|
|
|
const apiResponse = response as unknown as { result: boolean; msg: string };
|
|
|
|
|
|
|
|
|
|
if (apiResponse.result) {
|
|
|
|
|
console.log("✅ 상태 변경 성공:", apiResponse.msg);
|
|
|
|
|
|
|
|
|
|
// 전체 목록 새로고침 대신 개별 사용자 상태만 업데이트
|
2025-08-26 09:56:45 +09:00
|
|
|
setUsers((prevUsers) => prevUsers.map((u) => (u.userId === user.userId ? { ...u, status: newStatus } : u)));
|
2025-08-21 09:41:46 +09:00
|
|
|
} else {
|
|
|
|
|
console.error("❌ 상태 변경 실패:", apiResponse.msg);
|
|
|
|
|
alert(apiResponse.msg || "상태 변경에 실패했습니다.");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.error("❌ 예상치 못한 응답 형식:", response);
|
|
|
|
|
alert("상태 변경 중 오류가 발생했습니다.");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("상태 변경 오류:", error);
|
|
|
|
|
alert("상태 변경 중 오류가 발생했습니다.");
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 데이터 새로고침 (비밀번호 초기화 후 목록 갱신용)
|
|
|
|
|
const refreshData = useCallback(() => {
|
|
|
|
|
loadUsers(debouncedSearchFilter.searchValue, debouncedSearchFilter.searchType);
|
|
|
|
|
}, [loadUsers, debouncedSearchFilter.searchValue, debouncedSearchFilter.searchType]);
|
|
|
|
|
|
|
|
|
|
// 사용자 등록 핸들러
|
|
|
|
|
const handleCreate = useCallback(() => {
|
|
|
|
|
console.log("📝 사용자 등록");
|
|
|
|
|
alert("사용자 등록 기능은 추후 구현 예정입니다.");
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 에러 상태 초기화
|
|
|
|
|
const clearError = useCallback(() => {
|
|
|
|
|
setError(null);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
// 데이터
|
|
|
|
|
users,
|
|
|
|
|
searchFilter,
|
|
|
|
|
isLoading,
|
|
|
|
|
isSearching,
|
|
|
|
|
error,
|
|
|
|
|
paginationInfo,
|
|
|
|
|
|
|
|
|
|
// 검색 기능
|
|
|
|
|
updateSearchFilter,
|
|
|
|
|
|
|
|
|
|
// 페이지네이션
|
|
|
|
|
handlePageChange,
|
|
|
|
|
handlePageSizeChange,
|
|
|
|
|
|
|
|
|
|
// 액션 핸들러
|
|
|
|
|
handleStatusToggle,
|
|
|
|
|
handleCreate,
|
|
|
|
|
|
|
|
|
|
// 유틸리티
|
|
|
|
|
loadUsers,
|
|
|
|
|
refreshData,
|
|
|
|
|
clearError,
|
|
|
|
|
};
|
|
|
|
|
};
|