Skip to content

Commit d5bb6bc

Browse files
authored
Merge pull request #42 from AI-Tutor-2024/develop
[ADD] 문제 생성 + 저장 API 하나로 결합하기
2 parents d47fd91 + 70d67f1 commit d5bb6bc

File tree

5 files changed

+104
-16
lines changed

5 files changed

+104
-16
lines changed

src/main/java/com/example/ai_tutor/domain/practice/application/ProfessorPracticeService.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,77 @@ private User getUser(UserPrincipal userPrincipal) {
228228
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
229229
}
230230

231+
@Transactional
232+
public Mono<ResponseEntity<ApiResponse<List<ProfessorPracticeRes>>>> generateAndSavePractice(
233+
UserPrincipal userPrincipal, Long noteId, CreatePracticeReq createPracticeReq) {
234+
235+
// 유저와 교수 인증 (예외 발생 시 중단)
236+
validateUser(userPrincipal);
237+
validateProfessor(userPrincipal);
238+
239+
// 비동기로 노트 조회 (블로킹 방지)
240+
return getNoteAsync(noteId)
241+
.flatMap(note -> {
242+
String summary = note.getSummary().getContent();
243+
int practiceSize = determinePracticeSize(createPracticeReq.getPracticeSize());
244+
245+
Mono<List<CreatePracticeRes>> practiceMono;
246+
if ("BOTH".equalsIgnoreCase(createPracticeReq.getType())) {
247+
int oxCount = calculateOxCount(practiceSize);
248+
int shortCount = practiceSize - oxCount;
249+
250+
Mono<List<CreatePracticeRes>> oxMono = quizGeneratorService.generateQuestions(summary, oxCount, "OX", 1);
251+
Mono<List<CreatePracticeRes>> shortMono = quizGeneratorService.generateQuestions(summary, shortCount, "SHORT", oxCount + 1);
252+
practiceMono = Mono.zip(oxMono, shortMono)
253+
.map(tuple -> combineQuestions(tuple.getT1(), tuple.getT2()));
254+
} else {
255+
practiceMono = quizGeneratorService.generateQuestions(summary, practiceSize, createPracticeReq.getType(), 1);
256+
}
257+
258+
// 기존 sequence 최대값 조회 → Mono로 감싸서 flatMap 안에서 사용
259+
return Mono.fromCallable(() -> practiceRepository.findMaxSequenceByNoteId(noteId))
260+
.subscribeOn(Schedulers.boundedElastic())
261+
.flatMap(lastSequence -> practiceMono.flatMap(practices -> {
262+
263+
int startSequence = lastSequence + 1; // 기존 마지막 번호 다음부터 시작
264+
List<Practice> entities = new ArrayList<>();
265+
266+
for (int i = 0; i < practices.size(); i++) {
267+
CreatePracticeRes p = practices.get(i);
268+
PracticeType type = PracticeType.valueOf(p.getPracticeType());
269+
270+
entities.add(Practice.builder()
271+
.note(note)
272+
.sequence(startSequence + i)
273+
.content(p.getContent())
274+
.result(p.getResult())
275+
.solution(p.getSolution())
276+
.additionalResults(type == PracticeType.OX ? null : p.getAdditionalResults())
277+
.practiceType(type)
278+
.build());
279+
}
280+
281+
return Mono.fromCallable(() -> {
282+
practiceRepository.saveAll(entities);
283+
return entities;
284+
}).subscribeOn(Schedulers.boundedElastic())
285+
.map(saved -> {
286+
List<ProfessorPracticeRes> responses = saved.stream()
287+
.map(this::toResponse)
288+
.toList();
289+
290+
ApiResponse<List<ProfessorPracticeRes>> api = ApiResponse.<List<ProfessorPracticeRes>>builder()
291+
.check(true)
292+
.information(responses)
293+
.build();
294+
295+
return ResponseEntity.ok(api);
296+
});}));
297+
});
298+
299+
}
300+
301+
231302

232303
/** Practice → ProfessorPracticeRes 매핑용 */
233304
private ProfessorPracticeRes toResponse(Practice p) {

src/main/java/com/example/ai_tutor/domain/practice/application/QuizGeneratorService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
import lombok.RequiredArgsConstructor;
77
import lombok.extern.slf4j.Slf4j;
88
import org.springframework.stereotype.Service;
9+
import org.springframework.web.reactive.function.client.WebClientResponseException;
910
import reactor.core.publisher.Mono;
11+
import reactor.util.retry.Retry;
1012

13+
import java.time.Duration;
1114
import java.util.ArrayList;
1215
import java.util.List;
1316

@@ -24,12 +27,12 @@ public Mono<List<CreatePracticeRes>> generateQuestions(String summary, int size,
2427
log.info("GPT API 호출하여 문제를 생성합니다.");
2528
// 비동기 방식으로 GPT API 호출
2629
return gptService.callChatGpt(prompt)
30+
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2))
31+
.filter(e -> e instanceof WebClientResponseException.TooManyRequests))
2732
.flatMap(response -> {
2833
if (response == null) {
2934
return Mono.error(new RuntimeException("GPT API 응답이 없습니다."));
3035
}
31-
32-
// 응답 파싱하여 List<CreatePracticeRes> 반환
3336
List<CreatePracticeRes> practiceResList = parseResponse(response, num, type);
3437
return Mono.just(practiceResList);
3538
})

