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];
currentOrder++;
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)
.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();
// Meilisearch 동기화

View file

@ -262,6 +262,26 @@ function Schedule() {
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 (
<div className="py-16">
<div className="max-w-7xl mx-auto px-6">
@ -533,15 +553,8 @@ function Schedule() {
</span>
</button>
{/* 개별 카테고리 */}
{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;
{/* 개별 카테고리 - useMemo로 정렬됨 */}
{sortedCategories.map(category => {
const isSelected = selectedCategories.includes(category.id);
return (
<button
@ -558,7 +571,7 @@ function Schedule() {
/>
<span>{category.name}</span>
</div>
<span className="text-sm text-gray-400">{count}</span>
<span className="text-sm text-gray-400">{category.count}</span>
</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 { motion, AnimatePresence } from 'framer-motion';
import {
@ -379,6 +379,23 @@ function AdminSchedule() {
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 (
<div className="min-h-screen bg-gray-50">
<Toast toast={toast} onClose={() => setToast(null)} />
@ -777,17 +794,16 @@ function AdminSchedule() {
>
<h3 className="font-bold text-gray-900 mb-4">카테고리</h3>
<div className="space-y-2">
{categories.map(category => {
{/* 카테고리 - useMemo로 정렬됨 */}
{sortedCategories.map(category => {
const isSelected = category.id === 'all'
? selectedCategories.length === 0
: selectedCategories.includes(category.id);
const handleClick = () => {
if (category.id === 'all') {
//
setSelectedCategories([]);
} else {
//
if (selectedCategories.includes(category.id)) {
setSelectedCategories(selectedCategories.filter(id => id !== category.id));
} else {
@ -812,10 +828,7 @@ function AdminSchedule() {
/>
<span className="font-medium">{category.name}</span>
<span className="ml-auto text-sm text-gray-400">
{category.id === 'all'
? getTotalCount()
: getSearchCategoryCount(category.id)
}
{category.count}
</span>
</button>
);

File diff suppressed because it is too large Load diff