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
72 changes: 72 additions & 0 deletions src/app/quiz/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import { useState } from "react";
import CategorySelection from "@/components/quiz/CategorySelection";
import QuizSession from "@/components/quiz/QuizSession";
import QuizResult from "@/components/quiz/QuizResult";
import type { QuizQuestion, QuizResult as QuizResultType } from "@/lib/quiz";
import type { CategoryType } from "@/components/ui/category/config";

type QuizStage = "category" | "quiz" | "result";

export default function QuizPage() {
const [stage, setStage] = useState<QuizStage>("category");
const [selectedCategory, setSelectedCategory] = useState<CategoryType | null>(
null
);
const [questions, setQuestions] = useState<QuizQuestion[]>([]);
const [result, setResult] = useState<QuizResultType | null>(null);

const handleCategorySelect = (
category: CategoryType,
quizQuestions: QuizQuestion[]
) => {
setSelectedCategory(category);
setQuestions(quizQuestions);
setStage("quiz");
};

const handleQuizComplete = (quizResult: QuizResultType) => {
setResult(quizResult);
setStage("result");
};

const handleRestart = () => {
setStage("category");
setSelectedCategory(null);
setQuestions([]);
setResult(null);
};

const handleRetry = (newQuestions: QuizQuestion[]) => {
setQuestions(newQuestions);
setStage("quiz");
};

return (
<main className="flex min-h-screen w-full flex-col items-center">
<div className="flex w-full max-w-[1040px] flex-col items-center gap-20 px-20 pt-20 pb-20">
{stage === "category" && (
<CategorySelection onCategorySelect={handleCategorySelect} />
)}

{stage === "quiz" && selectedCategory && (
<QuizSession
questions={questions}
category={selectedCategory}
onComplete={handleQuizComplete}
/>
)}

{stage === "result" && result && selectedCategory && (
<QuizResult
result={result}
onRestart={handleRestart}
onRetry={handleRetry}
category={selectedCategory}
/>
)}
</div>
</main>
);
}
8 changes: 5 additions & 3 deletions src/components/landing/CTASection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import { SearchIcon, ArrowRightIcon, LogoText } from "@/components/icons";
import GradientButton from "@/components/ui/buttons/GradientButton";

export function CTASection() {
const router = useRouter();
Expand All @@ -27,9 +28,10 @@ export function CTASection() {
transition={{ duration: 0.6, delay: 0.1 }}
className="mt-10"
>
<button
<GradientButton
onClick={() => router.push("/search")}
className="group inline-flex items-center gap-4 rounded-full bg-gradient-to-r from-violet-700 to-red-400 px-8 py-4 text-white transition-all hover:opacity-90"
rounded="full"
className="group inline-flex items-center gap-4"
>
<SearchIcon size={20} color="currentColor" />
<span>용어 검색</span>
Expand All @@ -38,7 +40,7 @@ export function CTASection() {
color="currentColor"
className="transition-transform group-hover:translate-x-1"
/>
</button>
</GradientButton>
</motion.div>
</div>
</div>
Expand Down
150 changes: 150 additions & 0 deletions src/components/quiz/CategorySelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"use client";

import { useState } from "react";
import { type CategoryType } from "@/components/ui/category/config";
import CategoryButton from "@/app/onboarding/components/categoruButton";
import { generateQuizQuestions, type QuizQuestion } from "@/lib/quiz";
import { ArrowRightIcon } from "@/components/icons/ic_arrow_right";
import { useToast } from "@/contexts/ToastContext";
import GradientButton from "@/components/ui/buttons/GradientButton";

interface CategorySelectionProps {
onCategorySelect: (category: CategoryType, questions: QuizQuestion[]) => void;
}

const row1Categories: CategoryType[] = [
"all",
"frontend",
"backend",
"uxui",
"ai",
];
const row2Categories: CategoryType[] = [
"cloud",
"data",
"security",
"devops",
"business",
];

export default function CategorySelection({
onCategorySelect,
}: CategorySelectionProps) {
const [selectedCategory, setSelectedCategory] = useState<CategoryType | null>(
null
);
const [questionCount, setQuestionCount] = useState<number>(10);
const [isLoading, setIsLoading] = useState(false);
const { showToast } = useToast();

const handleSelectCategory = (category: CategoryType) => {
setSelectedCategory(category);
};

const handleStartQuiz = async () => {
if (!selectedCategory) {
showToast("카테고리를 선택해주세요!", "error");
return;
}

setIsLoading(true);

try {
const questions = await generateQuizQuestions(
selectedCategory,
questionCount
);
onCategorySelect(selectedCategory, questions);
} catch (err) {
showToast(
err instanceof Error ? err.message : "퀴즈 생성에 실패했습니다.",
"error"
);
setIsLoading(false);
}
};

return (
<div className="flex w-full flex-col items-center gap-12">
{/* 헤더 */}
<div className="flex flex-col items-center gap-4">
<h1 className="text-4xl font-bold text-white">IT 용어 퀴즈</h1>
<p className="text-xl text-gray-500">
카테고리를 선택하고 퀴즈를 시작하세요!
</p>
</div>

{/* 카테고리 선택 */}
<div className="flex w-full flex-col items-center gap-6">
<h2 className="text-lg text-gray-700">카테고리 선택</h2>
<div className="flex flex-col gap-10">
{/* 첫 번째 줄 */}
<div className="flex justify-center gap-10">
{row1Categories.map((category) => (
<CategoryButton
key={category}
category={category}
isSelected={selectedCategory === category}
onClick={() => handleSelectCategory(category)}
/>
))}
</div>

{/* 두 번째 줄 */}
<div className="flex justify-center gap-10">
{row2Categories.map((category) => (
<CategoryButton
key={category}
category={category}
isSelected={selectedCategory === category}
onClick={() => handleSelectCategory(category)}
/>
))}
</div>
</div>
</div>

{/* 문제 수 선택 */}
<div className="flex flex-col items-center gap-6">
<h2 className="text-xl font-bold text-white">문제 수</h2>
<div className="flex gap-6">
{[5, 10, 15, 20].map((count) => (
<button
key={count}
onClick={() => setQuestionCount(count)}
className={`relative pb-2 text-lg font-semibold transition-all ${
questionCount === count
? "text-white"
: "text-gray-500 hover:text-gray-300"
}`}
>
{count}문제
{questionCount === count && (
<div className="absolute right-0 bottom-0 left-0 h-0.5 bg-linear-to-r from-violet-700 to-red-400" />
)}
</button>
))}
</div>
</div>

{/* 시작 버튼 */}
<GradientButton
onClick={handleStartQuiz}
disabled={isLoading}
rounded="full"
className="group inline-flex items-center gap-4"
>
<span className="font-bold">
{isLoading ? "퀴즈 생성 중..." : "퀴즈 시작하기"}
</span>
{!isLoading && (
<ArrowRightIcon
size={16}
color="currentColor"
className="transition-transform group-hover:translate-x-1"
/>
)}
</GradientButton>
</div>
);
}
Loading