Skip to content

Commit a876303

Browse files
authored
Merge pull request #43 from GP-DriveU/refactor/#42/ai-client
[Refactor] AI 서버 통신로직 & AI 문제생성 관련 리펙토링
2 parents ac3f2f5 + 809f34e commit a876303

9 files changed

Lines changed: 195 additions & 186 deletions

File tree

.coderabbit/config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
features:
2+
docstrings: false
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.driveu.server.domain.question.application;
2+
3+
import com.driveu.server.domain.question.dto.request.QuestionCreateRequest;
4+
import com.driveu.server.domain.resource.application.S3Service;
5+
import com.driveu.server.domain.resource.dao.FileRepository;
6+
import com.driveu.server.domain.resource.dao.NoteRepository;
7+
import com.driveu.server.domain.resource.domain.File;
8+
import com.driveu.server.domain.resource.domain.Note;
9+
import com.driveu.server.domain.resource.domain.type.ResourceType;
10+
import jakarta.persistence.EntityNotFoundException;
11+
import lombok.RequiredArgsConstructor;
12+
import org.jetbrains.annotations.NotNull;
13+
import org.springframework.core.io.ByteArrayResource;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.util.LinkedMultiValueMap;
16+
import org.springframework.util.MultiValueMap;
17+
18+
import java.nio.charset.StandardCharsets;
19+
import java.nio.file.Paths;
20+
import java.util.List;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
public class QuestionResourceService {
25+
26+
private final NoteRepository noteRepository;
27+
private final S3Service s3Service;
28+
private final FileRepository fileRepository;
29+
30+
public @NotNull MultiValueMap<String, Object> createRequestBody(List<QuestionCreateRequest> requestList) {
31+
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
32+
33+
for (QuestionCreateRequest request : requestList) {
34+
if (request.getType().equals(ResourceType.FILE.name())){
35+
// 파일에 저장된 s3Path로 S3에서 가져오기
36+
addFileFromS3(request, body);
37+
38+
} else if(request.getType().equals(ResourceType.NOTE.name())) {
39+
// 노트 컨텐츠로 파일 만들기
40+
addNote(request, body);
41+
}
42+
else {
43+
throw new IllegalArgumentException("잘못된 resource type 입니다.");
44+
}
45+
}
46+
return body;
47+
}
48+
49+
private void addNote(QuestionCreateRequest request, MultiValueMap<String, Object> body) {
50+
Note note = noteRepository.findById(request.getResourceId())
51+
.orElseThrow(() -> new EntityNotFoundException("Note not found: " + request.getResourceId()));
52+
53+
String markdown = note.getContent();
54+
55+
// String → ByteArrayResource (가짜 파일)
56+
ByteArrayResource fileResource = new ByteArrayResource(
57+
markdown.getBytes(StandardCharsets.UTF_8)
58+
) {
59+
@Override
60+
public String getFilename() {
61+
return "note-" + request.getResourceId() + ".md";
62+
}
63+
};
64+
65+
// 같은 폼 필드명("files")에 여러 개를 add하면, 서버 쪽에서는 배열로 받는다.
66+
body.add("files", fileResource);
67+
}
68+
69+
private void addFileFromS3(QuestionCreateRequest request, MultiValueMap<String, Object> body) {
70+
File file = fileRepository.findById(request.getResourceId())
71+
.orElseThrow(() -> new EntityNotFoundException("File not found: " + request.getResourceId()));
72+
73+
String s3Path = file.getS3Path();
74+
String filename = Paths.get(s3Path).getFileName().toString();
75+
ByteArrayResource fileResource = s3Service.getFileAsResource(s3Path, filename);
76+
body.add("files", fileResource);
77+
}
78+
}

src/main/java/com/driveu/server/domain/question/application/QuestionService.java

Lines changed: 7 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package com.driveu.server.domain.question.application;
22

