refactor: CustomDatePicker 공통 컴포넌트로 분리
- components/admin/CustomDatePicker.jsx 생성 (269줄) - AdminMemberEdit에서 중복 제거 (-237줄) - AdminAlbumForm에서 중복 제거 (-273줄) - AdminScheduleForm에서 중복 제거 (-349줄) - 현재 년도/월 표시: 테두리 제거, 글씨색만 유지 - 오늘 날짜: 배경색 제거, 글씨색만 유지 - 요일/일요일/토요일 색상 구분 추가 총 코드 감소: 약 860줄
This commit is contained in:
parent
8124a1abe1
commit
7867eb8928
4 changed files with 266 additions and 862 deletions
261
frontend/src/components/admin/CustomDatePicker.jsx
Normal file
261
frontend/src/components/admin/CustomDatePicker.jsx
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
/**
|
||||||
|
* 커스텀 데이트픽커 컴포넌트
|
||||||
|
* 연/월/일 선택이 가능한 드롭다운 형태의 날짜 선택기
|
||||||
|
*/
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Calendar, ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
function CustomDatePicker({ value, onChange, placeholder = '날짜 선택', showDayOfWeek = false }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [viewMode, setViewMode] = useState('days');
|
||||||
|
const [viewDate, setViewDate] = useState(() => {
|
||||||
|
if (value) return new Date(value);
|
||||||
|
return new Date();
|
||||||
|
});
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (ref.current && !ref.current.contains(e.target)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
setViewMode('days');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const year = viewDate.getFullYear();
|
||||||
|
const month = viewDate.getMonth();
|
||||||
|
|
||||||
|
const firstDay = new Date(year, month, 1).getDay();
|
||||||
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
const days = [];
|
||||||
|
for (let i = 0; i < firstDay; i++) {
|
||||||
|
days.push(null);
|
||||||
|
}
|
||||||
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
|
days.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startYear = Math.floor(year / 10) * 10 - 1;
|
||||||
|
const years = Array.from({ length: 12 }, (_, i) => startYear + i);
|
||||||
|
|
||||||
|
const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
|
||||||
|
const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
|
||||||
|
const prevYearRange = () => setViewDate(new Date(year - 10, month, 1));
|
||||||
|
const nextYearRange = () => setViewDate(new Date(year + 10, month, 1));
|
||||||
|
|
||||||
|
const selectDate = (day) => {
|
||||||
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
onChange(dateStr);
|
||||||
|
setIsOpen(false);
|
||||||
|
setViewMode('days');
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectYear = (y) => {
|
||||||
|
setViewDate(new Date(y, month, 1));
|
||||||
|
setViewMode('months');
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectMonth = (m) => {
|
||||||
|
setViewDate(new Date(year, m, 1));
|
||||||
|
setViewMode('days');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 표시 포맷 (요일 포함 옵션)
|
||||||
|
const formatDisplayDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const [y, m, d] = dateStr.split('-');
|
||||||
|
if (showDayOfWeek) {
|
||||||
|
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||||
|
const date = new Date(parseInt(y), parseInt(m) - 1, parseInt(d));
|
||||||
|
const dayOfWeek = dayNames[date.getDay()];
|
||||||
|
return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일 (${dayOfWeek})`;
|
||||||
|
}
|
||||||
|
return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelected = (day) => {
|
||||||
|
if (!value || !day) return false;
|
||||||
|
const [y, m, d] = value.split('-');
|
||||||
|
return parseInt(y) === year && parseInt(m) === month + 1 && parseInt(d) === day;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = (day) => {
|
||||||
|
if (!day) return false;
|
||||||
|
const today = new Date();
|
||||||
|
return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day;
|
||||||
|
};
|
||||||
|
|
||||||
|
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월'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full px-4 py-3 border border-gray-200 rounded-xl bg-white flex items-center justify-between hover:border-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
>
|
||||||
|
<span className={value ? 'text-gray-900' : 'text-gray-400'}>
|
||||||
|
{value ? formatDisplayDate(value) : placeholder}
|
||||||
|
</span>
|
||||||
|
<Calendar size={18} className="text-gray-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="absolute z-50 mt-2 bg-white border border-gray-200 rounded-xl shadow-lg p-4 w-80"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={viewMode === 'years' ? prevYearRange : prevMonth}
|
||||||
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={20} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMode(viewMode === 'days' ? 'years' : 'days')}
|
||||||
|
className="font-medium text-gray-900 hover:text-primary transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{viewMode === 'years' ? `${years[0]} - ${years[years.length - 1]}` : `${year}년 ${month + 1}월`}
|
||||||
|
<ChevronDown size={16} className={`transition-transform ${viewMode !== 'days' ? 'rotate-180' : ''}`} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={viewMode === 'years' ? nextYearRange : nextMonth}
|
||||||
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight size={20} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{viewMode === 'years' && (
|
||||||
|
<motion.div
|
||||||
|
key="years"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
|
<div className="text-center text-sm text-gray-500 mb-3">년도</div>
|
||||||
|
<div className="grid grid-cols-4 gap-2 mb-4">
|
||||||
|
{years.map((y) => (
|
||||||
|
<button
|
||||||
|
key={y}
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectYear(y)}
|
||||||
|
className={`py-2 rounded-lg text-sm transition-colors ${year === y ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentYear(y) && year !== y ? 'text-primary font-medium' : ''}`}
|
||||||
|
>
|
||||||
|
{y}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-sm text-gray-500 mb-3">월</div>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{monthNames.map((m, i) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectMonth(i)}
|
||||||
|
className={`py-2 rounded-lg text-sm transition-colors ${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentMonth(i) && month !== i ? 'text-primary font-medium' : ''}`}
|
||||||
|
>
|
||||||
|
{m}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewMode === 'months' && (
|
||||||
|
<motion.div
|
||||||
|
key="months"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
|
<div className="text-center text-sm text-gray-500 mb-3">월 선택</div>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{monthNames.map((m, i) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectMonth(i)}
|
||||||
|
className={`py-2.5 rounded-lg text-sm transition-colors ${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentMonth(i) && month !== i ? 'text-primary font-medium' : ''}`}
|
||||||
|
>
|
||||||
|
{m}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewMode === 'days' && (
|
||||||
|
<motion.div
|
||||||
|
key="days"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
||||||
|
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
|
||||||
|
<div
|
||||||
|
key={d}
|
||||||
|
className={`text-center text-xs font-medium py-1 ${i === 0 ? 'text-red-400' : i === 6 ? 'text-blue-400' : 'text-gray-400'}`}
|
||||||
|
>
|
||||||
|
{d}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{days.map((day, i) => {
|
||||||
|
const dayOfWeek = i % 7;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
type="button"
|
||||||
|
disabled={!day}
|
||||||
|
onClick={() => day && selectDate(day)}
|
||||||
|
className={`aspect-square rounded-full text-sm font-medium flex items-center justify-center transition-all
|
||||||
|
${!day ? '' : 'hover:bg-gray-100'}
|
||||||
|
${isSelected(day) ? 'bg-primary text-white hover:bg-primary' : ''}
|
||||||
|
${isToday(day) && !isSelected(day) ? 'text-primary font-bold' : ''}
|
||||||
|
${day && !isSelected(day) && !isToday(day) && dayOfWeek === 0 ? 'text-red-500' : ''}
|
||||||
|
${day && !isSelected(day) && !isToday(day) && dayOfWeek === 6 ? 'text-blue-500' : ''}
|
||||||
|
${day && !isSelected(day) && !isToday(day) && dayOfWeek > 0 && dayOfWeek < 6 ? 'text-gray-700' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomDatePicker;
|
||||||
|
|
@ -3,9 +3,10 @@ import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Save, Home, ChevronRight, LogOut, Music, Trash2, Plus, Image, Star,
|
Save, Home, ChevronRight, LogOut, Music, Trash2, Plus, Image, Star,
|
||||||
ChevronDown, ChevronLeft, Calendar
|
ChevronDown
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Toast from '../../../components/Toast';
|
import Toast from '../../../components/Toast';
|
||||||
|
import CustomDatePicker from '../../../components/admin/CustomDatePicker';
|
||||||
|
|
||||||
// 커스텀 드롭다운 컴포넌트
|
// 커스텀 드롭다운 컴포넌트
|
||||||
function CustomSelect({ value, onChange, options, placeholder }) {
|
function CustomSelect({ value, onChange, options, placeholder }) {
|
||||||
|
|
@ -71,280 +72,6 @@ function CustomSelect({ value, onChange, options, placeholder }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 커스텀 데이트픽커 컴포넌트
|
|
||||||
function CustomDatePicker({ value, onChange }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [viewMode, setViewMode] = useState('days'); // 'days' | 'months' | 'years'
|
|
||||||
const [viewDate, setViewDate] = useState(() => {
|
|
||||||
if (value) return new Date(value);
|
|
||||||
return new Date();
|
|
||||||
});
|
|
||||||
const ref = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e) => {
|
|
||||||
if (ref.current && !ref.current.contains(e.target)) {
|
|
||||||
setIsOpen(false);
|
|
||||||
setViewMode('days');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const year = viewDate.getFullYear();
|
|
||||||
const month = viewDate.getMonth();
|
|
||||||
|
|
||||||
const firstDay = new Date(year, month, 1).getDay();
|
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
||||||
|
|
||||||
const days = [];
|
|
||||||
for (let i = 0; i < firstDay; i++) {
|
|
||||||
days.push(null);
|
|
||||||
}
|
|
||||||
for (let i = 1; i <= daysInMonth; i++) {
|
|
||||||
days.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 년도 범위 (현재 년도 기준 -10 ~ +10)
|
|
||||||
const startYear = Math.floor(year / 10) * 10 - 1;
|
|
||||||
const years = Array.from({ length: 12 }, (_, i) => startYear + i);
|
|
||||||
|
|
||||||
const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
|
|
||||||
const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
|
|
||||||
const prevYearRange = () => setViewDate(new Date(year - 10, month, 1));
|
|
||||||
const nextYearRange = () => setViewDate(new Date(year + 10, month, 1));
|
|
||||||
|
|
||||||
const selectDate = (day) => {
|
|
||||||
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
||||||
onChange(dateStr);
|
|
||||||
setIsOpen(false);
|
|
||||||
setViewMode('days');
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectYear = (y) => {
|
|
||||||
setViewDate(new Date(y, month, 1));
|
|
||||||
setViewMode('months');
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectMonth = (m) => {
|
|
||||||
setViewDate(new Date(year, m, 1));
|
|
||||||
setViewMode('days');
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDisplayDate = (dateStr) => {
|
|
||||||
if (!dateStr) return '';
|
|
||||||
const [y, m, d] = dateStr.split('-');
|
|
||||||
return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelected = (day) => {
|
|
||||||
if (!value || !day) return false;
|
|
||||||
const [y, m, d] = value.split('-');
|
|
||||||
return parseInt(y) === year && parseInt(m) === month + 1 && parseInt(d) === day;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isToday = (day) => {
|
|
||||||
if (!day) return false;
|
|
||||||
const today = new Date();
|
|
||||||
return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCurrentYear = (y) => {
|
|
||||||
return new Date().getFullYear() === y;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCurrentMonth = (m) => {
|
|
||||||
const today = new Date();
|
|
||||||
return today.getFullYear() === year && today.getMonth() === m;
|
|
||||||
};
|
|
||||||
|
|
||||||
const months = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="w-full px-4 py-2.5 border border-gray-200 rounded-lg bg-white flex items-center justify-between hover:border-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
||||||
>
|
|
||||||
<span className={value ? 'text-gray-900' : 'text-gray-400'}>
|
|
||||||
{value ? formatDisplayDate(value) : '날짜 선택'}
|
|
||||||
</span>
|
|
||||||
<Calendar size={18} className="text-gray-400" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -10 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
className="absolute z-50 mt-2 bg-white border border-gray-200 rounded-xl shadow-lg p-4 w-80"
|
|
||||||
>
|
|
||||||
{/* 헤더 */}
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={viewMode === 'years' ? prevYearRange : prevMonth}
|
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronLeft size={20} className="text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setViewMode(viewMode === 'days' ? 'years' : 'days')}
|
|
||||||
className="font-medium text-gray-900 hover:text-primary transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{viewMode === 'years' ? `${years[0]} - ${years[years.length - 1]}` : `${year}년 ${month + 1}월`}
|
|
||||||
<ChevronDown size={16} className={`transition-transform ${viewMode !== 'days' ? 'rotate-180' : ''}`} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={viewMode === 'years' ? nextYearRange : nextMonth}
|
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronRight size={20} className="text-gray-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{viewMode === 'years' && (
|
|
||||||
<motion.div
|
|
||||||
key="years"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
{/* 년도 라벨 */}
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">년도</div>
|
|
||||||
|
|
||||||
{/* 년도 그리드 */}
|
|
||||||
<div className="grid grid-cols-4 gap-2 mb-4">
|
|
||||||
{years.map((y) => (
|
|
||||||
<button
|
|
||||||
key={y}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectYear(y)}
|
|
||||||
className={`
|
|
||||||
py-2 rounded-lg text-sm transition-colors
|
|
||||||
${year === y ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'}
|
|
||||||
${isCurrentYear(y) && year !== y ? 'border border-primary text-primary' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{y}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 월 라벨 */}
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">월</div>
|
|
||||||
|
|
||||||
{/* 월 그리드 */}
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{months.map((m, i) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectMonth(i)}
|
|
||||||
className={`
|
|
||||||
py-2 rounded-lg text-sm transition-colors
|
|
||||||
${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'}
|
|
||||||
${isCurrentMonth(i) && month !== i ? 'border border-primary text-primary' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === 'months' && (
|
|
||||||
<motion.div
|
|
||||||
key="months"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
{/* 월 라벨 */}
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">월 선택</div>
|
|
||||||
|
|
||||||
{/* 월 그리드 */}
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{months.map((m, i) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectMonth(i)}
|
|
||||||
className={`
|
|
||||||
py-2.5 rounded-lg text-sm transition-colors
|
|
||||||
${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'}
|
|
||||||
${isCurrentMonth(i) && month !== i ? 'border border-primary text-primary' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === 'days' && (
|
|
||||||
<motion.div
|
|
||||||
key="days"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
{/* 요일 */}
|
|
||||||
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
||||||
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
|
|
||||||
<div
|
|
||||||
key={d}
|
|
||||||
className={`text-center text-xs font-medium py-1 ${
|
|
||||||
i === 0 ? 'text-red-400' : i === 6 ? 'text-blue-400' : 'text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{d}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 날짜 */}
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
|
||||||
{days.map((day, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
type="button"
|
|
||||||
disabled={!day}
|
|
||||||
onClick={() => day && selectDate(day)}
|
|
||||||
className={`
|
|
||||||
aspect-square rounded-lg text-sm flex items-center justify-center transition-colors
|
|
||||||
${!day ? '' : 'hover:bg-gray-100'}
|
|
||||||
${isSelected(day) ? 'bg-primary text-white hover:bg-primary-dark' : ''}
|
|
||||||
${isToday(day) && !isSelected(day) ? 'border border-primary text-primary' : ''}
|
|
||||||
${day && !isSelected(day) && !isToday(day) ? 'text-gray-700' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{day}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AdminAlbumForm() {
|
function AdminAlbumForm() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
|
||||||
|
|
@ -3,249 +3,13 @@ import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Save, Upload, LogOut,
|
Save, Upload, LogOut,
|
||||||
Home, ChevronRight, ChevronLeft, ChevronDown, User, Instagram, Calendar, Briefcase
|
Home, ChevronRight, User, Instagram, Calendar, Briefcase
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Toast from '../../../components/Toast';
|
import Toast from '../../../components/Toast';
|
||||||
|
import CustomDatePicker from '../../../components/admin/CustomDatePicker';
|
||||||
import * as authApi from '../../../api/admin/auth';
|
import * as authApi from '../../../api/admin/auth';
|
||||||
import * as membersApi from '../../../api/admin/members';
|
import * as membersApi from '../../../api/admin/members';
|
||||||
|
|
||||||
// 커스텀 데이트픽커 컴포넌트
|
|
||||||
function CustomDatePicker({ value, onChange }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [viewMode, setViewMode] = useState('days');
|
|
||||||
const [viewDate, setViewDate] = useState(() => {
|
|
||||||
if (value) return new Date(value);
|
|
||||||
return new Date();
|
|
||||||
});
|
|
||||||
const ref = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e) => {
|
|
||||||
if (ref.current && !ref.current.contains(e.target)) {
|
|
||||||
setIsOpen(false);
|
|
||||||
setViewMode('days');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const year = viewDate.getFullYear();
|
|
||||||
const month = viewDate.getMonth();
|
|
||||||
|
|
||||||
const firstDay = new Date(year, month, 1).getDay();
|
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
||||||
|
|
||||||
const days = [];
|
|
||||||
for (let i = 0; i < firstDay; i++) {
|
|
||||||
days.push(null);
|
|
||||||
}
|
|
||||||
for (let i = 1; i <= daysInMonth; i++) {
|
|
||||||
days.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startYear = Math.floor(year / 10) * 10 - 1;
|
|
||||||
const years = Array.from({ length: 12 }, (_, i) => startYear + i);
|
|
||||||
|
|
||||||
const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
|
|
||||||
const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
|
|
||||||
const prevYearRange = () => setViewDate(new Date(year - 10, month, 1));
|
|
||||||
const nextYearRange = () => setViewDate(new Date(year + 10, month, 1));
|
|
||||||
|
|
||||||
const selectDate = (day) => {
|
|
||||||
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
||||||
onChange(dateStr);
|
|
||||||
setIsOpen(false);
|
|
||||||
setViewMode('days');
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectYear = (y) => {
|
|
||||||
setViewDate(new Date(y, month, 1));
|
|
||||||
setViewMode('months');
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectMonth = (m) => {
|
|
||||||
setViewDate(new Date(year, m, 1));
|
|
||||||
setViewMode('days');
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDisplayDate = (dateStr) => {
|
|
||||||
if (!dateStr) return '';
|
|
||||||
const [y, m, d] = dateStr.split('-');
|
|
||||||
return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelected = (day) => {
|
|
||||||
if (!value || !day) return false;
|
|
||||||
const [y, m, d] = value.split('-');
|
|
||||||
return parseInt(y) === year && parseInt(m) === month + 1 && parseInt(d) === day;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isToday = (day) => {
|
|
||||||
if (!day) return false;
|
|
||||||
const today = new Date();
|
|
||||||
return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCurrentYear = (y) => new Date().getFullYear() === y;
|
|
||||||
const isCurrentMonth = (m) => {
|
|
||||||
const today = new Date();
|
|
||||||
return today.getFullYear() === year && today.getMonth() === m;
|
|
||||||
};
|
|
||||||
|
|
||||||
const months = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="w-full px-4 py-3 border border-gray-200 rounded-xl bg-white flex items-center justify-between hover:border-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
||||||
>
|
|
||||||
<span className={value ? 'text-gray-900' : 'text-gray-400'}>
|
|
||||||
{value ? formatDisplayDate(value) : '날짜 선택'}
|
|
||||||
</span>
|
|
||||||
<Calendar size={18} className="text-gray-400" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -10 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
className="absolute z-50 mt-2 bg-white border border-gray-200 rounded-xl shadow-lg p-4 w-80"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={viewMode === 'years' ? prevYearRange : prevMonth}
|
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronLeft size={20} className="text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setViewMode(viewMode === 'days' ? 'years' : 'days')}
|
|
||||||
className="font-medium text-gray-900 hover:text-primary transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{viewMode === 'years' ? `${years[0]} - ${years[years.length - 1]}` : `${year}년 ${month + 1}월`}
|
|
||||||
<ChevronDown size={16} className={`transition-transform ${viewMode !== 'days' ? 'rotate-180' : ''}`} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={viewMode === 'years' ? nextYearRange : nextMonth}
|
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronRight size={20} className="text-gray-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{viewMode === 'years' && (
|
|
||||||
<motion.div
|
|
||||||
key="years"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">년도</div>
|
|
||||||
<div className="grid grid-cols-4 gap-2 mb-4">
|
|
||||||
{years.map((y) => (
|
|
||||||
<button
|
|
||||||
key={y}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectYear(y)}
|
|
||||||
className={`py-2 rounded-lg text-sm transition-colors ${year === y ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentYear(y) && year !== y ? 'border border-primary text-primary' : ''}`}
|
|
||||||
>
|
|
||||||
{y}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">월</div>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{months.map((m, i) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectMonth(i)}
|
|
||||||
className={`py-2 rounded-lg text-sm transition-colors ${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentMonth(i) && month !== i ? 'border border-primary text-primary' : ''}`}
|
|
||||||
>
|
|
||||||
{m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === 'months' && (
|
|
||||||
<motion.div
|
|
||||||
key="months"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">월 선택</div>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{months.map((m, i) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectMonth(i)}
|
|
||||||
className={`py-2.5 rounded-lg text-sm transition-colors ${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentMonth(i) && month !== i ? 'border border-primary text-primary' : ''}`}
|
|
||||||
>
|
|
||||||
{m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === 'days' && (
|
|
||||||
<motion.div
|
|
||||||
key="days"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
||||||
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
|
|
||||||
<div
|
|
||||||
key={d}
|
|
||||||
className={`text-center text-xs font-medium py-1 ${i === 0 ? 'text-red-400' : i === 6 ? 'text-blue-400' : 'text-gray-400'}`}
|
|
||||||
>
|
|
||||||
{d}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
|
||||||
{days.map((day, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
type="button"
|
|
||||||
disabled={!day}
|
|
||||||
onClick={() => day && selectDate(day)}
|
|
||||||
className={`aspect-square rounded-lg text-sm flex items-center justify-center transition-colors ${!day ? '' : 'hover:bg-gray-100'} ${isSelected(day) ? 'bg-primary text-white hover:bg-primary-dark' : ''} ${isToday(day) && !isSelected(day) ? 'border border-primary text-primary' : ''} ${day && !isSelected(day) && !isToday(day) ? 'text-gray-700' : ''}`}
|
|
||||||
>
|
|
||||||
{day}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function AdminMemberEdit() {
|
function AdminMemberEdit() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { name } = useParams();
|
const { name } = useParams();
|
||||||
|
|
|
||||||
|
|
@ -26,359 +26,11 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Toast from "../../../components/Toast";
|
import Toast from "../../../components/Toast";
|
||||||
import Lightbox from "../../../components/common/Lightbox";
|
import Lightbox from "../../../components/common/Lightbox";
|
||||||
|
import CustomDatePicker from "../../../components/admin/CustomDatePicker";
|
||||||
import * as authApi from "../../../api/admin/auth";
|
import * as authApi from "../../../api/admin/auth";
|
||||||
import * as categoriesApi from "../../../api/admin/categories";
|
import * as categoriesApi from "../../../api/admin/categories";
|
||||||
import * as schedulesApi from "../../../api/admin/schedules";
|
import * as schedulesApi from "../../../api/admin/schedules";
|
||||||
import { getMembers } from "../../../api/public/members";
|
import { getMembers } from "../../../api/public/members";
|
||||||
// 커스텀 데이트픽커 컴포넌트 (AdminMemberEdit.jsx에서 가져옴)
|
|
||||||
function CustomDatePicker({ value, onChange, placeholder = "날짜 선택" }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [viewMode, setViewMode] = useState("days");
|
|
||||||
const [viewDate, setViewDate] = useState(() => {
|
|
||||||
if (value) return new Date(value);
|
|
||||||
return new Date();
|
|
||||||
});
|
|
||||||
const ref = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e) => {
|
|
||||||
if (ref.current && !ref.current.contains(e.target)) {
|
|
||||||
setIsOpen(false);
|
|
||||||
setViewMode("days");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const year = viewDate.getFullYear();
|
|
||||||
const month = viewDate.getMonth();
|
|
||||||
|
|
||||||
const firstDay = new Date(year, month, 1).getDay();
|
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
||||||
|
|
||||||
const days = [];
|
|
||||||
for (let i = 0; i < firstDay; i++) {
|
|
||||||
days.push(null);
|
|
||||||
}
|
|
||||||
for (let i = 1; i <= daysInMonth; i++) {
|
|
||||||
days.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startYear = Math.floor(year / 10) * 10 - 1;
|
|
||||||
const years = Array.from({ length: 12 }, (_, i) => startYear + i);
|
|
||||||
|
|
||||||
const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
|
|
||||||
const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
|
|
||||||
const prevYearRange = () => setViewDate(new Date(year - 10, month, 1));
|
|
||||||
const nextYearRange = () => setViewDate(new Date(year + 10, month, 1));
|
|
||||||
|
|
||||||
const selectDate = (day) => {
|
|
||||||
const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(
|
|
||||||
day
|
|
||||||
).padStart(2, "0")}`;
|
|
||||||
onChange(dateStr);
|
|
||||||
setIsOpen(false);
|
|
||||||
setViewMode("days");
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectYear = (y) => {
|
|
||||||
setViewDate(new Date(y, month, 1));
|
|
||||||
setViewMode("months");
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectMonth = (m) => {
|
|
||||||
setViewDate(new Date(year, m, 1));
|
|
||||||
setViewMode("days");
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDisplayDate = (dateStr) => {
|
|
||||||
if (!dateStr) return "";
|
|
||||||
const [y, m, d] = dateStr.split("-");
|
|
||||||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
|
||||||
const date = new Date(parseInt(y), parseInt(m) - 1, parseInt(d));
|
|
||||||
const dayOfWeek = days[date.getDay()];
|
|
||||||
return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일 (${dayOfWeek})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelected = (day) => {
|
|
||||||
if (!value || !day) return false;
|
|
||||||
const [y, m, d] = value.split("-");
|
|
||||||
return (
|
|
||||||
parseInt(y) === year && parseInt(m) === month + 1 && parseInt(d) === day
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isToday = (day) => {
|
|
||||||
if (!day) return false;
|
|
||||||
const today = new Date();
|
|
||||||
return (
|
|
||||||
today.getFullYear() === year &&
|
|
||||||
today.getMonth() === month &&
|
|
||||||
today.getDate() === day
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCurrentYear = (y) => new Date().getFullYear() === y;
|
|
||||||
const isCurrentMonth = (m) => {
|
|
||||||
const today = new Date();
|
|
||||||
return today.getFullYear() === year && today.getMonth() === m;
|
|
||||||
};
|
|
||||||
|
|
||||||
const months = [
|
|
||||||
"1월",
|
|
||||||
"2월",
|
|
||||||
"3월",
|
|
||||||
"4월",
|
|
||||||
"5월",
|
|
||||||
"6월",
|
|
||||||
"7월",
|
|
||||||
"8월",
|
|
||||||
"9월",
|
|
||||||
"10월",
|
|
||||||
"11월",
|
|
||||||
"12월",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="w-full px-4 py-3 border border-gray-200 rounded-xl bg-white flex items-center justify-between hover:border-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
||||||
>
|
|
||||||
<span className={value ? "text-gray-900" : "text-gray-400"}>
|
|
||||||
{value ? formatDisplayDate(value) : placeholder}
|
|
||||||
</span>
|
|
||||||
<Calendar size={18} className="text-gray-400" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -10 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
className="absolute z-50 mt-2 bg-white border border-gray-200 rounded-xl shadow-lg p-4 w-80"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={viewMode === "years" ? prevYearRange : prevMonth}
|
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronLeft size={20} className="text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setViewMode(viewMode === "days" ? "years" : "days")
|
|
||||||
}
|
|
||||||
className="font-medium text-gray-900 hover:text-primary transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{viewMode === "years"
|
|
||||||
? `${years[0]} - ${years[years.length - 1]}`
|
|
||||||
: `${year}년 ${month + 1}월`}
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={`transition-transform ${
|
|
||||||
viewMode !== "days" ? "rotate-180" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={viewMode === "years" ? nextYearRange : nextMonth}
|
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronRight size={20} className="text-gray-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{viewMode === "years" && (
|
|
||||||
<motion.div
|
|
||||||
key="years"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">
|
|
||||||
년도
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-2 mb-4">
|
|
||||||
{years.map((y) => (
|
|
||||||
<button
|
|
||||||
key={y}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectYear(y)}
|
|
||||||
className={`py-2 rounded-lg text-sm transition-colors ${
|
|
||||||
year === y
|
|
||||||
? "bg-primary text-white"
|
|
||||||
: "hover:bg-gray-100 text-gray-700"
|
|
||||||
} ${
|
|
||||||
isCurrentYear(y) && year !== y
|
|
||||||
? "border border-primary text-primary"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{y}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">
|
|
||||||
월
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{months.map((m, i) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectMonth(i)}
|
|
||||||
className={`py-2 rounded-lg text-sm transition-colors ${
|
|
||||||
month === i
|
|
||||||
? "bg-primary text-white"
|
|
||||||
: "hover:bg-gray-100 text-gray-700"
|
|
||||||
} ${
|
|
||||||
isCurrentMonth(i) && month !== i
|
|
||||||
? "border border-primary text-primary"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === "months" && (
|
|
||||||
<motion.div
|
|
||||||
key="months"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<div className="text-center text-sm text-gray-500 mb-3">
|
|
||||||
월 선택
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{months.map((m, i) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectMonth(i)}
|
|
||||||
className={`py-2.5 rounded-lg text-sm transition-colors ${
|
|
||||||
month === i
|
|
||||||
? "bg-primary text-white"
|
|
||||||
: "hover:bg-gray-100 text-gray-700"
|
|
||||||
} ${
|
|
||||||
isCurrentMonth(i) && month !== i
|
|
||||||
? "border border-primary text-primary"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === "days" && (
|
|
||||||
<motion.div
|
|
||||||
key="days"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
||||||
{["일", "월", "화", "수", "목", "금", "토"].map((d, i) => (
|
|
||||||
<div
|
|
||||||
key={d}
|
|
||||||
className={`text-center text-xs font-medium py-1 ${
|
|
||||||
i === 0
|
|
||||||
? "text-red-400"
|
|
||||||
: i === 6
|
|
||||||
? "text-blue-400"
|
|
||||||
: "text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{d}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
|
||||||
{days.map((day, i) => {
|
|
||||||
const dayOfWeek = i % 7;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
type="button"
|
|
||||||
disabled={!day}
|
|
||||||
onClick={() => day && selectDate(day)}
|
|
||||||
className={`aspect-square rounded-full text-sm font-medium flex items-center justify-center transition-all
|
|
||||||
${
|
|
||||||
!day
|
|
||||||
? ""
|
|
||||||
: "hover:bg-gray-100"
|
|
||||||
}
|
|
||||||
${
|
|
||||||
isSelected(day)
|
|
||||||
? "bg-primary text-white hover:bg-primary"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
isToday(day) &&
|
|
||||||
!isSelected(day)
|
|
||||||
? "bg-primary/10 text-primary font-bold hover:bg-primary/20"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
day &&
|
|
||||||
!isSelected(day) &&
|
|
||||||
!isToday(day) &&
|
|
||||||
dayOfWeek === 0
|
|
||||||
? "text-red-500"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
day &&
|
|
||||||
!isSelected(day) &&
|
|
||||||
!isToday(day) &&
|
|
||||||
dayOfWeek === 6
|
|
||||||
? "text-blue-500"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
day &&
|
|
||||||
!isSelected(day) &&
|
|
||||||
!isToday(day) &&
|
|
||||||
dayOfWeek > 0 &&
|
|
||||||
dayOfWeek < 6
|
|
||||||
? "text-gray-700"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{day}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 숫자 피커 컬럼 컴포넌트 (Vue 컴포넌트를 React로 변환)
|
// 숫자 피커 컬럼 컴포넌트 (Vue 컴포넌트를 React로 변환)
|
||||||
function NumberPicker({ items, value, onChange }) {
|
function NumberPicker({ items, value, onChange }) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue