feat(AdminSchedule): 검색어 추천 드롭다운 추가
- 검색 버튼을 초록색 돋보기 아이콘으로 변경 - 애니메이션 scale+opacity로 개선 - 검색 모드에서 빈 일정 메시지 숨김 - 추천 검색어 드롭다운 (유튜브 스타일) - 방향키 선택, 외부 클릭 닫기 지원 - 검색 모드 진입 시 기존 카테고리 유지
This commit is contained in:
parent
90f5a9a90a
commit
09706e42e3
1 changed files with 150 additions and 39 deletions
|
|
@ -152,6 +152,13 @@ function AdminSchedule() {
|
|||
const [user, setUser] = useState(null);
|
||||
const { toast, setToast } = useToast();
|
||||
const scrollContainerRef = useRef(null);
|
||||
const searchContainerRef = useRef(null); // 검색 컨테이너 (외부 클릭 감지용)
|
||||
|
||||
// 검색 추천 관련 상태
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
||||
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // 필터링용 원본 쿼리
|
||||
|
||||
const SEARCH_LIMIT = 20; // 페이지당 20개
|
||||
const ESTIMATED_ITEM_HEIGHT = 100; // 아이템 추정 높이 (동적 측정)
|
||||
|
||||
|
|
@ -402,6 +409,11 @@ function AdminSchedule() {
|
|||
if (categoryTooltipRef.current && !categoryTooltipRef.current.contains(event.target)) {
|
||||
setShowCategoryTooltip(false);
|
||||
}
|
||||
// 검색 추천 드롭다운 외부 클릭 시 닫기
|
||||
if (searchContainerRef.current && !searchContainerRef.current.contains(event.target)) {
|
||||
setShowSuggestions(false);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
if (showYearMonthPicker || showCategoryTooltip) {
|
||||
|
|
@ -951,35 +963,81 @@ function AdminSchedule() {
|
|||
/* 검색 모드 */
|
||||
<motion.div
|
||||
key="search-mode"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -10 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.15, ease: 'easeOut' }}
|
||||
className="flex items-center gap-3 flex-1"
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsSearchMode(false);
|
||||
setSearchInput('');
|
||||
setOriginalSearchQuery('');
|
||||
setSearchTerm('');
|
||||
setShowSuggestions(false);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} className="text-gray-500" />
|
||||
</button>
|
||||
|
||||
{/* 검색 입력 컨테이너 (드롭다운 포함) */}
|
||||
<div className="flex-1 relative" ref={searchContainerRef}>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="일정 검색..."
|
||||
value={searchInput}
|
||||
autoFocus
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setSearchInput(e.target.value);
|
||||
setOriginalSearchQuery(e.target.value);
|
||||
setShowSuggestions(true);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}}
|
||||
onFocus={() => setShowSuggestions(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// 필터링은 원본 쿼리 기준으로 유지
|
||||
const dummySuggestions = ['성수기', '성수기 이채영', '이채영 먹방', 'NOW TOMORROW', '하얀 그리움', '콘서트', '월드투어'].filter(s =>
|
||||
s.toLowerCase().includes(originalSearchQuery.toLowerCase())
|
||||
).slice(0, 7);
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const newIndex = selectedSuggestionIndex < dummySuggestions.length - 1
|
||||
? selectedSuggestionIndex + 1
|
||||
: 0;
|
||||
setSelectedSuggestionIndex(newIndex);
|
||||
if (dummySuggestions[newIndex]) {
|
||||
setSearchInput(dummySuggestions[newIndex]);
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const newIndex = selectedSuggestionIndex > 0
|
||||
? selectedSuggestionIndex - 1
|
||||
: dummySuggestions.length - 1;
|
||||
setSelectedSuggestionIndex(newIndex);
|
||||
if (dummySuggestions[newIndex]) {
|
||||
setSearchInput(dummySuggestions[newIndex]);
|
||||
}
|
||||
} else if (e.key === 'Enter') {
|
||||
if (selectedSuggestionIndex >= 0 && dummySuggestions[selectedSuggestionIndex]) {
|
||||
setSearchInput(dummySuggestions[selectedSuggestionIndex]);
|
||||
setSearchTerm(dummySuggestions[selectedSuggestionIndex]);
|
||||
} else if (searchInput.trim()) {
|
||||
setSearchTerm(searchInput);
|
||||
}
|
||||
setShowSuggestions(false);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsSearchMode(false);
|
||||
setSearchInput('');
|
||||
setOriginalSearchQuery('');
|
||||
setSearchTerm('');
|
||||
setShowSuggestions(false);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
setSearchResults([]);
|
||||
}
|
||||
}}
|
||||
|
|
@ -987,22 +1045,67 @@ function AdminSchedule() {
|
|||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (searchInput.trim()) {
|
||||
setSearchTerm(searchInput);
|
||||
setShowSuggestions(false);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}
|
||||
}}
|
||||
disabled={searchLoading}
|
||||
className="px-4 py-1.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{searchLoading ? '...' : '검색'}
|
||||
<Search size={20} className="text-primary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 검색어 추천 드롭다운 */}
|
||||
{showSuggestions && originalSearchQuery.length > 0 && (
|
||||
<div className="absolute top-full left-0 right-8 mt-2 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-50 overflow-hidden">
|
||||
{(() => {
|
||||
const dummySuggestions = ['성수기', '성수기 이채영', '이채영 먹방', 'NOW TOMORROW', '하얀 그리움', '콘서트', '월드투어'].filter(s =>
|
||||
s.toLowerCase().includes(originalSearchQuery.toLowerCase())
|
||||
).slice(0, 7);
|
||||
|
||||
if (dummySuggestions.length === 0) {
|
||||
return (
|
||||
<div className="px-4 py-3 text-sm text-gray-400 text-center">
|
||||
추천 검색어가 없습니다
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return dummySuggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
onClick={() => {
|
||||
setSearchInput(suggestion);
|
||||
setSearchTerm(suggestion);
|
||||
setShowSuggestions(false);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}}
|
||||
className={`w-full px-4 py-2 text-left text-sm flex items-center gap-3 transition-colors ${
|
||||
index === selectedSuggestionIndex
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Search size={14} className="text-gray-400 shrink-0" />
|
||||
<span>{suggestion}</span>
|
||||
</button>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
/* 일반 모드 */
|
||||
<motion.div
|
||||
key="normal-mode"
|
||||
initial={{ opacity: 0, x: 10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.15, ease: 'easeOut' }}
|
||||
className="flex items-center gap-3 flex-1"
|
||||
>
|
||||
<button
|
||||
|
|
@ -1071,10 +1174,18 @@ function AdminSchedule() {
|
|||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
|
||||
</div>
|
||||
) : filteredSchedules.length === 0 ? (
|
||||
<div className="text-center py-16 text-gray-500">
|
||||
// 검색 모드에서는 빈 메시지 표시 안 함
|
||||
!isSearchMode && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-center py-16 text-gray-500"
|
||||
>
|
||||
<Calendar size={48} className="mx-auto mb-4 text-gray-300" />
|
||||
<p>등록된 일정이 없습니다</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue