diff --git a/src/apis/axios/intercepter.ts b/src/apis/axios/intercepter.ts index 6f33e3c..38c5c1a 100644 --- a/src/apis/axios/intercepter.ts +++ b/src/apis/axios/intercepter.ts @@ -1,5 +1,5 @@ -import axiosConfig from './instance'; -axiosConfig.interceptors.request.use(config => { +import api from './instance'; +api.interceptors.request.use(config => { //요청 성공 직전 호출 //헤더에 인가 토큰 부착 //로컬스토리지에 저장한다고 가정한다면 @@ -10,10 +10,14 @@ axiosConfig.interceptors.request.use(config => { return config; }); -axiosConfig.interceptors.response.use( +api.interceptors.response.use( //http status가 200번대인 경우 호출 - response => response, + response => { + console.log(response); + return response; + }, error => { + console.log(error); //http status가 에러 코드인경우 실행 } ); diff --git a/src/apis/usersApis.ts b/src/apis/usersApis.ts index 761a64a..c8efc61 100644 --- a/src/apis/usersApis.ts +++ b/src/apis/usersApis.ts @@ -1,3 +1,4 @@ +import Quiz from '@/types/Quiz'; import Experience from '../types/Experience'; import User from '../types/User'; import api from './axios/instance'; @@ -24,5 +25,22 @@ const usersApis = { const { id, experience } = params; await api.patch(`/users/${id}/experience`, { experience }); }, + getQuizzes: async (params: { + id: User['id']; + partId: Quiz['partId']; + }): Promise => { + const { id, partId } = params; + const response = await api.get(`/quizzes/users/${id}/incorrect`, { + params: { partId }, + }); + return response.data; + }, + patchPoint: async (params: { + id: User['id']; + point: number; + }): Promise => { + const { id, point } = params; + await api.patch(`/users/${id}/point`, { point }); + }, }; export default usersApis; diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index d23fed0..204df17 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -4,24 +4,29 @@ import { HeaderBox } from './style'; import Login from '@features/login/ui/Login'; import { ProfileWrapper, ProfileIcon, HeaderIcon } from '../ui/style'; import useModal from '@hooks/useModal'; +import useUserStore from '@/store/useUserStore'; export default function Header() { const points: number = 2999999999; const lifePoints: number = 5; const { isShow, openModal, closeModal, Modal } = useModal(); - + const { user } = useUserStore(); return ( - - + {user && ( + <> + + + + )} diff --git a/src/features/login/styles.ts b/src/features/login/styles.ts index 64b2815..060f7f8 100644 --- a/src/features/login/styles.ts +++ b/src/features/login/styles.ts @@ -78,3 +78,32 @@ export const SocialLoginButton = styled.button` height: 24px; } `; +export const LoginPromptSection = styled.section` + animation: ${fadeInScaleUp} 0.7s ease-out; + display: flex; + flex-direction: column; + align-items: center; + width: 306px; + height: 370.47px; + border-radius: 40px; + background: #bfd683; + box-shadow: 0 10.53px #85705f; + > h2 { + margin-top: 20px; + color: #85705f; + } +`; +export const LoginPromptImg = styled.img` + width: 187.944px; + height: 219.219px; + margin-top: 7px; +`; + +export const GoToLoginButton = styled.button` + width: 145.444px; + height: 38.291px; + border-radius: 6px; + background: #000; + color: #bfd683; + font-size: 14px; +`; diff --git a/src/features/login/ui/LoginPrompt.tsx b/src/features/login/ui/LoginPrompt.tsx new file mode 100644 index 0000000..c043256 --- /dev/null +++ b/src/features/login/ui/LoginPrompt.tsx @@ -0,0 +1,25 @@ +import { + DashLineHr, + FlexContainer, + GoToLoginButton, + LoginPromptSection, + LoginPromptImg, +} from '../styles'; +import { getImageUrl } from '@/utils/getImageUrl'; +interface GoToLoginProps { + onNext: () => void; +} +export default function LoginPrompt({ onNext }: GoToLoginProps) { + return ( + <> + + +

문제 더 풀려면 로그인해 !

+ + + 로그인 창으로 +
+
+ + ); +} diff --git a/src/features/quiz/styles.ts b/src/features/quiz/styles.ts index 798c8d8..6769d82 100644 --- a/src/features/quiz/styles.ts +++ b/src/features/quiz/styles.ts @@ -239,7 +239,7 @@ const fadeInScaleUp = keyframes` transform: translate(-50%, -50%) scale(1); } `; -export const TotalResultSection = styled.section` +export const CompensationSection = styled.section` animation: ${fadeInScaleUp} 0.7s ease-out; position: fixed; display: flex; @@ -377,7 +377,7 @@ export const SectionWrapper = styled.div` `; // 섹션 제목(이름) -export const SectionTitle = styled.p` +export const SectionTitle = styled.h4` width: 693px; font-size: 17px; color: #ffffff; diff --git a/src/features/quiz/ui/PartClear.tsx b/src/features/quiz/ui/PartClear.tsx new file mode 100644 index 0000000..86615e0 --- /dev/null +++ b/src/features/quiz/ui/PartClear.tsx @@ -0,0 +1,33 @@ +import { useNavigate } from 'react-router-dom'; +import { CompensationSection } from '../styles'; +import { pointQuery } from '@queries/usersQuery'; +import { useTimeout } from '@modern-kit/react'; +import useUserStore from '@store/useUserStore'; +export default function PartClear() { + const navigate = useNavigate(); + const { mutate: updatePoint } = pointQuery.patch(); + const { user } = useUserStore(); + const point = 1500; + useTimeout( + () => { + if (user) { + updatePoint({ id: user.id, point }); + } + }, + { delay: 500 } + ); + return ( + <> + +

와 파트클리어 축하해

+ +
+ + ); +} diff --git a/src/features/quiz/ui/Result.tsx b/src/features/quiz/ui/Result.tsx index 1cfebe5..1838dfe 100644 --- a/src/features/quiz/ui/Result.tsx +++ b/src/features/quiz/ui/Result.tsx @@ -2,27 +2,21 @@ import { progressQuery } from '../../../queries/usersQuery'; import { useClientQuizStore } from '../../../store/useClientQuizStore'; import useUserStore from '../../../store/useUserStore'; import Quiz from '../../../types/Quiz'; -import handlePage from '../../../utils/handlePage'; -import { noop } from '@modern-kit/utils'; import { AnswerDiv, NextPageButton, ScoreSection } from '../styles'; +import { getImageUrl } from '@utils/getImageUrl'; interface ResultProps { quizId: Quiz['id']; answer: Quiz['answer']; result: boolean; - lastPage: number; closeModal: () => void; - openModal: () => void; } export default function Result({ quizId, answer, result, - lastPage, closeModal, - openModal, }: ResultProps) { - const imgUrl = import.meta.env.VITE_IMG_BASE_URL; const { nextPage, resetUserResponseAnswer, pushTotalResults, currentPage } = useClientQuizStore(); //임시 유저 가져오기 @@ -33,18 +27,15 @@ export default function Result({ return ( <> {!result && '정답 : ' + answer} { resetUserResponseAnswer(); - pushTotalResults(result); closeModal(); - handlePage(currentPage, lastPage, nextPage, noop, openModal); + nextPage(); userId && addProgress.mutate({ userId, diff --git a/src/features/quiz/ui/TotalResults.tsx b/src/features/quiz/ui/TotalResults.tsx index b0e4b85..0af3a36 100644 --- a/src/features/quiz/ui/TotalResults.tsx +++ b/src/features/quiz/ui/TotalResults.tsx @@ -1,33 +1,37 @@ import { experienceQuery } from '@queries/usersQuery'; import { useClientQuizStore } from '@store/useClientQuizStore'; -import useUserStore from '@store/useUserStore'; -import type Quiz from '@/types/Quiz'; -import User from '@/types/User'; import { DashLineHr, ImageDescriptionDiv, Img, RedirectToLearnButton, TotalResultProgressDiv, - TotalResultSection, + CompensationSection, TotalResultsRewardDiv, TotalResultsTextDiv, } from '../styles'; import { getImageUrl } from '@utils/getImageUrl'; import { useTimeout } from '@modern-kit/react'; import { useNavigate } from 'react-router-dom'; -import ProgressBar from '@/features/progress/ui/ProgressBar'; -interface TotalResultsProps { - quizzes: Quiz[]; - totalResults: boolean[]; +import ProgressBar from '@features/progress/ui/ProgressBar'; +import useUserStore from '@store/useUserStore'; +import User from '@type/User'; +interface TotalResultProps { + onNext: () => void; + quizzesLength: number; } -export default function TotalResults({ totalResults }: TotalResultsProps) { - const totalResultCount = totalResults.filter(result => result).length; +export default function TotalResults({ + onNext, + quizzesLength, +}: TotalResultProps) { + const { totalResults } = useClientQuizStore(); + const quizCorrectAnswers = totalResults.filter(result => result).length; + const isPartClear = quizzesLength === quizCorrectAnswers; const { user } = useUserStore() as { user: User }; - const experience = totalResultCount * 10; + const experience = quizCorrectAnswers * 10; const { mutate: experienceUpdate, isIdle } = experienceQuery.patch(); - const { data: userExperience, isSuccess } = experienceQuery.get(user.id); - const { reset } = useClientQuizStore(); + const { data: userExperience, isSuccess } = experienceQuery.get(user?.id); + const navigate = useNavigate(); useTimeout( () => { @@ -36,66 +40,62 @@ export default function TotalResults({ totalResults }: TotalResultsProps) { { delay: 1000, enabled: isSuccess } ); if (!userExperience) { - return
404...
; + return <>; } - return ( - <> - - - 총

  {totalResultCount} 

- 문제를 맞혔고

 {experience} 경험치

를 얻었어! -
- - - - 레벨업 이미지 -

Level.{userExperience.level}

-
- - 반짝이 -
-

경험치

- -
- 반짝이 + + 총

  {quizCorrectAnswers} 

+ 문제를 맞혔고

 {experience} 경험치

를 얻었어! +
+ + + + 레벨업 이미지 +

Level.{userExperience.level}

+
+ + 반짝이 +
+

경험치

+ - - - - { - reset(); - navigate('/learn'); - }} - > - 메인으로 - - - +
+ 반짝이 +
+
+ + { + isPartClear ? onNext() : navigate('/learn'); + }} + > + 메인으로 + + ); } diff --git a/src/hooks/useFunnel.tsx b/src/hooks/useFunnel.tsx new file mode 100644 index 0000000..b598cb3 --- /dev/null +++ b/src/hooks/useFunnel.tsx @@ -0,0 +1,77 @@ +import { ReactNode, isValidElement, FC, useState } from 'react'; +//Step 컴포넌트의 프롭스 타입 +//name과 children을 받는다 +type StepProps = { + name: string; + children: ReactNode; +}; + +//Funnel 컴포넌트의 Props 타입 +//ReactNode [] 타입을 받는다. +interface FunnelProps { + children: ReactNode[]; +} + +//FunnelComponent FC 타입을 상속받음으로써 +// 함수형 컴포넌트임을 알리고 props를 children: ReactNode[]타입 으로 지정 +interface FunnelComponent extends FC { + Step: FC; +} + +type UseFunnelReturn = { + setStep: (step: T) => void; + Funnel: FunnelComponent; +}; +/** + * `useFunnel` 훅 + * + * 이 훅은 상태를 기반으로 여러 단계(스텝) 중 하나만 활성화 상태로 렌더링하는 로직을 제공합니다. + * + * @template T + * @param {T} defaultStep - 초기 활성화 스텝. + * @returns {UseFunnelReturn} 현재 활성화된 스텝을 설정하거나 렌더링하는 로직 제공. + * @example + const { Funnel, setStep } = useFunnel('로그인 유도'); + + + + + + + + + */ +const useFunnel = (defaultStep: T): UseFunnelReturn => { + const [step, setStep] = useState(defaultStep); + + /** + * `Step` 컴포넌트 + * + * 각 스텝의 내용을 정의하는 컴포넌트입니다. + * + * @param {StepProps} props - 스텝의 이름과 내용을 받습니다. + * @returns {ReactNode} 스텝의 내용을 렌더링합니다. + */ + const Step: FC = ({ children }) => { + return children; + }; + + /** + * `Funnel` 컴포넌트 + * + * 현재 활성화된 스텝만 렌더링합니다. + * + * @param {FunnelProps} props - `Step` 컴포넌트 배열을 받습니다. + * @returns {ReactNode | null} 현재 활성화된 스텝의 내용을 렌더링하거나 없으면 null 반환. + */ + const Funnel: FunnelComponent = ({ children }) => { + const currentStep = children.find( + (child: ReactNode) => isValidElement(child) && child.props.name === step + ); + return currentStep || null; + }; + + Funnel.Step = Step; + return { setStep, Funnel }; +}; +export default useFunnel; diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts index 83e1105..925115c 100644 --- a/src/hooks/useModal.ts +++ b/src/hooks/useModal.ts @@ -9,8 +9,11 @@ import Modal from '../common/layout/Modal'; *