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
33 changes: 19 additions & 14 deletions src/components/common/Modal/SeatModal/SeatFocusModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,35 @@ const SeatFocusModal = ({
theaterName,
selectedSeatNumbers,
onClose,
focusedSeatIds,
}: SeatFocusModalProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);

const seatIds = selectedSeatNumbers.map((num) => getSeatLabel(auditoriumId, num));

useEffect(() => {
const container = containerRef.current;
const target = container?.querySelector('[data-seat-focus="true"]') as HTMLDivElement;
const timer = setTimeout(() => {
const container = containerRef.current;
const target = container?.querySelector('[data-seat-focus="true"]') as HTMLDivElement;

if (container && target) {
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
if (container && target) {
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();

const scrollTop = container.scrollTop + (targetRect.top - containerRect.top) - 100;
const scrollLeft = container.scrollLeft + (targetRect.left - containerRect.left) - 50;
const scrollTop = container.scrollTop + (targetRect.top - containerRect.top) - 100;
const scrollLeft = container.scrollLeft + (targetRect.left - containerRect.left) - 50;

container.scrollTo({
top: scrollTop,
left: scrollLeft,
behavior: 'smooth',
});
}
}, []);
container.scrollTo({
top: scrollTop,
left: scrollLeft,
behavior: 'smooth',
});
}
}, 50);

return () => clearTimeout(timer);
}, [focusedSeatIds]);

