Skip to content

Commit 83e6f25

Browse files
committed
✨ (#363) 퀴즈 상세 통계 데이터 연결
1 parent 60b3df3 commit 83e6f25

File tree

8 files changed

+175
-100
lines changed

8 files changed

+175
-100
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import axios from "axios";
2+
import { axiosInstance } from "@/api/axiosInstance";
3+
import { ENDPOINTS } from "@/constants/endpoints";
4+
import { ApiResponse } from "@/types/apiResponseTypes";
5+
import { fetchQuizDetailStatResult } from "@/types/quizzes/fetchQuizDetailStatTypes";
6+
7+
export async function fetchQuizDetailStat(lectureId: string) {
8+
try {
9+
const response = await axiosInstance.get<
10+
ApiResponse<fetchQuizDetailStatResult>
11+
>(ENDPOINTS.QUIZZES.GET_DETAIL_STAT(lectureId));
12+
return response.data;
13+
} catch (error: unknown) {
14+
if (axios.isAxiosError(error) && error.response) {
15+
return error.response.data as ApiResponse<fetchQuizDetailStatResult>;
16+
}
17+
throw error;
18+
}
19+
}

frontend/app/layout.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,40 @@
11
import "./globals.scss";
2+
import type { Metadata } from "next";
3+
4+
export const metadata: Metadata = {
5+
title: "ClassLog",
6+
manifest: "./manifest.webmanifest",
7+
themeColor: "#ffffff",
8+
appleWebApp: {
9+
capable: true,
10+
title: "ClassLog",
11+
statusBarStyle: "default",
12+
},
13+
icons: {
14+
apple: [
15+
{
16+
url: "/favicon/apple-touch-icon.png",
17+
sizes: "180x180",
18+
type: "image/png",
19+
},
20+
],
21+
icon: [
22+
{
23+
url: "/favicon/favicon-96x96.png",
24+
sizes: "96x96",
25+
type: "image/png",
26+
},
27+
{
28+
url: "/favicon/favicon.svg",
29+
type: "image/svg+xml",
30+
},
31+
{
32+
url: "/favicon/favicon.ico",
33+
type: "image/x-icon",
34+
},
35+
],
36+
},
37+
};
238

339
export default function RootLayout({
440
children,
@@ -7,25 +43,6 @@ export default function RootLayout({
743
}>) {
844
return (
945
<html lang="ko">
10-
<head>
11-
<link rel="manifest" href="./manifest.webmanifest" />
12-
<title>ClassLog</title>
13-
<meta name="theme-color" content="#ffffff" />
14-
<meta name="apple-mobile-web-app-title" content="ClassLog" />
15-
<link
16-
rel="apple-touch-icon"
17-
href="/favicon/apple-touch-icon.png"
18-
sizes="180x180"
19-
/>
20-
<link
21-
rel="icon"
22-
type="image/png"
23-
href="/favicon/favicon-96x96.png"
24-
sizes="96x96"
25-
/>
26-
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
27-
<link rel="shortcut icon" href="/favicon/favicon.ico" />
28-
</head>
2946
{children}
3047
</html>
3148
);

frontend/app/teacher/quiz-dashboard/[lectureId]/_components/StatisticsContainer/AverageCorrectRate/AverageCorrectRate.tsx

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React from "react";
2+
import React, { useEffect, useState } from "react";
33
import { PieChart, Pie, Cell } from "recharts";
44
import styles from "./AverageCorrectRate.module.scss";
55

@@ -14,29 +14,40 @@ export default function AverageCorrectRate({
1414
averageCorrectRate,
1515
totalQuizCount,
1616
}: AverageCorrectRateProps) {
17+
const [mounted, setMounted] = useState(false);
18+
19+
useEffect(() => {
20+
setMounted(true);
21+
}, []);
22+
1723
const data = [
1824
{ name: "정답률", value: averageCorrectRate },
1925
{ name: "오답률", value: 100 - averageCorrectRate },
2026
];
27+
2128
return (
2229
<div className={styles.chartCard}>
2330
<div className={styles.pieWrapper}>
24-
<PieChart width={100} height={100}>
25-
<Pie
26-
data={data}
27-
cx="50%"
28-
cy="50%"
29-
innerRadius={35}
30-
outerRadius={48}
31-
startAngle={90}
32-
endAngle={-270}
33-
dataKey="value"
34-
>
35-
{data.map((entry, idx) => (
36-
<Cell key={`cell-${idx}`} fill={COLORS[idx % COLORS.length]} />
37-
))}
38-
</Pie>
39-
</PieChart>
31+
{mounted ? (
32+
<PieChart width={100} height={100}>
33+
<Pie
34+
data={data}
35+
cx="50%"
36+
cy="50%"
37+
innerRadius={35}
38+
outerRadius={48}
39+
startAngle={90}
40+
endAngle={-270}
41+
dataKey="value"
42+
>
43+
{data.map((entry, idx) => (
44+
<Cell key={`cell-${idx}`} fill={COLORS[idx % COLORS.length]} />
45+
))}
46+
</Pie>
47+
</PieChart>
48+
) : (
49+
<div style={{ width: 100, height: 100 }} />
50+
)}
4051
<div className={styles.pieCenterText}>{averageCorrectRate}%</div>
4152
</div>
4253
<div className={styles.averageCorrectRateTextBox}>

frontend/app/teacher/quiz-dashboard/[lectureId]/_components/StatisticsContainer/QuizDetailChart/QuizDetailChart.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
"use client";
2-
import React from "react";
2+
import React, { useEffect, useState } from "react";
33
import { PieChart, Pie, Cell, Legend } from "recharts";
44
import styles from "./QuizDetailChart.module.scss";
5-
6-
// 타입 정의 예시
7-
export type QuizType = "multipleChoice" | "shortAnswer" | "trueFalse";
8-
export interface Quiz {
9-
quizId: string;
10-
quizOrder: number;
11-
type: QuizType;
12-
[key: string]: unknown;
13-
}
5+
import {
6+
MultipleChoiceQuizDetail,
7+
QuizDetailStat,
8+
ShortAnswerQuizDetail,
9+
TrueFalseQuizDetail,
10+
} from "@/types/quizzes/fetchQuizDetailStatTypes";
1411

1512
// 색상 팔레트
1613
const COLORS = ["#6C5CE7", "#4F8CFF", "#6AD1C9", "#B983FF"];
1714
const OX_COLORS = ["#6AD1C9", "#4F8CFF"];
1815

1916
// MultipleChoiceChart: 파이차트
20-
function MultipleChoiceChart({ data }: { data: Quiz }) {
17+
function MultipleChoiceChart({ data }: { data: MultipleChoiceQuizDetail }) {
2118
const chartData = [
2219
{ name: "1번", value: Number(data["1"] ?? 0) },
2320
{ name: "2번", value: Number(data["2"] ?? 0) },
@@ -58,7 +55,7 @@ function MultipleChoiceChart({ data }: { data: Quiz }) {
5855
}
5956

6057
// TrueFalseChart: OX 파이차트
61-
function TrueFalseChart({ data }: { data: Quiz }) {
58+
function TrueFalseChart({ data }: { data: TrueFalseQuizDetail }) {
6259
const chartData = [
6360
{ name: "O", value: Number(data.O ?? 0) },
6461
{ name: "X", value: Number(data.X ?? 0) },
@@ -100,7 +97,7 @@ function TrueFalseChart({ data }: { data: Quiz }) {
10097
}
10198

10299
// ShortAnswerTop3: 리스트
103-
function ShortAnswerTop3({ data }: { data: Quiz }) {
100+
function ShortAnswerTop3({ data }: { data: ShortAnswerQuizDetail }) {
104101
const top3 = data.top3Answers as { answer: string; rate: number }[];
105102
const etcAnswers = data.etcAnswers as string[] | undefined;
106103
return (
@@ -135,7 +132,21 @@ function ShortAnswerTop3({ data }: { data: Quiz }) {
135132
}
136133

137134
// 실제 QuizDetailChart 컴포넌트
138-
export default function QuizDetailChart({ quiz }: { quiz: Quiz }) {
135+
export default function QuizDetailChart({ quiz }: { quiz: QuizDetailStat }) {
136+
const [mounted, setMounted] = useState(false);
137+
138+
useEffect(() => {
139+
setMounted(true);
140+
}, []);
141+
142+
if (!mounted) {
143+
return (
144+
<div className={styles.chartCard}>
145+
<div className={styles.chartTitle}>퀴즈{quiz.quizOrder}</div>
146+
</div>
147+
);
148+
}
149+
139150
if (quiz.type === "multipleChoice") {
140151
return <MultipleChoiceChart data={quiz} />;
141152
}
Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
"use client";
2-
import React from "react";
2+
3+
import React, { useEffect, useState } from "react";
34
import QuizCorrectRates from "./QuizCorrectRates/QuizCorrectRates";
45
import AverageCorrectRate from "./AverageCorrectRate/AverageCorrectRate";
56
import QuizDetailChart from "./QuizDetailChart/QuizDetailChart";
67
import styles from "./StatisticsContainer.module.scss";
78
import Masonry from "react-masonry-css";
9+
import {
10+
fetchQuizDetailStatResult,
11+
QuizDetailStat,
12+
} from "@/types/quizzes/fetchQuizDetailStatTypes";
13+
import { useParams } from "next/navigation";
14+
import { fetchQuizDetailStat } from "@/api/quizzes/fetchQuizDetailStat";
815

916
interface StatData {
1017
averageCorrectRate: number;
@@ -23,46 +30,20 @@ export default function StatisticsContainer({
2330
statData,
2431
}: StatisticsContainerProps) {
2532
// 두 번째 데이터: 퀴즈별 분포/상세용
26-
const detailData = [
27-
{
28-
quizId: "qz-001",
29-
quizOrder: 1,
30-
type: "multipleChoice",
31-
1: 70.0,
32-
2: 20.0,
33-
3: 0.0,
34-
4: 10.0,
35-
},
36-
{
37-
quizId: "qz-002",
38-
quizOrder: 2,
39-
type: "trueFalse",
40-
O: 80.0,
41-
X: 20.0,
42-
},
43-
{
44-
quizId: "qz-003",
45-
quizOrder: 3,
46-
type: "shortAnswer",
47-
top3Answers: [
48-
{ answer: "배깅", rate: 50.0 },
49-
{ answer: "부스팅", rate: 20.0 },
50-
{ answer: "스태킹", rate: 10.0 },
51-
],
52-
etcAnswers: ["Voting", "랜덤포레스트"],
53-
},
54-
{
55-
quizId: "qz-004",
56-
quizOrder: 4,
57-
type: "shortAnswer",
58-
top3Answers: [
59-
{ answer: "복원 추출", rate: 30.0 },
60-
{ answer: "순차 샘플링", rate: 20.0 },
61-
{ answer: "K-켭 교차 검증", rate: 10.0 },
62-
],
63-
etcAnswers: ["부스트랩 샘플링", "계층 샘플링", "단순 샘플링"],
64-
},
65-
];
33+
const [detailData, setDetailData] =
34+
useState<fetchQuizDetailStatResult | null>(null);
35+
const { lectureId } = useParams<{ lectureId: string }>();
36+
37+
useEffect(() => {
38+
if (!lectureId) return;
39+
fetchQuizDetailStat(lectureId).then((res) => {
40+
if (res.isSuccess && res.result) {
41+
setDetailData(res.result);
42+
} else {
43+
setDetailData(null);
44+
}
45+
});
46+
}, [lectureId]);
6647

6748
return (
6849
<Masonry
@@ -75,15 +56,13 @@ export default function StatisticsContainer({
7556
totalQuizCount={statData.totalQuizCount}
7657
/>
7758
<QuizCorrectRates quizList={statData.quizList} />
78-
{detailData.map((quiz) => (
79-
<QuizDetailChart
80-
key={quiz.quizId}
81-
quiz={{
82-
...quiz,
83-
type: quiz.type as "multipleChoice" | "trueFalse" | "shortAnswer",
84-
}}
85-
/>
86-
))}
59+
{detailData && detailData.length > 0 ? (
60+
detailData.map((quiz: QuizDetailStat) => (
61+
<QuizDetailChart key={quiz.quizId} quiz={quiz} />
62+
))
63+
) : (
64+
<div className={styles.noData}>퀴즈 상세 통계 데이터가 없습니다.</div>
65+
)}
8766
</Masonry>
8867
);
8968
}

frontend/app/teacher/quiz-dashboard/[lectureId]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use client";
2+
23
import BackButtonHeader from "./_components/BackButtonHeader/BackButtonHeader";
34
import DashboardContainer from "./_components/DashboardContainer/DashboardContainer";
45
import style from "./page.module.scss";

frontend/constants/endpoints.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export const ENDPOINTS = {
105105

106106
// 퀴즈 관련
107107
QUIZZES: {
108+
GET_DETAIL_STAT: (lectureId: string) =>
109+
`${BASE_API}/quizzes/${lectureId}/result/statistics`,
108110
GET_SUBMIT_LIST: (lectureId: string) =>
109111
`${BASE_API}/quizzes/${lectureId}/result/list`,
110112
GET_INFO: (lectureId: string) =>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export type fetchQuizDetailStatResult = QuizDetailStat[];
2+
3+
export type QuizDetailStat =
4+
| MultipleChoiceQuizDetail
5+
| TrueFalseQuizDetail
6+
| ShortAnswerQuizDetail;
7+
8+
export interface MultipleChoiceQuizDetail {
9+
quizId: string;
10+
quizOrder: number;
11+
type: "multipleChoice";
12+
"1": number;
13+
"2": number;
14+
"3": number;
15+
"4": number;
16+
}
17+
18+
export interface TrueFalseQuizDetail {
19+
quizId: string;
20+
quizOrder: number;
21+
type: "trueFalse";
22+
O: number;
23+
X: number;
24+
}
25+
26+
export interface ShortAnswerQuizDetail {
27+
quizId: string;
28+
quizOrder: number;
29+
type: "shortAnswer";
30+
top3Answers: Array<{
31+
answer: string;
32+
rate: number;
33+
}>;
34+
etcAnswers: string[];
35+
}

0 commit comments

Comments
 (0)