Skip to content

Commit 736cfe0

Browse files
committed
[FIX] note 테이블 엔티티 변경 및 프롬포팅 수정
1 parent b00672c commit 736cfe0

File tree

4 files changed

+87
-23
lines changed

4 files changed

+87
-23
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ dependencies {
6363
implementation "io.projectreactor.netty:reactor-netty-core:1.1.0"
6464
implementation "io.projectreactor.netty:reactor-netty-http:1.1.0"
6565

66+
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.1.0'
67+
implementation 'io.github.resilience4j:resilience4j-ratelimiter:2.1.0'
68+
implementation 'io.github.resilience4j:resilience4j-reactor:2.1.0'
69+
6670

6771
compileOnly 'org.projectlombok:lombok'
6872
annotationProcessor 'org.projectlombok:lombok'

src/main/java/com/example/ai_tutor/domain/note/domain/Note.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import lombok.NoArgsConstructor;
1212

1313
@Entity
14-
@Table(name = "Note")
14+
@Table(name = "note")
1515
@NoArgsConstructor
1616
@AllArgsConstructor
1717
@Getter

src/main/java/com/example/ai_tutor/domain/note/presentation/NoteController.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,26 @@ public ResponseEntity<?> createNewNote(
5858
})
5959
@GetMapping()
6060
public ResponseEntity<?> getAllNotes(
61-
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
62-
@Parameter(description = "folder의 id를 입력해주세요", required = true) @PathVariable Long folderId
61+
@Parameter(description = "Access Token을 입력해주세요.", required = false) @AuthenticationPrincipal UserPrincipal userPrincipal,
62+
@Parameter(description = "folder의 id를 입력해주세요") @PathVariable Long folderId
6363

6464
) {
6565
return professorNoteService.getAllNotesByFolder(userPrincipal, folderId);
6666
}
6767

