Skip to content

Commit 99c99be

Browse files
committed
퀴즈 기능 완성
1 parent 3d48d2a commit 99c99be

7 files changed

Lines changed: 215 additions & 2 deletions

File tree

‎src/main/java/hello/cluebackend/domain/quizbattle/model/QuizOption.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package hello.cluebackend.domain.quizbattle.model;
22

3+
import com.fasterxml.jackson.annotation.JsonProperty;
34
import lombok.*;
45

56
import java.io.Serializable;
@@ -10,6 +11,9 @@
1011
@AllArgsConstructor
1112
@NoArgsConstructor
1213
public class QuizOption implements Serializable {
14+
@JsonProperty("index")
1315
private Integer index;
16+
17+
@JsonProperty("text")
1418
private String text;
1519
}

‎src/main/java/hello/cluebackend/domain/quizbattle/model/QuizQuestion.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package hello.cluebackend.domain.quizbattle.model;
22

3+
import com.fasterxml.jackson.annotation.JsonProperty;
34
import lombok.*;
45

56
import java.io.Serializable;
@@ -12,12 +13,25 @@
1213
@NoArgsConstructor
1314
public class QuizQuestion implements Serializable {
1415

16+
@JsonProperty("question_number")
1517
private Integer questionNumber;
18+
19+
@JsonProperty("question_text")
1620
private String questionText;
21+
22+
@JsonProperty("options")
1723
private List<QuizOption> options;
24+
25+
@JsonProperty("correct_answer")
1826
private Integer correctAnswer;
27+
28+
@JsonProperty("time_limit")
1929
private Integer timeLimit;
30+
31+
@JsonProperty("explanation")
2032
private String explanation;
33+
34+
@JsonProperty("difficulty")
2135
private String difficulty;
2236

2337
public boolean isCorrect(Integer answerIndex) {

‎src/main/java/hello/cluebackend/domain/quizbattle/model/QuizRoom.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,8 @@ public boolean canJoin() {
9090
public boolean isActive() {
9191
return this.status == QuizRoomStatus.WAITING || this.status == QuizRoomStatus.IN_PROGRESS;
9292
}
93+
94+
public void updateQuestionCount(int count) {
95+
this.questionCount = count;
96+
}
9397
}

‎src/main/java/hello/cluebackend/domain/quizbattle/service/QuizBattleService.java‎

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import org.springframework.stereotype.Service;
1818
import org.springframework.transaction.annotation.Transactional;
1919

20+
import java.util.HashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123
import java.util.Random;
2224
import java.util.UUID;
2325

@@ -75,7 +77,15 @@ public QuizRoom createRoom(
7577
List<QuizQuestion> questions = generateQuestions(finalQuestionCount, documentId, finalTimePerQuestion);
7678
redisService.storeQuestions(roomCode, questions);
7779

78-
log.info("with code: {} and {} questions", roomCode, questions.size());
80+
// 실제 생성된 문제 개수가 요청한 개수와 다를 경우 업데이트
81+
if (questions.size() != finalQuestionCount) {
82+
savedRoom.updateQuestionCount(questions.size());
83+
quizRoomRepository.save(savedRoom);
84+
log.info("Updated question count from {} to {} for room {}",
85+
finalQuestionCount, questions.size(), roomCode);
86+
}
87+
88+
log.info("Created room {} with {} questions", roomCode, questions.size());
7989

8090
return savedRoom;
8191
}
@@ -148,12 +158,38 @@ private List<QuizQuestion> generateQuestions(int count, UUID documentId, int tim
148158

149159
if (response.getData() != null && response.getData().getQuestions() != null) {
150160
List<QuizQuestion> questions = response.getData().getQuestions();
161+
162+
// 반환된 문제 개수 확인
163+
if (questions.isEmpty()) {
164+
log.error("No questions generated from FastAPI");
165+
throw new RuntimeException("No questions were generated");
166+
}
167+
168+
// 요청한 개수보다 많으면 자르기
169+
if (questions.size() > count) {
170+
log.warn("Generated {} questions but only {} were requested. Trimming...",
171+
questions.size(), count);
172+
questions = questions.subList(0, count);
173+
}
174+
175+
// 요청한 개수보다 적으면 경고
176+
if (questions.size() < count) {
177+
log.warn("Only {} questions generated, but {} were requested",
178+
questions.size(), count);
179+
}
180+
151181
// Manually set question numbers and time limits
152182
for (int i = 0; i < questions.size(); i++) {
153183
QuizQuestion question = questions.get(i);
154184
question.setQuestionNumber(i + 1);
155185
question.setTimeLimit(timePerQuestion);
186+
log.debug("Question {} generated: text={}, options={}, correctAnswer={}",
187+
i + 1, question.getQuestionText(),
188+
question.getOptions() != null ? question.getOptions().size() : "null",
189+
question.getCorrectAnswer());
156190
}
191+
192+
log.info("Successfully generated {} questions (requested: {})", questions.size(), count);
157193
return questions;
158194
} else {
159195
log.error("Failed to generate questions from FastAPI: {}", response.getMessage());
@@ -168,6 +204,23 @@ private List<QuizQuestion> generateQuestions(int count, UUID documentId, int tim
168204
public QuizAnswer submitAnswer(
169205
String roomCode, UUID userId, int questionNumber,
170206
int answerIndex, long submittedAt, int timeSpent) {
207+
QuizRoom room = quizRoomRepository.findByRoomCode(roomCode)
208+
.orElseThrow(() -> new IllegalArgumentException("Room not found: " + roomCode));
209+
210+
if (room.getStatus() != QuizRoomStatus.IN_PROGRESS) {
211+
throw new IllegalStateException("Quiz is not in progress");
212+
}
213+
214+
Integer currentQuestionNum = redisService.getCurrentQuestionNumber(roomCode);
215+
if (currentQuestionNum == null || currentQuestionNum != questionNumber) {
216+
throw new IllegalStateException("Not the current question");
217+
}
218+
219+
String questionStatus = redisService.getQuestionStatus(roomCode, questionNumber);
220+
if ("REVEALED".equals(questionStatus)) {
221+
throw new IllegalStateException("Answer already revealed, cannot submit");
222+
}
223+
171224
if (redisService.hasAnswered(roomCode, questionNumber, userId)) {
172225
throw new IllegalStateException("Already answered this question");
173226
}
@@ -275,6 +328,38 @@ public Integer getCurrentQuestionNumber(String roomCode) {
275328
return redisService.getCurrentQuestionNumber(roomCode);
276329
}
277330

331+
public void setQuestionActive(String roomCode, int questionNumber) {
332+
redisService.setQuestionStatus(roomCode, questionNumber, "ACTIVE");
333+
log.info("Set question {} to ACTIVE in room {}", questionNumber, roomCode);
334+
}
335+
336+
public Map<String, Object> revealAnswer(String roomCode, int questionNumber) {
337+
QuizQuestion question = redisService.getQuestion(roomCode, questionNumber);
338+
if (question == null) {
339+
throw new IllegalArgumentException("Question not found: " + questionNumber);
340+
}
341+
342+
String currentStatus = redisService.getQuestionStatus(roomCode, questionNumber);
343+
if ("REVEALED".equals(currentStatus)) {
344+
throw new IllegalStateException("Answer already revealed for question " + questionNumber);
345+
}
346+
347+
redisService.setQuestionStatus(roomCode, questionNumber, "REVEALED");
348+
349+
Map<Integer, Integer> statistics = redisService.getAnswerStatistics(roomCode, questionNumber);
350+
int totalAnswers = statistics.values().stream().mapToInt(Integer::intValue).sum();
351+
352+
Map<String, Object> result = new HashMap<>();
353+
result.put("correctAnswer", question.getCorrectAnswer());
354+
result.put("explanation", question.getExplanation());
355+
result.put("statistics", statistics);
356+
result.put("totalAnswers", totalAnswers);
357+
358+
log.info("Revealed answer for question {} in room {}", questionNumber, roomCode);
359+
360+
return result;
361+
}
362+
278363
public void nextQuestion(String roomCode) {
279364
Integer currentQuestion = redisService.getCurrentQuestionNumber(roomCode);
280365
if (currentQuestion != null) {

‎src/main/java/hello/cluebackend/domain/quizbattle/service/QuizRoomRedisService.java‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public class QuizRoomRedisService {
2525
private static final String ANSWER_KEY_SUFFIX = ":answers";
2626
private static final String CURRENT_QUESTION_KEY_PREFIX = "quiz:room:";
2727
private static final String CURRENT_QUESTION_KEY_SUFFIX = ":current";
28+
private static final String QUESTION_STATUS_KEY_PREFIX = "quiz:room:";
29+
private static final String QUESTION_STATUS_KEY_SUFFIX = ":status";
2830
private static final String SESSION_MAPPING_PREFIX = "quiz:session:";
2931
private static final long DEFAULT_EXPIRATION_HOURS = 24;
3032

@@ -103,7 +105,15 @@ public void storeQuestions(String roomCode, List<QuizQuestion> questions) {
103105
public QuizQuestion getQuestion(String roomCode, int questionNumber) {
104106
String key = getQuestionKey(roomCode);
105107
Object question = redisTemplate.opsForList().index(key, questionNumber - 1);
106-
return question != null ? (QuizQuestion) question : null;
108+
if (question != null) {
109+
QuizQuestion q = (QuizQuestion) question;
110+
log.debug("Retrieved question {} from Redis: text={}, options={}, correctAnswer={}",
111+
questionNumber, q.getQuestionText(),
112+
q.getOptions() != null ? q.getOptions().size() : "null",
113+
q.getCorrectAnswer());
114+
return q;
115+
}
116+
return null;
107117
}
108118

109119
public List<QuizQuestion> getAllQuestions(String roomCode) {
@@ -237,4 +247,32 @@ public UUID getUserIdBySessionId(String roomCode, String sessionId) {
237247
.findFirst()
238248
.orElse(null);
239249
}
250+
251+
public void setQuestionStatus(String roomCode, int questionNumber, String status) {
252+
String key = getQuestionStatusKey(roomCode, questionNumber);
253+
redisTemplate.opsForValue().set(key, status, DEFAULT_EXPIRATION_HOURS, TimeUnit.HOURS);
254+
log.info("Set question {} status to {} for room {}", questionNumber, status, roomCode);
255+
}
256+
257+
public String getQuestionStatus(String roomCode, int questionNumber) {
258+
String key = getQuestionStatusKey(roomCode, questionNumber);
259+
Object status = redisTemplate.opsForValue().get(key);
260+
return status != null ? (String) status : "ACTIVE";
261+
}
262+
263+
public Map<Integer, Integer> getAnswerStatistics(String roomCode, int questionNumber) {
264+
List<QuizAnswer> answers = getAnswersForQuestion(roomCode, questionNumber);
265+
Map<Integer, Integer> statistics = new HashMap<>();
266+
267+
for (QuizAnswer answer : answers) {
268+
int answerIndex = answer.getAnswerIndex();
269+
statistics.put(answerIndex, statistics.getOrDefault(answerIndex, 0) + 1);
270+
}
271+
272+
return statistics;
273+
}
274+
275+
private String getQuestionStatusKey(String roomCode, int questionNumber) {
276+
return QUESTION_STATUS_KEY_PREFIX + roomCode + QUESTION_STATUS_KEY_SUFFIX + ":" + questionNumber;
277+
}
240278
}

‎src/main/java/hello/cluebackend/presentation/websocket/quizbattle/QuizBattleWebSocketController.java‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,52 @@ public void submitAnswer(
243243
}
244244
}
245245

246+
@MessageMapping("/quiz/reveal-answer/{roomCode}")
247+
public void revealAnswer(
248+
@DestinationVariable String roomCode,
249+
SimpMessageHeaderAccessor headerAccessor
250+
) {
251+
try {
252+
UUID userId = getUserIdFromHeader(headerAccessor);
253+
QuizRoom room = quizBattleService.getRoom(roomCode);
254+
255+
if (!room.isHost(userId)) {
256+
throw new IllegalStateException("Only the host can reveal answers");
257+
}
258+
259+
Integer currentQuestionNum = quizBattleService.getCurrentQuestionNumber(roomCode);
260+
if (currentQuestionNum == null) {
261+
throw new IllegalStateException("No active question");
262+
}
263+
264+
quizTimerService.cancelQuestionTimer(roomCode, currentQuestionNum);
265+
266+
Map<String, Object> result = quizBattleService.revealAnswer(roomCode, currentQuestionNum);
267+
268+
AnswerRevealMessage message = AnswerRevealMessage.builder()
269+
.questionNumber(currentQuestionNum)
270+
.correctAnswer((Integer) result.get("correctAnswer"))
271+
.explanation((String) result.get("explanation"))
272+
.statistics((Map<Integer, Integer>) result.get("statistics"))
273+
.totalAnswers((Integer) result.get("totalAnswers"))
274+
.status("success")
275+
.message("Answer revealed")
276+
.build();
277+
278+
messagingTemplate.convertAndSend("/topic/quiz/" + roomCode + "/game", message);
279+
280+
log.info("Answer revealed for question {} in room {}", currentQuestionNum, roomCode);
281+
282+
} catch (Exception e) {
283+
log.error("Error revealing answer", e);
284+
ErrorMessage error = ErrorMessage.builder()
285+
.status("error")
286+
.message(e.getMessage())
287+
.build();
288+
messagingTemplate.convertAndSend("/topic/quiz/" + roomCode + "/game", error);
289+
}
290+
}
291+
246292
@MessageMapping("/quiz/next/{roomCode}")
247293
public void nextQuestion(
248294
@DestinationVariable String roomCode,
@@ -366,6 +412,8 @@ public void cancelRoom(
366412
}
367413

368414
private void sendQuestionToRoom(String roomCode, QuizQuestion question) {
415+
quizBattleService.setQuestionActive(roomCode, question.getQuestionNumber());
416+
369417
QuizQuestionMessage questionMessage = QuizQuestionMessage.builder()
370418
.questionNumber(question.getQuestionNumber())
371419
.questionText(question.getQuestionText())
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package hello.cluebackend.presentation.websocket.quizbattle.dto;
2+
3+
import lombok.*;
4+
5+
import java.util.Map;
6+
7+
@Getter
8+
@Setter
9+
@Builder
10+
@AllArgsConstructor
11+
@NoArgsConstructor
12+
public class AnswerRevealMessage {
13+
private Integer questionNumber;
14+
private Integer correctAnswer;
15+
private String explanation;
16+
private Map<Integer, Integer> statistics;
17+
private Integer totalAnswers;
18+
private String status;
19+
private String message;
20+
}

0 commit comments

Comments
 (0)