-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: 마크다운 관련 패키지 설치 및 설정 * chore: 새로운 테이블로부터 Supabase 타입 생성 * feat: 퀴즈 상세 데이터 가져오기 * feat: choice-form 작성 * feat: choice-form mutate 작성 * chore: zod 설치 * feat: 제출 처리하는 라우트 핸들러 작성 * chore: Supabase 제네릭 타입 전역적으로 설정 * feat: 제출 처리 화면 로직 작성 * feat: LoadingSpinner 컴포넌트 작성 * chore: Supabase 데이터베이스 타입 생성 * feat: 풀이 성공시 disalbed 처리 * refactor: answerChoice의 여러 eq를 하나의 match로 통합 * fix: getQuiz 함수에 조인 multiple choice 문제 해결 * style: errorMessage 조건부 렌더링에 && 사용 * chore: 마크다운 영역에만 non-reset css 적용 * chore: 데이터베이스 타입 업데이트 * style: 데이터 패칭 병렬 처리 * refactor: join 쿼리 수정
- Loading branch information
1 parent
7bbe2ec
commit 93f9645
Showing
12 changed files
with
6,449 additions
and
3,174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { cookies } from 'next/headers'; | ||
import { createClient } from '@/utils/supabase/server'; | ||
import { NextRequest } from 'next/server'; | ||
import { z } from 'zod'; | ||
|
||
export async function POST(request: NextRequest) { | ||
const cookieStore = cookies(); | ||
const supabase = createClient(cookieStore); | ||
|
||
const validateBody = await request.json().then((body) => | ||
z | ||
.object({ | ||
quizId: z.number(), | ||
choiceId: z.number(), | ||
}) | ||
.safeParse(body) | ||
); | ||
|
||
if (!validateBody.success) { | ||
return Response.json( | ||
{ error: '필드가 올바르지 않습니다.' }, | ||
{ status: 400 } | ||
); | ||
} | ||
|
||
const { quizId, choiceId } = validateBody.data; | ||
|
||
const { | ||
data: { user }, | ||
} = await supabase.auth.getUser(); | ||
|
||
if (!user) { | ||
return Response.json({ error: '로그인이 필요합니다.' }, { status: 401 }); | ||
} | ||
|
||
const { data: answerChoice } = await supabase | ||
.from('choices') | ||
.select(`*`) | ||
.match({ quiz_id: quizId, answer: true }) | ||
.single(); | ||
|
||
await supabase.from('quizsubmissions').upsert({ | ||
user_id: user?.id, | ||
quiz_id: quizId, | ||
success: choiceId === answerChoice?.id, | ||
updated_at: new Date().toISOString(), | ||
}); | ||
|
||
if (choiceId !== answerChoice?.id) { | ||
return Response.json( | ||
{ error: '틀렸어요! 다시 고민해주세요.' }, | ||
{ status: 400 } | ||
); | ||
} | ||
|
||
return Response.json({ data: '정답!' }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
'use client'; | ||
|
||
import React, { useState } from 'react'; | ||
import { useGetChoicesOfQuiz, postQuizSubmission } from '@/hooks/quiz'; | ||
import { useMutation } from '@tanstack/react-query'; | ||
import { useRouter } from 'next/navigation'; | ||
import Button from '@/components/common/buttons/button'; | ||
import LoadingSpinner from '@/components/common/loading-spinner/loading-spinner'; | ||
|
||
export default function ChoiceForm({ quizId }: { quizId: number }) { | ||
const { data: choices } = useGetChoicesOfQuiz(quizId); | ||
const [errorMessage, setErrorMessage] = useState(''); | ||
const router = useRouter(); | ||
|
||
const { mutate, isPending, isSuccess } = useMutation({ | ||
mutationFn: postQuizSubmission, | ||
onSuccess: () => router.push(`/quizzes/${quizId}/answer`), | ||
onError: (error) => setErrorMessage(error.message), | ||
}); | ||
|
||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||
e.preventDefault(); | ||
|
||
const formData = new FormData(e.currentTarget); | ||
|
||
const choice = choices?.find( | ||
(choice) => choice.id === Number(formData.get('choice')) | ||
); | ||
|
||
if (!choice) { | ||
setErrorMessage('선택지를 선택해주세요.'); | ||
return; | ||
} | ||
|
||
mutate({ | ||
quizId, | ||
choiceId: choice.id, | ||
}); | ||
}; | ||
|
||
return ( | ||
<form onSubmit={handleSubmit}> | ||
{choices?.map((choice) => ( | ||
<label className="flex gap-2" key={choice.id} htmlFor={`${choice.id}`}> | ||
<input | ||
id={`${choice.id}`} | ||
value={`${choice.id}`} | ||
type="radio" | ||
name="choice" | ||
onChange={() => setErrorMessage('')} | ||
/> | ||
<p className="my-1.5">{choice.description}</p> | ||
</label> | ||
))} | ||
{errorMessage && <p className="my-2 text-red-500">{errorMessage}</p>} | ||
<Button | ||
className="mt-4 flex h-10 w-full items-center justify-center disabled:bg-blue-500" | ||
disabled={isPending || isSuccess} | ||
> | ||
{isSuccess ? ( | ||
'성공!' | ||
) : isPending ? ( | ||
<LoadingSpinner size="lg" weight="sm" /> | ||
) : ( | ||
'제출하기' | ||
)} | ||
</Button> | ||
</form> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { cva, type VariantProps } from 'class-variance-authority'; | ||
import { forwardRef, type Ref } from 'react'; | ||
import { twMerge } from 'tailwind-merge'; | ||
|
||
type LoadingSpinnerProps = React.HTMLAttributes<HTMLElement> & | ||
VariantProps<typeof loadingSpinner>; | ||
|
||
const loadingSpinner = cva( | ||
[ | ||
'animate-spin rounded-full border-solid border-current border-t-transparent', | ||
], | ||
{ | ||
variants: { | ||
size: { | ||
sm: 'h-2 w-2', | ||
md: 'h-4 w-4', | ||
lg: 'h-6 w-6', | ||
xl: 'h-8 w-8', | ||
'2xl': 'h-12 w-12', | ||
'3xl': 'h-16 w-16', | ||
}, | ||
weight: { | ||
sm: 'border-2', | ||
md: 'border-4', | ||
lg: 'border-8', | ||
}, | ||
}, | ||
defaultVariants: { size: 'md', weight: 'sm' }, | ||
} | ||
); | ||
|
||
export default forwardRef(function LoadingSpinner( | ||
{ size, weight, className, ...props }: LoadingSpinnerProps, | ||
forwardedRef: Ref<HTMLDivElement> | ||
) { | ||
return ( | ||
<div | ||
ref={forwardedRef} | ||
className={twMerge(loadingSpinner({ size, weight, className }))} | ||
{...props} | ||
/> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.