diff --git a/src/components/common/Modal/SeatModal/SeatFocusModal.tsx b/src/components/common/Modal/SeatModal/SeatFocusModal.tsx index 924c313..198cef9 100644 --- a/src/components/common/Modal/SeatModal/SeatFocusModal.tsx +++ b/src/components/common/Modal/SeatModal/SeatFocusModal.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef } from 'react'; import { CloseIcon } from '@/assets'; -import { useModalStore } from '@/store/modalStore'; import SeatMap from '@/components/seat/SeatMap'; import ScreenBar from '@/components/seat/ScreenBar'; import { getSeatLabel } from '@/utils/getSeatLabel'; @@ -9,15 +8,16 @@ interface SeatFocusModalProps { auditoriumId: string; theaterName: string; selectedSeatNumbers: string[]; + focusedSeatIds: string[]; + onClose: () => void; } const SeatFocusModal = ({ auditoriumId, theaterName, selectedSeatNumbers, + onClose, }: SeatFocusModalProps) => { - const { closeModal } = useModalStore(); - const containerRef = useRef(null); const innerRef = useRef(null); @@ -44,7 +44,7 @@ const SeatFocusModal = ({ return (
-
+
{/* 모달 */}
@@ -52,7 +52,7 @@ const SeatFocusModal = ({ {/* 헤더 */}
{selectedSeatNumbers.join(', ')}
- +
{theaterName}
diff --git a/src/components/common/Modal/SeatModal/SeatModal.tsx b/src/components/common/Modal/SeatModal/SeatModal.tsx index 1bb077f..7157bab 100644 --- a/src/components/common/Modal/SeatModal/SeatModal.tsx +++ b/src/components/common/Modal/SeatModal/SeatModal.tsx @@ -16,18 +16,27 @@ interface SeatModalProps { type: SeatModalType; auditoriumId: string; theaterName?: string; + focusedSeatIds: string[]; theaterType?: CinemaFormat; // IMAX / DOLBY selectedSeatNumbers?: string[]; // seatFocus에서만 사용 } -const SeatModal = ({ type, auditoriumId, theaterName, selectedSeatNumbers }: SeatModalProps) => { +const SeatModal = ({ + type, + auditoriumId, + theaterName, + selectedSeatNumbers, + focusedSeatIds, +}: SeatModalProps) => { switch (type) { case 'seatFocus': return ( useModalStore.getState().closeModal()} auditoriumId={auditoriumId} theaterName={theaterName ?? ''} selectedSeatNumbers={selectedSeatNumbers ?? []} + focusedSeatIds={focusedSeatIds} /> ); case 'seatPicker': diff --git a/src/components/seat/SeatMap.tsx b/src/components/seat/SeatMap.tsx index 17c7298..f650b94 100644 --- a/src/components/seat/SeatMap.tsx +++ b/src/components/seat/SeatMap.tsx @@ -1,7 +1,9 @@ import SeatRow from './SeatRow'; import type { SeatRatingInfo } from '@/api/theater/theater.api'; import type { Seat } from '@/types/seat'; -import { useRef } from 'react'; +import { getSeatLayout } from '@/api/theater/theater.api'; +import { useEffect, useRef, useState } from 'react'; +import type { ApiError } from '@/types/api-response'; interface SeatMapProps { auditoriumId?: string; @@ -13,17 +15,41 @@ interface SeatMapProps { } const SeatMap = ({ + auditoriumId, onSeatClick, focusedSeatIds = [], selectedSeatNames = [], type = 'seatPicker', seatData = [], }: SeatMapProps) => { - const seatRows: Record = {}; + const [autoSeatData, setAutoSeatData] = useState([]); const focusedRef = useRef(null!); // row별로 묶기 - seatData.forEach((seat) => { + useEffect(() => { + const fetch = async () => { + if (type === 'seatFocus' && auditoriumId) { + try { + const data = await getSeatLayout(auditoriumId); + const parsedData: Seat[] = data.map((seat) => ({ + ...seat, + column: Number(seat.column), + })); + + setAutoSeatData(parsedData); + } catch (error) { + const apiError = error as ApiError; + console.error('좌석 정보 로딩 실패', apiError.error, apiError.message); + } + } + }; + fetch(); + }, [type, auditoriumId]); + + const dataToRender = type === 'seatFocus' ? autoSeatData : seatData; + + const seatRows: Record = {}; + dataToRender.forEach((seat) => { if (!seatRows[seat.row]) seatRows[seat.row] = []; seatRows[seat.row].push(seat); }); @@ -31,9 +57,8 @@ const SeatMap = ({ return (
{Object.entries(seatRows) - .sort(([a], [b]) => a.localeCompare(b)) // row 정렬: A, B, C... + .sort(([a], [b]) => a.localeCompare(b)) .map(([row, seats]) => { - // column 정렬 seats.sort((a, b) => a.column - b.column); const min = seats[0].column; diff --git a/src/pages/home/ReviewDetail.tsx b/src/pages/home/ReviewDetail.tsx index 1f24f39..79ea6eb 100644 --- a/src/pages/home/ReviewDetail.tsx +++ b/src/pages/home/ReviewDetail.tsx @@ -7,6 +7,7 @@ import { ProfileImageWithFallback, ConfirmModal, Loading, + SeatFocusModal, } from '@/components'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Pagination } from 'swiper/modules'; @@ -19,6 +20,8 @@ import { cn } from '@/utils/cn'; import { useModalStore, useReviewStore, useToastStore } from '@/store'; import ActionModal from '@/components/common/Modal/ActionModal'; import { deleteReview } from '@/api/review/reviewDelete.api'; +import { ChevronRightIcon } from '@/assets'; +import { getSeatLayout } from '@/api/theater/theater.api'; const ReviewDetailPage = () => { const navigate = useNavigate(); @@ -30,10 +33,11 @@ const ReviewDetailPage = () => { const { openModal, modalType } = useModalStore(); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const { setEditMode, setEditReview } = useReviewStore(); - + const [isSeatFocusOpen, setIsSeatFocusOpen] = useState(false); //사진 슬라이드 시 현재 사진 위치... const [currentIndex, setCurrentIndex] = useState(0); const [isScrolledPastImage, setIsScrolledPastImage] = useState(false); + const [focusedSeatIds, setFocusedSeatIds] = useState([]); useEffect(() => { if (!review || !review.imageInfo || review.imageInfo.length === 0) return; @@ -61,6 +65,25 @@ const ReviewDetailPage = () => { } }; + const handleSeatFocus = async () => { + if (!review || !review.auditoriumId) return; + + try { + const layout = await getSeatLayout(review.auditoriumId); + + const targetSeatIds = layout + .filter((s) => + review.seatInfo.map((seat) => seat.seatNumber).includes(`${s.row}${s.column}`), + ) + .map((s) => s.seatId); + + setFocusedSeatIds(targetSeatIds); + setIsSeatFocusOpen(true); + } catch (err) { + console.error('좌석 포커스 모달 에러:', err); + } + }; + useEffect(() => { if (!reviewId) return; const fetchReview = async () => { @@ -175,6 +198,11 @@ const ReviewDetailPage = () => { {review.seatInfo.map((seat) => seat.seatNumber).join(', ')} + + +
@@ -262,6 +290,15 @@ const ReviewDetailPage = () => { }} /> )} + {isSeatFocusOpen && ( + setIsSeatFocusOpen(false)} + auditoriumId={review.auditoriumId} + theaterName={review.auditoriumName} + focusedSeatIds={focusedSeatIds} + selectedSeatNumbers={review.seatInfo.map((s) => s.seatNumber)} + /> + )} ); }; diff --git a/src/pages/review/CinemaSelect.tsx b/src/pages/review/CinemaSelect.tsx index 0c27d59..6bfaf51 100644 --- a/src/pages/review/CinemaSelect.tsx +++ b/src/pages/review/CinemaSelect.tsx @@ -1,10 +1,9 @@ import { useNavigate } from 'react-router-dom'; import { ToggleTab, ReviewStepLayout, TheaterList } from '@/components'; -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useEffect } from 'react'; import { useReviewStore } from '@/store'; -import { getTheaters } from '@/api/theater/theater.api'; +import { useTheatersQuery } from '@/hooks/queries/useTheatersQuery'; import type { CinemaFormat } from '@/types/onboarding'; -import type { Theater } from '@/types/theater'; export default function CinemaSelect() { const { isInitialized } = useReviewStore(); @@ -12,12 +11,8 @@ export default function CinemaSelect() { const [selectedTab, setSelectedTab] = useState('IMAX'); const [selectedAuditorium, setSelectedAuditorium] = useState(null); - const [theaters, setTheaters] = useState([]); - const [page, setPage] = useState(1); - const [hasNext, setHasNext] = useState(true); - const [isLoading, setIsLoading] = useState(false); - const observerRef = useRef(null); + const { data: theaters } = useTheatersQuery({ type: selectedTab, page: 1, size: 10 }); useEffect(() => { if (!isInitialized) { @@ -25,61 +20,6 @@ export default function CinemaSelect() { } }, [isInitialized, navigate]); - const loadMore = useCallback(async () => { - if (isLoading || !hasNext) return; - - setIsLoading(true); - try { - const res = await getTheaters({ type: selectedTab, page, size: 10 }); - setTheaters((prev) => [...prev, ...res.content]); - setHasNext(res.hasNext); - setPage((prev) => prev + 1); - } catch (err) { - console.error('영화관 목록 로딩 실패:', err); - } finally { - setIsLoading(false); - } - }, [isLoading, hasNext, page, selectedTab]); - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting && !isLoading && hasNext) { - loadMore(); - } - }, - { - rootMargin: '100px', - threshold: 0.7, - }, - ); - - if (observerRef.current) observer.observe(observerRef.current); - - return () => { - if (observerRef.current) observer.unobserve(observerRef.current); - }; - }, [loadMore, isLoading, hasNext]); - - // 탭 변경 시 초기화 - useEffect(() => { - const reset = async () => { - setPage(1); - setTheaters([]); - setHasNext(true); - try { - const res = await getTheaters({ type: selectedTab, page: 1, size: 10 }); - setTheaters(res.content); - setHasNext(res.hasNext); - setPage(2); - } catch (err) { - console.error('초기 로딩 실패:', err); - } - }; - - reset(); - }, [selectedTab]); - const handleTabChange = (tab: string) => { setSelectedTab(tab as CinemaFormat); setSelectedAuditorium(null); @@ -108,7 +48,7 @@ export default function CinemaSelect() { setSelectedAuditorium(id)} /> - {hasNext &&
} ); } diff --git a/src/pages/review/ReviewContent.tsx b/src/pages/review/ReviewContent.tsx index a5c2c5c..e8efb2a 100644 --- a/src/pages/review/ReviewContent.tsx +++ b/src/pages/review/ReviewContent.tsx @@ -112,7 +112,7 @@ export default function ReviewTextForm() {
{/* 텍스트 입력 */} -
+