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,
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue