feat: 콘서트 폼 프론트엔드-백엔드 연결

- createConcertSchedule API 함수 추가
- handleSubmit에서 FormData 구성 및 API 호출 구현
- 유효성 검사 및 저장 성공 시 목록으로 이동

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-02-03 14:07:36 +09:00
parent 48f41c6db0
commit 9735206da7
4 changed files with 126 additions and 10 deletions

View file

@ -0,0 +1,13 @@
/**
* 콘서트 관리자 API
*/
import { fetchFormData } from '@/api/client';
/**
* 콘서트 일정 생성
* @param {FormData} formData - 콘서트 데이터
* @returns {Promise<{success: boolean, seriesId: number}>}
*/
export async function createConcertSchedule(formData) {
return fetchFormData('/admin/concert/schedule', formData, 'POST');
}

View file

@ -63,7 +63,7 @@ function VenueSearchDialog({ isOpen, onClose, onSelect }) {
id: place.id, id: place.id,
name: place.place_name, name: place.place_name,
address: place.road_address_name || place.address_name, address: place.road_address_name || place.address_name,
country: "한국", country: "South Korea",
lat: parseFloat(place.y), lat: parseFloat(place.y),
lng: parseFloat(place.x), lng: parseFloat(place.x),
category: place.category_name, category: place.category_name,

View file

@ -31,6 +31,13 @@ function ScheduleSection({ rounds, setRounds }) {
roundId: null, roundId: null,
}); });
//
const [venueDeleteConfirm, setVenueDeleteConfirm] = useState({
isOpen: false,
roundId: null,
venueName: null,
});
// //
const addRound = () => { const addRound = () => {
const newRound = { const newRound = {
@ -107,9 +114,24 @@ function ScheduleSection({ rounds, setRounds }) {
setLocationSearch({ isOpen: false, roundId: null }); setLocationSearch({ isOpen: false, roundId: null });
}; };
// //
const removeVenue = (roundId) => { const handleRemoveVenue = (roundId) => {
updateRound(roundId, "venue", null); const round = rounds.find((r) => r.id === roundId);
if (round?.venue) {
setVenueDeleteConfirm({
isOpen: true,
roundId,
venueName: round.venue.name,
});
}
};
//
const handleConfirmVenueDelete = () => {
if (venueDeleteConfirm.roundId !== null) {
updateRound(venueDeleteConfirm.roundId, "venue", null);
}
setVenueDeleteConfirm({ isOpen: false, roundId: null, venueName: null });
}; };
return ( return (
@ -141,6 +163,24 @@ function ScheduleSection({ rounds, setRounds }) {
onSelect={handleLocationSelect} onSelect={handleLocationSelect}
/> />
{/* 장소 삭제 확인 다이얼로그 */}
<ConfirmDialog
isOpen={venueDeleteConfirm.isOpen}
onClose={() =>
setVenueDeleteConfirm({ isOpen: false, roundId: null, venueName: null })
}
onConfirm={handleConfirmVenueDelete}
title="장소 삭제"
message={
<p>
<span className="font-medium">{venueDeleteConfirm.venueName}</span>
() 삭제하시겠습니까?
</p>
}
confirmText="삭제"
cancelText="취소"
/>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6"> <div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h2 className="text-lg font-bold text-gray-900 mb-6">공연 일정</h2> <h2 className="text-lg font-bold text-gray-900 mb-6">공연 일정</h2>
@ -221,7 +261,7 @@ function ScheduleSection({ rounds, setRounds }) {
</div> </div>
<button <button
type="button" type="button"
onClick={() => removeVenue(round.id)} onClick={() => handleRemoveVenue(round.id)}
className="p-1 text-gray-400 hover:text-red-500 transition-colors" className="p-1 text-gray-400 hover:text-red-500 transition-colors"
> >
<Trash2 size={14} /> <Trash2 size={14} />

View file

@ -9,6 +9,7 @@ import { useToast } from "@/hooks/common";
import { useAdminAuth } from "@/hooks/pc/admin"; import { useAdminAuth } from "@/hooks/pc/admin";
import { getMembers } from "@/api/public/members"; import { getMembers } from "@/api/public/members";
import { getAlbums } from "@/api/public/albums"; import { getAlbums } from "@/api/public/albums";
import { createConcertSchedule } from "@/api/admin/concert";
import ConcertInfoSection from "./ConcertInfoSection"; import ConcertInfoSection from "./ConcertInfoSection";
import ScheduleSection from "./ScheduleSection"; import ScheduleSection from "./ScheduleSection";
@ -96,13 +97,75 @@ function ConcertForm() {
setPosterPreview(null); setPosterPreview(null);
}; };
// (UI - ) //
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setToast({
type: "info", //
message: "UI만 구현된 상태입니다. 저장 기능은 아직 구현되지 않았습니다.", if (!title.trim()) {
setToast({ type: "error", message: "공연명을 입력해주세요." });
return;
}
const validRounds = rounds.filter((r) => r.date);
if (validRounds.length === 0) {
setToast({ type: "error", message: "최소 1개 이상의 공연 일정이 필요합니다." });
return;
}
setSaving(true);
try {
const formData = new FormData();
//
formData.append("title", title.trim());
formData.append("memberIds", JSON.stringify(selectedMemberIds));
//
if (posterFile) {
formData.append("poster", posterFile);
}
//
const roundsData = validRounds.map((r) => ({
date: r.date,
time: r.time || null,
venueId: r.venue?.id || null,
venueName: r.venue?.name || null,
venueCountry: r.venue?.country || null,
venueAddress: r.venue?.address || null,
venueLat: r.venue?.lat || null,
venueLng: r.venue?.lng || null,
}));
formData.append("rounds", JSON.stringify(roundsData));
//
const validSetlist = setlist.filter((s) => s.songName?.trim());
const setlistData = validSetlist.map((s) => ({
songName: s.songName.trim(),
albumName: s.albumName?.trim() || null,
memberIds: s.memberIds || [],
}));
formData.append("setlist", JSON.stringify(setlistData));
// 굿
merchandiseItems.forEach((item) => {
if (item.file) {
formData.append("merchandise", item.file);
}
}); });
await createConcertSchedule(formData);
setToast({ type: "success", message: "콘서트 일정이 저장되었습니다." });
setTimeout(() => navigate("/admin/schedule"), 1000);
} catch (err) {
console.error("콘서트 저장 실패:", err);
setToast({ type: "error", message: err.message || "저장에 실패했습니다." });
} finally {
setSaving(false);
}
}; };
return ( return (