103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
|
|
import { Search, Plus } 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";
|
||
|
|
|
||
|
|
interface UserToolbarProps {
|
||
|
|
searchFilter: UserSearchFilter;
|
||
|
|
totalCount: number;
|
||
|
|
isSearching?: boolean;
|
||
|
|
onSearchChange: (searchFilter: Partial<UserSearchFilter>) => void;
|
||
|
|
onCreateClick: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 관리 툴바 컴포넌트
|
||
|
|
* 검색, 필터링, 액션 버튼들을 포함
|
||
|
|
*/
|
||
|
|
export function UserToolbar({
|
||
|
|
searchFilter,
|
||
|
|
totalCount,
|
||
|
|
isSearching = false,
|
||
|
|
onSearchChange,
|
||
|
|
onCreateClick,
|
||
|
|
}: UserToolbarProps) {
|
||
|
|
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: "", // 옵션 변경 시 항상 검색어 초기화
|
||
|
|
})
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="w-[140px]">
|
||
|
|
<SelectValue placeholder="전체" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{SEARCH_OPTIONS.map((option) => (
|
||
|
|
<SelectItem key={option.value} value={option.value}>
|
||
|
|
{option.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</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" : ""
|
||
|
|
}`}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 액션 버튼 영역 */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
{/* 조회 결과 정보 */}
|
||
|
|
<div className="text-muted-foreground text-sm">
|
||
|
|
총 <span className="text-foreground font-medium">{totalCount}</span> 명
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 액션 버튼들 */}
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Button onClick={onCreateClick} className="gap-2">
|
||
|
|
<Plus className="h-4 w-4" />
|
||
|
|
사용자 등록
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|