Conversation
Summary of ChangesHello @unifolio0, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the resume evaluation feature by introducing an asynchronous processing system. Users can now submit their resumes and portfolios for AI-driven evaluation, receiving an immediate response with an evaluation ID while the actual evaluation runs in the background. This update also extends the evaluation capability to non-members, storing their results temporarily. Alongside these functional changes, the PR includes refactoring of career material management services, integration of PDF text extraction, and updates to API endpoints and database schema to support the new asynchronous flow and data storage requirements. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. PDFs now flow, AI reads, then gives its thoughts, Async, fast, smooth. Footnotes
|
There was a problem hiding this comment.
Code Review
안녕하세요. 이번 PR은 이력서 비동기 평가라는 큰 기능을 추가하는 내용이네요. 새로운 API 엔드포인트, 비동기 처리 서비스, PDF 텍스트 추출, 데이터베이스 스키마 업데이트 등 많은 변경사항이 포함되어 있습니다. 특히 GPT-3.5-turbo API 호출 실패 시 Bedrock으로 폴백하는 비동기 처리 로직은 시스템 안정성을 크게 향상시키는 좋은 구현입니다. 또한 기존 서비스를 CareerMaterialsService, PdfUploadService 등으로 리팩토링하여 코드 구조와 관심사 분리를 개선한 점도 훌륭합니다. 전반적으로 완성도 높은 기여라고 생각합니다. 다만, 코드 리뷰 중 한 가지 치명적인 버그와 한 가지 개선점을 발견하여 아래에 코멘트를 남겼습니다. 확인 부탁드립니다.
| public String resolveResumeCdnPath(Long memberId, String s3Key) { | ||
| return AwsConstant.CLOUD_FRONT_DOMAIN_URL + resumeS3Path + memberId + FOLDER_DELIMITER + s3Key | ||
| + PDF_FILE_EXTENSION; | ||
| } |
There was a problem hiding this comment.
resolveResumeCdnPath 메소드의 구현에 버그가 있습니다. s3Key 파라미터는 resolveResumeS3Key 메소드에서 생성된 전체 S3 객체 키(resumes/123/some-title-uuid.pdf와 같은 형식)를 전달받습니다. 하지만 현재 구현은 이 전체 키에 다시 경로와 확장자를 덧붙여 잘못된 CDN URL을 생성합니다. 예를 들어, https://.../resumes/123/resumes/123/my-resume-uuid.pdf.pdf 와 같은 잘못된 URL이 만들어질 수 있습니다. s3Key가 이미 전체 경로이므로, CloudFront 도메인만 앞에 붙여주면 올바른 URL이 생성됩니다. 이 수정으로 memberId 파라미터가 사용되지 않게 되는데, 추후 리팩토링 시 시그니처에서 제거하는 것을 고려해 보세요.
public String resolveResumeCdnPath(Long memberId, String s3Key) {
return AwsConstant.CLOUD_FRONT_DOMAIN_URL + s3Key;
}| public String resolvePortfolioCdnPath(Long memberId, String s3Key) { | ||
| return AwsConstant.CLOUD_FRONT_DOMAIN_URL + portfolioS3Path + memberId + FOLDER_DELIMITER + s3Key | ||
| + PDF_FILE_EXTENSION; | ||
| } |
There was a problem hiding this comment.
| public void processAndEvaluateMixedAsync(Long evaluationId, Member member, | ||
| ResumeFileData resumeFileData, ResumeFileData portfolioFileData, | ||
| MemberResume existingResume, MemberPortfolio existingPortfolio, | ||
| String jobPosition, String jobDescription, String jobCareer) { | ||
| Map<String, String> mdcContext = MDC.getCopyOfContextMap(); | ||
| executor.execute(() -> { | ||
| try { | ||
| setMdcContext(mdcContext); | ||
|
|
||
| String resumeText; | ||
| String portfolioText; | ||
| MemberResume finalResume = existingResume; | ||
| MemberPortfolio finalPortfolio = existingPortfolio; | ||
|
|
||
| if (resumeFileData != null && !resumeFileData.isEmpty()) { | ||
| resumeText = extractTextSafely(resumeFileData); | ||
| if (resumeText == null || resumeText.isBlank()) { | ||
| log.error("이력서 텍스트 추출 실패 - evaluationId: {}", evaluationId); | ||
| resumeEvaluationService.updateFailed(evaluationId); | ||
| return; | ||
| } | ||
| finalResume = pdfUploadService.saveResume( | ||
| resumeFileData.content(), resumeFileData.filename(), member, resumeText); | ||
| } else if (existingResume != null) { | ||
| resumeText = getOrExtractText(existingResume.getContent(), existingResume.getResumeUrl()); | ||
| } else { | ||
| log.error("이력서 없음 - evaluationId: {}", evaluationId); | ||
| resumeEvaluationService.updateFailed(evaluationId); | ||
| return; | ||
| } | ||
|
|
||
| if (resumeText == null || resumeText.isBlank()) { | ||
| log.error("이력서 텍스트 없음 - evaluationId: {}", evaluationId); | ||
| resumeEvaluationService.updateFailed(evaluationId); | ||
| return; | ||
| } | ||
|
|
||
| if (portfolioFileData != null && !portfolioFileData.isEmpty()) { | ||
| portfolioText = extractTextSafely(portfolioFileData); | ||
| if (portfolioText != null && !portfolioText.isBlank()) { | ||
| finalPortfolio = pdfUploadService.savePortfolio( | ||
| portfolioFileData.content(), portfolioFileData.filename(), member, portfolioText); | ||
| } | ||
| } else if (existingPortfolio != null) { | ||
| portfolioText = getOrExtractText(existingPortfolio.getContent(), | ||
| existingPortfolio.getPortfolioUrl()); | ||
| } else { | ||
| portfolioText = null; | ||
| } | ||
|
|
||
| resumeEvaluationService.updateMemberResume(evaluationId, finalResume, finalPortfolio); | ||
|
|
||
| ResumeEvaluationRequest evalRequest = new ResumeEvaluationRequest( | ||
| resumeText, portfolioText, jobPosition, jobDescription, jobCareer | ||
| ); | ||
| evaluateMemberAsync(evaluationId, evalRequest); | ||
| } catch (Exception e) { | ||
| log.error("혼합 이력서 평가 처리 실패 - evaluationId: {}", evaluationId, e); | ||
| resumeEvaluationService.updateFailed(evaluationId); | ||
| } finally { | ||
| MDC.clear(); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
processAndEvaluateMixedAsync 메소드에서 기존에 저장된 이력서(existingResume)나 포트폴리오(existingPortfolio)의 내용(content)이 DB에 없는 경우, S3에서 파일을 다운로드하여 텍스트를 추출합니다. 하지만 이렇게 추출된 텍스트를 다시 DB 엔티티에 저장하지 않아, 동일한 파일로 평가를 요청할 때마다 불필요하게 S3 다운로드 및 텍스트 추출 과정을 반복하게 됩니다.
추출된 텍스트를 MemberResume 및 MemberPortfolio 엔티티에 업데이트하여 캐싱하면 성능을 향상시킬 수 있습니다. getOrExtractText 호출 후에 내용이 없었는지 확인하고 업데이트하는 로직을 추가하는 것을 제안합니다.
예시:
// 이력서 처리 부분
} else if (existingResume != null) {
resumeText = getOrExtractText(existingResume.getContent(), existingResume.getResumeUrl());
if (resumeText != null && !resumeText.isBlank() && !existingResume.hasContent()) {
existingResume.updateContent(resumeText);
}
}
// ...
// 포트폴리오 처리 부분
} else if (existingPortfolio != null) {
portfolioText = getOrExtractText(existingPortfolio.getContent(), existingPortfolio.getPortfolioUrl());
if (portfolioText != null && !portfolioText.isBlank() && !existingPortfolio.hasContent()) {
existingPortfolio.updateContent(portfolioText);
}
}
closed #
작업 내용
스크린샷
참고 사항