From 1f37be919d4b57a34a190e13a755f697a5416c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Tue, 3 Feb 2026 16:46:49 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix=20:=20=EA=B7=B8=EB=A3=B9/=EB=A9=98?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=84=B0=EB=94=94=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20'=EB=AA=A8=EC=A7=91=EC=A4=91'=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit url 기반으로 판단하는게 확장성있는 판단일까 --- src/components/filtering/study-filter.tsx | 5 ++-- .../pages/group-study-list-page.tsx | 27 +++++++++++++++---- .../pages/premium-study-list-page.tsx | 25 ++++++++++++----- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/components/filtering/study-filter.tsx b/src/components/filtering/study-filter.tsx index 031dae3b..41cbdda6 100644 --- a/src/components/filtering/study-filter.tsx +++ b/src/components/filtering/study-filter.tsx @@ -167,15 +167,16 @@ export default function StudyFilter({ values, onChange }: StudyFilterProps) { type: [], targetRoles: [], method: [], - recruiting: false, + recruiting: true, // 기본값: 모집 중만 보기 }); }, [onChange]); + // recruiting은 기본값이므로 필터로 간주하지 않음 const hasAnyFilter = values.type.length > 0 || values.targetRoles.length > 0 || values.method.length > 0 || - values.recruiting; + !values.recruiting; // recruiting이 false면 필터 적용 중 return (
diff --git a/src/components/pages/group-study-list-page.tsx b/src/components/pages/group-study-list-page.tsx index 58c8d477..893229c1 100644 --- a/src/components/pages/group-study-list-page.tsx +++ b/src/components/pages/group-study-list-page.tsx @@ -38,12 +38,18 @@ export default function GroupStudyListPage() { const [searchQuery, setSearchQuery] = useState(''); // URL에서 필터 값 읽기 + // 기본값: recruiting = true (모집 중만 보기) + // 사용자가 토글을 조작하면 URL에 명시적으로 저장됨 const filterValues = useMemo(() => { const type = searchParams.get('type')?.split(',').filter(Boolean) ?? []; const targetRoles = searchParams.get('targetRoles')?.split(',').filter(Boolean) ?? []; const method = searchParams.get('method')?.split(',').filter(Boolean) ?? []; - const recruiting = searchParams.get('recruiting') === 'true'; + // URL에 recruiting 파라미터가 없으면 기본값 true (모집 중만 보기) + // 파라미터가 있으면 그 값 사용 (명시적 사용자 선택) + const recruitingParam = searchParams.get('recruiting'); + const recruiting = + recruitingParam === null ? true : recruitingParam === 'true'; return { type, targetRoles, method, recruiting }; }, [searchParams]); @@ -67,7 +73,8 @@ export default function GroupStudyListPage() { filterValues.method.length > 0 ? (filterValues.method as GetGroupStudiesMethodEnum[]) : undefined, - recruiting: filterValues.recruiting || undefined, + // 기본값: true (모집 중만), false면 전체 조회 + recruiting: filterValues.recruiting ? true : undefined, }); const allStudies = useMemo(() => data?.content ?? [], [data?.content]); @@ -78,10 +85,16 @@ export default function GroupStudyListPage() { const params = new URLSearchParams(searchParams.toString()); Object.entries(updates).forEach(([key, value]) => { - if (value === undefined || value === '' || value === 'false') { + if (value === undefined || value === '') { params.delete(key); } else { - params.set(key, value); + // recruiting의 경우 'false'도 명시적으로 저장 (기본값이 true이므로) + // 다른 필터는 'false'일 때 파라미터 제거 + if (key === 'recruiting' || value !== 'false') { + params.set(key, value); + } else { + params.delete(key); + } } }); @@ -97,6 +110,8 @@ export default function GroupStudyListPage() { ); // 필터 변경 핸들러 + // recruiting 토글: true면 'true' 저장, false면 'false' 명시적으로 저장 + // (기본값이 true이므로 false일 때도 명시적으로 저장해야 토글이 정상 작동) const handleFilterChange = useCallback( (values: StudyFilterValues) => { updateSearchParams({ @@ -106,7 +121,9 @@ export default function GroupStudyListPage() { ? values.targetRoles.join(',') : undefined, method: values.method.length > 0 ? values.method.join(',') : undefined, - recruiting: values.recruiting ? 'true' : undefined, + // recruiting: true면 'true', false면 'false' 명시적으로 저장 + // (기본값이 true이므로 false일 때도 URL에 저장해야 토글 해제 가능) + recruiting: values.recruiting ? 'true' : 'false', }); }, [updateSearchParams], diff --git a/src/components/pages/premium-study-list-page.tsx b/src/components/pages/premium-study-list-page.tsx index 0d9ed711..e0a3cef2 100644 --- a/src/components/pages/premium-study-list-page.tsx +++ b/src/components/pages/premium-study-list-page.tsx @@ -37,13 +37,15 @@ export default function PremiumStudyListPage() { // 로컬 검색 상태 const [searchQuery, setSearchQuery] = useState(''); - // URL에서 필터 값 읽기 + // URL에서 필터 값 읽기 (기본값: recruiting = true, 모집 중만 보기) const filterValues = useMemo(() => { const type = searchParams.get('type')?.split(',').filter(Boolean) ?? []; const targetRoles = searchParams.get('targetRoles')?.split(',').filter(Boolean) ?? []; const method = searchParams.get('method')?.split(',').filter(Boolean) ?? []; - const recruiting = searchParams.get('recruiting') === 'true'; + // URL에 recruiting 파라미터가 없으면 기본값 true (모집 중만 보기) + const recruitingParam = searchParams.get('recruiting'); + const recruiting = recruitingParam === null ? true : recruitingParam === 'true'; return { type, targetRoles, method, recruiting }; }, [searchParams]); @@ -67,7 +69,8 @@ export default function PremiumStudyListPage() { filterValues.method.length > 0 ? (filterValues.method as GetGroupStudiesMethodEnum[]) : undefined, - recruiting: filterValues.recruiting || undefined, + // 기본값: true (모집 중만), false면 전체 조회 + recruiting: filterValues.recruiting ? true : undefined, }); const allStudies = useMemo(() => data?.content ?? [], [data?.content]); @@ -78,10 +81,16 @@ export default function PremiumStudyListPage() { const params = new URLSearchParams(searchParams.toString()); Object.entries(updates).forEach(([key, value]) => { - if (value === undefined || value === '' || value === 'false') { + if (value === undefined || value === '') { params.delete(key); } else { - params.set(key, value); + // recruiting의 경우 'false'도 명시적으로 저장 (기본값이 true이므로) + // 다른 필터는 'false'일 때 파라미터 제거 + if (key === 'recruiting' || value !== 'false') { + params.set(key, value); + } else { + params.delete(key); + } } }); @@ -97,6 +106,8 @@ export default function PremiumStudyListPage() { ); // 필터 변경 핸들러 + // recruiting 토글: true면 'true' 저장, false면 'false' 명시적으로 저장 + // (기본값이 true이므로 false일 때도 명시적으로 저장해야 토글이 정상 작동) const handleFilterChange = useCallback( (values: StudyFilterValues) => { updateSearchParams({ @@ -106,7 +117,9 @@ export default function PremiumStudyListPage() { ? values.targetRoles.join(',') : undefined, method: values.method.length > 0 ? values.method.join(',') : undefined, - recruiting: values.recruiting ? 'true' : undefined, + // recruiting: true면 'true', false면 'false' 명시적으로 저장 + // (기본값이 true이므로 false일 때도 URL에 저장해야 토글 해제 가능) + recruiting: values.recruiting ? 'true' : 'false', }); }, [updateSearchParams], From 9f6cc594803dad05a05068dc2ba4b3a50d818204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Tue, 3 Feb 2026 22:09:18 +0900 Subject: [PATCH 2/6] chore : prettier --- src/components/pages/premium-study-list-page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/premium-study-list-page.tsx b/src/components/pages/premium-study-list-page.tsx index e0a3cef2..ac901cce 100644 --- a/src/components/pages/premium-study-list-page.tsx +++ b/src/components/pages/premium-study-list-page.tsx @@ -45,7 +45,8 @@ export default function PremiumStudyListPage() { const method = searchParams.get('method')?.split(',').filter(Boolean) ?? []; // URL에 recruiting 파라미터가 없으면 기본값 true (모집 중만 보기) const recruitingParam = searchParams.get('recruiting'); - const recruiting = recruitingParam === null ? true : recruitingParam === 'true'; + const recruiting = + recruitingParam === null ? true : recruitingParam === 'true'; return { type, targetRoles, method, recruiting }; }, [searchParams]); From 41b131338310c85506211a3a708134288ac4e9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Wed, 4 Feb 2026 00:49:23 +0900 Subject: [PATCH 3/6] =?UTF-8?q?chore=20:=20prettier=20=EC=97=90=EC=84=9C?= =?UTF-8?q?=20corderabbit=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index 555e66ac..cb737a04 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,7 @@ build/ # OpenAPI auto-generated files src/api/openapi -**/*.json \ No newline at end of file +**/*.json + +# CodeRabbit config +.coderabbit.yaml \ No newline at end of file From 92e916a27b8f53bade3e5d861c413877ac3cff46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Wed, 4 Feb 2026 02:59:06 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat=20:=20=EA=B7=B8=EB=A3=B9=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94/=EB=A9=98=ED=86=A0=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=EC=97=90=EC=84=9C=20=EB=82=B4=EA=B0=80=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=EC=A4=91=EC=9D=B8=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/group-study-list-page.tsx | 4 + .../pages/premium-study-list-page.tsx | 4 + .../my-participating-studies-section.tsx | 194 ++++++++++++++++++ .../study/group/api/group-study-types.ts | 4 +- 4 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 src/components/section/my-participating-studies-section.tsx diff --git a/src/components/pages/group-study-list-page.tsx b/src/components/pages/group-study-list-page.tsx index 893229c1..e9117aab 100644 --- a/src/components/pages/group-study-list-page.tsx +++ b/src/components/pages/group-study-list-page.tsx @@ -20,6 +20,7 @@ import { useGetStudies } from '@/hooks/queries/study-query'; import GroupStudyFormModal from '../../features/study/group/ui/group-study-form-modal'; import GroupStudyPagination from '../../features/study/group/ui/group-study-pagination'; import GroupStudyList from '../lists/group-study-list'; +import MyParticipatingStudiesSection from '../section/my-participating-studies-section'; // Carousel이 클라이언트 전용이므로 dynamic import로 로드 const Banner = dynamic(() => import('@/widgets/home/banner'), { @@ -175,6 +176,9 @@ export default function GroupStudyListPage() {
+ {/* 내가 참여중인 스터디 섹션 */} + + {/* 헤더 */}

diff --git a/src/components/pages/premium-study-list-page.tsx b/src/components/pages/premium-study-list-page.tsx index ac901cce..a4cb5676 100644 --- a/src/components/pages/premium-study-list-page.tsx +++ b/src/components/pages/premium-study-list-page.tsx @@ -20,6 +20,7 @@ import Button from '@/components/ui/button'; import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; import { useAuth } from '@/hooks/common/use-auth'; import { useGetStudies } from '@/hooks/queries/study-query'; +import MyParticipatingStudiesSection from '../section/my-participating-studies-section'; // Carousel이 클라이언트 전용이므로 dynamic import로 로드 const Banner = dynamic(() => import('@/widgets/home/banner'), { @@ -172,6 +173,9 @@ export default function PremiumStudyListPage() {

+ {/* 내가 참여중인 스터디 섹션 */} + + {/* 헤더 */}

diff --git a/src/components/section/my-participating-studies-section.tsx b/src/components/section/my-participating-studies-section.tsx new file mode 100644 index 00000000..5d143057 --- /dev/null +++ b/src/components/section/my-participating-studies-section.tsx @@ -0,0 +1,194 @@ +'use client'; + +import Link from 'next/link'; +import Image from 'next/image'; +import { useMemo } from 'react'; +import { useAuth } from '@/hooks/common/use-auth'; +import { useMemberStudyListQuery } from '@/features/study/group/model/use-member-study-list-query'; +import { useGetStudies } from '@/hooks/queries/study-query'; +import StudyCard from '@/components/card/study-card'; +import { sendGTMEvent } from '@next/third-parties/google'; +import { hashValue } from '@/utils/hash'; + +interface MyParticipatingStudiesSectionProps { + classification: 'GROUP_STUDY' | 'PREMIUM_STUDY'; +} + +export default function MyParticipatingStudiesSection({ + classification, +}: MyParticipatingStudiesSectionProps) { + const { data: authData } = useAuth(); + const memberId = authData?.memberId; + + // 비회원은 표시하지 않음 + if (!memberId) { + return null; + } + + /** + * TODO: 성능 최적화 필요 + * + * 현재 구현은 모든 스터디를 가져온 후 클라이언트에서 필터링하는 방식 + * + * 개선 방안: + * 1. 백엔드 API 개선: 스터디 ID 리스트를 받아서 해당 스터디만 반환하는 API + * GET /api/v1/group-studies/by-ids?ids=1,2,3&classification=GROUP_STUDY + * + * 2. 내 스터디 API 개선: getMemberStudyList에서 카드에 필요한 정보를 모두 반환 + * + * 3. 하이브리드 방식: + * - 내 스터디 ID가 적으면 (10개 이하) → ID 리스트로 상세 정보 조회 + * - 내 스터디 ID가 많으면 → 페이지네이션 + 서버 사이드 필터링 + */ + + // 내가 참여중인 스터디 ID 목록만 가져오기 + const { data: myStudiesData } = useMemberStudyListQuery({ + memberId, + studyType: classification, + studyStatus: 'NOT_COMPLETED', // 진행 중과 모집 중 모두 포함 + inProgressPage: 1, + inProgressPageSize: 100, // 충분히 많이 가져오기 + completedPage: 1, + completedPageSize: 1, + }); + + // 내가 참여중인 스터디 ID Set 생성 (IN_PROGRESS, RECRUITING) + const participatingStudyIds = useMemo(() => { + if (!myStudiesData?.notCompleted?.content) return new Set(); + + const filtered = myStudiesData.notCompleted.content.filter( + (study) => + study.status === 'IN_PROGRESS' || study.status === 'RECRUITING', + ); + + // GROUP_STUDY인 경우 type 필드로 추가 필터링 + const classificationFiltered = + classification === 'GROUP_STUDY' + ? filtered.filter((study) => study.type === 'GROUP_STUDY') + : filtered; // PREMIUM_STUDY는 type 필드에 없을 수 있으므로 일단 모두 포함 + + return new Set(classificationFiltered.map((study) => study.studyId)); + }, [myStudiesData?.notCompleted?.content, classification]); + + // 일반 스터디 목록 가져오기 (카드에 필요한 완전한 정보를 위해) + const { data: allStudiesData, isLoading } = useGetStudies({ + classification, + page: 1, + pageSize: 100, // 충분히 많이 가져와서 필터링 + recruiting: undefined, // 모든 상태 포함 (진행 중, 모집 중 모두) + }); + + // 내가 참여중인 스터디만 필터링 (최대 3개) + const participatingStudies = useMemo(() => { + if (!allStudiesData?.content || participatingStudyIds.size === 0) { + return []; + } + + // 내가 참여중인 스터디 ID에 해당하는 스터디만 필터링 + const filtered = allStudiesData.content.filter((study) => + participatingStudyIds.has(study.basicInfo?.groupStudyId ?? 0), + ); + + return filtered.slice(0, 3); // 최대 3개만 표시 + }, [allStudiesData?.content, participatingStudyIds]); + + // 로딩 중 + if (isLoading) { + return ( +
+
+

+ 내가 참여중인 스터디 +

+
+
+ 로딩 중... +
+
+ ); + } + + // 스터디가 없는 경우 빈 상태 표시 + if (participatingStudies.length === 0 && !isLoading) { + return ( +
+
+

+ 내가 참여중인 스터디 +

+
+
+ 참여중인 스터디가 없습니다. +
+ + 참여하는 스터디가 없습니다. + + + 원하는 주제로 스터디에 참여해 성장 파티를 시작해보세요. + +
+
+
+ ); + } + + // 전체보기 링크 표시 여부 (3개 이상이거나 더 많은 스터디가 있을 경우) + const hasMoreStudies = + participatingStudyIds.size > participatingStudies.length; + + const handleStudyClick = (studyId: number, title: string) => { + sendGTMEvent({ + event: + classification === 'GROUP_STUDY' + ? 'group_study_detail_view' + : 'premium_study_detail_view', + dl_timestamp: new Date().toISOString(), + dl_member_id: hashValue(String(memberId)), + dl_study_id: String(studyId), + dl_study_title: title, + }); + }; + + return ( +
+
+

