diff --git a/src/api/review/review.ts b/src/api/review/review.ts new file mode 100644 index 0000000..5674837 --- /dev/null +++ b/src/api/review/review.ts @@ -0,0 +1,17 @@ +import api from '../api'; +import type { ApiResponse } from '@/types/api-response'; + +export interface ReviewCreateRequest { + seatIds: string[]; + title: string; + movieTitle: string; + rating: number; + content: string; + hashtags: number[]; + imageUrl: string[]; +} + +export const postReview = async (data: ReviewCreateRequest) => { + const res = await api.post>('/reviews', data); + return res.data.data; +}; diff --git a/src/api/review/reviewRewrite.api.ts b/src/api/review/reviewRewrite.api.ts index 897ccc0..aab422f 100644 --- a/src/api/review/reviewRewrite.api.ts +++ b/src/api/review/reviewRewrite.api.ts @@ -5,7 +5,7 @@ interface ReviewUpdateRequest { title: string; rating: number; content: string; - hashtags: string[]; + hashtags: number[]; images: string[]; } diff --git a/src/api/theater/theater.api.ts b/src/api/theater/theater.api.ts index 571a6f5..4fe8e0a 100644 --- a/src/api/theater/theater.api.ts +++ b/src/api/theater/theater.api.ts @@ -33,8 +33,31 @@ const getTheaters = async ({ }; // 좌석 배치도 조회 +export interface SeatLayoutInfo { + seatId: string; + row: string; + column: string; +} + +const getSeatLayout = async (auditoriumId: string): Promise => { + const res = await api.get(`/theaters/seat/${auditoriumId}`); + return res.data.data; +}; // 평점/개수 포함된 좌석 배치도 조회 +export interface SeatRatingInfo { + seatId: string; + row: string; + column: number; + totalReviews: number; + averageRating: number; + isWheelchair: boolean; + type: string; +} +const getSeatRatingMap = async (auditoriumId: string): Promise => { + const res = await api.get>(`/theaters/seat/rating/${auditoriumId}`); + return res.data.data; +}; // 상영관 상세 조회 export interface GetTheatersDetailResponse { @@ -67,4 +90,4 @@ const getTheaterSummary = async (auditoriumId: string): Promise 좌석 정보 클릭 시 @@ -19,13 +20,7 @@ interface SeatModalProps { selectedSeatNumbers?: string[]; // seatFocus에서만 사용 } -const SeatModal = ({ - type, - auditoriumId, - theaterName, - theaterType, - selectedSeatNumbers, -}: SeatModalProps) => { +const SeatModal = ({ type, auditoriumId, theaterName, selectedSeatNumbers }: SeatModalProps) => { switch (type) { case 'seatFocus': return ( @@ -39,12 +34,18 @@ const SeatModal = ({ return ( useModalStore.getState().closeModal()} /> ); case 'seatWrite': - return ; + return ( + useModalStore.getState().closeModal()} + /> + ); default: return null; } diff --git a/src/components/common/Modal/SeatModal/SeatPickerModal.tsx b/src/components/common/Modal/SeatModal/SeatPickerModal.tsx index a05bf49..b17e188 100644 --- a/src/components/common/Modal/SeatModal/SeatPickerModal.tsx +++ b/src/components/common/Modal/SeatModal/SeatPickerModal.tsx @@ -1,29 +1,44 @@ import { SeatMap } from '@/components'; -import { useModalStore } from '@/store/modalStore'; import { CloseIcon } from '@/assets'; import ScreenBar from '@/components/seat/ScreenBar'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import type { CinemaFormat } from '@/types/onboarding'; +import { getSeatRatingMap, type SeatRatingInfo } from '@/api/theater/theater.api'; +import type { ApiError } from '@/types/api-response'; interface SeatPickerModalProps { - theaterType: CinemaFormat; + theaterType?: string; theaterName: string; auditoriumId: string; + seatData?: SeatRatingInfo[]; + onClose: () => void; } -const SeatPickerModal = ({ auditoriumId }: SeatPickerModalProps) => { - const { closeModal } = useModalStore(); +const SeatPickerModal = ({ auditoriumId, onClose }: SeatPickerModalProps) => { const nav = useNavigate(); const scrollContainerRef = useRef(null); const innerRef = useRef(null); + const [seatData, setSeatData] = useState([]); const handleSeatClick = (seatId: string) => { - closeModal(); + onClose(); nav(`/seat/review/${seatId}`); }; + useEffect(() => { + const fetchSeatData = async () => { + try { + const res = await getSeatRatingMap(auditoriumId); + setSeatData(res); + } catch (error) { + const apiError = error as ApiError; + console.error('좌석 정보 불러오기 실패:', apiError.error, apiError.message); + } + }; + fetchSeatData(); + }, [auditoriumId]); + useEffect(() => { const container = scrollContainerRef.current; const inner = innerRef.current; @@ -35,7 +50,7 @@ const SeatPickerModal = ({ auditoriumId }: SeatPickerModalProps) => { return (
-
+
{/* 모달 */}
@@ -43,7 +58,7 @@ const SeatPickerModal = ({ auditoriumId }: SeatPickerModalProps) => { {/* 헤더 */}
좌석의 후기를 볼 수 있어요
- +
@@ -57,7 +72,7 @@ const SeatPickerModal = ({ auditoriumId }: SeatPickerModalProps) => {
diff --git a/src/components/common/Modal/SeatModal/SeatWriteModal.tsx b/src/components/common/Modal/SeatModal/SeatWriteModal.tsx index aa578de..a1d3a33 100644 --- a/src/components/common/Modal/SeatModal/SeatWriteModal.tsx +++ b/src/components/common/Modal/SeatModal/SeatWriteModal.tsx @@ -1,18 +1,44 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { CloseIcon } from '@/assets'; -import { useModalStore } from '@/store/modalStore'; import ScreenBar from '@/components/seat/ScreenBar'; import SeatMap from '@/components/seat/SeatMap'; import { useSelectedSeatsStore } from '@/store'; import { Button } from '@/components'; +import { getSeatLayout } from '@/api/theater/theater.api'; +import type { SeatRatingInfo } from '@/api/theater/theater.api'; +import type { ApiError } from '@/types/api-response'; +import { useReviewStore } from '@/store'; interface SeatWriteModalProps { auditoriumId: string; theaterName: string; + onClose: (seatNames: string[]) => void; } -const SeatWriteModal = ({ auditoriumId, theaterName }: SeatWriteModalProps) => { - const { closeModal } = useModalStore(); +const SeatWriteModal = ({ auditoriumId, theaterName, onClose }: SeatWriteModalProps) => { + const [seatData, setSeatData] = useState([]); + const { addSeat, addSeatId } = useReviewStore(); + useEffect(() => { + const fetchSeatLayout = async () => { + try { + const res = await getSeatLayout(auditoriumId); + const parsed = res.map((seat) => ({ + ...seat, + column: Number(seat.column), + totalReviews: 0, + averageRating: 0, + type: 'NO_REVIEW', + isWheelchair: false, + })); + setSeatData(parsed); + } catch (error) { + const apiError = error as ApiError; + console.error('좌석 배치도 조회 실패:', apiError.message, apiError.error); + } + }; + + fetchSeatLayout(); + }, [auditoriumId]); // 선택 좌석 전역상태 관리 const { selectedSeats, toggleSeat } = useSelectedSeatsStore(); @@ -23,6 +49,17 @@ const SeatWriteModal = ({ auditoriumId, theaterName }: SeatWriteModalProps) => { toggleSeat(seatId); console.log('seatId : ', seatId); }; + const handleComplete = () => { + const selectedSeatObjs = seatData.filter((seat) => selectedSeats.includes(seat.seatId)); + + selectedSeatObjs.forEach((seat) => { + addSeat(`${seat.row}${seat.column}`); + addSeatId(seat.seatId); + }); + + const selectedNames = selectedSeatObjs.map((seat) => `${seat.row}${seat.column}`); + onClose(selectedNames); + }; const isDisabled = selectedSeats.length === 0; @@ -37,13 +74,13 @@ const SeatWriteModal = ({ auditoriumId, theaterName }: SeatWriteModalProps) => { return (
-
+
onClose([])} />
좌석을 선택해주세요
- + onClose([])} />
{theaterName}
@@ -53,14 +90,15 @@ const SeatWriteModal = ({ auditoriumId, theaterName }: SeatWriteModalProps) => {
-
-
diff --git a/src/components/review/TagSection.tsx b/src/components/review/TagSection.tsx index 5d10364..bd11a19 100644 --- a/src/components/review/TagSection.tsx +++ b/src/components/review/TagSection.tsx @@ -3,9 +3,9 @@ import { Button } from '@/components'; type TagSectionProps = { title: string; required?: boolean; - options: string[]; - selected: string[]; - onChange: (value: string) => void; + options: { id: number; label: string }[]; + selected: number[]; + onChange: (id: number) => void; }; export default function TagSection({ @@ -21,17 +21,17 @@ export default function TagSection({ {title} {required && *}
- {options.map((option) => ( + {options.map(({ id, label }) => ( ))}
diff --git a/src/components/seat/SeatMap.tsx b/src/components/seat/SeatMap.tsx index b55075d..17c7298 100644 --- a/src/components/seat/SeatMap.tsx +++ b/src/components/seat/SeatMap.tsx @@ -1,5 +1,5 @@ import SeatRow from './SeatRow'; -import { getMockSeats } from '@/__mocks/mockSeat'; +import type { SeatRatingInfo } from '@/api/theater/theater.api'; import type { Seat } from '@/types/seat'; import { useRef } from 'react'; @@ -8,7 +8,7 @@ interface SeatMapProps { onSeatClick?: (seatId: string) => void; focusedSeatIds?: string[]; // seatFocus용 selectedSeatNames?: string[]; // seatWrite 용 - isMock?: boolean; // 목데이터용 + seatData?: SeatRatingInfo[]; type?: 'seatFocus' | 'seatPicker' | 'seatWrite'; } @@ -17,13 +17,13 @@ const SeatMap = ({ focusedSeatIds = [], selectedSeatNames = [], type = 'seatPicker', + seatData = [], }: SeatMapProps) => { const seatRows: Record = {}; const focusedRef = useRef(null!); - const mockSeats = getMockSeats(type, focusedSeatIds); // row별로 묶기 - mockSeats.forEach((seat) => { + seatData.forEach((seat) => { if (!seatRows[seat.row]) seatRows[seat.row] = []; seatRows[seat.row].push(seat); }); diff --git a/src/components/seat/SeatRow.tsx b/src/components/seat/SeatRow.tsx index b341f71..87f0ee9 100644 --- a/src/components/seat/SeatRow.tsx +++ b/src/components/seat/SeatRow.tsx @@ -30,6 +30,7 @@ const SeatRow = ({ seatLabel={getSeatLabel(seat.row, seat.column)} isWheelchair={seat.isWheelchair} onClick={onSeatClick} + type={seat.type} row={seat.row} column={seat.column} isFocused={type === 'seatFocus' && focusedSeatIds.includes(seat.seatId)} diff --git a/src/components/seat/TestSeatButton.tsx b/src/components/seat/TestSeatButton.tsx deleted file mode 100644 index 041b4a2..0000000 --- a/src/components/seat/TestSeatButton.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useModalStore } from '@/store'; -import { ConfirmModal, SeatPickerModal } from '@/components'; -import SeatFocusModal from '../common/Modal/SeatModal/SeatFocusModal'; -import SeatWriteModal from '../common/Modal/SeatModal/SeatWriteModal'; - -export const TestSeatModalButton = () => { - const { openModal, modalType } = useModalStore(); - - const handleOpen = () => { - openModal('seatPicker'); - }; - - const handleOpenConfirm = () => { - openModal('confirm'); - }; - - const handleOpenFocus = () => { - openModal('seatFocus'); - }; - const handleOpenWrite = () => { - openModal('seatWrite'); - }; - - return ( - <> - - - - - - - - - {modalType === 'seatPicker' && ( - - )} - - {modalType === 'confirm' && ( - - )} - - {modalType === 'seatFocus' && ( - - )} - {modalType === 'seatWrite' && } - - ); -}; diff --git a/src/pages/home/PopularReview.tsx b/src/pages/home/PopularReview.tsx index f0f88f0..e57577b 100644 --- a/src/pages/home/PopularReview.tsx +++ b/src/pages/home/PopularReview.tsx @@ -36,7 +36,7 @@ const PopularReviewPage = () => { title={review.title} description={review.content} likeCount={review.heartCount} - onClick={() => navigate(`review/${review.reviewId}`)} + onClick={() => navigate(`/review/${review.reviewId}`)} /> ))}
diff --git a/src/pages/home/ReviewDetail.tsx b/src/pages/home/ReviewDetail.tsx index a1d4ca5..1f24f39 100644 --- a/src/pages/home/ReviewDetail.tsx +++ b/src/pages/home/ReviewDetail.tsx @@ -16,7 +16,7 @@ import { getReviewDetail } from '@/api/review/getReviewDetail.api'; import type { ReviewDetail } from '@/types/review'; import type { ApiError } from '@/types/api-response'; import { cn } from '@/utils/cn'; -import { useModalStore, useToastStore } from '@/store'; +import { useModalStore, useReviewStore, useToastStore } from '@/store'; import ActionModal from '@/components/common/Modal/ActionModal'; import { deleteReview } from '@/api/review/reviewDelete.api'; @@ -29,6 +29,7 @@ const ReviewDetailPage = () => { const [loading, setLoading] = useState(true); const { openModal, modalType } = useModalStore(); const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const { setEditMode, setEditReview } = useReviewStore(); //사진 슬라이드 시 현재 사진 위치... const [currentIndex, setCurrentIndex] = useState(0); @@ -142,7 +143,7 @@ const ReviewDetailPage = () => {
-
{review.auditoriumName}
+
{review.title}
{/*유저 정보, 추후 API 연결 시 프로필 사진 받아와서 조건부로...*/}
@@ -177,7 +178,7 @@ const ReviewDetailPage = () => {
-
+
{/*해시태그 영역*/} @@ -204,7 +205,39 @@ const ReviewDetailPage = () => {
{modalType === 'action' && !isConfirmOpen && ( console.log('수정하기 클릭')} + onEdit={() => { + if (!reviewId || !review) return; + setEditMode(Number(reviewId)); + + // 상태 초기화 + setEditReview({ + reviewTitle: review.title, + movieTitle: review.movieTitle, + cinema: { + name: review.auditoriumName, + hall: review.auditoriumName, + id: '', + }, + seats: review.seatInfo.map((s) => s.seatNumber), + seatIds: review.seatInfo.map((s) => s.seatId), + text: review.content, + rating: review.rating, + tags: { + 음향: review.hashtags + .filter((tag) => tag.hashTagType === '음향') + .map((t) => t.hashTagId), + 관람환경: review.hashtags + .filter((tag) => tag.hashTagType === '관람환경') + .map((t) => t.hashTagId), + 동반인: review.hashtags + .filter((tag) => tag.hashTagType === '동반인') + .map((t) => t.hashTagId), + }, + }); + + closeModal(); // 모달 닫기 + navigate(`/review/edit/${reviewId}/rating`); // 수정 페이지로 이동 + }} onDelete={() => { setIsConfirmOpen(true); closeModal(); diff --git a/src/pages/home/SeatReviewPage.tsx b/src/pages/home/SeatReviewPage.tsx index 647f2c3..56277eb 100644 --- a/src/pages/home/SeatReviewPage.tsx +++ b/src/pages/home/SeatReviewPage.tsx @@ -45,7 +45,7 @@ const SeatReviewPage = () => { return (
-
navigate('')} className="bg-gray-900" /> +
navigate(-1)} className="bg-gray-900" />
{!loading && !seatData && ( diff --git a/src/pages/home/TheaterDetail.tsx b/src/pages/home/TheaterDetail.tsx index 7198690..db3144d 100644 --- a/src/pages/home/TheaterDetail.tsx +++ b/src/pages/home/TheaterDetail.tsx @@ -9,6 +9,7 @@ import { getAuditoriumReviews } from '@/api/review/getAuditoriumReviews.api'; import type { ReviewSummary } from '@/types/review'; import { getTheaterTags } from '@/api/hashtag/hashtag.api'; import type { TheaterHashtag } from '@/api/hashtag/hashtag.api'; +import { SeatPickerModal } from '@/components'; //추후 태그 타입 들어오면 수정하기 const CinemaDetailPage = () => { @@ -20,6 +21,7 @@ const CinemaDetailPage = () => { const [summary, setSummary] = useState(null); const [hashtags, setHashtags] = useState([]); const [loading, setLoading] = useState(true); + const [isSeatModalOpen, setIsSeatModalOpen] = useState(false); useEffect(() => { if (!auditoriumId) return; @@ -131,7 +133,13 @@ const CinemaDetailPage = () => { {/*배치도 사진 들어갈 부분*/}
- {cinema?.imageUrl && } + {cinema?.imageUrl && ( + setIsSeatModalOpen(true)} + /> + )}
@@ -176,6 +184,13 @@ const CinemaDetailPage = () => {
+ {isSeatModalOpen && cinema && ( + setIsSeatModalOpen(false)} + /> + )}
); }; diff --git a/src/pages/review/MovieInfoStep.tsx b/src/pages/review/MovieInfoStep.tsx index 790f0d4..fda915b 100644 --- a/src/pages/review/MovieInfoStep.tsx +++ b/src/pages/review/MovieInfoStep.tsx @@ -1,13 +1,13 @@ import { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import { Badge, InputField, ReviewStepLayout } from '@/components'; +import { Badge, InputField, ReviewStepLayout, SeatWriteModal } from '@/components'; import { useReviewStore } from '@/store'; export default function MovieInfoForm() { - const { movieTitle, setTitle, cinema, setCinema, seats, addSeat, removeSeat, isInitialized } = + const { movieTitle, setTitle, cinema, setCinema, seats, removeSeat, isInitialized, resetSeats } = useReviewStore(); - const [seatInput, setSeatInput] = useState(''); + const [showSeatModal, setShowSeatModal] = useState(false); const navigate = useNavigate(); const location = useLocation(); @@ -22,19 +22,12 @@ export default function MovieInfoForm() { useEffect(() => { if (location.state?.cinema) { setCinema(location.state.cinema); + resetSeats(); } - }, [location.state?.cinema, setCinema]); + }, [location.state?.cinema, setCinema, resetSeats]); const isFormValid = movieTitle.trim() && cinema?.name && seats.length > 0; - const handleAddSeat = () => { - const trimmed = seatInput.trim(); - if (trimmed && !seats.includes(trimmed)) { - addSeat(trimmed); - setSeatInput(''); - } - }; - const handleNext = () => { navigate('/review/rating'); }; @@ -72,11 +65,12 @@ export default function MovieInfoForm() { {/*추후 좌석 페이지 연결 시 readOnly 속성 추가*/} {}} placeholder="관람하신 좌석을 선택해주세요" helperText="좌석을 여러 개 추가할 수 있어요." - onClickPlus={handleAddSeat} + readOnly + onClickPlus={() => setShowSeatModal(true)} />
@@ -87,6 +81,15 @@ export default function MovieInfoForm() { ))}
+ {showSeatModal && cinema?.id && ( + { + setShowSeatModal(false); + }} + auditoriumId={cinema.id} + theaterName={cinema.name} + /> + )} ); } diff --git a/src/pages/review/RatingStep.tsx b/src/pages/review/RatingStep.tsx index 51c46ca..c6bfbe5 100644 --- a/src/pages/review/RatingStep.tsx +++ b/src/pages/review/RatingStep.tsx @@ -1,5 +1,5 @@ import { useReviewStore } from '@/store'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { StarFill, StarHalf, StarLine } from '@/assets'; import { ReviewStepLayout } from '@/components'; import { useEffect } from 'react'; @@ -8,12 +8,14 @@ import { calculateRatingClick } from '@/utils/rating'; const RatingStep = () => { const { isInitialized, rating, setRating } = useReviewStore(); const navigate = useNavigate(); + const { reviewId } = useParams<{ reviewId: string }>(); + const isEdit = !!reviewId; useEffect(() => { - if (!isInitialized) { + if (!isInitialized && !isEdit) { navigate('/review'); } - }, [isInitialized, navigate]); + }, [isInitialized, isEdit, navigate]); const handleClick = (e: React.MouseEvent, value: number) => { const finalValue = calculateRatingClick(e, value); @@ -21,7 +23,11 @@ const RatingStep = () => { }; const handleNext = () => { - navigate('/review/tag'); + if (isEdit) { + navigate(`/review/edit/${reviewId}/tag`); + } else { + navigate('/review/tag'); + } }; return ( diff --git a/src/pages/review/ReviewContent.tsx b/src/pages/review/ReviewContent.tsx index e240529..a5c2c5c 100644 --- a/src/pages/review/ReviewContent.tsx +++ b/src/pages/review/ReviewContent.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { ReviewStepLayout, Textarea, @@ -8,16 +7,22 @@ import { } from '@/components'; import { PlusIcon } from '@/assets'; import { useReviewStore, useModalStore } from '@/store'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useImgUpload } from '@/hooks'; +import { postReview } from '@/api/review/review'; +import type { ApiError } from '@/types/api-response'; +import { patchReview } from '@/api/review/reviewRewrite.api'; const MAX_IMAGES = 5; const MIN_TEXT_LENGTH = 30; export default function ReviewTextForm() { - const { text, setText, reviewTitle, setReviewTitle, isInitialized } = useReviewStore(); + const { text, setText, reviewTitle, setReviewTitle, movieTitle, seatIds, rating, tags, reset } = + useReviewStore(); const { images, addImages, removeImage, previewUrls } = useImgUpload(5); const navigate = useNavigate(); + const { reviewId } = useParams<{ reviewId: string }>(); + const isEdit = !!reviewId; const isValid = reviewTitle.trim().length > 0 && text.trim().length >= MIN_TEXT_LENGTH; const { openModal, modalType, closeModal } = useModalStore(); @@ -25,11 +30,44 @@ export default function ReviewTextForm() { if (!text.trim()) return; openModal('confirm'); }; - useEffect(() => { - if (!isInitialized) { - navigate('/review'); + + const handleConfirmSubmit = async () => { + try { + const hashtagIds: number[] = Object.values(tags).flat(); + + if (isEdit) { + await patchReview(Number(reviewId), { + title: reviewTitle, + rating, + content: text, + hashtags: hashtagIds, + images: [], + }); + + closeModal(); + navigate(`/review/${reviewId}`); + reset(); + return; + } + + const { reviewId: newId } = await postReview({ + seatIds, + title: reviewTitle, + movieTitle, + rating, + content: text, + hashtags: hashtagIds, + imageUrl: [], + }); + + closeModal(); + navigate(`/review/${newId}`); + reset(); + } catch (error) { + const apiError = error as ApiError; + console.error('리뷰 등록 실패:', apiError.message, apiError.error); } - }, [isInitialized, navigate]); + }; return ( <> @@ -102,12 +140,9 @@ export default function ReviewTextForm() { subtitle="등록한 후기는 마이페이지에서 확인할 수 있어요." cancelText="취소" confirmText="등록하기" - onConfirm={() => { - console.log('후기 등록 로직'); - closeModal(); - }} + onConfirm={handleConfirmSubmit} /> )} ); -} \ No newline at end of file +} diff --git a/src/pages/review/TagPage.tsx b/src/pages/review/TagPage.tsx index 4caef58..e20d3d6 100644 --- a/src/pages/review/TagPage.tsx +++ b/src/pages/review/TagPage.tsx @@ -1,15 +1,16 @@ import { ReviewStepLayout, TagSection } from '@/components'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useReviewStore } from '@/store'; import { useEffect, useState } from 'react'; import type { Hashtag } from '@/types/hashtag'; import { getHashtags } from '@/api/hashtag/hashtag.api'; -import { - TAG_TYPE_TITLE_MAP, - REQUIRED_TAG_KEYS, - type TagKey, - type TagSectionConfig, -} from '@/constants'; +import { TAG_TYPE_TITLE_MAP, REQUIRED_TAG_KEYS, type TagKey } from '@/constants'; +type TagSectionConfig = { + key: TagKey; + title: string; + required: boolean; + options: { id: number; label: string }[]; +}; export default function ReviewTagsPage() { const navigate = useNavigate(); @@ -17,32 +18,38 @@ export default function ReviewTagsPage() { const [tagSections, setTagSections] = useState([]); const [isLoading, setIsLoading] = useState(true); const canProceed = REQUIRED_TAG_KEYS.every((key) => tags[key].length > 0); + const { reviewId } = useParams<{ reviewId: string }>(); + const isEdit = !!reviewId; const handleNext = () => { - navigate('/review/form'); + if (isEdit) { + navigate(`/review/edit/${reviewId}/form`); + } else { + navigate('/review/form'); + } }; - //초기 진입 조건 확인 + // 초기 진입 조건 확인 useEffect(() => { - if (!isInitialized) { + if (!isEdit && !isInitialized) { navigate('/review'); } - }, [isInitialized, navigate]); + }, [isEdit, isInitialized, navigate]); - //해시태그 API + // 해시태그 API useEffect(() => { const fetchTags = async () => { try { const data: Hashtag[] = await getHashtags(); - const grouped = data.reduce>( + const grouped = data.reduce>( (acc, tag) => { const key = tag.hashTagType as TagKey; if (!acc[key]) acc[key] = []; - acc[key].push(`#${tag.hashTagName}`); + acc[key].push({ id: tag.hashTagId, label: `#${tag.hashTagName}` }); return acc; }, - {} as Record, + {} as Record, ); const ORDERED_KEYS: TagKey[] = ['음향', '관람환경', '동반인']; @@ -83,7 +90,7 @@ export default function ReviewTagsPage() { options={options} required={required} selected={tags[key]} - onChange={(value) => toggleTag(key, value)} + onChange={(tagId) => toggleTag(key, tagId)} /> )) )} diff --git a/src/pages/seat/SeatTest.tsx b/src/pages/seat/SeatTest.tsx deleted file mode 100644 index 23ee818..0000000 --- a/src/pages/seat/SeatTest.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { TestSeatModalButton } from '@/components/seat/TestSeatButton'; - -const SeatTest = () => { - return ( - <> - - - ); -}; - -export default SeatTest; diff --git a/src/routes/route.tsx b/src/routes/route.tsx index 031a58b..bd4fac2 100644 --- a/src/routes/route.tsx +++ b/src/routes/route.tsx @@ -16,8 +16,6 @@ import TheaterReviewListPage from '@/pages/home/TheaterReviewListPage'; import ReviewDetailPage from '@/pages/home/ReviewDetail'; import PopularReviewPage from '@/pages/home/PopularReview'; -import SeatTest from '@/pages/seat/SeatTest'; - import { TicketUploadStep } from '@/pages/review/TicketPage'; import MovieInfoForm from '@/pages/review/MovieInfoStep'; import CinemaSelect from '@/pages/review/CinemaSelect'; @@ -92,10 +90,6 @@ const router = createBrowserRouter([ path: '/review/popular', element: , }, - { - path: '/seat', - element: , - }, { path: '/seat/review/:seatId', element: , @@ -129,6 +123,23 @@ const router = createBrowserRouter([ }, ], }, + { + path: '/review/edit/:reviewId', + children: [ + { + path: 'rating', + element: , + }, + { + path: 'tag', + element: , + }, + { + path: 'form', + element: , + }, + ], + }, { path: '/search', element: , diff --git a/src/store/useReviewStore.ts b/src/store/useReviewStore.ts index e272135..88c4dea 100644 --- a/src/store/useReviewStore.ts +++ b/src/store/useReviewStore.ts @@ -3,6 +3,7 @@ import { create } from 'zustand'; interface CinemaInfo { name: string; hall: string; + id: string; } interface ReviewState { @@ -13,6 +14,23 @@ interface ReviewState { seats: string[]; text: string; rating: number; + seatIds: string[]; + resetSeats: () => void; + isEdit: boolean; + editReviewId: number | null; + setEditMode: (id: number) => void; + setEditReview: (review: { + reviewTitle: string; + movieTitle: string; + cinema: CinemaInfo; + seats: string[]; + seatIds: string[]; + text: string; + rating: number; + tags: Record<'음향' | '관람환경' | '동반인', number[]>; + }) => void; + addSeatId: (id: string) => void; + removeSeatId: (id: string) => void; setRating: (value: number) => void; setTitle: (title: string) => void; setCinema: (cinema: CinemaInfo) => void; @@ -22,9 +40,9 @@ interface ReviewState { isInitialized: boolean; setInitialized: () => void; reset: () => void; - tags: Record; - setTags: (type: '음향' | '관람환경' | '동반인', tags: string[]) => void; - toggleTag: (type: '음향' | '관람환경' | '동반인', tag: string) => void; + tags: Record<'음향' | '관람환경' | '동반인', number[]>; + setTags: (type: '음향' | '관람환경' | '동반인', tags: number[]) => void; + toggleTag: (type: '음향' | '관람환경' | '동반인', tag: number) => void; } export const useReviewStore = create((set) => ({ @@ -36,9 +54,42 @@ export const useReviewStore = create((set) => ({ text: '', rating: 0, isInitialized: false, + isEdit: false, + editReviewId: null, + + setEditMode: (reviewId) => + set({ + isEdit: true, + editReviewId: reviewId, + }), + + setEditReview: (review) => + set({ + reviewTitle: review.reviewTitle, + movieTitle: review.movieTitle, + cinema: review.cinema, + seats: review.seats, + seatIds: review.seatIds, + text: review.text, + rating: review.rating, + tags: review.tags, + isInitialized: true, + }), setInitialized: () => set({ isInitialized: true }), setRating: (value) => set({ rating: value }), setTitle: (movieTitle) => set({ movieTitle }), + seatIds: [], + resetSeats: () => + set({ + seats: [], + seatIds: [], + }), + addSeatId: (id) => + set((state) => (state.seatIds.includes(id) ? state : { seatIds: [...state.seatIds, id] })), + removeSeatId: (id) => + set((state) => ({ + seatIds: state.seatIds.filter((s) => s !== id), + })), setCinema: (cinema) => set({ cinema }), addSeat: (seat) => set((state) => (state.seats.includes(seat) ? state : { seats: [...state.seats, seat] })), @@ -56,11 +107,14 @@ export const useReviewStore = create((set) => ({ rating: 0, text: '', tags: { - sound: [], - environment: [], - companion: [], + 음향: [], + 관람환경: [], + 동반인: [], }, + seatIds: [], isInitialized: false, + isEdit: false, + editReviewId: null, }), tags: { 음향: [], @@ -74,10 +128,10 @@ export const useReviewStore = create((set) => ({ [type]: tags, }, })), - toggleTag: (type, tag) => + toggleTag: (type, tagId) => set((state) => { const current = state.tags[type] ?? []; - const isSelected = current.includes(tag); + const isSelected = current.includes(tagId); const totalSelected = Object.values(state.tags).flat().length; @@ -85,7 +139,7 @@ export const useReviewStore = create((set) => ({ return { tags: { ...state.tags, - [type]: current.filter((t) => t !== tag), + [type]: current.filter((id) => id !== tagId), }, }; } @@ -93,7 +147,7 @@ export const useReviewStore = create((set) => ({ return { tags: { ...state.tags, - [type]: [...current, tag], + [type]: [...current, tagId], }, }; }