From dafc86fa3f8816d2cbc1c7949821f63ac8c26fa5 Mon Sep 17 00:00:00 2001 From: daeun084 <030804jk@naver.com> Date: Thu, 7 Nov 2024 16:07:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=97=AD=EB=9F=89=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=20=EC=9E=AC=EC=9A=94=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../constant/AnalysisSuccessStatus.java | 3 +- .../controller/AnalysisController.java | 9 + .../dev/domain/analysis/entity/Analysis.java | 15 +- .../exception/enums/AnalysisErrorStatus.java | 5 +- .../analysis/service/AnalysisService.java | 164 ++++++++++++++++-- 5 files changed, 176 insertions(+), 20 deletions(-) diff --git a/src/main/java/corecord/dev/domain/analysis/constant/AnalysisSuccessStatus.java b/src/main/java/corecord/dev/domain/analysis/constant/AnalysisSuccessStatus.java index 6da12bb..ce9e5f4 100644 --- a/src/main/java/corecord/dev/domain/analysis/constant/AnalysisSuccessStatus.java +++ b/src/main/java/corecord/dev/domain/analysis/constant/AnalysisSuccessStatus.java @@ -8,7 +8,8 @@ @Getter @AllArgsConstructor public enum AnalysisSuccessStatus implements BaseSuccessStatus { - ANALYSIS_GET_SUCCESS(HttpStatus.CREATED, "S502", "역량별 경험 조회가 성공적으로 완료되었습니다."), + ANALYSIS_POST_SUCCESS(HttpStatus.CREATED, "S501", "역량별 경험 분석이 성공적으로 완료되었습니다."), + ANALYSIS_GET_SUCCESS(HttpStatus.OK, "S502", "역량별 경험 조회가 성공적으로 완료되었습니다."), ANALYSIS_UPDATE_SUCCESS(HttpStatus.OK, "S701", "역량별 경험 수정이 성공적으로 완료되었습니다."), ANALYSIS_DELETE_SUCCESS(HttpStatus.OK, "S702", "역량별 경험 삭제가 성공적으로 완료되었습니다."), KEYWORD_LIST_GET_SUCCESS(HttpStatus.OK, "S505", "역량 키워드 리스트 조회가 성공적으로 완료되었습니다."), diff --git a/src/main/java/corecord/dev/domain/analysis/controller/AnalysisController.java b/src/main/java/corecord/dev/domain/analysis/controller/AnalysisController.java index e5b4a24..9affca9 100644 --- a/src/main/java/corecord/dev/domain/analysis/controller/AnalysisController.java +++ b/src/main/java/corecord/dev/domain/analysis/controller/AnalysisController.java @@ -16,6 +16,15 @@ public class AnalysisController { private final AnalysisService analysisService; + @PostMapping("/{recordId}") + public ResponseEntity> postAnalysis( + @UserId Long userId, + @PathVariable(name = "recordId") Long recordId + ) { + AnalysisResponse.AnalysisDto analysisResponse = analysisService.postAnalysis(userId, recordId); + return ApiResponse.success(AnalysisSuccessStatus.ANALYSIS_POST_SUCCESS, analysisResponse); + } + @GetMapping("/{analysisId}") public ResponseEntity> getAnalysis( @UserId Long userId, diff --git a/src/main/java/corecord/dev/domain/analysis/entity/Analysis.java b/src/main/java/corecord/dev/domain/analysis/entity/Analysis.java index fd1b469..b342203 100644 --- a/src/main/java/corecord/dev/domain/analysis/entity/Analysis.java +++ b/src/main/java/corecord/dev/domain/analysis/entity/Analysis.java @@ -30,7 +30,7 @@ public class Analysis extends BaseEntity { private String comment; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "record_id", nullable = false) + @JoinColumn(name = "record_id", nullable = true) private Record record; @BatchSize(size = 3) @@ -41,4 +41,17 @@ public void updateContent(String content) { if (content != null && !content.isEmpty()) this.content = content; } + + public void updateComment(String comment) { + if (!comment.isEmpty()) { + this.comment = comment; + } + } + + public void addAbility(Ability ability) { + if (ability != null) { + this.abilityList.add(ability); + } + } + } diff --git a/src/main/java/corecord/dev/domain/analysis/exception/enums/AnalysisErrorStatus.java b/src/main/java/corecord/dev/domain/analysis/exception/enums/AnalysisErrorStatus.java index 78f8af2..f4399b2 100644 --- a/src/main/java/corecord/dev/domain/analysis/exception/enums/AnalysisErrorStatus.java +++ b/src/main/java/corecord/dev/domain/analysis/exception/enums/AnalysisErrorStatus.java @@ -9,9 +9,12 @@ @AllArgsConstructor public enum AnalysisErrorStatus implements BaseErrorStatus { INVALID_KEYWORD(HttpStatus.BAD_REQUEST, "E400_INVALID_KEYWORD", "역량 분석에 존재하지 않는 키워드입니다."), + OVERFLOW_ANALYSIS_CONTENT(HttpStatus.BAD_REQUEST, "E0400_OVERFLOW_CONTENT", "경험 기록 내용은 500자 이내여야 합니다."), + OVERFLOW_ANALYSIS_COMMENT(HttpStatus.BAD_REQUEST, "E0400_OVERFLOW_COMMENT", "경험 기록 코멘트는 200자 이내여야 합니다."), + OVERFLOW_ANALYSIS_KEYWORD_CONTENT(HttpStatus.BAD_REQUEST, "E0400_OVERFLOW_KEYWORD_CONTENT", "경험 기록 키워드별 내용은 200자 이내여야 합니다."), USER_ANALYSIS_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "E401_ANALYSIS_UNAUTHORIZED", "유저가 역량 분석에 대한 권한이 없습니다."), ANALYSIS_NOT_FOUND(HttpStatus.NOT_FOUND, "E0404_ANALYSIS", "존재하지 않는 역량 분석입니다."), - INVALID_ABILITY_ANALYSIS(HttpStatus.BAD_REQUEST, "E400_INVALID_ANALYSIS", "역량 분석 데이터 파싱 중 오류가 발생했습니다.") + INVALID_ABILITY_ANALYSIS(HttpStatus.BAD_REQUEST, "E400_INVALID_ANALYSIS", "역량 분석 데이터 파싱 중 오류가 발생했습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/corecord/dev/domain/analysis/service/AnalysisService.java b/src/main/java/corecord/dev/domain/analysis/service/AnalysisService.java index d6dacb5..8b12d96 100644 --- a/src/main/java/corecord/dev/domain/analysis/service/AnalysisService.java +++ b/src/main/java/corecord/dev/domain/analysis/service/AnalysisService.java @@ -20,8 +20,10 @@ import corecord.dev.domain.record.entity.Record; import corecord.dev.domain.record.exception.enums.RecordErrorStatus; import corecord.dev.domain.record.exception.model.RecordException; +import corecord.dev.domain.record.repository.RecordRepository; import corecord.dev.domain.user.entity.User; import corecord.dev.domain.user.repository.UserRepository; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,40 +38,85 @@ public class AnalysisService { private final AnalysisRepository analysisRepository; private final AbilityRepository abilityRepository; private final UserRepository userRepository; + private final RecordRepository recordRepository; private final ClovaService clovaService; + private final EntityManager entityManager; + /* + * CLOVA STUDIO룰 활용해 역량 분석 객체를 생성 후 반환 + * @param record + * @param user + * @return + */ @Transactional - public void createAnalysis(Record record, User user) { + public Analysis createAnalysis(Record record, User user) { // MEMO 경험 기록이라면, CLOVA STUDIO를 이용해 요약 진행 - String content = record.isMemoType() - ? generateMemoSummary(record.getContent()) - : record.getContent(); + String content = getRecordContent(record); // CLOVA STUDIO API 호출 - String apiResponse = generateAbilityAnalysis(content); - AnalysisAiResponse response = parseAnalysisAiResponse(apiResponse); + AnalysisAiResponse response = callClovaStudioApi(content); // Analysis 객체 생성 및 저장 Analysis analysis = AnalysisConverter.toAnalysis(content, response.getComment(), record); analysisRepository.save(analysis); // Ability 객체 생성 및 저장 - int abilityCount = 0; - for (Map.Entry entry : response.getKeywordList().entrySet()) { - Keyword keyword = Keyword.getName(entry.getKey()); + parseAndSaveAbilities(response.getKeywordList(), analysis, user); - if (keyword == null) continue; + return analysis; + } - Ability ability = AnalysisConverter.toAbility(keyword, entry.getValue(), analysis, user); - abilityRepository.save(ability); - abilityCount++; - } + /* + * CLOVA STUDIO를 활용해 역량 분석을 재수행함 Analysis 객체 데이터 교체 후 반환 + * @param record + * @param user + * @return + */ + @Transactional + public Analysis recreateAnalysis(Record record, User user) { + Analysis analysis = record.getAnalysis(); - if (abilityCount < 1 || abilityCount > 3) { - throw new AnalysisException(AnalysisErrorStatus.INVALID_ABILITY_ANALYSIS); - } + // MEMO 경험 기록이라면, CLOVA STUDIO를 이용해 요약 진행 + String content = getRecordContent(record); + + // CLOVA STUDIO API 호출 + AnalysisAiResponse response = callClovaStudioApi(content); + + // Analysis 객체 수정 + analysis.updateContent(content); + analysis.updateComment(response.getComment()); + + // 기존 Ability 객체 삭제 + deleteOriginAbilityList(analysis); + + // Ability 객체 생성 및 저장 + parseAndSaveAbilities(response.getKeywordList(), analysis, user); + + return analysis; + } + + /* + * recordId를 받아, 해당 경험 기록에 대한 역량 분석을 수행 후 생성된 역량 분석 상세 정보를 반환 + * @param userId + * @param recordId + * @return + */ + @Transactional + public AnalysisResponse.AnalysisDto postAnalysis(Long userId, Long recordId) { + User user = findUserById(userId); + Record record = findRecordById(recordId); + + // User-Record 권한 유효성 검증 + validIsUserAuthorizedForRecord(user, record); + + // 역량 분석 API 호출 + Analysis analysis = record.getAnalysis() == null ? + createAnalysis(record, user) : + recreateAnalysis(record, user); // 기존 Analysis 객체가 있을 경우 교체 + + return AnalysisConverter.toAnalysisDto(analysis); } /* @@ -158,6 +205,47 @@ public AnalysisResponse.GraphDto getKeywordGraph(Long userId) { return AnalysisConverter.toGraphDto(keywordGraph); } + + // CLOVA STUDIO를 통해 얻은 키워드 정보 파싱 + @Transactional + public void parseAndSaveAbilities(Map keywordList, Analysis analysis, User user) { + int abilityCount = 0; + for (Map.Entry entry : keywordList.entrySet()) { + Keyword keyword = Keyword.getName(entry.getKey()); + + if (keyword == null) continue; + + Ability ability = AnalysisConverter.toAbility(keyword, entry.getValue(), analysis, user); + abilityRepository.save(ability); + analysis.addAbility(ability); + abilityCount++; + } + + if (abilityCount < 1 || abilityCount > 3) { + throw new AnalysisException(AnalysisErrorStatus.INVALID_ABILITY_ANALYSIS); + } + } + + private AnalysisAiResponse callClovaStudioApi(String content) { + String apiResponse = generateAbilityAnalysis(content); + AnalysisAiResponse response = parseAnalysisAiResponse(apiResponse); + + // 글자 수 validation + validAnalysisCommentLength(response.getComment()); + validAnalysisKeywordContentLength(response.getKeywordList()); + + return response; + } + + private String getRecordContent(Record record) { + String content = record.isMemoType() + ? generateMemoSummary(record.getContent()) + : record.getContent(); + + validAnalysisContentLength(content); + + return content; + } private String generateAbilityAnalysis(String content) { ClovaRequest clovaRequest = ClovaRequest.createAnalysisRequest(content); @@ -174,6 +262,30 @@ private void validIsUserAuthorizedForAnalysis(User user, Analysis analysis) { throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); } + private void validIsUserAuthorizedForRecord(User user, Record record) { + if (!record.getUser().equals(user)) + throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); + } + + private void validAnalysisContentLength(String content) { + if (content.isEmpty() || content.length() > 500) + throw new AnalysisException(AnalysisErrorStatus.OVERFLOW_ANALYSIS_CONTENT); + } + + private void validAnalysisCommentLength(String comment) { + if (comment.isEmpty() || comment.length() > 200) + throw new AnalysisException(AnalysisErrorStatus.OVERFLOW_ANALYSIS_COMMENT); + } + + private void validAnalysisKeywordContentLength(Map keywordList) { + for (Map.Entry entry : keywordList.entrySet()) { + String keyContent = entry.getValue(); + + if (keyContent.isEmpty() || keyContent.length() > 200) + throw new AnalysisException(AnalysisErrorStatus.OVERFLOW_ANALYSIS_KEYWORD_CONTENT); + } + } + private void updateAbilityContents(Analysis analysis, Map abilityMap) { abilityMap.forEach((keyword, content) -> { Keyword key = Keyword.getName(keyword); @@ -199,6 +311,19 @@ private AnalysisAiResponse parseAnalysisAiResponse(String aiResponse) { } } + private void deleteOriginAbilityList(Analysis analysis) { + List abilityList = analysis.getAbilityList(); + + if (!abilityList.isEmpty()) { + // 연관된 abilities 삭제 + abilityRepository.deleteAll(abilityList); + + // Analysis에서 abilities 리스트 비우기 + analysis.getAbilityList().clear(); + entityManager.flush(); + } + } + private List findKeywordList(User user) { return analysisRepository.getKeywordList(user).stream() .map(Keyword::getValue) @@ -215,6 +340,11 @@ private Analysis findAnalysisById(Long analysisId) { .orElseThrow(() -> new AnalysisException(AnalysisErrorStatus.ANALYSIS_NOT_FOUND)); } + private Record findRecordById(Long recordId) { + return recordRepository.findRecordById(recordId) + .orElseThrow(() -> new RecordException(RecordErrorStatus.RECORD_NOT_FOUND)); + } + private List findKeywordGraph(User user) { return abilityRepository.findKeywordStateDtoList(user); }