Complete quiz attempt service with result and history queries

This commit is contained in:
jieun committed 2026-10-20 20:52:53 +09:00
1 parent 1db02c4e15
commit b539a9a99a
1 file changed
+52 -13
@@ -16,6 +16,7 @@ import com.example.questionbank.repository.UserRepository;
import com.example.questionbank.web.rest.dto.AnswerResult; import com.example.questionbank.web.rest.dto.AnswerResult;
import com.example.questionbank.web.rest.dto.ChoiceView; import com.example.questionbank.web.rest.dto.ChoiceView;
import com.example.questionbank.web.rest.dto.ExamAttemptStartResult; import com.example.questionbank.web.rest.dto.ExamAttemptStartResult;
import com.example.questionbank.web.rest.dto.ExamAttemptSummary;
import com.example.questionbank.web.rest.dto.ExamQuestionView; import com.example.questionbank.web.rest.dto.ExamQuestionView;
import com.example.questionbank.web.rest.dto.ExamResult; import com.example.questionbank.web.rest.dto.ExamResult;
import com.example.questionbank.web.rest.dto.SubmitAnswerRequest; import com.example.questionbank.web.rest.dto.SubmitAnswerRequest;
@@ -140,22 +141,18 @@ public class QuizAttemptService {
int totalScore = 0; int totalScore = 0;
for (SubmitAnswerRequest answerRequest : request.answers()) { for (SubmitAnswerRequest answerRequest : request.answers()) {
// 동일한 문제를 중복 제출했는지 확인
if (!seenQuestionIds.add(answerRequest.questionId())) { if (!seenQuestionIds.add(answerRequest.questionId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "중복 제출된 문항이 있습니다."); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "중복 제출된 문항이 있습니다.");
} }
// 제출한 문제 조회
Question question = questionRepository Question question = questionRepository
.findById(answerRequest.questionId()) .findById(answerRequest.questionId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "문항을 찾을 수 없습니다.")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "문항을 찾을 수 없습니다."));
// 현재 과목에 속한 활성 문제인지 확인
if (!Boolean.TRUE.equals(question.getActive()) || !question.getSubject().getId().equals(attempt.getSubject().getId())) { if (!Boolean.TRUE.equals(question.getActive()) || !question.getSubject().getId().equals(attempt.getSubject().getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "응시할 수 없는 문항입니다."); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "응시할 수 없는 문항입니다.");
} }
// 보기 4개와 정답 1개가 올바르게 등록됐는지 확인
List<Choice> choices = new ArrayList<>(question.getChoices()); List<Choice> choices = new ArrayList<>(question.getChoices());
List<Choice> correctChoices = choices List<Choice> correctChoices = choices
@@ -169,7 +166,6 @@ public class QuizAttemptService {
throw new ResponseStatusException(HttpStatus.CONFLICT, "문항의 선택지 구성이 올바르지 않습니다."); throw new ResponseStatusException(HttpStatus.CONFLICT, "문항의 선택지 구성이 올바르지 않습니다.");
} }
// 사용자가 선택한 보기 찾기
OptionKey selectedOption = answerRequest.selectedOption(); OptionKey selectedOption = answerRequest.selectedOption();
Choice selectedChoice = Choice selectedChoice =
@@ -183,10 +179,8 @@ public class QuizAttemptService {
new ResponseStatusException(HttpStatus.BAD_REQUEST, "해당 문항에 존재하지 않는 선택지입니다.") new ResponseStatusException(HttpStatus.BAD_REQUEST, "해당 문항에 존재하지 않는 선택지입니다.")
); );
// 앞에서 만든 ExamScoringService로 채점
ExamScoringService.ScoringResult score = scoringService.score(correctChoices.getFirst().getOptionKey(), selectedOption); ExamScoringService.ScoringResult score = scoringService.score(correctChoices.getFirst().getOptionKey(), selectedOption);
// 문제·해설·선택 답안의 스냅샷 저장
ExamAnswer answer = new ExamAnswer(); ExamAnswer answer = new ExamAnswer();
answer.setAttempt(attempt); answer.setAttempt(attempt);
answer.setQuestion(question); answer.setQuestion(question);
@@ -199,10 +193,8 @@ public class QuizAttemptService {
answerRepository.save(answer); answerRepository.save(answer);
// 총점 계산
totalScore += score.awardedPoints(); totalScore += score.awardedPoints();
// 응시자에게 반환할 채점 결과 만들기
results.add( results.add(
new AnswerResult( new AnswerResult(
question.getId(), question.getId(),
@@ -216,14 +208,11 @@ public class QuizAttemptService {
); );
} }
// 모든 활성 문제를 제출했는지 확인
if (!seenQuestionIds.equals(requiredQuestionIds)) { if (!seenQuestionIds.equals(requiredQuestionIds)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "과목의 모든 활성 문항을 빠짐없이 제출해야 합니다."); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "과목의 모든 활성 문항을 빠짐없이 제출해야 합니다.");
} }
// 시험 제출 완료 처리
Instant submittedAt = Instant.now(); Instant submittedAt = Instant.now();
attempt.setScore(totalScore); attempt.setScore(totalScore);
attempt.setSubmittedAt(submittedAt); attempt.setSubmittedAt(submittedAt);
attempt.setStatus(AttemptStatus.SUBMITTED); attempt.setStatus(AttemptStatus.SUBMITTED);
@@ -231,7 +220,57 @@ public class QuizAttemptService {
return new ExamResult(attempt.getId(), totalScore, submittedAt, results); return new ExamResult(attempt.getId(), totalScore, submittedAt, results);
} }
// 4. 응시 기록 소유자 확인 // 4. 제출 완료된 시험 결과 조회
@Transactional(readOnly = true)
public ExamResult getResult(Long attemptId, String login) {
ExamAttempt attempt = getOwnedAttempt(attemptId, login);
if (attempt.getStatus() != AttemptStatus.SUBMITTED) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "아직 제출되지 않은 응시입니다.");
}
List<AnswerResult> results = attempt
.getAnswers()
.stream()
.sorted(Comparator.comparing(a -> a.getQuestion().getId()))
.map(answer ->
new AnswerResult(
answer.getQuestion().getId(),
answer.getQuestionTextSnapshot(),
answer.getSelectedOption(),
answer.getSelectedContentSnapshot(),
Boolean.TRUE.equals(answer.getCorrect()),
answer.getAwardedPoints() == null ? 0 : answer.getAwardedPoints(),
answer.getExplanationSnapshot()
)
)
.toList();
return new ExamResult(attempt.getId(), attempt.getScore() == null ? 0 : attempt.getScore(), attempt.getSubmittedAt(), results);
}
// 5. 로그인한 사용자의 응시 이력 조회
@Transactional(readOnly = true)
public List<ExamAttemptSummary> getMyAttempts(String login) {
return attemptRepository
.findAllByUserLogin(login)
.stream()
.sorted(Comparator.comparing(ExamAttempt::getStartedAt, Comparator.nullsLast(Comparator.reverseOrder())))
.map(attempt ->
new ExamAttemptSummary(
attempt.getId(),
attempt.getSubject().getId(),
attempt.getSubject().getName(),
attempt.getStartedAt(),
attempt.getSubmittedAt(),
attempt.getStatus(),
attempt.getScore()
)
)
.toList();
}
// 6. 응시 기록 소유자 확인
private ExamAttempt getOwnedAttempt(Long attemptId, String login) { private ExamAttempt getOwnedAttempt(Long attemptId, String login) {
ExamAttempt attempt = attemptRepository ExamAttempt attempt = attemptRepository
.findById(attemptId) .findById(attemptId)