src/main/java/com/example/ai_tutor/domain/practice/domain/repository/PracticeRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.example.ai_tutor.domain.note.domain.Note;
44
import com.example.ai_tutor.domain.practice.domain.Practice;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
68
import org.springframework.stereotype.Repository;
79

810
import java.util.List;
@@ -20,4 +22,8 @@ public interface PracticeRepository extends JpaRepository<Practice, Long> {
2022

2123
List<Practice> findByNoteOrderBySequenceAsc(Note note);
2224

25+
@Query("SELECT COALESCE(MAX(p.sequence), 0) FROM Practice p WHERE p.note.noteId = :noteId")
26+
int findMaxSequenceByNoteId(@Param("noteId") Long noteId);
27+
28+
2329
}

src/main/java/com/example/ai_tutor/domain/practice/dto/response/CreatePracticeRes.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@
1414
@AllArgsConstructor
1515
@NoArgsConstructor
1616
public class CreatePracticeRes {
17+
@Schema(type = "Long", example ="1", description="문제의 id입니다.")
18+
private Long practiceId;
1719

1820
@Schema(type = "int", example ="1", description="문제의 번호입니다.")
1921
private int practiceNumber;
2022

2123
@Schema(type = "String", example ="광복절의 중앙경축식은 매년 서울에서만 거행된다.", description="문제의 내용입니다.")
2224
private String content;
2325

26+
// 추가 답안 (교수자가 입력할 시 사용)
27+
@ArraySchema(schema = @Schema(type = "String", example ="[Answer, ANSWER, answer, 정답]", description="문제의 추가 인정 답안으로, 교수자가 직접 작성하며 단답형 문제에만 해당합니다."))
28+
private List<String> additionalResults;
29+
2430
// 문제 답안
2531
@Schema(type = "String", example ="X",
2632
description="문제의 답안입니다. OX문제의 경우 O 또는 X, 객관식 문제의 경우 A, B, C, D입니다.")
@@ -30,6 +36,6 @@ public class CreatePracticeRes {
3036
private String solution;
3137

3238
// 문제 타입
33-
@Schema(type = "String", example ="OX", description="문제의 타입입니다. OX, MULTIPLE")
39+
@Schema(type = "String", example ="OX", description="문제의 타입입니다. OX, SHORT")
3440
private String practiceType;
35-
}
41+
}

src/main/java/com/example/ai_tutor/domain/practice/presentation/ProfessorPracticeController.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,32 +38,34 @@ public class ProfessorPracticeController {
3838
private final ProfessorPracticeService professorPracticeService;
3939

4040
@Operation(
41-
summary = "요약본에 대한 문제 생성",
41+
summary = "요약본에 대한 문제 생성 후 저장",
4242
security = { @SecurityRequirement(name = "BearerAuth") },
43-
description = "이전에 요약본 생성 API를 통해 저장한 요약본 데이터를 기반으로 문제를 생성합니다." +
44-
"이는 CreatePracticeReq를 참고하여 문제를 생성하고 파일을 업로드합니다. " +
45-
"주의할 점은 이 요청은 응답을 DB에 저장하지 않습니다. 이는 응답만 할 뿐, 이후 선택된 문제들만 문제 저장 API를 요청하여 저장해주세요.",
43+
description = "요약본 데이터를 기반으로 문제를 생성하고, 생성된 문제를 DB에 곧바로 저장합니다. " +
44+
"기존 두 API(generate + save)를 하나로 통합한 API입니다.",
4645
responses = {
47-
@ApiResponse(responseCode = "200", description = "Practice 문제 생성 성공",
46+
@ApiResponse(responseCode = "200", description = "Practice 문제 생성 및 저장 성공",
4847
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class))),
4948
@ApiResponse(responseCode = "400", description = "잘못된 요청",
5049
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class))),
5150
@ApiResponse(responseCode = "500", description = "서버 오류",
5251
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class)))
53-
})
54-
@PostMapping(value = "/{noteId}/new")
55-
public Mono<ResponseEntity<com.example.ai_tutor.global.payload.ApiResponse<CreatePracticeListRes>>> generatePractice(
56-
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
57-
@Parameter(description = "note의 id를 입력해주세요", required = true) @PathVariable Long noteId,
52+
})
53+
@PostMapping(value = "/{noteId}/generate-and-save")
54+
public Mono<ResponseEntity<com.example.ai_tutor.global.payload.ApiResponse<List<ProfessorPracticeRes>>>> generateAndSavePractice(
55+
@Parameter(description = "Access Token을 입력해주세요.", required = true)
56+
@AuthenticationPrincipal UserPrincipal userPrincipal,
57+
58+
@Parameter(description = "노트의 ID를 입력해주세요", required = true)
59+
@PathVariable Long noteId,
60+
5861
@io.swagger.v3.oas.annotations.parameters.RequestBody(
5962
description = "Schemas의 CreatePracticeReq를 참고해주세요",
6063
required = true,
6164
content = @Content(schema = @Schema(implementation = CreatePracticeReq.class))
6265
)
6366
@RequestBody CreatePracticeReq createPracticeReq
6467
) {
65-
return professorPracticeService.generatePractice(userPrincipal, noteId, createPracticeReq)
66-
.map(ResponseEntity::ok);
68+
return professorPracticeService.generateAndSavePractice(userPrincipal, noteId, createPracticeReq);
6769
}
6870

6971
// 문제 저장

0 commit comments

Comments
 (0)