Skip to content

Commit 9008481

Browse files
authored
Merge pull request #167 from GDSC-Hongik/feature/student-statistics
[Feature] 멘토 페이지 스터디 통계 조회 기능 생성
2 parents 402a744 + 3b6c513 commit 9008481

File tree

15 files changed

+1374
-5
lines changed

15 files changed

+1374
-5
lines changed

apps/admin/apis/study/studyApi.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
import type { AttendanceApiResponseDto } from "types/dtos/attendance";
1010
import type { CurriculumApiResponseDto } from "types/dtos/curriculumList";
1111
import type { StudyBasicInfoApiResponseDto } from "types/dtos/studyBasicInfo";
12-
import type { PaginatedStudyStudentResponseDto } from "types/dtos/studyStudent";
12+
import type { StudyStatisticsApiResponseDto } from "types/dtos/studyStatistics";
13+
import type {
14+
PaginatedStudyStudentResponseDto,
15+
StudyStudentApiResponseDto,
16+
} from "types/dtos/studyStudent";
1317
import type { PageableType } from "types/entities/page";
1418
import type { StudyAnnouncementType } from "types/entities/study";
1519

@@ -172,4 +176,13 @@ export const studyApi = {
172176

173177
return response.data;
174178
},
179+
getStudyStatistics: async (studyId: number) => {
180+
const response = await fetcher.get<StudyStatisticsApiResponseDto>(
181+
`/mentor/study-details/statistics?studyId=${studyId}`,
182+
{
183+
next: { tags: [tags.statistics] },
184+
}
185+
);
186+
return response.data;
187+
},
175188
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { css } from "@styled-system/css";
2+
import { Flex } from "@styled-system/jsx";
3+
import { Text } from "@wow-class/ui";
4+
import type { StudyWeekStatisticsApiResponseDto } from "types/dtos/studyStatistics";
5+
6+
import BarGraph from "./graph/BarGraph";
7+
8+
const StudyAnalyticsGraph = ({
9+
graphTitle,
10+
averageRate = 0,
11+
totalStudent,
12+
studyWeekStatisticsResponses,
13+
}: {
14+
graphTitle: string;
15+
averageRate?: number;
16+
totalStudent?: number;
17+
studyWeekStatisticsResponses?: StudyWeekStatisticsApiResponseDto[];
18+
}) => {
19+
return (
20+
<Flex direction="column" gap="md">
21+
<Text className={staticsTitleStyle} typo="h3">
22+
{graphTitle}
23+
</Text>
24+
<Flex direction="column" gap="xs">
25+
<Flex alignItems="flex-start" direction="column" gap="md">
26+
{studyWeekStatisticsResponses?.map((data) => (
27+
<Flex direction="row" gap="lg" key={data.week} minWidth="340px">
28+
<Text
29+
as="div"
30+
className={studyWeekStyle}
31+
color="sub"
32+
typo="body1"
33+
>
34+
{data.week}주차
35+
</Text>
36+
<BarGraph
37+
isCurriculumCanceled={data.isCurriculumCanceled}
38+
percent={data.attendanceRate}
39+
totalStudent={totalStudent}
40+
/>
41+
</Flex>
42+
))}
43+
<Flex direction="row" gap="lg">
44+
<Text className={studyWeekStyle} color="sub" typo="body1">
45+
평균
46+
</Text>
47+
<BarGraph
48+
barColor="average"
49+
isToolTipActive={false}
50+
percent={averageRate}
51+
/>
52+
</Flex>
53+
</Flex>
54+
</Flex>
55+
</Flex>
56+
);
57+
};
58+
59+
export default StudyAnalyticsGraph;
60+
61+
const studyWeekStyle = css({
62+
minWidth: "45px",
63+
});
64+
65+
const staticsTitleStyle = css({
66+
marginBottom: "10px",
67+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
import { Flex } from "@styled-system/jsx";
3+
import { Text, Tooltip } from "@wow-class/ui";
4+
import { Help } from "wowds-icons";
5+
6+
import CircleGraph from "./graph/CircleGraph";
7+
8+
const StudyCompleteRate = ({
9+
studyCompleteCount,
10+
studyCompleteRate,
11+
}: {
12+
studyCompleteCount?: number;
13+
studyCompleteRate?: number;
14+
}) => {
15+
return (
16+
<Flex direction="column" gap="md" minWidth="300px">
17+
<Flex alignItems="center" gap="xs" marginLeft="8px">
18+
<Text typo="h3">수료율</Text>
19+
<Tooltip
20+
content={
21+
<Text color="white" typo="body1">
22+
출석율과 과제제출률의 합이
23+
<br />
24+
70% 이상인 학생 비율
25+
</Text>
26+
}
27+
>
28+
<Help fill="textBlack" height={20} stroke="textBlack" width={20} />
29+
</Tooltip>
30+
</Flex>
31+
<Flex alignItems="center" direction="column" gap="lg">
32+
<CircleGraph percentage={studyCompleteRate} />
33+
<Text as="span" color="sub" typo="label1">
34+
수료 가능한 인원{" "}
35+
<Text as="span" color="primary">
36+
{studyCompleteCount}
37+
</Text>
38+
</Text>
39+
</Flex>
40+
</Flex>
41+
);
42+
};
43+
44+
export default StudyCompleteRate;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Flex } from "@styled-system/jsx";
2+
import { Space, Text } from "@wow-class/ui";
3+
import { studyApi } from "apis/study/studyApi";
4+
5+
import StudyAnalyticsGraph from "./StudyAnalyticsGraph";
6+
import StudyCompleteRate from "./StudyCompleteRate";
7+
8+
const StudyStatics = async ({ studyId }: { studyId: string }) => {
9+
const studyStatistics = await studyApi.getStudyStatistics(
10+
parseInt(studyId, 10)
11+
);
12+
13+
return (
14+
<section aria-label="study-statics">
15+
<Flex direction="column" gap="sm">
16+
<Text typo="h2">스터디 통계</Text>
17+
<Text color="sub" typo="label2">
18+
전체 {studyStatistics?.totalStudentCount}명, 수료 인원{" "}
19+
{studyStatistics?.completeStudentCount}
20+
</Text>
21+
</Flex>
22+
<Space height={33} />
23+
<Flex alignItems="flex-start" gap="xl">
24+
<StudyAnalyticsGraph
25+
averageRate={studyStatistics?.averageAttendanceRate}
26+
graphTitle="출석률"
27+
totalStudent={studyStatistics?.totalStudentCount}
28+
studyWeekStatisticsResponses={
29+
studyStatistics?.studyWeekStatisticsResponses
30+
}
31+
/>
32+
<StudyAnalyticsGraph
33+
averageRate={studyStatistics?.averageAssignmentSubmissionRate}
34+
graphTitle="과제 제출률"
35+
totalStudent={studyStatistics?.totalStudentCount}
36+
studyWeekStatisticsResponses={
37+
studyStatistics?.studyWeekStatisticsResponses
38+
}
39+
/>
40+
<StudyCompleteRate
41+
studyCompleteCount={studyStatistics?.completeStudentCount}
42+
studyCompleteRate={studyStatistics?.studyCompleteRate}
43+
/>
44+
</Flex>
45+
<Space height={33} />
46+
</section>
47+
);
48+
};
49+
50+
export default StudyStatics;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"use client";
2+
import { css, cva } from "@styled-system/css";
3+
import { Flex } from "@styled-system/jsx";
4+
import { Text } from "@wow-class/ui";
5+
import clsx from "clsx";
6+
7+
import GraphToolTip from "./GraphToolTip";
8+
9+
const BarGraph = ({
10+
barColor = "default",
11+
totalStudent = 0,
12+
percent = 0,
13+
isCurriculumCanceled,
14+
isToolTipActive = true,
15+
}: {
16+
barColor?: "default" | "average";
17+
totalStudent?: number;
18+
isToolTipActive?: boolean;
19+
percent?: number;
20+
isCurriculumCanceled?: boolean;
21+
}) => {
22+
return (
23+
<Flex
24+
alignItems="center"
25+
flexShrink="0"
26+
gap="sm"
27+
justifyContent="flex-start"
28+
>
29+
<div
30+
className={BarGraphBackgroundStyle({
31+
type: isCurriculumCanceled ? "canceled" : "default",
32+
})}
33+
>
34+
{percent > 0 ? (
35+
<div
36+
style={{ width: `${50 + (232 - 50) * (percent / 100)}px` }}
37+
className={clsx(
38+
barGraphStyle({
39+
type: barColor,
40+
}),
41+
isToolTipActive &&
42+
css({
43+
"&:hover .tooltip": {
44+
visibility: "visible !important",
45+
},
46+
})
47+
)}
48+
>
49+
<div className={barGraphInnerStyle}>
50+
<Text className={percentLabelStyle} color="white" typo="label2">
51+
{percent}%
52+
</Text>
53+
<GraphToolTip
54+
studentCount={Math.ceil((percent / 100) * totalStudent)}
55+
/>
56+
</div>
57+
</div>
58+
) : (
59+
<Text className={zeroPercentLabelStyle} color="sub" typo="label2">
60+
0%
61+
</Text>
62+
)}
63+
</div>
64+
{isCurriculumCanceled && (
65+
<Text as="div" color="sub" typo="label2">
66+
휴강
67+
</Text>
68+
)}
69+
</Flex>
70+
);
71+
};
72+
73+
export default BarGraph;
74+
75+
const BarGraphBackgroundStyle = cva({
76+
base: {
77+
position: "relative",
78+
minWidth: "232px",
79+
width: "232px",
80+
height: "32px",
81+
display: "flex",
82+
alignItems: "center",
83+
gap: "4px",
84+
flex: 2,
85+
zIndex: "5",
86+
borderRadius: "4px",
87+
},
88+
variants: {
89+
type: {
90+
default: {
91+
backgroundColor: "monoBackgroundPressed",
92+
},
93+
canceled: {
94+
backgroundColor: "lightDisabled",
95+
},
96+
},
97+
},
98+
});
99+
100+
const barGraphStyle = cva({
101+
base: {
102+
position: "absolute",
103+
zIndex: 10,
104+
top: 0,
105+
left: 0,
106+
height: "32px",
107+
padding: "4px 8px",
108+
display: "flex",
109+
alignItems: "center",
110+
borderRadius: "4px",
111+
},
112+
113+
variants: {
114+
type: {
115+
default: {
116+
backgroundColor: "primary",
117+
},
118+
average: {
119+
backgroundColor: "secondaryYellow",
120+
},
121+
},
122+
},
123+
});
124+
125+
const percentLabelStyle = css({
126+
height: "100%",
127+
display: "flex",
128+
alignItems: "center",
129+
});
130+
131+
const barGraphInnerStyle = css({
132+
position: "relative",
133+
width: "100%",
134+
height: "100%",
135+
});
136+
137+
const zeroPercentLabelStyle = css({
138+
marginLeft: "8px",
139+
});

0 commit comments

Comments
 (0)