Skip to content

Commit 6649cb1

Browse files
authored
[Feature] TanstackQuery & Suspense 기반 APi 페칭 로딩 UI 적용 (#384)
* feat : Skeleton 공용 컴포넌트 구현 * Refactor : 열린 배틀 목록 조회, 닫힌 배틀 목록 조회 useQuery들 useSuspenseQuery로 마이그레이션 >> >> - 수동 로딩/에러 상태 관리 코드 제거 * fix : pnpm workspace 의존성 해결을 위한 Dockerfile 수정 - 백엔드 Dockerfile에 frontend/package.json 및 packages 폴더 추가 - 프론트엔드 Dockerfile에 backend/package.json 및 packages 폴더 추가 * style : 배틀 생성 페이지에서 배틀 생성 버튼 text 변경 - 기존 + 아이콘이였지만 배틀 생성하기 텍스트로 수정 했습니다 * feat : 로딩 모달 컴포넌트 구현 및 배틀 생성시 api 대기 시간까지 로딩 모달 띄우는 기능 구현 * feat : 메인 페이지에서 조회되는 배틀이 없는 경우 표시할 빈 배틀 목록 UI 컴포넌트 구현 - 진행중/종료된 배틀이 없는 경우 각각 안내 컴포넌트 표시 * refactor: 배틀/결과 페이지 로딩을 Suspense 패턴으로 통일 - useQuery → useSuspenseQuery 전환 (useGetBattleInfo, useGetBattleResult) - 페이지 내 isLoading 체크 로직 제거 후 BattlePage, BattleResultPage를 App.tsx에서 Suspense로 래핑 및 로딩 UI 제공 * refactor: LoadingModal을 LoadingOverlay로 이름 변경 - 여러 용도에서 사용 되기에 컴포넌트명을 수정했습니다
1 parent 1b83af7 commit 6649cb1

File tree

20 files changed

+367
-221
lines changed

20 files changed

+367
-221
lines changed

backend/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ RUN corepack enable
88
# Copy workspace
99
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
1010
COPY backend/package.json ./backend/package.json
11+
COPY frontend/package.json ./frontend/package.json
12+
COPY packages ./packages
1113

1214
# 의존성 설치
1315
ENV HUSKY=0
@@ -36,6 +38,7 @@ RUN corepack enable
3638
# Copy workspace files
3739
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
3840
COPY backend/package.json ./backend/package.json
41+
COPY packages ./packages
3942

4043
# 의존성 설치 (프로덕션용, husky 스크립트 무시)
4144
ENV HUSKY=0

frontend/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ RUN corepack enable
88

99
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
1010
COPY frontend/package.json ./frontend/package.json
11+
COPY backend/package.json ./backend/package.json
12+
COPY packages ./packages
1113

1214
# Install
1315
ENV HUSKY=0

frontend/src/App.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
2-
import { useEffect } from 'react';
2+
import { useEffect, Suspense } from 'react';
33
import * as Sentry from '@sentry/react';
44
import BattleCreatePage from './pages/battleCreatePage';
55
import MainPage from './pages/mainPage';
@@ -15,6 +15,7 @@ import ErrorPage from './pages/errorPage';
1515
import TutorialBattlePage from './pages/tutorialPage/battle';
1616
import { TUTORIAL_BATTLE_INFO } from './pages/tutorialPage/const/tutorialBattle';
1717
import { ToastContainer } from './commons/components/toast/ToastContainer';
18+
import LoadingOverlay from './commons/components/LoadingOverlay';
1819
import { useAuthStore } from './commons/stores/authStore';
1920
import './App.css';
2021

@@ -35,7 +36,14 @@ const router = sentryCreateBrowserRouter([
3536
path: 'battle/create',
3637
element: <BattleCreatePage />
3738
},
38-
{ path: 'battle/:id', element: <BattlePage /> },
39+
{
40+
path: 'battle/:id',
41+
element: (
42+
<Suspense fallback={<LoadingOverlay isOpen={true} message="배틀 정보를 불러오는 중..." />}>
43+
<BattlePage />
44+
</Suspense>
45+
)
46+
},
3947
{ path: 'battle/:id/team-select', element: <TeamSelectPage /> },
4048
{
4149
path: 'tutorial/team-select',
@@ -44,7 +52,14 @@ const router = sentryCreateBrowserRouter([
4452
},
4553
{ path: 'tutorial/battle', element: <TutorialBattlePage />, loader: () => TUTORIAL_BATTLE_INFO },
4654
{ path: 'battles/:inviteCode', element: <InvitePage /> },
47-
{ path: 'battles/:id/result', element: <BattleResultPage /> }
55+
{
56+
path: 'battles/:id/result',
57+
element: (
58+
<Suspense fallback={<LoadingOverlay isOpen={true} message="배틀 결과를 불러오는 중..." />}>
59+
<BattleResultPage />
60+
</Suspense>
61+
)
62+
}
4863
]
4964
}
5065
]);

frontend/src/commons/components/ErrorBoundary/SectionErrorFallback.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ interface SectionErrorFallbackProps {
55
reset: () => void;
66
title?: string;
77
className?: string;
8-
minHeight?: string;
8+
height?: string;
99
}
1010

