Skip to content

Commit d64c04d

Browse files
authored
Merge pull request #82 from capgoing/feat/#68
✨[feat] Connect Quiz 생성 로직 구현 & ♻️[refact] Quiz 생성 로직 리팩터링(strategy 패턴 적용)
2 parents 99a26a4 + 949a6c1 commit d64c04d

File tree

6 files changed

+244
-111
lines changed

6 files changed

+244
-111
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
package com.going.server.domain.quiz.dto;
22

3+
import com.going.server.domain.graph.dto.KnowledgeGraphDto;
34
import lombok.Builder;
45
import lombok.Getter;
56

7+
import java.util.List;
8+
69
@Builder
710
@Getter
811
public class ConnectQuizDto {
12+
private KnowledgeGraphDto knowledgeGraph; // 보여줄 지식 그래프
13+
private List<ConnectQuiz> quizList;
14+
15+
@Builder
16+
@Getter
17+
public static class ConnectQuiz{
18+
private String questionTargetId; // ? 띄울 노드 id
19+
private List<String> shuffledOptions; // 문제 리스트
20+
private String answer; // 정답
21+
}
922
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.going.server.domain.quiz.generate;
2+
3+
import com.going.server.domain.graph.dto.EdgeDto;
4+
import com.going.server.domain.graph.dto.KnowledgeGraphDto;
5+
import com.going.server.domain.graph.dto.NodeDto;
6+
import com.going.server.domain.graph.entity.Graph;
7+
import com.going.server.domain.graph.entity.GraphEdge;
8+
import com.going.server.domain.graph.entity.GraphNode;
9+
import com.going.server.domain.quiz.dto.ConnectQuizDto;
10+
import org.springframework.stereotype.Component;
11+
12+
import java.util.*;
13+
14+
@Component
15+
public class ConnectQuizGenerator implements QuizGenerator<ConnectQuizDto> {
16+
17+
@Override
18+
public ConnectQuizDto generate(Graph graph) {
19+
20+
// 1. 지식그래프 조회
21+
List<NodeDto> nodeDtoList = new ArrayList<>();
22+
List<EdgeDto> edgeDtoList = new ArrayList<>();
23+
24+
for (GraphNode node : graph.getNodes()) {
25+
NodeDto nodeDto = NodeDto.from(node, null);
26+
nodeDtoList.add(nodeDto);
27+
28+
if (node.getEdges() != null) {
29+
for (GraphEdge edge : node.getEdges()) {
30+
EdgeDto edgeDto = EdgeDto.from(edge.getSource(),edge.getTarget().getNodeId().toString(),edge.getLabel());
31+
edgeDtoList.add(edgeDto);
32+
}
33+
}
34+
}
35+
36+
// 2. 문제 생성
37+
Random random = new Random();
38+
// 최종 문제 리스트
39+
List<ConnectQuizDto.ConnectQuiz> quizList = new ArrayList<>();
40+
// 이미 사용한 노드 Id 기록용 (중복 방지)
41+
Set<Integer> usedNodeIds = new HashSet<>();
42+
43+
// 문제 3개 만들기
44+
for (int i = 0; i < 3; i++) {
45+
createConnectQuiz(random, nodeDtoList, quizList, usedNodeIds);
46+
}
47+
48+
// 3. 반환
49+
return ConnectQuizDto.builder()
50+
.knowledgeGraph(KnowledgeGraphDto.of(nodeDtoList, edgeDtoList))
51+
.quizList(quizList)
52+
.build();
53+
}
54+
55+
// connect 퀴즈 문제 생성
56+
private static void createConnectQuiz(Random random, List<NodeDto> nodeDtoList, List<ConnectQuizDto.ConnectQuiz> quizList, Set<Integer> usedNodeIndices) {
57+
if(usedNodeIndices.size() >= nodeDtoList.size()) {
58+
// 모든 노드를 다 사용했으면 추가 생성 불가
59+
return;
60+
}
61+
62+
int questionTargetId;
63+
64+
// nodeDtoList 중 1개의 id로 랜덤 선택 (중복 방지)
65+
do {
66+
questionTargetId = random.nextInt(nodeDtoList.size());
67+
} while (usedNodeIndices.contains(questionTargetId));
68+
69+
NodeDto targetNode = nodeDtoList.get(questionTargetId);
70+
usedNodeIndices.add(questionTargetId); // 사용한 Id 추가
71+
72+
// 정답
73+
String answer = targetNode.getLabel();
74+
75+
// 정답 포함 5개 보기 생성
76+
Set<String> options = new HashSet<>();
77+
options.add(answer); // 정답 보기 추가
78+
79+
while (options.size() < 5) { // 랜덤 보기 추가
80+
int randomIndex = random.nextInt(nodeDtoList.size());
81+
String option = nodeDtoList.get(randomIndex).getLabel();
82+
options.add(option);
83+
}
84+
85+
// 보기 리스트 랜덤 배치
86+
List<String> shuffledOptions = new ArrayList<>(options);
87+
Collections.shuffle(shuffledOptions);
88+
89+
// 문제 하나 생성
90+
ConnectQuizDto.ConnectQuiz quiz = ConnectQuizDto.ConnectQuiz.builder()
91+
.questionTargetId(String.valueOf(questionTargetId))
92+
.shuffledOptions(shuffledOptions)
93+
.answer(answer)
94+
.build();
95+
96+
// 문제 리스트에 추가
97+
quizList.add(quiz);
98+
}
99+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.going.server.domain.quiz.generate;
2+
3+
import com.going.server.domain.graph.entity.Graph;
4+
import com.going.server.domain.graph.entity.GraphNode;
5+
import com.going.server.domain.quiz.dto.ListenUpQuizDto;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.util.*;
9+
10+
@Component
11+
public class ListenUpQuizGenerator implements QuizGenerator<ListenUpQuizDto> {
12+
13+
@Override
14+
public ListenUpQuizDto generate(Graph graph) {
15+
Random random = new Random();
16+
List<ListenUpQuizDto.ListenUpQuiz> quizzes = new ArrayList<>();
17+
Set<String> usedSentences = new HashSet<>();
18+
List<String> options = new ArrayList<>();
19+
20+
// 1. 그래프 노드에서 문장 추출
21+
for (GraphNode node : graph.getNodes()) {
22+
if (node.getIncludeSentence() == null || node.getIncludeSentence().isBlank()) continue;
23+
24+
// "." 으로 문장 나누기
25+
String[] splitSentences = node.getIncludeSentence().split("\\.");
26+
27+
for (String rawSentence : splitSentences) {
28+
String sentence = rawSentence.trim();
29+
if (sentence.isBlank()) continue; // 공백은 스킵
30+
if (usedSentences.contains(sentence)) continue;
31+
32+
String[] words = sentence.split("\\s+");
33+
if (words.length < 5) continue; // 5단어 미만은 스킵
34+
35+
options.add(sentence);
36+
}
37+
}
38+
39+
// 2. 단어 수 기준 정렬 (5단어에 가까운 순서)
40+
options.sort(Comparator.comparingInt(
41+
s -> Math.abs(s.trim().split("\\s+").length - 5)
42+
));
43+
44+
int count = 0;
45+
46+
for (String sentence : options) {
47+
if (count >= 3) break;
48+
49+
String[] words = sentence.split("\\s+");
50+
51+
List<String> answer = new ArrayList<>();
52+
53+
if (words.length == 5) { // 5단어면 그대로
54+
answer = Arrays.asList(words);
55+
} else {
56+
// 6단어 이상이면 랜덤하게 5개로 압축
57+
int mergeCount = words.length - 5; // 합쳐야 할 횟수
58+
List<String> wordList = new ArrayList<>(Arrays.asList(words));
59+
60+
for (int i = 0; i < mergeCount; i++) {
61+
int mergeIdx = random.nextInt(wordList.size() - 1); // 마지막 단어는 제외
62+
String merged = wordList.get(mergeIdx) + " " + wordList.get(mergeIdx + 1);
63+
wordList.set(mergeIdx, merged);
64+
wordList.remove(mergeIdx + 1);
65+
}
66+
answer = wordList;
67+
}
68+
69+
if (answer.size() != 5) continue; // 안전망
70+
71+
List<String> shuffled = new ArrayList<>(answer);
72+
Collections.shuffle(shuffled, random);
73+
74+
// 퀴즈 생성
75+
ListenUpQuizDto.ListenUpQuiz quiz = ListenUpQuizDto.ListenUpQuiz.builder()
76+
.answer(answer)
77+
.shuffled(shuffled)
78+
.description(sentence) // 이 문장 전체가 TTS로 읽힐 문장
79+
.build();
80+
81+
quizzes.add(quiz);
82+
usedSentences.add(sentence);
83+
count++;
84+
}
85+
86+
// 최종 퀴즈 DTO에 담아서 번환
87+
return ListenUpQuizDto.builder()
88+
.quizzes(quizzes)
89+
.build();
90+
}
91+
92+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.going.server.domain.quiz.generate;
2+
3+
import com.going.server.domain.graph.entity.Graph;
4+
import com.going.server.domain.quiz.dto.PictureQuizDto;
5+
import org.springframework.stereotype.Component;
6+
7+
@Component
8+
public class PictureQuizGenerator implements QuizGenerator<PictureQuizDto> {
9+
10+
@Override
11+
public PictureQuizDto generate(Graph graph) {
12+
// TODO: picture 퀴즈 생성 로직
13+
return null;
14+
}
15+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.going.server.domain.quiz.generate;
2+
3+
import com.going.server.domain.graph.entity.Graph;
4+
5+
public interface QuizGenerator<T> {
6+
T generate(Graph graph);
7+
}
Lines changed: 18 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package com.going.server.domain.quiz.service;
22

3+
import com.going.server.domain.graph.dto.EdgeDto;
4+
import com.going.server.domain.graph.dto.KnowledgeGraphDto;
5+
import com.going.server.domain.graph.dto.NodeDto;
36
import com.going.server.domain.graph.entity.Graph;
7+
import com.going.server.domain.graph.entity.GraphEdge;
48
import com.going.server.domain.graph.entity.GraphNode;
5-
import com.going.server.domain.graph.exception.GraphNotFoundException;
69
import com.going.server.domain.graph.repository.GraphRepository;
710
import com.going.server.domain.quiz.dto.ConnectQuizDto;
811
import com.going.server.domain.quiz.dto.ListenUpQuizDto;
912
import com.going.server.domain.quiz.dto.PictureQuizDto;
1013
import com.going.server.domain.quiz.dto.QuizCreateResponseDto;
14+
import com.going.server.domain.quiz.generate.ConnectQuizGenerator;
15+
import com.going.server.domain.quiz.generate.ListenUpQuizGenerator;
16+
import com.going.server.domain.quiz.generate.PictureQuizGenerator;
17+
import com.going.server.domain.quiz.generate.QuizGenerator;
1118
import lombok.AllArgsConstructor;
1219
import org.springframework.stereotype.Service;
1320

@@ -17,124 +24,24 @@
1724
@AllArgsConstructor
1825
public class QuizServiceImpl implements QuizService{
1926
private final GraphRepository graphRepository;
27+
private final ListenUpQuizGenerator listenUpQuizGenerator;
28+
private final ConnectQuizGenerator connectQuizGenerator;
29+
private final PictureQuizGenerator pictureQuizGenerator;
2030

2131
@Override
2232
public QuizCreateResponseDto quizCreate(String graphIdStr, String mode) {
2333
Long graphId = Long.valueOf(graphIdStr);
2434

2535
// 404 : 지식그래프 찾을 수 없음
26-
Graph graph = graphRepository.findById(graphId)
27-
.orElseThrow(GraphNotFoundException::new);
36+
Graph graph = graphRepository.getByGraph(graphId);
2837

29-
Object quizDto = null;
30-
31-
switch (mode) {
32-
case "listenUp":
33-
quizDto = listenUpQuizCreate(graph);
34-
break;
35-
case "connect":
36-
quizDto = connectQuizCreate(graph);
37-
break;
38-
case "picture":
39-
quizDto = pictureQuizCreate(graph);
40-
break;
41-
default:
42-
// TODO : 퀴즈 모드 관련 예외처리 필요
43-
}
38+
Object quizDto = switch (mode) {
39+
case "listenUp" -> listenUpQuizGenerator.generate(graph);
40+
case "connect" -> connectQuizGenerator.generate(graph);
41+
case "picture" -> pictureQuizGenerator.generate(graph);
42+
default -> throw new IllegalArgumentException("지원하지 않는 모드입니다: " + mode);
43+
};
4444

4545
return new QuizCreateResponseDto<>(graphIdStr, mode, quizDto);
4646
}
47-
48-
// listenUp 퀴즈 생성 메서드
49-
private ListenUpQuizDto listenUpQuizCreate(Graph graph) {
50-
Random random = new Random();
51-
List<ListenUpQuizDto.ListenUpQuiz> quizzes = new ArrayList<>();
52-
Set<String> usedSentences = new HashSet<>();
53-
List<String> candidates = new ArrayList<>();
54-
55-
// 1. 그래프 노드에서 문장 추출
56-
for (GraphNode node : graph.getNodes()) {
57-
if (node.getIncludeSentence() == null || node.getIncludeSentence().isBlank()) continue;
58-
59-
// "." 으로 문장 나누기
60-
String[] splitSentences = node.getIncludeSentence().split("\\.");
61-
62-
for (String rawSentence : splitSentences) {
63-
String sentence = rawSentence.trim();
64-
if (sentence.isBlank()) continue; // 공백은 스킵
65-
if (usedSentences.contains(sentence)) continue;
66-
67-
String[] words = sentence.split("\\s+");
68-
if (words.length < 5) continue; // 5단어 미만은 스킵
69-
70-
candidates.add(sentence);
71-
}
72-
}
73-
74-
// 2. 단어 수 기준 정렬 (5단어에 가까운 순서)
75-
candidates.sort(Comparator.comparingInt(
76-
s -> Math.abs(s.trim().split("\\s+").length - 5)
77-
));
78-
79-
int count = 0;
80-
81-
for (String sentence : candidates) {
82-
if (count >= 3) break;
83-
84-
String[] words = sentence.split("\\s+");
85-
86-
List<String> answer = new ArrayList<>();
87-
88-
if (words.length == 5) { // 5단어면 그대로
89-
answer = Arrays.asList(words);
90-
} else {
91-
// 6단어 이상이면 랜덤하게 5개로 압축
92-
int mergeCount = words.length - 5; // 합쳐야 할 횟수
93-
List<String> wordList = new ArrayList<>(Arrays.asList(words));
94-
95-
for (int i = 0; i < mergeCount; i++) {
96-
int mergeIdx = random.nextInt(wordList.size() - 1); // 마지막 단어는 제외
97-
String merged = wordList.get(mergeIdx) + " " + wordList.get(mergeIdx + 1);
98-
wordList.set(mergeIdx, merged);
99-
wordList.remove(mergeIdx + 1);
100-
}
101-
answer = wordList;
102-
}
103-
104-
if (answer.size() != 5) continue; // 안전망
105-
106-
List<String> shuffled = new ArrayList<>(answer);
107-
Collections.shuffle(shuffled, random);
108-
109-
// 퀴즈 생성
110-
ListenUpQuizDto.ListenUpQuiz quiz = ListenUpQuizDto.ListenUpQuiz.builder()
111-
.answer(answer)
112-
.shuffled(shuffled)
113-
.description(sentence) // 이 문장 전체가 TTS로 읽힐 문장
114-
.build();
115-
116-
quizzes.add(quiz);
117-
usedSentences.add(sentence);
118-
count++;
119-
}
120-
121-
// 최종 퀴즈 DTO에 담아서 번환
122-
return ListenUpQuizDto.builder()
123-
.quizzes(quizzes)
124-
.build();
125-
}
126-
127-
// connect 퀴즈 생성 메서드
128-
private ConnectQuizDto connectQuizCreate(Graph graph) {
129-
// TODO : connect 퀴즈 생성 로직 작성
130-
return ConnectQuizDto.builder()
131-
.build();
132-
}
133-
134-
// picture 퀴즈 생성 메서드
135-
private PictureQuizDto pictureQuizCreate(Graph graph) {
136-
// TODO : picture 퀴즈 생성 로직 작성
137-
return PictureQuizDto.builder()
138-
.build();
139-
}
14047
}

0 commit comments

Comments
 (0)