diff --git a/package.json b/package.json index 0a039a4..6be3a2c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axios": "^1.11.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^7.8.1" }, "devDependencies": { diff --git a/src/components/profile/LearningHistoryTab.tsx b/src/components/profile/LearningHistoryTab.tsx index 2d50dee..106bb4b 100644 --- a/src/components/profile/LearningHistoryTab.tsx +++ b/src/components/profile/LearningHistoryTab.tsx @@ -1,17 +1,10 @@ import styled from '@emotion/styled'; - -const learningHistoryData = [ - { - icon: 'history_edu', - title: '역사 카드 학습 완료!', - date: '2025년 8월 28일', - }, - { - icon: 'science', - title: '과학 카드 학습 완료!', - date: '2025년 8월 27일', - }, -]; +import { useEffect, useState } from 'react'; +import type { PaginationResponse, StudyLog } from '@@types/index.ts'; +import { defaultPaginationValue } from '@@types/defaultValues.ts'; +import useStudylogs from '@hooks/useStudylogs.tsx'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import { CARD_CATEGORY_KO, categoryMaterialIcons } from '@utils/index.ts'; export const TabContent = styled.div<{ isActive: boolean }>` display: ${({ isActive }) => (isActive ? 'block' : 'none')}; @@ -54,17 +47,36 @@ interface LearningHistoryTabProps { } const LearningHistoryTab = ({ isActive }: LearningHistoryTabProps) => { + const [response, setResponse] = useState>(defaultPaginationValue); + const [studylogs, setStudylods] = useState([]); + const { fetchStudylogs } = useStudylogs(); + + const fetchMoreData = async () => { + const nextPage = response.pageable.pageNumber + 1; + const res = await fetchStudylogs(nextPage, response.pageable.pageSize); + setStudylods((prev) => [...prev, ...res.content]); + setResponse(res); + }; + + useEffect(() => { + fetchMoreData(); + }, []); + return ( - {learningHistoryData.map((item, index) => ( - - {item.icon} - -

{item.title}

-

{item.date}

