import { useState, useEffect, useRef } from 'react'; import { useNavigate, Link, useParams } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { LogOut, Home, ChevronRight, Calendar, Save, X, Upload, Link as LinkIcon, ChevronLeft, ChevronDown, Clock, Image, Users, Check, Plus, MapPin, Settings, AlertTriangle, Trash2, Search } from 'lucide-react'; import Toast from '../../../components/Toast'; import Lightbox from '../../../components/common/Lightbox'; // 커스텀 데이트픽커 컴포넌트 (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('-'); 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 (
{isOpen && (
{viewMode === 'years' && (
년도
{years.map((y) => ( ))}
{months.map((m, i) => ( ))}
)} {viewMode === 'months' && (
월 선택
{months.map((m, i) => ( ))}
)} {viewMode === 'days' && (
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
{d}
))}
{days.map((day, i) => { const dayOfWeek = i % 7; return ( ); })}
)}
)}
); } // 숫자 피커 컬럼 컴포넌트 (Vue 컴포넌트를 React로 변환) function NumberPicker({ items, value, onChange }) { const ITEM_HEIGHT = 40; const containerRef = useRef(null); const [offset, setOffset] = useState(0); const offsetRef = useRef(0); // 드래그용 ref const touchStartY = useRef(0); const startOffset = useRef(0); const isScrolling = useRef(false); // offset 변경시 ref도 업데이트 useEffect(() => { offsetRef.current = offset; }, [offset]); // 초기 위치 설정 useEffect(() => { if (value !== null && value !== undefined) { const index = items.indexOf(value); if (index !== -1) { const newOffset = -index * ITEM_HEIGHT; setOffset(newOffset); offsetRef.current = newOffset; } } }, []); // 값 변경시 위치 업데이트 useEffect(() => { const index = items.indexOf(value); if (index !== -1) { const targetOffset = -index * ITEM_HEIGHT; if (Math.abs(offset - targetOffset) > 1) { setOffset(targetOffset); offsetRef.current = targetOffset; } } }, [value, items]); const centerOffset = ITEM_HEIGHT; // 중앙 위치 오프셋 // 아이템이 중앙에 있는지 확인 const isItemInCenter = (item) => { const itemIndex = items.indexOf(item); const itemPosition = -itemIndex * ITEM_HEIGHT; const tolerance = ITEM_HEIGHT / 2; return Math.abs(offset - itemPosition) < tolerance; }; // 오프셋 업데이트 (경계 제한) const updateOffset = (newOffset) => { const maxOffset = 0; const minOffset = -(items.length - 1) * ITEM_HEIGHT; return Math.min(maxOffset, Math.max(minOffset, newOffset)); }; // 중앙 아이템 업데이트 const updateCenterItem = (currentOffset) => { const centerIndex = Math.round(-currentOffset / ITEM_HEIGHT); if (centerIndex >= 0 && centerIndex < items.length) { const centerItem = items[centerIndex]; if (value !== centerItem) { onChange(centerItem); } } }; // 가장 가까운 아이템에 스냅 const snapToClosestItem = (currentOffset) => { const targetOffset = Math.round(currentOffset / ITEM_HEIGHT) * ITEM_HEIGHT; setOffset(targetOffset); offsetRef.current = targetOffset; updateCenterItem(targetOffset); }; // 터치 시작 const handleTouchStart = (e) => { e.stopPropagation(); touchStartY.current = e.touches[0].clientY; startOffset.current = offsetRef.current; }; // 터치 이동 const handleTouchMove = (e) => { e.stopPropagation(); const touchY = e.touches[0].clientY; const deltaY = touchY - touchStartY.current; const newOffset = updateOffset(startOffset.current + deltaY); setOffset(newOffset); offsetRef.current = newOffset; }; // 터치 종료 const handleTouchEnd = (e) => { e.stopPropagation(); snapToClosestItem(offsetRef.current); }; // 마우스 휠 - 바깥 스크롤 방지 const handleWheel = (e) => { e.preventDefault(); e.stopPropagation(); if (isScrolling.current) return; isScrolling.current = true; const newOffset = updateOffset(offsetRef.current - Math.sign(e.deltaY) * ITEM_HEIGHT); setOffset(newOffset); offsetRef.current = newOffset; snapToClosestItem(newOffset); setTimeout(() => { isScrolling.current = false; }, 50); }; // 마우스 드래그 const handleMouseDown = (e) => { e.preventDefault(); e.stopPropagation(); touchStartY.current = e.clientY; startOffset.current = offsetRef.current; const handleMouseMove = (moveEvent) => { moveEvent.preventDefault(); const deltaY = moveEvent.clientY - touchStartY.current; const newOffset = updateOffset(startOffset.current + deltaY); setOffset(newOffset); offsetRef.current = newOffset; }; const handleMouseUp = () => { snapToClosestItem(offsetRef.current); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; // wheel 이벤트 passive false로 등록 useEffect(() => { const container = containerRef.current; if (container) { container.addEventListener('wheel', handleWheel, { passive: false }); return () => container.removeEventListener('wheel', handleWheel); } }, []); return (
{/* 중앙 선택 영역 */}
{/* 피커 내부 */}
{items.map((item) => (
{item}
))}
); } // 커스텀 시간 피커 (Vue 컴포넌트 기반) function CustomTimePicker({ value, onChange, placeholder = '시간 선택' }) { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); // 현재 값 파싱 const parseValue = () => { if (!value) return { hour: '12', minute: '00', period: '오후' }; const [h, m] = value.split(':'); const hour = parseInt(h); const isPM = hour >= 12; const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; return { hour: String(hour12).padStart(2, '0'), minute: m, period: isPM ? '오후' : '오전' }; }; const parsed = parseValue(); const [selectedHour, setSelectedHour] = useState(parsed.hour); const [selectedMinute, setSelectedMinute] = useState(parsed.minute); const [selectedPeriod, setSelectedPeriod] = useState(parsed.period); // 외부 클릭 시 닫기 useEffect(() => { const handleClickOutside = (e) => { if (ref.current && !ref.current.contains(e.target)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // 피커 열릴 때 현재 값으로 초기화 useEffect(() => { if (isOpen) { const parsed = parseValue(); setSelectedHour(parsed.hour); setSelectedMinute(parsed.minute); setSelectedPeriod(parsed.period); } }, [isOpen, value]); // 시간 확정 const handleSave = () => { let hour = parseInt(selectedHour); if (selectedPeriod === '오후' && hour !== 12) hour += 12; if (selectedPeriod === '오전' && hour === 12) hour = 0; const timeStr = `${String(hour).padStart(2, '0')}:${selectedMinute}`; onChange(timeStr); setIsOpen(false); }; // 취소 const handleCancel = () => { setIsOpen(false); }; // 초기화 const handleClear = () => { onChange(''); setIsOpen(false); }; // 표시용 포맷 const displayValue = () => { if (!value) return placeholder; const [h, m] = value.split(':'); const hour = parseInt(h); const isPM = hour >= 12; const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; return `${isPM ? '오후' : '오전'} ${hour12}:${m}`; }; // 피커 아이템 데이터 const periods = ['오전', '오후']; const hours = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; const minutes = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0')); return (
{isOpen && ( {/* 피커 영역 */}
{/* 오전/오후 (맨 앞) */} {/* 시간 */} : {/* 분 */}
{/* 푸터 버튼 */}
)}
); } function AdminScheduleForm() { const navigate = useNavigate(); const { id } = useParams(); const isEditMode = !!id; const [user, setUser] = useState(null); const [toast, setToast] = useState(null); const [loading, setLoading] = useState(false); const [members, setMembers] = useState([]); // 폼 데이터 (날짜/시간 범위 지원) const [formData, setFormData] = useState({ title: '', startDate: '', endDate: '', startTime: '', endTime: '', isRange: false, // 범위 설정 여부 category: '', description: '', url: '', sourceName: '', members: [], images: [], // 장소 정보 locationName: '', // 장소 이름 locationAddress: '', // 주소 locationDetail: '', // 상세주소 (예: 3관, N열 등) locationLat: null, // 위도 locationLng: null, // 경도 }); // 이미지 미리보기 const [imagePreviews, setImagePreviews] = useState([]); // 라이트박스 상태 const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxIndex, setLightboxIndex] = useState(0); // 삭제 다이얼로그 상태 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTargetIndex, setDeleteTargetIndex] = useState(null); // 카테고리 목록 (API에서 로드) const [categories, setCategories] = useState([]); // 저장 중 상태 const [saving, setSaving] = useState(false); // 장소 검색 관련 상태 const [locationDialogOpen, setLocationDialogOpen] = useState(false); const [locationSearch, setLocationSearch] = useState(''); const [locationResults, setLocationResults] = useState([]); const [locationSearching, setLocationSearching] = useState(false); // 수정 모드용 기존 이미지 ID 추적 const [existingImageIds, setExistingImageIds] = useState([]); // 카테고리 색상 맵핑 const colorMap = { blue: 'bg-blue-500', green: 'bg-green-500', purple: 'bg-purple-500', red: 'bg-red-500', pink: 'bg-pink-500', yellow: 'bg-yellow-500', orange: 'bg-orange-500', gray: 'bg-gray-500', cyan: 'bg-cyan-500', indigo: 'bg-indigo-500', }; // 색상 스타일 (기본 색상 또는 커스텀 HEX) const getColorStyle = (color) => { if (!color) return { className: 'bg-gray-500' }; if (color.startsWith('#')) { return { style: { backgroundColor: color } }; } return { className: colorMap[color] || 'bg-gray-500' }; }; // 카테고리 색상 const getCategoryColor = (categoryId) => { const cat = categories.find(c => c.id === categoryId); if (cat && colorMap[cat.color]) { return colorMap[cat.color]; } return 'bg-gray-500'; }; // 카테고리 로드 const fetchCategories = async () => { try { const res = await fetch('/api/admin/schedule-categories'); const data = await res.json(); setCategories(data); // 첫 번째 카테고리를 기본값으로 설정 if (data.length > 0 && !formData.category) { setFormData(prev => ({ ...prev, category: data[0].id })); } } catch (error) { console.error('카테고리 로드 오류:', error); } }; // Toast 자동 숨김 useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(null), 3000); return () => clearTimeout(timer); } }, [toast]); useEffect(() => { const token = localStorage.getItem('adminToken'); const userData = localStorage.getItem('adminUser'); if (!token || !userData) { navigate('/admin'); return; } setUser(JSON.parse(userData)); fetchMembers(); fetchCategories(); // 수정 모드일 경우 기존 데이터 로드 if (isEditMode && id) { fetchSchedule(); } }, [navigate, isEditMode, id]); // 기존 일정 데이터 로드 (수정 모드) const fetchSchedule = async () => { setLoading(true); try { const token = localStorage.getItem('adminToken'); const res = await fetch(`/api/admin/schedules/${id}`, { headers: { 'Authorization': `Bearer ${token}`, }, }); if (!res.ok) { throw new Error('일정을 찾을 수 없습니다.'); } const data = await res.json(); // 폼 데이터 설정 setFormData({ title: data.title || '', startDate: data.date ? new Date(data.date).toISOString().split('T')[0] : '', endDate: data.end_date ? new Date(data.end_date).toISOString().split('T')[0] : '', startTime: data.time?.slice(0, 5) || '', endTime: data.end_time?.slice(0, 5) || '', isRange: !!data.end_date, category: data.category_id || '', description: data.description || '', url: data.source_url || '', sourceName: data.source_name || '', members: data.members?.map(m => m.id) || [], images: [], locationName: data.location_name || '', locationAddress: data.location_address || '', locationDetail: data.location_detail || '', locationLat: data.location_lat || null, locationLng: data.location_lng || null, }); // 기존 이미지 설정 if (data.images && data.images.length > 0) { setImagePreviews(data.images.map(img => img.image_url)); setExistingImageIds(data.images.map(img => img.id)); } } catch (error) { console.error('일정 로드 오류:', error); setToast({ type: 'error', message: error.message || '일정을 불러오는 중 오류가 발생했습니다.' }); navigate('/admin/schedule'); } finally { setLoading(false); } }; const fetchMembers = async () => { try { const res = await fetch('/api/members'); const data = await res.json(); setMembers(data.filter(m => !m.is_former)); } catch (error) { console.error('멤버 로드 오류:', error); } }; const handleLogout = () => { localStorage.removeItem('adminToken'); localStorage.removeItem('adminUser'); navigate('/admin'); }; // 멤버 토글 const toggleMember = (memberId) => { const newMembers = formData.members.includes(memberId) ? formData.members.filter(id => id !== memberId) : [...formData.members, memberId]; setFormData({ ...formData, members: newMembers }); }; // 전체 선택/해제 const toggleAllMembers = () => { if (formData.members.length === members.length) { setFormData({ ...formData, members: [] }); } else { setFormData({ ...formData, members: members.map(m => m.id) }); } }; // 다중 이미지 업로드 const handleImagesUpload = (e) => { const files = Array.from(e.target.files); // 파일을 {file: File} 형태로 저장 (제출 시 image.file로 접근하기 위함) const newImageObjects = files.map(file => ({ file })); const newImages = [...formData.images, ...newImageObjects]; setFormData({ ...formData, images: newImages }); files.forEach(file => { const reader = new FileReader(); reader.onloadend = () => { setImagePreviews(prev => [...prev, reader.result]); }; reader.readAsDataURL(file); }); }; // 이미지 삭제 다이얼로그 열기 const openDeleteDialog = (index) => { setDeleteTargetIndex(index); setDeleteDialogOpen(true); }; // 이미지 삭제 확인 const confirmDeleteImage = () => { if (deleteTargetIndex !== null) { const newImages = formData.images.filter((_, i) => i !== deleteTargetIndex); const newPreviews = imagePreviews.filter((_, i) => i !== deleteTargetIndex); setFormData({ ...formData, images: newImages }); setImagePreviews(newPreviews); } setDeleteDialogOpen(false); setDeleteTargetIndex(null); }; // 라이트박스 열기 const openLightbox = (index) => { setLightboxIndex(index); setLightboxOpen(true); }; // 드래그 앤 드롭 상태 const [draggedIndex, setDraggedIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); // 드래그 시작 const handleDragStart = (e, index) => { setDraggedIndex(index); e.dataTransfer.effectAllowed = 'move'; // 드래그 이미지 설정 e.dataTransfer.setData('text/plain', index); }; // 드래그 오버 const handleDragOver = (e, index) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (dragOverIndex !== index) { setDragOverIndex(index); } }; // 드래그 종료 const handleDragEnd = () => { setDraggedIndex(null); setDragOverIndex(null); }; // 드롭 - 이미지 순서 변경 const handleDrop = (e, dropIndex) => { e.preventDefault(); if (draggedIndex === null || draggedIndex === dropIndex) { handleDragEnd(); return; } // 새 배열 생성 const newPreviews = [...imagePreviews]; const newImages = [...formData.images]; // 드래그된 아이템 제거 후 새 위치에 삽입 const [movedPreview] = newPreviews.splice(draggedIndex, 1); const [movedImage] = newImages.splice(draggedIndex, 1); newPreviews.splice(dropIndex, 0, movedPreview); newImages.splice(dropIndex, 0, movedImage); setImagePreviews(newPreviews); setFormData({ ...formData, images: newImages }); handleDragEnd(); }; // 카카오 장소 검색 API 호출 (엔터 키로 검색) const handleLocationSearch = async () => { if (!locationSearch.trim()) { setLocationResults([]); return; } setLocationSearching(true); try { const token = localStorage.getItem('token'); const response = await fetch( `/api/admin/kakao/places?query=${encodeURIComponent(locationSearch)}`, { headers: { 'Authorization': `Bearer ${token}`, } } ); if (response.ok) { const data = await response.json(); setLocationResults(data.documents || []); } } catch (error) { console.error('장소 검색 오류:', error); } finally { setLocationSearching(false); } }; // 장소 선택 const selectLocation = (place) => { setFormData({ ...formData, locationName: place.place_name, locationAddress: place.road_address_name || place.address_name, locationLat: parseFloat(place.y), locationLng: parseFloat(place.x) }); setLocationDialogOpen(false); setLocationSearch(''); setLocationResults([]); }; // 폼 제출 const handleSubmit = async (e) => { e.preventDefault(); // 유효성 검사 if (!formData.title.trim()) { setToast({ type: 'error', message: '제목을 입력해주세요.' }); return; } // 날짜 검증: 단일/기간 모드 모두 startDate를 사용함 if (!formData.startDate) { setToast({ type: 'error', message: '날짜를 선택해주세요.' }); return; } if (!formData.category) { setToast({ type: 'error', message: '카테고리를 선택해주세요.' }); return; } setSaving(true); try { const token = localStorage.getItem('adminToken'); // FormData 생성 const submitData = new FormData(); // JSON 데이터 - 항상 startDate를 date로 사용 (UI에서 단일/기간 모드 모두 startDate 사용) const jsonData = { title: formData.title.trim(), date: formData.startDate, time: formData.startTime || null, endDate: formData.isRange ? formData.endDate : null, endTime: formData.isRange ? formData.endTime : null, isRange: formData.isRange, category: formData.category, description: formData.description.trim() || null, url: formData.url.trim() || null, sourceName: formData.sourceName.trim() || null, members: formData.members, locationName: formData.locationName.trim() || null, locationAddress: formData.locationAddress.trim() || null, locationDetail: formData.locationDetail?.trim() || null, locationLat: formData.locationLat, locationLng: formData.locationLng, }; // 수정 모드일 경우 유지할 기존 이미지 ID 추가 if (isEditMode) { jsonData.existingImages = existingImageIds; } submitData.append('data', JSON.stringify(jsonData)); // 이미지 파일 추가 (새로 추가된 이미지만) for (const image of formData.images) { if (image.file) { submitData.append('images', image.file); } } // 수정 모드면 PUT, 생성 모드면 POST const url = isEditMode ? `/api/admin/schedules/${id}` : '/api/admin/schedules'; const method = isEditMode ? 'PUT' : 'POST'; const response = await fetch(url, { method, headers: { 'Authorization': `Bearer ${token}`, }, body: submitData, }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || (isEditMode ? '일정 수정에 실패했습니다.' : '일정 생성에 실패했습니다.')); } // 성공 메시지를 sessionStorage에 저장하고 목록 페이지로 이동 sessionStorage.setItem('scheduleToast', JSON.stringify({ type: 'success', message: isEditMode ? '일정이 수정되었습니다.' : '일정이 추가되었습니다.' })); navigate('/admin/schedule'); } catch (error) { console.error('일정 저장 오류:', error); setToast({ type: 'error', message: error.message || '일정 저장 중 오류가 발생했습니다.' }); } finally { setSaving(false); } }; return (
setToast(null)} /> {/* 삭제 확인 다이얼로그 */} {deleteDialogOpen && ( setDeleteDialogOpen(false)} > e.stopPropagation()} >

