diff --git a/src/App.tsx b/src/App.tsx
index 20107af..54999c7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,6 +6,7 @@ import EducationPage from '@pages/EducationPage';
import QuizHomePage from '@pages/QuizHomePage.tsx';
import ProfilePage from '@pages/ProfilePage';
import QuizPage from '@pages/QuizPage.tsx';
+import StatisticsPage from '@pages/StatisticsPage.tsx';
import { Global } from '@emotion/react';
import { globalStyles } from './styles/GlobalStyles';
@@ -20,6 +21,7 @@ function App() {
} />
} />
} />
+ } />
>
diff --git a/src/components/common/BottomNavBar.tsx b/src/components/common/BottomNavBar.tsx
index 362b54d..4a9a763 100644
--- a/src/components/common/BottomNavBar.tsx
+++ b/src/components/common/BottomNavBar.tsx
@@ -19,6 +19,7 @@ const NavContainer = styled.nav`
padding: 10px 0;
border-top: 2px solid #4d403d;
font-family: 'Noto Sans KR', sans-serif;
+ z-index: 100;
`;
const StyledNavLink = styled(NavLink)`
@@ -60,6 +61,10 @@ const BottomNavBar = () => {
quiz
+
+ bar_chart
+
+
person
diff --git a/src/components/statistics/StatsCard.tsx b/src/components/statistics/StatsCard.tsx
new file mode 100644
index 0000000..e84e37d
--- /dev/null
+++ b/src/components/statistics/StatsCard.tsx
@@ -0,0 +1,60 @@
+import type { CardCategory } from '@@types/index';
+import styled from '@emotion/styled';
+import { CARD_CATEGORY_KO, categoryMaterialIcons } from '../../utils';
+import { GGAMJA_COLOR } from '../../styles/Colors';
+
+const Container = styled.div`
+ background-color: white;
+ border: 2px solid ${GGAMJA_COLOR.DARK_BROWN};
+ border-radius: 8px;
+ padding: 10px;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ aspect-ratio: 1 / 1;
+ box-shadow: 4px 4px 0 ${GGAMJA_COLOR.DARK_BROWN};
+ gap: 5px;
+`;
+
+const Icon = styled.span`
+ font-size: 30px;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+`;
+
+const CardTitle = styled.h3`
+ font-size: 20px;
+ font-weight: 700;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+ margin: 8px 0 4px 0;
+`;
+
+const StatsInfo = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 12px;
+ color: ${GGAMJA_COLOR.LIGHT_BROWN};
+`;
+
+interface StatsCardProps {
+ category: CardCategory;
+ quizLogCount: number;
+ cardLogCount: number;
+}
+
+const StatsCard = ({ category, quizLogCount, cardLogCount }: StatsCardProps) => {
+ return (
+
+ {categoryMaterialIcons[category] || 'help'}
+ {CARD_CATEGORY_KO[category]}
+
+ 퀴즈: {quizLogCount}회
+ 학습: {cardLogCount}회
+
+
+ );
+};
+
+export default StatsCard;
diff --git a/src/components/statistics/StatsGrpah.tsx b/src/components/statistics/StatsGrpah.tsx
new file mode 100644
index 0000000..db0eee9
--- /dev/null
+++ b/src/components/statistics/StatsGrpah.tsx
@@ -0,0 +1,97 @@
+import styled from '@emotion/styled';
+import { CARD_CATEGORY_KO } from '@utils/index';
+import type { CardCategory } from '@@types/index';
+import { GGAMJA_COLOR } from '../../styles/Colors';
+
+interface CategoryData {
+ category: CardCategory;
+ accuracy: number;
+}
+
+interface StatsGraphProps {
+ data: CategoryData[];
+}
+
+const Container = styled.div`
+ width: 100%;
+ height: 200px;
+ display: flex;
+ align-items: end;
+ justify-content: space-around;
+ gap: 8px;
+ padding: 20px 0;
+`;
+
+const GraphItem = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ height: 100%;
+`;
+
+const BarContainer = styled.div`
+ width: 100%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: end;
+`;
+
+const CharacterImage = styled.img<{ barHeight: number }>`
+ width: 100%;
+ max-width: 30px;
+ height: 100%;
+ max-height: 30px;
+ object-fit: contain;
+ margin-bottom: ${({ barHeight }) => (barHeight === 0 ? '0px' : '-10px')};
+ z-index: 1;
+`;
+
+const Bar = styled.div<{ height: number }>`
+ width: 100%;
+ max-width: 20px;
+ background-color: ${GGAMJA_COLOR.GREEN};
+ border-radius: 8px;
+ height: ${({ height }) => height}%;
+ transition: height 0.3s ease;
+ min-height: 0;
+`;
+
+const CategoryName = styled.span`
+ font-size: 14px;
+ font-weight: 600;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+ text-align: center;
+ text-orientation: mixed;
+`;
+
+const AccuracyText = styled.span`
+ font-size: 12px;
+ font-weight: 500;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+ text-align: center;
+`;
+
+const StatsGraph = ({ data }: StatsGraphProps) => {
+ return (
+
+ {data.map((item, index) => (
+
+
+
+
+
+ {CARD_CATEGORY_KO[item.category]}
+ {item.accuracy}%
+
+ ))}
+
+ );
+};
+
+export default StatsGraph;
diff --git a/src/hooks/useMonthlyAnalysis.tsx b/src/hooks/useMonthlyAnalysis.tsx
new file mode 100644
index 0000000..4f4942c
--- /dev/null
+++ b/src/hooks/useMonthlyAnalysis.tsx
@@ -0,0 +1,14 @@
+import useApi from '@hooks/useApi';
+import type { ApiResponse, MonthlyAnalysisResponse } from '@@types/index.ts';
+
+function useMonthlyAnalysis() {
+ const { api } = useApi();
+
+ const fetchMonthlyAnalysis = () => {
+ return api.get>(`/analysis/categories/monthly`).then((response) => response.data.data);
+ };
+
+ return { fetchMonthlyAnalysis };
+}
+
+export default useMonthlyAnalysis;
diff --git a/src/pages/StatisticsPage.tsx b/src/pages/StatisticsPage.tsx
new file mode 100644
index 0000000..b013310
--- /dev/null
+++ b/src/pages/StatisticsPage.tsx
@@ -0,0 +1,162 @@
+import { useEffect, useState } from 'react';
+import BottomNavBar from '@components/common/BottomNavBar.tsx';
+import StatsCard from '@components/statistics/StatsCard.tsx';
+import StatsGraph from '@components/statistics/StatsGrpah.tsx';
+import useMonthlyAnalysis from '@hooks/useMonthlyAnalysis.tsx';
+import styled from '@emotion/styled';
+import { GGAMJA_COLOR } from '../styles/Colors.ts';
+import type { CardCategory } from '@@types/index.ts';
+import { CARD_CATEGORY_KO } from '@utils/index.ts';
+
+const PageWrapper = styled.div`
+ background-color: #faf8f5;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+ padding-bottom: 80px;
+ height: 100%;
+ overflow: auto;
+`;
+
+const Container = styled.div`
+ padding: 40px 20px 20px 20px;
+`;
+
+const Header = styled.header`
+ text-align: center;
+`;
+
+const PageTitle = styled.h1`
+ font-size: 36px;
+ font-weight: 700;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+`;
+
+const StatisticsContentWrapper = styled.div`
+ border: 1px solid ${GGAMJA_COLOR.DARK_BROWN};
+ border-radius: 10px;
+ padding: 40px 20px 40px 20px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ aspect-ratio: 1 / 1;
+ box-shadow: 4px 4px 0 ${GGAMJA_COLOR.DARK_BROWN};
+ gap: 10px;
+`;
+
+const StatsCardGridContainer = styled.div`
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+ gap: 20px;
+ width: 100%;
+ align-items: center;
+`;
+
+const StatisticsContent = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 100%;
+ border: 1px solid ${GGAMJA_COLOR.DARK_BROWN};
+ border-radius: 10px;
+ gap: 10px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 4px 4px 0 ${GGAMJA_COLOR.DARK_BROWN};
+`;
+
+const StatisticsTitle = styled.h2`
+ font-size: 24px;
+ font-weight: 700;
+ width: 100%;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+ text-align: left;
+`;
+
+const StatisticsTextContainer = styled.div<{ isSurpassed: boolean }>`
+ font-size: 20px;
+ font-weight: 400;
+ width: 100%;
+ color: ${GGAMJA_COLOR.DARK_BROWN};
+`;
+
+const StatisticsCategory = styled.h3`
+ display: inline;
+ font-weight: 700;
+`;
+
+const StatisticsPage = () => {
+ const { fetchMonthlyAnalysis } = useMonthlyAnalysis();
+ const [nickname, setNickname] = useState('사용자');
+ const [categories, setCategories] = useState<{ category: CardCategory; quizLogCount: number; cardLogCount: number; accuracy: number }[]>([]);
+ const [strengths, setStrengths] = useState([]);
+ const [weaknesses, setWeaknesses] = useState([]);
+ const [allEqual, setAllEqual] = useState(false);
+
+ useEffect(() => {
+ const loadStatistics = async () => {
+ const statisticsData = await fetchMonthlyAnalysis();
+ setNickname(statisticsData.nickname);
+ setCategories(statisticsData.categories);
+ setStrengths(statisticsData.strengths);
+ setWeaknesses(statisticsData.weaknesses);
+ setAllEqual(statisticsData.allEqual);
+ };
+
+ loadStatistics();
+ }, []);
+
+ const renderStatisticsContent = () => {
+ if (!allEqual) {
+ return [
+ { data: strengths, isSurpassed: true, message: '분야에 강하시네요!' },
+ { data: weaknesses, isSurpassed: false, message: '분야를 조금 더 공부해보아요!' },
+ ].map(
+ ({ data, isSurpassed, message }) =>
+ data.length > 0 && (
+
+ {data.map((category, index) => (
+
+ {CARD_CATEGORY_KO[category as CardCategory]}
+ {index < data.length - 1 ? ', ' : ''}
+
+ ))}
+ {message}
+
+ ),
+ );
+ }
+ const isNeverStudied = categories[0].accuracy == 0;
+ return isNeverStudied ? '지금 당장 공부하러 가볼까요?' : '모든 카테고리의 정답률이 동일합니다.';
+ };
+
+ return (
+
+
+
+
+ 카테고리 분석
+
+
+ {categories.map((item, index) => (
+
+ ))}
+
+
+ 정답률 분석
+
+
+
+ {nickname}님, 이번 달에는 ...
+ {renderStatisticsContent()}
+
+
+
+
+ );
+};
+
+export default StatisticsPage;
diff --git a/src/types/index.ts b/src/types/index.ts
index e55f423..ade6dbc 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -140,3 +140,18 @@ export interface AttendanceResponse {
];
allCompleted: boolean;
}
+
+export interface MonthlyAnalysisResponse {
+ nickname: string;
+ categories: [
+ {
+ category: CardCategory;
+ quizLogCount: number;
+ cardLogCount: number;
+ accuracy: number;
+ },
+ ];
+ strengths: CardCategory[];
+ weaknesses: CardCategory[];
+ allEqual: boolean;
+}