-
-
- ))} + + {studylogs.map((item, index) => ( + + {categoryMaterialIcons[item.category]} + +

{item.title}

+

+ {item.date.split('T')[0]} ・ {CARD_CATEGORY_KO[item.category]} +

+
+
+ ))} +
); }; diff --git a/src/components/profile/QuizHistoryTab.tsx b/src/components/profile/QuizHistoryTab.tsx index c9d4051..5619e71 100644 --- a/src/components/profile/QuizHistoryTab.tsx +++ b/src/components/profile/QuizHistoryTab.tsx @@ -1,25 +1,10 @@ import styled from '@emotion/styled'; - -const quizHistoryData = [ - { - icon: 'lightbulb', - title: '상식 퀴즈 완료!', - date: '2025년 8월 28일', - score: '8 / 10', - }, - { - icon: 'menu_book', - title: '속담 퀴즈 완료!', - date: '2025년 8월 26일', - score: '10 / 10', - }, - { - icon: 'account_balance', - title: '수도 퀴즈 완료!', - date: '2025년 8월 25일', - score: '6 / 10', - }, -]; +import { useEffect, useState } from 'react'; +import type { PaginationResponse, QuizLog } from '@@types/index.ts'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import { defaultPaginationValue } from '@@types/defaultValues.ts'; +import useQuizLog from '@hooks/useQuizLog.tsx'; +import { CARD_CATEGORY_KO, categoryMaterialIcons } from '@utils/index.ts'; export const TabContent = styled.div<{ isActive: boolean }>` display: ${({ isActive }) => (isActive ? 'block' : 'none')}; @@ -57,9 +42,9 @@ export const HistoryDetails = styled.div` } `; -export const QuizScore = styled.span` +export const QuizScore = styled.span<{ isCorrect: boolean }>` font-weight: 700; - color: #84cc16; + color: ${({ isCorrect }) => (isCorrect ? '#22c55e' : '#ef4444')}; `; interface QuizHistoryTabProps { @@ -67,19 +52,36 @@ interface QuizHistoryTabProps { } const QuizHistoryTab = ({ isActive }: QuizHistoryTabProps) => { + const [response, setResponse] = useState>(defaultPaginationValue); + const [quizlogs, setQuizlogs] = useState([]); + const { getQuizLogs } = useQuizLog(); + + useEffect(() => { + fetchMoreData(); + }, []); + + const fetchMoreData = async () => { + const nextPage = response.pageable.pageNumber + 1; + const res = await getQuizLogs(nextPage, response.pageable.pageSize); + setQuizlogs((prev) => [...prev, ...res.content]); + setResponse(res); + }; + return ( - {quizHistoryData.map((item, index) => ( - - {item.icon} - -

{item.title}

-

- {item.date} ・ 점수: {item.score} -

-
-
- ))} + + {quizlogs.map((item, index) => ( + + {categoryMaterialIcons[item.category]} + +

{CARD_CATEGORY_KO[item.category]} 퀴즈 완료!

+

+ {item.date.split('T')[0]} ・ {item.isCorrect ? '정답' : '오답'} +

+
+
+ ))} +
); }; diff --git a/src/hooks/useQuizLog.tsx b/src/hooks/useQuizLog.tsx new file mode 100644 index 0000000..a0c9436 --- /dev/null +++ b/src/hooks/useQuizLog.tsx @@ -0,0 +1,18 @@ +import useApi from '@hooks/useApi.tsx'; +import type { ApiResponse, PaginationResponse, QuizLog } from '@@types/index.ts'; +import { defaultPaginationValue } from '@@types/defaultValues.ts'; + +function useQuizLog() { + const { api } = useApi(); + + const getQuizLogs = (page: number, size: number) => { + return api + .get>>(`/quizlogs`, { params: { page, size } }) + .then((response) => response.data.data) + .catch(() => defaultPaginationValue); + }; + + return { getQuizLogs }; +} + +export default useQuizLog; diff --git a/src/hooks/useStudylogs.tsx b/src/hooks/useStudylogs.tsx new file mode 100644 index 0000000..aef435f --- /dev/null +++ b/src/hooks/useStudylogs.tsx @@ -0,0 +1,18 @@ +import useApi from '@hooks/useApi.tsx'; +import type { ApiResponse, PaginationResponse, StudyLog } from '@@types/index.ts'; +import { defaultPaginationValue } from '@@types/defaultValues.ts'; + +function useStudylogs() { + const { api } = useApi(); + + const fetchStudylogs = (page: number, size: number) => { + return api + .get>>('/studylogs', { params: { page, size } }) + .then((response) => response.data.data) + .catch(() => defaultPaginationValue); + }; + + return { fetchStudylogs }; +} + +export default useStudylogs; diff --git a/src/types/defaultValues.ts b/src/types/defaultValues.ts index 212f5e4..230c119 100644 --- a/src/types/defaultValues.ts +++ b/src/types/defaultValues.ts @@ -1,4 +1,4 @@ -import type { Card, Quiz } from '@@types/index.ts'; +import type { Card, PaginationResponse, Quiz } from '@@types/index.ts'; export const defaultQuizValue: Quiz = { todayQuizId: 0, @@ -15,3 +15,33 @@ export const defaultCardValue: Card = { meaning: 'Loading...', difficulty: 1, }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const defaultPaginationValue: PaginationResponse = { + content: [], + pageable: { + pageNumber: -1, + pageSize: 10, + sort: { + sorted: false, + empty: true, + unsorted: true, + }, + offset: 0, + paged: true, + unpaged: false, + }, + totalPages: 0, + totalElements: 0, + last: false, + number: 0, + size: 6, + numberOfElements: 0, + sort: { + sorted: false, + empty: true, + unsorted: true, + }, + first: true, + empty: true, +}; diff --git a/src/types/index.ts b/src/types/index.ts index e30b359..fd8e9d3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,6 +15,35 @@ export interface ApiResponse { data: T; } +export interface Sort { + sorted: boolean; + empty: boolean; + unsorted: boolean; +} + +export interface Pageable { + pageNumber: number; + pageSize: number; + sort: Sort; + offset: number; + paged: boolean; + unpaged: boolean; +} + +export interface PaginationResponse { + content: T[]; + pageable: Pageable; + totalPages: number; + totalElements: number; + last: boolean; + number: number; + size: number; + numberOfElements: number; + sort: Sort; + first: boolean; + empty: boolean; +} + export interface SignupResponse { id: number; nickname: string; @@ -61,3 +90,26 @@ export interface SubmitTodayQuizAnswerResponse { isCorrect: boolean; correctAnswer: string; } + +export interface QuizLog { + quizLogId: number; + date: string; + category: CardCategory; + isCorrect: boolean; +} + +export interface DetailQuizLog { + quizLogId: number; + isCorrect: boolean; + selectedAnswer: string; + date: string; + quiz: Quiz; +} + +export interface StudyLog { + logId: number; + category: CardCategory; + title: string; + difficulty: number; + date: string; +} diff --git a/yarn.lock b/yarn.lock index 7f36a6a..dbf3db6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1748,6 +1748,13 @@ react-dom@^19.1.1: dependencies: scheduler "^0.26.0" +react-infinite-scroll-component@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f" + integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ== + dependencies: + throttle-debounce "^2.1.0" + react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -1897,6 +1904,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +throttle-debounce@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" + integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== + tinyglobby@^0.2.14: version "0.2.14" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"