성적 관리 시스템
선생님은 모든 학생의 과제 및 퀴즈 성적을 한눈에 볼 수 있는 종합 성적표를 확인할 수 있습니다.
개요
- 경로:
/c/[slug]/grades - 권한: Owner, Admin만 접근 가능
- 목적: 전체 학생의 성적을 표 형태로 통합 관리
- 형식: 학생별 행, 과제/퀴즈별 열로 구성된 매트릭스 테이블
주요 기능
1. 종합 성적표
테이블 구조
- 행 (Rows): 각 학생
- 열 (Columns):
- 학생 정보 (이름, 레벨, 포인트)
- 각 과제별 점수
- 각 퀴즈별 점수
- 통계 (과제 평균, 퀴즈 평균, 종합 평균)
학생 정보
- 프로필 사진
- 이름 (한국어 이름 우선)
- 레벨 뱃지
- 총 포인트
점수 셀
- 점수 / 만점 형식 (예: 85/100)
- 미제출: "-" 표시
- 색상 코딩:
- 90% 이상: 초록색 (우수)
- 80-89%: 파란색 (양호)
- 70-79%: 노란색 (보통)
- 70% 미만: 빨간색 (미흡)
- 미제출: 회색
통계 열
- 과제 평균: 제출한 과제의 평균 점수
- 퀴즈 평균: 시도한 퀴즈의 평균 점수
- 종합 평균: 전체 과제 및 퀴즈의 종합 평균
- 완료율: 완료한 과제/퀴즈 비율
2. 가로 스크롤
과제/퀴즈가 많을 경우 가로 스크롤 가능:
- 학생 정보 열은 고정 (sticky)
- 과제/퀴즈 열은 스크롤 가능
- 통계 열은 우측 고정 (sticky)
3. 정렬 기능
헤더 클릭으로 정렬:
- 이름순 (가나다순, A-Z)
- 레벨순 (높음 → 낮음)
- 포인트순 (높음 → 낮음)
- 각 과제/퀴즈 점수순
- 평균 점수순
4. 필터링
- 특정 과제/퀴즈만 표시
- 미제출 학생만 필터링
- 저성취 학생 필터링 (평균 70% 미만)
API 엔드포인트
GET /api/teacher/grades?community=slug
전체 학생의 성적 조회
Response:
{
"students": [
{
"memberId": 1,
"userId": 10,
"name": "김철수",
"avatar": "/avatars/user1.jpg",
"level": 5,
"points": 1250,
"assignmentScores": [
{
"assignmentId": 1,
"title": "한국어 작문 1",
"score": 85,
"maxScore": 100
},
{
"assignmentId": 2,
"title": "한국어 작문 2",
"score": null,
"maxScore": 100
}
],
"quizScores": [
{
"quizId": 1,
"title": "TOPIK 모의고사 1",
"score": 82,
"maxScore": 100
}
],
"stats": {
"completedAssignments": 1,
"totalAssignments": 2,
"assignmentAverage": 85,
"completedQuizzes": 1,
"totalQuizzes": 1,
"quizAverage": 82,
"overallAverage": 83.5
}
}
],
"assignments": [
{
"id": "1",
"title": "한국어 작문 1",
"maxScore": 100
}
],
"quizzes": [
{
"id": "1",
"title": "TOPIK 모의고사 1",
"maxScore": 100
}
]
}
데이터 계산 로직
과제 평균 계산
const completedAssignments = assignmentScores.filter((a) => a.score !== null).length;
const totalAssignmentScore = assignmentScores.reduce(
(sum, a) => sum + (a.score ?? 0),
0
);
const assignmentAverage = completedAssignments > 0
? Math.round(totalAssignmentScore / completedAssignments)
: 0;
퀴즈 평균 계산
const completedQuizzes = quizScores.filter((q) => q.score !== null).length;
const totalQuizScore = quizScores.reduce((sum, q) => sum + (q.score ?? 0), 0);
const quizAverage = completedQuizzes > 0
? Math.round(totalQuizScore / completedQuizzes)
: 0;
종합 평균 계산
const overallAverage = completedAssignments + completedQuizzes > 0
? Math.round(
((totalAssignmentScore + totalQuizScore) /
(completedAssignments + completedQuizzes)) *
100
) / 100
: 0;
점수 우선순위
과제 점수는 수동 점수 우선, AI 점수 차선:
const score = submission?.manualScore ?? submission?.score ?? null;
UI 컴포넌트
성적표 테이블
<Table>
<TableHeader>
<TableRow>
<TableHead className="sticky left-0">학생</TableHead>
{assignments.map(a => (
<TableHead key={a.id}>{a.title}</TableHead>
))}
{quizzes.map(q => (
<TableHead key={q.id}>{q.title}</TableHead>
))}
<TableHead className="sticky right-0">평균</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{students.map(student => (
<TableRow key={student.memberId}>
<TableCell className="sticky left-0">
<StudentInfo student={student} />
</TableCell>
{student.assignmentScores.map(score => (
<TableCell key={score.assignmentId}>
<ScoreCell score={score} />
</TableCell>
))}
{student.quizScores.map(score => (
<TableCell key={score.quizId}>
<ScoreCell score={score} />
</TableCell>
))}
<TableCell className="sticky right-0">
<Stats stats={student.stats} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
점수 셀 색상 코딩
const getScoreColor = (score: number | null, maxScore: number) => {
if (score === null) return "text-gray-400";
const percentage = (score / maxScore) * 100;
if (percentage >= 90) return "text-emerald-600 font-semibold";
if (percentage >= 80) return "text-blue-600";
if (percentage >= 70) return "text-amber-600";
return "text-red-600";
};
학생 정보 셀
<div className="flex items-center gap-3">
<Avatar src={student.avatar} className="w-8 h-8" />
<div>
<div className="font-medium">{student.name}</div>
<div className="text-xs text-muted-foreground">
Lv.{student.level} · {student.points}pts
</div>
</div>
</div>
색상 코딩 시스템
점수 색상
- 90% 이상:
text-emerald-600- 진한 초록색, 굵게 (우수) - 80-89%:
text-blue-600- 파란색 (양호) - 70-79%:
text-amber-600- 노란색 (보통) - 70% 미만:
text-red-600- 빨간색 (미흡) - 미제출:
text-gray-400- 회색, "-" 표시
배경 색상 (Optional)
- 선택한 행:
bg-primary/10 - 호버 시:
hover:bg-muted/50 - 고정 열:
bg-background
사용 시나리오
학기말 성적 산출
- 성적표 페이지 접속
- 전체 학생의 과제/퀴즈 점수 확인
- 종합 평균 열로 최종 성적 산출
- Excel 내보내기 (향후 기능)
- 학교 시스템에 입력
미제출 학생 파악
- 성적표에서 "-" 표시 확인
- 미제출 학생에게 독촉 메시지 발송
- 과제 마감 연장 여부 결정
저성취 학생 지원
- 빨간색/노란색 점수가 많은 학생 확인
- 개별 학생 진도 페이지로 이동
- 구체적인 학습 패턴 분석
- 보충 수업 또는 추가 자료 제공
수업 평가
- 특정 과제/퀴즈 열 확인
- 전체 학생의 평균 점수 계산
- 난이도 적정성 판단
- 다음 수업 계획에 반영
내보내기 기능 (향후 구현)
Excel 내보내기
const exportToExcel = () => {
const data = students.map(student => ({
이름: student.name,
레벨: student.level,
포인트: student.points,
...student.assignmentScores.reduce((acc, a) => ({
...acc,
[a.title]: a.score ?? "-"
}), {}),
...student.quizScores.reduce((acc, q) => ({
...acc,
[q.title]: q.score ?? "-"
}), {}),
"과제 평균": student.stats.assignmentAverage,
"퀴즈 평균": student.stats.quizAverage,
"종합 평균": student.stats.overallAverage,
}));
// Export using xlsx library
};
PDF 내보내기
const exportToPDF = () => {
// Generate PDF using jsPDF or similar library
};
권한 관리
Owner & Admin
- 모든 학생의 성적 조회 가능
- Excel/PDF 내보내기 가능
- 성적 정렬 및 필터링 가능
Student
- 성적표 페이지 접근 불가
- 본인 성적은 개인 대시보드에서 확인
성능 최적화
데이터 로딩
- 모든 데이터를 한 번에 로드 (학생 × 과제 × 퀴즈)
- 서버에서 모든 계산 완료 후 전송
- 클라이언트는 단순 렌더링만 수행
쿼리 최적화
// 필요한 데이터만 조회
const allAssignments = await db
.select({
id: assignments.id,
title: assignments.title,
maxScore: assignments.maxScore,
})
.from(assignments)
.where(eq(assignments.communityId, communityId));
// 모든 제출 데이터를 한 번에 조회 (N+1 문제 방지)
const allSubmissions = await db
.select({
memberId: assignmentSubmissions.memberId,
assignmentId: assignmentSubmissions.assignmentId,
score: assignmentSubmissions.score,
manualScore: assignmentSubmissions.manualScore,
})
.from(assignmentSubmissions)
.leftJoin(assignments, eq(assignmentSubmissions.assignmentId, assignments.id))
.where(eq(assignments.communityId, communityId));
메모리 최적화
- 학생별 점수를 메모리에서 매핑 (O(n) 탐색)
- 중복 쿼리 방지
- 필요한 컬럼만 선택
캐싱
- 성적표는 5분 캐시 권장
- 과제/퀴즈 채점 시 캐시 무효화
- React Query staleTime: 5분
확장 가능성
향후 추가 기능
- 📊 과제/퀴즈별 평균 점수 표시 (열 통계)
- 📈 학생별 성적 추이 그래프
- 📤 Excel/PDF 내보내기
- 📧 성적표 이메일 발송
- 🎯 성적 기반 자동 그룹 분류
- 📋 성적 코멘트 일괄 입력
- 🔍 고급 필터 (날짜 범위, 점수 범위)
- 📱 모바일 최적화 (카드 뷰)
- 🎨 커스텀 색상 코딩 설정
- 🏆 석차 표시
트러블슈팅
성적표가 로딩되지 않는 경우
- Owner/Admin 권한 확인
- 커뮤니티 멤버십 확인
- API 응답 확인 (개발자 도구)
- 페이지 새로고침
점수가 표시되지 않는 경우
- 실제로 제출/시도가 있는지 확인
- 채점이 완료되었는지 확인 (AI 또는 수동)
- 데이터베이스 연결 상태 확인
평균 점수가 이상한 경우
- 계산 로직 확인 (완료한 과제만 평균에 포함)
- 수동 점수 우선순위 확인
- null 값 처리 확인
테이블이 깨지는 경우
- 과제/퀴즈 수가 너무 많은 경우 가로 스크롤 확인
- 브라우저 창 크기 조정
- 모바일에서는 카드 뷰 사용 권장 (향후 구현)