Skip to content

Commit fb255ed

Browse files
authored
Merge pull request #94 from Leets-Official/feat/#92/좌석배치도-ui-수정
[Refactor] 좌석 배치도 아이템 수정
2 parents 66412b9 + ca35d2e commit fb255ed

4 files changed

Lines changed: 127 additions & 54 deletions

File tree

src/components/common/Modal/SeatModal/SeatFocusModal.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,35 @@ const SeatFocusModal = ({
1717
theaterName,
1818
selectedSeatNumbers,
1919
onClose,
20+
focusedSeatIds,
2021
}: SeatFocusModalProps) => {
2122
const containerRef = useRef<HTMLDivElement>(null);
2223
const innerRef = useRef<HTMLDivElement>(null);
2324

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

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

30-
if (container && target) {
31-
const containerRect = container.getBoundingClientRect();
32-
const targetRect = target.getBoundingClientRect();
32+
if (container && target) {
33+
const containerRect = container.getBoundingClientRect();
34+
const targetRect = target.getBoundingClientRect();
3335

34-
const scrollTop = container.scrollTop + (targetRect.top - containerRect.top) - 100;
35-
const scrollLeft = container.scrollLeft + (targetRect.left - containerRect.left) - 50;
36+
const scrollTop = container.scrollTop + (targetRect.top - containerRect.top) - 100;
37+
const scrollLeft = container.scrollLeft + (targetRect.left - containerRect.left) - 50;
3638

37-
container.scrollTo({
38-
top: scrollTop,
39-
left: scrollLeft,
40-
behavior: 'smooth',
41-
});
42-
}
43-
}, []);
39+
container.scrollTo({
40+
top: scrollTop,
41+
left: scrollLeft,
42+
behavior: 'smooth',
43+
});
44+
}
45+
}, 50);
46+
47+
return () => clearTimeout(timer);
48+
}, [focusedSeatIds]);
4449