return (
<div className="fixed inset-0 z-50">
Expand Down
61 changes: 28 additions & 33 deletions src/components/seat/SeatMap.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import SeatRow from './SeatRow';
import type { SeatRatingInfo } from '@/api/theater/theater.api';
import { useEffect, useRef, useState } from 'react';
import type { Seat } from '@/types/seat';
import type { SeatRatingInfo } from '@/api/theater/theater.api';
import { getSeatLayout } from '@/api/theater/theater.api';
import { useEffect, useRef, useState } from 'react';
import type { ApiError } from '@/types/api-response';

interface SeatMapProps {
auditoriumId?: string;
onSeatClick?: (seatId: string) => void;
focusedSeatIds?: string[]; // seatFocus용
selectedSeatNames?: string[]; // seatWrite 용
focusedSeatIds?: string[];
selectedSeatNames?: string[];
seatData?: SeatRatingInfo[];
type?: 'seatFocus' | 'seatPicker' | 'seatWrite';
}
Expand All @@ -19,13 +19,13 @@ const SeatMap = ({
onSeatClick,
focusedSeatIds = [],
selectedSeatNames = [],
type = 'seatPicker',
seatData = [],
type = 'seatPicker',
}: SeatMapProps) => {
const [autoSeatData, setAutoSeatData] = useState<Seat[]>([]);
const focusedRef = useRef<HTMLDivElement>(null!);
const focusedRef = useRef<HTMLDivElement>(null);

// row별로 묶기
// seatFocus 모드일 때만 서버에서 좌석 배치도 불러오기
useEffect(() => {
const fetch = async () => {
if (type === 'seatFocus' && auditoriumId) {
Expand All @@ -35,7 +35,6 @@ const SeatMap = ({
...seat,
column: Number(seat.column),
}));

setAutoSeatData(parsedData);
} catch (error) {
const apiError = error as ApiError;
Expand All @@ -46,43 +45,39 @@ const SeatMap = ({
fetch();
}, [type, auditoriumId]);

// 사용할 seatData 결정
const dataToRender = type === 'seatFocus' ? autoSeatData : seatData;

// row별로 그룹핑 + 전체 column 범위 추출
const seatRows: Record<string, Seat[]> = {};
const allColumns = new Set<number>();

dataToRender.forEach((seat) => {
if (!seatRows[seat.row]) seatRows[seat.row] = [];
seatRows[seat.row].push(seat);
allColumns.add(seat.column);
});

const minColumn = Math.min(...Array.from(allColumns));
const maxColumn = Math.max(...Array.from(allColumns));

return (
<div className="flex flex-col gap-1">
{Object.entries(seatRows)
.sort(([a], [b]) => a.localeCompare(b))
.map(([row, seats]) => {
seats.sort((a, b) => a.column - b.column);

const min = seats[0].column;
const max = seats[seats.length - 1].column;

const filledRow: (Seat | null)[] = Array(max - min + 1).fill(null);

seats.forEach((seat) => {
const index = seat.column - min;
filledRow[index] = seat;
});

return (
<SeatRow
key={row}
rowSeats={filledRow}
onSeatClick={onSeatClick}
focusedSeatIds={focusedSeatIds}
selectedSeatNames={selectedSeatNames}
focusedRef={focusedRef}
type={type}
/>
);
})}
.map(([row, seats]) => (
<SeatRow
key={row}
rowSeats={seats}
onSeatClick={onSeatClick}
focusedSeatIds={focusedSeatIds}
selectedSeatNames={selectedSeatNames}
focusedRef={focusedRef as React.RefObject<HTMLDivElement>}
type={type}
minColumn={minColumn}
maxColumn={maxColumn}
/>
))}
</div>
);
};
Expand Down
15 changes: 13 additions & 2 deletions src/components/seat/SeatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import type { ReviewedSeat } from '@/types/seat';
import { getSeatLabel } from '@/utils/getSeatLabel';

interface SeatRowProps {
rowSeats: (ReviewedSeat | null)[];
rowSeats: ReviewedSeat[];
onSeatClick?: (seatId: string) => void;
focusedSeatIds?: string[];
selectedSeatNames?: string[];
focusedRef?: React.RefObject<HTMLDivElement>;
type?: 'seatFocus' | 'seatPicker' | 'seatWrite';
minColumn: number;
maxColumn: number;
}

const SeatRow = ({
Expand All @@ -18,10 +20,19 @@ const SeatRow = ({
selectedSeatNames = [],
focusedRef,
type = 'seatPicker',
minColumn,
maxColumn,
}: SeatRowProps) => {
const filledRow: (ReviewedSeat | null)[] = Array(maxColumn - minColumn + 1).fill(null);

rowSeats.forEach((seat) => {
const index = seat.column - minColumn;
filledRow[index] = seat;
});

return (
<div className="flex gap-1">
{rowSeats.map((seat, idx) =>
{filledRow.map((seat, idx) =>
seat ? (
<SeatItem
key={seat.seatId}
Expand Down
72 changes: 67 additions & 5 deletions src/pages/review/CinemaSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useNavigate } from 'react-router-dom';
import { ToggleTab, ReviewStepLayout, TheaterList } from '@/components';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useReviewStore } from '@/store';
import { useTheatersQuery } from '@/hooks/queries/useTheatersQuery';
import { getTheaters } from '@/api/theater/theater.api';
import type { CinemaFormat } from '@/types/onboarding';
import type { Theater } from '@/types/theater';

export default function CinemaSelect() {
const { isInitialized } = useReviewStore();
Expand All @@ -12,21 +13,81 @@ export default function CinemaSelect() {
const [selectedTab, setSelectedTab] = useState<CinemaFormat>('IMAX');
const [selectedAuditorium, setSelectedAuditorium] = useState<string | null>(null);

const { data: theaters } = useTheatersQuery({ type: selectedTab, page: 1, size: 10 });
// 무한스크롤용 상태
const [theaters, setTheaters] = useState<Theater[]>([]);
const [page, setPage] = useState(1);
const [hasNext, setHasNext] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const observerRef = useRef<HTMLDivElement>(null);

// 초기화 조건
useEffect(() => {
if (!isInitialized) {
navigate('/review');
}
}, [isInitialized, navigate]);

// theater 목록 초기화 (탭 변경 시)
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]);

// theater 추가 로딩
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, selectedTab, page]);

// IntersectionObserver 연결
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]);

// 탭 변경
const handleTabChange = (tab: string) => {
setSelectedTab(tab as CinemaFormat);
setSelectedAuditorium(null);
};

// 다음 단계로 이동
const handleNext = () => {
const selected = theaters?.find((d) => d.auditoriumId === selectedAuditorium);
const selected = theaters.find((d) => d.auditoriumId === selectedAuditorium);
if (!selected) return;

navigate('/review/info', {
Expand Down Expand Up @@ -58,10 +119,11 @@ export default function CinemaSelect() {

{/* 영화관 목록 */}
<TheaterList
data={theaters ?? []}
data={theaters}
selected={selectedAuditorium ? [selectedAuditorium] : []}
onSelect={(id) => setSelectedAuditorium(id)}
/>
{hasNext && <div ref={observerRef} className="h-[100px]" />}
</ReviewStepLayout>
);
}