Skip to content

Commit bb0a287

Browse files
authored
Merge pull request #110 from blaybus-hackathon/dev
Dev
2 parents 82366e4 + 61ffb48 commit bb0a287

File tree

10 files changed

+170
-63
lines changed

10 files changed

+170
-63
lines changed

package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"react-dom": "^19.0.0",
3434
"react-router-dom": "^7.1.5",
3535
"react-spinners": "^0.17.0",
36+
"recharts": "^3.1.0",
3637
"sockjs-client": "^1.6.1",
3738
"tailwind-merge": "^3.0.1",
3839
"tailwindcss-animate": "^1.0.7",

src/App.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Spinner from './components/loading/Spinner';
99
const SignIn = lazy(() => import('./pages/SignIn'));
1010
const CenterSignUp = lazy(() => import('./pages/center/SignUp'));
1111
const HelperSignUp = lazy(() => import('./pages/helper/SignUp'));
12-
const FindAccount = lazy(() => import ('./pages/FindAccount'));
12+
const FindAccount = lazy(() => import('./pages/FindAccount'));
1313
const SearchCenter = lazy(() => import('./pages/SearchCenter'));
1414
const KakaoCallback = lazy(() => import('./components/Auth/KakaoCallback'));
1515
const Account = lazy(() => import('./pages/helper/Account/Account'));
@@ -19,6 +19,7 @@ const HelperAddress = lazy(() => import('./pages/helper/HelperAddress'));
1919
const AccountSchedule = lazy(() => import('./pages/helper/AccountEdit/navigate/AccountSchedule'));
2020
const AccountPay = lazy(() => import('./pages/helper/AccountEdit/navigate/AccountPay'));
2121
const AccountCareType = lazy(() => import('./pages/helper/AccountEdit/navigate/AccountCareType'));
22+
const DashBoard = lazy(() => import('./pages/center/Dashboard'));
2223
const Matching = lazy(() => import('./pages/center/Matching'));
2324
const RecriutDetail = lazy(() => import('./pages/center/RecriutDetail'));
2425
const ModifyRecruit = lazy(() => import('./pages/center/ModifyRecruit'));
@@ -41,6 +42,7 @@ function App() {
4142
<Route index element={<Home />} />
4243
<Route path='/login/oauth2/code/kakao' element={<KakaoCallback />} />
4344
<Route path='/*' element={<NotFound />} />
45+
<Route path='center' element={<DashBoard />} />
4446

4547
{/* 레이아웃 */}
4648
<Route path='/' element={<Layout />}>
@@ -57,7 +59,6 @@ function App() {
5759
<Route path='center/matching' element={<Matching />} />
5860
<Route path='center/matching-info' element={<MatchingInfo />} />
5961
<Route path='center/register/address' element={<ElderAddress />} />
60-
6162
<Route path='center/recruit/detail' element={<RecriutDetail />} />
6263
<Route path='center/recruit/modify' element={<ModifyRecruit />} />
6364
<Route path='center/care-info' element={<CaregiverInfo />} />
@@ -75,8 +76,6 @@ function App() {
7576
{/* Chatting */}
7677
<Route path='chatrooms' element={<ChatRoomsPage />} />
7778
<Route path='chatroom/:roomid' element={<PrivateChatRoom />} />
78-
79-
{/* <Route path="/center" element={} /> */}
8079
</Route>
8180
</Routes>
8281
</Suspense>

src/components/Center/Dashboard/DonutChart.jsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ export function DonutChart({
9090
const renderCustomLegend = (props) => {
9191
const { payload } = props;
9292
return (
93-
<div
94-
className={`flex justify-between md:justify-center md:gap-4 lg:justify-center lg:gap-10 w-full ${currentSize.legendMargin}`}
95-
>
93+
<div className={`flex justify-center gap-4 lg:gap-10 w-full ${currentSize.legendMargin}`}>
9694
{payload.map((entry, index) => (
9795
<div key={index} className='flex items-center gap-1 lg:gap-2'>
9896
<div
@@ -108,15 +106,12 @@ export function DonutChart({
108106

109107
// render center label
110108
const renderCenterLabel = () => (
111-
<text
112-
x='50%'
113-
y='50%'
114-
textAnchor='middle'
115-
dominantBaseline='middle'
116-
className={`${currentSize.centerText} font-semibold text-[var(--main)] flex gap-[0.1rem] items-center`}
109+
<div
110+
className={`flex items-center justify-center ${currentSize.centerText} font-semibold text-[var(--main)]`}
117111
>
118-
{acceptRate} <span className={`${currentSize.centerPercent}`}>%</span>
119-
</text>
112+
{acceptRate}
113+
<span className={`${currentSize.centerPercent} ml-1`}>%</span>
114+
</div>
120115
);
121116

122117
return (

src/components/Center/Dashboard/StatsCards.jsx

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
1-
import { StatCard } from './StatCard';
2-
import { DonutChart } from './DonutChart';
3-
import { BarChart } from './BarChart';
1+
import { memo } from 'react';
2+
import { StatCard } from '@/components/Center/Dashboard/StatCard';
3+
import { DonutChart } from '@/components/Center/Dashboard/DonutChart';
4+
import { BarChart } from '@/components/Center/Dashboard/BarChart';
45

5-
export function StatsCards() {
6-
// dummy data
7-
const statsData = {
8-
newMatches: 20,
9-
totalMatches: 380,
10-
// 도넛 차트용 매칭 비율 데이터
11-
matching: {
12-
acceptanceRate: 70,
13-
rejectionRate: 30,
14-
},
15-
// 막대 차트용 상태별 데이터 (ReChart 형식에 맞게 변경)
16-
statusData: [
17-
{ name: '대기', value: 140, color: '#c9c1de' },
18-
{ name: '진행중', value: 278, color: '#8976C0' },
19-
{ name: '완료', value: 380, color: '#522e9a' },
20-
],
21-
};
6+
export default memo(function StatsCard({ statsData }) {
7+
if (!statsData) {
8+
return (
9+
<div className='text-center p-8'>
10+
<p className='text-[var(--placeholder-gray)]'>표시할 데이터가 없습니다.</p>
11+
</div>
12+
);
13+
}
2214

2315
return (
2416
<>
@@ -77,4 +69,4 @@ export function StatsCards() {
7769
</div>
7870
</>
7971
);
80-
}
72+
});

src/components/ui/temp/Footer.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ export default function Footer({ isManager = false }) {
2929
<span className={textClass}>채팅</span>
3030
</Link>
3131

32-
<Link to='/matching' className={baseItemClass}>
32+
<Link to='/matching-info' className={baseItemClass}>
3333
<img src={MatchingIcon} alt='matching' className={iconClass} />
3434
<span className={textClass}>매칭 관리</span>
3535
</Link>
3636

37-
<Link to='/center/elder-register' className={baseItemClass}>
37+
<Link to='/center/elder-info ' className={baseItemClass}>
3838
<img src={PillIcon} alt='pill' className={iconClass} />
3939
<span className={textClass}>어르신 관리</span>
4040
</Link>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { getDashboardStats } from '@/services/center/dashboardService';
3+
4+
export const useDashboardStats = () => {
5+
return useQuery({
6+
queryKey: ['dashboardStats'],
7+
queryFn: getDashboardStats,
8+
staleTime: 3 * 60 * 1000,
9+
refetchInterval: 5 * 60 * 1000,
10+
retry: 2,
11+
select: (data) => {
12+
// use != to detect both undefined and null
13+
const toSafeNumber = (val) => (val != null ? Number(val) : 0);
14+
15+
return {
16+
newMatches: toSafeNumber(data.newCnt),
17+
totalMatches: toSafeNumber(data.totalCnt),
18+
19+
matching: {
20+
acceptanceRate:
21+
data.permitRate != null ? Number((data.permitRate * 100).toFixed(1)) : 0.0,
22+
rejectionRate: data.rejectRate != null ? Number((data.rejectRate * 100).toFixed(1)) : 0.0,
23+
},
24+
25+
statusData: [
26+
{
27+
name: '대기',
28+
value: toSafeNumber(data.stateWaitCnt),
29+
color: '#c9c1de',
30+
},
31+
{
32+
name: '진행중',
33+
value: toSafeNumber(data.stateInProgressCnt),
34+
color: '#8976c0',
35+
},
36+
{
37+
name: '완료',
38+
value: toSafeNumber(data.stateFinCnt),
39+
color: '#522e9a',
40+
},
41+
],
42+
};
43+
},
44+
});
45+
};

src/pages/center/Dashboard.jsx

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,100 @@
11
import Header, { DesktopHeader } from '@/components/ui/temp/Header';
22
import Footer from '@/components/ui/temp/Footer';
3-
import { StatsCards } from '@/components/Center/Dashboard/StatsCards';
3+
import StatsCards from '@/components/Center/Dashboard/StatsCards';
44
import { Sidebar } from '@/components/ui/custom/Sidebar';
5+
import { useDashboardStats } from '@/hooks/center/service/useDashboardStats';
6+
import { TriangleAlert } from 'lucide-react';
57

6-
export default function Dashboard() {
7-
return (
8-
<>
9-
{/* Mobile Layout */}
10-
<div className='flex flex-col h-screen lg:hidden'>
11-
<Header hasBorder={false} />
12-
<main className='flex-1 bg-[var(--button-inactive)] p-5 min-h-0'>
13-
<StatsCards />
14-
</main>
15-
<Footer />
16-
</div>
8+
const ErrorFallback = ({ error, onRetry }) => (
9+
<div className='flex flex-col items-center justify-center min-h-[50vh] p-8'>
10+
<div className='bg-red-50 border border-red-200 rounded-[1.25rem] p-6 max-w-md text-center'>
11+
<TriangleAlert className='mx-auto w-15 h-auto text-red-800' />
12+
<h2 className='text-xl font-semibold text-red-800 mb-2'>오류가 발생했습니다</h2>
13+
<p className='text-red-600 text-base mb-4'>
14+
{error?.message || '페이지를 불러오는 중 문제가 발생했습니다.'}
15+
</p>
16+
<button
17+
onClick={onRetry}
18+
className='bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-base transition-colors cursor-pointer'
19+
>
20+
다시 시도하기
21+
</button>
22+
</div>
23+
</div>
24+
);
1725

18-
{/* Desktop Layout */}
19-
<div className='hidden lg:flex lg:h-screen'>
26+
const MobileLayout = ({ statsData, isLoading, isError, fallbackProps }) => (
27+
<div className='flex flex-col h-screen lg:hidden'>
28+
<header className='flex-shrink-0'>
29+
<Header hasBorder={false} />
30+
</header>
31+
32+
<main className='flex-1 bg-[var(--button-inactive)] p-5 min-h-0 overflow-y-auto'>
33+
{/* TODO: 추후 로딩 처리 */}
34+
{isLoading ? (
35+
<div>로딩중...</div>
36+
) : isError ? (
37+
<ErrorFallback {...fallbackProps} />
38+
) : (
39+
<StatsCards statsData={statsData} />
40+
)}
41+
</main>
42+
43+
<footer className='flex-shrink-0'>
44+
<Footer isManager={true} />
45+
</footer>
46+
</div>
47+
);
48+
49+
const DesktopLayout = ({ statsData, isLoading, isError, fallbackProps }) => {
50+
return (
51+
<div className='hidden lg:flex lg:h-screen'>
52+
<aside className='flex-shrink-0'>
2053
<Sidebar />
54+
</aside>
2155

22-
<div className='flex-1 flex flex-col'>
56+
<div className='flex-1 flex flex-col min-w-0'>
57+
<header className='flex-shrink-0'>
2358
<DesktopHeader onSidebarToggle={() => {}} isSidebarCollapsed={false} />
59+
</header>
2460

25-
<main className='flex-1 bg-[var(--button-inactive)] lg:bg-[#f6f8fc] px-8 py-5 lg:py-20 lg:px-14 min-h-0'>
26-
<StatsCards />
27-
</main>
28-
</div>
61+
<main className='flex-1 bg-[var(--button-inactive)] lg:bg-[#f6f8fc] px-8 py-5 lg:py-20 lg:px-14 min-h-0 overflow-y-auto'>
62+
{/* TODO: 추후 로딩 처리 필요 */}
63+
{isLoading ? (
64+
<div>로딩중..</div>
65+
) : isError ? (
66+
<ErrorFallback {...fallbackProps} />
67+
) : (
68+
<StatsCards statsData={statsData} />
69+
)}
70+
</main>
2971
</div>
72+
</div>
73+
);
74+
};
75+
76+
export default function Dashboard() {
77+
const { data: statsData, isLoading, isError, error, refetch } = useDashboardStats();
78+
79+
const fallbackProps = {
80+
error,
81+
onRetry: refetch,
82+
};
83+
84+
return (
85+
<>
86+
<MobileLayout
87+
statsData={statsData}
88+
isLoading={isLoading}
89+
isError={isError}
90+
fallbackProps={fallbackProps}
91+
/>
92+
<DesktopLayout
93+
statsData={statsData}
94+
isLoading={isLoading}
95+
isError={isError}
96+
fallbackProps={fallbackProps}
97+
/>
3098
</>
3199
);
32100
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { request } from '@/api';
2+
3+
export const getDashboardStats = async () => {
4+
const response = await request('GET', '/center-manager/statistics-dashboard');
5+
return response;
6+
};

0 commit comments

Comments
 (0)