Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[be/fix] ai 피드백 생성, 조회 수정 #287

Merged
merged 5 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,32 +1,55 @@
package com.techeer.backend.api.aifeedback.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.techeer.backend.api.aifeedback.converter.AIFeedbackConverter;
import com.techeer.backend.api.aifeedback.domain.AIFeedback;
import com.techeer.backend.api.aifeedback.dto.AIFeedbackResponse;
import com.techeer.backend.api.aifeedback.service.AIFeedbackService;

import com.techeer.backend.global.common.response.CommonResponse;
import com.techeer.backend.global.success.SuccessCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "AIFeedback", description = "AIFeedback 받기")
@Tag(name = "AIFeedback", description = "AI 피드백 받기")
@RestController
@RequestMapping("/api/v1/aifeedbacks")
public class AIFeedbackController {
private final AIFeedbackService aifeedbackService;

public AIFeedbackController(AIFeedbackService aifeedbackService) {
this.aifeedbackService = aifeedbackService;
}
private final AIFeedbackService aifeedbackService;

public AIFeedbackController(AIFeedbackService aifeedbackService) {
this.aifeedbackService = aifeedbackService;
}

@Operation(summary = "AI 피드백 생성", description = "본인 이력서에 대한 AI 피드백을 진행합니다.")
@PostMapping("/{resume_id}")
public CommonResponse<AIFeedbackResponse> createFeedbackFromS3(@PathVariable("resume_id") Long resumeId) {
AIFeedback feedback = aifeedbackService.generateAIFeedbackFromS3(resumeId);
AIFeedbackResponse feedbackResponse = AIFeedbackConverter.toResponse(feedback);
return CommonResponse.of(SuccessCode.CREATED, feedbackResponse);
}

@Operation(summary = "단일 피드백 조회", description = "피드백 ID를 통해 단일 AI 피드백을 조회합니다.")
@GetMapping("/{aifeedback_id}")
public CommonResponse<AIFeedbackResponse> getFeedbackById(@PathVariable("aifeedback_id") Long aifeedbackId) {
AIFeedback aifeedback = aifeedbackService.getFeedbackById(aifeedbackId);
AIFeedbackResponse response = AIFeedbackConverter.toResponse(aifeedback);
return CommonResponse.of(SuccessCode.OK, response);
}

@Operation(summary = "AIFeedback 생성", description = "본인 이력서에 대한 ai 피드백을 진행합니다.")
@PostMapping("/{resume_id}")
public ResponseEntity<AIFeedbackResponse> createFeedbackFromS3(@PathVariable("resume_id") Long resumeId) {
AIFeedbackResponse feedbackResponse = aifeedbackService.generateAIFeedbackFromS3(resumeId);
return ResponseEntity.status(HttpStatus.CREATED).body(feedbackResponse);
}
@Operation(summary = "이력서별 피드백 목록 조회", description = "특정 이력서에 대한 모든 AI 피드백을 조회합니다.")
@GetMapping("/resume/{resume_id}")
public CommonResponse<List<AIFeedbackResponse>> getFeedbacksByResumeId(@PathVariable("resume_id") Long resumeId) {
List<AIFeedback> feedbacks = aifeedbackService.getFeedbacksByResumeId(resumeId);
List<AIFeedbackResponse> responses = feedbacks.stream()
.map(feedback -> AIFeedbackConverter.toResponse(feedback))
.collect(Collectors.toList());
return CommonResponse.of(SuccessCode.OK, responses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.techeer.backend.api.aifeedback.converter;

import com.techeer.backend.api.aifeedback.domain.AIFeedback;
import com.techeer.backend.api.aifeedback.dto.AIFeedbackResponse;

public class AIFeedbackConverter {
public static AIFeedbackResponse toResponse(AIFeedback aifeedback) {
if (aifeedback == null) {
return null;
}
return AIFeedbackResponse.builder()
.id(aifeedback.getId())
.resumeId(aifeedback.getResumeId())
.feedback(aifeedback.getFeedback())
.build();
}
}

Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.techeer.backend.api.aifeedback.repository;

import com.techeer.backend.api.aifeedback.domain.AIFeedback;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AIFeedbackRepository extends JpaRepository<AIFeedback, Long> {
Optional<AIFeedback> findByResumeId(Long resumeId);
List<AIFeedback> findByResumeId(Long resumeId);

Optional<AIFeedback> findById(Long feedbackId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove redundant findById method.

This method is redundant since JpaRepository already provides a findById method with the same signature. The method can be safely removed to reduce code duplication.

-    Optional<AIFeedback> findById(Long feedbackId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Optional<AIFeedback> findById(Long feedbackId);


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.nimbusds.jose.shaded.gson.JsonObject;
import com.nimbusds.jose.shaded.gson.JsonParser;
import com.techeer.backend.api.aifeedback.domain.AIFeedback;
import com.techeer.backend.api.aifeedback.dto.AIFeedbackResponse;
import com.techeer.backend.api.aifeedback.repository.AIFeedbackRepository;
import com.techeer.backend.api.resume.domain.Resume;
import com.techeer.backend.api.resume.domain.ResumePdf;
Expand All @@ -15,76 +14,97 @@
import jakarta.transaction.Transactional;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class AIFeedbackService {

private static final Logger log = LoggerFactory.getLogger(AIFeedbackService.class);

private final AIFeedbackRepository aiFeedbackRepository;
private final ResumeRepository resumeRepository; // 이력서 데이터를 가져오기 위한 레포지토리 추가
private final AmazonS3 amazonS3;
private final OpenAIService openAiService;

public AIFeedbackService(AIFeedbackRepository aiFeedbackRepository, ResumeRepository resumeRepository, AmazonS3 amazonS3, OpenAIService openAiService) {
public AIFeedbackService(AIFeedbackRepository aiFeedbackRepository, ResumeRepository resumeRepository,
AmazonS3 amazonS3, OpenAIService openAiService) {
this.aiFeedbackRepository = aiFeedbackRepository;
this.resumeRepository = resumeRepository;
this.amazonS3 = amazonS3;
this.openAiService = openAiService;
}

// PDF 파일을 S3에서 읽어온 뒤 텍스트로 변환하고, GPT로 피드백을 요청한 후, 결과를 저장하는 메서드
@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Transactional
public AIFeedbackResponse generateAIFeedbackFromS3(Long resumeId) {
// 1. 이력서 정보 데이터베이스에서 조회
public AIFeedback generateAIFeedbackFromS3(Long resumeId) {
// 1. 이력서 정보 조회 및 S3에서 PDF 읽기, 텍스트 변환 등 기존 로직 수행
Resume resume = resumeRepository.findById(resumeId)
.orElseThrow(() -> new BusinessException(ErrorCode.RESUME_NOT_FOUND));

// 2. ResumePdf 객체에서 S3 버킷 이름과 키 가져오기
ResumePdf resumePdf = resume.getResumePdf();
if (resumePdf == null) {
throw new BusinessException(ErrorCode.RESUME_PDF_NOT_FOUND);
}

String bucketName = resumePdf.getPdf().getPdfUrl(); // Assuming pdfUrl contains the bucket name
String key = resumePdf.getPdf().getPdfUUID(); // Assuming pdfUUID contains the S3 key

// 3. S3에서 PDF 파일 가져오기
InputStream pdfInputStream = getPdfFileFromS3(bucketName, key);

// 4. PDF 파���을 텍스트로 변환
String resumeText = extractTextFromPdf(pdfInputStream);

// 5. OpenAI GPT API 호출을 통한 피드백 생성
// S3 key 추출, PDF 텍스트 변환, OpenAI API 호출 등 기존 로직...
String resumeText = extractTextFromPdf(
getPdfFileFromS3(bucket, extractKeyFromUrl(resumePdf.getPdf().getPdfUrl())));
String fullResponse;
try {
fullResponse = openAiService.getAIFeedback(resumeText);
} catch (IOException e) {
// IOException 발생 시 처리 로직
throw new BusinessException(ErrorCode.OPENAI_SERVER_ERROR);
}

// 6. ai피드백에서 content만 추출
String feedbackContent = extractContentFromOpenAIResponse(fullResponse);

// 7. AIFeedback 엔티티 생성 및 저장
// 2. 도메인 객체(AIFeedback) 생성 및 저장
AIFeedback aiFeedback = AIFeedback.builder()
.resumeId(resumeId)
.feedback(feedbackContent) // content만 저장
.feedback(feedbackContent)
.build();

AIFeedback savedFeedback = aiFeedbackRepository.save(aiFeedback);
return aiFeedbackRepository.save(aiFeedback);
}

public AIFeedback getFeedbackById(Long feedbackId) {
return aiFeedbackRepository.findById(feedbackId)
.orElseThrow(() -> new BusinessException(ErrorCode.FEEDBACK_NOT_FOUND));
}

public List<AIFeedback> getFeedbacksByResumeId(Long resumeId) {
return aiFeedbackRepository.findByResumeId(resumeId);
}

return AIFeedbackResponse.of(savedFeedback);
// 별도의 메서드로 URL에서 key 추출 (예시)
private String extractKeyFromUrl(String fileUrl) {
try {
URL url = new URL(fileUrl);
String key = url.getPath().substring(1); // 선행 '/' 제거
return URLDecoder.decode(key, StandardCharsets.UTF_8.name());
} catch (Exception e) {
throw new BusinessException(ErrorCode.RESUME_UPLOAD_ERROR);
}
}

// S3에서 PDF 파일을 가져오는 메서드
// S3에서 PDF 파일을 가져오는 메서드 (로그 추가)
private InputStream getPdfFileFromS3(String bucketName, String key) {
try {
S3Object s3Object = amazonS3.getObject(bucketName, key);
log.info("Successfully retrieved S3 object. Content length: {}",
s3Object.getObjectMetadata().getContentLength());
return s3Object.getObjectContent();
} catch (Exception e) {
log.error("Error retrieving PDF from S3. Bucket: '{}', Key: '{}'", bucketName, key, e);
throw new BusinessException(ErrorCode.RESUME_UPLOAD_ERROR);
}
}
Expand All @@ -108,4 +128,5 @@ private String extractContentFromOpenAIResponse(String fullResponse) {
.getAsJsonObject("message")
.get("content").getAsString();
}

}
Original file line number Diff line number Diff line change
@@ -1,77 +1,83 @@
package com.techeer.backend.api.aifeedback.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;

@Service
public class OpenAIService {

@Value("${chatgpt.api.key}")
private String apiKey;
@Value("${chatgpt.api.key}")
private String apiKey;

@Value("${chatgpt.api.url}")
private String apiUrl;

@Value("${chatgpt.api.url}")
private String apiUrl;
private final CloseableHttpClient httpClient;
private static final int TIMEOUT = 30000;

private final CloseableHttpClient httpClient;
private static final int TIMEOUT = 30000;
private final ObjectMapper objectMapper = new ObjectMapper();

private final ObjectMapper objectMapper = new ObjectMapper();
public OpenAIService() {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(TIMEOUT)
.setSocketTimeout(TIMEOUT)
.build();

public OpenAIService() {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(TIMEOUT)
.setSocketTimeout(TIMEOUT)
.build();
this.httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.build();
}

this.httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.build();
}
public String getAIFeedback(String resumeText) throws IOException {
HttpPost httpPost = new HttpPost(apiUrl);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Authorization", "Bearer " + apiKey);

public String getAIFeedback(String resumeText) throws IOException {
HttpPost httpPost = new HttpPost(apiUrl);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Authorization", "Bearer " + apiKey);
String jsonBody = createRequestBody(resumeText);
StringEntity entity = new StringEntity(jsonBody, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);

// JSON 요청 본문 생성
StringEntity entity = new StringEntity(createRequestBody(resumeText));
httpPost.setEntity(entity);
HttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());

HttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new IOException("OpenAI API 호출 실패: 상태 코드 " + statusCode + ", 응답: " + responseBody);
}
return responseBody;
}

if (statusCode != 200) {
String errorMessage = EntityUtils.toString(response.getEntity());
throw new IOException("OpenAI API 호출 실패: 상태 코드 " + statusCode + ", 응답: " + errorMessage);
}

return EntityUtils.toString(response.getEntity());
}
// ObjectMapper를 사용하여 JSON 요청 본문 생성
private String createRequestBody(String resumeText) throws IOException {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", "gpt-4");
requestBody.put("temperature", 0.7);

// ObjectMapper를 사용하여 JSON 요청 본문 생성
private String createRequestBody(String resumeText) throws IOException {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", "gpt-4o");
Map<String, String> message = new HashMap<>();
message.put("role", "user");
// 불필요한 공백이나 개행이 문제를 일으킬 수 있으므로 trim()으로 정리합니다.
message.put("content", ("이 이력서에서 잘 작성된 부분과 개선해야 할 부분을 구체적으로 지적해 주세요. " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에 예를들어 gpt한테 너는 IT 빅테크 기업(구글, 페이스북, AWS)면접관이야. 이런식으로 role 부여해주면 대답이 더 좋아져. 이런거 찾아보고 role 추가해도 좋을거 같아.

"특히, 내용의 명확성, 경험 기술의 구체성, 그리고 부족한 스킬이나 프로젝트가 있는지에 대한 피드백을 제공해 주세요.\n" + resumeText).trim());

Map<String, String> message = new HashMap<>();
message.put("role", "user");
message.put("content", "\"이 이력서에서 잘 작성된 부분과 개선해야 할 부분을 구체적으로 지적해 주세요. 특히, 내용의 명확성, 경험 기술의 구체성, 그리고 부족한 스킬이나 프로젝트가 있는지에 대한 피드백을 제공해 주세요.\": \n" + resumeText);
// messages는 List 또는 배열 형태여야 합니다.
requestBody.put("messages", new Object[]{message});

requestBody.put("messages", new Object[] {message});
// JSON 문자열 생성 후 로그 출력 (디버깅용)
String jsonBody = objectMapper.writeValueAsString(requestBody);
return jsonBody;
}

return objectMapper.writeValueAsString(requestBody);
}
}
Loading
Loading