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
1 change: 1 addition & 0 deletions src/api/rooms/getMyRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Room {
memberCount: number;
endDate: string;
type: string;
isPublic: boolean;
}

// 내 방 조회 응답 타입
Expand Down
4 changes: 2 additions & 2 deletions src/assets/common/filledSaveIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/components/common/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const Container = styled.div`
position: relative;
justify-content: center;
align-items: center;
width: 85px;
`;

const Text = styled.p`
Expand Down
15 changes: 8 additions & 7 deletions src/components/feed/BookInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import rightArrow from '../../assets/common/rightArrow.svg';
import { colors, typography } from '@/styles/global/global';

interface BookInfoCardProps {
bookTitle: string;
Expand Down Expand Up @@ -37,17 +38,17 @@ const BookContainer = styled.div`
align-items: center;
justify-content: space-between;
border-radius: 12px;
background: var(--color-darkgrey-main);
background: ${colors.darkgrey.main};
cursor: pointer;

.left {
overflow: hidden;
width: 220px;
white-space: nowrap;
color: var(--color-white);
color: ${colors.white};
text-overflow: ellipsis;
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
font-size: ${typography.fontSize.base};
font-weight: ${typography.fontWeight.semibold};
line-height: 24px;
}

Expand All @@ -56,12 +57,12 @@ const BookContainer = styled.div`
flex-direction: row;
gap: 4px;
overflow: hidden;
color: var(--color-grey-100);
color: ${colors.grey[100]};
text-align: right;
text-overflow: ellipsis;
font-size: var(--font-size-xs);
font-size: ${typography.fontSize.xs};
font-style: normal;
font-weight: var(--font-weight-regular);
font-weight: ${typography.fontWeight.regular};
line-height: 24px;

.name {
Expand Down
3 changes: 2 additions & 1 deletion src/components/group/GroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Props {
isRecommend?: boolean;
onClick?: () => void;
isFirstCard?: boolean;
isPublic?: boolean;
}

export const GroupCard = forwardRef<HTMLDivElement, Props>(
Expand All @@ -20,7 +21,7 @@ export const GroupCard = forwardRef<HTMLDivElement, Props>(
<Card ref={ref} cardType={type} isFirstCard={isFirstCard} onClick={onClick}>
<CoverWrapper>
<Cover src={group.coverUrl} alt="cover" cardType={type} isRecommend={isRecommend} />
{group.isOnGoing === false && (
{group.isPublic === false && (
<LockedOverlay>
<img src={lockedBookImg} alt="locked" />
</LockedOverlay>
Expand Down
1 change: 1 addition & 0 deletions src/components/group/MyGroupBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Group {
deadLine?: string;
genre?: string;
isOnGoing?: boolean;
isPublic?: boolean;
}

const convertJoinedRoomToGroup = (room: JoinedRoomItem): Group => ({
Expand Down
25 changes: 13 additions & 12 deletions src/components/group/MyGroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { forwardRef } from 'react';
import styled from '@emotion/styled';
import peopleImg from '../../assets/common/people.svg';
import type { Group } from './MyGroupBox';
import { colors, typography } from '@/styles/global/global';

interface MyGroupCardProps {
group: Group;
Expand Down Expand Up @@ -66,8 +67,8 @@ const Info = styled.div`
`;

const CardTitle = styled.h2`
font-size: var(--font-size-large01);
font-weight: var(--font-weight-semibold);
font-size: ${typography.fontSize.lg};
font-weight: ${typography.fontWeight.semibold};
color: #000;
margin: 0;
white-space: nowrap;
Expand All @@ -79,38 +80,38 @@ const Participants = styled.p`
display: flex;
align-items: center;
gap: 4px;
font-size: var(--font-size-small03);
font-weight: var(--font-weight-medium);
color: var(--color-grey-300);
font-size: ${typography.fontSize.xs};
font-weight: ${typography.fontWeight.medium};
color: ${colors.grey[300]};
margin: 8px 0;
> span {
line-height: 20px;
}
`;

const ProgressText = styled.p`
font-size: var(--font-size-medium01);
color: var(--color-grey-300);
font-size: ${typography.fontSize.sm};
color: ${colors.grey[300]};
margin: 12px 0;
`;

