Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/api/review/review.ts
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<{ reviewId: number }>>('/reviews', data);
return res.data.data;
};
2 changes: 1 addition & 1 deletion src/api/review/reviewRewrite.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface ReviewUpdateRequest {
title: string;
rating: number;
content: string;
hashtags: string[];
hashtags: number[];
images: string[];
}

Expand Down
25 changes: 24 additions & 1 deletion src/api/theater/theater.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,31 @@ const getTheaters = async ({
};

// 좌석 배치도 조회
export interface SeatLayoutInfo {
seatId: string;
row: string;
column: string;
}

const getSeatLayout = async (auditoriumId: string): Promise<SeatLayoutInfo[]> => {
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<SeatRatingInfo[]> => {
const res = await api.get<ApiResponse<SeatRatingInfo[]>>(`/theaters/seat/rating/${auditoriumId}`);
return res.data.data;
};

// 상영관 상세 조회
export interface GetTheatersDetailResponse {
Expand Down Expand Up @@ -67,4 +90,4 @@ const getTheaterSummary = async (auditoriumId: string): Promise<TheaterSummaryRe
return res.data.data;
};

export { getTheaters, getTheatersDetail, getTheaterSummary };
export { getTheaters, getTheatersDetail, getTheaterSummary, getSeatRatingMap, getSeatLayout };
19 changes: 10 additions & 9 deletions src/components/common/Modal/SeatModal/SeatModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CinemaFormat } from '@/types/onboarding';
import SeatFocusModal from './SeatFocusModal';
import SeatPickerModal from './SeatPickerModal';
import SeatWriteModal from './SeatWriteModal';
import { useModalStore } from '@/store';

/*
* seatFocus: 리뷰 상세 조회 > 좌석 정보 클릭 시
Expand All @@ -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 (
Expand All @@ -39,12 +34,18 @@ const SeatModal = ({
return (
<SeatPickerModal
auditoriumId={auditoriumId}
theaterType={theaterType ?? 'IMAX'}
theaterName={theaterName ?? ''}
onClose={() => useModalStore.getState().closeModal()}
/>
);
case 'seatWrite':
return <SeatWriteModal auditoriumId={auditoriumId} theaterName={theaterName ?? ''} />;
return (
<SeatWriteModal
auditoriumId={auditoriumId}
theaterName={theaterName ?? ''}
onClose={() => useModalStore.getState().closeModal()}
/>
);
default:
return null;
}
Expand Down
35 changes: 25 additions & 10 deletions src/components/common/Modal/SeatModal/SeatPickerModal.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const [seatData, setSeatData] = useState<SeatRatingInfo[]>([]);

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;
Expand All @@ -35,15 +50,15 @@ const SeatPickerModal = ({ auditoriumId }: SeatPickerModalProps) => {

return (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-gray-800/20" onClick={closeModal} />
<div className="absolute inset-0 bg-gray-800/20" onClick={onClose} />

{/* 모달 */}
<div className="relative z-10 flex h-full items-center justify-center p-3">
<div className="relative w-full max-w-md rounded-lg bg-gray-950 px-2 py-3 md:max-h-3/4 md:max-w-2xl lg:max-h-4/5 lg:max-w-5xl">
{/* 헤더 */}
<div className="mb-1 flex items-center justify-between px-2">
<div className="text-title-3">좌석의 후기를 볼 수 있어요</div>
<CloseIcon className="cursor-pointer" onClick={closeModal} />
<CloseIcon className="cursor-pointer" onClick={onClose} />
</div>

<div className="text-body-2 btn-text-gray-500 mb-4 px-2 text-left">
Expand All @@ -57,7 +72,7 @@ const SeatPickerModal = ({ auditoriumId }: SeatPickerModalProps) => {
<SeatMap
type="seatPicker"
auditoriumId={auditoriumId}
isMock
seatData={seatData}
onSeatClick={handleSeatClick}
/>
</div>
Expand Down
54 changes: 46 additions & 8 deletions src/components/common/Modal/SeatModal/SeatWriteModal.tsx
Original file line number Diff line number Diff line change
@@ -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<SeatRatingInfo[]>([]);
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();
Expand All @@ -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;

Expand All @@ -37,13 +74,13 @@ const SeatWriteModal = ({ auditoriumId, theaterName }: SeatWriteModalProps) => {

return (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-gray-800/20" onClick={closeModal} />
<div className="absolute inset-0 bg-gray-800/20" onClick={() => onClose([])} />

<div className="relative z-10 flex h-full items-center justify-center p-3">
<div className="relative w-full max-w-md rounded-lg bg-gray-950 px-2 py-3 md:max-h-3/4 md:max-w-2xl lg:max-h-4/5 lg:max-w-5xl">
<div className="mb-1 flex items-center justify-between px-2">
<div className="text-title-3">좌석을 선택해주세요</div>
<CloseIcon className="cursor-pointer" onClick={closeModal} />
<CloseIcon className="cursor-pointer" onClick={() => onClose([])} />
</div>

<div className="text-body-2 btn-text-gray-500 mb-4 px-2 text-left">{theaterName}</div>
Expand All @@ -53,14 +90,15 @@ const SeatWriteModal = ({ auditoriumId, theaterName }: SeatWriteModalProps) => {
<ScreenBar />
<SeatMap
type="seatWrite"
seatData={seatData}
auditoriumId={auditoriumId}
onSeatClick={handleSeatClick}
selectedSeatNames={selectedSeats}
/>
</div>
</div>
<div className="px-4" onClick={closeModal}>
<Button className="mt-5 w-full" disabled={isDisabled}>
<div className="px-4">
<Button className="mt-5 w-full" disabled={isDisabled} onClick={handleComplete}>
선택 완료
</Button>
</div>
Expand Down
16 changes: 8 additions & 8 deletions src/components/review/TagSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -21,17 +21,17 @@ export default function TagSection({
{title} {required && <span className="text-red-400">*</span>}
</h2>
<div className="flex flex-wrap gap-2">
{options.map((option) => (
{options.map(({ id, label }) => (
<Button
key={option}
onClick={() => onChange(option)}
key={id}
onClick={() => onChange(id)}
variant="secondary-assistive"
color="gray"
size="xs"
rounded="md"
selected={selected.includes(option)}
selected={selected.includes(id)}
>
{option}
{label}
</Button>
))}
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/components/seat/SeatMap.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -8,7 +8,7 @@ interface SeatMapProps {
onSeatClick?: (seatId: string) => void;
focusedSeatIds?: string[]; // seatFocus용
selectedSeatNames?: string[]; // seatWrite 용
isMock?: boolean; // 목데이터용
seatData?: SeatRatingInfo[];
type?: 'seatFocus' | 'seatPicker' | 'seatWrite';
}

Expand All @@ -17,13 +17,13 @@ const SeatMap = ({
focusedSeatIds = [],
selectedSeatNames = [],
type = 'seatPicker',
seatData = [],
}: SeatMapProps) => {
const seatRows: Record<string, Seat[]> = {};
const focusedRef = useRef<HTMLDivElement>(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);
});
Expand Down
1 change: 1 addition & 0 deletions src/components/seat/SeatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
Loading