사용자 검색 기능 구현
This commit is contained in:
parent
6f68fa5639
commit
7267cc52eb
|
|
@ -174,13 +174,30 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
|||
user: req.user,
|
||||
});
|
||||
|
||||
const { page = 1, countPerPage = 20, search, deptCode, status } = req.query;
|
||||
const {
|
||||
page = 1,
|
||||
countPerPage = 20,
|
||||
// 통합 검색 (전체 필드 대상)
|
||||
search,
|
||||
// 고급 검색 (개별 필드별)
|
||||
searchField,
|
||||
searchValue,
|
||||
search_sabun,
|
||||
search_companyName,
|
||||
search_deptName,
|
||||
search_positionName,
|
||||
search_userId,
|
||||
search_userName,
|
||||
search_tel,
|
||||
search_email,
|
||||
// 기존 필터
|
||||
deptCode,
|
||||
status,
|
||||
} = req.query;
|
||||
|
||||
// PostgreSQL 클라이언트 생성
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/ilshin",
|
||||
connectionString: config.databaseUrl,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
|
@ -214,27 +231,109 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
|||
|
||||
const queryParams: any[] = [];
|
||||
let paramIndex = 1;
|
||||
let searchType = "none";
|
||||
|
||||
// 검색 조건 처리
|
||||
if (search && typeof search === "string" && search.trim()) {
|
||||
// 통합 검색 (우선순위: 모든 주요 필드에서 검색)
|
||||
searchType = "unified";
|
||||
const searchTerm = search.trim();
|
||||
|
||||
// 검색 조건 추가
|
||||
if (search) {
|
||||
query += ` AND (
|
||||
u.user_name ILIKE $${paramIndex} OR
|
||||
u.user_id ILIKE $${paramIndex} OR
|
||||
u.sabun ILIKE $${paramIndex} OR
|
||||
u.email ILIKE $${paramIndex}
|
||||
UPPER(COALESCE(u.sabun, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.user_type_name, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.dept_name, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.position_name, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.user_id, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.user_name, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.tel, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.cell_phone, '')) LIKE UPPER($${paramIndex}) OR
|
||||
UPPER(COALESCE(u.email, '')) LIKE UPPER($${paramIndex})
|
||||
)`;
|
||||
queryParams.push(`%${search}%`);
|
||||
queryParams.push(`%${searchTerm}%`);
|
||||
paramIndex++;
|
||||
|
||||
logger.info("통합 검색 실행", { searchTerm });
|
||||
} else if (searchField && searchValue) {
|
||||
// 단일 필드 검색
|
||||
searchType = "single";
|
||||
const fieldMap: { [key: string]: string } = {
|
||||
sabun: "u.sabun",
|
||||
companyName: "u.user_type_name",
|
||||
deptName: "u.dept_name",
|
||||
positionName: "u.position_name",
|
||||
userId: "u.user_id",
|
||||
userName: "u.user_name",
|
||||
tel: "u.tel",
|
||||
cellPhone: "u.cell_phone",
|
||||
email: "u.email",
|
||||
};
|
||||
|
||||
if (fieldMap[searchField as string]) {
|
||||
if (searchField === "tel") {
|
||||
// 전화번호는 TEL과 CELL_PHONE 모두 검색
|
||||
query += ` AND (UPPER(u.tel) LIKE UPPER($${paramIndex}) OR UPPER(u.cell_phone) LIKE UPPER($${paramIndex}))`;
|
||||
} else {
|
||||
query += ` AND UPPER(${fieldMap[searchField as string]}) LIKE UPPER($${paramIndex})`;
|
||||
}
|
||||
queryParams.push(`%${searchValue}%`);
|
||||
paramIndex++;
|
||||
|
||||
logger.info("단일 필드 검색 실행", { searchField, searchValue });
|
||||
}
|
||||
} else {
|
||||
// 고급 검색 (개별 필드별 AND 조건)
|
||||
const advancedSearchFields = [
|
||||
{ param: search_sabun, field: "u.sabun" },
|
||||
{ param: search_companyName, field: "u.user_type_name" },
|
||||
{ param: search_deptName, field: "u.dept_name" },
|
||||
{ param: search_positionName, field: "u.position_name" },
|
||||
{ param: search_userId, field: "u.user_id" },
|
||||
{ param: search_userName, field: "u.user_name" },
|
||||
{ param: search_email, field: "u.email" },
|
||||
];
|
||||
|
||||
let hasAdvancedSearch = false;
|
||||
|
||||
for (const { param, field } of advancedSearchFields) {
|
||||
if (param && typeof param === "string" && param.trim()) {
|
||||
query += ` AND UPPER(${field}) LIKE UPPER($${paramIndex})`;
|
||||
queryParams.push(`%${param.trim()}%`);
|
||||
paramIndex++;
|
||||
hasAdvancedSearch = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 전화번호 검색 (TEL 또는 CELL_PHONE)
|
||||
if (search_tel && typeof search_tel === "string" && search_tel.trim()) {
|
||||
query += ` AND (UPPER(u.tel) LIKE UPPER($${paramIndex}) OR UPPER(u.cell_phone) LIKE UPPER($${paramIndex}))`;
|
||||
queryParams.push(`%${search_tel.trim()}%`);
|
||||
paramIndex++;
|
||||
hasAdvancedSearch = true;
|
||||
}
|
||||
|
||||
if (hasAdvancedSearch) {
|
||||
searchType = "advanced";
|
||||
logger.info("고급 검색 실행", {
|
||||
search_sabun,
|
||||
search_companyName,
|
||||
search_deptName,
|
||||
search_positionName,
|
||||
search_userId,
|
||||
search_userName,
|
||||
search_tel,
|
||||
search_email,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 부서 코드 필터
|
||||
// 기존 필터들
|
||||
if (deptCode) {
|
||||
query += ` AND u.dept_code = $${paramIndex}`;
|
||||
queryParams.push(deptCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (status) {
|
||||
query += ` AND u.status = $${paramIndex}`;
|
||||
queryParams.push(status);
|
||||
|
|
@ -244,26 +343,15 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
|||
// 정렬
|
||||
query += ` ORDER BY u.regdate DESC, u.user_name`;
|
||||
|
||||
// 총 개수 조회
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM user_info u
|
||||
WHERE 1=1
|
||||
${
|
||||
search
|
||||
? `AND (
|
||||
u.user_name ILIKE $1 OR
|
||||
u.user_id ILIKE $1 OR
|
||||
u.sabun ILIKE $1 OR
|
||||
u.email ILIKE $1
|
||||
)`
|
||||
: ""
|
||||
}
|
||||
${deptCode ? `AND u.dept_code = $${search ? 2 : 1}` : ""}
|
||||
${status ? `AND u.status = $${search ? (deptCode ? 3 : 2) : deptCode ? 2 : 1}` : ""}
|
||||
`;
|
||||
// 페이징 파라미터 제외한 카운트용 파라미터
|
||||
const countParams = [...queryParams];
|
||||
|
||||
// 총 개수 조회를 위해 기존 쿼리를 COUNT로 변환
|
||||
const countQuery = query.replace(
|
||||
/SELECT[\s\S]*?FROM/i,
|
||||
"SELECT COUNT(*) as total FROM"
|
||||
).replace(/ORDER BY.*$/i, "");
|
||||
|
||||
const countParams = queryParams.slice(0, -2); // 페이징 파라미터 제외
|
||||
const countResult = await client.query(countQuery, countParams);
|
||||
const totalCount = parseInt(countResult.rows[0].total);
|
||||
|
||||
|
|
@ -360,14 +448,13 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
|||
|
||||
const response = {
|
||||
success: true,
|
||||
data: {
|
||||
users: processedUsers,
|
||||
pagination: {
|
||||
currentPage: Number(page),
|
||||
countPerPage: Number(countPerPage),
|
||||
totalCount: totalCount,
|
||||
totalPages: Math.ceil(totalCount / Number(countPerPage)),
|
||||
},
|
||||
data: processedUsers,
|
||||
total: totalCount,
|
||||
searchType, // 검색 타입 정보 (unified, single, advanced, none)
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(countPerPage),
|
||||
totalPages: Math.ceil(totalCount / Number(countPerPage)),
|
||||
},
|
||||
message: "사용자 목록 조회 성공",
|
||||
};
|
||||
|
|
@ -375,6 +462,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
|||
logger.info("사용자 목록 조회 성공", {
|
||||
totalCount,
|
||||
returnedCount: processedUsers.length,
|
||||
searchType,
|
||||
currentPage: Number(page),
|
||||
countPerPage: Number(countPerPage),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Search, Plus } from "lucide-react";
|
||||
import { Search, Plus, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { UserSearchFilter } from "@/types/user";
|
||||
import { SEARCH_OPTIONS } from "@/constants/user";
|
||||
import { useState } from "react";
|
||||
|
||||
interface UserToolbarProps {
|
||||
searchFilter: UserSearchFilter;
|
||||
|
|
@ -15,7 +14,7 @@ interface UserToolbarProps {
|
|||
|
||||
/**
|
||||
* 사용자 관리 툴바 컴포넌트
|
||||
* 검색, 필터링, 액션 버튼들을 포함
|
||||
* 통합 검색 + 고급 검색 옵션 지원
|
||||
*/
|
||||
export function UserToolbar({
|
||||
searchFilter,
|
||||
|
|
@ -24,62 +23,197 @@ export function UserToolbar({
|
|||
onSearchChange,
|
||||
onCreateClick,
|
||||
}: UserToolbarProps) {
|
||||
const [showAdvancedSearch, setShowAdvancedSearch] = useState(false);
|
||||
|
||||
// 통합 검색어 변경
|
||||
const handleUnifiedSearchChange = (value: string) => {
|
||||
onSearchChange({
|
||||
searchValue: value,
|
||||
// 통합 검색 시 고급 검색 필드들 클리어
|
||||
searchType: undefined,
|
||||
search_sabun: undefined,
|
||||
search_companyName: undefined,
|
||||
search_deptName: undefined,
|
||||
search_positionName: undefined,
|
||||
search_userId: undefined,
|
||||
search_userName: undefined,
|
||||
search_tel: undefined,
|
||||
search_email: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// 고급 검색 필드 변경
|
||||
const handleAdvancedSearchChange = (field: string, value: string) => {
|
||||
onSearchChange({
|
||||
[field]: value,
|
||||
// 고급 검색 시 통합 검색어 클리어
|
||||
searchValue: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// 고급 검색 모드인지 확인
|
||||
const isAdvancedSearchMode = !!(
|
||||
searchFilter.search_sabun ||
|
||||
searchFilter.search_companyName ||
|
||||
searchFilter.search_deptName ||
|
||||
searchFilter.search_positionName ||
|
||||
searchFilter.search_userId ||
|
||||
searchFilter.search_userName ||
|
||||
searchFilter.search_tel ||
|
||||
searchFilter.search_email
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 검색 필터 영역 */}
|
||||
<div className="bg-muted/30 flex flex-wrap gap-4 rounded-lg p-4">
|
||||
{/* 검색 대상 선택 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">검색 대상</label>
|
||||
<Select
|
||||
value={searchFilter.searchType || "all"}
|
||||
onValueChange={(value) =>
|
||||
onSearchChange({
|
||||
searchType: value as (typeof SEARCH_OPTIONS)[number]["value"],
|
||||
searchValue: "", // 옵션 변경 시 항상 검색어 초기화
|
||||
})
|
||||
}
|
||||
{/* 메인 검색 영역 */}
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
{/* 통합 검색 */}
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search
|
||||
className={`absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform ${
|
||||
isSearching ? "animate-pulse text-blue-500" : "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
<Input
|
||||
placeholder="통합 검색..."
|
||||
value={searchFilter.searchValue || ""}
|
||||
onChange={(e) => handleUnifiedSearchChange(e.target.value)}
|
||||
disabled={isAdvancedSearchMode}
|
||||
className={`pl-10 ${isSearching ? "border-blue-300 ring-1 ring-blue-200" : ""} ${
|
||||
isAdvancedSearchMode ? "bg-muted text-muted-foreground cursor-not-allowed" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{isSearching && <p className="mt-1 text-xs text-blue-500">검색 중...</p>}
|
||||
{isAdvancedSearchMode && (
|
||||
<p className="mt-1 text-xs text-amber-600">
|
||||
고급 검색 모드가 활성화되어 있습니다. 통합 검색을 사용하려면 고급 검색 조건을 초기화하세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 고급 검색 토글 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAdvancedSearch(!showAdvancedSearch)}
|
||||
className="gap-2"
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SEARCH_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
🔍 고급 검색
|
||||
{showAdvancedSearch ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 검색어 입력 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">
|
||||
검색어
|
||||
{isSearching && <span className="ml-1 text-xs text-blue-500">(검색 중...)</span>}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Search
|
||||
className={`absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform ${
|
||||
isSearching ? "animate-pulse text-blue-500" : "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
<Input
|
||||
placeholder={
|
||||
(searchFilter.searchType || "all") === "all"
|
||||
? "전체 목록을 조회합니다"
|
||||
: `${SEARCH_OPTIONS.find((opt) => opt.value === (searchFilter.searchType || "all"))?.label || "전체"}을 입력하세요`
|
||||
}
|
||||
value={searchFilter.searchValue || ""}
|
||||
onChange={(e) => onSearchChange({ searchValue: e.target.value })}
|
||||
disabled={(searchFilter.searchType || "all") === "all"}
|
||||
className={`w-[300px] pl-10 ${isSearching ? "border-blue-300 ring-1 ring-blue-200" : ""} ${
|
||||
(searchFilter.searchType || "all") === "all" ? "bg-muted text-muted-foreground cursor-not-allowed" : ""
|
||||
}`}
|
||||
/>
|
||||
{/* 고급 검색 옵션 */}
|
||||
{showAdvancedSearch && (
|
||||
<div className="border-t pt-4">
|
||||
<div className="mb-3">
|
||||
<h4 className="text-sm font-medium">고급 검색 옵션</h4>
|
||||
<span className="text-muted-foreground text-xs">(각 필드별로 개별 검색 조건을 설정할 수 있습니다)</span>
|
||||
</div>
|
||||
|
||||
{/* 고급 검색 필드들 */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">사번</label>
|
||||
<Input
|
||||
placeholder="사번 검색"
|
||||
value={searchFilter.search_sabun || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_sabun", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">회사명</label>
|
||||
<Input
|
||||
placeholder="회사명 검색"
|
||||
value={searchFilter.search_companyName || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_companyName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">부서명</label>
|
||||
<Input
|
||||
placeholder="부서명 검색"
|
||||
value={searchFilter.search_deptName || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_deptName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">직책</label>
|
||||
<Input
|
||||
placeholder="직책 검색"
|
||||
value={searchFilter.search_positionName || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_positionName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">사용자 ID</label>
|
||||
<Input
|
||||
placeholder="사용자 ID 검색"
|
||||
value={searchFilter.search_userId || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_userId", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">사용자명</label>
|
||||
<Input
|
||||
placeholder="사용자명 검색"
|
||||
value={searchFilter.search_userName || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_userName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">전화번호</label>
|
||||
<Input
|
||||
placeholder="전화번호/휴대폰 검색"
|
||||
value={searchFilter.search_tel || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_tel", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-muted-foreground mb-1 block text-xs font-medium">이메일</label>
|
||||
<Input
|
||||
placeholder="이메일 검색"
|
||||
value={searchFilter.search_email || ""}
|
||||
onChange={(e) => handleAdvancedSearchChange("search_email", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 고급 검색 초기화 버튼 */}
|
||||
{isAdvancedSearchMode && (
|
||||
<div className="mt-4 border-t pt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
onSearchChange({
|
||||
search_sabun: undefined,
|
||||
search_companyName: undefined,
|
||||
search_deptName: undefined,
|
||||
search_positionName: undefined,
|
||||
search_userId: undefined,
|
||||
search_userName: undefined,
|
||||
search_tel: undefined,
|
||||
search_email: undefined,
|
||||
})
|
||||
}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
고급 검색 조건 초기화
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 영역 */}
|
||||
|
|
|
|||
|
|
@ -15,20 +15,85 @@ export const useUserManagement = () => {
|
|||
// 검색 필터 상태
|
||||
const [searchFilter, setSearchFilter] = useState<UserSearchFilter>({});
|
||||
|
||||
// 검색어만 디바운싱 (500ms 지연) - searchType은 즉시 반영
|
||||
// 통합 검색어 디바운싱 (500ms 지연)
|
||||
const debouncedSearchValue = useDebounce(searchFilter.searchValue || "", 500);
|
||||
|
||||
// 고급 검색 필드들 디바운싱
|
||||
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);
|
||||
|
||||
// 디바운싱된 검색 필터 (useMemo로 최적화)
|
||||
const debouncedSearchFilter = useMemo(
|
||||
() => ({
|
||||
// 통합 검색
|
||||
searchValue: debouncedSearchValue,
|
||||
|
||||
// 고급 검색
|
||||
search_sabun: debouncedSabun,
|
||||
search_companyName: debouncedCompanyName,
|
||||
search_deptName: debouncedDeptName,
|
||||
search_positionName: debouncedPositionName,
|
||||
search_userId: debouncedUserId,
|
||||
search_userName: debouncedUserName,
|
||||
search_tel: debouncedTel,
|
||||
search_email: debouncedEmail,
|
||||
|
||||
// 하위 호환성
|
||||
searchType: searchFilter.searchType || "all",
|
||||
}),
|
||||
[debouncedSearchValue, searchFilter.searchType],
|
||||
[
|
||||
debouncedSearchValue,
|
||||
debouncedSabun,
|
||||
debouncedCompanyName,
|
||||
debouncedDeptName,
|
||||
debouncedPositionName,
|
||||
debouncedUserId,
|
||||
debouncedUserName,
|
||||
debouncedTel,
|
||||
debouncedEmail,
|
||||
searchFilter.searchType,
|
||||
],
|
||||
);
|
||||
|
||||
// 검색 중인지 확인 (검색어만 체크)
|
||||
const isSearching = (searchFilter.searchValue || "") !== debouncedSearchValue;
|
||||
// 검색 중인지 확인 (모든 검색 필드를 고려)
|
||||
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,
|
||||
]);
|
||||
|
||||
// 로딩 및 에러 상태
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -39,52 +104,55 @@ export const useUserManagement = () => {
|
|||
const [pageSize, setPageSize] = useState(20);
|
||||
const [totalItems, setTotalItems] = useState(0);
|
||||
|
||||
// 사용자 목록 로드 (특정 검색 조건으로 호출)
|
||||
// 사용자 목록 로드 (새로운 통합 검색 방식)
|
||||
const loadUsers = useCallback(
|
||||
async (searchValue?: string, searchType?: string) => {
|
||||
async (filter?: UserSearchFilter) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// 백엔드 API 호출
|
||||
// 검색 파라미터 구성 (단순 검색 방식)
|
||||
// 검색 파라미터 구성
|
||||
const searchParams: Record<string, string | number | undefined> = {
|
||||
page: currentPage,
|
||||
countPerPage: pageSize,
|
||||
};
|
||||
|
||||
// 검색어가 있고 searchType이 'all'이 아닐 때만 검색 파라미터 추가
|
||||
if (searchValue && searchValue.trim() && searchType && searchType !== "all") {
|
||||
const trimmedValue = searchValue.trim();
|
||||
// 검색 조건 추가
|
||||
if (filter) {
|
||||
// 통합 검색 (우선순위 최고)
|
||||
if (filter.searchValue && filter.searchValue.trim()) {
|
||||
searchParams.search = filter.searchValue.trim();
|
||||
}
|
||||
|
||||
// 각 검색 타입별로 해당하는 백엔드 파라미터 매핑 (백엔드 MyBatis와 정확히 일치)
|
||||
switch (searchType) {
|
||||
case "sabun":
|
||||
searchParams.search_sabun = trimmedValue;
|
||||
break;
|
||||
case "company_name":
|
||||
searchParams.search_companyName = trimmedValue; // MyBatis: search_companyName
|
||||
break;
|
||||
case "dept_name":
|
||||
searchParams.search_deptName = trimmedValue; // MyBatis: search_deptName
|
||||
break;
|
||||
case "position_name":
|
||||
searchParams.search_positionName = trimmedValue; // MyBatis: search_positionName
|
||||
break;
|
||||
case "user_id":
|
||||
searchParams.search_userId = trimmedValue; // MyBatis: search_userId
|
||||
break;
|
||||
case "user_name":
|
||||
searchParams.search_userName = trimmedValue; // MyBatis: search_userName
|
||||
break;
|
||||
case "tel":
|
||||
searchParams.search_tel = trimmedValue; // MyBatis: search_tel
|
||||
break;
|
||||
case "email":
|
||||
searchParams.search_email = trimmedValue; // MyBatis: search_email
|
||||
break;
|
||||
default:
|
||||
searchParams.search_userName = trimmedValue; // 기본값
|
||||
// 고급 검색 (개별 필드별)
|
||||
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) {
|
||||
// 기존 방식 변환은 일단 제거 (통합 검색과 고급 검색만 지원)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,30 +193,41 @@ export const useUserManagement = () => {
|
|||
loadUsers();
|
||||
}, [loadUsers]);
|
||||
|
||||
// 검색어 변경 시에만 API 호출 (검색어가 있고 'all'이 아닐 때)
|
||||
// 디바운싱된 검색 조건이 변경될 때마다 API 호출
|
||||
useEffect(() => {
|
||||
if (
|
||||
debouncedSearchFilter.searchValue &&
|
||||
debouncedSearchFilter.searchValue.trim() &&
|
||||
debouncedSearchFilter.searchType !== "all"
|
||||
) {
|
||||
loadUsers(debouncedSearchFilter.searchValue, debouncedSearchFilter.searchType);
|
||||
}
|
||||
}, [debouncedSearchFilter.searchValue, loadUsers]);
|
||||
|
||||
// '전체' 선택 시에만 즉시 반영
|
||||
useEffect(() => {
|
||||
if (searchFilter.searchType === "all" && !searchFilter.searchValue) {
|
||||
loadUsers(); // 전체 목록 로드 (검색 조건 없음)
|
||||
}
|
||||
}, [searchFilter.searchType, loadUsers]);
|
||||
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,
|
||||
]);
|
||||
|
||||
// 검색 필터 업데이트
|
||||
const updateSearchFilter = useCallback((newFilter: Partial<UserSearchFilter>) => {
|
||||
setSearchFilter((prev) => ({ ...prev, ...newFilter }));
|
||||
|
||||
// searchType이 변경되거나 searchValue가 변경될 때만 첫 페이지로 이동
|
||||
if (newFilter.searchType !== undefined || newFilter.searchValue !== undefined) {
|
||||
// 검색 조건이 변경될 때마다 첫 페이지로 이동
|
||||
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) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ interface ApiResponse<T> {
|
|||
message?: string;
|
||||
errorCode?: string;
|
||||
total?: number;
|
||||
searchType?: "unified" | "single" | "advanced" | "none"; // 검색 타입 정보
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
// 백엔드 호환성을 위한 추가 필드
|
||||
result?: boolean;
|
||||
msg?: string;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,21 @@ export interface User {
|
|||
|
||||
// 사용자 검색 필터
|
||||
export interface UserSearchFilter {
|
||||
searchType?: "all" | "sabun" | "companyCode" | "deptName" | "positionName" | "userId" | "userName" | "tel" | "email"; // 검색 대상
|
||||
searchValue?: string; // 검색어
|
||||
// 통합 검색 (우선순위: 가장 높음)
|
||||
searchValue?: string; // 통합 검색어 (모든 필드 대상)
|
||||
|
||||
// 단일 필드 검색 (중간 우선순위)
|
||||
searchType?: "all" | "sabun" | "companyCode" | "deptName" | "positionName" | "userId" | "userName" | "tel" | "email"; // 검색 대상 (하위 호환성)
|
||||
|
||||
// 고급 검색 (개별 필드별 AND 조건)
|
||||
search_sabun?: string; // 사번 검색
|
||||
search_companyName?: string; // 회사명 검색
|
||||
search_deptName?: string; // 부서명 검색
|
||||
search_positionName?: string; // 직책 검색
|
||||
search_userId?: string; // 사용자 ID 검색
|
||||
search_userName?: string; // 사용자명 검색
|
||||
search_tel?: string; // 전화번호 검색 (TEL + CELL_PHONE)
|
||||
search_email?: string; // 이메일 검색
|
||||
}
|
||||
|
||||
// 사용자 목록 테이블 컬럼 정의
|
||||
|
|
|
|||
Loading…
Reference in New Issue