const Percent = styled.span`
font-size: var(--font-size-medium02);
color: var(--color-purple-main);
font-weight: var(--font-weight-semibold);
font-size: ${typography.fontSize.base};
color: ${colors.purple.main};
font-weight: ${typography.fontWeight.semibold};
`;

const Bar = styled.div`
width: 100%;
height: 6px;
background: var(--color-grey-300);
background: ${colors.grey[300]};
border-radius: 4px;
margin-top: 4px;
`;

const Fill = styled.div<{ width: number }>`
width: ${({ width }) => width}%;
height: 100%;
background-color: var(--color-purple-main);
background-color: ${colors.purple.main};
border-radius: 4px;
`;
161 changes: 138 additions & 23 deletions src/components/group/MyGroupModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import styled from '@emotion/styled';
import TitleHeader from '../common/TitleHeader';
import leftArrow from '../../assets/common/leftArrow.svg';
Expand All @@ -14,11 +14,20 @@ interface MyGroupModalProps {
}

export const MyGroupModal = ({ onClose }: MyGroupModalProps) => {
useEffect(() => {
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = '';
};
}, []);
const navigate = useNavigate();
const [selected, setSelected] = useState<'진행중' | '모집중' | ''>('');
const [rooms, setRooms] = useState<Room[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const [isLast, setIsLast] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);

