diff --git a/src/api/apis/mainFetch.ts b/src/api/apis/mainFetch.ts index f2f569f..165f474 100644 --- a/src/api/apis/mainFetch.ts +++ b/src/api/apis/mainFetch.ts @@ -51,6 +51,5 @@ export const mainfetch = async ( headers.Authorization = `Bearer ${newAccessToken}`; response = await fetch(url, { ...fetchOptions, headers }); } - return response; }; diff --git a/src/api/types/analysis.ts b/src/api/types/analysis.ts new file mode 100644 index 0000000..5c4d1f3 --- /dev/null +++ b/src/api/types/analysis.ts @@ -0,0 +1,18 @@ +interface VulnerableSubject { + subjectId: number; + subjectName: string; + vulnerableRate: number; +} + +interface VulnerableTag { + tagId: number; + tagName: string; + vulnerableRate: number; +} + +interface AnalysisToday { + studyModeCount: number; + examModeCount: number; + studyModeCorrectRate: number; + examModeCorrectRate: number; +} diff --git a/src/app/analysis/atom/UserAnalysisInfo.tsx b/src/app/analysis/atom/UserAnalysisInfo.tsx new file mode 100644 index 0000000..8142f49 --- /dev/null +++ b/src/app/analysis/atom/UserAnalysisInfo.tsx @@ -0,0 +1,76 @@ +import { Box, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; + +type UserInfo = { + nickname: string; + profileImage: string; +}; + +const UserAnalysisInfo = () => { + const [userInfo, setUserInfo] = useState(null); + const [certificateInfo, setCertificateInfo] = useState(null); + useEffect(() => { + const getUserInfo = localStorage.getItem("userInfo"); + const getCertificateInfo = localStorage.getItem("certificate"); + if (getUserInfo) { + setUserInfo(JSON.parse(getUserInfo)); + } + if (getCertificateInfo) { + setCertificateInfo(JSON.parse(getCertificateInfo)); + } + }, []); + if (!userInfo || !certificateInfo) { + return <>로딩중...; + } + + return ( + + + + {userInfo!.nickname}님의  + + {certificateInfo!.name}  + + 학습 분석 + + + + ); +}; + +export default UserAnalysisInfo; diff --git a/src/app/analysis/atom/correctRateCircle.tsx b/src/app/analysis/atom/correctRateCircle.tsx new file mode 100644 index 0000000..35eaf6f --- /dev/null +++ b/src/app/analysis/atom/correctRateCircle.tsx @@ -0,0 +1,197 @@ +import { Typography, useMediaQuery, useTheme } from "@mui/material"; +import styled, { keyframes } from "styled-components"; +import { memo, useEffect, useState } from "react"; + +const rotateRight = (end: string) => keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(${end}); + } +`; + +const rotateLeft = (end: string) => keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(${end}); + } +`; + +const fillBackground = (bgColor: string) => keyframes` + 0% { + background-color: #fff; + } + 99% { + background-color: #fff; + } + 100% { + background-color: ${bgColor}; + } +`; + +const Main = styled.div<{ size: number; disabled?: boolean; cursor?: string }>` + width: ${props => props.size}px; + height: ${props => props.size}px; + margin: 10px 0px; + position: relative; + background-color: ${props => (props.disabled ? "transparent" : props.theme.bgColor)}; + border-radius: 100%; + cursor: ${props => props.cursor}; +`; + +const Inner = styled.div<{ size: number; disabled?: boolean }>` + z-index: 4; + position: absolute; + top: 50%; + left: 50%; + width: ${props => props.size - props.theme.innerSize}px; + height: ${props => props.size - props.theme.innerSize}px; + margin-left: ${props => -(props.size - props.theme.innerSize) / 2}px; + margin-top: ${props => -(props.size - props.theme.innerSize) / 2}px; + border-radius: 100%; + background-color: ${props => (props.disabled ? "transparent" : "#fff")}; + animation: ${props => !props.disabled && fillBackground(props.theme.innerBgColor)} 2s linear both; +`; + +const HoldingRight = styled.div<{ size: number; disabled?: boolean }>` + position: absolute; + width: 100%; + height: 100%; + clip: rect(0px, ${props => props.size}px, ${props => props.size}px, ${props => props.size / 2}px); + background-color: ${props => (props.disabled ? "transparent" : props.theme.emptyBgColor)}; + border-radius: 100%; +`; + +const HoldingLeft = styled.div<{ size: number; disabled?: boolean }>` + position: absolute; + width: 100%; + height: 100%; + clip: rect(0px, ${props => props.size / 2}px, ${props => props.size}px, 0px); + background-color: ${props => (props.disabled ? "transparent" : props.theme.emptyBgColor)}; + border-radius: 100%; +`; + +const FillRight = styled.div<{ rotate: string; size: number; disabled?: boolean }>` + position: absolute; + width: 100%; + height: 100%; + border-radius: 100%; + clip: rect(0px, ${props => props.size / 2}px, ${props => props.size}px, 0px); + background-color: ${props => (props.disabled ? "transparent" : props.theme.fillBgColor)}; + animation: ${props => !props.disabled && rotateRight(props.rotate)} 1s linear both; +`; + +const FillLeft = styled.div<{ rotate: string; size: number; disabled?: boolean }>` + position: absolute; + width: 100%; + height: 100%; + border-radius: 100%; + clip: rect(0px, ${props => props.size}px, ${props => props.size}px, ${props => props.size / 2}px); + background-color: ${props => (props.disabled ? "transparent" : props.theme.fillBgColor)}; + animation: ${props => !props.disabled && rotateLeft(props.rotate)} 1s linear both; + animation-delay: 1s; +`; + +const Middle = styled.div<{ size: number }>` + z-index: 5; + position: absolute; + top: 50%; + left: 50%; + width: ${props => props.size}px; + height: ${props => props.size}px; + transform: translate(-50%, -50%); + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + color: ${props => props.theme.fontColor}; +`; + +type CorrectRateCircleProps = { + data: string; + rotateRightDegree: string; + rotateLeftDegree: string; + fillBorderColor: string; + emptyBorderColor: string; + innerBackgroundColor: string; + disabled?: boolean; + isXs?: boolean; + isSm?: boolean; + achive: boolean; +}; + +const CorrectRateCircle = memo( + ({ + data, + rotateRightDegree, + rotateLeftDegree, + fillBorderColor, + emptyBorderColor, + innerBackgroundColor, + disabled = false, + achive = false, + }: CorrectRateCircleProps) => { + const theme = useTheme(); + const isXs = useMediaQuery(theme.breakpoints.down("sm")); + const isSm = useMediaQuery(theme.breakpoints.between("sm", "md")); + const [fontColor, setFontColor] = useState(disabled ? "grey" : "black"); + const [size, setSize] = useState(isXs ? 30 : isSm ? 48 : 64); + const [innerSize, setInnerSize] = useState(isXs ? 6 : isSm ? 8 : 10); + // useEffect(() => { + // if (!disabled && parseFloat(rotateRightDegree) + parseFloat(rotateLeftDegree) === 360) { + // const timeout = setTimeout(() => { + // setFontColor("white"); + // }, 2010); + // return () => clearTimeout(timeout); + // } + // }, [disabled, rotateRightDegree, rotateLeftDegree]); + + return ( +
+ + + + + + + + + + {data} + + +
+ ); + } +); + +export default CorrectRateCircle; diff --git a/src/app/analysis/atom/progressBar.tsx b/src/app/analysis/atom/progressBar.tsx new file mode 100644 index 0000000..ad50e8d --- /dev/null +++ b/src/app/analysis/atom/progressBar.tsx @@ -0,0 +1,31 @@ +import { Box } from "@mui/material"; + +const ProgressBar = ({ progress }: { progress: number }) => { + return ( + + + + ); +}; + +export default ProgressBar; diff --git a/src/app/analysis/molecule/recommendTest.tsx b/src/app/analysis/molecule/recommendTest.tsx new file mode 100644 index 0000000..61282a9 --- /dev/null +++ b/src/app/analysis/molecule/recommendTest.tsx @@ -0,0 +1,104 @@ +import { Box, Button, Typography } from "@mui/material"; + +const RecommendTest = () => { + return ( + + + + + 추천 모의고사 + + + 자비서가 취약한 문제들을 골라줘요! + + + + + + + + ); +}; + +export default RecommendTest; diff --git a/src/app/analysis/molecule/studyCurrentBox.tsx b/src/app/analysis/molecule/studyCurrentBox.tsx new file mode 100644 index 0000000..996f47f --- /dev/null +++ b/src/app/analysis/molecule/studyCurrentBox.tsx @@ -0,0 +1,94 @@ +import { Box, Typography } from "@mui/material"; +import CorrectRateCircle from "../atom/correctRateCircle"; + +interface StudyCurrentBoxProps { + title: string; + number: number; + progress: number; +} + +const StudyCurrentBox: React.FC = ({ title, number, progress }) => { + return ( + + + {title} + + + + + + 오늘의 학습 현황 + + + + {number} + + + 회 풀이 + + + + + + 오늘의 정답률 + + 50 ? "180deg" : (progress / 50) * 180 + "deg"} + rotateLeftDegree={progress > 50 ? ((progress - 50) / 50) * 180 + "deg" : "0deg"} + fillBorderColor={"var(--c-point)"} + emptyBorderColor={"var(--c-gray1)"} + innerBackgroundColor={"#fff"} + achive={false} + /> + + + + ); +}; + +export default StudyCurrentBox; diff --git a/src/app/analysis/molecule/vulnerableSubjectItem.tsx b/src/app/analysis/molecule/vulnerableSubjectItem.tsx new file mode 100644 index 0000000..e427861 --- /dev/null +++ b/src/app/analysis/molecule/vulnerableSubjectItem.tsx @@ -0,0 +1,159 @@ +import { Box, Button, Typography } from "@mui/material"; +import ProgressBar from "../atom/progressBar"; + +interface VulnerableSubjectItemProps { + item: VulnerableSubject; + index: number; +} + +const VulnerableSubjectItem: React.FC = ({ item, index }) => { + return ( + + + {index}. {item.subjectName} + + + + + + + + 취약률  + + + {item.vulnerableRate}% + + + + + + + + + ); +}; + +export default VulnerableSubjectItem; diff --git a/src/app/analysis/molecule/vulnerableSubjectsBox.tsx b/src/app/analysis/molecule/vulnerableSubjectsBox.tsx new file mode 100644 index 0000000..8ff66f4 --- /dev/null +++ b/src/app/analysis/molecule/vulnerableSubjectsBox.tsx @@ -0,0 +1,47 @@ +import { Box, Typography } from "@mui/material"; +import VulnerableSubjectItem from "./vulnerableSubjectItem"; + +interface VulnerableSubjectsBoxProps { + title: string; + userName: string; + vulnerableSubjects: VulnerableSubject[]; +} +const VulnerableSubjectsBox = ({ + title, + userName, + vulnerableSubjects, +}: VulnerableSubjectsBoxProps) => { + return ( + + + {userName}님의 {title} + + {vulnerableSubjects.map((item, index) => ( + + ))} + + ); +}; + +export default VulnerableSubjectsBox; diff --git a/src/app/analysis/molecule/vulnerableTagItem.tsx b/src/app/analysis/molecule/vulnerableTagItem.tsx new file mode 100644 index 0000000..3379405 --- /dev/null +++ b/src/app/analysis/molecule/vulnerableTagItem.tsx @@ -0,0 +1,111 @@ +import { Box, Button, Typography } from "@mui/material"; +import ProgressBar from "../atom/progressBar"; + +interface VulnerableTagItemProps { + item: VulnerableTag; + index: number; +} + +const VulnerableTagItem: React.FC = ({ item, index }) => { + return ( + + + {index}. {item.tagName} + + + + + + + + 취약률  + + + {item.vulnerableRate}% + + + + + + + ); +}; + +export default VulnerableTagItem; diff --git a/src/app/analysis/molecule/vulnerableTagsBox.tsx b/src/app/analysis/molecule/vulnerableTagsBox.tsx new file mode 100644 index 0000000..6d46854 --- /dev/null +++ b/src/app/analysis/molecule/vulnerableTagsBox.tsx @@ -0,0 +1,43 @@ +import { Box, Typography } from "@mui/material"; +import VulnerableTagItem from "./vulnerableTagItem"; + +interface VulnerableTagsBoxProps { + title: string; + userName: string; + VulnerableTags: VulnerableTag[]; +} +const VulnerableTagsBox = ({ title, userName, VulnerableTags }: VulnerableTagsBoxProps) => { + return ( + + + {userName}님의 {title} + + {VulnerableTags.map((item, index) => ( + + ))} + + ); +}; + +export default VulnerableTagsBox; diff --git a/src/app/analysis/organism/analysisReportOrganism.tsx b/src/app/analysis/organism/analysisReportOrganism.tsx new file mode 100644 index 0000000..2d674d6 --- /dev/null +++ b/src/app/analysis/organism/analysisReportOrganism.tsx @@ -0,0 +1,128 @@ +import { Box, CircularProgress, Typography } from "@mui/material"; +import VulnerableSubjectsBox from "../molecule/vulnerableSubjectsBox"; +import VulnerableTagsBox from "../molecule/vulnerableTagsBox"; +import { useEffect, useState } from "react"; +import useUserInfo from "@/src/hooks/useUserInfo"; +import { mainfetch } from "@/src/api/apis/mainFetch"; + +const AnalysisReportOrganism = () => { + const { userInfo, loading, error } = useUserInfo(); + const [isLoading, setIsLoading] = useState(true); + const [vulnerableSubjects, setVulnerableSubjects] = useState(); + const [vulnerableTags, setVulnerableTags] = useState(); + useEffect(() => { + const fetchData = async () => { + try { + const [subjectsResponse, tagsResponse] = await Promise.all([ + mainfetch("/analyses/vulnerable-subjects", { method: "GET" }, true), + mainfetch("/analyses/vulnerable-tags", { method: "GET" }, true), + ]); + + if (!subjectsResponse.ok || !tagsResponse.ok) { + throw new Error("데이터를 가져오는 데 실패했습니다."); + } + + const subjectsData = await subjectsResponse.json(); + const tagsData = await tagsResponse.json(); + + setVulnerableSubjects(subjectsData); + setVulnerableTags(tagsData); + } catch (err) { + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, []); + if (loading || isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + + 오류가 발생했습니다. 다시 시도해 주세요. + + + ); + } + return ( + + + + 분석 리포트 + + + 내가 푼 기록을 바탕으로 취약점을 분석해줘요 + + + + + + + + ); +}; + +export default AnalysisReportOrganism; diff --git a/src/app/analysis/organism/studyCurrentOrganism.tsx b/src/app/analysis/organism/studyCurrentOrganism.tsx new file mode 100644 index 0000000..fcbc464 --- /dev/null +++ b/src/app/analysis/organism/studyCurrentOrganism.tsx @@ -0,0 +1,138 @@ +import ArrawRight from "@/public/icons/arrow-right.svg"; +import { Box, Button, CircularProgress, Typography } from "@mui/material"; +import StudyCurrentBox from "../molecule/studyCurrentBox"; +import { useEffect, useState } from "react"; +import { mainfetch } from "@/src/api/apis/mainFetch"; +const StudyCurrentOrganism = () => { + const [analysisToday, setAnalysisToday] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const getAnalysisToday = async () => { + try { + const response = await mainfetch( + "/analyses/today", + { + method: "GET", + }, + true + ); + const data = await response.json(); + setAnalysisToday(data); + } finally { + setIsLoading(false); + } + }; + getAnalysisToday(); + }, []); + + if (isLoading) { + return ( + + + + ); + } + return ( + + + 학습 현황 + + + + + + + + + + ); +}; +export default StudyCurrentOrganism; diff --git a/src/app/analysis/page.tsx b/src/app/analysis/page.tsx new file mode 100644 index 0000000..155d8d5 --- /dev/null +++ b/src/app/analysis/page.tsx @@ -0,0 +1,32 @@ +"use client"; +import Appbar from "@/src/components/Appbar"; +import { MiddleBoxColumn } from "@/src/components/elements/styledElements"; +import Footer from "@/src/components/Footer"; +import { globalTheme } from "@/src/components/globalStyle"; +import { ThemeProvider } from "@mui/material"; +import UserAnalysisInfo from "./atom/UserAnalysisInfo"; +import RecommendTest from "./molecule/recommendTest"; +import AnalysisReportOrganism from "./organism/analysisReportOrganism"; +import StudyCurrentOrganism from "./organism/studyCurrentOrganism"; +import AnalysisMainTemplate from "./template/analysisMainTemplate"; + +const AnalyzeMainPage = () => { + return ( + + + + + + + + + + + +