feat: 일정 관련 UI/UX 개선

- 카테고리 정렬: 일정 개수 기준 내림차순, 0개 숨김, 기타는 맨 아래 고정
- useMemo로 카테고리 정렬 메모이제이션 (깜빡임 방지)
- 일정 수정 시 이미지 삭제 버그 수정 (existingImageIds 업데이트)
- 이미지 파일명에서 Date.now() 제거 (01.webp 형식 유지)
- 이미지 삭제 후 sort_order 재정렬 로직 추가
- 날짜 선택 시 요일 표시 추가 (2026년 1월 7일 (수) 형식)
This commit is contained in:
caadiq 2026-01-06 14:16:29 +09:00
parent dac2234a0b
commit bb027df914
4 changed files with 1996 additions and 1606 deletions

View file

@ -1607,7 +1607,8 @@ router.put(
const file = req.files[i]; const file = req.files[i];
currentOrder++; currentOrder++;
const orderNum = String(currentOrder).padStart(2, "0"); const orderNum = String(currentOrder).padStart(2, "0");
const filename = `${orderNum}_${Date.now()}.webp`; // 파일명: 01.webp, 02.webp 형식 (Date.now() 제거)
const filename = `${orderNum}.webp`;
const imageBuffer = await sharp(file.buffer) const imageBuffer = await sharp(file.buffer)
.webp({ quality: 90 }) .webp({ quality: 90 })
@ -1631,6 +1632,18 @@ router.put(
} }
} }
// sort_order 재정렬 (삭제로 인한 간격 제거)
const [remainingImages] = await connection.query(
"SELECT id FROM schedule_images WHERE schedule_id = ? ORDER BY sort_order ASC",
[id]
);
for (let i = 0; i < remainingImages.length; i++) {
await connection.query(
"UPDATE schedule_images SET sort_order = ? WHERE id = ?",
[i + 1, remainingImages[i].id]
);
}
await connection.commit(); await connection.commit();
// Meilisearch 동기화 // Meilisearch 동기화

View file

@ -262,6 +262,26 @@ function Schedule() {
return cat?.name || ''; return cat?.name || '';
}; };
// ( )
const sortedCategories = useMemo(() => {
return categories
.map(category => {
const count = isSearchMode && searchTerm
? searchResults.filter(s => s.category_id === category.id).length
: schedules.filter(s => {
const scheduleDate = s.date ? s.date.split('T')[0] : '';
return scheduleDate.startsWith(currentYearMonth) && s.category_id === category.id;
}).length;
return { ...category, count };
})
.filter(category => category.count > 0)
.sort((a, b) => {
if (a.name === '기타') return 1;
if (b.name === '기타') return -1;
return b.count - a.count;
});
}, [categories, schedules, searchResults, isSearchMode, searchTerm, currentYearMonth]);
return ( return (
<div className="py-16"> <div className="py-16">
<div className="max-w-7xl mx-auto px-6"> <div className="max-w-7xl mx-auto px-6">
@ -533,15 +553,8 @@ function Schedule() {
</span> </span>
</button> </button>
{/* 개별 카테고리 */} {/* 개별 카테고리 - useMemo로 정렬됨 */}
{categories.map(category => { {sortedCategories.map(category => {
// ,
const count = isSearchMode && searchTerm
? searchResults.filter(s => s.category_id === category.id).length
: schedules.filter(s => {
const scheduleDate = s.date ? s.date.split('T')[0] : '';
return scheduleDate.startsWith(currentYearMonth) && s.category_id === category.id;
}).length;
const isSelected = selectedCategories.includes(category.id); const isSelected = selectedCategories.includes(category.id);
return ( return (
<button <button
@ -558,7 +571,7 @@ function Schedule() {
/> />
<span>{category.name}</span> <span>{category.name}</span>
</div> </div>
<span className="text-sm text-gray-400">{count}</span> <span className="text-sm text-gray-400">{category.count}</span>
</button> </button>
); );
})} })}

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useMemo } from 'react';
import { useNavigate, Link } from 'react-router-dom'; import { useNavigate, Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
@ -379,6 +379,23 @@ function AdminSchedule() {
return searchResults.length; return searchResults.length;
}; };
// ( )
const sortedCategories = useMemo(() => {
return categories
.map(category => ({
...category,
count: category.id === 'all' ? getTotalCount() : getSearchCategoryCount(category.id)
}))
.filter(category => category.id === 'all' || category.count > 0)
.sort((a, b) => {
if (a.id === 'all') return -1;
if (b.id === 'all') return 1;
if (a.name === '기타') return 1;
if (b.name === '기타') return -1;
return b.count - a.count;
});
}, [categories, schedules, searchResults, isSearchMode, searchTerm]);
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
@ -777,17 +794,16 @@ function AdminSchedule() {
> >
<h3 className="font-bold text-gray-900 mb-4">카테고리</h3> <h3 className="font-bold text-gray-900 mb-4">카테고리</h3>
<div className="space-y-2"> <div className="space-y-2">
{categories.map(category => { {/* 카테고리 - useMemo로 정렬됨 */}
{sortedCategories.map(category => {
const isSelected = category.id === 'all' const isSelected = category.id === 'all'
? selectedCategories.length === 0 ? selectedCategories.length === 0
: selectedCategories.includes(category.id); : selectedCategories.includes(category.id);
const handleClick = () => { const handleClick = () => {
if (category.id === 'all') { if (category.id === 'all') {
//
setSelectedCategories([]); setSelectedCategories([]);
} else { } else {
//
if (selectedCategories.includes(category.id)) { if (selectedCategories.includes(category.id)) {
setSelectedCategories(selectedCategories.filter(id => id !== category.id)); setSelectedCategories(selectedCategories.filter(id => id !== category.id));
} else { } else {
@ -812,10 +828,7 @@ function AdminSchedule() {
/> />
<span className="font-medium">{category.name}</span> <span className="font-medium">{category.name}</span>
<span className="ml-auto text-sm text-gray-400"> <span className="ml-auto text-sm text-gray-400">
{category.id === 'all' {category.count}
? getTotalCount()
: getSearchCategoryCount(category.id)
}
</span> </span>
</button> </button>
); );

File diff suppressed because it is too large Load diff