From 1cf53a1d165f6b112213f86390e5ae10efe01650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Fri, 23 Jan 2026 23:40:53 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EA=B6=8C=ED=95=9C=20=EC=83=81?= =?UTF-8?q?=EA=B4=80=EC=97=86=EB=8A=94=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f41a414..c77f378 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,13 +47,14 @@ function App() { } /> - }> - - } /> - } /> - } /> - } /> - + + + }> + + } /> + } /> + } /> + } /> From 019bbdab3d9ff2a6cddcdf056583ae909c076ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Fri, 23 Jan 2026 23:41:17 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=8A=B9=EC=9D=B8=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0=20=EB=8F=99=EC=95=84=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/club/entity.ts | 12 +++++++ src/apis/club/index.ts | 6 ++++ src/assets/svg/circle-warning.svg | 3 ++ src/pages/Club/ClubList/hooks/useGetClubs.ts | 19 +++++++++-- .../Home/components/SimpleAppliedClubCard.tsx | 34 +++++++++++++++++++ src/pages/Home/hooks/useGetAppliedClubs.ts | 10 ++++++ src/pages/Home/index.tsx | 8 ++++- 7 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/assets/svg/circle-warning.svg create mode 100644 src/pages/Home/components/SimpleAppliedClubCard.tsx create mode 100644 src/pages/Home/hooks/useGetAppliedClubs.ts diff --git a/src/apis/club/entity.ts b/src/apis/club/entity.ts index 214115f..6b9462f 100644 --- a/src/apis/club/entity.ts +++ b/src/apis/club/entity.ts @@ -111,3 +111,15 @@ export interface ClubRecruitment { images: ClubRecruitmentImage[]; isApplied: boolean; } + +export interface AppliedClub { + id: number; + name: string; + imageUrl: string; + categoryName: string; + appliedAt: string; +} + +export interface AppliedClubResponse { + appliedClubs: AppliedClub[]; +} diff --git a/src/apis/club/index.ts b/src/apis/club/index.ts index 708eb9a..4fe2a81 100644 --- a/src/apis/club/index.ts +++ b/src/apis/club/index.ts @@ -9,6 +9,7 @@ import { type ClubResponse, type JoinClubResponse, type ClubRecruitment, + type AppliedClubResponse, } from './entity'; export const getClubs = async (params: ClubRequestParams) => { @@ -53,3 +54,8 @@ export const getClubRecruitment = async (clubId: number) => { const response = await apiClient.get(`clubs/${clubId}/recruitments`); return response; }; + +export const getAppliedClubs = async () => { + const response = await apiClient.get('clubs/applied'); + return response; +}; diff --git a/src/assets/svg/circle-warning.svg b/src/assets/svg/circle-warning.svg new file mode 100644 index 0000000..41b25f9 --- /dev/null +++ b/src/assets/svg/circle-warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/Club/ClubList/hooks/useGetClubs.ts b/src/pages/Club/ClubList/hooks/useGetClubs.ts index e146f78..cc04f74 100644 --- a/src/pages/Club/ClubList/hooks/useGetClubs.ts +++ b/src/pages/Club/ClubList/hooks/useGetClubs.ts @@ -1,5 +1,5 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; -import { getClubs } from '@/apis/club'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { getClubs, getAppliedClubs, getJoinedClubs } from '@/apis/club'; import type { ClubResponse } from '@/apis/club/entity'; export const clubQueryKeys = { @@ -17,6 +17,7 @@ export const clubQueryKeys = { fee: (clubId: number) => [...clubQueryKeys.all, 'fee', clubId], questions: (clubId: number) => [...clubQueryKeys.all, 'questions', clubId], joined: () => [...clubQueryKeys.all, 'joined'], + applied: () => [...clubQueryKeys.all, 'applied'], }; interface UseGetClubsParams { @@ -49,3 +50,17 @@ export const useGetClubs = ({ limit = 10, query, enabled = true, isRecruiting = enabled, }); }; + +export const useGetAppliedClubs = () => { + return useQuery({ + queryKey: clubQueryKeys.applied(), + queryFn: getAppliedClubs, + }); +}; + +export const useGetJoinedClubs = () => { + return useQuery({ + queryKey: clubQueryKeys.joined(), + queryFn: getJoinedClubs, + }); +}; diff --git a/src/pages/Home/components/SimpleAppliedClubCard.tsx b/src/pages/Home/components/SimpleAppliedClubCard.tsx new file mode 100644 index 0000000..c072045 --- /dev/null +++ b/src/pages/Home/components/SimpleAppliedClubCard.tsx @@ -0,0 +1,34 @@ +import { Link } from 'react-router-dom'; +import type { AppliedClub } from '@/apis/club/entity'; +import CircleWarningIcon from '@/assets/svg/circle-warning.svg'; + +interface SimpleAppliedClubCardProps { + club: AppliedClub; +} + +function SimpleAppliedClubCard({ club }: SimpleAppliedClubCardProps) { + return ( + + {club.name} +
+
+
+
{club.name}
+
+ + 승인 대기 중 +
+
+
+
+ 지원일: {new Date(club.appliedAt).toLocaleDateString('ko-KR')} +
+
+ + ); +} + +export default SimpleAppliedClubCard; diff --git a/src/pages/Home/hooks/useGetAppliedClubs.ts b/src/pages/Home/hooks/useGetAppliedClubs.ts new file mode 100644 index 0000000..3196db9 --- /dev/null +++ b/src/pages/Home/hooks/useGetAppliedClubs.ts @@ -0,0 +1,10 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { getAppliedClubs } from '@/apis/club'; +import { clubQueryKeys } from '@/pages/Club/ClubList/hooks/useGetClubs'; + +export const useGetAppliedClubs = () => { + return useSuspenseQuery({ + queryKey: clubQueryKeys.applied(), + queryFn: () => getAppliedClubs(), + }); +}; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index e5cf332..098cfa9 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -4,7 +4,9 @@ import Card from '@/components/common/Card'; import NavigateCard from '@/components/common/NavigateCard'; import ClubCard from '../Club/ClubList/components/ClubCard'; import { useGetClubs } from '../Club/ClubList/hooks/useGetClubs'; +import SimpleAppliedClubCard from './components/SimpleAppliedClubCard'; import SimpleClubCard from './components/SimpleClubCard'; +import { useGetAppliedClubs } from './hooks/useGetAppliedClubs'; import { useGetJoinedClubs } from './hooks/useGetJoinedClubs'; import { useGetUpComingScheduleList } from './hooks/useGetUpComingSchedule'; @@ -32,6 +34,7 @@ function scheduleDateToPath(startedAt: string) { function Home() { const { data: clubsData } = useGetClubs({ limit: 10, isRecruiting: true }); const { data: allClubsData } = useGetClubs({ limit: 1, isRecruiting: false }); + const { data: appliedClubsData } = useGetAppliedClubs(); const { data: joinedClubsData } = useGetJoinedClubs(); const { data: scheduleListData } = useGetUpComingScheduleList(); @@ -41,7 +44,7 @@ function Home() { return (
- {joinedClubsData?.joinedClubs.length === 0 ? ( + {appliedClubsData?.appliedClubs.length === 0 && joinedClubsData?.joinedClubs.length === 0 ? (
환영합니다!
@@ -58,6 +61,9 @@ function Home() {
내 동아리
+ {appliedClubsData.appliedClubs.map((club) => ( + + ))} {joinedClubsData.joinedClubs.map((club) => ( ))} From a5b37e437adde66309b99183b79e2336756fc178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Fri, 23 Jan 2026 23:41:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=A0=80=EC=9E=A5=20=ED=9B=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Club/ClubList/index.tsx | 5 +-- src/utils/hooks/useScrollRestore.ts | 54 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/utils/hooks/useScrollRestore.ts diff --git a/src/pages/Club/ClubList/index.tsx b/src/pages/Club/ClubList/index.tsx index fad3e7f..9038d6f 100644 --- a/src/pages/Club/ClubList/index.tsx +++ b/src/pages/Club/ClubList/index.tsx @@ -1,14 +1,15 @@ import { useInfiniteScroll } from '@/utils/hooks/useInfiniteScroll'; -import useScrollToTop from '@/utils/hooks/useScrollToTop'; +import useScrollRestore from '@/utils/hooks/useScrollRestore'; import ClubCard from './components/ClubCard'; import SearchBar from './components/SearchBar'; import { useGetClubs } from './hooks/useGetClubs'; function ClubList() { - useScrollToTop(); const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetClubs({ limit: 10 }); const observerRef = useInfiniteScroll(fetchNextPage, hasNextPage, isFetchingNextPage); + useScrollRestore('clubList', !!data); + const totalCount = data?.pages[0]?.totalCount ?? 0; const allClubs = data?.pages.flatMap((page) => page.clubs) ?? []; diff --git a/src/utils/hooks/useScrollRestore.ts b/src/utils/hooks/useScrollRestore.ts new file mode 100644 index 0000000..1ae7241 --- /dev/null +++ b/src/utils/hooks/useScrollRestore.ts @@ -0,0 +1,54 @@ +import { useEffect, useLayoutEffect, useRef } from 'react'; + +const scrollPositions: Record = {}; + +/** + * 페이지 이탈 시 스크롤 위치를 저장하고, 재방문 시 복원하는 훅 + * @param key - 저장할 고유 키 + * @param isReady - 데이터 로드 완료 여부 (무한 스크롤 등에서 데이터 로드 후 복원하기 위함) + */ +export default function useScrollRestore(key: string, isReady: boolean = true) { + const hasRestored = useRef(false); + const scrollPositionRef = useRef(0); + + const getScrollContainer = () => document.querySelector('main'); + + // 스크롤 위치 복원 + useLayoutEffect(() => { + if (!isReady || hasRestored.current) return; + + const savedPosition = scrollPositions[key]; + console.log('[useScrollRestore] savedPosition from memory:', savedPosition); + + if (savedPosition) { + requestAnimationFrame(() => { + const container = getScrollContainer(); + if (container) { + container.scrollTop = savedPosition; + console.log('[useScrollRestore] restored to:', container.scrollTop); + } + }); + hasRestored.current = true; + } + }, [key, isReady]); + + // 스크롤 이벤트로 메모리에 실시간 저장 + useEffect(() => { + const container = getScrollContainer(); + if (!container) return; + + const saveScrollPosition = () => { + scrollPositionRef.current = container.scrollTop; + scrollPositions[key] = container.scrollTop; + console.log('[useScrollRestore] saved to memory:', container.scrollTop); + }; + + container.addEventListener('scroll', saveScrollPosition); + + return () => { + container.removeEventListener('scroll', saveScrollPosition); + scrollPositions[key] = scrollPositionRef.current; + console.log('[useScrollRestore] cleanup, final position:', scrollPositionRef.current); + }; + }, [key]); +}