33
import com.amazonaws.services.kms.model.NotFoundException;
4-
import com.amazonaws.services.s3.AmazonS3Client;
5-
import com.amazonaws.services.s3.model.S3Object;
6-
import com.amazonaws.services.s3.model.S3ObjectInputStream;
74
import com.driveu.server.domain.directory.dao.DirectoryRepository;
85
import com.driveu.server.domain.directory.domain.Directory;
96
import com.driveu.server.domain.question.dao.QuestionRepository;
@@ -12,29 +9,18 @@
129
import com.driveu.server.domain.question.dto.request.QuestionCreateRequest;
1310
import com.driveu.server.domain.question.dto.response.QuestionListResponse;
1411
import com.driveu.server.domain.question.dto.response.QuestionResponse;
15-
import com.driveu.server.domain.resource.dao.FileRepository;
16-
import com.driveu.server.domain.resource.dao.NoteRepository;
1712
import com.driveu.server.domain.resource.dao.ResourceDirectoryRepository;
1813
import com.driveu.server.domain.resource.dao.ResourceRepository;
19-
import com.driveu.server.domain.resource.domain.File;
20-
import com.driveu.server.domain.resource.domain.Note;
2114
import com.driveu.server.domain.resource.domain.Resource;
2215
import com.driveu.server.domain.resource.domain.ResourceDirectory;
23-
import jakarta.persistence.EntityNotFoundException;
16+
import com.driveu.server.infra.ai.AiService;
2417
import lombok.RequiredArgsConstructor;
2518
import org.jetbrains.annotations.NotNull;
26-
import org.springframework.beans.factory.annotation.Value;
27-
import org.springframework.core.io.ByteArrayResource;
2819
import org.springframework.http.*;
2920
import org.springframework.stereotype.Service;
3021
import org.springframework.transaction.annotation.Transactional;
31-
import org.springframework.util.LinkedMultiValueMap;
3222
import org.springframework.util.MultiValueMap;
33-
import org.springframework.web.client.RestTemplate;
3423

