diff --git a/src/main/java/com/going/server/domain/quiz/dto/ConnectQuizDto.java b/src/main/java/com/going/server/domain/quiz/dto/ConnectQuizDto.java index 564dff5..b2ebabc 100644 --- a/src/main/java/com/going/server/domain/quiz/dto/ConnectQuizDto.java +++ b/src/main/java/com/going/server/domain/quiz/dto/ConnectQuizDto.java @@ -1,9 +1,22 @@ package com.going.server.domain.quiz.dto; +import com.going.server.domain.graph.dto.KnowledgeGraphDto; import lombok.Builder; import lombok.Getter; +import java.util.List; + @Builder @Getter public class ConnectQuizDto { + private KnowledgeGraphDto knowledgeGraph; // 보여줄 지식 그래프 + private List quizList; + + @Builder + @Getter + public static class ConnectQuiz{ + private String questionTargetId; // ? 띄울 노드 id + private List shuffledOptions; // 문제 리스트 + private String answer; // 정답 + } } diff --git a/src/main/java/com/going/server/domain/quiz/generate/ConnectQuizGenerator.java b/src/main/java/com/going/server/domain/quiz/generate/ConnectQuizGenerator.java new file mode 100644 index 0000000..cf04cef --- /dev/null +++ b/src/main/java/com/going/server/domain/quiz/generate/ConnectQuizGenerator.java @@ -0,0 +1,99 @@ +package com.going.server.domain.quiz.generate; + +import com.going.server.domain.graph.dto.EdgeDto; +import com.going.server.domain.graph.dto.KnowledgeGraphDto; +import com.going.server.domain.graph.dto.NodeDto; +import com.going.server.domain.graph.entity.Graph; +import com.going.server.domain.graph.entity.GraphEdge; +import com.going.server.domain.graph.entity.GraphNode; +import com.going.server.domain.quiz.dto.ConnectQuizDto; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +public class ConnectQuizGenerator implements QuizGenerator { + + @Override + public ConnectQuizDto generate(Graph graph) { + + // 1. 지식그래프 조회 + List nodeDtoList = new ArrayList<>(); + List edgeDtoList = new ArrayList<>(); + + for (GraphNode node : graph.getNodes()) { + NodeDto nodeDto = NodeDto.from(node, null); + nodeDtoList.add(nodeDto); + + if (node.getEdges() != null) { + for (GraphEdge edge : node.getEdges()) { + EdgeDto edgeDto = EdgeDto.from(edge.getSource(),edge.getTarget().getNodeId().toString(),edge.getLabel()); + edgeDtoList.add(edgeDto); + } + } + } + + // 2. 문제 생성 + Random random = new Random(); + // 최종 문제 리스트 + List quizList = new ArrayList<>(); + // 이미 사용한 노드 Id 기록용 (중복 방지) + Set usedNodeIds = new HashSet<>(); + + // 문제 3개 만들기 + for (int i = 0; i < 3; i++) { + createConnectQuiz(random, nodeDtoList, quizList, usedNodeIds); + } + + // 3. 반환 + return ConnectQuizDto.builder() + .knowledgeGraph(KnowledgeGraphDto.of(nodeDtoList, edgeDtoList)) + .quizList(quizList) + .build(); + } + + // connect 퀴즈 문제 생성 + private static void createConnectQuiz(Random random, List nodeDtoList, List quizList, Set usedNodeIndices) { + if(usedNodeIndices.size() >= nodeDtoList.size()) { + // 모든 노드를 다 사용했으면 추가 생성 불가 + return; + } + + int questionTargetId; + + // nodeDtoList 중 1개의 id로 랜덤 선택 (중복 방지) + do { + questionTargetId = random.nextInt(nodeDtoList.size()); + } while (usedNodeIndices.contains(questionTargetId)); + + NodeDto targetNode = nodeDtoList.get(questionTargetId); + usedNodeIndices.add(questionTargetId); // 사용한 Id 추가 + + // 정답 + String answer = targetNode.getLabel(); + + // 정답 포함 5개 보기 생성 + Set options = new HashSet<>(); + options.add(answer); // 정답 보기 추가 + + while (options.size() < 5) { // 랜덤 보기 추가 + int randomIndex = random.nextInt(nodeDtoList.size()); + String option = nodeDtoList.get(randomIndex).getLabel(); + options.add(option); + } + + // 보기 리스트 랜덤 배치 + List shuffledOptions = new ArrayList<>(options); + Collections.shuffle(shuffledOptions); + + // 문제 하나 생성 + ConnectQuizDto.ConnectQuiz quiz = ConnectQuizDto.ConnectQuiz.builder() + .questionTargetId(String.valueOf(questionTargetId)) + .shuffledOptions(shuffledOptions) + .answer(answer) + .build(); + + // 문제 리스트에 추가 + quizList.add(quiz); + } +} diff --git a/src/main/java/com/going/server/domain/quiz/generate/ListenUpQuizGenerator.java b/src/main/java/com/going/server/domain/quiz/generate/ListenUpQuizGenerator.java new file mode 100644 index 0000000..9ad6f76 --- /dev/null +++ b/src/main/java/com/going/server/domain/quiz/generate/ListenUpQuizGenerator.java @@ -0,0 +1,92 @@ +package com.going.server.domain.quiz.generate; + +import com.going.server.domain.graph.entity.Graph; +import com.going.server.domain.graph.entity.GraphNode; +import com.going.server.domain.quiz.dto.ListenUpQuizDto; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +public class ListenUpQuizGenerator implements QuizGenerator { + + @Override + public ListenUpQuizDto generate(Graph graph) { + Random random = new Random(); + List quizzes = new ArrayList<>(); + Set usedSentences = new HashSet<>(); + List options = new ArrayList<>(); + + // 1. 그래프 노드에서 문장 추출 + for (GraphNode node : graph.getNodes()) { + if (node.getIncludeSentence() == null || node.getIncludeSentence().isBlank()) continue; + + // "." 으로 문장 나누기 + String[] splitSentences = node.getIncludeSentence().split("\\."); + + for (String rawSentence : splitSentences) { + String sentence = rawSentence.trim(); + if (sentence.isBlank()) continue; // 공백은 스킵 + if (usedSentences.contains(sentence)) continue; + + String[] words = sentence.split("\\s+"); + if (words.length < 5) continue; // 5단어 미만은 스킵 + + options.add(sentence); + } + } + + // 2. 단어 수 기준 정렬 (5단어에 가까운 순서) + options.sort(Comparator.comparingInt( + s -> Math.abs(s.trim().split("\\s+").length - 5) + )); + + int count = 0; + + for (String sentence : options) { + if (count >= 3) break; + + String[] words = sentence.split("\\s+"); + + List answer = new ArrayList<>(); + + if (words.length == 5) { // 5단어면 그대로 + answer = Arrays.asList(words); + } else { + // 6단어 이상이면 랜덤하게 5개로 압축 + int mergeCount = words.length - 5; // 합쳐야 할 횟수 + List wordList = new ArrayList<>(Arrays.asList(words)); + + for (int i = 0; i < mergeCount; i++) { + int mergeIdx = random.nextInt(wordList.size() - 1); // 마지막 단어는 제외 + String merged = wordList.get(mergeIdx) + " " + wordList.get(mergeIdx + 1); + wordList.set(mergeIdx, merged); + wordList.remove(mergeIdx + 1); + } + answer = wordList; + } + + if (answer.size() != 5) continue; // 안전망 + + List shuffled = new ArrayList<>(answer); + Collections.shuffle(shuffled, random); + + // 퀴즈 생성 + ListenUpQuizDto.ListenUpQuiz quiz = ListenUpQuizDto.ListenUpQuiz.builder() + .answer(answer) + .shuffled(shuffled) + .description(sentence) // 이 문장 전체가 TTS로 읽힐 문장 + .build(); + + quizzes.add(quiz); + usedSentences.add(sentence); + count++; + } + + // 최종 퀴즈 DTO에 담아서 번환 + return ListenUpQuizDto.builder() + .quizzes(quizzes) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/going/server/domain/quiz/generate/PictureQuizGenerator.java b/src/main/java/com/going/server/domain/quiz/generate/PictureQuizGenerator.java new file mode 100644 index 0000000..ad76e01 --- /dev/null +++ b/src/main/java/com/going/server/domain/quiz/generate/PictureQuizGenerator.java @@ -0,0 +1,15 @@ +package com.going.server.domain.quiz.generate; + +import com.going.server.domain.graph.entity.Graph; +import com.going.server.domain.quiz.dto.PictureQuizDto; +import org.springframework.stereotype.Component; + +@Component +public class PictureQuizGenerator implements QuizGenerator { + + @Override + public PictureQuizDto generate(Graph graph) { + // TODO: picture 퀴즈 생성 로직 + return null; + } +} diff --git a/src/main/java/com/going/server/domain/quiz/generate/QuizGenerator.java b/src/main/java/com/going/server/domain/quiz/generate/QuizGenerator.java new file mode 100644 index 0000000..e6e576e --- /dev/null +++ b/src/main/java/com/going/server/domain/quiz/generate/QuizGenerator.java @@ -0,0 +1,7 @@ +package com.going.server.domain.quiz.generate; + +import com.going.server.domain.graph.entity.Graph; + +public interface QuizGenerator { + T generate(Graph graph); +} diff --git a/src/main/java/com/going/server/domain/quiz/service/QuizServiceImpl.java b/src/main/java/com/going/server/domain/quiz/service/QuizServiceImpl.java index 1606f34..fe6da9b 100644 --- a/src/main/java/com/going/server/domain/quiz/service/QuizServiceImpl.java +++ b/src/main/java/com/going/server/domain/quiz/service/QuizServiceImpl.java @@ -1,13 +1,20 @@ package com.going.server.domain.quiz.service; +import com.going.server.domain.graph.dto.EdgeDto; +import com.going.server.domain.graph.dto.KnowledgeGraphDto; +import com.going.server.domain.graph.dto.NodeDto; import com.going.server.domain.graph.entity.Graph; +import com.going.server.domain.graph.entity.GraphEdge; import com.going.server.domain.graph.entity.GraphNode; -import com.going.server.domain.graph.exception.GraphNotFoundException; import com.going.server.domain.graph.repository.GraphRepository; import com.going.server.domain.quiz.dto.ConnectQuizDto; import com.going.server.domain.quiz.dto.ListenUpQuizDto; import com.going.server.domain.quiz.dto.PictureQuizDto; import com.going.server.domain.quiz.dto.QuizCreateResponseDto; +import com.going.server.domain.quiz.generate.ConnectQuizGenerator; +import com.going.server.domain.quiz.generate.ListenUpQuizGenerator; +import com.going.server.domain.quiz.generate.PictureQuizGenerator; +import com.going.server.domain.quiz.generate.QuizGenerator; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; @@ -17,124 +24,24 @@ @AllArgsConstructor public class QuizServiceImpl implements QuizService{ private final GraphRepository graphRepository; + private final ListenUpQuizGenerator listenUpQuizGenerator; + private final ConnectQuizGenerator connectQuizGenerator; + private final PictureQuizGenerator pictureQuizGenerator; @Override public QuizCreateResponseDto quizCreate(String graphIdStr, String mode) { Long graphId = Long.valueOf(graphIdStr); // 404 : 지식그래프 찾을 수 없음 - Graph graph = graphRepository.findById(graphId) - .orElseThrow(GraphNotFoundException::new); + Graph graph = graphRepository.getByGraph(graphId); - Object quizDto = null; - - switch (mode) { - case "listenUp": - quizDto = listenUpQuizCreate(graph); - break; - case "connect": - quizDto = connectQuizCreate(graph); - break; - case "picture": - quizDto = pictureQuizCreate(graph); - break; - default: - // TODO : 퀴즈 모드 관련 예외처리 필요 - } + Object quizDto = switch (mode) { + case "listenUp" -> listenUpQuizGenerator.generate(graph); + case "connect" -> connectQuizGenerator.generate(graph); + case "picture" -> pictureQuizGenerator.generate(graph); + default -> throw new IllegalArgumentException("지원하지 않는 모드입니다: " + mode); + }; return new QuizCreateResponseDto<>(graphIdStr, mode, quizDto); } - - // listenUp 퀴즈 생성 메서드 - private ListenUpQuizDto listenUpQuizCreate(Graph graph) { - Random random = new Random(); - List quizzes = new ArrayList<>(); - Set usedSentences = new HashSet<>(); - List candidates = new ArrayList<>(); - - // 1. 그래프 노드에서 문장 추출 - for (GraphNode node : graph.getNodes()) { - if (node.getIncludeSentence() == null || node.getIncludeSentence().isBlank()) continue; - - // "." 으로 문장 나누기 - String[] splitSentences = node.getIncludeSentence().split("\\."); - - for (String rawSentence : splitSentences) { - String sentence = rawSentence.trim(); - if (sentence.isBlank()) continue; // 공백은 스킵 - if (usedSentences.contains(sentence)) continue; - - String[] words = sentence.split("\\s+"); - if (words.length < 5) continue; // 5단어 미만은 스킵 - - candidates.add(sentence); - } - } - - // 2. 단어 수 기준 정렬 (5단어에 가까운 순서) - candidates.sort(Comparator.comparingInt( - s -> Math.abs(s.trim().split("\\s+").length - 5) - )); - - int count = 0; - - for (String sentence : candidates) { - if (count >= 3) break; - - String[] words = sentence.split("\\s+"); - - List answer = new ArrayList<>(); - - if (words.length == 5) { // 5단어면 그대로 - answer = Arrays.asList(words); - } else { - // 6단어 이상이면 랜덤하게 5개로 압축 - int mergeCount = words.length - 5; // 합쳐야 할 횟수 - List wordList = new ArrayList<>(Arrays.asList(words)); - - for (int i = 0; i < mergeCount; i++) { - int mergeIdx = random.nextInt(wordList.size() - 1); // 마지막 단어는 제외 - String merged = wordList.get(mergeIdx) + " " + wordList.get(mergeIdx + 1); - wordList.set(mergeIdx, merged); - wordList.remove(mergeIdx + 1); - } - answer = wordList; - } - - if (answer.size() != 5) continue; // 안전망 - - List shuffled = new ArrayList<>(answer); - Collections.shuffle(shuffled, random); - - // 퀴즈 생성 - ListenUpQuizDto.ListenUpQuiz quiz = ListenUpQuizDto.ListenUpQuiz.builder() - .answer(answer) - .shuffled(shuffled) - .description(sentence) // 이 문장 전체가 TTS로 읽힐 문장 - .build(); - - quizzes.add(quiz); - usedSentences.add(sentence); - count++; - } - - // 최종 퀴즈 DTO에 담아서 번환 - return ListenUpQuizDto.builder() - .quizzes(quizzes) - .build(); - } - - // connect 퀴즈 생성 메서드 - private ConnectQuizDto connectQuizCreate(Graph graph) { - // TODO : connect 퀴즈 생성 로직 작성 - return ConnectQuizDto.builder() - .build(); - } - - // picture 퀴즈 생성 메서드 - private PictureQuizDto pictureQuizCreate(Graph graph) { - // TODO : picture 퀴즈 생성 로직 작성 - return PictureQuizDto.builder() - .build(); - } } \ No newline at end of file