feat(mobile/schedule): 검색 UX 개선 및 데이트픽커 스타일 수정
- 데이트픽커 년/월 선택에서 오늘 날짜 초록색 강조 - 검색어 변경 시 스크롤 초기화 개선 (requestAnimationFrame) - 유튜브 스타일 검색 UX 구현 - X 버튼 클릭 시 추천 검색어 화면으로 전환 - 뒤로가기 시 검색 결과 복원 및 검색어 유지 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
02fe9314e4
commit
2de5cb8f93
1 changed files with 111 additions and 67 deletions
|
|
@ -33,6 +33,8 @@ function MobileSchedule() {
|
||||||
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // 필터링용 원본 쿼리
|
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // 필터링용 원본 쿼리
|
||||||
const [suggestions, setSuggestions] = useState([]); // 추천 검색어 목록
|
const [suggestions, setSuggestions] = useState([]); // 추천 검색어 목록
|
||||||
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
||||||
|
const [lastSearchTerm, setLastSearchTerm] = useState(''); // 마지막 검색어 (복원용)
|
||||||
|
const [showSuggestionsScreen, setShowSuggestionsScreen] = useState(false); // 추천 검색어 화면 표시 여부
|
||||||
|
|
||||||
// 검색 모드 진입 함수 (history 상태 추가)
|
// 검색 모드 진입 함수 (history 상태 추가)
|
||||||
const enterSearchMode = () => {
|
const enterSearchMode = () => {
|
||||||
|
|
@ -46,22 +48,37 @@ function MobileSchedule() {
|
||||||
setSearchInput('');
|
setSearchInput('');
|
||||||
setOriginalSearchQuery('');
|
setOriginalSearchQuery('');
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
|
setLastSearchTerm('');
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
|
setShowSuggestionsScreen(false);
|
||||||
setSelectedSuggestionIndex(-1);
|
setSelectedSuggestionIndex(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 추천 검색어 화면 숨기고 검색 결과로 돌아가기
|
||||||
|
const hideSuggestionsScreen = () => {
|
||||||
|
setShowSuggestionsScreen(false);
|
||||||
|
setSearchInput(lastSearchTerm); // 검색어 복원
|
||||||
|
setOriginalSearchQuery(lastSearchTerm);
|
||||||
|
};
|
||||||
|
|
||||||
// 뒤로가기 버튼 처리
|
// 뒤로가기 버튼 처리
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handlePopState = (e) => {
|
const handlePopState = (e) => {
|
||||||
if (isSearchMode) {
|
if (isSearchMode) {
|
||||||
// 검색 모드에서 뒤로가기 시 검색 모드 종료
|
// 추천 검색어 화면이고 검색 결과가 있으면 → 검색 결과로 돌아가기
|
||||||
|
if (showSuggestionsScreen && searchTerm) {
|
||||||
|
hideSuggestionsScreen();
|
||||||
|
window.history.pushState({ searchMode: true }, '');
|
||||||
|
} else {
|
||||||
|
// 그 외에는 검색 모드 종료
|
||||||
exitSearchMode();
|
exitSearchMode();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('popstate', handlePopState);
|
window.addEventListener('popstate', handlePopState);
|
||||||
return () => window.removeEventListener('popstate', handlePopState);
|
return () => window.removeEventListener('popstate', handlePopState);
|
||||||
}, [isSearchMode]);
|
}, [isSearchMode, showSuggestionsScreen, searchTerm, lastSearchTerm]);
|
||||||
|
|
||||||
// 달력 월 변경 함수
|
// 달력 월 변경 함수
|
||||||
const changeCalendarMonth = (delta) => {
|
const changeCalendarMonth = (delta) => {
|
||||||
|
|
@ -111,15 +128,16 @@ function MobileSchedule() {
|
||||||
|
|
||||||
// 검색어 변경 시 스크롤 위치 초기화
|
// 검색어 변경 시 스크롤 위치 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchTerm) {
|
if (searchTerm && !showSuggestionsScreen) {
|
||||||
// virtualizer 스크롤 초기화
|
// 약간의 지연 후 스크롤 초기화 (렌더링 완료 후)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
virtualizer.scrollToOffset(0);
|
virtualizer.scrollToOffset(0);
|
||||||
// DOM 스크롤도 초기화 (fallback)
|
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
scrollContainerRef.current.scrollTop = 0;
|
scrollContainerRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [searchTerm]);
|
}, [searchTerm, showSuggestionsScreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inView && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) {
|
if (inView && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) {
|
||||||
|
|
@ -326,9 +344,13 @@ function MobileSchedule() {
|
||||||
setSearchInput(e.target.value);
|
setSearchInput(e.target.value);
|
||||||
setOriginalSearchQuery(e.target.value);
|
setOriginalSearchQuery(e.target.value);
|
||||||
setShowSuggestions(true);
|
setShowSuggestions(true);
|
||||||
|
setShowSuggestionsScreen(true);
|
||||||
setSelectedSuggestionIndex(-1);
|
setSelectedSuggestionIndex(-1);
|
||||||
}}
|
}}
|
||||||
onFocus={() => setShowSuggestions(true)}
|
onFocus={() => {
|
||||||
|
setShowSuggestions(true);
|
||||||
|
setShowSuggestionsScreen(true);
|
||||||
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'ArrowDown') {
|
if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -350,11 +372,14 @@ function MobileSchedule() {
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (selectedSuggestionIndex >= 0 && suggestions[selectedSuggestionIndex]) {
|
const term = selectedSuggestionIndex >= 0 && suggestions[selectedSuggestionIndex]
|
||||||
setSearchInput(suggestions[selectedSuggestionIndex]);
|
? suggestions[selectedSuggestionIndex]
|
||||||
setSearchTerm(suggestions[selectedSuggestionIndex]);
|
: searchInput.trim();
|
||||||
} else if (searchInput.trim()) {
|
if (term) {
|
||||||
setSearchTerm(searchInput);
|
setSearchInput(term);
|
||||||
|
setSearchTerm(term);
|
||||||
|
setLastSearchTerm(term); // 검색어 저장
|
||||||
|
setShowSuggestionsScreen(false);
|
||||||
}
|
}
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
setSelectedSuggestionIndex(-1);
|
setSelectedSuggestionIndex(-1);
|
||||||
|
|
@ -370,9 +395,10 @@ function MobileSchedule() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchInput('');
|
setSearchInput('');
|
||||||
setOriginalSearchQuery('');
|
setOriginalSearchQuery('');
|
||||||
setSearchTerm('');
|
setShowSuggestions(true);
|
||||||
setShowSuggestions(false);
|
setShowSuggestionsScreen(true);
|
||||||
setSelectedSuggestionIndex(-1);
|
setSelectedSuggestionIndex(-1);
|
||||||
|
// searchTerm과 lastSearchTerm은 유지하여 뒤로가기 시 복원 가능
|
||||||
}}
|
}}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
>
|
>
|
||||||
|
|
@ -591,12 +617,12 @@ function MobileSchedule() {
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* 컨텐츠 영역 */}
|
{/* 컨텐츠 영역 */}
|
||||||
<div className="mobile-content" ref={isSearchMode && searchTerm ? scrollContainerRef : contentRef}>
|
<div className="mobile-content" ref={isSearchMode && searchTerm && !showSuggestionsScreen ? scrollContainerRef : contentRef}>
|
||||||
<div className={`px-4 pb-4 ${isSearchMode && !searchTerm ? 'pt-0' : 'pt-4'}`}>
|
<div className={`px-4 pb-4 ${isSearchMode && showSuggestionsScreen ? 'pt-0' : 'pt-4'}`}>
|
||||||
{isSearchMode ? (
|
{isSearchMode ? (
|
||||||
// 검색 모드
|
// 검색 모드
|
||||||
!searchTerm ? (
|
showSuggestionsScreen ? (
|
||||||
// 검색어 입력 전 - 추천 검색어 리스트 표시 (유튜브 스타일)
|
// 추천 검색어 화면 (유튜브 스타일)
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{suggestions.length === 0 ? (
|
{suggestions.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-400">
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
|
@ -609,7 +635,9 @@ function MobileSchedule() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchInput(suggestion);
|
setSearchInput(suggestion);
|
||||||
setSearchTerm(suggestion);
|
setSearchTerm(suggestion);
|
||||||
|
setLastSearchTerm(suggestion); // 검색어 저장
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
|
setShowSuggestionsScreen(false);
|
||||||
setSelectedSuggestionIndex(-1);
|
setSelectedSuggestionIndex(-1);
|
||||||
}}
|
}}
|
||||||
className={`w-full px-0 py-3.5 text-left flex items-center gap-4 border-b border-gray-100 active:bg-gray-50 ${
|
className={`w-full px-0 py-3.5 text-left flex items-center gap-4 border-b border-gray-100 active:bg-gray-50 ${
|
||||||
|
|
@ -624,6 +652,11 @@ function MobileSchedule() {
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : !searchTerm ? (
|
||||||
|
// 검색어 없음 (첫 진입)
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
검색어를 입력하세요
|
||||||
|
</div>
|
||||||
) : searchLoading ? (
|
) : searchLoading ? (
|
||||||
<div className="flex justify-center py-8">
|
<div className="flex justify-center py-8">
|
||||||
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
|
@ -1162,7 +1195,9 @@ function CalendarPicker({
|
||||||
{/* 년도 선택 */}
|
{/* 년도 선택 */}
|
||||||
<div className="text-center text-xs text-gray-400 mb-2">년도</div>
|
<div className="text-center text-xs text-gray-400 mb-2">년도</div>
|
||||||
<div className="grid grid-cols-4 gap-2 mb-4">
|
<div className="grid grid-cols-4 gap-2 mb-4">
|
||||||
{yearRange.map(y => (
|
{yearRange.map(y => {
|
||||||
|
const isCurrentYear = y === new Date().getFullYear();
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={y}
|
key={y}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -1173,18 +1208,24 @@ function CalendarPicker({
|
||||||
className={`py-2 text-sm rounded-lg transition-colors ${
|
className={`py-2 text-sm rounded-lg transition-colors ${
|
||||||
y === year
|
y === year
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-white'
|
||||||
|
: isCurrentYear
|
||||||
|
? 'text-primary font-semibold hover:bg-gray-100'
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{y}
|
{y}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 월 선택 */}
|
{/* 월 선택 */}
|
||||||
<div className="text-center text-xs text-gray-400 mb-2">월</div>
|
<div className="text-center text-xs text-gray-400 mb-2">월</div>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
{Array.from({ length: 12 }, (_, i) => i + 1).map(m => (
|
{Array.from({ length: 12 }, (_, i) => i + 1).map(m => {
|
||||||
|
const today = new Date();
|
||||||
|
const isCurrentMonth = year === today.getFullYear() && m === today.getMonth() + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={m}
|
key={m}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -1195,12 +1236,15 @@ function CalendarPicker({
|
||||||
className={`py-2 text-sm rounded-lg transition-colors ${
|
className={`py-2 text-sm rounded-lg transition-colors ${
|
||||||
m === month + 1
|
m === month + 1
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-white'
|
||||||
|
: isCurrentMonth
|
||||||
|
? 'text-primary font-semibold hover:bg-gray-100'
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{m}월
|
{m}월
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue