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("-"); 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 (
{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 getDayOfWeek = (dateStr) => { if (!dateStr) return ''; const days = ['일', '월', '화', '수', '목', '금', '토']; const date = new Date(dateStr); return days[date.getDay()]; }; // 카테고리 로드 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) { // 기존 이미지를 formData.images에 저장 (id 포함) setFormData(prev => ({ ...prev, title: data.title || "", isRange: data.is_range || false, startDate: data.date?.split("T")[0] || "", endDate: data.end_date?.split("T")[0] || "", startTime: data.time?.slice(0, 5) || "", endTime: data.end_time?.slice(0, 5) || "", category: data.category_id || 1, description: data.description || "", url: data.source_url || "", sourceName: data.source_name || "", members: data.members?.map((m) => m.id) || [], images: data.images.map(img => ({ id: img.id, url: img.image_url })), locationName: data.location_name || "", locationAddress: data.location_address || "", locationDetail: data.location_detail || "", locationLat: data.location_lat || null, locationLng: data.location_lng || null, })); 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 deletedImage = formData.images[deleteTargetIndex]; const newImages = formData.images.filter( (_, i) => i !== deleteTargetIndex ); const newPreviews = imagePreviews.filter( (_, i) => i !== deleteTargetIndex ); setFormData({ ...formData, images: newImages }); setImagePreviews(newPreviews); // 기존 이미지(서버에 있는)를 삭제한 경우 existingImageIds에서도 제거 if (deletedImage && deletedImage.id) { setExistingImageIds(prev => prev.filter(id => id !== deletedImage.id)); } } 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("adminToken"); 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) => ( ))}
{/* 장소 */}
{/* 검색으로 입력된 경우(좌표가 있는 경우) 초기화 버튼 표시 */} {formData.locationLat && formData.locationLng && ( )}
{/* 장소 이름 + 검색 버튼 */}
setFormData({ ...formData, locationName: e.target.value, }) } placeholder="장소 이름 (예: 잠실실내체육관)" disabled={!!(formData.locationLat && formData.locationLng)} 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 ${formData.locationLat && formData.locationLng ? 'bg-gray-50 text-gray-700 cursor-not-allowed' : ''}`} />
{/* 주소 */} setFormData({ ...formData, locationAddress: e.target.value, }) } placeholder="주소 (예: 서울특별시 송파구 올림픽로 25)" disabled={!!(formData.locationLat && formData.locationLng)} 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 ${formData.locationLat && formData.locationLng ? 'bg-gray-50 text-gray-700 cursor-not-allowed' : ''}`} /> {/* 상세주소 */} 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" />
{/* 설명 */}