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() { } /> - }> - - } /> - } /> - } /> - } /> - + + + }> + + } /> + } /> + } /> + } /> 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/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/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) => ( ))} 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]); +}