Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
70 changes: 48 additions & 22 deletions src/components/profile/LearningHistoryTab.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
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 { DetailStudyLog, PaginationResponse, StudyLog } from '@@types/index.ts';
import { defaultDetailStudylogValue, 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';
import Modal from '@components/common/Modal.tsx';
import LearningHistoryDetail from './details/LearningHistoryDetail.tsx';

export const TabContent = styled.div<{ isActive: boolean }>`
display: ${({ isActive }) => (isActive ? 'block' : 'none')};
Expand All @@ -26,6 +21,7 @@ export const HistoryItem = styled.div`
padding: 16px;
margin-bottom: 16px;
box-shadow: 4px 4px 0px #4d403d;
cursor: pointer;
`;

export const HistoryIcon = styled.span`
Expand Down Expand Up @@ -54,17 +50,47 @@ interface LearningHistoryTabProps {
}

const LearningHistoryTab = ({ isActive }: LearningHistoryTabProps) => {
const [response, setResponse] = useState<PaginationResponse<StudyLog[]>>(defaultPaginationValue);
const [studylogs, setStudylods] = useState<StudyLog[]>([]);
const { fetchStudylogs, fetchDetailStudylog } = useStudylogs();
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedLog, setSelectedLog] = useState<DetailStudyLog>(defaultDetailStudylogValue);

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();
}, []);

const handleItemClick = async (log: StudyLog) => {
const card = await fetchDetailStudylog(log.logId);
setSelectedLog(card);
setIsModalOpen(true);
};

return (
<TabContent isActive={isActive}>
{learningHistoryData.map((item, index) => (
<HistoryItem key={index}>
<HistoryIcon className="material-symbols-outlined">{item.icon}</HistoryIcon>
<HistoryDetails>
<h3>{item.title}</h3>
<p>{item.date}</p>
</HistoryDetails>
</HistoryItem>
))}
<InfiniteScroll next={fetchMoreData} hasMore={!response.last} loader={null} dataLength={studylogs.length}>
{studylogs.map((item, index) => (
<HistoryItem key={index} onClick={() => handleItemClick(item)}>
<HistoryIcon className="material-symbols-outlined">{categoryMaterialIcons[item.category]}</HistoryIcon>
<HistoryDetails>
<h3>{item.title}</h3>
<p>
{item.date.split('T')[0]} ・ {CARD_CATEGORY_KO[item.category]}
</p>
</HistoryDetails>
</HistoryItem>
))}
</InfiniteScroll>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
{selectedLog && <LearningHistoryDetail detailStudylog={selectedLog} />}
</Modal>
</TabContent>
);
};
Expand Down
84 changes: 50 additions & 34 deletions src/components/profile/QuizHistoryTab.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
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 { DetailQuizLog, 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';
import Modal from '@components/common/Modal.tsx';
import QuizHistoryDetail from './details/QuizHistoryDetail.tsx';

export const TabContent = styled.div<{ isActive: boolean }>`
display: ${({ isActive }) => (isActive ? 'block' : 'none')};
Expand All @@ -34,6 +21,7 @@ export const HistoryItem = styled.div`
padding: 16px;
margin-bottom: 16px;
box-shadow: 4px 4px 0px #4d403d;
cursor: pointer;
`;

export const HistoryIcon = styled.span`
Expand All @@ -57,29 +45,57 @@ 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 {
isActive: boolean;
}

const QuizHistoryTab = ({ isActive }: QuizHistoryTabProps) => {
const [response, setResponse] = useState<PaginationResponse<QuizLog[]>>(defaultPaginationValue);
const [quizlogs, setQuizlogs] = useState<QuizLog[]>([]);
const { getQuizLogs, getDetailQuizLog } = useQuizLog();
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedLog, setSelectedLog] = useState<DetailQuizLog | null>(null);

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);
};

const handleItemClick = async (log: QuizLog) => {
const quizlog = await getDetailQuizLog(log.quizLogId);
setSelectedLog(quizlog);
setIsModalOpen(true);
};

return (
<TabContent isActive={isActive}>
{quizHistoryData.map((item, index) => (
<HistoryItem key={index}>
<HistoryIcon className="material-symbols-outlined">{item.icon}</HistoryIcon>
<HistoryDetails>
<h3>{item.title}</h3>
<p>
{item.date} ・ <QuizScore>점수: {item.score}</QuizScore>
</p>
</HistoryDetails>
</HistoryItem>
))}
<InfiniteScroll next={fetchMoreData} hasMore={!response.last} loader={null} dataLength={quizlogs.length}>
{quizlogs.map((item, index) => (
<HistoryItem key={index} onClick={() => handleItemClick(item)}>
<HistoryIcon className="material-symbols-outlined">{categoryMaterialIcons[item.category]}</HistoryIcon>
<HistoryDetails>
<h3>{CARD_CATEGORY_KO[item.category]} 퀴즈 완료!</h3>
<p>
{item.date.split('T')[0]} ・ <QuizScore isCorrect={item.isCorrect}>{item.isCorrect ? '정답' : '오답'}</QuizScore>
</p>
</HistoryDetails>
</HistoryItem>
))}
</InfiniteScroll>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
{selectedLog && <QuizHistoryDetail quizLog={selectedLog} />}
</Modal>
</TabContent>
);
};
Expand Down
82 changes: 82 additions & 0 deletions src/components/profile/details/LearningHistoryDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import styled from '@emotion/styled';
import type { DetailStudyLog } from '@@types/index.ts';
import { CARD_CATEGORY_KO } from '@utils/index.ts';