+ 내가 참여중인 스터디 +

+ {hasMoreStudies && ( + + 전체보기 + + )} +
+ +
+ {participatingStudies.map((study) => { + const studyId = study.basicInfo?.groupStudyId ?? 0; + const title = study.simpleDetailInfo?.title ?? ''; + + return ( + handleStudyClick(studyId, title)} + /> + ); + })} +
+
+ ); +} diff --git a/src/features/study/group/api/group-study-types.ts b/src/features/study/group/api/group-study-types.ts index 696da154..753e1e66 100644 --- a/src/features/study/group/api/group-study-types.ts +++ b/src/features/study/group/api/group-study-types.ts @@ -332,7 +332,7 @@ export interface GroupStudyMyStatusResponse { // 회원의 스터디 리스트 조회 API 타입 export interface MemberStudyListRequest { memberId: number; - studyType?: 'BOTH' | 'GROUP_STUDY' | 'ONE_ON_ONE_STUDY'; + studyType?: 'BOTH' | 'GROUP_STUDY' | 'ONE_ON_ONE_STUDY' | 'PREMIUM_STUDY'; studyStatus?: 'BOTH' | 'NOT_COMPLETED' | 'COMPLETED'; inProgressPage?: number; inProgressPageSize?: number; @@ -355,7 +355,7 @@ export interface MemberStudyItem { endTime: string; studyRole: 'PARTICIPANT' | 'LEADER'; status: 'RECRUITING' | 'IN_PROGRESS' | 'COMPLETED'; - type: 'GROUP_STUDY' | 'ONE_ON_ONE_STUDY'; + type: 'GROUP_STUDY' | 'ONE_ON_ONE_STUDY' | 'PREMIUM_STUDY'; } export interface MemberStudyListResponse { From c396df8cc9ece0ec025956de4a2d96bc07575e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Wed, 4 Feb 2026 03:01:40 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore=20:=20=EB=82=98=EC=9D=98=20=EC=86=8C?= =?UTF-8?q?=EC=A4=91=ED=95=9C=20=EC=8A=A4=ED=84=B0=EB=94=94=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../my-participating-studies-section.tsx | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/section/my-participating-studies-section.tsx b/src/components/section/my-participating-studies-section.tsx index 5d143057..7537cb54 100644 --- a/src/components/section/my-participating-studies-section.tsx +++ b/src/components/section/my-participating-studies-section.tsx @@ -25,22 +25,24 @@ export default function MyParticipatingStudiesSection({ return null; } + /** + * TODO : PREMIUM_STUDY 에서도 동작확인 필요 (BE 미구현) + */ + /** * TODO: 성능 최적화 필요 - * - * 현재 구현은 모든 스터디를 가져온 후 클라이언트에서 필터링하는 방식 - * + * + * '참여중인 스터디 목록' 과 '전체 스터디 목록' 의 데이터타입이 맞지 않아, + * 현재 구현은 '참여중인 스터디 목록' ID set 을 만들고, 모든 '전체 스터디 목록'을 가져온 후 클라이언트에서 필터링하는 방식. + * * 개선 방안: * 1. 백엔드 API 개선: 스터디 ID 리스트를 받아서 해당 스터디만 반환하는 API * GET /api/v1/group-studies/by-ids?ids=1,2,3&classification=GROUP_STUDY - * + * * 2. 내 스터디 API 개선: getMemberStudyList에서 카드에 필요한 정보를 모두 반환 - * - * 3. 하이브리드 방식: - * - 내 스터디 ID가 적으면 (10개 이하) → ID 리스트로 상세 정보 조회 - * - 내 스터디 ID가 많으면 → 페이지네이션 + 서버 사이드 필터링 + * */ - + // 내가 참여중인 스터디 ID 목록만 가져오기 const { data: myStudiesData } = useMemberStudyListQuery({ memberId, @@ -114,10 +116,10 @@ export default function MyParticipatingStudiesSection({

- 내가 참여중인 스터디 + 나의 소중한 스터디

-
+
참여중인 스터디가 없습니다. participatingStudies.length; + const hasMoreStudies = participatingStudyIds.size > participatingStudies.length; const handleStudyClick = (studyId: number, title: string) => { sendGTMEvent({ @@ -158,7 +159,7 @@ export default function MyParticipatingStudiesSection({

- 내가 참여중인 스터디 + 나의 소중한 스터디

{hasMoreStudies && ( ); } + From 9237593820aa320c6716e3cbb0b7cfb3c0ddb0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Wed, 4 Feb 2026 03:23:26 +0900 Subject: [PATCH 6/6] chore : CI --- .../my-participating-studies-section.tsx | 46 ++++++++++--------- .../model/use-member-study-list-query.ts | 1 + 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/section/my-participating-studies-section.tsx b/src/components/section/my-participating-studies-section.tsx index 7537cb54..0f204fa0 100644 --- a/src/components/section/my-participating-studies-section.tsx +++ b/src/components/section/my-participating-studies-section.tsx @@ -20,32 +20,29 @@ export default function MyParticipatingStudiesSection({ const { data: authData } = useAuth(); const memberId = authData?.memberId; - // 비회원은 표시하지 않음 - if (!memberId) { - return null; - } - /** * TODO : PREMIUM_STUDY 에서도 동작확인 필요 (BE 미구현) */ /** * TODO: 성능 최적화 필요 - * + * * '참여중인 스터디 목록' 과 '전체 스터디 목록' 의 데이터타입이 맞지 않아, * 현재 구현은 '참여중인 스터디 목록' ID set 을 만들고, 모든 '전체 스터디 목록'을 가져온 후 클라이언트에서 필터링하는 방식. - * + * * 개선 방안: * 1. 백엔드 API 개선: 스터디 ID 리스트를 받아서 해당 스터디만 반환하는 API * GET /api/v1/group-studies/by-ids?ids=1,2,3&classification=GROUP_STUDY - * + * * 2. 내 스터디 API 개선: getMemberStudyList에서 카드에 필요한 정보를 모두 반환 - * + * */ - + // 내가 참여중인 스터디 ID 목록만 가져오기 + // React Hooks 규칙: hooks는 항상 같은 순서로 호출되어야 하므로 early return 전에 호출 + // memberId가 없으면 enabled: false로 설정하여 실제 API 호출은 하지 않음 const { data: myStudiesData } = useMemberStudyListQuery({ - memberId, + memberId: memberId ?? 0, studyType: classification, studyStatus: 'NOT_COMPLETED', // 진행 중과 모집 중 모두 포함 inProgressPage: 1, @@ -54,6 +51,14 @@ export default function MyParticipatingStudiesSection({ completedPageSize: 1, }); + // 일반 스터디 목록 가져오기 (카드에 필요한 완전한 정보를 위해) + const { data: allStudiesData, isLoading } = useGetStudies({ + classification, + page: 1, + pageSize: 100, // 충분히 많이 가져와서 필터링 + recruiting: undefined, // 모든 상태 포함 (진행 중, 모집 중 모두) + }); + // 내가 참여중인 스터디 ID Set 생성 (IN_PROGRESS, RECRUITING) const participatingStudyIds = useMemo(() => { if (!myStudiesData?.notCompleted?.content) return new Set(); @@ -72,14 +77,6 @@ export default function MyParticipatingStudiesSection({ return new Set(classificationFiltered.map((study) => study.studyId)); }, [myStudiesData?.notCompleted?.content, classification]); - // 일반 스터디 목록 가져오기 (카드에 필요한 완전한 정보를 위해) - const { data: allStudiesData, isLoading } = useGetStudies({ - classification, - page: 1, - pageSize: 100, // 충분히 많이 가져와서 필터링 - recruiting: undefined, // 모든 상태 포함 (진행 중, 모집 중 모두) - }); - // 내가 참여중인 스터디만 필터링 (최대 3개) const participatingStudies = useMemo(() => { if (!allStudiesData?.content || participatingStudyIds.size === 0) { @@ -94,6 +91,11 @@ export default function MyParticipatingStudiesSection({ return filtered.slice(0, 3); // 최대 3개만 표시 }, [allStudiesData?.content, participatingStudyIds]); + // 비회원은 표시하지 않음 (hooks 호출 후 early return) + if (!memberId) { + return null; + } + // 로딩 중 if (isLoading) { return ( @@ -119,7 +121,7 @@ export default function MyParticipatingStudiesSection({ 나의 소중한 스터디

-
+
참여중인 스터디가 없습니다. participatingStudies.length; + const hasMoreStudies = + participatingStudyIds.size > participatingStudies.length; const handleStudyClick = (studyId: number, title: string) => { sendGTMEvent({ @@ -193,4 +196,3 @@ export default function MyParticipatingStudiesSection({ ); } - diff --git a/src/features/study/group/model/use-member-study-list-query.ts b/src/features/study/group/model/use-member-study-list-query.ts index ff6e1c22..dfcb0c27 100644 --- a/src/features/study/group/model/use-member-study-list-query.ts +++ b/src/features/study/group/model/use-member-study-list-query.ts @@ -33,5 +33,6 @@ export const useMemberStudyListQuery = ({ completedPage, completedPageSize, }), + enabled: memberId > 0, // memberId가 유효할 때만 쿼리 실행 }); };