68+
@Operation(summary = "노트 단일 조회 API", description = "강의 노트 목록을 조회하는 API입니다.")
69+
@ApiResponses(value = {
70+
@ApiResponse(responseCode = "200", description = "강의 노트 목록 조회 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = NoteListRes.class) ) } ),
71+
@ApiResponse(responseCode = "400", description = "강의 노트 목록 조회 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ),
72+
})
73+
@GetMapping("/{noteId}")
74+
public ResponseEntity<?> getNote(
75+
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
76+
@Parameter(description = "삭제하려는 note의 id를 입력해주세요", required = true) @PathVariable Long noteId
77+
) {
78+
return professorNoteService.deleteNoteById(userPrincipal, noteId);
79+
}
80+
6881
@Operation(summary = "노트 삭제 API", description = "특정 강의 노트를 삭제하는 API입니다.")
6982
@ApiResponses(value = {
7083
@ApiResponse(responseCode = "200", description = "강의 노트 삭제 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class) ) } ),
@@ -78,6 +91,8 @@ public ResponseEntity<?> deleteNote(
7891
return professorNoteService.deleteNoteById(userPrincipal, noteId);
7992
}
8093

94+
95+
8196
@Operation(summary = "노트 STT 변환 API", description = "노트의 강의 영상을 CLOVA API를 활용하여 STT 변환하는 API입니다. 처음 영상을 올리는 것이라면 이 API를 활용하여 영상을 TEXT로 변환하여야 합니다.")
8297
@ApiResponses(value = {
8398
@ApiResponse(responseCode = "200", description = "STT 변환 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class)) }),

src/main/java/com/example/ai_tutor/domain/summary/application/SummaryService.java

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
import com.example.ai_tutor.global.config.security.token.UserPrincipal;
1212
import com.example.ai_tutor.global.payload.ApiResponse;
1313
import com.fasterxml.jackson.databind.JsonNode;
14+
import io.github.resilience4j.ratelimiter.RateLimiter;
15+
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
16+
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
1417
import lombok.RequiredArgsConstructor;
1518
import lombok.extern.slf4j.Slf4j;
1619
import org.springframework.http.ResponseEntity;
1720
import org.springframework.stereotype.Service;
1821
import org.springframework.transaction.annotation.Transactional;
1922
import org.springframework.web.multipart.MultipartFile;
23+
import org.springframework.web.reactive.function.client.WebClientResponseException;
2024
import reactor.core.publisher.Flux;
2125
import reactor.core.publisher.Mono;
2226

@@ -52,6 +56,16 @@ public class SummaryService {
5256
private static final int CHUNK_TOKEN_SIZE = 7000; // 청크 단위 토큰 수
5357

5458

59+
//
60+
RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
61+
.timeoutDuration(Duration.ofMillis(500)) // 획득 대기 시간
62+
.limitForPeriod(3) // period당 호출 가능 횟수
63+
.limitRefreshPeriod(Duration.ofSeconds(1)) // period 단위 시간
64+
.build();
65+
66+
RateLimiter rateLimiter = RateLimiter.of("gptRateLimiter", rateLimiterConfig);
67+
68+
5569
public Mono<String> processSttAndSummary(MultipartFile file, String keywords, String requirement, Long noteId) {
5670
Note note = noteRepository.findById(noteId)
5771
.orElseThrow(() -> new RuntimeException("해당 노트를 찾을 수 없습니다."));
@@ -106,21 +120,21 @@ private String generatePrompt(String fullText, String keywords, String requireme
106120
// 청크 요약용 프롬프트
107121
promptBuilder.append("""
108122
당신은 전문적인 강의 분석가입니다.
109-
아래 강의 내용을 간결하고 논리적으로 요약하십시오.
110-
123+
아래 강의 내용을 **상세하고 논리적으로 요약**하십시오.
124+
111125
---
112126
### 역할과 목표
113-
- 역할: 주어진 강의 청크를 분석하고, 핵심 개념과 주요 이론만 추출하는 전문가입니다.
114-
- 목표: 최종 요약의 재료로 사용될 일관성 있고 체계적인 부분 요약을 생성하는 것입니다.
115-
127+
- 역할: 주어진 강의 청크를 분석하여 **단일 청크만으로 완전한 내용을 담은 부분 요약**을 생성하는 전문가입니다. \s
128+
- 목표: 각 청크 요약이 최종 요약과 동일한 수준의 완결성을 갖추도록 작성합니다. **최종 통합 시 추가하거나 보완하는 과정이 없음을 전제로 작성합니다.**
129+
116130
---
117131
### 작성 기준
118-
1. 핵심 개념과 이론만 요약하며 불필요한 설명과 반복은 반드시 제거합니다.
119-
2. 모든 문장은 간결하고 논리적이어야 하며, 전문적이고 학문적인 어투로 작성합니다. 각 문장은 '~이다.'로 끝맺습니다.
120-
3. 제공된 키워드와 요구사항은 반드시 반영하고 논리적으로 통합합니다.
121-
4. 결과는 마크다운 형식으로 작성하며, 번호나 리스트를 사용하여 시각적 구분을 명확히 합니다.
122-
5. 요약 결과는 반드시 500단어 이상 800단어 이하로 제한합니다.
123-
132+
1. **핵심 개념, 주요 이론, 사례 및 교수자의 중요한 설명을 절대 누락하지 않고 상세하게 요약합니다.** 특히 해당 강의 주제에서 중요해보이는 사례와 개념을 강조하여 이야기합니다.
133+
2. 불필요한 설명과 반복은 제거하되, **내용이 축소되거나 정보가 손실되지 않도록 주의합니다.**
134+
3. 모든 문장은 간결하고 논리적이어야 하며, 전문적이고 학문적인 어투로 작성합니다. 각 문장은 ‘~이다.’로 끝맺습니다.
135+
4. 제공된 키워드와 요구사항을 반드시 반영하고, 자연스럽게 통합합니다.
136+
5. **핵심 내용과 보조 설명, 사례 등을 시각적으로 구분**하여 명확성을 높입니다. 마크다운 형식을 사용하며, 번호나 리스트를 적극적으로 활용합니다.
137+
6. 요약 결과는 **500단어 이상, 1200단어 이하로 제한**합니다.
124138
---
125139
""");
126140

@@ -151,7 +165,13 @@ private String generatePrompt(String fullText, String keywords, String requireme
151165
당신은 다양한 전공 분야의 강의를 쉽고 명료하게 전달하는 교양서적의 저자입니다.
152166
학습자가 복잡한 개념을 명확히 이해하고, 논리적으로 정리된 지식을 통해 사고를 확장할 수 있도록 돕는 것이 당신의 역할입니다.
153167
154-
※ 주의사항
168+
지금부터 작성할 내용은 청크별 부분 요약본을 종합하여 작성하는 **최종 요약본**입니다.
169+
170+
필수 지침
171+
- 강의의 **모든 핵심 개념, 주요 이론, 사례, 교수자의 강조 의도가 누락되지 않도록 작성하십시오.**
172+
- 작성 후 반드시 검토하여, 정보 누락과 왜곡이 없는지 확인하고 제출하십시오.
173+
- 청크별 요약본을 종합하며, 중복 없이 논리적 흐름과 일관성을 유지하십시오.
174+
155175
- 반드시 제공된 강의 원문(STT 변환 텍스트)의 내용만 사용하여 작성합니다.
156176
- 외부 지식이나 상상, 창작은 절대 포함하지 않습니다.
157177
- 원문에 없는 정보는 작성하지 않습니다.
@@ -186,11 +206,10 @@ private String generatePrompt(String fullText, String keywords, String requireme
186206
187207
4. **형식**
188208
- 명확하고 간결한 문장으로 서술하며, 한 문장은 지나치게 길지 않게 유지합니다.
189-
- 중복 표현은 피하고, 동일한 개념을 반복 설명하지 않습니다.
190-
- 불필요한 감정적 어구나 과도한 수식은 사용하지 않습니다.
209+
- 불필요한 감정적 어구와 중복 표현은 피하고, 동일한 개념을 반복 설명하지 않습니다.
191210
192211
4. **검증**
193-
- 요약을 작성한 후, 이전의 요약본과 작성된 요약본을 다시 확인하여, 핵심 겨냄이 빠짐없이 포함되어있는지와 내용의 왜곡이 없는지 확인하고 보완하시오.
212+
- 요약을 작성한 후, 이전의 요약본과 작성된 요약본을 다시 확인하여, 핵심 개념이 빠짐없이 포함되어있는지와 내용의 왜곡이 없는지 확인하고 보완하시오.
194213
195214
---
196215
@@ -201,6 +220,19 @@ private String generatePrompt(String fullText, String keywords, String requireme
201220
202221
- **개념 설명 예시**
203222
"니체가 언급한 '르상티망'은 억눌린 감정이 왜곡되어 타인에 대한 적대감으로 표출되는 심리 상태를 의미합니다. 강의에서는 이를 현대 사회의 무차별 공격과 같은 현상과 연결지어 설명하고 있습니다."
223+
224+
---
225+
### 수식 작성 기준 (선택적 적용)
226+
1. 강의 내용에 수식이 포함된 경우, 반드시 **LaTeX 문법**을 사용하여 작성합니다.
227+
2. 수식은 문장 내에 `$ ... $` 형태로 인라인으로 작성합니다.
228+
예시: "피타고라스 정리는 $a^2 + b^2 = c^2$이다."
229+
3. 복잡하거나 강조가 필요한 수식은 `$$ ... $$`으로 감싸서 별도의 수식 블록으로 작성합니다.
230+
예시:
231+
$$
232+
E = mc^2
233+
$$
234+
4. 수식이 없는 경우는 수식을 억지로 생성하지 않고, 텍스트 기반 설명만 제공합니다.
235+
5. 수식이 포함될 경우에도 설명과 함께 자연스럽게 통합하여, 논리적인 흐름을 유지합니다.
204236
205237
---
206238
@@ -321,12 +353,13 @@ public Mono<List<String>> summarizeChunks(List<String> chunks, String keywords,
321353
log.info("Chunk 개수: {}", chunks.size());
322354

323355
return Flux.fromIterable(chunks)
324-
.delayElements(Duration.ofMillis(500)) // 1초 간격으로 호출
325-
.flatMapSequential(chunk -> getGptResult(keywords, requirement, chunk, true))
356+
.flatMap(chunk -> getGptResult(keywords, requirement, chunk, true)
357+
.transformDeferred(RateLimiterOperator.of(rateLimiter)) // 비동기 처리
358+
, 2) // 동시 처리 수 조절 가능
326359
.collectList();
327-
328360
}
329361

362+
330363
/**
331364
* 부분 요약을 합쳐 최종 요약을 생성합니다.
332365
*/
@@ -341,15 +374,27 @@ private Mono<String> getGptResult(String keywords, String requirement, String me
341374
validatePromptLength(prompt);
342375

343376
return gptService.callChatGpt(prompt)
344-
.retryWhen(Retry.backoff(3, Duration.ofMillis(500))
345-
.doBeforeRetry(retrySignal -> log.warn("GPT 호출 재시도 {}회", retrySignal.totalRetriesInARow()))
377+
.retryWhen(
378+
Retry.backoff(4, Duration.ofSeconds(1))
379+
.maxBackoff(Duration.ofSeconds(5))
380+
.jitter(0.5) // 최대 50% 랜덤 지연
381+
.filter(this::isTooManyRequests)
382+
.doBeforeRetry(retry -> log.warn("GPT 호출 재시도 {}회", retry.totalRetriesInARow()))
346383
)
347384
.map(gptResponse -> {
348385
JsonNode choices = gptResponse.get("choices");
349386
return choices.get(0).get("message").get("content").asText();
350387
});
351388
}
352389

390+
private boolean isTooManyRequests(Throwable throwable) {
391+
if (throwable instanceof WebClientResponseException.TooManyRequests) {
392+
return true;
393+
}
394+
return false;
395+
}
396+
397+
353398
/**
354399
* 프롬프트 길이를 토큰 수 기준으로 검증합니다.
355400
*/

0 commit comments

Comments
 (0)