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 레이아웃: 영상(왼쪽) + 정보/멤버(오른쪽) */ + + + {/* 왼쪽: 영상 */} + + + + + + + + + {/* 오른쪽: 정보 + 멤버 선택 */} + + {/* 영상 정보 */} + + + 영상 정보 + + + + {schedule.title} + + + + + 채널:{" "} + {schedule.channelName} + + + 업로드:{" "} + {formatDatetime(schedule.datetime)} + + + + + + 유형: + setVideoType("video")} + className="px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-500 hover:bg-gray-200 transition-colors" + > + Video + + setVideoType("shorts")} + className="px-3 py-1 rounded-full text-xs font-medium bg-pink-500 text-white transition-colors" + > + Shorts + + + + + YouTube + + + + {/* 멤버 선택 */} + + + + + 출연 멤버 + + + {selectedMembers.length === members.length + ? "전체 해제" + : "전체 선택"} + + + + + {members.map((member) => { + const isSelected = selectedMembers.includes(member.id); + return ( + toggleMember(member.id)} + className={`relative rounded-xl overflow-hidden transition-all ${ + isSelected + ? "ring-2 ring-primary ring-offset-2" + : "hover:opacity-80" + }`} + > + + {member.image_url ? ( + + ) : ( + + + + )} + + + + {member.name} + + + {isSelected && ( + + + + )} + + ); + })} + + + + + + ) : ( + /* Video 레이아웃: 기존 세로 배치 */ + <> + + + + 영상 정보 + + + + + + + + + + + {schedule.title} + + + + + 채널:{" "} + {schedule.channelName} + + + 업로드:{" "} + {formatDatetime(schedule.datetime)} + + + 유형: + setVideoType("video")} + className="px-3 py-1 rounded-full text-xs font-medium bg-blue-500 text-white transition-colors" + > + Video + + setVideoType("shorts")} + className="px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-500 hover:bg-gray-200 transition-colors" + > + Shorts + + + + + + + YouTube에서 보기 + + + + + + + + + 출연 멤버 + + + {selectedMembers.length === members.length + ? "전체 해제" + : "전체 선택"} + + + + + {members.map((member) => { + const isSelected = selectedMembers.includes(member.id); + return ( + toggleMember(member.id)} + className={`relative rounded-xl overflow-hidden transition-all ${ + isSelected + ? "ring-2 ring-primary ring-offset-2" + : "hover:opacity-80" + }`} + > + + {member.image_url ? ( + + ) : ( + + + + )} + + + + {member.name} + + + {isSelected && ( + + + + )} + + ); + })} + + + > + )} + + {/* 버튼 */} + + navigate("/admin/schedule")} + className="px-6 py-3 text-gray-700 hover:bg-gray-100 rounded-xl transition-colors font-medium" + > + 취소 + + + {saving ? ( + <> + + 저장 중... + > + ) : ( + <> + + 저장하기 + > + )} + + + + + + ); +} + +export default YouTubeEditForm; diff --git a/frontend-temp/src/pages/pc/admin/schedules/form/XForm.jsx b/frontend-temp/src/pages/pc/admin/schedules/form/XForm.jsx new file mode 100644 index 0000000..fc20e17 --- /dev/null +++ b/frontend-temp/src/pages/pc/admin/schedules/form/XForm.jsx @@ -0,0 +1,346 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; +import { + Hash, + Loader2, + Check, + AlertCircle, + Save, + Image as ImageIcon, +} from "lucide-react"; + +import Toast from "@/components/common/Toast"; +import { useToast } from "@/hooks/common"; + +// X 로고 아이콘 +const XLogo = ({ size = 24, className = "" }) => ( + + + +); + +/** + * X(Twitter) 일정 추가 폼 + * - 게시글 ID 입력 시 자동으로 정보 조회 + */ +function XForm() { + const navigate = useNavigate(); + const { toast, setToast } = useToast(); + + const [postId, setPostId] = useState(""); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [postInfo, setPostInfo] = useState(null); + const [error, setError] = useState(null); + + // 게시글 ID 추출 (URL에서도 추출 가능) + const extractPostId = (input) => { + // 숫자만 있으면 그대로 반환 + if (/^\d+$/.test(input.trim())) { + return input.trim(); + } + // URL에서 추출 + const match = input.match(/status\/(\d+)/); + return match ? match[1] : null; + }; + + // X 게시글 정보 조회 + const fetchPostInfo = async () => { + const id = extractPostId(postId); + if (!id) { + setError("게시글 ID 또는 URL을 입력해주세요."); + return; + } + + setLoading(true); + setError(null); + setPostInfo(null); + + try { + const token = localStorage.getItem("adminToken"); + const response = await fetch( + `/api/admin/x/post-info?postId=${id}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "게시글 정보를 가져올 수 없습니다."); + } + + const data = await response.json(); + setPostInfo(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + // 입력 후 엔터 키 + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + fetchPostInfo(); + } + }; + + // 초기화 + const handleReset = () => { + setPostId(""); + setPostInfo(null); + setError(null); + }; + + // 폼 제출 + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!postInfo) { + setError("먼저 게시글 ID를 입력하고 조회해주세요."); + return; + } + + setSaving(true); + + try { + const token = localStorage.getItem("adminToken"); + + const response = await fetch("/api/admin/x/schedule", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + postId: postInfo.postId, + title: postInfo.title, + content: postInfo.text, + imageUrls: postInfo.imageUrls, + date: postInfo.date, + time: postInfo.time, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "일정 저장에 실패했습니다."); + } + + sessionStorage.setItem( + "scheduleToast", + JSON.stringify({ + type: "success", + message: "X 일정이 추가되었습니다.", + }) + ); + navigate("/admin/schedule"); + } catch (err) { + setToast({ + type: "error", + message: err.message, + }); + } finally { + setSaving(false); + } + }; + + return ( + <> + setToast(null)} /> + + + {/* 게시글 ID 입력 */} + + + + X 게시글 + + + + {/* ID 입력 필드 */} + + + 게시글 ID 또는 URL * + + + + + setPostId(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="1234567890 또는 https://x.com/realfromis_9/status/1234567890" + className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent" + disabled={loading || postInfo} + /> + + {!postInfo ? ( + + {loading ? ( + <> + + 조회 중... + > + ) : ( + "조회" + )} + + ) : ( + + 다시 입력 + + )} + + + + {/* 에러 메시지 */} + {error && ( + + + {error} + + )} + + {/* 게시글 정보 미리보기 */} + {postInfo && ( + + + + + 게시글 정보를 가져왔습니다 + + + + {/* 프로필 */} + {postInfo.profile?.displayName && ( + + {postInfo.profile.avatarUrl && ( + + )} + + + {postInfo.profile.displayName} + + @{postInfo.username} + + + )} + + {/* 제목 (첫 문단) */} + + 제목 (자동 추출) + {postInfo.title} + + + {/* 전체 내용 */} + + 전체 내용 + + {postInfo.text} + + + + {/* 이미지 */} + {postInfo.imageUrls?.length > 0 && ( + + + + 이미지 ({postInfo.imageUrls.length}개) + + + {postInfo.imageUrls.map((url, index) => ( + + + + ))} + + + )} + + {/* 날짜/시간 */} + + 게시:{" "} + {postInfo.date} {postInfo.time} + + + )} + + + + {/* 버튼 */} + + navigate("/admin/schedule")} + className="px-6 py-3 text-gray-700 hover:bg-gray-100 rounded-xl transition-colors font-medium" + > + 취소 + + + {saving ? ( + <> + + 저장 중... + > + ) : ( + <> + + 추가하기 + > + )} + + + + > + ); +} + +export default XForm; diff --git a/frontend-temp/src/pages/pc/admin/schedules/form/YouTubeForm.jsx b/frontend-temp/src/pages/pc/admin/schedules/form/YouTubeForm.jsx new file mode 100644 index 0000000..3d6b05a --- /dev/null +++ b/frontend-temp/src/pages/pc/admin/schedules/form/YouTubeForm.jsx @@ -0,0 +1,301 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; +import { + Youtube, + Link as LinkIcon, + Loader2, + Check, + AlertCircle, + Save, +} from "lucide-react"; +import Toast from "@/components/common/Toast"; +import { useToast } from "@/hooks/common"; + +/** + * YouTube 일정 추가 폼 + * - URL 입력 시 자동으로 영상 정보 조회 + * - 조회된 정보로 일정 저장 + */ +function YouTubeForm() { + const navigate = useNavigate(); + const { toast, setToast } = useToast(); + + const [url, setUrl] = useState(""); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [videoInfo, setVideoInfo] = useState(null); + const [error, setError] = useState(null); + + // YouTube URL에서 영상 정보 조회 + const fetchVideoInfo = async () => { + if (!url.trim()) { + setError("YouTube URL을 입력해주세요."); + return; + } + + setLoading(true); + setError(null); + setVideoInfo(null); + + try { + const token = localStorage.getItem("adminToken"); + const response = await fetch( + `/api/admin/youtube/video-info?url=${encodeURIComponent(url)}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "영상 정보를 가져올 수 없습니다."); + } + + const data = await response.json(); + setVideoInfo(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + // URL 입력 후 엔터 키 + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + fetchVideoInfo(); + } + }; + + // 초기화 + const handleReset = () => { + setUrl(""); + setVideoInfo(null); + setError(null); + }; + + // 폼 제출 + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!videoInfo) { + setError("먼저 YouTube URL을 입력하고 조회해주세요."); + return; + } + + setSaving(true); + + try { + const token = localStorage.getItem("adminToken"); + + const response = await fetch("/api/admin/youtube/schedule", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + videoId: videoInfo.videoId, + title: videoInfo.title, + channelId: videoInfo.channelId, + channelName: videoInfo.channelName, + date: videoInfo.date, + time: videoInfo.time, + videoType: videoInfo.videoType, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "일정 저장에 실패했습니다."); + } + + // 성공 메시지를 sessionStorage에 저장하고 목록 페이지로 이동 + sessionStorage.setItem( + "scheduleToast", + JSON.stringify({ + type: "success", + message: "YouTube 일정이 추가되었습니다.", + }) + ); + navigate("/admin/schedule"); + } catch (err) { + setToast({ + type: "error", + message: err.message, + }); + } finally { + setSaving(false); + } + }; + + return ( + <> + setToast(null)} /> + + + {/* YouTube URL 입력 */} + + + + YouTube 영상 + + + + {/* URL 입력 필드 */} + + + YouTube URL * + + + + + setUrl(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="https://www.youtube.com/watch?v=... 또는 https://youtu.be/..." + className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent" + disabled={loading || videoInfo} + /> + + {!videoInfo ? ( + + {loading ? ( + <> + + 조회 중... + > + ) : ( + "조회" + )} + + ) : ( + + 다시 입력 + + )} + + + + {/* 에러 메시지 */} + {error && ( + + + {error} + + )} + + {/* 영상 정보 미리보기 */} + {videoInfo && ( + + + {/* 썸네일 */} + + + + + {/* 정보 */} + + + + + 영상 정보를 가져왔습니다 + + + + + {videoInfo.title} + + + + + 채널:{" "} + {videoInfo.channelName} + + + 업로드:{" "} + {videoInfo.date} {videoInfo.time} + + + 유형:{" "} + + {videoInfo.videoType === "shorts" ? "Shorts" : "Video"} + + + + + + + )} + + + + {/* 버튼 */} + + navigate("/admin/schedule")} + className="px-6 py-3 text-gray-700 hover:bg-gray-100 rounded-xl transition-colors font-medium" + > + 취소 + + + {saving ? ( + <> + + 저장 중... + > + ) : ( + <> + + 추가하기 + > + )} + + + + > + ); +} + +export default YouTubeForm; diff --git a/frontend-temp/src/pages/pc/admin/schedules/form/components/CategorySelector.jsx b/frontend-temp/src/pages/pc/admin/schedules/form/components/CategorySelector.jsx new file mode 100644 index 0000000..c049bf1 --- /dev/null +++ b/frontend-temp/src/pages/pc/admin/schedules/form/components/CategorySelector.jsx @@ -0,0 +1,78 @@ +import { Link } from "react-router-dom"; +import { Settings } from "lucide-react"; + +/** + * 카테고리 선택 컴포넌트 + */ +function CategorySelector({ categories, selectedId, onChange }) { + // 색상 스타일 (기본 색상 또는 커스텀 HEX) + const getColorStyle = (color) => { + 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", + }; + + if (!color) return { className: "bg-gray-500" }; + if (color.startsWith("#")) { + return { style: { backgroundColor: color } }; + } + return { className: colorMap[color] || "bg-gray-500" }; + }; + + return ( + + + 카테고리 선택 + + + 카테고리 관리 + + + + + {categories.map((category) => { + const colorStyle = getColorStyle(category.color); + const isSelected = selectedId === category.id; + + return ( + onChange(category.id)} + className={`flex items-center justify-center gap-2 px-4 py-4 rounded-xl border-2 transition-all ${ + isSelected + ? "border-primary bg-primary/5 shadow-sm" + : "border-gray-200 hover:border-gray-300 hover:bg-gray-50" + }`} + > + + + {category.name} + + + ); + })} + + + ); +} + +export default CategorySelector; diff --git a/frontend-temp/src/pages/pc/admin/schedules/form/index.jsx b/frontend-temp/src/pages/pc/admin/schedules/form/index.jsx new file mode 100644 index 0000000..31e72d9 --- /dev/null +++ b/frontend-temp/src/pages/pc/admin/schedules/form/index.jsx @@ -0,0 +1,181 @@ +import { useState, useEffect } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { motion, AnimatePresence } from "framer-motion"; +import { Home, ChevronRight } from "lucide-react"; +import AdminLayout from "@/components/pc/admin/Layout"; +import { useAdminAuth } from "@/hooks/pc/admin"; +import * as categoriesApi from "@/api/pc/admin/categories"; +import CategorySelector from "./components/CategorySelector"; +import YouTubeForm from "./YouTubeForm"; +import XForm from "./XForm"; + +// 애니메이션 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" }, + }, +}; + +// 카테고리 ID 상수 +const CATEGORY_IDS = { + YOUTUBE: 2, + X: 3, +}; + +/** + * 일정 추가 페이지 (카테고리별 폼 분기) + */ +function ScheduleFormPage() { + const navigate = useNavigate(); + const { user, isAuthenticated } = useAdminAuth(); + + const [categories, setCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); + const [loading, setLoading] = useState(true); + const [isInitialLoad, setIsInitialLoad] = useState(true); + + // 카테고리 로드 + useEffect(() => { + if (!isAuthenticated) return; + + const fetchCategories = async () => { + try { + const data = await categoriesApi.getCategories(); + setCategories(data); + // 첫 번째 카테고리를 기본값으로 + if (data.length > 0) { + setSelectedCategory(data[0].id); + } + } catch (error) { + console.error("카테고리 로드 오류:", error); + } finally { + setLoading(false); + } + }; + + fetchCategories(); + }, [isAuthenticated]); + + // 카테고리에 따른 폼 렌더링 + const renderForm = () => { + switch (selectedCategory) { + case CATEGORY_IDS.YOUTUBE: + return ; + + case CATEGORY_IDS.X: + return ; + + // 다른 카테고리는 기존 폼으로 리다이렉트 + default: + return ( + + + 이 카테고리는 아직 전용 폼이 없습니다. + + navigate(`/admin/schedule/new-legacy?category=${selectedCategory}`)} + className="px-6 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-colors" + > + 기존 폼으로 추가하기 + + + ); + } + }; + + if (loading) { + return ( + + + + + + ); + } + + return ( + + + {/* 브레드크럼 */} + + + + + + + 일정 관리 + + + 일정 추가 + + + {/* 타이틀 */} + + 일정 추가 + 카테고리를 선택하고 일정을 등록하세요 + + + {/* 카테고리 선택 */} + + { + setSelectedCategory(id); + setIsInitialLoad(false); + }} + /> + + + {/* 카테고리별 폼 */} + + + {renderForm()} + + + + + ); +} + +export default ScheduleFormPage;
+ {member.name} +
+ {postInfo.profile.displayName} +
@{postInfo.username}
제목 (자동 추출)
{postInfo.title}
전체 내용
+ {postInfo.text} +
+ + 이미지 ({postInfo.imageUrls.length}개) +
+ 채널:{" "} + {videoInfo.channelName} +
+ 업로드:{" "} + {videoInfo.date} {videoInfo.time} +
+ 유형:{" "} + + {videoInfo.videoType === "shorts" ? "Shorts" : "Video"} + +
+ 이 카테고리는 아직 전용 폼이 없습니다. +
카테고리를 선택하고 일정을 등록하세요