Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into be/fix/#259
Browse files Browse the repository at this point in the history
  • Loading branch information
YunJuwon0825 committed Feb 27, 2025
2 parents 1cd1eea + 6bb5ee6 commit 87db610
Show file tree
Hide file tree
Showing 11 changed files with 466 additions and 387 deletions.
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);

}
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", ("이 이력서에서 잘 작성된 부분과 개선해야 할 부분을 구체적으로 지적해 주세요. " +
"특히, 내용의 명확성, 경험 기술의 구체성, 그리고 부족한 스킬이나 프로젝트가 있는지에 대한 피드백을 제공해 주세요.\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

0 comments on commit 87db610

Please sign in to comment.