Skip to content

Commit

Permalink
feat: quiz record (#310)
Browse files Browse the repository at this point in the history
* feat: calendar

* feat: quiz record

* fix: name

* feat: record detail 1

* refactor: 폴더 이동

* refactor: 폴더 구조

* fix: 주석

* fix: format error

* fix: cspell

* fix: build error

* fix: build error

* fix: format
  • Loading branch information
rabyeoljji authored Dec 16, 2024
1 parent 2c395d2 commit dd3b455
Show file tree
Hide file tree
Showing 36 changed files with 888 additions and 41 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"svgs",
"swipeable",
"taglib",
"taglibs",
"Tanstack",
"Uncapitalize",
"vaul",
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"emoji-picker-react": "^4.9.4",
"firebase": "^11.0.2",
"framer-motion": "^11.2.6",
Expand All @@ -61,6 +62,7 @@
"qs": "^6.12.1",
"react": "^18",
"react-canvas-confetti": "^2.0.7",
"react-day-picker": "^8.10.1",
"react-div-100vh": "^0.7.0",
"react-dom": "^18",
"react-hook-form": "^7.52.0",
Expand Down
7 changes: 5 additions & 2 deletions src/app/(routes)/main/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,18 @@ const Home = async () => {
</div>

{/* 연속으로 푸는 중 */}
<button className="mt-[16px] flex h-fit w-full items-center gap-[20px] rounded-[20px] bg-background-base-01 px-[24px] py-[19px]">
<Link
href={'/record'}
className="mt-[16px] flex h-fit w-full items-center gap-[20px] rounded-[20px] bg-background-base-01 px-[24px] py-[19px]"
>
<Icon name="calendar" className="size-[40px] p-[4px]" />
<div className="flex flex-col items-start gap-[4px]">
<Text typography="title3">{25}</Text>
<Text typography="text2-medium" color="sub">
연속으로 푸는 중
</Text>
</div>
</button>
</Link>
</div>

{/* 복습 필수 노트 TOP5 */}
Expand Down
12 changes: 12 additions & 0 deletions src/app/(routes)/record/(detail)/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FunctionComponent, PropsWithChildren } from 'react'
import type { Metadata } from 'next'

export const metadata: Metadata = {}

interface InnerLayoutProps extends PropsWithChildren {}

const Layout: FunctionComponent<InnerLayoutProps> = ({ children }) => {
return <main>{children}</main>
}

export default Layout
112 changes: 112 additions & 0 deletions src/app/(routes)/record/(detail)/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import QuizCard from '@/features/quiz/components/quiz-card'
import { getQuizDetailRecord } from '@/requests/quiz/server'
import Icon from '@/shared/components/custom/icon'
import Text from '@/shared/components/ui/text'
import { msToElapsedTimeKorean } from '@/shared/utils/time'

interface Props {
params: {
id: string
}
searchParams: {
type: Quiz.Set.Type
name: string
quizCount: number
score: number
}
}

const RecordDetailPage = async ({ params, searchParams }: Props) => {
const date = params.id.split('_')[0]
const quizSetId = params.id
.split('_')
.filter((value) => value !== date)
.join('_')
const quizSetType = searchParams.type
const quizSetName = searchParams.name
const quizCount = searchParams.quizCount
const correctRate = Math.round((searchParams.score / searchParams.quizCount) * 100)

const { totalElapsedTimeMs, quizzes } = await getQuizDetailRecord({ quizSetId, quizSetType })

return (
<main className="flex h-[calc(100dvh-54px)] flex-col overflow-y-auto">
<div className="flex-center flex-col gap-[40px] px-[16px] pb-[33px] pt-[20px]">
<div className="flex-center flex-col gap-[7px]">
<Text typography="title2">{quizSetName}</Text>
<Text typography="text2-medium" color="secondary">
{date}
</Text>
</div>

<div className="flex">
<div className="flex-center flex-col px-[30px]">
<div className="flex-center mb-[6px] size-[40px]">
<Icon name="speech-bubble-color" className="w-[36px]" />
</div>
<Text typography="text2-medium" color="sub" className="mb-[2px]">
문제 수
</Text>
<Text typography="subtitle2-bold">{quizCount}문제</Text>
</div>
<div className="flex-center flex-col border-x border-border-divider px-[30px]">
<div className="flex-center mb-[6px] size-[40px]">
<Icon name="timer-color" className="w-[30px]" />
</div>
<Text typography="text2-medium" color="sub" className="mb-[2px]">
소요시간
</Text>
<Text typography="subtitle2-bold">{msToElapsedTimeKorean(totalElapsedTimeMs)}</Text>
</div>
<div className="flex-center flex-col px-[30px]">
<div className="flex-center mb-[6px] size-[40px]">
<Icon name="correct-check-round" className="size-[34px]" />
</div>
<Text typography="text2-medium" color="sub" className="mb-[2px]">
정답률
</Text>
<Text typography="subtitle2-bold">{correctRate}%</Text>
</div>
</div>
</div>

<div className="flex-center h-fit flex-col gap-[12px] bg-[var(--color-gray-50)] px-[16px] py-[20px]">
{quizzes.map((quiz, index) => (
<QuizCard
key={quiz.id + '-idx:' + index}
answerMode
userAnswer={quiz.choseAnswer}
header={
<div className="flex items-center justify-between pr-[6px] text-icon-tertiary">
{quiz.answer === quiz.choseAnswer ? (
<Text typography="text1-bold" color="right">
정답
</Text>
) : (
<Text typography="text1-bold" color="critical">
오답
</Text>
)}

{quiz.quizSetType === 'COLLECTION_QUIZ_SET' ? (
<Text typography="text2-medium" color="caption">
{quiz.collectionName}
</Text>
) : (
<Text typography="text2-medium" color="caption">
{quiz.directoryName}
{'>'}
{quiz.documentName}
</Text>
)}
</div>
}
quiz={quiz}
/>
))}
</div>
</main>
)
}

export default RecordDetailPage
12 changes: 12 additions & 0 deletions src/app/(routes)/record/(detail)/all/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FunctionComponent, PropsWithChildren } from 'react'
import type { Metadata } from 'next'

