-
Notifications
You must be signed in to change notification settings - Fork 0
FEAT - 통계 페이지 구현 및 API 연동 #37
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
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
27d163c
ADD - 통계 페이지 생성 및 네비게이션바에 바로가기 추가
Joshcho426 ed0e54b
ADD - 통계페이지 정답률 막대그래프 컴포넌트 생성 및 페이지에 dummy data 추가
Joshcho426 c592741
FEAT - 통계페이지 API 연동
Joshcho426 d2b83d5
ADD - 닉네임 state 관리
Joshcho426 89a33e7
EDIT - 네비게이션 바 위치 변경
Joshcho426 dd23ab2
EDIT - 네비게이션 바 z-index 추가
Joshcho426 27ced4f
FEAT - 통계 페이지 카테고리 분석란 추가
Joshcho426 34832e1
EDIT - 삼항 연산자 중첩 제거 및 useState 타입 변경
Joshcho426 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <Container> | ||
| <Icon className="material-symbols-outlined">{categoryMaterialIcons[category] || 'help'}</Icon> | ||
| <CardTitle>{CARD_CATEGORY_KO[category]}</CardTitle> | ||
| <StatsInfo> | ||
| <div>퀴즈: {quizLogCount}회</div> | ||
| <div>학습: {cardLogCount}회</div> | ||
| </StatsInfo> | ||
| </Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default StatsCard; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <Container> | ||
| {data.map((item, index) => ( | ||
| <GraphItem key={index}> | ||
| <BarContainer> | ||
| <CharacterImage src="https://ggamja-images.s3.ap-northeast-2.amazonaws.com/level1.png" barHeight={item.accuracy} /> | ||
| <Bar height={item.accuracy} /> | ||
| </BarContainer> | ||
| <CategoryName>{CARD_CATEGORY_KO[item.category]}</CategoryName> | ||
| <AccuracyText>{item.accuracy}%</AccuracyText> | ||
| </GraphItem> | ||
| ))} | ||
| </Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default StatsGraph; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ApiResponse<MonthlyAnalysisResponse>>(`/analysis/categories/monthly`).then((response) => response.data.data); | ||
| }; | ||
|
|
||
| return { fetchMonthlyAnalysis }; | ||
| } | ||
|
|
||
| export default useMonthlyAnalysis; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string>('사용자'); | ||
| const [categories, setCategories] = useState<{ category: CardCategory; quizLogCount: number; cardLogCount: number; accuracy: number }[]>([]); | ||
| const [strengths, setStrengths] = useState<CardCategory[]>([]); | ||
| const [weaknesses, setWeaknesses] = useState<CardCategory[]>([]); | ||
| 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 && ( | ||
| <StatisticsTextContainer key={message} isSurpassed={isSurpassed}> | ||
| {data.map((category, index) => ( | ||
| <StatisticsCategory key={index}> | ||
| {CARD_CATEGORY_KO[category as CardCategory]} | ||
| {index < data.length - 1 ? ', ' : ''} | ||
| </StatisticsCategory> | ||
| ))} | ||
| {message} | ||
| </StatisticsTextContainer> | ||
| ), | ||
| ); | ||
| } | ||
| const isNeverStudied = categories[0].accuracy == 0; | ||
| return isNeverStudied ? '지금 당장 공부하러 가볼까요?' : '모든 카테고리의 정답률이 동일합니다.'; | ||
| }; | ||
|
|
||
| return ( | ||
| <PageWrapper> | ||
| <Container> | ||
| <Header> | ||
| <PageTitle>이번 달 통계</PageTitle> | ||
| </Header> | ||
| <StatisticsContentWrapper> | ||
| <StatisticsTitle>카테고리 분석</StatisticsTitle> | ||
| <StatisticsContent> | ||
| <StatsCardGridContainer> | ||
| {categories.map((item, index) => ( | ||
| <StatsCard key={index} category={item.category} quizLogCount={item.quizLogCount} cardLogCount={item.cardLogCount} /> | ||
| ))} | ||
| </StatsCardGridContainer> | ||
| </StatisticsContent> | ||
| <StatisticsTitle>정답률 분석</StatisticsTitle> | ||
| <StatisticsContent> | ||
| <StatsGraph data={categories} /> | ||
| </StatisticsContent> | ||
| <StatisticsTitle>{nickname}님, 이번 달에는 ...</StatisticsTitle> | ||
| <StatisticsContent>{renderStatisticsContent()}</StatisticsContent> | ||
| </StatisticsContentWrapper> | ||
| <BottomNavBar /> | ||
| </Container> | ||
| </PageWrapper> | ||
| ); | ||
| }; | ||
|
|
||
| export default StatisticsPage; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.