1111
export default function SectionErrorFallback({
1212
error,
1313
reset,
1414
title = '데이터를 불러올 수 없습니다',
1515
className = '',
16-
minHeight = '18.75rem'
16+
height = '18.75rem'
1717
}: SectionErrorFallbackProps) {
1818
return (
1919
<div
20-
style={{ minHeight }}
20+
style={{ height }}
2121
className={`w-full bg-[#1A1A2E] border border-[#364153] rounded-xl p-8 flex flex-col items-center justify-center animate-fadeIn ${className}`}
2222
>
2323
<div className="flex flex-col items-center gap-4 text-center">
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
interface LoadingModalProps {
2+
isOpen: boolean;
3+
message?: string;
4+
}
5+
6+
export default function LoadingModal({ isOpen, message = '처리 중입니다...' }: LoadingModalProps) {
7+
if (!isOpen) return null;
8+
9+
return (
10+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md animate-in fade-in duration-200">
11+
<div className="relative rounded-3xl border border-orange-500/20 bg-gradient-to-br from-[#1a1a2e] to-[#121226] p-10 shadow-[0_20px_60px_rgba(255,105,0,0.3)] animate-in zoom-in-95 duration-300">
12+
<div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-orange-500/10 to-transparent blur-xl"></div>
13+
14+
<div className="relative flex flex-col items-center gap-6">
15+
<div className="relative">
16+
<div className="h-16 w-16 animate-spin rounded-full border-4 border-orange-500/20 border-t-orange-500 shadow-[0_0_20px_rgba(255,105,0,0.5)]"></div>
17+
<div className="absolute inset-0 h-16 w-16 animate-ping rounded-full border-4 border-orange-500/30 opacity-20"></div>
18+
</div>
19+
20+
<div className="flex flex-col items-center gap-2">
21+
<p className="text-xl font-bold bg-gradient-to-r from-orange-400 to-orange-600 bg-clip-text text-transparent">
22+
{message}
23+
</p>
24+
<div className="flex gap-1">
25+
<span className="h-2 w-2 animate-bounce rounded-full bg-orange-500 [animation-delay:-0.3s]"></span>
26+
<span className="h-2 w-2 animate-bounce rounded-full bg-orange-500 [animation-delay:-0.15s]"></span>
27+
<span className="h-2 w-2 animate-bounce rounded-full bg-orange-500"></span>
28+
</div>
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
);
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
interface LoadingOverlayProps {
2+
isOpen: boolean;
3+
message?: string;
4+
}
5+
6+
export default function LoadingOverlay({ isOpen, message = '처리 중입니다...' }: LoadingOverlayProps) {
7+
if (!isOpen) return null;
8+
9+
return (
10+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md animate-in fade-in duration-200">
11+
<div className="relative rounded-3xl border border-orange-500/20 bg-gradient-to-br from-[#1a1a2e] to-[#121226] p-10 shadow-[0_20px_60px_rgba(255,105,0,0.3)] animate-in zoom-in-95 duration-300">
12+
<div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-orange-500/10 to-transparent blur-xl"></div>
13+
14+
<div className="relative flex flex-col items-center gap-6">
15+
<div className="relative">
16+
<div className="h-16 w-16 animate-spin rounded-full border-4 border-orange-500/20 border-t-orange-500 shadow-[0_0_20px_rgba(255,105,0,0.5)]"></div>
17+
<div className="absolute inset-0 h-16 w-16 animate-ping rounded-full border-4 border-orange-500/30 opacity-20"></div>
18+
</div>
19+
20+
<div className="flex flex-col items-center gap-2">
21+
<p className="text-xl font-bold bg-gradient-to-r from-orange-400 to-orange-600 bg-clip-text text-transparent">
22+
{message}
23+
</p>
24+
<div className="flex gap-1">
25+
<span className="h-2 w-2 animate-bounce rounded-full bg-orange-500 [animation-delay:-0.3s]"></span>
26+
<span className="h-2 w-2 animate-bounce rounded-full bg-orange-500 [animation-delay:-0.15s]"></span>
27+
<span className="h-2 w-2 animate-bounce rounded-full bg-orange-500"></span>
28+
</div>
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
);
34+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
interface SkeletonProps {
2+
width?: string;
3+
height?: string;
4+
className?: string;
5+
}
6+
7+
export default function Skeleton({ width, height, className = '' }: SkeletonProps) {
8+
return (
9+
<div
10+
className={`bg-gradient-to-r from-[#1a1a2e] via-[#252540] to-[#1a1a2e] bg-[length:200%_100%] animate-pulse ${className}`}
11+
style={{ width: width, height: height }}
12+
/>
13+
);
14+
}

frontend/src/commons/hooks/useGetBattleInfo.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { useQuery } from '@tanstack/react-query';
1+
import { useSuspenseQuery } from '@tanstack/react-query';
22
import getBattleInfo from '@/commons/apis/getBattleInfo';
33

44
export function useGetBattleInfo(battleId: string) {
5-
const { data, ...rest } = useQuery({
5+
const { data, ...rest } = useSuspenseQuery({
66
queryKey: ['battle', 'info', battleId],
77
queryFn: () => getBattleInfo(battleId),
88
staleTime: 0,
9-
gcTime: 1000 * 60 * 5,
10-
enabled: !!battleId,
11-
throwOnError: true
9+
gcTime: 1000 * 60 * 5
1210
});
1311

1412
return {

0 commit comments

Comments
 (0)