일정 페이지 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>
)}
</AnimatePresence>
{/* 검색 모드가 아닐 때만 개수 표시 */}
{!isSearchMode && (
<span className="text-sm text-gray-500">{filteredSchedules.length} 일정</span>
)}
<AnimatePresence>
{!isSearchMode && (
<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 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 ? (
<div className="text-center py-20 text-gray-500">로딩 ...</div>
) : filteredSchedules.length > 0 ? (
filteredSchedules.map((schedule, index) => {
const formatted = formatDate(schedule.date);
const categoryColor = getCategoryColor(schedule.category_id);
@ -704,10 +722,11 @@ function Schedule() {
return (
<motion.div
key={schedule.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
key={`${schedule.id}-${selectedDate || 'all'}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: Math.min(index, 10) * 0.03 }}
onClick={() => handleScheduleClick(schedule)}
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 ? '선택한 날짜에 일정이 없습니다.' : '예정된 일정이 없습니다.'}
</div>
)}
</div>
</motion.div>
</div>
</div>
</div>
</div>

View file

@ -19,17 +19,29 @@ function AdminSchedule() {
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 [user, setUser] = useState(null);
const [toast, setToast] = useState(null);
const [searchInput, setSearchInput] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [isSearchMode, setIsSearchMode] = useState(false);
const [searchInput, setSearchInput] = useState(storedState?.searchInput || '');
const [searchTerm, setSearchTerm] = useState(storedState?.searchTerm || '');
const [isSearchMode, setIsSearchMode] = useState(storedState?.isSearchMode || false);
const [searchResults, setSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const [selectedCategories, setSelectedCategories] = useState([]);
const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedCategories, setSelectedCategories] = useState(storedState?.selectedCategories || []);
const [selectedDate, setSelectedDate] = useState(storedState?.selectedDate || getTodayKST());
const [currentDate, setCurrentDate] = useState(
storedState?.currentDate ? new Date(storedState.currentDate) : new Date()
);
const [slideDirection, setSlideDirection] = useState(0);
@ -157,6 +169,27 @@ function AdminSchedule() {
fetchSchedules();
}, [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 () => {
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"
>
<Tag size={14} />
<span>{selectedCategories.length}</span>
<span>{selectedCategories.length} 일정</span>
</button>
<AnimatePresence>
{showCategoryTooltip && (
@ -911,7 +944,7 @@ function AdminSchedule() {
</AnimatePresence>
</div>
)}
<span className="text-sm text-gray-400">{filteredSchedules.length}</span>
<span className="text-sm text-gray-400">{filteredSchedules.length} 일정</span>
</motion.div>
)}
</AnimatePresence>
@ -928,14 +961,16 @@ function AdminSchedule() {
<p>등록된 일정이 없습니다</p>
</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) => (
<motion.div
key={schedule.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
key={`${schedule.id}-${selectedDate || 'all'}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: Math.min(index, 10) * 0.03 }}
className="p-6 hover:bg-gray-50 transition-colors group"
>
<div className="flex items-start gap-4">