From 57aa431d5acd422261cae8842e24bd6c814332ff Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Tue, 19 Nov 2024 22:26:12 +0900 Subject: [PATCH 01/12] =?UTF-8?q?FEAT:=20=EC=9E=90=EB=8F=99=EC=B1=84?= =?UTF-8?q?=EC=A0=90=20=EA=B8=B0=EB=8A=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 답안을 자동 채점하고 검증하는 gradeExamResult 메소드 구현 - 총점 계산하는 verifyTotalScore 메소드 구현 --- .../epari/exam/service/GradingService.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/main/java/com/example/epari/exam/service/GradingService.java diff --git a/src/main/java/com/example/epari/exam/service/GradingService.java b/src/main/java/com/example/epari/exam/service/GradingService.java new file mode 100644 index 00000000..070bb3e5 --- /dev/null +++ b/src/main/java/com/example/epari/exam/service/GradingService.java @@ -0,0 +1,53 @@ +package com.example.epari.exam.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.epari.exam.domain.ExamQuestion; +import com.example.epari.exam.domain.ExamResult; +import com.example.epari.exam.domain.ExamScore; +import com.example.epari.exam.repository.ExamResultRepository; +import com.example.epari.global.common.enums.ExamStatus; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class GradingService { + + private final ExamResultRepository examResultRepository; + + public void gradeExamResult(ExamResult examResult) { + if (examResult.getStatus() != ExamStatus.SUBMITTED) { + throw new IllegalStateException("제출된 시험만 채점할 수 있습니다."); + } + + int totalScore = 0; + + for (ExamScore score : examResult.getScores()) { + ExamQuestion question = score.getQuestion(); + String studentAnswer = score.getStudentAnswer(); + + // 답안 채점 + boolean isCorrect = question.validateAnswer(studentAnswer); + int earnedScore = isCorrect ? question.getScore() : 0; + score.updateScore(earnedScore); + + totalScore += earnedScore; + } + + examResult.updateStatus(ExamStatus.GRADED); + examResultRepository.save(examResult); + } + + @Transactional(readOnly = true) + public boolean verifyTotalScore(ExamResult examResult) { + int calculatedTotal = examResult.getScores().stream() + .mapToInt(ExamScore::getEarnedScore) + .sum(); + + return calculatedTotal == examResult.getEarnedScore(); + } + +} From 6278abe4b6576c97e70491d5a3f7c535fda37df1 Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 10:50:53 +0900 Subject: [PATCH 02/12] =?UTF-8?q?FEAT:=20=EB=8B=B5=EC=95=88=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ExamQuestion의 정답 채점 메서드 추가 - MultipleChoiceQuestion에서 학생의 객관식 답안이 유효한 선택지 범위에 포함되는지 및 정답 여부를 검증 - SubjectiveQuestion에서 학생의 주관식 답안 비교 시 대소문자를 무시하고, 앞뒤 공백을 제거하도록 처리 --- .../epari/exam/domain/ExamQuestion.java | 23 +++++++++++++++---- .../exam/domain/MultipleChoiceQuestion.java | 18 +++++++++++++-- .../epari/exam/domain/SubjectiveQuestion.java | 9 +++++--- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/epari/exam/domain/ExamQuestion.java b/src/main/java/com/example/epari/exam/domain/ExamQuestion.java index 60df6478..d020a861 100644 --- a/src/main/java/com/example/epari/exam/domain/ExamQuestion.java +++ b/src/main/java/com/example/epari/exam/domain/ExamQuestion.java @@ -53,7 +53,7 @@ public abstract class ExamQuestion extends BaseTimeEntity { @Column(nullable = false) private ExamQuestionType type; - @Column(name = "correct_answer", nullable = false) // 단일 정답 필드 + @Column(name = "correct_answer", nullable = false) private String correctAnswer; @Embedded @@ -73,9 +73,6 @@ protected ExamQuestion(String questionText, int examNumber, int score, this.correctAnswer = correctAnswer; } - // 정답 검증을 위한 추상 메소드 - public abstract boolean validateAnswer(String studentAnswer); - public void setImage(QuestionImage image) { this.image = image; } @@ -84,6 +81,24 @@ void setExam(Exam exam) { this.exam = exam; } + /** + * 학생의 답안을 채점하여 맞았는지 여부를 반환 + * @param studentAnswer 학생이 제출한 답안 + * @return 정답 여부 + */ + public final boolean validateAnswer(String studentAnswer) { + if (studentAnswer == null || studentAnswer.trim().isEmpty()) { + return false; + } + return doValidateAnswer(studentAnswer.trim()); + } + + /** + * 실제 답안 검증 로직을 구현하는 추상 메서드 + * 각 문제 타입별로 구체적인 검증 방식을 구현 + */ + protected abstract boolean doValidateAnswer(String studentAnswer); + public void updateExamNumber(int newNumber) { this.examNumber = newNumber; } diff --git a/src/main/java/com/example/epari/exam/domain/MultipleChoiceQuestion.java b/src/main/java/com/example/epari/exam/domain/MultipleChoiceQuestion.java index 7c0d5f1d..0c4c8732 100644 --- a/src/main/java/com/example/epari/exam/domain/MultipleChoiceQuestion.java +++ b/src/main/java/com/example/epari/exam/domain/MultipleChoiceQuestion.java @@ -36,8 +36,22 @@ private MultipleChoiceQuestion(String questionText, int examNumber, int score, } @Override - public boolean validateAnswer(String studentAnswer) { - return getCorrectAnswer().equals(studentAnswer); + protected boolean doValidateAnswer(String studentAnswer) { + try { + // 선택지 번호가 숫자인지 확인 + int selectedNumber = Integer.parseInt(studentAnswer); + + // 유효한 선택지 범위인지 확인 + if (selectedNumber < 1 || selectedNumber > choices.size()) { + return false; + } + + // 정답과 일치하는지 확인 + return studentAnswer.equals(getCorrectAnswer()); + + } catch (NumberFormatException e) { + return false; + } } public void addChoice(Choice choice) { diff --git a/src/main/java/com/example/epari/exam/domain/SubjectiveQuestion.java b/src/main/java/com/example/epari/exam/domain/SubjectiveQuestion.java index e37f2298..709c1c9a 100644 --- a/src/main/java/com/example/epari/exam/domain/SubjectiveQuestion.java +++ b/src/main/java/com/example/epari/exam/domain/SubjectiveQuestion.java @@ -22,9 +22,12 @@ private SubjectiveQuestion(String questionText, int examNumber, int score, } @Override - public boolean validateAnswer(String studentAnswer) { - // 주관식은 좀 더 유연한 검증이 필요할 수 있음 - return getCorrectAnswer().trim().equalsIgnoreCase(studentAnswer.trim()); + protected boolean doValidateAnswer(String studentAnswer) { + // 대소문자 무시, 앞뒤 공백 제거 후 비교 + String normalizedCorrectAnswer = getCorrectAnswer().trim().toLowerCase(); + String normalizedStudentAnswer = studentAnswer.trim().toLowerCase(); + + return normalizedCorrectAnswer.equals(normalizedStudentAnswer); } } From 18afeaa5a04f53c8904b8ce7726f827c68f7d2a2 Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 11:04:24 +0900 Subject: [PATCH 03/12] =?UTF-8?q?FEAT:=20ExamResult=20=EB=B0=8F=20ExamScor?= =?UTF-8?q?e=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채점 결과 로직 개선 - ExamResult에 totalScore 필드 추가 - 상태 관리 강화 (IN_PROGRESS -> SUBMITTED -> GRADED) - 통계 메서드 추가 (정답률 등) - 검증 로직 강화 - 채점 결과 업데이트 로직 추가 - ExamScore에 불필요한 필드 제거 및 로직 개선 - feedback 필드 제거 - 임시 저장 상태 관리 개선 - 점수 업데이트 로직 개선 - 정답 여부 확인 메서드 추가 --- .../example/epari/exam/domain/ExamResult.java | 77 +++++++++++++++---- .../example/epari/exam/domain/ExamScore.java | 24 ++++-- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/example/epari/exam/domain/ExamResult.java b/src/main/java/com/example/epari/exam/domain/ExamResult.java index cc67a527..2e720e32 100644 --- a/src/main/java/com/example/epari/exam/domain/ExamResult.java +++ b/src/main/java/com/example/epari/exam/domain/ExamResult.java @@ -52,6 +52,9 @@ public class ExamResult extends BaseTimeEntity { @Column(nullable = false) private LocalDateTime submitTime; + @Column(nullable = false) + private Integer totalScore = 0; + @Enumerated(EnumType.STRING) @Column(nullable = false) private ExamStatus status; @@ -74,12 +77,19 @@ public void updateStatus(ExamStatus status) { this.status = status; } + /** + * 답안 추가 + */ public void addScore(ExamScore score) { validateScoreAddable(); scores.add(score); score.setExamResult(this); } + /** + * 답안 제출 처리 + * @param force 강제 제출 여부 (시험 시간 초과 등의 경우) + */ public void submit(boolean force) { if (!force) { validateSubmittable(); @@ -89,6 +99,29 @@ public void submit(boolean force) { scores.forEach(ExamScore::markAsSubmitted); } + /** + * 임시저장 답안 여부 확인 + */ + public boolean hasTemporaryAnswer(Long questionId) { + return scores.stream() + .anyMatch(score -> + score.getQuestion().getId().equals(questionId) && + score.isTemporary() + ); + } + + /** + * 채점 결과 반영 + */ + public void updateScore() { + if (status != ExamStatus.SUBMITTED) { + throw new BusinessBaseException(ErrorCode.EXAM_NOT_SUBMITTED); + } + + this.totalScore = calculateTotalScore(); + this.status = ExamStatus.GRADED; + } + public int getEarnedScore() { return scores.stream() .filter(score -> !score.isTemporary()) @@ -96,6 +129,36 @@ public int getEarnedScore() { .sum(); } + /** + * 제출된 문제 수 조회 + */ + public int getSubmittedQuestionCount() { + return (int)scores.stream() + .filter(score -> !score.isTemporary()) + .count(); + } + + /** + * 정답률 계산 + */ + public double getCorrectAnswerRate() { + if (scores.isEmpty()) { + return 0.0; + } + + long correctCount = scores.stream() + .filter(score -> score.getEarnedScore() > 0) + .count(); + + return (double)correctCount / scores.size() * 100; + } + + private int calculateTotalScore() { + return scores.stream() + .mapToInt(ExamScore::getEarnedScore) + .sum(); + } + private void validateExam(Exam exam) { if (exam == null) { throw new BusinessBaseException(ErrorCode.EXAM_NOT_FOUND); @@ -134,18 +197,4 @@ private void validateAllQuestionsAnswered() { } } - public int getSubmittedQuestionCount() { - return (int)scores.stream() - .filter(score -> !score.isTemporary()) - .count(); - } - - public boolean hasTemporaryAnswer(Long questionId) { - return scores.stream() - .anyMatch(score -> - score.getQuestion().getId().equals(questionId) && - score.isTemporary() - ); - } - } diff --git a/src/main/java/com/example/epari/exam/domain/ExamScore.java b/src/main/java/com/example/epari/exam/domain/ExamScore.java index 9734a65f..2c51808a 100644 --- a/src/main/java/com/example/epari/exam/domain/ExamScore.java +++ b/src/main/java/com/example/epari/exam/domain/ExamScore.java @@ -19,7 +19,6 @@ /** * 개별 문제에 대한 학생의 답안과 채점 결과를 관리하는 엔티티 */ - @Entity @Table(name = "exam_scores") @Getter @@ -47,8 +46,6 @@ public class ExamScore extends BaseTimeEntity { @Column(nullable = false) private boolean temporary; - private String feedback; - @Builder public ExamScore(ExamResult examResult, ExamQuestion question, String studentAnswer, boolean temporary) { this.examResult = examResult; @@ -62,21 +59,32 @@ void setExamResult(ExamResult examResult) { this.examResult = examResult; } - public void updateScore(int earnedScore, String feedback) { - this.earnedScore = earnedScore; - this.feedback = feedback; - } - + /** + * 답안 업데이트 + */ public void updateAnswer(String answer) { this.studentAnswer = answer; } + /** + * 임시저장 상태 해제 (최종 제출) + */ public void markAsSubmitted() { this.temporary = false; } + /** + * 채점 결과 업데이트 + */ public void updateScore(int score) { this.earnedScore = score; } + /** + * 정답 여부 확인 + */ + public boolean isCorrect() { + return earnedScore > 0; + } + } From f6002a9a5096bfa50716bf5577cacdc08d830fca Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 11:08:31 +0900 Subject: [PATCH 04/12] =?UTF-8?q?FEAT:=20GradingService=EC=97=90=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=20=EC=B1=84=EC=A0=90=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제출 여부 확인 및 각 문제별 답안 채점 기능 구현 - 총점 계산 및 시험 검토 상태 업데이트 기능 구현 - 평균 점수 계산 및 총점에서 최고/최저 점수 조회 기능 구현 --- .../epari/exam/service/GradingService.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/main/java/com/example/epari/exam/service/GradingService.java b/src/main/java/com/example/epari/exam/service/GradingService.java index 070bb3e5..e7237834 100644 --- a/src/main/java/com/example/epari/exam/service/GradingService.java +++ b/src/main/java/com/example/epari/exam/service/GradingService.java @@ -1,5 +1,7 @@ package com.example.epari.exam.service; +import java.util.List; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -8,7 +10,10 @@ import com.example.epari.exam.domain.ExamScore; import com.example.epari.exam.repository.ExamResultRepository; import com.example.epari.global.common.enums.ExamStatus; +import com.example.epari.global.exception.BusinessBaseException; +import com.example.epari.global.exception.ErrorCode; +import lombok.Getter; import lombok.RequiredArgsConstructor; @Service @@ -50,4 +55,92 @@ public boolean verifyTotalScore(ExamResult examResult) { return calculatedTotal == examResult.getEarnedScore(); } + /** + * 개별 시험 결과 채점 + */ + public void gradeExamResult(Long examResultId) { + ExamResult examResult = examResultRepository.findById(examResultId) + .orElseThrow(() -> new BusinessBaseException(ErrorCode.EXAM_RESULT_NOT_FOUND)); + + if (examResult.getStatus() != ExamStatus.SUBMITTED) { + throw new BusinessBaseException(ErrorCode.EXAM_NOT_SUBMITTED); + } + + // 각 문제별 채점 + for (ExamScore score : examResult.getScores()) { + int earnedScore = gradeAnswer(score); + score.updateScore(earnedScore); + } + + // 채점 결과 반영 + examResult.updateScore(); + examResultRepository.save(examResult); + + log.info("Exam graded - resultId: {}, totalScore: {}", examResultId, examResult.getTotalScore()); + } + + /** + * 평균 점수 계산 + */ + @Transactional(readOnly = true) + public double calculateAverageScore(Long examId) { + List gradedResults = examResultRepository.findByExamIdAndStatus(examId, ExamStatus.GRADED); + + if (gradedResults.isEmpty()) { + return 0.0; + } + + int totalScore = gradedResults.stream() + .mapToInt(ExamResult::getTotalScore) + .sum(); + + return (double)totalScore / gradedResults.size(); + } + + /** + * 총점에서 최고/최저 점수 정보 조회 + */ + @Transactional(readOnly = true) + public ScoreStatistics calculateScoreStatistics(Long examId) { + List gradedResults = examResultRepository.findByExamIdAndStatus(examId, ExamStatus.GRADED); + + if (gradedResults.isEmpty()) { + return new ScoreStatistics(0, 0); + } + + int maxScore = gradedResults.stream() + .mapToInt(ExamResult::getTotalScore) + .max() + .orElse(0); + + int minScore = gradedResults.stream() + .mapToInt(ExamResult::getTotalScore) + .min() + .orElse(0); + + return new ScoreStatistics(maxScore, minScore); + } + + private int gradeAnswer(ExamScore score) { + String studentAnswer = score.getStudentAnswer(); + if (score.getQuestion().validateAnswer(studentAnswer)) { + return score.getQuestion().getScore(); + } + return 0; + } + + @Getter + public static class ScoreStatistics { + + private final int maxScore; + + private final int minScore; + + public ScoreStatistics(int maxScore, int minScore) { + this.maxScore = maxScore; + this.minScore = minScore; + } + + } + } From ae72baceb1812a4242fd7e12aebc313f7e798c55 Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 11:11:45 +0900 Subject: [PATCH 05/12] =?UTF-8?q?FEAT:=20=EC=B1=84=EC=A0=90=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GradingController에 시험 결과 채점 요청, 평균 점수 조회 최고/최저 점수 통계 조회 엔드포인트 구현 --- .../exam/controller/GradingController.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/main/java/com/example/epari/exam/controller/GradingController.java diff --git a/src/main/java/com/example/epari/exam/controller/GradingController.java b/src/main/java/com/example/epari/exam/controller/GradingController.java new file mode 100644 index 00000000..fb9502d7 --- /dev/null +++ b/src/main/java/com/example/epari/exam/controller/GradingController.java @@ -0,0 +1,88 @@ +package com.example.epari.exam.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.epari.exam.service.GradingService; +import com.example.epari.exam.service.GradingService.ScoreStatistics; +import com.example.epari.global.annotation.CurrentUserEmail; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 시험 채점 및 성적 통계 관련 요청을 처리하는 Controller + */ +@Slf4j +@RestController +@RequestMapping("/api/courses/{courseId}/exams/{examId}/grades") +@RequiredArgsConstructor +public class GradingController { + + private final GradingService gradingService; + + /** + * 시험 결과 채점 요청 + * @param courseId 강의 ID + * @param examId 시험 ID + * @param resultId 채점할 시험 결과 ID + * @param instructorEmail 강사 이메일 + * @return 채점 완료 응답 + */ + @PostMapping("/results/{resultId}") + @PreAuthorize("hasRole('INSTRUCTOR') and @courseSecurityChecker.checkInstructorAccess(#courseId, #instructorEmail)") + public ResponseEntity gradeExam( + @PathVariable Long courseId, + @PathVariable Long examId, + @PathVariable Long resultId, + @CurrentUserEmail String instructorEmail) { + + log.info("Grading request - courseId: {}, examId: {}, resultId: {}", courseId, examId, resultId); + gradingService.gradeExamResult(resultId); + return ResponseEntity.ok().build(); + } + + /** + * 평균 점수 조회 + * @param courseId 강의 ID + * @param examId 시험 ID + * @param instructorEmail 강사 이메일 + * @return 평균 점수 + */ + @GetMapping("/average") + @PreAuthorize("hasRole('INSTRUCTOR') and @courseSecurityChecker.checkInstructorAccess(#courseId, #instructorEmail)") + public ResponseEntity getAverageScore( + @PathVariable Long courseId, + @PathVariable Long examId, + @CurrentUserEmail String instructorEmail) { + + log.info("Retrieving average score - courseId: {}, examId: {}", courseId, examId); + double averageScore = gradingService.calculateAverageScore(examId); + return ResponseEntity.ok(averageScore); + } + + /** + * 최고/최저 점수 통계 조회 + * @param courseId 강의 ID + * @param examId 시험 ID + * @param instructorEmail 강사 이메일 + * @return 점수 통계 정보 + */ + @GetMapping("/statistics") + @PreAuthorize("hasRole('INSTRUCTOR') and @courseSecurityChecker.checkInstructorAccess(#courseId, #instructorEmail)") + public ResponseEntity getScoreStatistics( + @PathVariable Long courseId, + @PathVariable Long examId, + @CurrentUserEmail String instructorEmail) { + + log.info("Retrieving score statistics - courseId: {}, examId: {}", courseId, examId); + ScoreStatistics statistics = gradingService.calculateScoreStatistics(examId); + return ResponseEntity.ok(statistics); + } +} +``` From ea865969a6acb27d9b45116110a165b74f522542 Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 11:12:11 +0900 Subject: [PATCH 06/12] =?UTF-8?q?FIX:=20log=20import=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - log import 오휴 해결 --- .../java/com/example/epari/exam/service/GradingService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/epari/exam/service/GradingService.java b/src/main/java/com/example/epari/exam/service/GradingService.java index e7237834..e857eb73 100644 --- a/src/main/java/com/example/epari/exam/service/GradingService.java +++ b/src/main/java/com/example/epari/exam/service/GradingService.java @@ -15,10 +15,12 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Service @RequiredArgsConstructor @Transactional +@Slf4j public class GradingService { private final ExamResultRepository examResultRepository; From d32cb176607a61e09392f0832ab3f231e3dd5cc8 Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 11:21:12 +0900 Subject: [PATCH 07/12] =?UTF-8?q?FEAT:=20ExamResultRepository=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EB=B0=8F=20AutoGradingSchedul?= =?UTF-8?q?er=20=EC=B1=84=EC=A0=90=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ExamResultRepository에 조회 쿼리 추가 - 특정 시험의 채점 상태를 확인할 수 있는 쿼리 추가 - 시험 시간이 초과된 시험의 조회 쿼리 추가 - 채점 대기 중인 시험 조회 쿼리 추가 - AutoGradingScheduler에 자동화 기능 구현 - 시간 초과시 시험 자동 제출 기능 구현 (1분 간격 스케줄) - 제출된 시험 자동 채점 기능 구현 (5분 간격 스케줄) --- .../exam/repository/ExamResultRepository.java | 19 +++-- .../exam/scheduler/AutoGradingScheduler.java | 72 +++++++++++++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java diff --git a/src/main/java/com/example/epari/exam/repository/ExamResultRepository.java b/src/main/java/com/example/epari/exam/repository/ExamResultRepository.java index 0bf3c3bc..d901ddfa 100644 --- a/src/main/java/com/example/epari/exam/repository/ExamResultRepository.java +++ b/src/main/java/com/example/epari/exam/repository/ExamResultRepository.java @@ -38,7 +38,7 @@ List findByCourseIdAndStudentEmail( @Param("studentEmail") String studentEmail ); - // 시험 상태별 결과 조회 + // 특정 상태의 시험 결과 조회 @Query("SELECT er FROM ExamResult er " + "WHERE er.exam.id = :examId " + "AND er.status = :status") @@ -47,11 +47,20 @@ List findByExamIdAndStatus( @Param("status") ExamStatus status ); + // 채점 대기중인 시험 결과 조회 (자동 채점용) @Query(""" - SELECT er FROM ExamResult er - JOIN FETCH er.exam e - WHERE er.status = 'IN_PROGRESS' - AND e.examDateTime <= :baseTime + SELECT er FROM ExamResult er + WHERE er.status = 'SUBMITTED' + AND er.submitTime <= :baseTime + """) + List findPendingGradingExams(@Param("baseTime") LocalDateTime baseTime); + + // 시간 초과된 진행중 시험 조회 (자동 제출용) + @Query(""" + SELECT er FROM ExamResult er + JOIN FETCH er.exam e + WHERE er.status = 'IN_PROGRESS' + AND e.examDateTime <= :baseTime """) List findExpiredExams(@Param("baseTime") LocalDateTime baseTime); diff --git a/src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java b/src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java new file mode 100644 index 00000000..df164726 --- /dev/null +++ b/src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java @@ -0,0 +1,72 @@ +package com.example.epari.exam.scheduler; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.example.epari.exam.domain.ExamResult; +import com.example.epari.exam.repository.ExamResultRepository; +import com.example.epari.exam.service.GradingService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 시험 자동 제출 및 채점을 처리하는 스케줄러 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AutoGradingScheduler { + + private final ExamResultRepository examResultRepository; + private final GradingService gradingService; + + /** + * 1분마다 실행되는 시험 자동 제출 처리 + * 시험 시간이 종료된 시험을 자동으로 제출 처리 + */ + @Scheduled(fixedRate = 60000) + @Transactional + public void autoSubmitExpiredExams() { + LocalDateTime baseTime = LocalDateTime.now().minusMinutes(1); + List expiredExams = examResultRepository.findExpiredExams(baseTime); + + for (ExamResult result : expiredExams) { + try { + result.submit(true); // force submit + log.info("Auto submitted exam - resultId: {}, studentId: {}, submittedAt: {}", + result.getId(), + result.getStudent().getId(), + result.getSubmitTime()); + } catch (Exception e) { + log.error("Failed to auto submit exam - resultId: " + result.getId(), e); + } + } + } + + /** + * 5분마다 실행되는 자동 채점 처리 + * 제출된 시험 중 채점되지 않은 시험을 자동으로 채점 + */ + @Scheduled(fixedRate = 300000) + public void autoGradeSubmittedExams() { + LocalDateTime baseTime = LocalDateTime.now().minusMinutes(5); + List pendingExams = examResultRepository.findPendingGradingExams(baseTime); + + for (ExamResult result : pendingExams) { + try { + gradingService.gradeExamResult(result.getId()); + log.info("Auto graded exam - resultId: {}, totalScore: {}", + result.getId(), + result.getTotalScore()); + } catch (Exception e) { + log.error("Failed to auto grade exam - resultId: " + result.getId(), e); + } + } + } +} +``` From c7bc57df378debd8264727e1d5cd1fc1b0502ee2 Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 11:36:30 +0900 Subject: [PATCH 08/12] =?UTF-8?q?FEAT:=20=EC=B1=84=EC=A0=90=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20ErrorCode=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorCode에 채첨 관련 에러 메세지 정의 - 채점 예외 처리 클래스 생성 - GradingException - GradingNotPossibleException - InvalidScoreException --- .../epari/exam/exception/GradingException.java | 15 +++++++++++++++ .../exception/GradingNotPossibleException.java | 8 ++++++++ .../exam/exception/InvalidScoreException.java | 8 ++++++++ .../epari/global/exception/ErrorCode.java | 17 ++++++++++++----- 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/epari/exam/exception/GradingException.java create mode 100644 src/main/java/com/example/epari/exam/exception/GradingNotPossibleException.java create mode 100644 src/main/java/com/example/epari/exam/exception/InvalidScoreException.java diff --git a/src/main/java/com/example/epari/exam/exception/GradingException.java b/src/main/java/com/example/epari/exam/exception/GradingException.java new file mode 100644 index 00000000..3ce508e2 --- /dev/null +++ b/src/main/java/com/example/epari/exam/exception/GradingException.java @@ -0,0 +1,15 @@ +package com.example.epari.exam.exception; + +public class GradingException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public GradingException(String message) { + super(message); + } + + public GradingException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/src/main/java/com/example/epari/exam/exception/GradingNotPossibleException.java b/src/main/java/com/example/epari/exam/exception/GradingNotPossibleException.java new file mode 100644 index 00000000..8645595f --- /dev/null +++ b/src/main/java/com/example/epari/exam/exception/GradingNotPossibleException.java @@ -0,0 +1,8 @@ +package com.example.epari.exam.exception; + +public class GradingNotPossibleException extends GradingException { + + public GradingNotPossibleException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/epari/exam/exception/InvalidScoreException.java b/src/main/java/com/example/epari/exam/exception/InvalidScoreException.java new file mode 100644 index 00000000..fd97b025 --- /dev/null +++ b/src/main/java/com/example/epari/exam/exception/InvalidScoreException.java @@ -0,0 +1,8 @@ +package com.example.epari.exam.exception; + +public class InvalidScoreException extends GradingException { + + public InvalidScoreException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/epari/global/exception/ErrorCode.java b/src/main/java/com/example/epari/global/exception/ErrorCode.java index 9d0c47c7..e7fb7134 100644 --- a/src/main/java/com/example/epari/global/exception/ErrorCode.java +++ b/src/main/java/com/example/epari/global/exception/ErrorCode.java @@ -29,7 +29,7 @@ public enum ErrorCode { VERIFICATION_CODE_LIMIT_EXCEEDED(HttpStatus.TOO_MANY_REQUESTS, "AUTH-009", "잠시 후 다시 시도해주세요."), // Student 관련 에러 코드(ST - STUDENT_NOT_FOUND(HttpStatus.NOT_FOUND, "STD-001", "학생 정보를 찾을 수 없습니다."), + STUDENT_NOT_FOUND(HttpStatus.NOT_FOUND, "STD-001", "학생 정보를 찾을 수 없습니다."), // Course 관련 에러 코드 (CRS) COURSE_NOT_FOUND(HttpStatus.NOT_FOUND, "CRS-001", "강의를 찾을 수 없습니다."), @@ -39,9 +39,9 @@ public enum ErrorCode { ASSIGNMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "ASM-001", "과제를 찾을 수 없습니다."), SUBMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "ASM-002", "과제 제출물을 찾을 수 없습니다."), - // Question 관련 에러 코드(EXAM) - QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, "QST-001", "문제를 찾을 수 없습니다."), - QUESTION_HAS_SUBMISSIONS(HttpStatus.BAD_REQUEST, "QST-002", "답안이 제출된 문제는 삭제할 수 없습니다."), + // Question 관련 에러 코드(EXAM) + QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, "QST-001", "문제를 찾을 수 없습니다."), + QUESTION_HAS_SUBMISSIONS(HttpStatus.BAD_REQUEST, "QST-002", "답안이 제출된 문제는 삭제할 수 없습니다."), // Exam 관련 에러 코드 (EXAM) EXAM_NOT_FOUND(HttpStatus.NOT_FOUND, "EXAM-001", "시험을 찾을 수 없습니다."), @@ -73,7 +73,14 @@ public enum ErrorCode { // 알림 관련 에러 코드 (NTF) NOTIFICATION_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "NTF-001", "알림 발송에 실패했습니다."), - SES_SERVICE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "NTF-002", "이메일 서비스 연동 중 오류가 발생했습니다."); + SES_SERVICE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "NTF-002", "이메일 서비스 연동 중 오류가 발생했습니다."), + + // 채점 관련 에러 코드 (GRD) + EXAM_NOT_SUBMITTED(HttpStatus.BAD_REQUEST, "GRD-001", "제출되지 않은 시험은 채점할 수 없습니다."), + EXAM_ALREADY_GRADED(HttpStatus.BAD_REQUEST, "GRD-002", "이미 채점이 완료된 시험입니다."), + GRADING_IN_PROGRESS(HttpStatus.BAD_REQUEST, "GRD-003", "채점이 진행 중인 시험입니다."), + INVALID_SCORE_VALUE(HttpStatus.BAD_REQUEST, "GRD-004", "유효하지 않은 점수입니다."), + GRADING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "GRD-005", "채점 처리 중 오류가 발생했습니다."); private final HttpStatus status; From c9419f05d579dd5c4f978d1fe419db43b8901bda Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 12:16:56 +0900 Subject: [PATCH 09/12] =?UTF-8?q?REFACTOR:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AC=B8=EC=9E=90,=20=EA=B3=B5=EB=B0=B1=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorCode 내 공백 수정 - AutoGradingScheduler에 불필요한 문자 삭제 - GradingService에 클래스명 및 주석 수정 --- .../epari/exam/scheduler/AutoGradingScheduler.java | 1 - .../com/example/epari/exam/service/GradingService.java | 7 +++++-- .../com/example/epari/global/exception/ErrorCode.java | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java b/src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java index df164726..798584b9 100644 --- a/src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java +++ b/src/main/java/com/example/epari/exam/scheduler/AutoGradingScheduler.java @@ -69,4 +69,3 @@ public void autoGradeSubmittedExams() { } } } -``` diff --git a/src/main/java/com/example/epari/exam/service/GradingService.java b/src/main/java/com/example/epari/exam/service/GradingService.java index e857eb73..d30c5dab 100644 --- a/src/main/java/com/example/epari/exam/service/GradingService.java +++ b/src/main/java/com/example/epari/exam/service/GradingService.java @@ -25,7 +25,10 @@ public class GradingService { private final ExamResultRepository examResultRepository; - public void gradeExamResult(ExamResult examResult) { + /** + * 단순 채점 처리 (내부용) + */ + public void processGrading(ExamResult examResult) { if (examResult.getStatus() != ExamStatus.SUBMITTED) { throw new IllegalStateException("제출된 시험만 채점할 수 있습니다."); } @@ -58,7 +61,7 @@ public boolean verifyTotalScore(ExamResult examResult) { } /** - * 개별 시험 결과 채점 + * 개별 시험 결과 채점 (API용) */ public void gradeExamResult(Long examResultId) { ExamResult examResult = examResultRepository.findById(examResultId) diff --git a/src/main/java/com/example/epari/global/exception/ErrorCode.java b/src/main/java/com/example/epari/global/exception/ErrorCode.java index e7fb7134..9958a772 100644 --- a/src/main/java/com/example/epari/global/exception/ErrorCode.java +++ b/src/main/java/com/example/epari/global/exception/ErrorCode.java @@ -77,10 +77,10 @@ public enum ErrorCode { // 채점 관련 에러 코드 (GRD) EXAM_NOT_SUBMITTED(HttpStatus.BAD_REQUEST, "GRD-001", "제출되지 않은 시험은 채점할 수 없습니다."), - EXAM_ALREADY_GRADED(HttpStatus.BAD_REQUEST, "GRD-002", "이미 채점이 완료된 시험입니다."), - GRADING_IN_PROGRESS(HttpStatus.BAD_REQUEST, "GRD-003", "채점이 진행 중인 시험입니다."), - INVALID_SCORE_VALUE(HttpStatus.BAD_REQUEST, "GRD-004", "유효하지 않은 점수입니다."), - GRADING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "GRD-005", "채점 처리 중 오류가 발생했습니다."); + EXAM_ALREADY_GRADED(HttpStatus.BAD_REQUEST, "GRD-002", "이미 채점이 완료된 시험입니다."), + GRADING_IN_PROGRESS(HttpStatus.BAD_REQUEST, "GRD-003", "채점이 진행 중인 시험입니다."), + INVALID_SCORE_VALUE(HttpStatus.BAD_REQUEST, "GRD-004", "유효하지 않은 점수입니다."), + GRADING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "GRD-005", "채점 처리 중 오류가 발생했습니다."); private final HttpStatus status; From c82be2159845568f890c2378a000eec74a1a984b Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 12:19:51 +0900 Subject: [PATCH 10/12] =?UTF-8?q?FEAT:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorResponse 응답 객체 구현 - GradingException 등 채점 관련 예외 처리 핸들러 추가 - ExamQuestion 조회를 위한 쿼리 메소드 추가 --- .../exam/controller/GradingController.java | 28 +++++++++++++++++-- .../repository/ExamQuestionRepository.java | 8 +++++- .../epari/global/exception/ErrorResponse.java | 14 ++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/epari/exam/controller/GradingController.java b/src/main/java/com/example/epari/exam/controller/GradingController.java index fb9502d7..1b1eb0a3 100644 --- a/src/main/java/com/example/epari/exam/controller/GradingController.java +++ b/src/main/java/com/example/epari/exam/controller/GradingController.java @@ -2,26 +2,33 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.example.epari.exam.exception.GradingException; +import com.example.epari.exam.exception.GradingNotPossibleException; +import com.example.epari.exam.exception.InvalidScoreException; import com.example.epari.exam.service.GradingService; import com.example.epari.exam.service.GradingService.ScoreStatistics; import com.example.epari.global.annotation.CurrentUserEmail; +import com.example.epari.global.exception.ErrorCode; +import com.example.epari.global.exception.ErrorResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + /** * 시험 채점 및 성적 통계 관련 요청을 처리하는 Controller */ @Slf4j @RestController -@RequestMapping("/api/courses/{courseId}/exams/{examId}/grades") @RequiredArgsConstructor +@RequestMapping("/api/courses/{courseId}/exams/{examId}/grades") public class GradingController { private final GradingService gradingService; @@ -84,5 +91,22 @@ public ResponseEntity getScoreStatistics( ScoreStatistics statistics = gradingService.calculateScoreStatistics(examId); return ResponseEntity.ok(statistics); } + + @ExceptionHandler(GradingException.class) + public ResponseEntity handleGradingException(GradingException e) { + log.error("채점 처리 중 오류 발생", e); + return ErrorResponse.toResponseEntity(ErrorCode.GRADING_FAILED); + } + + @ExceptionHandler(GradingNotPossibleException.class) + public ResponseEntity handleGradingNotPossibleException(GradingNotPossibleException e) { + log.error("채점 불가능한 상태", e); + return ErrorResponse.toResponseEntity(ErrorCode.EXAM_NOT_SUBMITTED); + } + + @ExceptionHandler(InvalidScoreException.class) + public ResponseEntity handleInvalidScoreException(InvalidScoreException e) { + log.error("유효하지 않은 점수", e); + return ErrorResponse.toResponseEntity(ErrorCode.INVALID_SCORE_VALUE); + } } -``` diff --git a/src/main/java/com/example/epari/exam/repository/ExamQuestionRepository.java b/src/main/java/com/example/epari/exam/repository/ExamQuestionRepository.java index d13f69c7..89aad3c7 100644 --- a/src/main/java/com/example/epari/exam/repository/ExamQuestionRepository.java +++ b/src/main/java/com/example/epari/exam/repository/ExamQuestionRepository.java @@ -4,6 +4,8 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.example.epari.exam.domain.ExamQuestion; @@ -18,6 +20,10 @@ public interface ExamQuestionRepository extends JpaRepository findByExamIdAndId(Long examId, Long id); + @Query("SELECT q FROM ExamQuestion q WHERE q.exam.id = :examId AND q.id = :questionId") + Optional findByExamIdAndId( + @Param("examId") Long examId, + @Param("questionId") Long questionId + ); } diff --git a/src/main/java/com/example/epari/global/exception/ErrorResponse.java b/src/main/java/com/example/epari/global/exception/ErrorResponse.java index 069cfed9..950037ef 100644 --- a/src/main/java/com/example/epari/global/exception/ErrorResponse.java +++ b/src/main/java/com/example/epari/global/exception/ErrorResponse.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.List; +import org.springframework.http.ResponseEntity; + import lombok.Getter; /** @@ -38,4 +40,16 @@ public static ErrorResponse of(ErrorCode errorCode, List errors return new ErrorResponse(errorCode, errors); } + public static ResponseEntity toResponseEntity(ErrorCode errorCode) { + return ResponseEntity + .status(errorCode.getStatus()) + .body(ErrorResponse.of(errorCode)); + } + + public static ResponseEntity toResponseEntity(ErrorCode errorCode, List errors) { + return ResponseEntity + .status(errorCode.getStatus()) + .body(ErrorResponse.of(errorCode, errors)); + +} } From 0193672c49e577a265eaf2a9e4ca5c4d58d7effc Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 15:57:01 +0900 Subject: [PATCH 11/12] =?UTF-8?q?FIX:=20InitData=EC=9D=98=20createExamResu?= =?UTF-8?q?lts=20=EC=A0=90=EC=88=98=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 점수 업데이트 시 사용되던 하드코딩된 리터럴 값 제거 --- .../java/com/example/epari/global/init/InitDataLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/epari/global/init/InitDataLoader.java b/src/main/java/com/example/epari/global/init/InitDataLoader.java index f43cffc2..4fe73f9c 100644 --- a/src/main/java/com/example/epari/global/init/InitDataLoader.java +++ b/src/main/java/com/example/epari/global/init/InitDataLoader.java @@ -1419,9 +1419,9 @@ private void createExamResults(List courses, List students) { .build(); if (question.validateAnswer(studentAnswer)) { - score.updateScore(question.getScore(), "정답입니다."); + score.updateScore(question.getScore()); } else { - score.updateScore(0, "오답입니다. 정답은 " + question.getCorrectAnswer() + "입니다."); + score.updateScore(0); } result.addScore(score); From 6a820d6e36bb53f6485177907be206295504f0b0 Mon Sep 17 00:00:00 2001 From: hyerin315 Date: Wed, 20 Nov 2024 16:33:50 +0900 Subject: [PATCH 12/12] =?UTF-8?q?FIX:=20=EC=8B=9C=ED=97=98=20=EC=B1=84?= =?UTF-8?q?=EC=A0=90=20=ED=9B=84=20=EC=B4=9D=EC=A0=90=EC=9D=B4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GradingService에 총점 계산 및 상태 업데이트 로직 추가 --- .../example/epari/exam/service/GradingService.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/epari/exam/service/GradingService.java b/src/main/java/com/example/epari/exam/service/GradingService.java index d30c5dab..c8dda4a6 100644 --- a/src/main/java/com/example/epari/exam/service/GradingService.java +++ b/src/main/java/com/example/epari/exam/service/GradingService.java @@ -33,21 +33,18 @@ public void processGrading(ExamResult examResult) { throw new IllegalStateException("제출된 시험만 채점할 수 있습니다."); } - int totalScore = 0; - for (ExamScore score : examResult.getScores()) { ExamQuestion question = score.getQuestion(); String studentAnswer = score.getStudentAnswer(); - + // 답안 채점 boolean isCorrect = question.validateAnswer(studentAnswer); int earnedScore = isCorrect ? question.getScore() : 0; score.updateScore(earnedScore); - - totalScore += earnedScore; } - - examResult.updateStatus(ExamStatus.GRADED); + + // 총점 계산 및 상태 업데이트 + examResult.updateScore(); // updateStatus 대신 updateScore 호출 examResultRepository.save(examResult); }