const convertRoomToGroup = (room: Room): Group => {
return {
Expand All @@ -31,6 +40,7 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => {
deadLine: room.endDate || '',
genre: '',
isOnGoing: room.type === 'playing' || room.type === 'playingAndRecruiting',
isPublic: room.isPublic,
};
};

Expand All @@ -39,6 +49,8 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => {
try {
setIsLoading(true);
setError(null);
setNextCursor(null);
setIsLast(false);

const roomType: RoomType =
selected === '진행중'
Expand All @@ -51,6 +63,8 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => {

if (response.isSuccess) {
setRooms(response.data.roomList);
setNextCursor(response.data.nextCursor);
setIsLast(response.data.isLast);
} else {
setError(response.message);
}
Expand All @@ -65,6 +79,104 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => {
fetchRooms();
}, [selected]);

const isFetchingRef = useRef(false);

const loadMore = async () => {
if (isFetchingRef.current || isLast || !nextCursor) return;

isFetchingRef.current = true;
setIsLoading(true);
try {
const roomType: RoomType =
selected === '진행중'
? 'playing'
: selected === '모집중'
? 'recruiting'
: 'playingAndRecruiting';

const res = await getMyRooms(roomType, nextCursor);
if (res.isSuccess) {
setRooms(prev => [...prev, ...res.data.roomList]);
setNextCursor(res.data.nextCursor);
setIsLast(res.data.isLast);
} else {
setError(res.message);
}
} catch (e) {
console.log(e);
setError('방 목록을 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
isFetchingRef.current = false;
}
};

const handleScroll = async (e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget;
const { scrollTop, scrollHeight, clientHeight } = el;
if (scrollHeight - scrollTop - clientHeight < 100) {
await loadMore();
}
};

useEffect(() => {
const fetchRooms = async () => {
try {
setIsLoading(true);
setError(null);
setNextCursor(null);
setIsLast(false);

const roomType: RoomType =
selected === '진행중'
? 'playing'
: selected === '모집중'
? 'recruiting'
: 'playingAndRecruiting';

const res = await getMyRooms(roomType, null);
if (res.isSuccess) {
setRooms(res.data.roomList);
setNextCursor(res.data.nextCursor);
setIsLast(res.data.isLast);
} else {
setError(res.message);
}
} catch (e) {
console.log(e);
setError('방 목록을 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}
};

fetchRooms();
}, [selected]);

Comment on lines +122 to +155
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중복 데이터 로딩 useEffect로 인한 중복 호출/레이스 컨디션

선택 탭 변경 시 방 목록을 가져오는 useEffect가 동일 로직으로 두 번 존재합니다(라인 47-80, 122-155). 현재 구조는 API를 중복 호출하고 상태 업데이트 순서가 뒤엉킬 수 있습니다. 아래와 같이 두 번째 useEffect를 제거해 주세요.

-  useEffect(() => {
-    const fetchRooms = async () => {
-      try {
-        setIsLoading(true);
-        setError(null);
-        setNextCursor(null);
-        setIsLast(false);
-
-        const roomType: RoomType =
-          selected === '진행중'
-            ? 'playing'
-            : selected === '모집중'
-              ? 'recruiting'
-              : 'playingAndRecruiting';
-
-        const res = await getMyRooms(roomType, null);
-        if (res.isSuccess) {
-          setRooms(res.data.roomList);
-          setNextCursor(res.data.nextCursor);
-          setIsLast(res.data.isLast);
-        } else {
-          setError(res.message);
-        }
-      } catch (e) {
-        console.log(e);
-        setError('방 목록을 불러오는데 실패했습니다.');
-      } finally {
-        setIsLoading(false);
-      }
-    };
-
-    fetchRooms();
-  }, [selected]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const fetchRooms = async () => {
try {
setIsLoading(true);
setError(null);
setNextCursor(null);
setIsLast(false);
const roomType: RoomType =
selected === '진행중'
? 'playing'
: selected === '모집중'
? 'recruiting'
: 'playingAndRecruiting';
const res = await getMyRooms(roomType, null);
if (res.isSuccess) {
setRooms(res.data.roomList);
setNextCursor(res.data.nextCursor);
setIsLast(res.data.isLast);
} else {
setError(res.message);
}
} catch (e) {
console.log(e);
setError('방 목록을 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}
};
fetchRooms();
}, [selected]);
🤖 Prompt for AI Agents
src/components/group/MyGroupModal.tsx around lines 122 to 155, there is a
duplicate useEffect that fetches rooms causing double API calls and race
conditions; remove this second useEffect block entirely and ensure any unique
logic it contained is merged into the existing useEffect at lines ~47-80 (adjust
dependencies to include selected if not already), so room fetching runs only
once per selection change; after removal, run the app/tests to verify no missing
behavior or regressions.

useEffect(() => {
const tryFill = async () => {
if (!contentRef.current || isLast) return;
let guard = 2; // 최대 3페이지까지 자동 프리로드(필요시 늘리기)
while (
guard-- > 0 &&
contentRef.current &&
contentRef.current.scrollHeight <= contentRef.current.clientHeight &&
!isLast &&
nextCursor
) {
await loadMore();
await new Promise(requestAnimationFrame);
}
};
tryFill();
}, [rooms, nextCursor, isLast]);

useEffect(() => {
if (contentRef.current) {
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
}
}, [selected]);

const convertedGroups = rooms.map(convertRoomToGroup);

const handleGroupCardClick = (group: Group) => {
Expand Down Expand Up @@ -101,22 +213,22 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => {
))}
</TabContainer>

<Content>
{isLoading ? (
<LoadingMessage>로딩 중...</LoadingMessage>
) : error ? (
<ErrorMessage>{error}</ErrorMessage>
) : convertedGroups.length > 0 ? (
convertedGroups.map(group => (
<GroupCard
key={group.id}
group={group}
isOngoing={group.isOnGoing}
type={'modal'}
onClick={() => handleGroupCardClick(group)}
/>
))
) : (
<Content ref={contentRef} onScroll={handleScroll}>
{error && <ErrorMessage>{error}</ErrorMessage>}

{convertedGroups.map(group => (
<GroupCard
key={group.id}
group={group}
isOngoing={group.isOnGoing}
type="modal"
onClick={() => handleGroupCardClick(group)}
/>
))}

{isLoading && <BottomSpinner>불러오는 중…</BottomSpinner>}

{!isLoading && convertedGroups.length === 0 && (
<EmptyState>
<EmptyTitle>
{selected === '진행중'
Expand Down Expand Up @@ -163,21 +275,24 @@ const Content = styled.div`
gap: 20px;
overflow-y: auto;
padding: 0 20px 20px 20px;

grid-template-columns: 1fr;

scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
@media (min-width: 584px) {
grid-template-columns: 1fr 1fr;
}
`;

const LoadingMessage = styled.div`
const BottomSpinner = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
color: #fff;
font-size: ${typography.fontSize.base};
padding: 16px 0 24px;
color: ${colors.grey[100]};
font-size: ${typography.fontSize.sm};
`;

const ErrorMessage = styled.div`
Expand Down
Loading