이미지 삭제

이 이미지를 삭제하시겠습니까?
이 작업은 되돌릴 수 없습니다.

)}
{/* 장소 검색 다이얼로그 */} {locationDialogOpen && ( { setLocationDialogOpen(false); setLocationSearch(''); setLocationResults([]); }} > e.stopPropagation()} >

장소 검색

{/* 검색 입력 */}
setLocationSearch(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleLocationSearch(); } }} placeholder="장소명을 입력하세요" className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" autoFocus />
{/* 검색 결과 */}
{locationResults.length > 0 ? (
{locationResults.map((place, index) => ( ))}
) : locationSearch && !locationSearching ? (

검색어를 입력하고 검색 버튼을 눌러주세요

) : (

장소명을 입력하고 검색해주세요

)}
)}
{/* 이미지 라이트박스 - 공통 컴포넌트 사용 */} setLightboxOpen(false)} onIndexChange={setLightboxIndex} /> {/* 헤더 */}
fromis_9 Admin
안녕하세요, {user?.username}
{/* 메인 콘텐츠 */}
{/* 브레드크럼 */}
일정 관리 {isEditMode ? '일정 수정' : '일정 추가'}
{/* 타이틀 */}

{isEditMode ? '일정 수정' : '일정 추가'}

새로운 일정을 등록합니다

