feat: 일정 관련 UI/UX 개선
- 카테고리 정렬: 일정 개수 기준 내림차순, 0개 숨김, 기타는 맨 아래 고정 - useMemo로 카테고리 정렬 메모이제이션 (깜빡임 방지) - 일정 수정 시 이미지 삭제 버그 수정 (existingImageIds 업데이트) - 이미지 파일명에서 Date.now() 제거 (01.webp 형식 유지) - 이미지 삭제 후 sort_order 재정렬 로직 추가 - 날짜 선택 시 요일 표시 추가 (2026년 1월 7일 (수) 형식)
This commit is contained in:
parent
dac2234a0b
commit
bb027df914
4 changed files with 1996 additions and 1606 deletions
|
|
@ -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 동기화
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue