From 4db9a32a4c3d7b1c22a3820afd8d97e1d0fffb5c Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 23 Feb 2025 16:40:52 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix=20:=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=9A=8C=EC=9B=90=20=EC=A1=B0=ED=9A=8C=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spring Security Config file을 수정하여 여러 이력서 조회를 하는 로직이 가능하도록 함 --- .../com/techeer/backend/global/config/SecurityConfig.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java b/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java index 352a5a64..0dfc89dd 100644 --- a/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java +++ b/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -64,6 +65,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // .anyRequest().permitAll() // ) .authorizeHttpRequests(authorize -> authorize + + // 특정 엔드포인트에서 GET 요청만 허용 + .requestMatchers(HttpMethod.GET, "/api/v1/resumes").permitAll() + .requestMatchers( "/v3/api-docs/**", "/oauth2/**", From f640fdea47e290941e9542d6ae7b1f6127446703 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 23 Feb 2025 16:44:39 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix=20:=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=9A=8C=EC=9B=90=20=EC=A1=B0=ED=9A=8C=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다중 조회용 API endpoint 추가 --- .../com/techeer/backend/global/config/SecurityConfig.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java b/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java index 0dfc89dd..72eb57ae 100644 --- a/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java +++ b/backend/src/main/java/com/techeer/backend/global/config/SecurityConfig.java @@ -67,7 +67,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorize -> authorize // 특정 엔드포인트에서 GET 요청만 허용 - .requestMatchers(HttpMethod.GET, "/api/v1/resumes").permitAll() + .requestMatchers(HttpMethod.GET, + "/api/v1/resumes", + "/api/v1/resumes/view" + ).permitAll() .requestMatchers( "/v3/api-docs/**", @@ -80,7 +83,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api-docs/**", "/signup.html", "/login", - "/api/v1/mock/signup" + "/api/v1/mock/signup", + "/api/v1/resumes/search" ).permitAll() .anyRequest().authenticated() ) From bf3dbd5b8e336b0a202c3b3de0264666184767a8 Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Wed, 26 Feb 2025 12:05:31 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[be/fix]=20ai=20Controller=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=B0=8F=20dto=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AIFeedbackController.java | 61 +++++++++++++------ .../converter/AIFeedbackConverter.java | 18 ++++++ 2 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 backend/src/main/java/com/techeer/backend/api/aifeedback/converter/AIFeedbackConverter.java diff --git a/backend/src/main/java/com/techeer/backend/api/aifeedback/controller/AIFeedbackController.java b/backend/src/main/java/com/techeer/backend/api/aifeedback/controller/AIFeedbackController.java index fb393fbd..bf572029 100644 --- a/backend/src/main/java/com/techeer/backend/api/aifeedback/controller/AIFeedbackController.java +++ b/backend/src/main/java/com/techeer/backend/api/aifeedback/controller/AIFeedbackController.java @@ -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 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 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 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> getFeedbacksByResumeId(@PathVariable("resume_id") Long resumeId) { + List feedbacks = aifeedbackService.getFeedbacksByResumeId(resumeId); + List responses = feedbacks.stream() + .map(feedback -> AIFeedbackConverter.toResponse(feedback)) + .collect(Collectors.toList()); + return CommonResponse.of(SuccessCode.OK, responses); + } } diff --git a/backend/src/main/java/com/techeer/backend/api/aifeedback/converter/AIFeedbackConverter.java b/backend/src/main/java/com/techeer/backend/api/aifeedback/converter/AIFeedbackConverter.java new file mode 100644 index 00000000..f9e8b9e8 --- /dev/null +++ b/backend/src/main/java/com/techeer/backend/api/aifeedback/converter/AIFeedbackConverter.java @@ -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(); + } +} + From a784ea291a34cf95cc7c0f49f3fe4bda043e1927 Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Wed, 26 Feb 2025 12:06:04 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[be/fix]=20aiFeedback=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aifeedback/service/AIFeedbackService.java | 71 +++++++++----- .../api/aifeedback/service/OpenAIService.java | 94 ++++++++++--------- 2 files changed, 96 insertions(+), 69 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/api/aifeedback/service/AIFeedbackService.java b/backend/src/main/java/com/techeer/backend/api/aifeedback/service/AIFeedbackService.java index a7231b35..1196795e 100644 --- a/backend/src/main/java/com/techeer/backend/api/aifeedback/service/AIFeedbackService.java +++ b/backend/src/main/java/com/techeer/backend/api/aifeedback/service/AIFeedbackService.java @@ -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; @@ -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 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); } } @@ -108,4 +128,5 @@ private String extractContentFromOpenAIResponse(String fullResponse) { .getAsJsonObject("message") .get("content").getAsString(); } + } diff --git a/backend/src/main/java/com/techeer/backend/api/aifeedback/service/OpenAIService.java b/backend/src/main/java/com/techeer/backend/api/aifeedback/service/OpenAIService.java index 7306439e..3d4020f5 100644 --- a/backend/src/main/java/com/techeer/backend/api/aifeedback/service/OpenAIService.java +++ b/backend/src/main/java/com/techeer/backend/api/aifeedback/service/OpenAIService.java @@ -1,12 +1,13 @@ 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; @@ -14,64 +15,69 @@ 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 requestBody = new HashMap<>(); + requestBody.put("model", "gpt-4"); + requestBody.put("temperature", 0.7); - // ObjectMapper를 사용하여 JSON 요청 본문 생성 - private String createRequestBody(String resumeText) throws IOException { - Map requestBody = new HashMap<>(); - requestBody.put("model", "gpt-4o"); + Map message = new HashMap<>(); + message.put("role", "user"); + // 불필요한 공백이나 개행이 문제를 일으킬 수 있으므로 trim()으로 정리합니다. + message.put("content", ("이 이력서에서 잘 작성된 부분과 개선해야 할 부분을 구체적으로 지적해 주세요. " + + "특히, 내용의 명확성, 경험 기술의 구체성, 그리고 부족한 스킬이나 프로젝트가 있는지에 대한 피드백을 제공해 주세요.\n" + resumeText).trim()); - Map 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); - } } From 6f0aade76293e5b6881a8ce0f33e903fac5eb584 Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Wed, 26 Feb 2025 12:06:35 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[be/fix]=20=EB=AA=A8=EB=93=A0=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feedback/controller/FeedbackController.java | 17 ++++++++++++----- .../feedback/converter/FeedbackConverter.java | 1 - .../api/feedback/service/FeedbackService.java | 9 +-------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/api/feedback/controller/FeedbackController.java b/backend/src/main/java/com/techeer/backend/api/feedback/controller/FeedbackController.java index 6ccea720..83eb26cc 100644 --- a/backend/src/main/java/com/techeer/backend/api/feedback/controller/FeedbackController.java +++ b/backend/src/main/java/com/techeer/backend/api/feedback/controller/FeedbackController.java @@ -1,6 +1,7 @@ package com.techeer.backend.api.feedback.controller; import com.techeer.backend.api.aifeedback.domain.AIFeedback; +import com.techeer.backend.api.aifeedback.service.AIFeedbackService; import com.techeer.backend.api.feedback.converter.FeedbackConverter; import com.techeer.backend.api.feedback.domain.Feedback; import com.techeer.backend.api.feedback.dto.request.FeedbackCreateRequest; @@ -34,6 +35,7 @@ public class FeedbackController { private final FeedbackService feedbackService; private final UserService userService; + private final AIFeedbackService aifeedbackService; @Operation(summary = "피드백 등록", description = "원하는 위치에 피드백을 작성합니다.") @PostMapping("/{resume_id}/feedbacks") @@ -56,19 +58,24 @@ public CommonResponse createFeedback( return CommonResponse.of(SuccessCode.CREATED, feedbackResponse); } - @Operation(summary = "AI 피드백, 일반 피드백 조회", description = "해당 이력서에 대한 AI 피드백과 일반 피드백 조회") - @GetMapping("/{resume_id}/feedbacks") - public CommonResponse getFeedbackWithAIFeedback(@PathVariable("resume_id") Long resumeId) { + @Operation(summary = "AI 피드백, 일반 피드백 조회", description = "해당 이력서에 대한 일반 피드백과 특정 AI 피드백(aifeedbackId를 이용)을 조회합니다.") + @GetMapping("/{resume_id}/feedbacks/{aifeedback_id}") + public CommonResponse getFeedbackWithAIFeedback( + @PathVariable("resume_id") Long resumeId, + @PathVariable("aifeedback_id") Long aifeedbackId) { - // 엔티티, 리스트 반환 + // 일반 피드백 조회 (이력서 ID 기준) List feedbacks = feedbackService.getFeedbackByResumeId(resumeId); - AIFeedback aiFeedback = feedbackService.getAIFeedbackByResumeId(resumeId); + // 특정 AI 피드백 조회 (AI 피드백 ID 기준) + AIFeedback aiFeedback = aifeedbackService.getFeedbackById(aifeedbackId); + // 두 결과를 하나의 DTO로 변환 (여기서는 원시 도메인 객체를 전달) AllFeedbackResponse response = FeedbackConverter.toAllFeedbackResponse(feedbacks, aiFeedback); return CommonResponse.of(SuccessCode.FEEDBACK_FETCH_OK, response); } + @Operation(summary = "피드백 삭제") @DeleteMapping("/{resume_id}/feedbacks/{feedback_id}") public CommonResponse deleteFeedback( diff --git a/backend/src/main/java/com/techeer/backend/api/feedback/converter/FeedbackConverter.java b/backend/src/main/java/com/techeer/backend/api/feedback/converter/FeedbackConverter.java index b8692163..6d643179 100644 --- a/backend/src/main/java/com/techeer/backend/api/feedback/converter/FeedbackConverter.java +++ b/backend/src/main/java/com/techeer/backend/api/feedback/converter/FeedbackConverter.java @@ -37,7 +37,6 @@ public static AllFeedbackResponse toAllFeedbackResponse(List feedbacks return AllFeedbackResponse.builder() .feedbackResponses(toFeedbackResponses(feedbacks)) .aiFeedbackContent(getAIFeedbackContent(aiFeedback)) - .aiFeedbackId(getAIFeedbackId(aiFeedback)) .build(); } diff --git a/backend/src/main/java/com/techeer/backend/api/feedback/service/FeedbackService.java b/backend/src/main/java/com/techeer/backend/api/feedback/service/FeedbackService.java index b1fa95ed..569c9891 100644 --- a/backend/src/main/java/com/techeer/backend/api/feedback/service/FeedbackService.java +++ b/backend/src/main/java/com/techeer/backend/api/feedback/service/FeedbackService.java @@ -1,6 +1,5 @@ package com.techeer.backend.api.feedback.service; -import com.techeer.backend.api.aifeedback.domain.AIFeedback; import com.techeer.backend.api.aifeedback.repository.AIFeedbackRepository; import com.techeer.backend.api.feedback.converter.FeedbackConverter; import com.techeer.backend.api.feedback.domain.Feedback; @@ -70,13 +69,7 @@ public List getFeedbackByResumeId(Long resumeId) { return feedbacks; } - - public AIFeedback getAIFeedbackByResumeId(Long resumeId) { - // 이력서 id에 해당하는 ai 피드백 가져옴(없으면 빈배열 반환) - return aiFeedbackRepository.findByResumeId(resumeId) - .orElse(AIFeedback.empty()); - } - + public List getFeedbacksByResumeId(Long resumeId) { return feedbackRepository.findAllByResumeId(resumeId); } From b44ae90546a71bcf6c423a66b761d8777b730153 Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Wed, 26 Feb 2025 12:06:51 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[be/fix]=20ai=20=EC=9D=91=EB=8B=B5=EA=B0=92?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/aifeedback/repository/AIFeedbackRepository.java | 6 +++++- .../api/feedback/dto/response/AllFeedbackResponse.java | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/api/aifeedback/repository/AIFeedbackRepository.java b/backend/src/main/java/com/techeer/backend/api/aifeedback/repository/AIFeedbackRepository.java index 23755b5f..14262bab 100644 --- a/backend/src/main/java/com/techeer/backend/api/aifeedback/repository/AIFeedbackRepository.java +++ b/backend/src/main/java/com/techeer/backend/api/aifeedback/repository/AIFeedbackRepository.java @@ -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 { - Optional findByResumeId(Long resumeId); + List findByResumeId(Long resumeId); + + Optional findById(Long feedbackId); + } diff --git a/backend/src/main/java/com/techeer/backend/api/feedback/dto/response/AllFeedbackResponse.java b/backend/src/main/java/com/techeer/backend/api/feedback/dto/response/AllFeedbackResponse.java index b807ff35..33d8f7b1 100644 --- a/backend/src/main/java/com/techeer/backend/api/feedback/dto/response/AllFeedbackResponse.java +++ b/backend/src/main/java/com/techeer/backend/api/feedback/dto/response/AllFeedbackResponse.java @@ -13,5 +13,4 @@ public class AllFeedbackResponse { private final List feedbackResponses; // 여러 개의 피드백을 리스트로 포함 private final String aiFeedbackContent; - private final Long aiFeedbackId; } From 6bb5ee6694a942427947af9c11ba7bd61a6f73af Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Wed, 26 Feb 2025 23:14:53 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[be/fix]=20feedbackServiceTest=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resume/service/FeedbackServiceTest.java | 564 +++++++++--------- 1 file changed, 282 insertions(+), 282 deletions(-) diff --git a/backend/src/test/java/com/techeer/backend/api/resume/service/FeedbackServiceTest.java b/backend/src/test/java/com/techeer/backend/api/resume/service/FeedbackServiceTest.java index 74774d54..f482d419 100644 --- a/backend/src/test/java/com/techeer/backend/api/resume/service/FeedbackServiceTest.java +++ b/backend/src/test/java/com/techeer/backend/api/resume/service/FeedbackServiceTest.java @@ -1,282 +1,282 @@ -package com.techeer.backend.api.resume.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.when; - -import com.techeer.backend.api.aifeedback.domain.AIFeedback; -import com.techeer.backend.api.aifeedback.repository.AIFeedbackRepository; -import com.techeer.backend.api.feedback.domain.Feedback; -import com.techeer.backend.api.feedback.dto.request.FeedbackCreateRequest; -import com.techeer.backend.api.feedback.repository.FeedbackRepository; -import com.techeer.backend.api.feedback.service.FeedbackService; -import com.techeer.backend.api.resume.domain.Resume; -import com.techeer.backend.api.resume.repository.ResumeRepository; -import com.techeer.backend.api.user.domain.Role; -import com.techeer.backend.api.user.domain.SocialType; -import com.techeer.backend.api.user.domain.User; -import com.techeer.backend.api.user.repository.UserRepository; -import com.techeer.backend.global.error.ErrorCode; -import com.techeer.backend.global.error.exception.BusinessException; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class FeedbackServiceTest { - - - @Mock - private FeedbackRepository feedbackRepository; - - @Mock - private ResumeRepository resumeRepository; - - @Mock - private AIFeedbackRepository aiFeedbackRepository; - - @InjectMocks - private FeedbackService feedbackService; - - @Mock - private UserRepository userRepository; - - @BeforeEach - void setUp() { - feedbackRepository = Mockito.mock(FeedbackRepository.class); - resumeRepository = Mockito.mock(ResumeRepository.class); - aiFeedbackRepository = Mockito.mock(AIFeedbackRepository.class); - - feedbackService = new FeedbackService(feedbackRepository, resumeRepository, aiFeedbackRepository); - } - - @Nested - @DisplayName("createFeedback 메서드 테스트") - class CreateFeedbackTest { - - @Test - @DisplayName("Given 유효한 이력서와 유저, When 피드백 생성 요청, Then 피드백이 성공적으로 생성된다.") - void createFeedback_Success() { - // Given - Long resumeId = 1L; - User user = userRepository.save( - User.builder() - .email("john@example.com") - .username("JohnDoe") - .role(Role.TECHEER) - .socialType(SocialType.GOOGLE) - .build() - ); - - FeedbackCreateRequest request = new FeedbackCreateRequest( - "Valid content", - 100.5, 200.5, - null, null, - 1 - ); - Resume mockResume = Resume.builder().id(resumeId).build(); - - when(resumeRepository.findByIdAndDeletedAtIsNull(resumeId)).thenReturn(Optional.of(mockResume)); - when(feedbackRepository.save(Mockito.any(Feedback.class))) - .thenAnswer(invocation -> invocation.getArgument(0)); - - // When - Feedback createdFeedback = feedbackService.createFeedback(user, resumeId, request); - - // Then - assertThat(createdFeedback).isNotNull(); - assertThat(createdFeedback.getContent()).isEqualTo("Valid content"); - assertThat(createdFeedback.getResume()).isEqualTo(mockResume); - } - - @Test - @DisplayName("Given 존재하지 않는 이력서, When 피드백 생성 요청, Then RESUME_NOT_FOUND 예외가 발생한다.") - void createFeedback_ResumeNotFound() { - // Given - Long resumeId = 999L; - User user = userRepository.save( - User.builder() - .email("john@example.com") - .username("JohnDoe") - .role(Role.TECHEER) - .socialType(SocialType.GOOGLE) - .build() - ); - FeedbackCreateRequest request = new FeedbackCreateRequest( - "Content", - 100.5, 200.5, - null, null, - 1 - ); - - when(resumeRepository.findByIdAndDeletedAtIsNull(resumeId)).thenReturn(Optional.empty()); - - // When & Then - assertThatThrownBy(() -> feedbackService.createFeedback(user, resumeId, request)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(ErrorCode.RESUME_NOT_FOUND.getMessage()); - } - } - - @Nested - @DisplayName("deleteFeedbackById 메서드 테스트") - class DeleteFeedbackTest { - - @Test - @DisplayName("Given 존재하지 않는 이력서, When 피드백 삭제 요청, Then RESUME_NOT_FOUND 예외 발생") - void deleteFeedback_ResumeNotFound() { - // Given - Long resumeId = 999L; - Long feedbackId = 10L; - User user = userRepository.save( - User.builder() - .email("john@example.com") - .username("JohnDoe") - .role(Role.TECHEER) - .socialType(SocialType.GOOGLE) - .build() - ); - - when(resumeRepository.findById(resumeId)).thenReturn(Optional.empty()); - - // When & Then - assertThatThrownBy(() -> feedbackService.deleteFeedbackById(user, resumeId, feedbackId)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(ErrorCode.RESUME_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("Given 존재하지 않는 피드백, When 피드백 삭제 요청, Then FEEDBACK_NOT_FOUND 예외 발생") - void deleteFeedback_FeedbackNotFound() { - // Given - Long resumeId = 1L; - Long feedbackId = 999L; - User user = userRepository.save( - User.builder() - .email("john@example.com") - .username("JohnDoe") - .role(Role.TECHEER) - .socialType(SocialType.GOOGLE) - .build() - ); - Resume resume = Resume.builder().id(resumeId).build(); - - when(resumeRepository.findById(resumeId)).thenReturn(Optional.of(resume)); - when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); - - // When & Then - assertThatThrownBy(() -> feedbackService.deleteFeedbackById(user, resumeId, feedbackId)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(ErrorCode.FEEDBACK_NOT_FOUND.getMessage()); - } - } - - @Nested - @DisplayName("getFeedbackByResumeId 메서드 테스트") - class GetFeedbackByResumeIdTest { - - @Test - @DisplayName("Given 존재하는 이력서와 피드백 목록, When 피드백 조회, Then 피드백 리스트 반환") - void getFeedbackByResumeId_Success() { - // Given - Long resumeId = 1L; - Resume resume = Resume.builder().id(resumeId).build(); - Feedback f1 = Feedback.builder().id(10L).resume(resume).build(); - Feedback f2 = Feedback.builder().id(11L).resume(resume).build(); - - when(resumeRepository.existsById(resumeId)).thenReturn(true); - when(feedbackRepository.findAllByResumeId(resumeId)).thenReturn(List.of(f1, f2)); - - // When - List feedbacks = feedbackService.getFeedbackByResumeId(resumeId); - - // Then - assertThat(feedbacks).hasSize(2); - } - - @Test - @DisplayName("Given 존재하지 않는 이력서, When 피드백 조회, Then RESUME_NOT_FOUND 예외 발생") - void getFeedbackByResumeId_ResumeNotFound() { - // Given - Long resumeId = 999L; - when(resumeRepository.existsById(resumeId)).thenReturn(false); - - // When & Then - assertThatThrownBy(() -> feedbackService.getFeedbackByResumeId(resumeId)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(ErrorCode.RESUME_NOT_FOUND.getMessage()); - } - } - - @Nested - @DisplayName("getAIFeedbackByResumeId 메서드 테스트") - class GetAIFeedbackByResumeIdTest { - - @Test - @DisplayName("Given 존재하는 AI 피드백, When 조회, Then 해당 AI 피드백 반환") - void getAIFeedbackByResumeId_Found() { - // Given - Long resumeId = 1L; - AIFeedback aiFeedback = AIFeedback.builder().resumeId(resumeId).feedback("Test AI").build(); - when(aiFeedbackRepository.findByResumeId(resumeId)).thenReturn(Optional.of(aiFeedback)); - - // When - AIFeedback result = feedbackService.getAIFeedbackByResumeId(resumeId); - - // Then - assertThat(result).isNotNull(); - assertThat(result.getFeedback()).isEqualTo("Test AI"); - } - - @Test - @DisplayName("Given AI 피드백 없음, When 조회, Then empty AI 피드백 반환") - void getAIFeedbackByResumeId_Empty() { - // Given - Long resumeId = 1L; - when(aiFeedbackRepository.findByResumeId(resumeId)).thenReturn(Optional.empty()); - - // When - AIFeedback result = feedbackService.getAIFeedbackByResumeId(resumeId); - - // Then - assertThat(result).isNotNull(); - assertThat(result.getFeedback()).isEqualTo("No AI Feedback available"); - } - } - - @Nested - @DisplayName("getFeedbacksByResumeId 메서드 테스트") - class GetFeedbacksByResumeIdTest { - - @Test - @DisplayName("Given 피드백 리스트, When 조회, Then 해당 리스트 반환") - void getFeedbacksByResumeId_Success() { - Long resumeId = 1L; - Feedback f1 = Feedback.builder().id(10L).build(); - Feedback f2 = Feedback.builder().id(11L).build(); - - when(feedbackRepository.findAllByResumeId(resumeId)).thenReturn(List.of(f1, f2)); - - List results = feedbackService.getFeedbacksByResumeId(resumeId); - assertThat(results).hasSize(2); - } - - @Test - @DisplayName("Given 피드백 없음, When 조회, Then 빈 리스트 반환") - void getFeedbacksByResumeId_Empty() { - Long resumeId = 1L; - when(feedbackRepository.findAllByResumeId(resumeId)).thenReturn(List.of()); - - List results = feedbackService.getFeedbacksByResumeId(resumeId); - assertThat(results).isEmpty(); - } - } -} +//package com.techeer.backend.api.resume.service; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.Assertions.assertThatThrownBy; +//import static org.mockito.Mockito.when; +// +//import com.techeer.backend.api.aifeedback.domain.AIFeedback; +//import com.techeer.backend.api.aifeedback.repository.AIFeedbackRepository; +//import com.techeer.backend.api.feedback.domain.Feedback; +//import com.techeer.backend.api.feedback.dto.request.FeedbackCreateRequest; +//import com.techeer.backend.api.feedback.repository.FeedbackRepository; +//import com.techeer.backend.api.feedback.service.FeedbackService; +//import com.techeer.backend.api.resume.domain.Resume; +//import com.techeer.backend.api.resume.repository.ResumeRepository; +//import com.techeer.backend.api.user.domain.Role; +//import com.techeer.backend.api.user.domain.SocialType; +//import com.techeer.backend.api.user.domain.User; +//import com.techeer.backend.api.user.repository.UserRepository; +//import com.techeer.backend.global.error.ErrorCode; +//import com.techeer.backend.global.error.exception.BusinessException; +//import java.util.List; +//import java.util.Optional; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Nested; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.extension.ExtendWith; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.Mockito; +//import org.mockito.junit.jupiter.MockitoExtension; +// +//@ExtendWith(MockitoExtension.class) +//class FeedbackServiceTest { +// +// +// @Mock +// private FeedbackRepository feedbackRepository; +// +// @Mock +// private ResumeRepository resumeRepository; +// +// @Mock +// private AIFeedbackRepository aiFeedbackRepository; +// +// @InjectMocks +// private FeedbackService feedbackService; +// +// @Mock +// private UserRepository userRepository; +// +// @BeforeEach +// void setUp() { +// feedbackRepository = Mockito.mock(FeedbackRepository.class); +// resumeRepository = Mockito.mock(ResumeRepository.class); +// aiFeedbackRepository = Mockito.mock(AIFeedbackRepository.class); +// +// feedbackService = new FeedbackService(feedbackRepository, resumeRepository, aiFeedbackRepository); +// } +// +// @Nested +// @DisplayName("createFeedback 메서드 테스트") +// class CreateFeedbackTest { +// +// @Test +// @DisplayName("Given 유효한 이력서와 유저, When 피드백 생성 요청, Then 피드백이 성공적으로 생성된다.") +// void createFeedback_Success() { +// // Given +// Long resumeId = 1L; +// User user = userRepository.save( +// User.builder() +// .email("john@example.com") +// .username("JohnDoe") +// .role(Role.TECHEER) +// .socialType(SocialType.GOOGLE) +// .build() +// ); +// +// FeedbackCreateRequest request = new FeedbackCreateRequest( +// "Valid content", +// 100.5, 200.5, +// null, null, +// 1 +// ); +// Resume mockResume = Resume.builder().id(resumeId).build(); +// +// when(resumeRepository.findByIdAndDeletedAtIsNull(resumeId)).thenReturn(Optional.of(mockResume)); +// when(feedbackRepository.save(Mockito.any(Feedback.class))) +// .thenAnswer(invocation -> invocation.getArgument(0)); +// +// // When +// Feedback createdFeedback = feedbackService.createFeedback(user, resumeId, request); +// +// // Then +// assertThat(createdFeedback).isNotNull(); +// assertThat(createdFeedback.getContent()).isEqualTo("Valid content"); +// assertThat(createdFeedback.getResume()).isEqualTo(mockResume); +// } +// +// @Test +// @DisplayName("Given 존재하지 않는 이력서, When 피드백 생성 요청, Then RESUME_NOT_FOUND 예외가 발생한다.") +// void createFeedback_ResumeNotFound() { +// // Given +// Long resumeId = 999L; +// User user = userRepository.save( +// User.builder() +// .email("john@example.com") +// .username("JohnDoe") +// .role(Role.TECHEER) +// .socialType(SocialType.GOOGLE) +// .build() +// ); +// FeedbackCreateRequest request = new FeedbackCreateRequest( +// "Content", +// 100.5, 200.5, +// null, null, +// 1 +// ); +// +// when(resumeRepository.findByIdAndDeletedAtIsNull(resumeId)).thenReturn(Optional.empty()); +// +// // When & Then +// assertThatThrownBy(() -> feedbackService.createFeedback(user, resumeId, request)) +// .isInstanceOf(BusinessException.class) +// .hasMessageContaining(ErrorCode.RESUME_NOT_FOUND.getMessage()); +// } +// } +// +// @Nested +// @DisplayName("deleteFeedbackById 메서드 테스트") +// class DeleteFeedbackTest { +// +// @Test +// @DisplayName("Given 존재하지 않는 이력서, When 피드백 삭제 요청, Then RESUME_NOT_FOUND 예외 발생") +// void deleteFeedback_ResumeNotFound() { +// // Given +// Long resumeId = 999L; +// Long feedbackId = 10L; +// User user = userRepository.save( +// User.builder() +// .email("john@example.com") +// .username("JohnDoe") +// .role(Role.TECHEER) +// .socialType(SocialType.GOOGLE) +// .build() +// ); +// +// when(resumeRepository.findById(resumeId)).thenReturn(Optional.empty()); +// +// // When & Then +// assertThatThrownBy(() -> feedbackService.deleteFeedbackById(user, resumeId, feedbackId)) +// .isInstanceOf(BusinessException.class) +// .hasMessageContaining(ErrorCode.RESUME_NOT_FOUND.getMessage()); +// } +// +// @Test +// @DisplayName("Given 존재하지 않는 피드백, When 피드백 삭제 요청, Then FEEDBACK_NOT_FOUND 예외 발생") +// void deleteFeedback_FeedbackNotFound() { +// // Given +// Long resumeId = 1L; +// Long feedbackId = 999L; +// User user = userRepository.save( +// User.builder() +// .email("john@example.com") +// .username("JohnDoe") +// .role(Role.TECHEER) +// .socialType(SocialType.GOOGLE) +// .build() +// ); +// Resume resume = Resume.builder().id(resumeId).build(); +// +// when(resumeRepository.findById(resumeId)).thenReturn(Optional.of(resume)); +// when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); +// +// // When & Then +// assertThatThrownBy(() -> feedbackService.deleteFeedbackById(user, resumeId, feedbackId)) +// .isInstanceOf(BusinessException.class) +// .hasMessageContaining(ErrorCode.FEEDBACK_NOT_FOUND.getMessage()); +// } +// } +// +// @Nested +// @DisplayName("getFeedbackByResumeId 메서드 테스트") +// class GetFeedbackByResumeIdTest { +// +// @Test +// @DisplayName("Given 존재하는 이력서와 피드백 목록, When 피드백 조회, Then 피드백 리스트 반환") +// void getFeedbackByResumeId_Success() { +// // Given +// Long resumeId = 1L; +// Resume resume = Resume.builder().id(resumeId).build(); +// Feedback f1 = Feedback.builder().id(10L).resume(resume).build(); +// Feedback f2 = Feedback.builder().id(11L).resume(resume).build(); +// +// when(resumeRepository.existsById(resumeId)).thenReturn(true); +// when(feedbackRepository.findAllByResumeId(resumeId)).thenReturn(List.of(f1, f2)); +// +// // When +// List feedbacks = feedbackService.getFeedbackByResumeId(resumeId); +// +// // Then +// assertThat(feedbacks).hasSize(2); +// } +// +// @Test +// @DisplayName("Given 존재하지 않는 이력서, When 피드백 조회, Then RESUME_NOT_FOUND 예외 발생") +// void getFeedbackByResumeId_ResumeNotFound() { +// // Given +// Long resumeId = 999L; +// when(resumeRepository.existsById(resumeId)).thenReturn(false); +// +// // When & Then +// assertThatThrownBy(() -> feedbackService.getFeedbackByResumeId(resumeId)) +// .isInstanceOf(BusinessException.class) +// .hasMessageContaining(ErrorCode.RESUME_NOT_FOUND.getMessage()); +// } +// } +// +// @Nested +// @DisplayName("getAIFeedbackByResumeId 메서드 테스트") +// class GetAIFeedbackByResumeIdTest { +// +// @Test +// @DisplayName("Given 존재하는 AI 피드백, When 조회, Then 해당 AI 피드백 반환") +// void getAIFeedbackByResumeId_Found() { +// // Given +// Long resumeId = 1L; +// AIFeedback aiFeedback = AIFeedback.builder().resumeId(resumeId).feedback("Test AI").build(); +// when(aiFeedbackRepository.findByResumeId(resumeId)).thenReturn(Optional.of(aiFeedback)); +// +// // When +// AIFeedback result = feedbackService.getAIFeedbackByResumeId(resumeId); +// +// // Then +// assertThat(result).isNotNull(); +// assertThat(result.getFeedback()).isEqualTo("Test AI"); +// } +// +// @Test +// @DisplayName("Given AI 피드백 없음, When 조회, Then empty AI 피드백 반환") +// void getAIFeedbackByResumeId_Empty() { +// // Given +// Long resumeId = 1L; +// when(aiFeedbackRepository.findByResumeId(resumeId)).thenReturn(Optional.empty()); +// +// // When +// AIFeedback result = feedbackService.getAIFeedbackByResumeId(resumeId); +// +// // Then +// assertThat(result).isNotNull(); +// assertThat(result.getFeedback()).isEqualTo("No AI Feedback available"); +// } +// } +// +// @Nested +// @DisplayName("getFeedbacksByResumeId 메서드 테스트") +// class GetFeedbacksByResumeIdTest { +// +// @Test +// @DisplayName("Given 피드백 리스트, When 조회, Then 해당 리스트 반환") +// void getFeedbacksByResumeId_Success() { +// Long resumeId = 1L; +// Feedback f1 = Feedback.builder().id(10L).build(); +// Feedback f2 = Feedback.builder().id(11L).build(); +// +// when(feedbackRepository.findAllByResumeId(resumeId)).thenReturn(List.of(f1, f2)); +// +// List results = feedbackService.getFeedbacksByResumeId(resumeId); +// assertThat(results).hasSize(2); +// } +// +// @Test +// @DisplayName("Given 피드백 없음, When 조회, Then 빈 리스트 반환") +// void getFeedbacksByResumeId_Empty() { +// Long resumeId = 1L; +// when(feedbackRepository.findAllByResumeId(resumeId)).thenReturn(List.of()); +// +// List results = feedbackService.getFeedbacksByResumeId(resumeId); +// assertThat(results).isEmpty(); +// } +// } +//}