diff --git a/frontend/src/components/pc/admin/common/VenueSearchDialog.jsx b/frontend/src/components/pc/admin/common/VenueSearchDialog.jsx
new file mode 100644
index 0000000..7e0968e
--- /dev/null
+++ b/frontend/src/components/pc/admin/common/VenueSearchDialog.jsx
@@ -0,0 +1,288 @@
+/**
+ * 장소 검색 다이얼로그 컴포넌트
+ * - 국내: 카카오맵 API
+ * - 해외: 구글맵 API
+ */
+import { useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { X, Search, MapPin, Globe } from "lucide-react";
+
+/**
+ * @param {Object} props
+ * @param {boolean} props.isOpen - 다이얼로그 열림 여부
+ * @param {Function} props.onClose - 닫기 핸들러
+ * @param {Function} props.onSelect - 장소 선택 핸들러 ({ name, address, country, lat, lng })
+ */
+function VenueSearchDialog({ isOpen, onClose, onSelect }) {
+ const [region, setRegion] = useState("domestic"); // domestic | overseas
+ const [searchQuery, setSearchQuery] = useState("");
+ const [results, setResults] = useState([]);
+ const [searching, setSearching] = useState(false);
+ const [error, setError] = useState(null);
+
+ // 다이얼로그 닫기 시 상태 초기화
+ const handleClose = () => {
+ setSearchQuery("");
+ setResults([]);
+ setError(null);
+ onClose();
+ };
+
+ // 지역 변경 시 결과 초기화
+ const handleRegionChange = (newRegion) => {
+ setRegion(newRegion);
+ setResults([]);
+ setError(null);
+ };
+
+ // 검색 실행
+ const handleSearch = async () => {
+ if (!searchQuery.trim()) {
+ setResults([]);
+ return;
+ }
+
+ setSearching(true);
+ setError(null);
+
+ try {
+ const token = localStorage.getItem("adminToken");
+
+ if (region === "domestic") {
+ // 카카오맵 API
+ const response = await fetch(
+ `/api/admin/kakao/places?query=${encodeURIComponent(searchQuery)}`,
+ {
+ headers: { Authorization: `Bearer ${token}` },
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ const places = (data.documents || []).map((place) => ({
+ id: place.id,
+ name: place.place_name,
+ address: place.road_address_name || place.address_name,
+ country: "한국",
+ lat: parseFloat(place.y),
+ lng: parseFloat(place.x),
+ category: place.category_name,
+ }));
+ setResults(places);
+ } else {
+ setError("검색 중 오류가 발생했습니다.");
+ }
+ } else {
+ // 구글맵 API
+ const response = await fetch(
+ `/api/admin/google/places?query=${encodeURIComponent(searchQuery)}`,
+ {
+ headers: { Authorization: `Bearer ${token}` },
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ const places = (data.results || []).map((place) => ({
+ id: place.place_id,
+ name: place.name,
+ address: place.formatted_address,
+ country: extractCountry(place.formatted_address),
+ lat: place.geometry?.location?.lat,
+ lng: place.geometry?.location?.lng,
+ category: place.types?.[0]?.replace(/_/g, " "),
+ }));
+ setResults(places);
+ } else {
+ setError("검색 중 오류가 발생했습니다.");
+ }
+ }
+ } catch (err) {
+ console.error("장소 검색 오류:", err);
+ setError("검색 중 오류가 발생했습니다.");
+ } finally {
+ setSearching(false);
+ }
+ };
+
+ // 주소에서 국가 추출 (구글맵용)
+ const extractCountry = (address) => {
+ if (!address) return "";
+ const parts = address.split(", ");
+ return parts[parts.length - 1] || "";
+ };
+
+ // 장소 선택
+ const handleSelectPlace = (place) => {
+ onSelect({
+ name: place.name,
+ address: place.address,
+ country: place.country,
+ lat: place.lat,
+ lng: place.lng,
+ });
+ handleClose();
+ };
+
+ return (
+
+ {isOpen && (
+
+ e.stopPropagation()}
+ >
+ {/* 헤더 */}
+
+
장소 검색
+
+
+
+ {/* 지역 선택 탭 */}
+
+
+
+
+
+ {/* 검색 입력 */}
+
+
+
+ setSearchQuery(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleSearch();
+ }
+ }}
+ placeholder={
+ region === "domestic"
+ ? "장소명을 입력하세요 (예: 올림픽홀)"
+ : "장소명을 입력하세요 (예: Tokyo Dome)"
+ }
+ 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"
+ autoFocus
+ />
+
+
+
+
+ {/* 에러 메시지 */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* 검색 결과 */}
+
+ {results.length > 0 ? (
+
+ {results.map((place) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+ );
+}
+
+export default VenueSearchDialog;
diff --git a/frontend/src/pages/pc/admin/schedules/form/concert/ConcertInfoSection.jsx b/frontend/src/pages/pc/admin/schedules/form/concert/ConcertInfoSection.jsx
new file mode 100644
index 0000000..d487912
--- /dev/null
+++ b/frontend/src/pages/pc/admin/schedules/form/concert/ConcertInfoSection.jsx
@@ -0,0 +1,160 @@
+import { useRef } from "react";
+import { Image, Users, Check } from "lucide-react";
+
+/**
+ * 콘서트 정보 섹션
+ * - 공연명
+ * - 포스터
+ * - 참여 멤버
+ */
+function ConcertInfoSection({
+ title,
+ setTitle,
+ posterPreview,
+ onPosterChange,
+ onPosterRemove,
+ members,
+ selectedMemberIds,
+ onToggleMember,
+ onToggleAllMembers,
+}) {
+ const posterInputRef = useRef(null);
+
+ const handlePosterChange = (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ onPosterChange(file);
+ }
+ };
+
+ const isAllSelected = members.length > 0 && selectedMemberIds.length === members.length;
+
+ return (
+
+
콘서트 정보
+
+
+ {/* 공연명 */}
+
+
+ setTitle(e.target.value)}
+ placeholder="예: fromis_9 WORLD TOUR NOW TOMORROW."
+ className="w-full px-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+ />
+
+
+ {/* 포스터 */}
+
+
+
+
posterInputRef.current?.click()}
+ className="w-40 h-56 rounded-xl border-2 border-dashed border-gray-200 flex items-center justify-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors overflow-hidden"
+ >
+ {posterPreview ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ 권장 크기: 세로형 포스터 (예: 700x1000px)
+
+
지원 형식: JPG, PNG, WebP
+ {posterPreview && (
+
+ )}
+
+
+
+
+ {/* 참여 멤버 */}
+
+
+
+
+
+
+
+ {members.map((member) => {
+ const isSelected = selectedMemberIds.includes(member.id);
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+export default ConcertInfoSection;
diff --git a/frontend/src/pages/pc/admin/schedules/form/concert/ScheduleSection.jsx b/frontend/src/pages/pc/admin/schedules/form/concert/ScheduleSection.jsx
new file mode 100644
index 0000000..25ff031
--- /dev/null
+++ b/frontend/src/pages/pc/admin/schedules/form/concert/ScheduleSection.jsx
@@ -0,0 +1,265 @@
+import { useState, useRef } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { Plus, Trash2, MapPin, Search } from "lucide-react";
+
+import DatePicker from "@/components/pc/admin/common/DatePicker";
+import TimePicker from "@/components/pc/admin/common/TimePicker";
+import ConfirmDialog from "@/components/pc/admin/common/ConfirmDialog";
+import VenueSearchDialog from "@/components/pc/admin/common/VenueSearchDialog";
+
+/**
+ * 공연 일정 섹션
+ * - 다회차 지원 (날짜, 시간, 장소)
+ */
+function ScheduleSection({ rounds, setRounds }) {
+ const containerRef = useRef(null);
+ const [nextId, setNextId] = useState(() => {
+ const maxId = rounds.reduce((max, r) => Math.max(max, r.id || 0), 0);
+ return maxId + 1;
+ });
+
+ // 삭제 확인 다이얼로그
+ const [deleteConfirm, setDeleteConfirm] = useState({
+ isOpen: false,
+ roundId: null,
+ roundIndex: null,
+ });
+
+ // 장소 검색 다이얼로그
+ const [locationSearch, setLocationSearch] = useState({
+ isOpen: false,
+ roundId: null,
+ });
+
+ // 회차 추가
+ const addRound = () => {
+ const newRound = {
+ id: nextId,
+ date: "",
+ time: "",
+ venue: null, // { name, address, lat, lng }
+ };
+ setRounds([...rounds, newRound]);
+ setNextId(nextId + 1);
+
+ // 새 회차로 스크롤
+ setTimeout(() => {
+ if (containerRef.current) {
+ const lastChild = containerRef.current.lastElementChild;
+ if (lastChild) {
+ lastChild.scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+ }
+ }, 100);
+ };
+
+ // 회차 삭제 시도
+ const handleRemoveRound = (id) => {
+ if (rounds.length <= 1) return;
+
+ const round = rounds.find((r) => r.id === id);
+ const roundIndex = rounds.findIndex((r) => r.id === id);
+
+ // 입력값이 있으면 확인 다이얼로그 표시
+ if (round && (round.date || round.time || round.venue)) {
+ setDeleteConfirm({
+ isOpen: true,
+ roundId: id,
+ roundIndex: roundIndex + 1,
+ });
+ } else {
+ removeRound(id);
+ }
+ };
+
+ // 회차 삭제 실행
+ const removeRound = (id) => {
+ setRounds(rounds.filter((round) => round.id !== id));
+ };
+
+ // 삭제 확인
+ const handleConfirmDelete = () => {
+ if (deleteConfirm.roundId !== null) {
+ removeRound(deleteConfirm.roundId);
+ }
+ setDeleteConfirm({ isOpen: false, roundId: null, roundIndex: null });
+ };
+
+ // 회차 업데이트
+ const updateRound = (id, field, value) => {
+ setRounds(
+ rounds.map((round) =>
+ round.id === id ? { ...round, [field]: value } : round
+ )
+ );
+ };
+
+ // 장소 검색 열기
+ const openLocationSearch = (roundId) => {
+ setLocationSearch({ isOpen: true, roundId });
+ };
+
+ // 장소 선택
+ const handleLocationSelect = (place) => {
+ if (locationSearch.roundId !== null) {
+ updateRound(locationSearch.roundId, "venue", place);
+ }
+ setLocationSearch({ isOpen: false, roundId: null });
+ };
+
+ // 장소 제거
+ const removeVenue = (roundId) => {
+ updateRound(roundId, "venue", null);
+ };
+
+ return (
+ <>
+ {/* 삭제 확인 다이얼로그 */}
+
+ setDeleteConfirm({ isOpen: false, roundId: null, roundIndex: null })
+ }
+ onConfirm={handleConfirmDelete}
+ title="회차 삭제"
+ message={
+
+ {deleteConfirm.roundIndex}회차에
+ 입력된 정보가 있습니다.
+
+ 정말 삭제하시겠습니까?
+
+ }
+ confirmText="삭제"
+ cancelText="취소"
+ />
+
+ {/* 장소 검색 다이얼로그 */}
+ setLocationSearch({ isOpen: false, roundId: null })}
+ onSelect={handleLocationSelect}
+ />
+
+
+
+
+
+
+ {rounds.map((round, index) => (
+
+
+ {/* 헤더 */}
+
+
+ {index + 1}회차
+
+ {rounds.length > 1 && (
+
+ )}
+
+
+ {/* 날짜 & 시간 */}
+
+
+
+ updateRound(round.id, "date", val)}
+ placeholder="날짜 선택"
+ minYear={2017}
+ />
+
+
+
+ updateRound(round.id, "time", val)}
+ placeholder="시간 선택"
+ />
+
+
+
+ {/* 장소 */}
+
+
+ {round.venue ? (
+
+
+
+
+
+ {round.venue.name}
+
+ {round.venue.country && (
+
+ {round.venue.country}
+
+ )}
+
+
+ {round.venue.address}
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ ))}
+
+
+
+
+ 시간과 장소는 선택사항입니다. 미정인 경우 비워두세요.
+
+
+ >
+ );
+}
+
+export default ScheduleSection;
diff --git a/frontend/src/pages/pc/admin/schedules/form/concert/index.jsx b/frontend/src/pages/pc/admin/schedules/form/concert/index.jsx
new file mode 100644
index 0000000..da216e1
--- /dev/null
+++ b/frontend/src/pages/pc/admin/schedules/form/concert/index.jsx
@@ -0,0 +1,148 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import { motion } from "framer-motion";
+import { Save } from "lucide-react";
+
+import Toast from "@/components/common/Toast";
+import { useToast } from "@/hooks/common";
+import { useAdminAuth } from "@/hooks/pc/admin";
+import { getMembers } from "@/api/public/members";
+
+import ConcertInfoSection from "./ConcertInfoSection";
+import ScheduleSection from "./ScheduleSection";
+
+/**
+ * 콘서트 일정 추가 폼
+ */
+function ConcertForm() {
+ const navigate = useNavigate();
+ const { toast, setToast } = useToast();
+ const { isAuthenticated } = useAdminAuth();
+
+ // 멤버 목록 조회
+ const { data: membersData = [] } = useQuery({
+ queryKey: ["members"],
+ queryFn: getMembers,
+ enabled: isAuthenticated,
+ staleTime: 5 * 60 * 1000,
+ });
+ const members = membersData.filter((m) => !m.is_former);
+
+ // 콘서트 정보
+ const [title, setTitle] = useState("");
+ const [posterFile, setPosterFile] = useState(null);
+ const [posterPreview, setPosterPreview] = useState(null);
+ const [selectedMemberIds, setSelectedMemberIds] = useState([]);
+
+ // 공연 일정 (다회차)
+ const [rounds, setRounds] = useState([
+ { id: 1, date: "", time: "", venue: null },
+ ]);
+
+ // 로딩 상태
+ const [saving, setSaving] = useState(false);
+
+ // 멤버 토글
+ const toggleMember = (memberId) => {
+ setSelectedMemberIds((prev) =>
+ prev.includes(memberId)
+ ? prev.filter((id) => id !== memberId)
+ : [...prev, memberId]
+ );
+ };
+
+ // 전체 선택/해제
+ const toggleAllMembers = () => {
+ if (selectedMemberIds.length === members.length) {
+ setSelectedMemberIds([]);
+ } else {
+ setSelectedMemberIds(members.map((m) => m.id));
+ }
+ };
+
+ // 포스터 변경
+ const handlePosterChange = (file) => {
+ setPosterFile(file);
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setPosterPreview(reader.result);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ // 포스터 제거
+ const handlePosterRemove = () => {
+ setPosterFile(null);
+ setPosterPreview(null);
+ };
+
+ // 폼 제출 (UI만 - 실제 저장 로직 없음)
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setToast({
+ type: "info",
+ message: "UI만 구현된 상태입니다. 저장 기능은 아직 구현되지 않았습니다.",
+ });
+ };
+
+ return (
+ <>
+ setToast(null)} />
+
+
+ {/* 콘서트 정보 */}
+
+
+ {/* 공연 일정 */}
+
+
+ {/* 버튼 */}
+
+
+
+
+
+ >
+ );
+}
+
+export default ConcertForm;
diff --git a/frontend/src/pages/pc/admin/schedules/form/index.jsx b/frontend/src/pages/pc/admin/schedules/form/index.jsx
index a58d481..0362bfa 100644
--- a/frontend/src/pages/pc/admin/schedules/form/index.jsx
+++ b/frontend/src/pages/pc/admin/schedules/form/index.jsx
@@ -8,6 +8,7 @@ import * as categoriesApi from "@/api/admin/categories";
import CategorySelector from "@/components/pc/admin/schedule/CategorySelector";
import YouTubeForm from "./YouTubeForm";
import XForm from "./XForm";
+import ConcertForm from "./concert";
// 애니메이션 variants
const containerVariants = {
@@ -74,6 +75,9 @@ function ScheduleFormPage() {
case 'X':
return ;
+ case '콘서트':
+ return ;
+
// 다른 카테고리는 기존 폼으로 리다이렉트
default:
return (