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
15 changes: 8 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ function App() {
<Route path="finish" element={<FinishStep />} />
</Route>
</Route>
<Route element={<Layout />}>
<Route path="legal">
<Route path="oss" element={<LicensePage />} />
<Route path="terms" element={<TermsPage />} />
<Route path="privacy" element={<PrivacyPolicyPage />} />
<Route path="marketing" element={<MarketingPolicyPage />} />
</Route>
</Route>

<Route element={<Layout />}>
<Route path="legal">
<Route path="oss" element={<LicensePage />} />
<Route path="terms" element={<TermsPage />} />
<Route path="privacy" element={<PrivacyPolicyPage />} />
<Route path="marketing" element={<MarketingPolicyPage />} />
</Route>
</Route>

Expand Down
12 changes: 12 additions & 0 deletions src/apis/club/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
6 changes: 6 additions & 0 deletions src/apis/club/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type ClubResponse,
type JoinClubResponse,
type ClubRecruitment,
type AppliedClubResponse,
} from './entity';

export const getClubs = async (params: ClubRequestParams) => {
Expand Down Expand Up @@ -53,3 +54,8 @@ export const getClubRecruitment = async (clubId: number) => {
const response = await apiClient.get<ClubRecruitment>(`clubs/${clubId}/recruitments`);
return response;
};

export const getAppliedClubs = async () => {
const response = await apiClient.get<AppliedClubResponse>('clubs/applied');
return response;
};
3 changes: 3 additions & 0 deletions src/assets/svg/circle-warning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 17 additions & 2 deletions src/pages/Club/ClubList/hooks/useGetClubs.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
});
};
5 changes: 3 additions & 2 deletions src/pages/Club/ClubList/index.tsx
Original file line number Diff line number Diff line change
@@ -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) ?? [];

Expand Down
34 changes: 34 additions & 0 deletions src/pages/Home/components/SimpleAppliedClubCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
to={`/clubs/${club.id}`}
className="border-indigo-5 flex w-full items-start gap-3 rounded-lg border bg-white p-3"
>
<img src={club.imageUrl} className="border-indigo-5 h-13 w-13 rounded-sm border" alt={club.name} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<div className="flex w-full items-center justify-between gap-1">
<div className="text-h3 text-indigo-700">{club.name}</div>
<div className="text-cap1 flex items-center gap-0.5 rounded-full bg-[#E8EBEFE5] px-3 py-1.5 text-[#5A6B7F]">
<CircleWarningIcon />
승인 대기 중
</div>
</div>
</div>
<div className="text-sub2 mt-1 text-indigo-300">
지원일: {new Date(club.appliedAt).toLocaleDateString('ko-KR')}
</div>
</div>
</Link>
);
}

export default SimpleAppliedClubCard;
10 changes: 10 additions & 0 deletions src/pages/Home/hooks/useGetAppliedClubs.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
};
8 changes: 7 additions & 1 deletion src/pages/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();

Expand All @@ -41,7 +44,7 @@ function Home() {
return (
<div className="flex flex-col gap-3 p-3 pb-6">
<div className="flex flex-col gap-2">
{joinedClubsData?.joinedClubs.length === 0 ? (
{appliedClubsData?.appliedClubs.length === 0 && joinedClubsData?.joinedClubs.length === 0 ? (
<Card>
<div>
<div className="text-sub2 mb-1.5">환영합니다!</div>
Expand All @@ -58,6 +61,9 @@ function Home() {
<div className="text-h3">내 동아리</div>

<div className="flex flex-col gap-2">
{appliedClubsData.appliedClubs.map((club) => (
<SimpleAppliedClubCard key={club.id} club={club} />
))}
{joinedClubsData.joinedClubs.map((club) => (
<SimpleClubCard key={club.id} club={club} />
))}
Expand Down
54 changes: 54 additions & 0 deletions src/utils/hooks/useScrollRestore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect, useLayoutEffect, useRef } from 'react';

const scrollPositions: Record<string, number> = {};

/**
* 페이지 이탈 시 스크롤 위치를 저장하고, 재방문 시 복원하는 훅
* @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]);
}