feat(mobile/schedule): 검색 UX 개선 및 데이트픽커 스타일 수정

- 데이트픽커 년/월 선택에서 오늘 날짜 초록색 강조
- 검색어 변경 시 스크롤 초기화 개선 (requestAnimationFrame)
- 유튜브 스타일 검색 UX 구현
  - X 버튼 클릭 시 추천 검색어 화면으로 전환
  - 뒤로가기 시 검색 결과 복원 및 검색어 유지

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-13 23:06:08 +09:00
parent 02fe9314e4
commit 2de5cb8f93

View file

@ -33,6 +33,8 @@ function MobileSchedule() {
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); //
const [suggestions, setSuggestions] = useState([]); //
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [lastSearchTerm, setLastSearchTerm] = useState(''); // ()
const [showSuggestionsScreen, setShowSuggestionsScreen] = useState(false); //
// (history )
const enterSearchMode = () => {
@ -46,22 +48,37 @@ function MobileSchedule() {
setSearchInput('');
setOriginalSearchQuery('');
setSearchTerm('');
setLastSearchTerm('');
setShowSuggestions(false);
setShowSuggestionsScreen(false);
setSelectedSuggestionIndex(-1);
};
//
const hideSuggestionsScreen = () => {
setShowSuggestionsScreen(false);
setSearchInput(lastSearchTerm); //
setOriginalSearchQuery(lastSearchTerm);
};
//
useEffect(() => {
const handlePopState = (e) => {
if (isSearchMode) {
//
exitSearchMode();
//
if (showSuggestionsScreen && searchTerm) {
hideSuggestionsScreen();
window.history.pushState({ searchMode: true }, '');
} else {
//
exitSearchMode();
}
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [isSearchMode]);
}, [isSearchMode, showSuggestionsScreen, searchTerm, lastSearchTerm]);
//
const changeCalendarMonth = (delta) => {
@ -111,15 +128,16 @@ function MobileSchedule() {
//
useEffect(() => {
if (searchTerm) {
// virtualizer
virtualizer.scrollToOffset(0);
// DOM (fallback)
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0;
}
if (searchTerm && !showSuggestionsScreen) {
// ( )
requestAnimationFrame(() => {
virtualizer.scrollToOffset(0);
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0;
}
});
}
}, [searchTerm]);
}, [searchTerm, showSuggestionsScreen]);
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) {
@ -326,9 +344,13 @@ function MobileSchedule() {
setSearchInput(e.target.value);
setOriginalSearchQuery(e.target.value);
setShowSuggestions(true);
setShowSuggestionsScreen(true);
setSelectedSuggestionIndex(-1);
}}
onFocus={() => setShowSuggestions(true)}
onFocus={() => {
setShowSuggestions(true);
setShowSuggestionsScreen(true);
}}
onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
@ -350,11 +372,14 @@ function MobileSchedule() {
}
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedSuggestionIndex >= 0 && suggestions[selectedSuggestionIndex]) {
setSearchInput(suggestions[selectedSuggestionIndex]);
setSearchTerm(suggestions[selectedSuggestionIndex]);
} else if (searchInput.trim()) {
setSearchTerm(searchInput);
const term = selectedSuggestionIndex >= 0 && suggestions[selectedSuggestionIndex]
? suggestions[selectedSuggestionIndex]
: searchInput.trim();
if (term) {
setSearchInput(term);
setSearchTerm(term);
setLastSearchTerm(term); //
setShowSuggestionsScreen(false);
}
setShowSuggestions(false);
setSelectedSuggestionIndex(-1);
@ -370,9 +395,10 @@ function MobileSchedule() {
onClick={() => {
setSearchInput('');
setOriginalSearchQuery('');
setSearchTerm('');
setShowSuggestions(false);
setShowSuggestions(true);
setShowSuggestionsScreen(true);
setSelectedSuggestionIndex(-1);
// searchTerm lastSearchTerm
}}
className="flex-shrink-0"
>
@ -591,12 +617,12 @@ function MobileSchedule() {
</AnimatePresence>
{/* 컨텐츠 영역 */}
<div className="mobile-content" ref={isSearchMode && searchTerm ? scrollContainerRef : contentRef}>
<div className={`px-4 pb-4 ${isSearchMode && !searchTerm ? 'pt-0' : 'pt-4'}`}>
<div className="mobile-content" ref={isSearchMode && searchTerm && !showSuggestionsScreen ? scrollContainerRef : contentRef}>
<div className={`px-4 pb-4 ${isSearchMode && showSuggestionsScreen ? 'pt-0' : 'pt-4'}`}>
{isSearchMode ? (
//
!searchTerm ? (
// - ( )
showSuggestionsScreen ? (
// ( )
<div className="space-y-0">
{suggestions.length === 0 ? (
<div className="text-center py-8 text-gray-400">
@ -609,7 +635,9 @@ function MobileSchedule() {
onClick={() => {
setSearchInput(suggestion);
setSearchTerm(suggestion);
setLastSearchTerm(suggestion); //
setShowSuggestions(false);
setShowSuggestionsScreen(false);
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 ${
@ -624,6 +652,11 @@ function MobileSchedule() {
))
)}
</div>
) : !searchTerm ? (
// ( )
<div className="text-center py-8 text-gray-400">
검색어를 입력하세요
</div>
) : searchLoading ? (
<div className="flex justify-center py-8">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
@ -1162,45 +1195,56 @@ function CalendarPicker({
{/* 년도 선택 */}
<div className="text-center text-xs text-gray-400 mb-2">년도</div>
<div className="grid grid-cols-4 gap-2 mb-4">
{yearRange.map(y => (
<button
key={y}
onClick={() => {
const newDate = new Date(viewDate);
newDate.setFullYear(y);
setViewDate(newDate);
}}
className={`py-2 text-sm rounded-lg transition-colors ${
y === year
? 'bg-primary text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{y}
</button>
))}
{yearRange.map(y => {
const isCurrentYear = y === new Date().getFullYear();
return (
<button
key={y}
onClick={() => {
const newDate = new Date(viewDate);
newDate.setFullYear(y);
setViewDate(newDate);
}}
className={`py-2 text-sm rounded-lg transition-colors ${
y === year
? 'bg-primary text-white'
: isCurrentYear
? 'text-primary font-semibold hover:bg-gray-100'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{y}
</button>
);
})}
</div>
{/* 월 선택 */}
<div className="text-center text-xs text-gray-400 mb-2"></div>
<div className="grid grid-cols-4 gap-2">
{Array.from({ length: 12 }, (_, i) => i + 1).map(m => (
<button
key={m}
onClick={() => {
const newDate = new Date(year, m - 1, 1);
setViewDate(newDate);
setShowYearMonth(false);
}}
className={`py-2 text-sm rounded-lg transition-colors ${
m === month + 1
? 'bg-primary text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{m}
</button>
))}
{Array.from({ length: 12 }, (_, i) => i + 1).map(m => {
const today = new Date();
const isCurrentMonth = year === today.getFullYear() && m === today.getMonth() + 1;
return (
<button
key={m}
onClick={() => {
const newDate = new Date(year, m - 1, 1);
setViewDate(newDate);
setShowYearMonth(false);
}}
className={`py-2 text-sm rounded-lg transition-colors ${
m === month + 1
? 'bg-primary text-white'
: isCurrentMonth
? 'text-primary font-semibold hover:bg-gray-100'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{m}
</button>
);
})}
</div>
</motion.div>
) : (