35-
import java.io.IOException;
36-
import java.nio.charset.StandardCharsets;
37-
import java.nio.file.Paths;
3824
import java.util.HashSet;
3925
import java.util.List;
4026
import java.util.Set;
@@ -49,13 +35,8 @@ public class QuestionService {
4935
private final QuestionResourceRepository questionResourceRepository;
5036
private final ResourceRepository resourceRepository;
5137
private final ResourceDirectoryRepository resourceDirectoryRepository;
52-
private final NoteRepository noteRepository;
53-
private final RestTemplate restTemplate;
54-
private final AmazonS3Client amazonS3Client;
55-
private final FileRepository fileRepository;
56-
57-
@Value("${spring.cloud.aws.s3.bucket}")
58-
private String bucketName;
38+
private final AiService aiService;
39+
private final QuestionResourceService questionResourceService;
5940

6041
@Transactional
6142
public QuestionResponse createQuestion(Long directoryId, List<QuestionCreateRequest> requestList) {
@@ -81,31 +62,8 @@ public QuestionResponse createQuestion(Long directoryId, List<QuestionCreateRequ
8162
int version = getVersion(existingSameQuestions);
8263

8364
// resource type에 따라 파일을 추출해서 multipart/form-data 데이터 형식으로 만듦
84-
MultiValueMap<String, Object> requestBody = createRequestBody(requestList);
85-
86-
HttpHeaders headers = new HttpHeaders();
87-
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
88-
89-
HttpEntity<MultiValueMap<String, Object>> requestEntity =
90-
new HttpEntity<>(requestBody, headers);
91-
92-
// 3) AI 서버 URL (여러 파일을 받는 엔드포인트)
93-
String aiUrl = "http://3.37.182.184:8000/api/ai/generate";
94-
95-
// ResponseEntity<String> 으로 받아서 raw JSON 전체를 꺼냄
96-
ResponseEntity<String> response = restTemplate.exchange(
97-
aiUrl,
98-
HttpMethod.POST,
99-
requestEntity,
100-
String.class
101-
);
102-
if (response.getBody() == null) {
103-
throw new RuntimeException("AI 서버 오류: " + response.getStatusCode());
104-
}
105-
106-
String aiResponse = response.getBody();
107-
108-
System.out.println(aiResponse);
65+
MultiValueMap<String, Object> requestBody = questionResourceService.createRequestBody(requestList);
66+
String aiResponse = aiService.generateQuestion(requestBody);
10967

11068
Question question = Question.of(title, version, aiResponse);
11169
Question savedQuestion = questionRepository.save(question);
@@ -123,67 +81,6 @@ public QuestionResponse createQuestion(Long directoryId, List<QuestionCreateRequ
12381
return QuestionResponse.fromEntity(savedQuestion);
12482
}
12583

126-
private @NotNull MultiValueMap<String, Object> createRequestBody(List<QuestionCreateRequest> requestList) {
127-
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
128-
129-
for (QuestionCreateRequest request : requestList) {
130-
if (request.getType().equals("FILE")){
131-
// 파일에 저장된 s3Path로 S3에서 가져오기
132-
addFileFromS3(request, body);
133-
134-
} else if(request.getType().equals("NOTE")) {
135-
// 노트 컨텐츠로 파일 만들기
136-
addNote(request, body);
137-
}
138-
else {
139-
throw new IllegalArgumentException("잘못된 resource type 입니다.");
140-
}
141-
}
142-
return body;
143-
}
144-
145-
private void addNote(QuestionCreateRequest request, MultiValueMap<String, Object> body) {
146-
Note note = noteRepository.findById(request.getResourceId())
147-
.orElseThrow(() -> new EntityNotFoundException("Note not found: " + request.getResourceId()));
148-
149-
String markdown = note.getContent();
150-
151-
// String → ByteArrayResource (가짜 파일)
152-
ByteArrayResource fileResource = new ByteArrayResource(
153-
markdown.getBytes(StandardCharsets.UTF_8)
154-
) {
155-
@Override
156-
public String getFilename() {
157-
return "note-" + request.getResourceId() + ".md";
158-
}
159-
};
160-
161-
// 같은 폼 필드명("files")에 여러 개를 add하면, 서버 쪽에서는 배열로 받는다.
162-
body.add("files", fileResource);
163-
}
164-
165-
private void addFileFromS3(QuestionCreateRequest request, MultiValueMap<String, Object> body) {
166-
File file = fileRepository.findById(request.getResourceId())
167-
.orElseThrow(() -> new EntityNotFoundException("File not found: " + request.getResourceId()));
168-
169-
String s3Path = file.getS3Path();
170-
String filename = Paths.get(s3Path).getFileName().toString();
171-
172-
S3Object s3Object = amazonS3Client.getObject(bucketName, s3Path);
173-
try (S3ObjectInputStream is = s3Object.getObjectContent()) {
174-
byte[] bytes = is.readAllBytes();
175-
ByteArrayResource fileResource = new ByteArrayResource(bytes) {
176-
@Override
177-
public String getFilename() {
178-
return filename;
179-
}
180-
};
181-
body.add("files", fileResource);
182-
} catch (IOException e) {
183-
throw new RuntimeException("S3에서 파일 읽기 실패: " + s3Path, e);
184-
}
185-
}
186-
18784
private @NotNull String createTitle(List<QuestionCreateRequest> requestList, Directory directory) {
18885
String title;
18986
if (requestList.getFirst().getTagId() != null) {
@@ -211,15 +108,15 @@ private static int getVersion(List<Question> existingSameQuestions) {
211108
return version;
212109
}
213110

214-
@Transactional
111+
@Transactional(readOnly = true)
215112
public QuestionResponse getQuestionById(Long questionId) {
216113
Question question = questionRepository.findById(questionId)
217114
.orElseThrow(() -> new NotFoundException("Question not found"));
218115

219116
return QuestionResponse.fromEntity(question);
220117
}
221118

222-
@Transactional
119+
@Transactional(readOnly = true)
223120
public List<QuestionListResponse> getQuestionsByUserSemester(Long userSemesterId) {
224121

225122
// 1) 해당 학기에 속한 모든 디렉토리 조회

src/main/java/com/driveu/server/domain/question/domain/Question.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
@Entity
1313
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1414
@AllArgsConstructor
15+
@Builder
1516
@Getter
1617
@Table(name = "question")
1718
public class Question {
@@ -35,15 +36,9 @@ public class Question {
3536
private LocalDateTime createdAt;
3637

3738
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
39+
@Builder.Default
3840
private List<QuestionResource> questionResources = new ArrayList<>();
3941

40-
@Builder
41-
private Question(String title, int version, String questionsData) {
42-
this.title = title;
43-
this.version = version;
44-
this.questionsData = questionsData;
45-
}
46-
4742
public static Question of(String title, int version, String questionsData) {
4843
return Question.builder()
4944
.title(title)

src/main/java/com/driveu/server/domain/question/domain/QuestionResource.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
@Entity
88
@Getter
99
@NoArgsConstructor(access = AccessLevel.PROTECTED)
10+
@Builder
1011
@AllArgsConstructor
1112
@Table(name = "question_resource",
1213
uniqueConstraints = @UniqueConstraint(columnNames = {"question_id", "resource_id"}))
@@ -26,12 +27,6 @@ public class QuestionResource {
2627
@JoinColumn(name = "resource_id", nullable = false)
2728
private Resource resource;
2829

29-
@Builder
30-
private QuestionResource(Question question, Resource resource) {
31-
this.resource = resource;
32-
this.question = question;
33-
}
34-
3530
public static QuestionResource of(Question question, Resource resource) {
3631
return QuestionResource.builder()
3732
.resource(resource)

src/main/java/com/driveu/server/domain/resource/application/S3Service.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.driveu.server.domain.resource.application;
22

3+
import com.amazonaws.services.s3.AmazonS3Client;
4+
import com.amazonaws.services.s3.model.S3Object;
5+
import com.amazonaws.services.s3.model.S3ObjectInputStream;
36
import com.driveu.server.domain.auth.infra.JwtProvider;
47
import com.driveu.server.domain.resource.domain.File;
58
import com.driveu.server.domain.resource.domain.Note;
@@ -10,6 +13,7 @@
1013
import jakarta.persistence.EntityNotFoundException;
1114
import lombok.RequiredArgsConstructor;
1215
import org.springframework.beans.factory.annotation.Value;
16+
import org.springframework.core.io.ByteArrayResource;
1317
import org.springframework.stereotype.Service;
1418
import org.springframework.transaction.annotation.Transactional;
1519
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
@@ -18,6 +22,7 @@
1822
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
1923
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
2024

25+
import java.io.IOException;
2126
import java.net.URL;
2227
import java.time.Duration;
2328

@@ -29,6 +34,7 @@ public class S3Service {
2934
private final ResourceService resourceService;
3035
private final JwtProvider jwtProvider;
3136
private final UserRepository userRepository;
37+
private final AmazonS3Client amazonS3Client;
3238

3339
@Value("${spring.cloud.aws.s3.bucket}")
3440
private String bucketName;
@@ -100,4 +106,23 @@ else if (resource instanceof Note note){
100106

101107
return s3Presigner.presignGetObject(presignRequest).url();
102108
}
109+
110+
public ByteArrayResource getFileAsResource(String s3Path, String filename) {
111+
try {
112+
S3Object s3Object = amazonS3Client.getObject(bucketName, s3Path);
113+
S3ObjectInputStream is = s3Object.getObjectContent();
114+
115+
byte[] bytes = is.readAllBytes();
116+
is.close();
117+
118+
return new ByteArrayResource(bytes) {
119+
@Override
120+
public String getFilename() {
121+
return filename;
122+
}
123+
};
124+
} catch (IOException e) {
125+
throw new RuntimeException("S3에서 파일 읽기 실패: " + s3Path, e);
126+
}
127+
}
103128
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.driveu.server.domain.resource.domain.type;
2+
3+
public enum ResourceType {
4+
FILE, NOTE, LINK;
5+
}

0 commit comments

Comments
 (0)