Date: Tue, 3 Feb 2026 16:46:49 +0900
Subject: [PATCH 10/15] =?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 11/15] 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 12/15] =?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 13/15] =?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 14/15] =?UTF-8?q?chore=20:=20=EB=82=98=EC=9D=98=20?=
=?UTF-8?q?=EC=86=8C=EC=A4=91=ED=95=9C=20=EC=8A=A4=ED=84=B0=EB=94=94?=
=?UTF-8?q?=EB=A1=9C=20=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 15/15] 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가 유효할 때만 쿼리 실행
});
};