일정 페이지 UI/UX 개선

- 검색 모드 전환 시 일정 목록 fade 애니메이션 통일
- 일정 개수 텍스트 애니메이션 추가
- 관리자 일정 개수 표시 'N개 일정'으로 변경
- 일정 항목 애니메이션 y 이동 제거 (스크롤바 깜빡임 방지)
- 관리자 일정 페이지 상태 유지 (sessionStorage)
This commit is contained in:
caadiq 2026-01-06 09:50:29 +09:00
parent 2572ce2195
commit b6b212821e
2 changed files with 76 additions and 21 deletions

View file

@ -687,16 +687,34 @@ function Schedule() {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{/* 검색 모드가 아닐 때만 개수 표시 */} <AnimatePresence>
{!isSearchMode && ( {!isSearchMode && (
<span className="text-sm text-gray-500">{filteredSchedules.length} 일정</span> <motion.span
key="count"
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.15 }}
className="text-sm text-gray-500"
>
{filteredSchedules.length} 일정
</motion.span>
)} )}
</AnimatePresence>
</div> </div>
<div className="max-h-[calc(100vh-200px)] overflow-y-auto space-y-4 pr-2 py-2"> <motion.div
key={isSearchMode ? 'search' : 'normal'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
className="max-h-[calc(100vh-200px)] overflow-y-auto space-y-4 py-2 pr-2"
>
{loading ? ( {loading ? (
<div className="text-center py-20 text-gray-500">로딩 ...</div> <div className="text-center py-20 text-gray-500">로딩 ...</div>
) : filteredSchedules.length > 0 ? ( ) : filteredSchedules.length > 0 ? (
filteredSchedules.map((schedule, index) => { filteredSchedules.map((schedule, index) => {
const formatted = formatDate(schedule.date); const formatted = formatDate(schedule.date);
const categoryColor = getCategoryColor(schedule.category_id); const categoryColor = getCategoryColor(schedule.category_id);
@ -704,10 +722,11 @@ function Schedule() {
return ( return (
<motion.div <motion.div
key={schedule.id} key={`${schedule.id}-${selectedDate || 'all'}`}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1 }}
transition={{ delay: Math.min(index, 10) * 0.03 }} transition={{ delay: Math.min(index, 10) * 0.03 }}
onClick={() => handleScheduleClick(schedule)} onClick={() => handleScheduleClick(schedule)}
className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer" className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer"
> >
@ -788,8 +807,9 @@ function Schedule() {
{selectedDate ? '선택한 날짜에 일정이 없습니다.' : '예정된 일정이 없습니다.'} {selectedDate ? '선택한 날짜에 일정이 없습니다.' : '예정된 일정이 없습니다.'}
</div> </div>
)} )}
</motion.div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -19,17 +19,29 @@ function AdminSchedule() {
return kstDate.toISOString().split('T')[0]; return kstDate.toISOString().split('T')[0];
}; };
// sessionStorage
const getStoredState = () => {
try {
const stored = sessionStorage.getItem('adminScheduleState');
return stored ? JSON.parse(stored) : null;
} catch { return null; }
};
const storedState = getStoredState();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [toast, setToast] = useState(null); const [toast, setToast] = useState(null);
const [searchInput, setSearchInput] = useState(''); const [searchInput, setSearchInput] = useState(storedState?.searchInput || '');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState(storedState?.searchTerm || '');
const [isSearchMode, setIsSearchMode] = useState(false); const [isSearchMode, setIsSearchMode] = useState(storedState?.isSearchMode || false);
const [searchResults, setSearchResults] = useState([]); const [searchResults, setSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false);
const [selectedCategories, setSelectedCategories] = useState([]); const [selectedCategories, setSelectedCategories] = useState(storedState?.selectedCategories || []);
const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST const [selectedDate, setSelectedDate] = useState(storedState?.selectedDate || getTodayKST());
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(
storedState?.currentDate ? new Date(storedState.currentDate) : new Date()
);
const [slideDirection, setSlideDirection] = useState(0); const [slideDirection, setSlideDirection] = useState(0);
@ -157,6 +169,27 @@ function AdminSchedule() {
fetchSchedules(); fetchSchedules();
}, [year, month]); }, [year, month]);
// sessionStorage ( )
useEffect(() => {
const stateToSave = {
searchInput,
searchTerm,
isSearchMode,
selectedCategories,
selectedDate,
currentDate: currentDate.toISOString(),
};
sessionStorage.setItem('adminScheduleState', JSON.stringify(stateToSave));
}, [searchInput, searchTerm, isSearchMode, selectedCategories, selectedDate, currentDate]);
//
useEffect(() => {
if (isSearchMode && searchTerm) {
searchSchedules(searchTerm);
}
}, []); //
// //
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
@ -882,7 +915,7 @@ function AdminSchedule() {
className="flex items-center gap-1 px-2 py-1 bg-gray-100 rounded-md text-sm text-gray-600 hover:bg-gray-200 transition-colors" className="flex items-center gap-1 px-2 py-1 bg-gray-100 rounded-md text-sm text-gray-600 hover:bg-gray-200 transition-colors"
> >
<Tag size={14} /> <Tag size={14} />
<span>{selectedCategories.length}</span> <span>{selectedCategories.length} 일정</span>
</button> </button>
<AnimatePresence> <AnimatePresence>
{showCategoryTooltip && ( {showCategoryTooltip && (
@ -911,7 +944,7 @@ function AdminSchedule() {
</AnimatePresence> </AnimatePresence>
</div> </div>
)} )}
<span className="text-sm text-gray-400">{filteredSchedules.length}</span> <span className="text-sm text-gray-400">{filteredSchedules.length} 일정</span>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
@ -928,14 +961,16 @@ function AdminSchedule() {
<p>등록된 일정이 없습니다</p> <p>등록된 일정이 없습니다</p>
</div> </div>
) : ( ) : (
<div className="max-h-[calc(100vh-280px)] overflow-y-auto divide-y divide-gray-100 pr-2 py-2"> <div className="max-h-[calc(100vh-280px)] overflow-y-auto divide-y divide-gray-100 py-2">
{filteredSchedules.map((schedule, index) => ( {filteredSchedules.map((schedule, index) => (
<motion.div <motion.div
key={schedule.id} key={`${schedule.id}-${selectedDate || 'all'}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: Math.min(index, 10) * 0.03 }} transition={{ delay: Math.min(index, 10) * 0.03 }}
className="p-6 hover:bg-gray-50 transition-colors group" className="p-6 hover:bg-gray-50 transition-colors group"
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">