Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 웹 최적화 & Suspense를 이용해 Skeleton UI 적용 #257

Merged
merged 11 commits into from
Dec 3, 2024
Merged
74 changes: 37 additions & 37 deletions FE/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion FE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2",
"react-helmet": "^6.1.0",
"react-helmet-async": "^2.0.5",
"react-router-dom": "^6.27.0",
"react-toastify": "^10.0.6",
"socket.io-client": "^4.8.1",
Expand All @@ -28,6 +28,7 @@
"@types/node": "^22.9.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.13.0",
Expand Down
2 changes: 2 additions & 0 deletions FE/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
user-agent: *
allow: /
31 changes: 30 additions & 1 deletion FE/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import MyPage from 'page/MyPage';
import Rank from 'page/Rank.tsx';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Helmet } from 'react-helmet-async';
import { Suspense } from 'react';
import { RankingSkeleton } from './components/Rank/RankingSkeleton.tsx';

function App() {
return (
Expand All @@ -23,7 +26,14 @@ function App() {
<Route index element={<Home />} />
<Route path='stocks/:id' element={<StocksDetail />} />
<Route path='mypage' element={<MyPage />} />
<Route path='rank' element={<Rank />} />
<Route
path='rank'
element={
<Suspense fallback={<RankingSkeleton />}>
<Rank />
</Suspense>
}
/>
</Route>
</Routes>
</Router>
Expand All @@ -35,6 +45,25 @@ export default App;
function Layout() {
return (
<>
<Helmet>
<meta charSet='utf-8' />
<meta
name='description'
content='실시간 주식 데이터를 활용한 모의투자 경험을 통해 주식 투자에 대해 배울 수 있는 서비스.'
/>
<meta property='og:title' content='JuGa' />
<meta property='og:url' content='https://juga.kro.kr/' />
<meta
property='og:image'
content='https://juga.kro.kr/assets/logo-BUoSezEL.webp'
/>
<meta property='og:image:alt' content='JuGa Logo' />
<meta
property='og:description'
content='실시간 주식 데이터를 활용한 모의투자 경험을 통해 주식 투자에 대해 배울 수 있는 서비스.'
/>
<title>JuGa</title>
</Helmet>
<Header />
<main className='mt-[60px] flex flex-col gap-4'>
<Outlet />
Expand Down
31 changes: 31 additions & 0 deletions FE/src/components/GlobalErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FallbackProps } from 'react-error-boundary';
import logoPng from 'assets/logo.png';
import logoWebp from 'assets/logo.webp';

export default function GlobalErrorFallback({ error }: FallbackProps) {
return (
<div className='flex flex-col items-center justify-center mt-40 text-gray-800'>
<div className='flex items-center mb-6'>
<picture>
<source srcSet={logoWebp} type='image/webp' />
<img src={logoPng} alt='Logo' className='h-48' />
</picture>
</div>
<h1 className='mb-4 text-2xl font-bold'>오류가 발생했습니다!</h1>
<p className='mb-6 text-lg text-center'>
문제가 지속적으로 발생하면 관리자에게 문의해주세요.
</p>
<pre className='w-full max-w-lg p-4 mb-6 text-sm text-left bg-gray-200 rounded-md shadow-md'>
{error.message}
</pre>
<div className='flex space-x-4'>
<a
href='/'
className='px-6 py-2 font-medium text-white transition bg-blue-500 rounded-md shadow hover:bg-blue-600'
>
Home으로 돌아가기
</a>
</div>
</div>
);
}
10 changes: 6 additions & 4 deletions FE/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import logoPng from 'assets/logo.png';
import logoWebp from 'assets/logo.webp';
import { checkAuth, logout } from 'service/auth.ts';
import { useEffect } from 'react';
import Toast from './Toast';

export default function Header() {
const { toggleModal } = useLoginModalStore();
Expand All @@ -29,6 +30,7 @@ export default function Header() {
const handleLogout = () => {
logout().then(() => {
setIsLogin(false);
Toast({ message: '로그아웃 되었습니다!', type: 'success' });
});
};

Expand All @@ -49,7 +51,7 @@ export default function Header() {
type='image/webp'
className={'h-[32px]'}
/>
<img src={logoPng} className={'h-[32px]'} />
<img src={logoPng} alt={'Logo'} className={'h-[32px]'} />
</picture>
<h1 className='text-xl font-bold text-juga-grayscale-black'>JuGa</h1>
</Link>
Expand All @@ -58,20 +60,20 @@ export default function Header() {
<nav className='flex items-center gap-6 text-sm font-bold text-juga-grayscale-500'>
<div
onClick={() => handleLink('/')}
className='cursor-pointer px-1 py-2'
className='px-1 py-2 cursor-pointer'
>
</div>
<div
onClick={() => handleLink('/rank')}
className='cursor-pointer px-1 py-2'
className='px-1 py-2 cursor-pointer'
>
랭킹
</div>
{isLogin && (
<div
onClick={() => handleLink('/mypage')}
className='cursor-pointer px-1 py-2'
className='px-1 py-2 cursor-pointer'
>
마이페이지
</div>
Expand Down
15 changes: 5 additions & 10 deletions FE/src/components/Mypage/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,13 @@ import { getAssets } from 'service/assets';
import { isWithinTimeRange } from 'utils/common';

export default function Account() {
const { data, isLoading, isError } = useQuery(
['account', 'assets'],
() => getAssets(),
{
staleTime: 1000,
refetchInterval: isWithinTimeRange('09:00', '15:30') ? 5000 : false,
},
);
const { data } = useQuery(['account', 'assets'], () => getAssets(), {
staleTime: 1000,
refetchInterval: isWithinTimeRange('09:00', '15:30') ? 5000 : false,
suspense: true,
});

if (isLoading) return <div>loading</div>;
if (!data) return <div>No data</div>;
if (isError) return <div>error</div>;

const { asset, stocks } = data;

Expand Down
61 changes: 61 additions & 0 deletions FE/src/components/Mypage/AccountSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const AccountConditionSkeleton = () => {
return (
<div className='flex flex-col gap-4 rounded-lg bg-white p-4'>
<div className='flex items-center justify-between'>
{/* 계좌 정보 타이틀 */}
<div className='h-6 w-24 animate-pulse rounded bg-gray-200' />
{/* 날짜 정보 */}
<div className='h-5 w-32 animate-pulse rounded bg-gray-200' />
</div>

<div className='flex flex-col gap-2'>
{/* 총자산, 수익률 등의 정보 */}
<div className='flex items-center justify-between'>
<div className='h-5 w-20 animate-pulse rounded bg-gray-200' />
<div className='h-7 w-32 animate-pulse rounded bg-gray-200' />
</div>
<div className='flex items-center justify-between'>
<div className='h-5 w-20 animate-pulse rounded bg-gray-200' />
<div className='h-7 w-32 animate-pulse rounded bg-gray-200' />
</div>
</div>
</div>
);
};

const MyStocksListSkeleton = () => {
return (
<div className='flex flex-col gap-2 rounded-lg bg-white p-4'>
{/* 보유종목 타이틀 */}
<div className='mb-2 h-6 w-24 animate-pulse rounded bg-gray-200' />

{/* 테이블 헤더 */}
<div className='flex items-center justify-between border-b pb-2'>
<div className='h-4 w-32 animate-pulse rounded bg-gray-200' />
<div className='h-4 w-20 animate-pulse rounded bg-gray-200' />
<div className='h-4 w-24 animate-pulse rounded bg-gray-200' />
</div>

{/* 보유주식 리스트 */}
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className='flex items-center justify-between border-b py-3'
>
<div className='h-5 w-40 animate-pulse rounded bg-gray-200' />
<div className='h-5 w-24 animate-pulse rounded bg-gray-200' />
<div className='h-5 w-28 animate-pulse rounded bg-gray-200' />
</div>
))}
</div>
);
};

export const AccountSkeleton = () => {
return (
<div className='flex min-h-[500px] flex-col gap-3'>
<AccountConditionSkeleton />
<MyStocksListSkeleton />
</div>
);
};
Loading
Loading