const CardScene = styled.div`
perspective: 1000px;
width: 100%;
height: 400px;
`;

const CardContainer = styled.div`
width: 100%;
height: 100%;
position: relative;
`;

const CardFace = styled.div`
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 10px;
background-color: #f8f9fa;
`;

const CardHeader = styled.div`
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #6c757d;
`;

const CardCategory = styled.span`
font-weight: bold;
`;

const InfoTitle = styled.div`
font-weight: bold;
font-size: 1rem;
color: #495057;
margin-bottom: 5px;
`;

const CardText = styled.div`
font-size: 1rem;
color: #212529;
margin-bottom: 15px;
line-height: 1.6;
`;

const CardDescription = styled(CardText)`
flex-grow: 1;
`;

interface LearningHistoryDetailProps {
detailStudylog: DetailStudyLog;
}

const LearningHistoryDetail = ({ detailStudylog }: LearningHistoryDetailProps) => {
return (
<CardScene>
<CardContainer>
<CardFace>
<CardHeader>
<CardCategory>{CARD_CATEGORY_KO[detailStudylog.category]}</CardCategory>
</CardHeader>
<InfoTitle>해설</InfoTitle>
<CardDescription>{detailStudylog.meaning}</CardDescription>
</CardFace>
</CardContainer>
</CardScene>
);
};

export default LearningHistoryDetail;
66 changes: 66 additions & 0 deletions src/components/profile/details/QuizHistoryDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import styled from '@emotion/styled';
import type { DetailQuizLog } from '@@types/index.ts';

const QuizDetailContainer = styled.div`
padding: 20px;
background-color: #f7f7f7;
border-radius: 8px;
`;

const Question = styled.h3`
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 20px;
`;

const OptionList = styled.ul`
list-style: none;
padding: 0;
margin: 0 0 20px 0;
`;

const OptionItem = styled.li<{
isCorrect: boolean;
isSelected: boolean;
}>`
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
border: 1px solid #ddd;
background-color: ${({ isSelected, isCorrect }) => (isSelected ? (isCorrect ? '#d4edda' : '#f8d7da') : '#fff')};
color: ${({ isSelected, isCorrect }) => (isSelected ? (isCorrect ? '#155724' : '#721c24') : '#333')};
`;

const Result = styled.div`
font-weight: bold;
color: ${({ isCorrect }: { isCorrect: boolean }) => (isCorrect ? '#28a745' : '#dc3545')};
`;

interface QuizHistoryDetailProps {
quizLog: DetailQuizLog;
}

const QuizHistoryDetail = ({ quizLog }: QuizHistoryDetailProps) => {
const { quiz, selectedAnswer, isCorrect } = quizLog;
const options = [quiz.option1, quiz.option2, quiz.option3, quiz.answer].sort(() => Math.random() - 0.5);

return (
<QuizDetailContainer>
<Question>{quiz.question}</Question>
<OptionList>
{options.map((option, index) => {
const isSelected = option === selectedAnswer;
const isCorrectOption = option === quiz.answer;
return (
<OptionItem key={index} isSelected={isSelected} isCorrect={isCorrectOption}>
{option}
</OptionItem>
);
})}
</OptionList>
<Result isCorrect={isCorrect}>{isCorrect ? '정답입니다!' : `오답입니다. (정답: ${quiz.answer})`}</Result>
</QuizDetailContainer>
);
};

export default QuizHistoryDetail;
25 changes: 25 additions & 0 deletions src/hooks/useQuizLog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import useApi from '@hooks/useApi.tsx';
import type { ApiResponse, DetailQuizLog, PaginationResponse, QuizLog } from '@@types/index.ts';
import { defaultDetailQuizLogValue, defaultPaginationValue } from '@@types/defaultValues.ts';

function useQuizLog() {
const { api } = useApi();

const getQuizLogs = (page: number, size: number) => {
return api
.get<ApiResponse<PaginationResponse<QuizLog[]>>>(`/quizlogs`, { params: { page, size } })
.then((response) => response.data.data)
.catch(() => defaultPaginationValue);
};

const getDetailQuizLog = (quizLogId: number) => {
return api
.get<ApiResponse<DetailQuizLog>>(`/quizlogs/${quizLogId}`)
.then((response) => response.data.data)
.catch(() => defaultDetailQuizLogValue);
};

return { getQuizLogs, getDetailQuizLog };
}

export default useQuizLog;
Loading