feat(admin-schedule): 공용 일정 폼 정비 (컴백·팬사인회·기타)

ScheduleForm을 지원 필드(제목·날짜·시간·멤버)만으로 재작성 +
컴백에 "날짜 미정(월만)" 토글 추가, JSON으로 /admin/schedules 저장.
카테고리 선택은 컴백·팬사인회·기타만 노출(전용 폼 카테고리·생일·기념일
제외). DB에 저장 못 하던 범위·장소·설명·링크·이미지 필드 제거.
form 라우팅: 컴백/팬사인회/기타→공용폼, 티켓팅 등→준비 중 안내.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-06-16 22:03:50 +09:00
parent a11a027682
commit 72c65021fb
2 changed files with 151 additions and 560 deletions

View file

@ -1,364 +1,151 @@
import { useState, useEffect } from 'react';
import { useNavigate, Link, useParams } from 'react-router-dom';
import { useState, useEffect, useMemo } from 'react';
import { useNavigate, Link, useParams, useSearchParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { formatDate } from '@/utils/date';
import {
Home,
ChevronRight,
Save,
X,
Link as LinkIcon,
MapPin,
Settings,
Search,
} from 'lucide-react';
import { Toast, Lightbox } from '@/components/common';
import { AdminLayout, ConfirmDialog, DatePicker, TimePicker } from '@/components/pc/admin';
import {
LocationSearchDialog,
MemberSelector,
ImageUploader,
} from '@/components/pc/admin/schedule';
import { Home, ChevronRight, Save, Settings } from 'lucide-react';
import { AdminLayout, DatePicker, TimePicker } from '@/components/pc/admin';
import { Toast } from '@/components/common';
import { MemberSelector } from '@/components/pc/admin/schedule';
import { useAdminAuth } from '@/hooks/pc/admin';
import { useToast } from '@/hooks/common';
import * as categoriesApi from '@/api/admin/categories';
import * as schedulesApi from '@/api/admin/schedules';
import { getSchedule } from '@/api/admin/schedules';
import { getMembers } from '@/api/public/members';
import { getColorStyle } from '@/utils/color';
import useAuthStore from '@/stores/useAuthStore';
//
const SHARED_CATEGORIES = ['컴백', '팬사인회', '기타'];
// " ()" ( )
const DATE_PRECISION_CATEGORIES = ['컴백'];
/**
* 일반 일정 추가/수정 (컴백·팬사인회·기타)
* 제목·날짜·시간·멤버 + 컴백의 "날짜 미정(월만)" 토글
*/
function ScheduleForm() {
const navigate = useNavigate();
const { id } = useParams();
const [searchParams] = useSearchParams();
const isEditMode = !!id;
const { user, isAuthenticated } = useAdminAuth();
const { user } = useAdminAuth();
const { toast, setToast } = useToast();
const [loading, setLoading] = useState(false);
// (/ )
const [formData, setFormData] = useState({
title: '',
startDate: '',
endDate: '',
startTime: '',
endTime: '',
isRange: false, //
date: '',
time: '',
category: '',
description: '',
url: '',
sourceName: '',
members: [],
images: [],
//
locationName: '', //
locationAddress: '', //
locationDetail: '', // (: 3, N )
locationLat: null, //
locationLng: null, //
datePrecision: 'day',
});
//
const [imagePreviews, setImagePreviews] = useState([]);
//
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
//
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteTargetIndex, setDeleteTargetIndex] = useState(null);
//
const { data: membersData = [] } = useQuery({
queryKey: ['members'],
queryFn: getMembers,
enabled: isAuthenticated,
staleTime: 5 * 60 * 1000,
});
const members = membersData.filter((m) => !m.is_former);
//
const { data: categories = [] } = useQuery({
queryKey: ['admin', 'categories'],
queryFn: categoriesApi.getCategories,
enabled: isAuthenticated,
staleTime: 5 * 60 * 1000,
});
//
const [saving, setSaving] = useState(false);
//
const [locationDialogOpen, setLocationDialogOpen] = useState(false);
// /
const { data: allCategories = [] } = useQuery({
queryKey: ['scheduleCategories'],
queryFn: categoriesApi.getCategories,
staleTime: 10 * 60 * 1000,
});
const { data: members = [] } = useQuery({
queryKey: ['members'],
queryFn: getMembers,
staleTime: 10 * 60 * 1000,
});
// ID
const [existingImageIds, setExistingImageIds] = useState([]);
const categories = useMemo(
() => allCategories.filter((c) => SHARED_CATEGORIES.includes(c.name)),
[allCategories]
);
//
const selectedCategoryName = allCategories.find((c) => c.id === formData.category)?.name;
const showPrecisionToggle = DATE_PRECISION_CATEGORIES.includes(selectedCategoryName);
const isMonthPrecision = formData.datePrecision === 'month';
// : URL ?category ,
useEffect(() => {
if (categories.length > 0 && !formData.category && !isEditMode) {
setFormData((prev) => ({ ...prev, category: categories[0].id }));
}
}, [categories, isEditMode]);
if (isEditMode || categories.length === 0 || formData.category) return;
const fromUrl = parseInt(searchParams.get('category'), 10);
const preselect = categories.find((c) => c.id === fromUrl);
setFormData((p) => ({ ...p, category: (preselect || categories[0]).id }));
}, [categories, isEditMode, formData.category, searchParams]);
//
// :
const { data: existing } = useQuery({
queryKey: ['schedule', id],
queryFn: () => getSchedule(id),
enabled: isEditMode,
});
useEffect(() => {
if (isAuthenticated && isEditMode && id) {
fetchSchedule();
}
}, [isAuthenticated, isEditMode, id]);
// ( )
const fetchSchedule = async () => {
setLoading(true);
try {
const data = await schedulesApi.getSchedule(id);
//
setFormData({
title: data.title || '',
startDate: data.date ? formatDate(data.date) : '',
endDate: data.end_date ? formatDate(data.end_date) : '',
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 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 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);
};
// (LocationSearchDialog )
const handleLocationSelect = (place) => {
if (!existing) return;
setFormData({
...formData,
locationName: place.name,
locationAddress: place.address,
locationLat: place.lat,
locationLng: place.lng,
title: existing.title || '',
date: existing.date ? existing.date.slice(0, 10) : '',
time: existing.time ? existing.time.slice(0, 5) : '',
category: existing.category?.id || existing.category_id || '',
members: (existing.members || []).filter((m) => m.id).map((m) => m.id),
datePrecision: existing.datePrecision || 'day',
});
};
}, [existing]);
// (ImageUploader )
const handleImagesUploadFromUploader = (files) => {
const newImageObjects = files.map((file) => ({ file }));
const newImages = [...formData.images, ...newImageObjects];
setFormData({ ...formData, images: newImages });
const toggleMember = (memberId) =>
setFormData((p) => ({
...p,
members: p.members.includes(memberId)
? p.members.filter((x) => x !== memberId)
: [...p.members, memberId],
}));
const toggleAllMembers = () =>
setFormData((p) => ({
...p,
members: p.members.length === members.length ? [] : members.map((m) => m.id),
}));
files.forEach((file) => {
const reader = new FileReader();
reader.onloadend = () => {
setImagePreviews((prev) => [...prev, reader.result]);
};
reader.readAsDataURL(file);
});
};
const setPrecision = (month) =>
setFormData((p) => ({ ...p, datePrecision: month ? 'month' : 'day', time: month ? '' : p.time }));
// (ImageUploader )
const handleImageDelete = (index) => {
setDeleteTargetIndex(index);
setDeleteDialogOpen(true);
};
// (ImageUploader )
const handleImageReorder = (fromIndex, toIndex) => {
const newPreviews = [...imagePreviews];
const newImages = [...formData.images];
const [movedPreview] = newPreviews.splice(fromIndex, 1);
const [movedImage] = newImages.splice(fromIndex, 1);
newPreviews.splice(toIndex, 0, movedPreview);
newImages.splice(toIndex, 0, movedImage);
setImagePreviews(newPreviews);
setFormData({ ...formData, images: newImages });
};
//
const handleSubmit = async (e) => {
e.preventDefault();
//
if (!formData.title.trim()) {
setToast({ type: 'error', message: '제목을 입력해주세요.' });
return;
}
// : / startDate
if (!formData.startDate) {
if (!formData.date) {
setToast({ type: 'error', message: '날짜를 선택해주세요.' });
return;
}
if (!formData.category) {
setToast({ type: 'error', message: '카테고리를 선택해주세요.' });
return;
}
setSaving(true);
try {
const token = useAuthStore.getState().token;
// FormData
const submitData = new FormData();
// JSON - startDate date (UI / startDate )
const jsonData = {
const body = {
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,
date: formData.date,
time: isMonthPrecision ? null : formData.time || null,
category: formData.category,
description: formData.description.trim() || null,
url: formData.url.trim() || null,
sourceName: formData.sourceName.trim() || null,
datePrecision: showPrecisionToggle ? formData.datePrecision : 'day',
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,
const res = await fetch(url, {
method: isEditMode ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(body),
});
if (!response.ok) {
const error = await response.json();
throw new Error(
error.error || (isEditMode ? '일정 수정에 실패했습니다.' : '일정 생성에 실패했습니다.')
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || (isEditMode ? '일정 수정에 실패했습니다.' : '일정 생성에 실패했습니다.'));
}
// sessionStorage
sessionStorage.setItem(
'scheduleToast',
JSON.stringify({
type: 'success',
message: isEditMode ? '일정이 수정되었습니다.' : '일정이 추가되었습니다.',
})
JSON.stringify({ type: 'success', message: isEditMode ? '일정이 수정되었습니다.' : '일정이 추가되었습니다.' })
);
navigate('/admin/schedule');
} catch (error) {
console.error('일정 저장 오류:', error);
setToast({
type: 'error',
message: error.message || '일정 저장 중 오류가 발생했습니다.',
});
setToast({ type: 'error', message: error.message });
} finally {
setSaving(false);
}
@ -368,38 +155,6 @@ function ScheduleForm() {
<AdminLayout user={user}>
<Toast toast={toast} onClose={() => setToast(null)} />
{/* 삭제 확인 다이얼로그 */}
<ConfirmDialog
isOpen={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
onConfirm={confirmDeleteImage}
title="이미지 삭제"
message={
<>
이미지를 삭제하시겠습니까?
<br />
<span className="text-sm text-red-500"> 작업은 되돌릴 없습니다.</span>
</>
}
/>
{/* 장소 검색 다이얼로그 */}
<LocationSearchDialog
isOpen={locationDialogOpen}
onClose={() => setLocationDialogOpen(false)}
onSelect={handleLocationSelect}
/>
{/* 이미지 라이트박스 - 공통 컴포넌트 사용 */}
<Lightbox
images={imagePreviews}
currentIndex={lightboxIndex}
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
onIndexChange={setLightboxIndex}
/>
{/* 메인 콘텐츠 */}
<div className="max-w-4xl mx-auto px-6 py-8">
{/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
@ -414,17 +169,14 @@ function ScheduleForm() {
<span className="text-gray-700">{isEditMode ? '일정 수정' : '일정 추가'}</span>
</div>
{/* 타이틀 */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
{isEditMode ? '일정 수정' : '일정 추가'}
</h1>
<p className="text-gray-500">새로운 일정을 등록합니다</p>
<p className="text-gray-500">제목·날짜·멤버 중심의 일반 일정을 등록합니다</p>
</div>
{/* 폼 */}
<form onSubmit={handleSubmit} className="space-y-8">
{/* 기본 정보 카드 */}
<div className="bg-white rounded-2xl shadow-sm p-8">
<h2 className="text-lg font-bold text-gray-900 mb-6">기본 정보</h2>
@ -436,96 +188,12 @@ function ScheduleForm() {
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="일정 제목을 입력하세요"
placeholder="일정 제목을 입력하세요 (예: 9월 컴백)"
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
/>
</div>
{/* 범위 설정 토글 */}
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => setFormData({ ...formData, isRange: !formData.isRange })}
className={`relative w-12 h-6 rounded-full transition-colors ${
formData.isRange ? 'bg-primary' : 'bg-gray-300'
}`}
>
<span
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${
formData.isRange ? 'left-6' : 'left-0.5'
}`}
/>
</button>
<span className="text-sm text-gray-700">기간 설정 (시작~종료)</span>
</div>
{/* 날짜 + 시간 */}
{formData.isRange ? (
//
<div className="space-y-4">
{/* 시작 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">시작 날짜 *</label>
<DatePicker
value={formData.startDate}
onChange={(date) => setFormData({ ...formData, startDate: date })}
placeholder="시작 날짜 선택"
minYear={2017}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">시작 시간</label>
<TimePicker
value={formData.startTime}
onChange={(time) => setFormData({ ...formData, startTime: time })}
placeholder="시작 시간 선택"
/>
</div>
</div>
{/* 종료 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">종료 날짜 *</label>
<DatePicker
value={formData.endDate}
onChange={(date) => setFormData({ ...formData, endDate: date })}
placeholder="종료 날짜 선택"
minYear={2017}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">종료 시간</label>
<TimePicker
value={formData.endTime}
onChange={(time) => setFormData({ ...formData, endTime: time })}
placeholder="종료 시간 선택"
/>
</div>
</div>
</div>
) : (
//
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">날짜 *</label>
<DatePicker
value={formData.startDate}
onChange={(date) => setFormData({ ...formData, startDate: date })}
minYear={2017}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">시간</label>
<TimePicker
value={formData.startTime}
onChange={(time) => setFormData({ ...formData, startTime: time })}
/>
</div>
</div>
)}
{/* 카테고리 */}
<div>
<div className="flex items-center justify-between mb-2">
@ -560,133 +228,55 @@ function ScheduleForm() {
</div>
</div>
{/* 장소 */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">장소</label>
{/* 검색으로 입력된 경우(좌표가 있는 경우) 초기화 버튼 표시 */}
{formData.locationLat && formData.locationLng && (
<button
type="button"
onClick={() =>
setFormData({
...formData,
locationName: '',
locationAddress: '',
locationDetail: '',
locationLat: null,
locationLng: null,
})
}
className="text-xs text-gray-400 hover:text-red-500 transition-colors flex items-center gap-1"
>
<X size={12} />
초기화
</button>
{/* 날짜 미정 토글 (컴백 등) */}
{showPrecisionToggle && (
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-700">날짜 미정 (월만)</p>
<p className="text-xs text-gray-400 mt-0.5">
"9월 컴백 예정"처럼 날짜는 미정이고 월만 확정일
</p>
</div>
<button
type="button"
onClick={() => setPrecision(!isMonthPrecision)}
className={`relative w-12 h-6 rounded-full transition-colors ${
isMonthPrecision ? 'bg-primary' : 'bg-gray-300'
}`}
>
<span
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${
isMonthPrecision ? 'left-6' : 'left-0.5'
}`}
/>
</button>
</div>
)}
{/* 날짜 + 시간 */}
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{isMonthPrecision ? '월 *' : '날짜 *'}
</label>
<DatePicker
value={formData.date}
onChange={(date) => setFormData({ ...formData, date })}
minYear={2017}
/>
{isMonthPrecision && (
<p className="text-xs text-gray-400 mt-1">선택한 날짜의 "월" 사용됩니다 (일자는 무시)</p>
)}
</div>
<div className="space-y-3">
{/* 장소 이름 + 검색 버튼 */}
<div className="flex gap-2">
<div className="flex-1 relative">
<MapPin
size={18}
className={`absolute left-4 top-1/2 -translate-y-1/2 ${formData.locationLat ? 'text-primary' : 'text-gray-400'}`}
/>
<input
type="text"
value={formData.locationName}
onChange={(e) =>
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' : ''}`}
/>
</div>
<button
type="button"
onClick={() => setLocationDialogOpen(true)}
className="px-4 py-3 bg-gray-100 text-gray-600 rounded-xl hover:bg-gray-200 transition-colors flex items-center gap-2"
>
<Search size={18} />
<span className="hidden sm:inline">검색</span>
</button>
{!isMonthPrecision && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">시간</label>
<TimePicker
value={formData.time}
onChange={(time) => setFormData({ ...formData, time })}
/>
</div>
{/* 주소 */}
<input
type="text"
value={formData.locationAddress}
onChange={(e) =>
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' : ''}`}
/>
{/* 상세주소 */}
<input
type="text"
value={formData.locationDetail}
onChange={(e) =>
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"
/>
</div>
</div>
{/* 설명 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">설명</label>
<div className="border border-gray-200 rounded-xl overflow-hidden focus-within:ring-2 focus-within:ring-primary focus-within:border-transparent">
<textarea
rows={6}
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="일정에 대한 설명을 입력하세요"
className="w-full px-4 py-3 border-none focus:outline-none resize-none"
/>
</div>
</div>
{/* URL */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">관련 URL</label>
<div className="relative">
<LinkIcon size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="url"
value={formData.url}
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
placeholder="https://..."
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"
/>
</div>
</div>
{/* 출처 이름 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">출처 이름</label>
<input
type="text"
value={formData.sourceName}
onChange={(e) => setFormData({ ...formData, sourceName: e.target.value })}
placeholder="예: 스프:스튜디오 프로미스나인, MUSINSA TV"
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"
/>
)}
</div>
</div>
</div>
@ -699,15 +289,6 @@ function ScheduleForm() {
onToggleAll={toggleAllMembers}
/>
{/* 다중 이미지 업로드 카드 */}
<ImageUploader
previews={imagePreviews}
onUpload={handleImagesUploadFromUploader}
onDelete={handleImageDelete}
onReorder={handleImageReorder}
onOpenLightbox={openLightbox}
/>
{/* 버튼 */}
<div className="flex items-center justify-end gap-4">
<button
@ -719,7 +300,7 @@ function ScheduleForm() {
</button>
<button
type="submit"
disabled={loading || saving}
disabled={saving}
className="flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-colors font-medium disabled:opacity-50"
>
{saving ? (

View file

@ -84,21 +84,31 @@ function ScheduleFormPage() {
case '행사':
return <EventForm />;
//
default:
// ··:
case '컴백':
case '팬사인회':
case '기타':
return (
<div className="bg-white rounded-2xl shadow-sm p-8 text-center">
<p className="text-gray-500 mb-4">
카테고리는 아직 전용 폼이 없습니다.
제목·날짜·멤버 중심의 기본 폼으로 추가합니다.
</p>
<button
onClick={() => navigate(`/admin/schedule/new-legacy?category=${selectedCategory}`)}
className="px-6 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-colors"
>
기존 폼으로 추가하
일정 추가
</button>
</div>
);
// ( ):
default:
return (
<div className="bg-white rounded-2xl shadow-sm p-8 text-center">
<p className="text-gray-500"> 카테고리의 전용 폼은 준비 중입니다.</p>
</div>
);
}
};