From 387db937b0ccf67d734bf464e7c0a4758491a994 Mon Sep 17 00:00:00 2001 From: caadiq Date: Mon, 5 Jan 2026 22:08:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(schedule):=20=EA=B3=B5=EA=B0=9C=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더에 검색 토글 UI 추가 (밑줄 스타일 검색창) - API 검색 기능 (/api/admin/schedules?search=) 연동 - 검색 모드에서 달력/카테고리 비활성화 (framer-motion animate) - 검색 결과에 년.월 형식 날짜 표시 (2025.4) - 카테고리 개수: 검색 시 결과 기준, 일반 시 해당 월 기준 - 달력/카테고리 구조 분리하여 독립 제어 - AdminSchedule.jsx도 동일한 비활성화 방식 적용 --- frontend/src/pages/pc/Schedule.jsx | 784 +++++++++++++----- frontend/src/pages/pc/admin/AdminSchedule.jsx | 362 +++++--- 2 files changed, 824 insertions(+), 322 deletions(-) diff --git a/frontend/src/pages/pc/Schedule.jsx b/frontend/src/pages/pc/Schedule.jsx index ade4915..10abd29 100644 --- a/frontend/src/pages/pc/Schedule.jsx +++ b/frontend/src/pages/pc/Schedule.jsx @@ -1,16 +1,86 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; -import { Clock, MapPin, Users, ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'; -import { schedules } from '../../data/dummy'; +import { Clock, ChevronLeft, ChevronRight, ChevronDown, Tag, Search, ArrowLeft } from 'lucide-react'; function Schedule() { + const navigate = useNavigate(); const [currentDate, setCurrentDate] = useState(new Date()); - const [selectedDate, setSelectedDate] = useState(null); + const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); // 오늘 기본값 const [showYearMonthPicker, setShowYearMonthPicker] = useState(false); - const [viewMode, setViewMode] = useState('yearMonth'); // 'yearMonth' | 'months' - const [slideDirection, setSlideDirection] = useState(0); // -1: prev, 1: next + const [viewMode, setViewMode] = useState('yearMonth'); + const [slideDirection, setSlideDirection] = useState(0); const pickerRef = useRef(null); + // 데이터 상태 + const [schedules, setSchedules] = useState([]); + const [categories, setCategories] = useState([]); + const [selectedCategories, setSelectedCategories] = useState([]); + const [loading, setLoading] = useState(true); + + // 카테고리 필터 툴팁 + const [showCategoryTooltip, setShowCategoryTooltip] = useState(false); + const categoryRef = useRef(null); + + // 검색 상태 + const [isSearchMode, setIsSearchMode] = useState(false); + const [searchInput, setSearchInput] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [searchLoading, setSearchLoading] = useState(false); + + // 데이터 로드 + useEffect(() => { + fetchSchedules(); + fetchCategories(); + }, []); + + const fetchSchedules = async () => { + try { + const response = await fetch('/api/schedules'); + if (response.ok) { + const data = await response.json(); + setSchedules(data); + } + } catch (error) { + console.error('일정 로드 오류:', error); + } finally { + setLoading(false); + } + }; + + const fetchCategories = async () => { + try { + const response = await fetch('/api/schedule-categories'); + if (response.ok) { + const data = await response.json(); + setCategories(data); + } + } catch (error) { + console.error('카테고리 로드 오류:', error); + } + }; + + // 검색 함수 (API 호출) + const searchSchedules = async (term) => { + if (!term.trim()) { + setSearchResults([]); + return; + } + setSearchLoading(true); + try { + const response = await fetch(`/api/admin/schedules?search=${encodeURIComponent(term)}`); + if (response.ok) { + const data = await response.json(); + setSearchResults(data); + } + } catch (error) { + console.error('검색 오류:', error); + } finally { + setSearchLoading(false); + } + }; + // 외부 클릭시 팝업 닫기 useEffect(() => { const handleClickOutside = (event) => { @@ -18,16 +88,14 @@ function Schedule() { setShowYearMonthPicker(false); setViewMode('yearMonth'); } + if (categoryRef.current && !categoryRef.current.contains(event.target)) { + setShowCategoryTooltip(false); + } }; - if (showYearMonthPicker) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [showYearMonthPicker]); + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); // 달력 관련 함수 const getDaysInMonth = (year, month) => new Date(year, month + 1, 0).getDate(); @@ -40,8 +108,17 @@ function Schedule() { const days = ['일', '월', '화', '수', '목', '금', '토']; - // 스케줄이 있는 날짜 목록 - const scheduleDates = schedules.map(s => s.date); + // 스케줄이 있는 날짜 목록 (ISO 형식에서 YYYY-MM-DD 추출) + const scheduleDates = schedules.map(s => s.date ? s.date.split('T')[0] : ''); + + // 해당 날짜의 첫 번째 일정 카테고리 색상 + const getScheduleColor = (day) => { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const schedule = schedules.find(s => (s.date ? s.date.split('T')[0] : '') === dateStr); + if (!schedule) return null; + const cat = categories.find(c => c.id === schedule.category_id); + return cat?.color || '#4A7C59'; + }; const hasSchedule = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; @@ -51,11 +128,13 @@ function Schedule() { const prevMonth = () => { setSlideDirection(-1); setCurrentDate(new Date(year, month - 1, 1)); + setSelectedDate(null); // 월 변경 시 초기화 }; const nextMonth = () => { setSlideDirection(1); setCurrentDate(new Date(year, month + 1, 1)); + setSelectedDate(null); // 월 변경 시 초기화 }; const selectDate = (day) => { @@ -63,23 +142,63 @@ function Schedule() { setSelectedDate(selectedDate === dateStr ? null : dateStr); }; - // 년도 선택 시 월 선택 모드로 전환 const selectYear = (newYear) => { setCurrentDate(new Date(newYear, month, 1)); setViewMode('months'); }; - // 월 선택 시 적용 후 닫기 const selectMonth = (newMonth) => { setCurrentDate(new Date(year, newMonth, 1)); setShowYearMonthPicker(false); setViewMode('yearMonth'); }; - // 필터링된 스케줄 - const filteredSchedules = selectedDate - ? schedules.filter(s => s.date === selectedDate) - : schedules; + // 카테고리 토글 + const toggleCategory = (categoryId) => { + setSelectedCategories(prev => + prev.includes(categoryId) + ? prev.filter(id => id !== categoryId) + : [...prev, categoryId] + ); + }; + + // 필터링된 스케줄 (useMemo로 성능 최적화, 시간순 정렬) + const currentYearMonth = `${year}-${String(month + 1).padStart(2, '0')}`; + + const filteredSchedules = useMemo(() => { + // 검색 모드일 때 + if (isSearchMode) { + // 검색 전엔 빈 목록, 검색 후엔 API 결과 + if (!searchTerm) return []; + return searchResults.sort((a, b) => { + const dateA = a.date ? a.date.split('T')[0] : ''; + const dateB = b.date ? b.date.split('T')[0] : ''; + if (dateA !== dateB) return dateA.localeCompare(dateB); + const timeA = a.time || '00:00:00'; + const timeB = b.time || '00:00:00'; + return timeA.localeCompare(timeB); + }); + } + + // 일반 모드: 기존 필터링 + return schedules + .filter(s => { + const scheduleDate = s.date ? s.date.split('T')[0] : ''; + const matchesDate = selectedDate + ? scheduleDate === selectedDate + : scheduleDate.startsWith(currentYearMonth); + const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(s.category_id); + return matchesDate && matchesCategory; + }) + .sort((a, b) => { + const dateA = a.date ? a.date.split('T')[0] : ''; + const dateB = b.date ? b.date.split('T')[0] : ''; + if (dateA !== dateB) return dateA.localeCompare(dateB); + const timeA = a.time || '00:00:00'; + const timeB = b.time || '00:00:00'; + return timeA.localeCompare(timeB); + }); + }, [schedules, selectedDate, currentYearMonth, selectedCategories, isSearchMode, searchTerm, searchResults]); const formatDate = (dateStr) => { const date = new Date(dateStr); @@ -91,24 +210,54 @@ function Schedule() { }; }; - // 년도 범위 (현재 년도 기준 10년 단위) + // 일정 클릭 핸들러 + const handleScheduleClick = (schedule) => { + // 설명이 없고 URL만 있으면 바로 링크 열기 + if (!schedule.description && schedule.source_url) { + window.open(schedule.source_url, '_blank'); + } else { + // 상세 페이지로 이동 (추후 구현) + navigate(`/schedule/${schedule.id}`); + } + }; + + // 년도 범위 const startYear = Math.floor(year / 10) * 10 - 1; const yearRange = Array.from({ length: 12 }, (_, i) => startYear + i); - // 현재 년도/월 확인 함수 const isCurrentYear = (y) => new Date().getFullYear() === y; const isCurrentMonth = (m) => { const today = new Date(); return today.getFullYear() === year && today.getMonth() === m; }; - // 월 배열 const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; - // 년도 범위 이동 const prevYearRange = () => setCurrentDate(new Date(year - 10, month, 1)); const nextYearRange = () => setCurrentDate(new Date(year + 10, month, 1)); + // 선택된 카테고리 이름 + const getSelectedCategoryNames = () => { + if (selectedCategories.length === 0) return '전체'; + const names = selectedCategories.map(id => { + const cat = categories.find(c => c.id === id); + return cat?.name || ''; + }).filter(Boolean); + if (names.length <= 2) return names.join(', '); + return `${names.slice(0, 2).join(', ')} 외 ${names.length - 2}개`; + }; + + // 카테고리 색상 가져오기 + const getCategoryColor = (categoryId) => { + const cat = categories.find(c => c.id === categoryId); + return cat?.color || '#808080'; + }; + + const getCategoryName = (categoryId) => { + const cat = categories.find(c => c.id === categoryId); + return cat?.name || ''; + }; + return (
@@ -132,13 +281,16 @@ function Schedule() {
- {/* 달력 - 더 큰 사이즈 */} - -
+ {/* 왼쪽: 달력 + 카테고리 */} +
+ {/* 달력 */} + +
{/* 달력 헤더 */}
- {/* 년/월 선택 팝업 - 달력 카드 중앙 정렬 */} + {/* 년/월 선택 팝업 */} {showYearMonthPicker && ( - {/* 헤더 - 년도 범위 이동 */} -
- - - {viewMode === 'yearMonth' ? `${yearRange[0]} - ${yearRange[yearRange.length - 1]}` : `${year}년`} - - -
+
+ + + {viewMode === 'yearMonth' ? `${yearRange[0]} - ${yearRange[yearRange.length - 1]}` : `${year}년`} + + +
- - {viewMode === 'yearMonth' && ( - - {/* 년도 선택 */} -
년도
-
- {yearRange.map((y) => ( - - ))} -
- - {/* 월 선택 */} -
-
- {monthNames.map((m, i) => ( - - ))} -
-
- )} - - {viewMode === 'months' && ( - - {/* 월 선택 */} -
월 선택
-
- {monthNames.map((m, i) => ( - - ))} -
-
- )} -
+ + {viewMode === 'yearMonth' && ( + +
년도
+
+ {yearRange.map((y) => ( + + ))} +
+
+
+ {monthNames.map((m, i) => ( + + ))} +
+
+ )} + {viewMode === 'months' && ( + +
월 선택
+
+ {monthNames.map((m, i) => ( + + ))} +
+
+ )} +
)}
- {/* 요일 헤더 + 날짜 그리드 (함께 슬라이드) */} + {/* 요일 헤더 + 날짜 그리드 */} - {/* 요일 헤더 */}
{days.map((day, i) => (
- {/* 날짜 그리드 */}
- {/* 전달 날짜 */} - {Array.from({ length: firstDay }).map((_, i) => { - const prevMonthDays = getDaysInMonth(year, month - 1); - const day = prevMonthDays - firstDay + i + 1; - return ( -
- {day} -
- ); - })} + {/* 전달 날짜 */} + {Array.from({ length: firstDay }).map((_, i) => { + const prevMonthDays = getDaysInMonth(year, month - 1); + const day = prevMonthDays - firstDay + i + 1; + return ( +
+ {day} +
+ ); + })} - {/* 현재 달 날짜 */} - {Array.from({ length: daysInMonth }).map((_, i) => { - const day = i + 1; - const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - const isSelected = selectedDate === dateStr; - const hasEvent = hasSchedule(day); - const dayOfWeek = (firstDay + i) % 7; - const isToday = new Date().toDateString() === new Date(year, month, day).toDateString(); + {/* 현재 달 날짜 */} + {Array.from({ length: daysInMonth }).map((_, i) => { + const day = i + 1; + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const isSelected = selectedDate === dateStr; + const hasEvent = hasSchedule(day); + const eventColor = getScheduleColor(day); + const dayOfWeek = (firstDay + i) % 7; + const isToday = new Date().toDateString() === new Date(year, month, day).toDateString(); - return ( - - ); - })} + return ( + + ); + })} - {/* 다음달 날짜 (마지막 주만 채우기) */} - {(() => { - const totalCells = firstDay + daysInMonth; - const remainder = totalCells % 7; - const nextDays = remainder === 0 ? 0 : 7 - remainder; - return Array.from({ length: nextDays }).map((_, i) => ( -
- {i + 1} -
- )); - })()} + {/* 다음달 날짜 */} + {(() => { + const totalCells = firstDay + daysInMonth; + const remainder = totalCells % 7; + const nextDays = remainder === 0 ? 0 : 7 - remainder; + return Array.from({ length: nextDays }).map((_, i) => ( +
+ {i + 1} +
+ )); + })()}
@@ -376,53 +499,260 @@ function Schedule() {
- {/* 스케줄 리스트 */} -
- {filteredSchedules.length > 0 ? ( - filteredSchedules.map((schedule, index) => { - const formatted = formatDate(schedule.date); - return ( - - {/* 날짜 영역 */} -
- {formatted.month}월 - {formatted.day} - {formatted.weekday} -
+ {/* 카테고리 필터 */} + +

카테고리

+
+ {/* 전체 */} + - {/* 스케줄 내용 */} -
-

{schedule.title}

- -
-
- - {schedule.time} -
-
- - {schedule.platform} -
-
- - {schedule.members.join(', ')} + {/* 개별 카테고리 */} + {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; + const isSelected = selectedCategories.includes(category.id); + return ( + + ); + })} +
+ +
+ + {/* 스케줄 리스트 */} +
+ {/* 헤더 */} +
+ + {isSearchMode ? ( + /* 검색 모드 - 밑줄 스타일 */ + + +
+ setSearchInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + setSearchTerm(searchInput); + searchSchedules(searchInput); + } else if (e.key === 'Escape') { + setIsSearchMode(false); + setSearchInput(''); + setSearchTerm(''); + setSearchResults([]); + } + }} + className="flex-1 bg-transparent focus:outline-none text-gray-700 placeholder-gray-400" + /> +
+ +
+ ) : ( + /* 일반 모드 */ + + +

+ {selectedDate + ? (() => { + const d = new Date(selectedDate); + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + return `${d.getMonth() + 1}월 ${d.getDate()}일 ${dayNames[d.getDay()]}요일`; + })() + : `${month + 1}월 전체 일정` + } +

+ {selectedCategories.length > 0 && ( +
+ + + {showCategoryTooltip && ( + + {selectedCategories.map(id => { + const cat = categories.find(c => c.id === id); + if (!cat) return null; + return ( +
+ + {cat.name} +
+ ); + })} +
+ )} +
+
+ )} +
+ )} +
+ {/* 검색 모드가 아닐 때만 개수 표시 */} + {!isSearchMode && ( + {filteredSchedules.length}개 일정 + )} +
+ +
+ {loading ? ( +
로딩 중...
+ ) : filteredSchedules.length > 0 ? ( + filteredSchedules.map((schedule, index) => { + const formatted = formatDate(schedule.date); + const categoryColor = getCategoryColor(schedule.category_id); + const categoryName = getCategoryName(schedule.category_id); + + return ( + handleScheduleClick(schedule)} + className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer" + > + {/* 날짜 영역 */} +
+ {/* 검색 모드일 때 년.월 표시, 일반 모드에서는 월 표시 안함 */} + {isSearchMode && searchTerm && ( + + {new Date(schedule.date).getFullYear()}.{new Date(schedule.date).getMonth() + 1} + + )} + {formatted.day} + {formatted.weekday} +
+ + {/* 스케줄 내용 */} +
+

{schedule.title}

+ +
+ {schedule.time && ( +
+ + {schedule.time.slice(0, 5)} +
+ )} + {categoryName && ( +
+ + {categoryName} +
+ )}
-
- - ); - }) - ) : ( -
- {selectedDate ? '선택한 날짜에 일정이 없습니다.' : '예정된 일정이 없습니다.'} -
- )} + + ); + }) + ) : ( +
+ {selectedDate ? '선택한 날짜에 일정이 없습니다.' : '예정된 일정이 없습니다.'} +
+ )} +
diff --git a/frontend/src/pages/pc/admin/AdminSchedule.jsx b/frontend/src/pages/pc/admin/AdminSchedule.jsx index 8666038..89e0b16 100644 --- a/frontend/src/pages/pc/admin/AdminSchedule.jsx +++ b/frontend/src/pages/pc/admin/AdminSchedule.jsx @@ -3,7 +3,7 @@ import { useNavigate, Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { LogOut, Home, ChevronRight, Calendar, Plus, Edit2, Trash2, - ChevronLeft, Search, ChevronDown, AlertTriangle + ChevronLeft, Search, ChevronDown, AlertTriangle, Bot, Tag, ArrowLeft } from 'lucide-react'; import Toast from '../../../components/Toast'; import Tooltip from '../../../components/Tooltip'; @@ -13,16 +13,22 @@ function AdminSchedule() { const [loading, setLoading] = useState(false); const [user, setUser] = useState(null); const [toast, setToast] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); + const [searchInput, setSearchInput] = useState(''); // 입력 상태 + const [searchTerm, setSearchTerm] = useState(''); // 실제 검색어 (엔터 시 적용) + const [isSearchMode, setIsSearchMode] = useState(false); // 검색 모드 활성화 + const [searchResults, setSearchResults] = useState([]); // 검색 결과 (API 응답) + const [searchLoading, setSearchLoading] = useState(false); // 검색 로딩 const [selectedCategories, setSelectedCategories] = useState([]); // 빈 배열 = 전체 - const [selectedDate, setSelectedDate] = useState(null); + const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [currentDate, setCurrentDate] = useState(new Date()); const [slideDirection, setSlideDirection] = useState(0); // 년월 선택 관련 (Schedule.jsx와 동일한 패턴) const [showYearMonthPicker, setShowYearMonthPicker] = useState(false); + const [showCategoryTooltip, setShowCategoryTooltip] = useState(false); const [viewMode, setViewMode] = useState('yearMonth'); // 'yearMonth' | 'months' const pickerRef = useRef(null); + const categoryTooltipRef = useRef(null); // 달력 관련 const year = currentDate.getFullYear(); @@ -101,6 +107,18 @@ function AdminSchedule() { }); }; + // 해당 날짜의 첫 번째 일정 카테고리 색상 + const getScheduleColor = (day) => { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const schedule = schedules.find(s => { + const scheduleDate = new Date(s.date).toISOString().split('T')[0]; + return scheduleDate === dateStr; + }); + if (!schedule) return null; + const cat = categories.find(c => c.id === schedule.category_id); + return cat?.color || '#4A7C59'; + }; + // Toast 자동 숨김 useEffect(() => { if (toast) { @@ -156,6 +174,24 @@ function AdminSchedule() { setLoading(false); } }; + + // 검색 함수 (API 호출) + const searchSchedules = async (term) => { + if (!term.trim()) { + setSearchResults([]); + return; + } + setSearchLoading(true); + try { + const res = await fetch(`/api/admin/schedules?search=${encodeURIComponent(term)}`); + const data = await res.json(); + setSearchResults(data); + } catch (error) { + console.error('검색 오류:', error); + } finally { + setSearchLoading(false); + } + }; // 외부 클릭 시 피커 닫기 useEffect(() => { @@ -164,14 +200,17 @@ function AdminSchedule() { setShowYearMonthPicker(false); setViewMode('yearMonth'); } + if (categoryTooltipRef.current && !categoryTooltipRef.current.contains(event.target)) { + setShowCategoryTooltip(false); + } }; - if (showYearMonthPicker) { + if (showYearMonthPicker || showCategoryTooltip) { document.addEventListener('mousedown', handleClickOutside); } return () => document.removeEventListener('mousedown', handleClickOutside); - }, [showYearMonthPicker]); + }, [showYearMonthPicker, showCategoryTooltip]); const handleLogout = () => { localStorage.removeItem('adminToken'); @@ -183,11 +222,15 @@ function AdminSchedule() { const prevMonth = () => { setSlideDirection(-1); setCurrentDate(new Date(year, month - 1, 1)); + setSelectedDate(null); + setSchedules([]); // 이전 달 데이터 즉시 초기화 }; const nextMonth = () => { setSlideDirection(1); setCurrentDate(new Date(year, month + 1, 1)); + setSelectedDate(null); + setSchedules([]); // 이전 달 데이터 즉시 초기화 }; // 년도 범위 이동 @@ -260,16 +303,36 @@ function AdminSchedule() { } }; - // 필터링된 일정 - const filteredSchedules = schedules.filter(schedule => { - const matchesSearch = schedule.title.toLowerCase().includes(searchTerm.toLowerCase()); - // 카테고리 필터링: 빈 배열이면 전체, 아니면 선택된 카테고리들에 포함되는지 확인 - const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(schedule.category_id); - // 날짜 필터링 추가 - const scheduleDate = new Date(schedule.date).toISOString().split('T')[0]; - const matchesDate = !selectedDate || scheduleDate === selectedDate; - return matchesSearch && matchesCategory && matchesDate; - }); + // 검색어 정규화 (대소문자, 띄어쓰기, 특수문자 무시) + const normalizeForSearch = (str) => { + return (str || '').toLowerCase().replace(/[\s\-_.,!?#@]/g, ''); + }; + + // 일정 목록 (검색 모드일 때 searchResults, 일반 모드일 때 로컬 필터링) + const filteredSchedules = isSearchMode + ? (searchTerm ? searchResults : []) // 검색 모드: 검색 전엔 빈 목록, 검색 후엔 API 결과 + : schedules.filter(schedule => { // 일반 모드: 로컬 필터링 + const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(schedule.category_id); + const scheduleDate = new Date(schedule.date).toISOString().split('T')[0]; + const matchesDate = !selectedDate || scheduleDate === selectedDate; + return matchesCategory && matchesDate; + }); + + // 검색 모드일 때 카테고리별 검색 결과 카운트 계산 + const getSearchCategoryCount = (categoryId) => { + if (!isSearchMode || !searchTerm) { + return schedules.filter(s => s.category_id === categoryId).length; + } + return searchResults.filter(s => s.category_id === categoryId).length; + }; + + // 검색 모드일 때 전체 일정 수 + const getTotalCount = () => { + if (!isSearchMode || !searchTerm) { + return schedules.length; + } + return searchResults.length; + }; return (
@@ -389,38 +452,55 @@ function AdminSchedule() {

일정 관리

fromis_9의 일정을 관리합니다

- +
+ + +
{/* 왼쪽: 달력 + 카테고리 필터 */}
{/* 달력 (Schedule.jsx와 동일한 패턴) */} -
+ {/* 달력 헤더 */} -
+
@@ -581,24 +661,29 @@ function AdminSchedule() { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const isSelected = selectedDate === dateStr; const hasEvent = hasSchedule(day); + const eventColor = getScheduleColor(day); const dayOfWeek = (firstDay + i) % 7; const isToday = new Date().toDateString() === new Date(year, month, day).toDateString(); return ( ); })} @@ -626,19 +711,25 @@ function AdminSchedule() {
-
+
{/* 카테고리 필터 */} -
+

카테고리

{categories.map(category => { @@ -677,70 +768,142 @@ function AdminSchedule() { {category.name} {category.id === 'all' - ? schedules.length - : schedules.filter(s => s.category_id === category.id).length + ? getTotalCount() + : getSearchCategoryCount(category.id) } ); })}
-
+
{/* 오른쪽: 일정 목록 */}
- {/* 검색 */} -
-
- - setSearchTerm(e.target.value)} - className="w-full pl-12 pr-4 py-3 bg-gray-50 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all" - /> -
-
- {/* 일정 목록 */}
-
-
- {selectedCategories.length > 1 ? ( - - {selectedCategories.map(id => { - const cat = categories.find(c => c.id === id); - if (!cat) return null; - return ( -
- - {cat.name} -
- ); - })} -
- } - > -

- {`${selectedCategories.length}개 카테고리 선택됨`} -

- - ) : ( -

- {selectedCategories.length === 0 - ? '전체 일정' - : categories.find(c => c.id === selectedCategories[0])?.name - } -

- )} - {filteredSchedules.length}개의 일정 +
+
+ + {isSearchMode ? ( + /* 검색 모드 */ + + + setSearchInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + setSearchTerm(searchInput); + searchSchedules(searchInput); + } else if (e.key === 'Escape') { + setIsSearchMode(false); + setSearchInput(''); + setSearchTerm(''); + setSearchResults([]); + } + }} + className="flex-1 bg-transparent focus:outline-none text-gray-700 placeholder-gray-400" + /> + + + ) : ( + /* 일반 모드 */ + + + + {selectedDate + ? (() => { + const d = new Date(selectedDate); + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + return `${d.getMonth() + 1}월 ${d.getDate()}일 ${dayNames[d.getDay()]}요일`; + })() + : `${month + 1}월 전체 일정` + } + +
+ {/* 카테고리 필터 */} + {selectedCategories.length > 0 && ( +
+ + + {showCategoryTooltip && ( + + {selectedCategories.map(id => { + const cat = categories.find(c => c.id === id); + if (!cat) return null; + return ( +
+ + {cat.name} +
+ ); + })} +
+ )} +
+
+ )} + {filteredSchedules.length}개 + + )} +
@@ -760,12 +923,18 @@ function AdminSchedule() { key={schedule.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - transition={{ delay: index * 0.05 }} + transition={{ delay: Math.min(index, 10) * 0.03 }} className="p-6 hover:bg-gray-50 transition-colors group" >
{/* 날짜 */} -
+
+ {/* 검색 모드일 때 년/월 표시 */} + {isSearchMode && searchTerm && ( +
+ {new Date(schedule.date).getFullYear()}.{new Date(schedule.date).getMonth() + 1} +
+ )}
{new Date(schedule.date).getDate()}
@@ -777,7 +946,10 @@ function AdminSchedule() { {/* 내용 */}
- + {schedule.category_name || '미지정'} {schedule.time?.slice(0, 5)}