{/* 폼 */}
{/* 기본 정보 카드 */}

기본 정보

{/* 제목 */}
setFormData({ ...formData, title: e.target.value })} placeholder="일정 제목을 입력하세요" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required />
{/* 범위 설정 토글 */}
기간 설정 (시작~종료)
{/* 날짜 + 시간 */} {formData.isRange ? ( // 범위 설정 모드
{/* 시작 */}
setFormData({ ...formData, startDate: date })} placeholder="시작 날짜 선택" />
setFormData({ ...formData, startTime: time })} placeholder="시작 시간 선택" />
{/* 종료 */}
setFormData({ ...formData, endDate: date })} placeholder="종료 날짜 선택" />
setFormData({ ...formData, endTime: time })} placeholder="종료 시간 선택" />
) : ( // 단일 날짜 모드
setFormData({ ...formData, startDate: date })} />
setFormData({ ...formData, startTime: time })} />
)} {/* 카테고리 */}
카테고리 관리
{categories.map(category => ( ))}
{/* 장소 */}
{/* 장소 이름 + 검색 버튼 */}
setFormData({ ...formData, locationName: e.target.value })} placeholder="장소 이름 (예: 잠실실내체육관)" className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" />
{/* 주소 */} setFormData({ ...formData, locationAddress: e.target.value })} placeholder="주소 (예: 서울특별시 송파구 올림픽로 25)" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" /> {/* 상세주소 */} setFormData({ ...formData, locationDetail: e.target.value })} placeholder="상세주소 (예: 3관 A구역, N열 등)" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" />
{/* 설명 */}