export const metadata: Metadata = {}

interface InnerLayoutProps extends PropsWithChildren {}

const Layout: FunctionComponent<InnerLayoutProps> = ({ children }) => {
return <main>{children}</main>
}

export default Layout
11 changes: 11 additions & 0 deletions src/app/(routes)/record/(detail)/all/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AllRecordDetail from '@/features/record/screen/all-record-detail'

const AllRecordPage = () => {
return (
<main className="flex h-[calc(100dvh-54px)] flex-col gap-[32px] overflow-y-auto px-[16px] py-[24px]">
<AllRecordDetail />
</main>
)
}

export default AllRecordPage
26 changes: 26 additions & 0 deletions src/app/(routes)/record/@header/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'

import GoBackButton from '@/shared/components/custom/go-back-button'
import Text from '@/shared/components/ui/text'
import { useParams, usePathname } from 'next/navigation'

const Header = () => {
const pathname = usePathname()
const params = useParams()

const isDetailPage = params.id ? true : false
const isAllPage = pathname === '/record/all'

const headerText = isDetailPage ? '퀴즈 상세' : isAllPage ? '퀴즈 기록 전체' : '퀴즈 기록'

return (
<header className="relative flex h-[54px] w-full items-center bg-background-base-01 px-[16px]">
<GoBackButton />
<Text typography="subtitle2-medium" className="center">
{headerText}
</Text>
</header>
)
}

export default Header
19 changes: 19 additions & 0 deletions src/app/(routes)/record/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FunctionComponent, PropsWithChildren } from 'react'
import type { Metadata } from 'next'

export const metadata: Metadata = {}

interface LayoutProps extends PropsWithChildren {
header: React.ReactNode
}

const Layout: FunctionComponent<LayoutProps> = ({ children, header }) => {
return (
<>
{header}
{children}
</>
)
}

export default Layout
73 changes: 73 additions & 0 deletions src/app/(routes)/record/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import CustomCalendar from '@/features/record/calendar'
import RecordItem from '@/features/record/components/record-item'
import { getQuizRecords } from '@/requests/quiz/server'
import Icon from '@/shared/components/custom/icon'
import { Button } from '@/shared/components/ui/button'
import Text from '@/shared/components/ui/text'
import { formatDateKorean, getFormattedDate } from '@/shared/utils/date'
import Link from 'next/link'

interface Props {
searchParams: {
selectedDate: string
}
}

const RecordPage = async ({ searchParams }: Props) => {
const today = new Date()
const selectedDate = searchParams.selectedDate ?? getFormattedDate(today)
const { currentConsecutiveDays, maxConsecutiveDays, quizRecords } = await getQuizRecords()

// 데이터가 많아지면 다른 방법으로 처리해보는 것을 고민
// 백엔드 측에 특정 solvedDate를 api 요청 시 보내면 일치하는 데이터를 보내주는 방법 제안해봐도 좋을 듯
const dateRecord = quizRecords.find((quizInfo) => quizInfo.solvedDate === selectedDate)

return (
<main className="h-[calc(100dvh-54px)] overflow-y-auto px-[16px]">
<div className="flex-center flex-col gap-[8px] border-b border-border-divider pb-[20px] pt-[10px]">
<Text typography="title3">
<Text as={'span'} color="accent">
{currentConsecutiveDays}
</Text>
일 연속으로 푸는 중
</Text>

<Text typography="text1-medium" color="caption">
최장 연속일: {maxConsecutiveDays}
</Text>
</div>

<CustomCalendar className="mt-[3px]" />

<div className="flex flex-col">
<Text typography="text2-medium" color="sub" className="my-[8px]">
{formatDateKorean(selectedDate, { month: true, day: true })}
</Text>
{dateRecord?.quizRecords.map((record, index) => (
<RecordItem
key={record.quizSetId + '-' + index}
type={record.quizSetType}
name={record.name}
quizCount={record.quizCount}
score={record.score}
date={dateRecord.solvedDate}
quizSetId={record.quizSetId}
/>
))}
</div>

<Link href={'/record/all' + `?selectedDate=${selectedDate}`} className="size-fit">
<Button
variant={'smallSquare'}
colors={'tertiary'}
className="mb-[27px] mt-[8px] h-fit w-full py-[13.5px]"
>
기록 전체보기
<Icon name="chevron-right" className="size-[16px] text-icon-tertiary" />
</Button>
</Link>
</main>
)
}

export default RecordPage
Loading

0 comments on commit dd3b455

Please sign in to comment.