4550
return (
4651
<div className="fixed inset-0 z-50">

src/components/seat/SeatMap.tsx

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import SeatRow from './SeatRow';
2-
import type { SeatRatingInfo } from '@/api/theater/theater.api';
2+
import { useEffect, useRef, useState } from 'react';
33
import type { Seat } from '@/types/seat';
4+
import type { SeatRatingInfo } from '@/api/theater/theater.api';
45
import { getSeatLayout } from '@/api/theater/theater.api';
5-
import { useEffect, useRef, useState } from 'react';
66
import type { ApiError } from '@/types/api-response';
77

88
interface SeatMapProps {
99
auditoriumId?: string;
1010
onSeatClick?: (seatId: string) => void;
11-
focusedSeatIds?: string[]; // seatFocus용
12-
selectedSeatNames?: string[]; // seatWrite 용
11+
focusedSeatIds?: string[];
12+
selectedSeatNames?: string[];
1313
seatData?: SeatRatingInfo[];
1414
type?: 'seatFocus' | 'seatPicker' | 'seatWrite';
1515
}
@@ -19,13 +19,13 @@ const SeatMap = ({
1919
onSeatClick,
2020
focusedSeatIds = [],
2121
selectedSeatNames = [],
22-
type = 'seatPicker',
2322
seatData = [],
23+
type = 'seatPicker',
2424
}: SeatMapProps) => {
2525
const [autoSeatData, setAutoSeatData] = useState<Seat[]>([]);
26-
const focusedRef = useRef<HTMLDivElement>(null!);
26+
const focusedRef = useRef<HTMLDivElement>(null);
2727

28-
// row별로 묶기
28+
// seatFocus 모드일 때만 서버에서 좌석 배치도 불러오기
2929
useEffect(() => {
3030
const fetch = async () => {
3131
if (type === 'seatFocus' && auditoriumId) {
@@ -35,7 +35,6 @@ const SeatMap = ({
3535
...seat,
3636
column: Number(seat.column),
3737
}));
38-
3938
setAutoSeatData(parsedData);
4039
} catch (error) {
4140
const apiError = error as ApiError;
@@ -46,43 +45,39 @@ const SeatMap = ({
4645
fetch();
4746
}, [type, auditoriumId]);
4847

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

51+
// row별로 그룹핑 + 전체 column 범위 추출
5152
const seatRows: Record<string, Seat[]> = {};
53+
const allColumns = new Set<number>();
54+
5255
dataToRender.forEach((seat) => {
5356
if (!seatRows[seat.row]) seatRows[seat.row] = [];
5457
seatRows[seat.row].push(seat);
58+
allColumns.add(seat.column);
5559
});
5660

61+
const minColumn = Math.min(...Array.from(allColumns));
62+
const maxColumn = Math.max(...Array.from(allColumns));
63+
5764
return (
5865
<div className="flex flex-col gap-1">
5966
{Object.entries(seatRows)
6067
.sort(([a], [b]) => a.localeCompare(b))
61-
.map(([row, seats]) => {
62-
seats.sort((a, b) => a.column - b.column);
63-
64-
const min = seats[0].column;
65-
const max = seats[seats.length - 1].column;
66-
67-
const filledRow: (Seat | null)[] = Array(max - min + 1).fill(null);
68-
69-
seats.forEach((seat) => {
70-
const index = seat.column - min;
71-
filledRow[index] = seat;
72-
});
73-
74-
return (
75-
<SeatRow
76-
key={row}
77-
rowSeats={filledRow}
78-
onSeatClick={onSeatClick}
79-
focusedSeatIds={focusedSeatIds}
80-
selectedSeatNames={selectedSeatNames}
81-
focusedRef={focusedRef}
82-
type={type}
83-
/>
84-
);
85-
})}
68+
.map(([row, seats]) => (
69+
<SeatRow
70+
key={row}
71+
rowSeats={seats}
72+
onSeatClick={onSeatClick}
73+
focusedSeatIds={focusedSeatIds}
74+
selectedSeatNames={selectedSeatNames}
75+
focusedRef={focusedRef as React.RefObject<HTMLDivElement>}
76+
type={type}
77+
minColumn={minColumn}
78+
maxColumn={maxColumn}
79+
/>
80+
))}
8681
</div>
8782
);
8883
};

src/components/seat/SeatRow.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import type { ReviewedSeat } from '@/types/seat';
33
import { getSeatLabel } from '@/utils/getSeatLabel';
44

55
interface SeatRowProps {
6-
rowSeats: (ReviewedSeat | null)[];
6+
rowSeats: ReviewedSeat[];
77
onSeatClick?: (seatId: string) => void;
88
focusedSeatIds?: string[];
99
selectedSeatNames?: string[];
1010
focusedRef?: React.RefObject<HTMLDivElement>;
1111
type?: 'seatFocus' | 'seatPicker' | 'seatWrite';
12+
minColumn: number;
13+
maxColumn: number;
1214
}
1315

1416
const SeatRow = ({
@@ -18,10 +20,19 @@ const SeatRow = ({
1820
selectedSeatNames = [],
1921
focusedRef,
2022
type = 'seatPicker',
23+
minColumn,
24+
maxColumn,
2125
}: SeatRowProps) => {
26+
const filledRow: (ReviewedSeat | null)[] = Array(maxColumn - minColumn + 1).fill(null);
27+
28+
rowSeats.forEach((seat) => {
29+
const index = seat.column - minColumn;
30+
filledRow[index] = seat;
31+
});
32+
2233
return (
2334
<div className="flex gap-1">
24-
{rowSeats.map((seat, idx) =>
35+
{filledRow.map((seat, idx) =>
2536
seat ? (
2637
<SeatItem
2738
key={seat.seatId}

src/pages/review/CinemaSelect.tsx

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { useNavigate } from 'react-router-dom';
22
import { ToggleTab, ReviewStepLayout, TheaterList } from '@/components';
3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useRef, useCallback } from 'react';
44
import { useReviewStore } from '@/store';
5-
import { useTheatersQuery } from '@/hooks/queries/useTheatersQuery';
5+
import { getTheaters } from '@/api/theater/theater.api';
66
import type { CinemaFormat } from '@/types/onboarding';
7+
import type { Theater } from '@/types/theater';
78

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

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

23+
// 초기화 조건
1724
useEffect(() => {
1825
if (!isInitialized) {
1926
navigate('/review');
2027
}
2128
}, [isInitialized, navigate]);
2229

30+
// theater 목록 초기화 (탭 변경 시)
31+
useEffect(() => {
32+
const reset = async () => {
33+
setPage(1);
34+
setTheaters([]);
35+
setHasNext(true);
36+
try {
37+
const res = await getTheaters({ type: selectedTab, page: 1, size: 10 });
38+
setTheaters(res.content);
39+
setHasNext(res.hasNext);
40+
setPage(2);
41+
} catch (err) {
42+
console.error('초기 로딩 실패:', err);
43+
}
44+
};
45+
reset();
46+
}, [selectedTab]);
47+
48+
// theater 추가 로딩
49+
const loadMore = useCallback(async () => {
50+
if (isLoading || !hasNext) return;
51+
setIsLoading(true);
52+
try {
53+
const res = await getTheaters({ type: selectedTab, page, size: 10 });
54+
setTheaters((prev) => [...prev, ...res.content]);
55+
setHasNext(res.hasNext);
56+
setPage((prev) => prev + 1);
57+
} catch (err) {
58+
console.error('추가 로딩 실패:', err);
59+
} finally {
60+
setIsLoading(false);
61+
}
62+
}, [isLoading, hasNext, selectedTab, page]);
63+
64+
// IntersectionObserver 연결
65+
useEffect(() => {
66+
const observer = new IntersectionObserver(
67+
([entry]) => {
68+
if (entry.isIntersecting && !isLoading && hasNext) {
69+
loadMore();
70+
}
71+
},
72+
{ rootMargin: '100px', threshold: 0.7 },
73+
);
74+
75+
if (observerRef.current) observer.observe(observerRef.current);
76+
77+
return () => {
78+
if (observerRef.current) observer.unobserve(observerRef.current);
79+
};
80+
}, [loadMore, isLoading, hasNext]);
81+
82+
// 탭 변경
2383
const handleTabChange = (tab: string) => {
2484
setSelectedTab(tab as CinemaFormat);
2585
setSelectedAuditorium(null);
2686
};
2787

88+
// 다음 단계로 이동
2889
const handleNext = () => {
29-
const selected = theaters?.find((d) => d.auditoriumId === selectedAuditorium);
90+
const selected = theaters.find((d) => d.auditoriumId === selectedAuditorium);
3091
if (!selected) return;
3192

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

59120
{/* 영화관 목록 */}
60121
<TheaterList
61-
data={theaters ?? []}
122+
data={theaters}
62123
selected={selectedAuditorium ? [selectedAuditorium] : []}
63124
onSelect={(id) => setSelectedAuditorium(id)}
64125
/>
126+
{hasNext && <div ref={observerRef} className="h-[100px]" />}
65127
</ReviewStepLayout>
66128
);
67129
}

0 commit comments

Comments
 (0)