feat: 콘서트 폼 프론트엔드-백엔드 연결
- createConcertSchedule API 함수 추가 - handleSubmit에서 FormData 구성 및 API 호출 구현 - 유효성 검사 및 저장 성공 시 목록으로 이동 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
48f41c6db0
commit
9735206da7
4 changed files with 126 additions and 10 deletions
13
frontend/src/api/admin/concert.js
Normal file
13
frontend/src/api/admin/concert.js
Normal 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');
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ function VenueSearchDialog({ isOpen, onClose, onSelect }) {
|
|||
id: place.id,
|
||||
name: place.place_name,
|
||||
address: place.road_address_name || place.address_name,
|
||||
country: "한국",
|
||||
country: "South Korea",
|
||||
lat: parseFloat(place.y),
|
||||
lng: parseFloat(place.x),
|
||||
category: place.category_name,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ function ScheduleSection({ rounds, setRounds }) {
|
|||
roundId: null,
|
||||
});
|
||||
|
||||
// 장소 삭제 확인 다이얼로그
|
||||
const [venueDeleteConfirm, setVenueDeleteConfirm] = useState({
|
||||
isOpen: false,
|
||||
roundId: null,
|
||||
venueName: null,
|
||||
});
|
||||
|
||||
// 회차 추가
|
||||
const addRound = () => {
|
||||
const newRound = {
|
||||
|
|
@ -107,9 +114,24 @@ function ScheduleSection({ rounds, setRounds }) {
|
|||
setLocationSearch({ isOpen: false, roundId: null });
|
||||
};
|
||||
|
||||
// 장소 제거
|
||||
const removeVenue = (roundId) => {
|
||||
updateRound(roundId, "venue", null);
|
||||
// 장소 삭제 시도
|
||||
const handleRemoveVenue = (roundId) => {
|
||||
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 (
|
||||
|
|
@ -141,6 +163,24 @@ function ScheduleSection({ rounds, setRounds }) {
|
|||
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">
|
||||
<h2 className="text-lg font-bold text-gray-900 mb-6">공연 일정</h2>
|
||||
|
||||
|
|
@ -221,7 +261,7 @@ function ScheduleSection({ rounds, setRounds }) {
|
|||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeVenue(round.id)}
|
||||
onClick={() => handleRemoveVenue(round.id)}
|
||||
className="p-1 text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useToast } from "@/hooks/common";
|
|||
import { useAdminAuth } from "@/hooks/pc/admin";
|
||||
import { getMembers } from "@/api/public/members";
|
||||
import { getAlbums } from "@/api/public/albums";
|
||||
import { createConcertSchedule } from "@/api/admin/concert";
|
||||
|
||||
import ConcertInfoSection from "./ConcertInfoSection";
|
||||
import ScheduleSection from "./ScheduleSection";
|
||||
|
|
@ -96,13 +97,75 @@ function ConcertForm() {
|
|||
setPosterPreview(null);
|
||||
};
|
||||
|
||||
// 폼 제출 (UI만 - 실제 저장 로직 없음)
|
||||
// 폼 제출
|
||||
const handleSubmit = async (e) => {
|
||||
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 (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue