diff --git a/docs/admin-migration.md b/docs/admin-migration.md index 80cc240..3f280b5 100644 --- a/docs/admin-migration.md +++ b/docs/admin-migration.md @@ -503,6 +503,11 @@ frontend-temp/src/ - [x] AdminScheduleCategory 마이그레이션 - [x] AdminScheduleDict 마이그레이션 - [x] AdminScheduleBots 마이그레이션 +- [x] 카테고리별 폼 분기 페이지 마이그레이션 (form/index.jsx) +- [x] YouTubeForm 마이그레이션 (form/YouTubeForm.jsx) +- [x] XForm 마이그레이션 (form/XForm.jsx) +- [x] CategorySelector 마이그레이션 (form/components/CategorySelector.jsx) +- [x] YouTubeEditForm 마이그레이션 (edit/YouTubeEditForm.jsx) ### 6단계: 검증 - [ ] 공개 페이지 동작 확인 (PC/Mobile) diff --git a/frontend-temp/src/App.jsx b/frontend-temp/src/App.jsx index 22939ae..576e22f 100644 --- a/frontend-temp/src/App.jsx +++ b/frontend-temp/src/App.jsx @@ -33,6 +33,8 @@ import AdminAlbumForm from '@/pages/pc/admin/albums/AlbumForm'; import AdminAlbumPhotos from '@/pages/pc/admin/albums/AlbumPhotos'; import AdminSchedules from '@/pages/pc/admin/schedules/Schedules'; import AdminScheduleForm from '@/pages/pc/admin/schedules/ScheduleForm'; +import AdminScheduleFormPage from '@/pages/pc/admin/schedules/form'; +import AdminYouTubeEditForm from '@/pages/pc/admin/schedules/edit/YouTubeEditForm'; import AdminScheduleCategory from '@/pages/pc/admin/schedules/ScheduleCategory'; import AdminScheduleDict from '@/pages/pc/admin/schedules/ScheduleDict'; import AdminScheduleBots from '@/pages/pc/admin/schedules/ScheduleBots'; @@ -83,8 +85,10 @@ function App() { } /> } /> } /> - } /> + } /> + } /> } /> + } /> } /> } /> } /> diff --git a/frontend-temp/src/pages/pc/admin/schedules/edit/YouTubeEditForm.jsx b/frontend-temp/src/pages/pc/admin/schedules/edit/YouTubeEditForm.jsx new file mode 100644 index 0000000..434dcdf --- /dev/null +++ b/frontend-temp/src/pages/pc/admin/schedules/edit/YouTubeEditForm.jsx @@ -0,0 +1,539 @@ +import { useState, useEffect } from "react"; +import { useNavigate, useParams, Link } from "react-router-dom"; +import { motion } from "framer-motion"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + Youtube, + Loader2, + Save, + ExternalLink, + Home, + ChevronRight, + Users, + Check, +} from "lucide-react"; +import AdminLayout from "@/components/pc/admin/Layout"; +import Toast from "@/components/common/Toast"; +import { useAdminAuth } from "@/hooks/pc/admin"; +import { useToast } from "@/hooks/common"; + +// 애니메이션 variants +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, +}; + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4, ease: "easeOut" }, + }, +}; + +/** + * YouTube 일정 수정 폼 + * - 기존 일정 데이터 로드 + * - 멤버 선택 수정 + */ +function YouTubeEditForm() { + const navigate = useNavigate(); + const { id } = useParams(); + const queryClient = useQueryClient(); + const { user, isAuthenticated } = useAdminAuth(); + const { toast, setToast } = useToast(); + + const [saving, setSaving] = useState(false); + const [selectedMembers, setSelectedMembers] = useState([]); + const [videoType, setVideoType] = useState("video"); + const [isInitialized, setIsInitialized] = useState(false); + + // 일정 데이터 로드 + const { data: schedule, isLoading: scheduleLoading } = useQuery({ + queryKey: ["schedule", id], + queryFn: async () => { + const token = localStorage.getItem("adminToken"); + const res = await fetch(`/api/schedules/${id}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) throw new Error("일정을 찾을 수 없습니다."); + return res.json(); + }, + enabled: isAuthenticated && !!id, + }); + + // 멤버 목록 로드 + const { data: membersData = [], isLoading: membersLoading } = useQuery({ + queryKey: ["members"], + queryFn: async () => { + const res = await fetch("/api/members"); + if (!res.ok) throw new Error("멤버 목록을 불러올 수 없습니다."); + return res.json(); + }, + enabled: isAuthenticated, + }); + + // 현재 멤버만 필터링 + const members = membersData.filter((m) => !m.is_former); + + // 일정 데이터 로드 후 초기값 설정 + useEffect(() => { + if (schedule && !isInitialized) { + // YouTube 일정인지 확인 + if (schedule.category?.id !== 2) { + setToast({ type: "error", message: "YouTube 일정이 아닙니다." }); + navigate("/admin/schedule"); + return; + } + setSelectedMembers(schedule.members?.map((m) => m.id) || []); + setVideoType(schedule.videoType || "video"); + setIsInitialized(true); + } + }, [schedule, isInitialized, navigate, setToast]); + + const loading = scheduleLoading || membersLoading; + + // 멤버 토글 + const toggleMember = (memberId) => { + setSelectedMembers((prev) => + prev.includes(memberId) + ? prev.filter((id) => id !== memberId) + : [...prev, memberId] + ); + }; + + // 전체 선택/해제 + const toggleAllMembers = () => { + if (selectedMembers.length === members.length) { + setSelectedMembers([]); + } else { + setSelectedMembers(members.map((m) => m.id)); + } + }; + + // 폼 제출 + const handleSubmit = async (e) => { + e.preventDefault(); + setSaving(true); + + try { + const token = localStorage.getItem("adminToken"); + + const response = await fetch(`/api/admin/youtube/schedule/${id}`, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + memberIds: selectedMembers, + videoType, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "수정에 실패했습니다."); + } + + // 캐시 무효화 + queryClient.invalidateQueries({ queryKey: ["schedule", id] }); + + sessionStorage.setItem( + "scheduleToast", + JSON.stringify({ + type: "success", + message: "YouTube 일정이 수정되었습니다.", + }) + ); + navigate("/admin/schedule"); + } catch (err) { + setToast({ + type: "error", + message: err.message, + }); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( + +
+
+
+ + ); + } + + if (!schedule) { + return null; + } + + const videoUrl = videoType === "shorts" + ? `https://www.youtube.com/shorts/${schedule.videoId}` + : `https://www.youtube.com/watch?v=${schedule.videoId}`; + + // 날짜 포맷팅 함수 (datetime 문자열 파싱) + const formatDatetime = (datetime) => { + if (!datetime) return ""; + // datetime: "2025-01-20T14:00:00" 또는 "2025-01-20" + const dateStr = datetime.split("T")[0]; + const timeStr = datetime.includes("T") ? datetime.split("T")[1]?.slice(0, 5) : ""; + const date = new Date(dateStr); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const dayNames = ["일", "월", "화", "수", "목", "금", "토"]; + const dayName = dayNames[date.getDay()]; + return `${year}년 ${month}월 ${day}일 (${dayName}) ${timeStr}`; + }; + + return ( + + setToast(null)} /> + + + {/* 브레드크럼 */} + + + + + + + 일정 관리 + + + YouTube 일정 수정 + + +
+ {videoType === "shorts" ? ( + /* Shorts 레이아웃: 영상(왼쪽) + 정보/멤버(오른쪽) */ + +
+ {/* 왼쪽: 영상 */} +
+
+
+