diff --git a/src/main/java/corecord/dev/domain/ability/application/AbilityDbService.java b/src/main/java/corecord/dev/domain/ability/application/AbilityDbService.java index 04ceeb2..3c66841 100644 --- a/src/main/java/corecord/dev/domain/ability/application/AbilityDbService.java +++ b/src/main/java/corecord/dev/domain/ability/application/AbilityDbService.java @@ -5,7 +5,6 @@ import corecord.dev.domain.ability.domain.entity.Keyword; import corecord.dev.domain.ability.domain.repository.AbilityRepository; import corecord.dev.domain.folder.domain.entity.Folder; -import corecord.dev.domain.user.domain.entity.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -37,12 +36,12 @@ public void deleteAbilityList(List abilityList) { abilityRepository.deleteAll(abilityList); } - public List findKeywordGraph(User user) { - return abilityRepository.findKeywordStateDtoList(user); + public List findKeywordGraph(Long userId) { + return abilityRepository.findKeywordStateDtoList(userId); } - public List findKeywordList(User user) { - return abilityRepository.getKeywordList(user).stream() + public List findKeywordList(Long userId) { + return abilityRepository.getKeywordList(userId).stream() .map(Keyword::getValue) .toList(); } diff --git a/src/main/java/corecord/dev/domain/ability/application/AbilityService.java b/src/main/java/corecord/dev/domain/ability/application/AbilityService.java index f460456..1955880 100644 --- a/src/main/java/corecord/dev/domain/ability/application/AbilityService.java +++ b/src/main/java/corecord/dev/domain/ability/application/AbilityService.java @@ -1,110 +1,18 @@ package corecord.dev.domain.ability.application; -import corecord.dev.domain.ability.domain.converter.AbilityConverter; + import corecord.dev.domain.ability.domain.dto.response.AbilityResponse; -import corecord.dev.domain.ability.domain.entity.Ability; -import corecord.dev.domain.ability.domain.entity.Keyword; -import corecord.dev.domain.ability.status.AbilityErrorStatus; -import corecord.dev.domain.ability.exception.AbilityException; import corecord.dev.domain.analysis.domain.entity.Analysis; -import corecord.dev.domain.user.application.UserDbService; import corecord.dev.domain.user.domain.entity.User; -import jakarta.persistence.EntityManager; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.List; import java.util.Map; -@Service -@RequiredArgsConstructor -public class AbilityService { - private final EntityManager entityManager; - private final UserDbService userDbService; - private final AbilityDbService abilityDbService; - - /* - * user의 역량 키워드 리스트를 반환 - * @param userId - * @return - */ - @Transactional(readOnly = true) - public AbilityResponse.KeywordListDto getKeywordList(Long userId) { - User user = userDbService.findUserById(userId); - List keywordList = abilityDbService.findKeywordList(user); - - return AbilityConverter.toKeywordListDto(keywordList); - } - - /* - * user의 각 역량 키워드에 대한 개수, 퍼센티지 정보를 반환 - * @param userId - * @return - */ - @Transactional(readOnly = true) - public AbilityResponse.GraphDto getKeywordGraph(Long userId) { - User user = userDbService.findUserById(userId); - - // keyword graph 정보 조회 - List keywordGraph = abilityDbService.findKeywordGraph(user); - - return AbilityConverter.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 = AbilityConverter.toAbility(keyword, entry.getValue(), analysis, user); - abilityDbService.saveAbility(ability); - - if (analysis.getAbilityList() != null) - analysis.addAbility(ability); - abilityCount++; - } - - validAbilityCount(abilityCount); - } - - private void validAbilityCount(int abilityCount) { - if (abilityCount < 1 || abilityCount > 3) - throw new AbilityException(AbilityErrorStatus.INVALID_ABILITY_KEYWORD); - } - - @Transactional - public void deleteOriginAbilityList(Analysis analysis) { - List abilityList = analysis.getAbilityList(); - - if (!abilityList.isEmpty()) { - // 연관된 abilities 삭제 - abilityDbService.deleteAbilityList(abilityList); - - // Analysis에서 abilities 리스트 비우기 - analysis.getAbilityList().clear(); - entityManager.flush(); - } - } +public interface AbilityService { - @Transactional - public void updateAbilityContents(Analysis analysis, Map abilityMap) { - abilityMap.forEach((keyword, content) -> { - Keyword key = Keyword.getName(keyword); - Ability ability = findAbilityByKeyword(analysis, key); - ability.updateContent(content); - }); - } + AbilityResponse.KeywordListDto getKeywordList(Long userId); + AbilityResponse.GraphDto getKeywordGraph(Long userId); - private Ability findAbilityByKeyword(Analysis analysis, Keyword key) { - // keyword가 기존 역량 분석에 존재했는지 확인 - return analysis.getAbilityList().stream() - .filter(ability -> ability.getKeyword().equals(key)) - .findFirst() - .orElseThrow(() -> new AbilityException(AbilityErrorStatus.INVALID_KEYWORD)); - } + void parseAndSaveAbilities(Map keywordList, Analysis analysis, User user); + void deleteOriginAbilityList(Analysis analysis); + void updateAbilityContents(Analysis analysis, Map abilityMap); } diff --git a/src/main/java/corecord/dev/domain/ability/application/AbilityServiceImpl.java b/src/main/java/corecord/dev/domain/ability/application/AbilityServiceImpl.java new file mode 100644 index 0000000..5855989 --- /dev/null +++ b/src/main/java/corecord/dev/domain/ability/application/AbilityServiceImpl.java @@ -0,0 +1,131 @@ +package corecord.dev.domain.ability.application; + +import corecord.dev.domain.ability.domain.converter.AbilityConverter; +import corecord.dev.domain.ability.domain.dto.response.AbilityResponse; +import corecord.dev.domain.ability.domain.entity.Ability; +import corecord.dev.domain.ability.domain.entity.Keyword; +import corecord.dev.domain.ability.exception.AbilityException; +import corecord.dev.domain.ability.status.AbilityErrorStatus; +import corecord.dev.domain.analysis.domain.entity.Analysis; +import corecord.dev.domain.user.domain.entity.User; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AbilityServiceImpl implements AbilityService { + + private final EntityManager entityManager; + private final AbilityDbService abilityDbService; + + /** + * user의 역량 키워드 리스트를 반환합니다. + * 정렬 기준: 개수 내림차순, 최근 생성 순 + * + * @param userId + * @return String type 키워드 명칭 리스트 + */ + @Override + @Transactional(readOnly = true) + public AbilityResponse.KeywordListDto getKeywordList(Long userId) { + List keywordList = abilityDbService.findKeywordList(userId); + return AbilityConverter.toKeywordListDto(keywordList); + } + + /** + * user의 각 역량 키워드에 대한 개수, 퍼센티지 정보를 반환합니다. + * 정렬 기준: 퍼센티지 내림차순 + * + * @param userId + * @return 각 키워드의 명칭, 개수, 전체 키워드 중 퍼센트 정보를 담은 리스트 + */ + @Override + @Transactional(readOnly = true) + public AbilityResponse.GraphDto getKeywordGraph(Long userId) { + List keywordGraph = abilityDbService.findKeywordGraph(userId); + return AbilityConverter.toGraphDto(keywordGraph); + } + + /** + * AI를 통해 얻은 키워드 정보를 파싱해 저장합니다. + * keywordList를 순회하며 최대 3개의 Keyword를 파싱합니다. + * 파싱된 데이터를 기반으로 Ability entity를 생성 및 저장합니다. + * + * @param keywordList AI로부터 받은 {키워드: 키워드 코멘트} 데이터 + * @param analysis + * @param user + */ + @Override + @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 = AbilityConverter.toAbility(keyword, entry.getValue(), analysis, user); + abilityDbService.saveAbility(ability); + + if (analysis.getAbilityList() != null) + analysis.addAbility(ability); + abilityCount++; + } + validAbilityCount(abilityCount); + } + + private void validAbilityCount(int abilityCount) { + if (abilityCount < 1 || abilityCount > 3) + throw new AbilityException(AbilityErrorStatus.INVALID_ABILITY_KEYWORD); + } + + /** + * Analysis와 연관된 모든 Ability entity를 제거합니다. + * + * @param analysis + */ + @Override + @Transactional + public void deleteOriginAbilityList(Analysis analysis) { + List abilityList = analysis.getAbilityList(); + + if (!abilityList.isEmpty()) { + // 연관된 abilities 삭제 + abilityDbService.deleteAbilityList(abilityList); + + // Analysis에서 abilities 리스트 비우기 + analysis.getAbilityList().clear(); + entityManager.flush(); + } + } + + /** + * 기존 Analysis의 역량 분석 데이터를 변경합니다. + * 기존 keyword의 content를 새로운 응답값으로 변경합니다. + * + * @param analysis + * @param abilityMap 새로운 AI 역량 분석 응답값 + */ + @Override + @Transactional + public void updateAbilityContents(Analysis analysis, Map abilityMap) { + abilityMap.forEach((keyword, content) -> { + Keyword key = Keyword.getName(keyword); + Ability ability = findAbilityByKeyword(analysis, key); + ability.updateContent(content); + }); + } + + private Ability findAbilityByKeyword(Analysis analysis, Keyword key) { + // keyword가 기존 역량 분석에 존재했는지 확인 + return analysis.getAbilityList().stream() + .filter(ability -> ability.getKeyword().equals(key)) + .findFirst() + .orElseThrow(() -> new AbilityException(AbilityErrorStatus.INVALID_KEYWORD)); + } +} diff --git a/src/main/java/corecord/dev/domain/ability/domain/entity/Ability.java b/src/main/java/corecord/dev/domain/ability/domain/entity/Ability.java index 42eef02..10d7dbc 100644 --- a/src/main/java/corecord/dev/domain/ability/domain/entity/Ability.java +++ b/src/main/java/corecord/dev/domain/ability/domain/entity/Ability.java @@ -15,7 +15,8 @@ @AllArgsConstructor @NoArgsConstructor @Table(name = "ability", - indexes = {@Index(name = "user_keyword_created_idx", columnList = "user_id, keyword, created_at")}) + indexes = {@Index(name = "user_keyword_created_idx", columnList = "user_id, keyword, created_at"), + @Index(name = "ability_analysis_idx", columnList = "analysis_id, user_id, keyword")}) public class Ability extends BaseEntity { @Id diff --git a/src/main/java/corecord/dev/domain/ability/domain/repository/AbilityRepository.java b/src/main/java/corecord/dev/domain/ability/domain/repository/AbilityRepository.java index 1a5c155..abc6b05 100644 --- a/src/main/java/corecord/dev/domain/ability/domain/repository/AbilityRepository.java +++ b/src/main/java/corecord/dev/domain/ability/domain/repository/AbilityRepository.java @@ -4,7 +4,6 @@ import corecord.dev.domain.ability.domain.entity.Ability; import corecord.dev.domain.ability.domain.entity.Keyword; import corecord.dev.domain.folder.domain.entity.Folder; -import corecord.dev.domain.user.domain.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -17,12 +16,12 @@ public interface AbilityRepository extends JpaRepository { @Query("SELECT new corecord.dev.domain.ability.domain.dto.response.AbilityResponse$KeywordStateDto(" + "a.keyword, COUNT(a), " + // 각 키워드의 개수 집계 - "(COUNT(a) * 1.0 / (SELECT COUNT(a2) FROM Ability a2 WHERE a2.user = :user)) * 100.0) " + // 각 키워드의 비율 집계 + "(COUNT(a) * 1.0 / (SELECT COUNT(a2) FROM Ability a2 WHERE a2.user.userId = :userId)) * 100.0) " + // 각 키워드의 비율 집계 "FROM Ability a " + - "WHERE a.user = :user " + + "WHERE a.user.userId = :userId " + "GROUP BY a.keyword " + - "ORDER BY 3 desc ") // 비율 높은 순 정렬 - List findKeywordStateDtoList(@Param(value = "user") User user); + "ORDER BY COUNT(a) DESC, MAX(a.createdAt) DESC ") // 개수 많은 순 정렬 + List findKeywordStateDtoList(@Param(value = "userId") Long userId); @Modifying @Query("DELETE " + @@ -39,8 +38,8 @@ public interface AbilityRepository extends JpaRepository { @Query("SELECT distinct a.keyword AS keyword " + // unique한 keyword list 반환 "FROM Ability a " + "JOIN a.analysis ana " + - "WHERE a.user = :user " + + "WHERE a.user.userId = :userId " + "GROUP BY a.keyword " + "ORDER BY COUNT(a.keyword) DESC, MAX(ana.createdAt) DESC ") // 개수가 많은 순, 최근 생성 순 정렬 - List getKeywordList(@Param(value = "user") User user); + List getKeywordList(@Param(value = "userId") Long userId); } diff --git a/src/main/java/corecord/dev/domain/analysis/application/AnalysisService.java b/src/main/java/corecord/dev/domain/analysis/application/AnalysisService.java index a4f97b2..4ed6c37 100644 --- a/src/main/java/corecord/dev/domain/analysis/application/AnalysisService.java +++ b/src/main/java/corecord/dev/domain/analysis/application/AnalysisService.java @@ -1,225 +1,17 @@ package corecord.dev.domain.analysis.application; -import corecord.dev.domain.ability.application.AbilityService; -import corecord.dev.domain.analysis.domain.converter.AnalysisConverter; import corecord.dev.domain.analysis.domain.dto.request.AnalysisRequest; -import corecord.dev.domain.analysis.infra.openai.dto.response.AnalysisAiResponse; import corecord.dev.domain.analysis.domain.dto.response.AnalysisResponse; import corecord.dev.domain.analysis.domain.entity.Analysis; -import corecord.dev.domain.analysis.status.AnalysisErrorStatus; -import corecord.dev.domain.analysis.exception.AnalysisException; -import corecord.dev.domain.record.application.RecordDbService; import corecord.dev.domain.record.domain.entity.Record; -import corecord.dev.domain.record.status.RecordErrorStatus; -import corecord.dev.domain.record.exception.RecordException; -import corecord.dev.domain.user.application.UserDbService; import corecord.dev.domain.user.domain.entity.User; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.Map; +public interface AnalysisService { -@Service -@RequiredArgsConstructor -public class AnalysisService { - private final AnalysisAIService analysisAIService; - private final AbilityService abilityService; - private final AnalysisDbService analysisDbService; - private final UserDbService userDbService; - private final RecordDbService recordDbService; + Analysis createAnalysis(Record record, User user); + AnalysisResponse.AnalysisDto postAnalysis(Long userId, Long recordId); + AnalysisResponse.AnalysisDto getAnalysis(Long userId, Long analysisId); + AnalysisResponse.AnalysisDto updateAnalysis(Long userId, AnalysisRequest.AnalysisUpdateDto analysisUpdateDto); + void deleteAnalysis(Long userId, Long analysisId); - /* - * OpenAI룰 활용해 역량 분석 객체를 생성 후 반환 - * @param record - * @param user - * @return - */ - public Analysis createAnalysis(Record record, User user) { - - // MEMO 경험 기록이라면, OpenAI를 이용해 요약 진행 - String content = getRecordContent(record); - - // OpenAI API 호출 - AnalysisAiResponse response = generateAbilityAnalysis(content); - - // Analysis 객체 생성 및 저장 - Analysis analysis = AnalysisConverter.toAnalysis(content, response.getComment(), record); - analysisDbService.saveAnalysis(analysis); - - // Ability 객체 생성 및 저장 - abilityService.parseAndSaveAbilities(response.getKeywordList(), analysis, user); - - return analysis; - } - - /* - * OpenAI를 활용해 역량 분석을 재수행함 Analysis 객체 데이터 교체 후 반환 - * @param record - * @param user - * @return - */ - public Analysis recreateAnalysis(Record record, User user) { - Analysis analysis = record.getAnalysis(); - - // MEMO 경험 기록이라면, CLOVA STUDIO를 이용해 요약 진행 - String content = getRecordContent(record); - - // Open-ai API 호출 - AnalysisAiResponse response = generateAbilityAnalysis(content); - - // Analysis 객체 수정 - analysisDbService.updateAnalysisContent(analysis, content); - analysisDbService.updateAnalysisComment(analysis, response.getComment()); - - // 기존 Ability 객체 삭제 - abilityService.deleteOriginAbilityList(analysis); - - // Ability 객체 생성 및 저장 - abilityService.parseAndSaveAbilities(response.getKeywordList(), analysis, user); - - return analysis; - } - - /* - * recordId를 받아, 해당 경험 기록에 대한 역량 분석을 수행 후 생성된 역량 분석 상세 정보를 반환 - * @param userId - * @param recordId - * @return - */ - public AnalysisResponse.AnalysisDto postAnalysis(Long userId, Long recordId) { - User user = userDbService.findUserById(userId); - Record record = recordDbService.findRecordById(recordId); - - // User-Record 권한 유효성 검증 - validIsUserAuthorizedForRecord(user, record); - - // 역량 분석 API 호출 - Analysis analysis = record.getAnalysis() == null ? - createAnalysis(record, user) : - recreateAnalysis(record, user); // 기존 Analysis 객체가 있을 경우 교체 - - return AnalysisConverter.toAnalysisDto(analysis); - } - - private void validIsUserAuthorizedForRecord(User user, Record record) { - if (!record.getUser().equals(user)) - throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); - } - - /* - * analysisId를 받아 경험 분석 상세 정보를 반환 - * @param userId, analysisId - * @return - */ - public AnalysisResponse.AnalysisDto getAnalysis(Long userId, Long analysisId) { - User user = userDbService.findUserById(userId); - Analysis analysis = analysisDbService.findAnalysisById(analysisId); - - // User-Analysis 권한 유효성 검증 - validIsUserAuthorizedForAnalysis(user, analysis); - - return AnalysisConverter.toAnalysisDto(analysis); - } - - /* - * 역량 분석의 기록 내용 혹은 각 키워드에 대한 내용을 수정한 후 수정된 역량 분석 정보를 반환 - * @param userId, analysisUpdateDto - * @return - */ - @Transactional - public AnalysisResponse.AnalysisDto updateAnalysis(Long userId, AnalysisRequest.AnalysisUpdateDto analysisUpdateDto) { - User user = userDbService.findUserById(userId); - Analysis analysis = analysisDbService.findAnalysisById(analysisUpdateDto.getAnalysisId()); - - // User-Analysis 권한 유효성 검증 - validIsUserAuthorizedForAnalysis(user, analysis); - - // 경험 기록 제목 수정 - String title = analysisUpdateDto.getTitle(); - recordDbService.updateRecordTitle(analysis.getRecord(), title); - - // 경험 역량 분석 요약 내용 수정 - String content = analysisUpdateDto.getContent(); - analysisDbService.updateAnalysisContent(analysis, content); - - // 키워드 경험 내용 수정 - Map abilityMap = analysisUpdateDto.getAbilityMap(); - abilityService.updateAbilityContents(analysis, abilityMap); - - return AnalysisConverter.toAnalysisDto(analysis); - } - - /* - * analysisId를 받아 역량 분석, 경험 기록 데이터를 제거 - * @param userId, analysisId - */ - @Transactional - public void deleteAnalysis(Long userId, Long analysisId) { - User user = userDbService.findUserById(userId); - Analysis analysis = analysisDbService.findAnalysisById(analysisId); - - // User-Analysis 권한 유효성 검증 - validIsUserAuthorizedForAnalysis(user, analysis); - - analysisDbService.deleteAnalysis(analysis); - } - - private AnalysisAiResponse generateAbilityAnalysis(String content) { - AnalysisAiResponse response = analysisAIService.generateAbilityAnalysis(content); - - // 글자 수 validation - validAnalysisCommentLength(response.getComment()); - validAnalysisKeywordContentLength(response.getKeywordList()); - - return response; - } - - private void validAnalysisCommentLength(String comment) { - if (comment.isEmpty() || comment.length() > 300) - 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() > 300) - throw new AnalysisException(AnalysisErrorStatus.OVERFLOW_ANALYSIS_KEYWORD_CONTENT); - } - } - - private String getRecordContent(Record record) { - String content = record.isMemoType() - ? generateMemoSummary(record.getContent()) - : record.getContent(); - - validAnalysisContentLength(content); - - return content; - } - - private String generateMemoSummary(String content) { - String response = analysisAIService.generateMemoSummary(content); - - validIsRecordEnough(response); - validAnalysisContentLength(response); - - return response; - } - - private void validIsRecordEnough(String response) { - if (response.contains("NO_RECORD")) - throw new RecordException(RecordErrorStatus.NO_RECORD); - } - - private void validAnalysisContentLength(String content) { - if (content.isEmpty() || content.length() > 500) - throw new AnalysisException(AnalysisErrorStatus.OVERFLOW_ANALYSIS_CONTENT); - } - - private void validIsUserAuthorizedForAnalysis(User user, Analysis analysis) { - if (!analysis.getRecord().getUser().equals(user)) - throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); - } } diff --git a/src/main/java/corecord/dev/domain/analysis/application/AnalysisServiceImpl.java b/src/main/java/corecord/dev/domain/analysis/application/AnalysisServiceImpl.java new file mode 100644 index 0000000..31142b1 --- /dev/null +++ b/src/main/java/corecord/dev/domain/analysis/application/AnalysisServiceImpl.java @@ -0,0 +1,209 @@ +package corecord.dev.domain.analysis.application; + +import corecord.dev.domain.ability.application.AbilityService; +import corecord.dev.domain.analysis.domain.converter.AnalysisConverter; +import corecord.dev.domain.analysis.domain.dto.request.AnalysisRequest; +import corecord.dev.domain.analysis.domain.dto.response.AnalysisResponse; +import corecord.dev.domain.analysis.domain.entity.Analysis; +import corecord.dev.domain.analysis.exception.AnalysisException; +import corecord.dev.domain.analysis.infra.openai.dto.response.AnalysisAiResponse; +import corecord.dev.domain.analysis.status.AnalysisErrorStatus; +import corecord.dev.domain.record.application.RecordDbService; +import corecord.dev.domain.record.domain.entity.Record; +import corecord.dev.domain.record.exception.RecordException; +import corecord.dev.domain.record.status.RecordErrorStatus; +import corecord.dev.domain.user.application.UserDbService; +import corecord.dev.domain.user.domain.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AnalysisServiceImpl implements AnalysisService { + private final AnalysisAIService analysisAIService; + private final AbilityService abilityService; + private final AnalysisDbService analysisDbService; + private final UserDbService userDbService; + private final RecordDbService recordDbService; + + /** + * recordId를 받아, 해당 경험 기록에 대한 역량 분석을 수행 후 생성된 역량 분석 상세 정보를 반환합니다. + * + * @param userId + * @param recordId + * @return 분석된 역량 키워드, 코멘트, 기록, 기록 수단 정보를 반환 + */ + @Override + public AnalysisResponse.AnalysisDto postAnalysis(Long userId, Long recordId) { + User user = userDbService.findUserById(userId); + Record record = recordDbService.findRecordById(recordId); + + // User-Record 권한 유효성 검증 + validIsUserAuthorizedForRecord(user, record); + + // 역량 분석 API 호출 + Analysis analysis = createAnalysis(record, user); + + return AnalysisConverter.toAnalysisDto(analysis); + } + + private void validIsUserAuthorizedForRecord(User user, Record record) { + if (!record.getUser().equals(user)) + throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); + } + + @Override + public Analysis createAnalysis(Record record, User user) { + + // MEMO 경험 기록이라면, AI를 이용해 요약 진행 + String content = getRecordContent(record); + + // AI API 호출 + AnalysisAiResponse response = generateAbilityAnalysis(content); + + Analysis analysis = record.getAnalysis() == null ? + createAndSaveNewAnalysis(record, content, response) : // 역량 분석 + updateAnalysisAndDeleteAbility(record, content, response); // 역량 분석 업데이트 + + // Ability 객체 생성 및 저장 + abilityService.parseAndSaveAbilities(response.getKeywordList(), analysis, user); + + return analysis; + } + + private String getRecordContent(Record record) { + String content = record.isMemoType() + ? generateMemoSummary(record.getContent()) + : record.getContent(); + + validAnalysisContentLength(content); + return content; + } + + private String generateMemoSummary(String content) { + String response = analysisAIService.generateMemoSummary(content); + + validIsRecordEnough(response); + return response; + } + + private void validAnalysisContentLength(String content) { + if (content.isEmpty() || content.length() > 500) + throw new AnalysisException(AnalysisErrorStatus.OVERFLOW_ANALYSIS_CONTENT); + } + + private void validIsRecordEnough(String response) { + if (response.contains("NO_RECORD")) + throw new RecordException(RecordErrorStatus.NO_RECORD); + } + + private AnalysisAiResponse generateAbilityAnalysis(String content) { + AnalysisAiResponse response = analysisAIService.generateAbilityAnalysis(content); + + validAnalysisCommentLength(response.getComment()); + validAnalysisKeywordContentLength(response.getKeywordList()); + + return response; + } + + private void validAnalysisCommentLength(String comment) { + if (comment.isEmpty() || comment.length() > 300) + 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() > 300) + throw new AnalysisException(AnalysisErrorStatus.OVERFLOW_ANALYSIS_KEYWORD_CONTENT); + } + } + + private Analysis createAndSaveNewAnalysis(Record record, String content, AnalysisAiResponse response) { + Analysis analysis = AnalysisConverter.toAnalysis(content, response.getComment(), record); + analysisDbService.saveAnalysis(analysis); + return analysis; + } + + private Analysis updateAnalysisAndDeleteAbility(Record record, String content, AnalysisAiResponse response) { + // analysis content 내용 변경 + Analysis analysis = record.getAnalysis(); + analysisDbService.updateAnalysisContent(analysis, content); + analysisDbService.updateAnalysisComment(analysis, response.getComment()); + + // 기존 Ability 객체 삭제 + abilityService.deleteOriginAbilityList(analysis); + return analysis; + } + + /** + * analysisId를 받아 경험 분석 상세 정보를 반환합니다. + * + * @param userId + * @param analysisId + * @return 분석된 역량 키워드, 코멘트, 기록, 기록 수단 정보를 반환 + */ + @Override + public AnalysisResponse.AnalysisDto getAnalysis(Long userId, Long analysisId) { + Analysis analysis = analysisDbService.findAnalysisById(analysisId); + + // User-Analysis 권한 유효성 검증 + validIsUserAuthorizedForAnalysis(userId, analysis); + + return AnalysisConverter.toAnalysisDto(analysis); + } + + /** + * 역량 분석의 기록 내용 혹은 각 키워드에 대한 내용을 수정한 후 수정된 역량 분석 정보를 반환합니다. + * + * @param userId, analysisUpdateDto + * @return 분석된 역량 키워드, 코멘트, 기록, 기록 수단 정보를 반환 + */ + @Override + @Transactional + public AnalysisResponse.AnalysisDto updateAnalysis(Long userId, AnalysisRequest.AnalysisUpdateDto analysisUpdateDto) { + Analysis analysis = analysisDbService.findAnalysisById(analysisUpdateDto.getAnalysisId()); + + // User-Analysis 권한 유효성 검증 + validIsUserAuthorizedForAnalysis(userId, analysis); + + // 경험 기록 제목 수정 + String title = analysisUpdateDto.getTitle(); + recordDbService.updateRecordTitle(analysis.getRecord(), title); + + // 경험 역량 분석 요약 내용 수정 + String content = analysisUpdateDto.getContent(); + analysisDbService.updateAnalysisContent(analysis, content); + + // 키워드 경험 내용 수정 + Map abilityMap = analysisUpdateDto.getAbilityMap(); + abilityService.updateAbilityContents(analysis, abilityMap); + + return AnalysisConverter.toAnalysisDto(analysis); + } + + /** + * analysisId를 받아 역량 분석, 경험 기록 데이터를 제거합니다. + * + * @param userId, analysisId + */ + @Override + @Transactional + public void deleteAnalysis(Long userId, Long analysisId) { + Analysis analysis = analysisDbService.findAnalysisById(analysisId); + + // User-Analysis 권한 유효성 검증 + validIsUserAuthorizedForAnalysis(userId, analysis); + + analysisDbService.deleteAnalysis(analysis); + } + + private void validIsUserAuthorizedForAnalysis(Long userId, Analysis analysis) { + if (!analysis.getRecord().getUser().getUserId().equals(userId)) + throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); + } +} diff --git a/src/main/java/corecord/dev/domain/analysis/domain/repository/AnalysisRepository.java b/src/main/java/corecord/dev/domain/analysis/domain/repository/AnalysisRepository.java index ef11c25..229e28b 100644 --- a/src/main/java/corecord/dev/domain/analysis/domain/repository/AnalysisRepository.java +++ b/src/main/java/corecord/dev/domain/analysis/domain/repository/AnalysisRepository.java @@ -18,6 +18,7 @@ public interface AnalysisRepository extends JpaRepository { "JOIN FETCH a.record r " + "JOIN FETCH r.folder f " + "JOIN FETCH a.abilityList al " + + "JOIN FETCH r.user u " + "WHERE a.analysisId = :id") Optional findAnalysisById(@Param(value = "id") Long id); diff --git a/src/main/java/corecord/dev/domain/chat/application/ChatDbService.java b/src/main/java/corecord/dev/domain/chat/application/ChatDbService.java index 8a913b1..e7a800d 100644 --- a/src/main/java/corecord/dev/domain/chat/application/ChatDbService.java +++ b/src/main/java/corecord/dev/domain/chat/application/ChatDbService.java @@ -37,7 +37,7 @@ public Chat saveChat(int author, String content, ChatRoom chatRoom) { @Transactional public void deleteChatRoom(ChatRoom chatRoom) { chatRepository.deleteByChatRoomId(chatRoom.getChatRoomId()); - chatRoomRepository.delete(chatRoom); + chatRoomRepository.deleteById(chatRoom.getChatRoomId()); } @Transactional @@ -55,8 +55,8 @@ public void deleteChatRoomByFolder(Folder folder) { chatRepository.deleteChatByFolderId(folder.getFolderId()); } - public ChatRoom findChatRoomById(Long chatRoomId, User user) { - return chatRoomRepository.findByChatRoomIdAndUser(chatRoomId, user) + public ChatRoom findChatRoomById(Long chatRoomId, Long userId) { + return chatRoomRepository.findByChatRoomIdAndUserId(chatRoomId, userId) .orElseThrow(() -> new ChatException(ChatErrorStatus.CHAT_ROOM_NOT_FOUND)); } diff --git a/src/main/java/corecord/dev/domain/chat/application/ChatService.java b/src/main/java/corecord/dev/domain/chat/application/ChatService.java index 0621c0f..2c5b813 100644 --- a/src/main/java/corecord/dev/domain/chat/application/ChatService.java +++ b/src/main/java/corecord/dev/domain/chat/application/ChatService.java @@ -1,201 +1,16 @@ package corecord.dev.domain.chat.application; -import corecord.dev.domain.chat.domain.converter.ChatConverter; import corecord.dev.domain.chat.domain.dto.request.ChatRequest; import corecord.dev.domain.chat.domain.dto.response.ChatResponse; -import corecord.dev.domain.chat.domain.dto.response.ChatSummaryAiResponse; -import corecord.dev.domain.chat.domain.entity.Chat; -import corecord.dev.domain.chat.domain.entity.ChatRoom; -import corecord.dev.domain.chat.status.ChatErrorStatus; -import corecord.dev.domain.chat.exception.ChatException; -import corecord.dev.domain.user.application.UserDbService; -import corecord.dev.domain.user.domain.entity.User; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import java.util.List; +public interface ChatService { + ChatResponse.ChatRoomDto createChatRoom(Long userId); + ChatResponse.ChatsDto createChat(Long userId, Long chatRoomId, ChatRequest.ChatDto chatDto); + ChatResponse.ChatListDto getChatList(Long userId, Long chatRoomId); + void deleteChatRoom(Long userId, Long chatRoomId); -@Service -@Slf4j -@RequiredArgsConstructor -public class ChatService { + ChatResponse.ChatSummaryDto getChatSummary(Long userId, Long chatRoomId); + ChatResponse.ChatTmpDto getChatTmp(Long userId); + void saveChatTmp(Long userId, Long chatRoomId); - private final ChatDbService chatDbService; - private final ChatAIService chatAIService; - private final UserDbService userDbService; - - /* - * user의 채팅방을 생성하고 생성된 채팅방 정보를 반환 - * @param userId - * @return chatRoomDto - */ - public ChatResponse.ChatRoomDto createChatRoom(Long userId) { - User user = userDbService.findUserById(userId); - - // 채팅방 생성 - ChatRoom chatRoom = chatDbService.createChatRoom(user); - - // 첫번째 채팅 생성 - "안녕하세요! {nickName}님! {nickName}님의 경험이 궁금해요. {nickName}님의 경험을 들려주세요!" - String firstChatContent = String.format("안녕하세요! %s님의 경험을 말해주세요.\n어떤 경험을 했나요? 당시 상황과 문제를 해결하기 위한 %s님의 노력이 궁금해요", user.getNickName(), user.getNickName()); - Chat firstChat = chatDbService.saveChat(0, firstChatContent, chatRoom); - - return ChatConverter.toChatRoomDto(chatRoom, firstChat); - } - - /* - * user의 채팅방에 채팅을 생성하고 생성된 채팅 정보와 AI 답변 반환 - * @param chatRoomId - * @param chatDto - * @return - */ - public ChatResponse.ChatsDto createChat(Long userId, Long chatRoomId, ChatRequest.ChatDto chatDto) { - User user = userDbService.findUserById(userId); - ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, user); - - // 사용자 채팅 생성 - chatDbService.saveChat(1, chatDto.getContent(), chatRoom); - - // 가이드이면 가이드 채팅 생성 - if (chatDto.isGuide()) { - checkGuideChat(chatRoom); - return generateGuideChats(chatRoom); - } - - // AI 답변 생성 - List chatHistory = chatDbService.findChatsByChatRoom(chatRoom); - String aiAnswer = chatAIService.generateChatResponse(chatHistory, chatDto.getContent()); - Chat aiChat = chatDbService.saveChat(0, aiAnswer, chatRoom); - - return ChatConverter.toChatsDto(List.of(aiChat)); - } - - private static void checkGuideChat(ChatRoom chatRoom) { - if (chatRoom.getChatList().size() > 2) - throw new ChatException(ChatErrorStatus.INVALID_GUIDE_CHAT); - } - - private ChatResponse.ChatsDto generateGuideChats(ChatRoom chatRoom) { - Chat guideChat1 = chatDbService.saveChat(0, "아래 질문에 답하다 보면 경험이 정리될 거예요! \n" + - "S(상황) : 어떤 상황이었나요?\n" + - "T(과제) : 마주한 문제나 목표는 무엇이었나요?\n" + - "A(행동) : 문제를 해결하기 위해 어떻게 노력했나요?\n" + - "R(결과) : 그 결과는 어땠나요?", chatRoom); - Chat guideChat2 = chatDbService.saveChat(0, "우선 기억나는 내용부터 가볍게 적어보세요.\n" + - "부족한 부분은 대화를 통해 모아모아가 도와줄게요! \uD83D\uDCDD", chatRoom); - return ChatConverter.toChatsDto(List.of(guideChat1, guideChat2)); - } - - /* - * user의 채팅방의 채팅 목록을 반환 - * @param userId - * @param chatRoomId - * @return chatListDto - */ - public ChatResponse.ChatListDto getChatList(Long userId, Long chatRoomId) { - User user = userDbService.findUserById(userId); - ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, user); - List chatList = chatDbService.findChatsByChatRoom(chatRoom); - - return ChatConverter.toChatListDto(chatList); - } - - /* - * user의 채팅방을 삭제 - * @param userId - * @param chatRoomId - */ - public void deleteChatRoom(Long userId, Long chatRoomId) { - User user = userDbService.findUserById(userId); - ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, user); - - // 임시 저장된 ChatRoom 인지 확인 후 삭제 - checkTmpChat(user, chatRoom); - chatDbService.deleteChatRoom(chatRoom); - } - - private void checkTmpChat(User user, ChatRoom chatRoom) { - if (user.getTmpChat() == null) { - return; - } - if (user.getTmpChat().equals(chatRoom.getChatRoomId())) { - user.deleteTmpChat(); - } - } - - /* - * user의 채팅의 요약 정보를 반환 - * @param userId - * @param chatRoomId - * @return chatSummaryDto - */ - public ChatResponse.ChatSummaryDto getChatSummary(Long userId, Long chatRoomId) { - User user = userDbService.findUserById(userId); - ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, user); - List chatList = chatDbService.findChatsByChatRoom(chatRoom); - - // 사용자 입력 없이 저장하려는 경우 체크 - validateChatList(chatList); - - // 채팅 정보 요약 생성 - ChatSummaryAiResponse response = chatAIService.generateChatSummaryResponse(chatList); - - validateResponse(response); - - return ChatConverter.toChatSummaryDto(chatRoom, response); - } - - private static void validateChatList(List chatList) { - if (chatList.size() <= 1) - throw new ChatException(ChatErrorStatus.NO_RECORD); - } - - /* - * user의 임시 채팅방과 유무를 반환 - * @param userId - * @return chatTmpDto - */ - @Transactional - public ChatResponse.ChatTmpDto getChatTmp(Long userId) { - User user = userDbService.findUserById(userId); - if (user.getTmpChat() == null) { - return ChatConverter.toNotExistingChatTmpDto(); - } - // 임시 채팅 제거 후 반환 - Long chatRoomId = user.getTmpChat(); - userDbService.deleteUserTmpChat(user); - return ChatConverter.toExistingChatTmpDto(chatRoomId); - } - - /* - * user의 채팅방을 임시 저장 - * @param userId - * @param chatRoomId - */ - @Transactional - public void saveChatTmp(Long userId, Long chatRoomId) { - User user = userDbService.findUserById(userId); - ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, user); - - // 이미 임시 저장된 채팅방이 있는 경우 - if (user.getTmpChat() != null) { - throw new ChatException(ChatErrorStatus.TMP_CHAT_EXIST); - } - userDbService.updateUserTmpChat(user, chatRoom.getChatRoomId()); - } - - private static void validateResponse(ChatSummaryAiResponse response) { - if (response.getTitle().equals("NO_RECORD") || response.getContent().equals("NO_RECORD") || response.getContent().equals("") || response.getTitle().equals("")) { - throw new ChatException(ChatErrorStatus.NO_RECORD); - } - - if (response.getTitle().length() > 30) { - throw new ChatException(ChatErrorStatus.OVERFLOW_SUMMARY_TITLE); - } - - if (response.getContent().length() > 500) { - throw new ChatException(ChatErrorStatus.OVERFLOW_SUMMARY_CONTENT); - } - } } diff --git a/src/main/java/corecord/dev/domain/chat/application/ChatServiceImpl.java b/src/main/java/corecord/dev/domain/chat/application/ChatServiceImpl.java new file mode 100644 index 0000000..6b6c6e8 --- /dev/null +++ b/src/main/java/corecord/dev/domain/chat/application/ChatServiceImpl.java @@ -0,0 +1,209 @@ +package corecord.dev.domain.chat.application; + +import corecord.dev.domain.chat.domain.converter.ChatConverter; +import corecord.dev.domain.chat.domain.dto.request.ChatRequest; +import corecord.dev.domain.chat.domain.dto.response.ChatResponse; +import corecord.dev.domain.chat.domain.dto.response.ChatSummaryAiResponse; +import corecord.dev.domain.chat.domain.entity.Chat; +import corecord.dev.domain.chat.domain.entity.ChatRoom; +import corecord.dev.domain.chat.exception.ChatException; +import corecord.dev.domain.chat.status.ChatErrorStatus; +import corecord.dev.domain.user.application.UserDbService; +import corecord.dev.domain.user.domain.entity.User; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChatServiceImpl implements ChatService { + + private final ChatDbService chatDbService; + private final ChatAIService chatAIService; + private final UserDbService userDbService; + + /** + * user의 채팅방을 생성하고 생성된 채팅방 정보를 반환합니다. + * + * @param userId + * @return chatRoomId, 첫 번째 메시지 + */ + @Override + public ChatResponse.ChatRoomDto createChatRoom(Long userId) { + User user = userDbService.findUserById(userId); + + // 채팅방 생성 + ChatRoom chatRoom = chatDbService.createChatRoom(user); + + // 첫번째 채팅 생성 + String firstChatContent = String.format("안녕하세요! %s님의 경험을 말해주세요.\n어떤 경험을 했나요? 당시 상황과 문제를 해결하기 위한 %s님의 노력이 궁금해요", user.getNickName(), user.getNickName()); + Chat firstChat = chatDbService.saveChat(0, firstChatContent, chatRoom); + + return ChatConverter.toChatRoomDto(chatRoom, firstChat); + } + + /** + * user의 채팅방에 채팅을 생성하고 생성된 채팅 정보와 AI 답변 반환합니다. + * + * @param chatRoomId + * @param chatDto + * @return 각 chat의 chatId, content를 담은 리스트 + */ + @Override + public ChatResponse.ChatsDto createChat(Long userId, Long chatRoomId, ChatRequest.ChatDto chatDto) { + ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, userId); + + // 사용자 채팅 생성 + chatDbService.saveChat(1, chatDto.getContent(), chatRoom); + + // 가이드이면 가이드 채팅 생성 + if (chatDto.isGuide()) { + checkGuideChat(chatRoom); + return generateGuideChats(chatRoom); + } + + // AI 답변 생성 + List chatHistory = chatDbService.findChatsByChatRoom(chatRoom); + String aiAnswer = chatAIService.generateChatResponse(chatHistory, chatDto.getContent()); + Chat aiChat = chatDbService.saveChat(0, aiAnswer, chatRoom); + + return ChatConverter.toChatsDto(List.of(aiChat)); + } + + private static void checkGuideChat(ChatRoom chatRoom) { + if (chatRoom.getChatList().size() > 2) + throw new ChatException(ChatErrorStatus.INVALID_GUIDE_CHAT); + } + + private ChatResponse.ChatsDto generateGuideChats(ChatRoom chatRoom) { + Chat guideChat1 = chatDbService.saveChat(0, "아래 질문에 답하다 보면 경험이 정리될 거예요! \n" + + "S(상황) : 어떤 상황이었나요?\n" + + "T(과제) : 마주한 문제나 목표는 무엇이었나요?\n" + + "A(행동) : 문제를 해결하기 위해 어떻게 노력했나요?\n" + + "R(결과) : 그 결과는 어땠나요?", chatRoom); + Chat guideChat2 = chatDbService.saveChat(0, "우선 기억나는 내용부터 가볍게 적어보세요.\n" + + "부족한 부분은 대화를 통해 모아모아가 도와줄게요! \uD83D\uDCDD", chatRoom); + return ChatConverter.toChatsDto(List.of(guideChat1, guideChat2)); + } + + /** + * user의 채팅방의 채팅 목록을 반환합니다. + * + * @param userId + * @param chatRoomId + * @return 각 chat의 chatId, 저자, content, 생성된 기점을 담은 리스트 + */ + @Override + public ChatResponse.ChatListDto getChatList(Long userId, Long chatRoomId) { + ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, userId); + List chatList = chatDbService.findChatsByChatRoom(chatRoom); + + return ChatConverter.toChatListDto(chatList); + } + + /** + * user의 채팅방을 삭제합니다. + * + * @param userId + * @param chatRoomId + */ + @Override + public void deleteChatRoom(Long userId, Long chatRoomId) { + User user = userDbService.findUserById(userId); + ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, userId); + + // 임시 저장된 ChatRoom 인지 확인 후 삭제 + checkTmpChat(user, chatRoom); + chatDbService.deleteChatRoom(chatRoom); + } + + private void checkTmpChat(User user, ChatRoom chatRoom) { + if (user.getTmpChat() == null) { + return; + } + if (user.getTmpChat().equals(chatRoom.getChatRoomId())) { + user.deleteTmpChat(); + } + } + + /** + * user의 채팅의 요약 정보를 반환합니다. + * + * @param userId + * @param chatRoomId + * @return chat 요약 결과(제목, 본문) + */ + public ChatResponse.ChatSummaryDto getChatSummary(Long userId, Long chatRoomId) { + ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, userId); + List chatList = chatDbService.findChatsByChatRoom(chatRoom); + + // 사용자 입력 없이 저장하려는 경우 체크 + validateChatList(chatList); + + // 채팅 정보 요약 생성 + ChatSummaryAiResponse response = chatAIService.generateChatSummaryResponse(chatList); + validateResponse(response); + + return ChatConverter.toChatSummaryDto(chatRoom, response); + } + + private static void validateChatList(List chatList) { + if (chatList.size() <= 1) + throw new ChatException(ChatErrorStatus.NO_RECORD); + } + + private static void validateResponse(ChatSummaryAiResponse response) { + if (response.getTitle().equals("NO_RECORD") || response.getContent().equals("NO_RECORD") || response.getContent().equals("") || response.getTitle().equals("")) { + throw new ChatException(ChatErrorStatus.NO_RECORD); + } + + if (response.getTitle().length() > 30) { + throw new ChatException(ChatErrorStatus.OVERFLOW_SUMMARY_TITLE); + } + + if (response.getContent().length() > 500) { + throw new ChatException(ChatErrorStatus.OVERFLOW_SUMMARY_CONTENT); + } + } + + /** + * user의 임시 채팅방과 임시 채팅 저장 유무를 반환합니다. + * + * @param userId + * @return chatTmpDto + */ + @Transactional + public ChatResponse.ChatTmpDto getChatTmp(Long userId) { + User user = userDbService.findUserById(userId); + if (user.getTmpChat() == null) { + return ChatConverter.toNotExistingChatTmpDto(); + } + + // 임시 채팅 제거 후 반환 + Long chatRoomId = user.getTmpChat(); + userDbService.deleteUserTmpChat(user); + + return ChatConverter.toExistingChatTmpDto(chatRoomId); + } + + /** + * user의 채팅방을 임시 저장합니다. + * + * @param userId + * @param chatRoomId + */ + @Transactional + public void saveChatTmp(Long userId, Long chatRoomId) { + User user = userDbService.findUserById(userId); + ChatRoom chatRoom = chatDbService.findChatRoomById(chatRoomId, userId); + + // 이미 임시 저장된 채팅방이 있는 경우 + if (user.getTmpChat() != null) { + throw new ChatException(ChatErrorStatus.TMP_CHAT_EXIST); + } + userDbService.updateUserTmpChat(user, chatRoom.getChatRoomId()); + } + +} diff --git a/src/main/java/corecord/dev/domain/chat/domain/repository/ChatRoomRepository.java b/src/main/java/corecord/dev/domain/chat/domain/repository/ChatRoomRepository.java index c798406..6e7ba73 100644 --- a/src/main/java/corecord/dev/domain/chat/domain/repository/ChatRoomRepository.java +++ b/src/main/java/corecord/dev/domain/chat/domain/repository/ChatRoomRepository.java @@ -1,7 +1,6 @@ package corecord.dev.domain.chat.domain.repository; import corecord.dev.domain.chat.domain.entity.ChatRoom; -import corecord.dev.domain.user.domain.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -12,11 +11,24 @@ @Repository public interface ChatRoomRepository extends JpaRepository { - Optional findByChatRoomIdAndUser(Long chatRoomId, User user); + + @Query("SELECT cr " + + "FROM ChatRoom cr " + + "JOIN FETCH cr.user u " + + "WHERE cr.chatRoomId = :chatRoomId " + + "AND u.userId = :userId ") + Optional findByChatRoomIdAndUserId(@Param(value = "chatRoomId") Long chatRoomId, + @Param(value = "userId") Long userId); @Modifying @Query("DELETE " + "FROM ChatRoom cr " + "WHERE cr.user.userId IN :userId") void deleteChatRoomByUserId(@Param(value = "userId") Long userId); + + @Modifying + @Query("DELETE " + + "FROM ChatRoom cr " + + "WHERE cr.chatRoomId = :chatRoomId") + void deleteById(@Param(value = "chatRoomId") Long chatRoomId); } diff --git a/src/main/java/corecord/dev/domain/folder/application/FolderDbService.java b/src/main/java/corecord/dev/domain/folder/application/FolderDbService.java index d4db50d..22dc894 100644 --- a/src/main/java/corecord/dev/domain/folder/application/FolderDbService.java +++ b/src/main/java/corecord/dev/domain/folder/application/FolderDbService.java @@ -32,8 +32,8 @@ public void deleteFolderByUserId(Long userId) { folderRepository.deleteFolderByUserId(userId); } - public Folder findFolderByTitle(User user, String title) { - return folderRepository.findFolderByTitle(title, user) + public Folder findFolderByTitle(Long userId, String title) { + return folderRepository.findFolderByTitle(title, userId) .orElseThrow(() -> new FolderException(FolderErrorStatus.FOLDER_NOT_FOUND)); } @@ -42,8 +42,8 @@ public Folder findFolderById(Long folderId) { .orElseThrow(() -> new FolderException(FolderErrorStatus.FOLDER_NOT_FOUND)); } - public List findFolderDtoList(User user) { - return folderRepository.findFolderDtoList(user); + public List findFolderDtoList(Long userId) { + return folderRepository.findFolderDtoList(userId); } public boolean isFolderExist(String title, User user) { diff --git a/src/main/java/corecord/dev/domain/folder/application/FolderService.java b/src/main/java/corecord/dev/domain/folder/application/FolderService.java index c1f2c3e..c087776 100644 --- a/src/main/java/corecord/dev/domain/folder/application/FolderService.java +++ b/src/main/java/corecord/dev/domain/folder/application/FolderService.java @@ -1,130 +1,14 @@ package corecord.dev.domain.folder.application; -import corecord.dev.domain.ability.application.AbilityDbService; -import corecord.dev.domain.analysis.application.AnalysisDbService; -import corecord.dev.domain.chat.application.ChatDbService; -import corecord.dev.domain.folder.domain.converter.FolderConverter; import corecord.dev.domain.folder.domain.dto.request.FolderRequest; import corecord.dev.domain.folder.domain.dto.response.FolderResponse; -import corecord.dev.domain.folder.domain.entity.Folder; -import corecord.dev.domain.folder.status.FolderErrorStatus; -import corecord.dev.domain.folder.exception.FolderException; -import corecord.dev.domain.record.application.RecordDbService; -import corecord.dev.domain.user.application.UserDbService; -import corecord.dev.domain.user.domain.entity.User; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.List; -@Service -@Slf4j -@RequiredArgsConstructor -public class FolderService { - private final FolderDbService folderDbService; - private final UserDbService userDbService; - private final AnalysisDbService analysisDbService; - private final AbilityDbService abilityDbService; - private final ChatDbService chatDbService; - private final RecordDbService recordDbService; +public interface FolderService { + FolderResponse.FolderDtoList createFolder(Long userId, FolderRequest.FolderDto folderDto); + FolderResponse.FolderDtoList deleteFolder(Long userId, Long folderId); + FolderResponse.FolderDtoList updateFolder(Long userId, FolderRequest.FolderUpdateDto folderDto); + FolderResponse.FolderDtoList getFolderList(Long userId); - /* - * 폴더명(title)을 request로 받아, 새로운 폴더를 생성 후 생성 순 풀더 리스트 반환 - * @param userId, folderDto - * @return - */ - @Transactional - public FolderResponse.FolderDtoList createFolder(Long userId, FolderRequest.FolderDto folderDto) { - User user = userDbService.findUserById(userId); - String title = folderDto.getTitle(); - - // 폴더명 유효성 검증 - validDuplicatedFolderTitleAndLength(title, user); - - // folder 객체 생성 및 User 연관관계 설정 - Folder folder = FolderConverter.toFolderEntity(title, user); - folderDbService.saveFolder(folder); - - List folderList = folderDbService.findFolderDtoList(user); - return FolderConverter.toFolderDtoList(folderList); - } - - /* - * folderId를 받아 folder를 삭제한 후 생성 순 폴더 리스트 반환 - * @param userId, folderId - * @return - */ - @Transactional - public FolderResponse.FolderDtoList deleteFolder(Long userId, Long folderId) { - User user = userDbService.findUserById(userId); - Folder folder = folderDbService.findFolderById(folderId); - - // User-Folder 권한 유효성 검증 - validIsUserAuthorizedForFolder(user, folder); - - abilityDbService.deleteAbilityByFolder(folder); - analysisDbService.deleteAnalysisByFolder(folder); - chatDbService.deleteChatRoomByFolder(folder); - recordDbService.deleteRecordByFolder(folder); - folderDbService.deleteFolder(folder); - - List folderList = folderDbService.findFolderDtoList(user); - return FolderConverter.toFolderDtoList(folderList); - } - - /* - * folderId를 받아, 해당 folder의 title을 수정 - * @param userId, folderDto - * @return - */ - @Transactional - public FolderResponse.FolderDtoList updateFolder(Long userId, FolderRequest.FolderUpdateDto folderDto) { - User user = userDbService.findUserById(userId); - Folder folder = folderDbService.findFolderById(folderDto.getFolderId()); - String title = folderDto.getTitle(); - - // 폴더명 유효성 검증 - validDuplicatedFolderTitleAndLength(title, user); - - // User-Folder 권한 유효성 검증 - validIsUserAuthorizedForFolder(user, folder); - - folder.updateTitle(title); - - List folderList = folderDbService.findFolderDtoList(user); - return FolderConverter.toFolderDtoList(folderList); - } - - private void validIsUserAuthorizedForFolder(User user, Folder folder) { - if (!folder.getUser().equals(user)) - throw new FolderException(FolderErrorStatus.USER_FOLDER_UNAUTHORIZED); - } - - /* - * 생성일 오름차순으로 폴더 리스트를 조회 - * @param userId - * @return - */ - @Transactional(readOnly = true) - public FolderResponse.FolderDtoList getFolderList(Long userId) { - User user = userDbService.findUserById(userId); - - List folderList = folderDbService.findFolderDtoList(user); - return FolderConverter.toFolderDtoList(folderList); - } - - private void validDuplicatedFolderTitleAndLength(String title, User user) { - // 폴더명 글자 수 검사 - if (title.length() > 15) { - throw new FolderException(FolderErrorStatus.OVERFLOW_FOLDER_TITLE); - } - - // 폴더명 중복 검사 - if (folderDbService.isFolderExist(title, user)) { - throw new FolderException(FolderErrorStatus.DUPLICATED_FOLDER_TITLE); - } - } } diff --git a/src/main/java/corecord/dev/domain/folder/application/FolderServiceImpl.java b/src/main/java/corecord/dev/domain/folder/application/FolderServiceImpl.java new file mode 100644 index 0000000..ffb9287 --- /dev/null +++ b/src/main/java/corecord/dev/domain/folder/application/FolderServiceImpl.java @@ -0,0 +1,131 @@ +package corecord.dev.domain.folder.application; + +import corecord.dev.domain.ability.application.AbilityDbService; +import corecord.dev.domain.analysis.application.AnalysisDbService; +import corecord.dev.domain.chat.application.ChatDbService; +import corecord.dev.domain.folder.domain.converter.FolderConverter; +import corecord.dev.domain.folder.domain.dto.request.FolderRequest; +import corecord.dev.domain.folder.domain.dto.response.FolderResponse; +import corecord.dev.domain.folder.domain.entity.Folder; +import corecord.dev.domain.folder.exception.FolderException; +import corecord.dev.domain.folder.status.FolderErrorStatus; +import corecord.dev.domain.record.application.RecordDbService; +import corecord.dev.domain.user.application.UserDbService; +import corecord.dev.domain.user.domain.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FolderServiceImpl implements FolderService { + + private final FolderDbService folderDbService; + private final UserDbService userDbService; + private final AnalysisDbService analysisDbService; + private final AbilityDbService abilityDbService; + private final ChatDbService chatDbService; + private final RecordDbService recordDbService; + + + /** + * 폴더명(title)을 request로 받아, 새로운 폴더를 생성 후 생성 순 풀더 리스트 반환합니다. + * 정렬 기준: 최근 생성 순 + * + * @param userId, folderDto + * @return 각 folder의 Id, title을 담은 리스트 + */ + @Override + @Transactional + public FolderResponse.FolderDtoList createFolder(Long userId, FolderRequest.FolderDto folderDto) { + User user = userDbService.findUserById(userId); + String title = folderDto.getTitle(); + + validDuplicatedFolderTitleAndLength(title, user); + + // folder 객체 생성 및 User 연관관계 설정 + Folder folder = FolderConverter.toFolderEntity(title, user); + folderDbService.saveFolder(folder); + + return getFolderList(userId); + } + + /** + * folderId를 받아 folder를 삭제 후 폴더 리스트를 반환합니다. + * 정렬 기준: 최근 생성 순 + * + * @param userId, folderId + * @return 각 folder의 Id, title을 담은 리스트 + */ + @Override + @Transactional + public FolderResponse.FolderDtoList deleteFolder(Long userId, Long folderId) { + Folder folder = folderDbService.findFolderById(folderId); + validIsUserAuthorizedForFolder(userId, folder); + + abilityDbService.deleteAbilityByFolder(folder); + analysisDbService.deleteAnalysisByFolder(folder); + chatDbService.deleteChatRoomByFolder(folder); + recordDbService.deleteRecordByFolder(folder); + folderDbService.deleteFolder(folder); + + return getFolderList(userId); + } + + /** + * folderId를 받아, 해당 folder의 title을 수정 후 폴더 리스트를 반환합니다. + * 정렬 기준: 최근 생성 순 + * + * @param userId, folderDto + * @return 각 folder의 Id, title을 담은 리스트 + */ + @Override + @Transactional + public FolderResponse.FolderDtoList updateFolder(Long userId, FolderRequest.FolderUpdateDto folderDto) { + User user = userDbService.findUserById(userId); + Folder folder = folderDbService.findFolderById(folderDto.getFolderId()); + String title = folderDto.getTitle(); + + validDuplicatedFolderTitleAndLength(title, user); + validIsUserAuthorizedForFolder(userId, folder); + + folder.updateTitle(title); + + return getFolderList(userId); + } + + /** + * user의 폴더 리스트를 조회합니다. + * 정렬 기준: 최근 생성 순 + * + * @param userId + * @return 각 folder의 Id, title을 담은 리스트 + */ + @Override + @Transactional(readOnly = true) + public FolderResponse.FolderDtoList getFolderList(Long userId) { + List folderList = folderDbService.findFolderDtoList(userId); + return FolderConverter.toFolderDtoList(folderList); + } + + private void validDuplicatedFolderTitleAndLength(String title, User user) { + // 폴더명 글자 수 검사 + if (title.length() > 15) { + throw new FolderException(FolderErrorStatus.OVERFLOW_FOLDER_TITLE); + } + + // 폴더명 중복 검사 + if (folderDbService.isFolderExist(title, user)) { + throw new FolderException(FolderErrorStatus.DUPLICATED_FOLDER_TITLE); + } + } + + private void validIsUserAuthorizedForFolder(Long userId, Folder folder) { + if (!folder.getUser().getUserId().equals(userId)) + throw new FolderException(FolderErrorStatus.USER_FOLDER_UNAUTHORIZED); + } + +} + diff --git a/src/main/java/corecord/dev/domain/folder/domain/repository/FolderRepository.java b/src/main/java/corecord/dev/domain/folder/domain/repository/FolderRepository.java index cedeb15..36a385a 100644 --- a/src/main/java/corecord/dev/domain/folder/domain/repository/FolderRepository.java +++ b/src/main/java/corecord/dev/domain/folder/domain/repository/FolderRepository.java @@ -3,7 +3,6 @@ import corecord.dev.domain.folder.domain.dto.response.FolderResponse; import corecord.dev.domain.folder.domain.entity.Folder; import corecord.dev.domain.user.domain.entity.User; -import jakarta.validation.Valid; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -18,16 +17,17 @@ public interface FolderRepository extends JpaRepository { @Query("SELECT new corecord.dev.domain.folder.domain.dto.response.FolderResponse$FolderDto(f.folderId, f.title) " + "FROM Folder f " + - "WHERE f.user = :user " + + "WHERE f.user.userId = :userId " + "ORDER BY f.createdAt desc ") - List findFolderDtoList(@Param(value = "user") User user); + List findFolderDtoList(@Param(value = "userId") Long userId); @Query("SELECT f " + "FROM Folder f " + - "WHERE f.title = :title AND f.user = :user ") + "JOIN FETCH f.user u " + + "WHERE f.title = :title AND u.userId = :userId ") Optional findFolderByTitle( @Param(value = "title") String title, - @Param(value = "user") User user); + @Param(value = "userId") Long userId); boolean existsByTitleAndUser(String title, User user); diff --git a/src/main/java/corecord/dev/domain/record/application/RecordDbService.java b/src/main/java/corecord/dev/domain/record/application/RecordDbService.java index d99fcc8..6c25946 100644 --- a/src/main/java/corecord/dev/domain/record/application/RecordDbService.java +++ b/src/main/java/corecord/dev/domain/record/application/RecordDbService.java @@ -62,23 +62,21 @@ public Record findTmpRecordById(Long recordId) { .orElseThrow(() -> new RecordException(RecordErrorStatus.RECORD_NOT_FOUND)); } - public List findRecordListByFolder(User user, Folder folder, Long lastRecordId) { + public List findRecordListByFolder(Long userId, Folder folder, Long lastRecordId) { Pageable pageable = PageRequest.of(0, listSize + 1, Sort.by("createdAt").descending()); - return recordRepository.findRecordsByFolder(folder, user, lastRecordId, pageable); + return recordRepository.findRecordsByFolder(folder, userId, lastRecordId, pageable); } - public List findRecordList(User user, Long lastRecordId) { - Pageable pageable = PageRequest.of(0, listSize + 1, Sort.by("createdAt").descending()); - return recordRepository.findRecords(user, lastRecordId, pageable); - } + public List findRecordList(Long userId, Long lastRecordId) { + // -1일 경우 최근 생성된 6개 리스트만 조회 + int newListSize = lastRecordId == -1 ? 6 : listSize + 1; - public List findRecordListOrderByCreatedAt(User user) { - Pageable pageable = PageRequest.of(0, 6, Sort.by("createdAt").descending()); - return recordRepository.findRecordsOrderByCreatedAt(user, pageable); + Pageable pageable = PageRequest.of(0, newListSize, Sort.by("createdAt").descending()); + return recordRepository.findRecords(userId, lastRecordId, pageable); } - public List findRecordListByKeyword(User user, Keyword keyword, Long lastRecordId) { + public List findRecordListByKeyword(Long userId, Keyword keyword, Long lastRecordId) { Pageable pageable = PageRequest.of(0, listSize + 1, Sort.by("createdAt").descending()); - return recordRepository.findRecordsByKeyword(keyword, user, lastRecordId, pageable); + return recordRepository.findRecordsByKeyword(keyword, userId, lastRecordId, pageable); } } diff --git a/src/main/java/corecord/dev/domain/record/application/RecordService.java b/src/main/java/corecord/dev/domain/record/application/RecordService.java index 814dea8..7e41265 100644 --- a/src/main/java/corecord/dev/domain/record/application/RecordService.java +++ b/src/main/java/corecord/dev/domain/record/application/RecordService.java @@ -1,239 +1,24 @@ package corecord.dev.domain.record.application; -import corecord.dev.domain.ability.domain.entity.Keyword; -import corecord.dev.domain.ability.status.AbilityErrorStatus; -import corecord.dev.domain.ability.exception.AbilityException; -import corecord.dev.domain.analysis.application.AnalysisService; -import corecord.dev.domain.chat.application.ChatDbService; -import corecord.dev.domain.chat.domain.entity.ChatRoom; -import corecord.dev.domain.folder.application.FolderDbService; -import corecord.dev.domain.folder.domain.entity.Folder; -import corecord.dev.domain.record.domain.entity.RecordType; -import corecord.dev.domain.record.domain.converter.RecordConverter; import corecord.dev.domain.record.domain.dto.request.RecordRequest; import corecord.dev.domain.record.domain.dto.response.RecordResponse; -import corecord.dev.domain.record.domain.entity.Record; -import corecord.dev.domain.record.status.RecordErrorStatus; -import corecord.dev.domain.record.exception.RecordException; -import corecord.dev.domain.user.application.UserDbService; -import corecord.dev.domain.user.domain.entity.User; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Optional; -@Service -@Slf4j -@RequiredArgsConstructor -public class RecordService { +public interface RecordService { - private final AnalysisService analysisService; - private final RecordDbService recordDbService; - private final UserDbService userDbService; - private final FolderDbService folderDbService; - private final ChatDbService chatDbService; + // record + RecordResponse.MemoRecordDto createMemoRecord(Long userId, RecordRequest.RecordDto recordDto); + RecordResponse.MemoRecordDto getMemoRecordDetail(Long userId, Long recordId); - private final int listSize = 30; + // tmp record + void createTmpMemoRecord(Long userId, RecordRequest.TmpMemoRecordDto tmpMemoRecordDto); + RecordResponse.TmpMemoRecordDto getTmpMemoRecord(Long userId); - /* - * user의 MEMO ver. 경험을 기록하고 폴더를 지정한 후 생성된 경험 기록 정보를 반환 - * @param userId, recordDto - * @return - */ - public RecordResponse.MemoRecordDto createMemoRecord(Long userId, RecordRequest.RecordDto recordDto) { - User user = userDbService.findUserById(userId); - String title = recordDto.getTitle(); - String content = recordDto.getContent(); - Folder folder = folderDbService.findFolderById(recordDto.getFolderId()); + // record list + RecordResponse.RecordListDto getRecordListByFolder(Long userId, String folderName, Long lastRecordId); + RecordResponse.RecordListDto getRecordListByKeyword(Long userId, String keywordValue, Long lastRecordId); + RecordResponse.RecordListDto getRecentRecordList(Long userId); - // 제목, 본문 글자 수 검사 - validTextLength(title, content); + void updateFolderOfRecord(Long userId, RecordRequest.UpdateFolderDto updateFolderDto); - // 경험 기록 종류에 따른 Record 생성 - Record record = createRecordBasedOnType(recordDto, user, folder); - - // 역량 분석 레포트 생성 - analysisService.createAnalysis(record, user); - recordDbService.saveRecord(record); - - return RecordConverter.toMemoRecordDto(record); - } - - private Record createRecordBasedOnType(RecordRequest.RecordDto recordDto, User user, Folder folder) { - if (recordDto.getRecordType() == RecordType.MEMO) - return RecordConverter.toMemoRecordEntity(recordDto.getTitle(), recordDto.getContent(), user, folder); - - ChatRoom chatRoom = chatDbService.findChatRoomById(recordDto.getChatRoomId(), user); - return RecordConverter.toChatRecordEntity(recordDto.getTitle(), recordDto.getContent(), user, folder, chatRoom); - } - - /* - * recordId를 받아 MEMO ver. 경험 기록의 상세 정보를 반환 - * @param userId, recordId - * @return - */ - @Transactional(readOnly = true) - public RecordResponse.MemoRecordDto getMemoRecordDetail(Long userId, Long recordId) { - User user = userDbService.findUserById(userId); - Record record = recordDbService.findRecordById(recordId); - - // User-Record 권한 유효성 검증 - validIsUserAuthorizedForRecord(user, record); - - return RecordConverter.toMemoRecordDto(record); - } - - private void validIsUserAuthorizedForRecord(User user, Record record) { - if (!record.getUser().equals(user)) - throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); - } - - /* - * title, content를 받아 Record 객체를 생성한 후, recordId를 User.tmpMemo에 저장 - * @param userId, tmpMemoRecordDto - */ - @Transactional - public void createTmpMemoRecord(Long userId, RecordRequest.TmpMemoRecordDto tmpMemoRecordDto) { - User user = userDbService.findUserById(userId); - String title = tmpMemoRecordDto.getTitle(); - String content = tmpMemoRecordDto.getContent(); - - // User의 임시 메모 저장 유무 확인 - validHasUserTmpMemo(user); - - // 제목, 본문 글자 수 검사 - validTextLength(title, content); - - // Record entity 생성 후 user.tmpMemo 필드에 recordId 저장 - Record record = RecordConverter.toMemoRecordEntity(title, content, user, null); - Record tmpRecord = recordDbService.saveRecord(record); - user.updateTmpMemo(tmpRecord.getRecordId()); - } - - private void validHasUserTmpMemo(User user) { - if (user.getTmpMemo() != null) - throw new RecordException(RecordErrorStatus.ALREADY_TMP_MEMO); - } - - /* - * user의 임시 저장된 메모 기록이 있다면 해당 Record row와 tmpMemo 필드 정보를 제거한 후 저장된 데이터를 반환 - * @param userId - * @return - */ - @Transactional - public RecordResponse.TmpMemoRecordDto getTmpMemoRecord(Long userId) { - User user = userDbService.findUserById(userId); - Long tmpMemoRecordId = user.getTmpMemo(); - - // 임시 저장 내역이 없는 경우 isExist=false 반환 - if (tmpMemoRecordId == null) { - return RecordConverter.toNotExistingTmpMemoRecordDto(); - } - - // 임시 저장 내역이 있는 경우 결과 조회 - Record tmpMemoRecord = recordDbService.findTmpRecordById(tmpMemoRecordId); - - // 기존 데이터 제거 후 결과 반환 - user.deleteTmpMemo(); - recordDbService.deleteRecord(tmpMemoRecord); - return RecordConverter.toExistingTmpMemoRecordDto(tmpMemoRecord); - } - - /* - * 폴더별 경험 기록 리스트를 반환합니다. folder의 default value는 'all'입니다. - * @param userId, folderName - * @return - */ - @Transactional(readOnly = true) - public RecordResponse.RecordListDto getRecordList(Long userId, String folderName, Long lastRecordId) { - User user = userDbService.findUserById(userId); - - List recordList = fetchRecords(user, folderName, lastRecordId); - - // 다음 조회할 데이터가 남아있는지 확인 - boolean hasNext = recordList.size() == listSize + 1; - if (hasNext) - recordList = recordList.subList(0, listSize); - - return RecordConverter.toRecordListDto(recordList, hasNext); - } - - private List fetchRecords(User user, String folderName, Long lastRecordId) { - if (folderName.equals("all")) { - return recordDbService.findRecordList(user, lastRecordId); - } - Folder folder = folderDbService.findFolderByTitle(user, folderName); - return recordDbService.findRecordListByFolder(user, folder, lastRecordId); - } - - /* - * keyword를 받아 해당 키워드를 가진 역량 분석 정보와 경험 기록 정보를 반환 - * @param userId - * @param keywordValue - * @param lastRecordId - * @return - */ - @Transactional(readOnly = true) - public RecordResponse.RecordListDto getKeywordRecordList(Long userId, String keywordValue, Long lastRecordId) { - User user = userDbService.findUserById(userId); - - // 해당 keyword를 가진 ability 객체 조회 후 맵핑된 Record 객체 리스트 조회 - Keyword keyword = getKeyword(keywordValue); - List recordList = recordDbService.findRecordListByKeyword(user, keyword, lastRecordId); - - // 다음 조회할 데이터가 남아있는지 확인 - boolean hasNext = recordList.size() == listSize + 1; - if (hasNext) - recordList = recordList.subList(0, listSize); - - return RecordConverter.toRecordListDto(recordList, hasNext); - } - - private Keyword getKeyword(String keywordValue) { - return Optional.ofNullable(Keyword.getName(keywordValue)) - .orElseThrow(() -> new AbilityException(AbilityErrorStatus.INVALID_KEYWORD)); - } - - /* - * record가 속한 폴더를 변경 - * @param userId - * @param updateFolderDto - */ - @Transactional - public void updateFolder(Long userId, RecordRequest.UpdateFolderDto updateFolderDto) { - User user = userDbService.findUserById(userId); - Record record = recordDbService.findRecordById(updateFolderDto.getRecordId()); - Folder folder = folderDbService.findFolderByTitle(user, updateFolderDto.getFolder()); - - record.updateFolder(folder); - } - - /* - * 최근 생성된 경험 기록 리스트 3개를 반환 - * @param userId - * @return RecordListDto - */ - public RecordResponse.RecordListDto getRecentRecordList(Long userId) { - User user = userDbService.findUserById(userId); - - // 최근 생성된 3개의 데이터만 조회 - List recordList = recordDbService.findRecordListOrderByCreatedAt(user); - - return RecordConverter.toRecordListDto(recordList, false); - } - - private void validTextLength(String title, String content) { - if (title != null && title.length() > 50) - throw new RecordException(RecordErrorStatus.OVERFLOW_MEMO_RECORD_TITLE); - - if (content != null && content.length() < 50) - throw new RecordException(RecordErrorStatus.NOT_ENOUGH_MEMO_RECORD_CONTENT); - - if (content != null && content.length() > 500) { - throw new RecordException(RecordErrorStatus.OVERFLOW_MEMO_RECORD_CONTENT); - } - } } diff --git a/src/main/java/corecord/dev/domain/record/application/RecordServiceImpl.java b/src/main/java/corecord/dev/domain/record/application/RecordServiceImpl.java new file mode 100644 index 0000000..0d95fa1 --- /dev/null +++ b/src/main/java/corecord/dev/domain/record/application/RecordServiceImpl.java @@ -0,0 +1,238 @@ +package corecord.dev.domain.record.application; + +import corecord.dev.domain.ability.domain.entity.Keyword; +import corecord.dev.domain.ability.exception.AbilityException; +import corecord.dev.domain.ability.status.AbilityErrorStatus; +import corecord.dev.domain.analysis.application.AnalysisService; +import corecord.dev.domain.chat.application.ChatDbService; +import corecord.dev.domain.chat.domain.entity.ChatRoom; +import corecord.dev.domain.folder.application.FolderDbService; +import corecord.dev.domain.folder.domain.entity.Folder; +import corecord.dev.domain.record.domain.converter.RecordConverter; +import corecord.dev.domain.record.domain.dto.request.RecordRequest; +import corecord.dev.domain.record.domain.dto.response.RecordResponse; +import corecord.dev.domain.record.domain.entity.Record; +import corecord.dev.domain.record.domain.entity.RecordType; +import corecord.dev.domain.record.exception.RecordException; +import corecord.dev.domain.record.status.RecordErrorStatus; +import corecord.dev.domain.user.application.UserDbService; +import corecord.dev.domain.user.domain.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class RecordServiceImpl implements RecordService { + + private final AnalysisService analysisService; + private final RecordDbService recordDbService; + private final UserDbService userDbService; + private final FolderDbService folderDbService; + private final ChatDbService chatDbService; + + private final int listSize = 30; + + /** + * user의 MEMO ver. 경험을 기록하고 폴더를 지정한 후 생성된 경험 기록 정보를 반환합니다. + * + * @param userId, recordDto + * @return recordId, title, content, folderName, createdAt + */ + @Override + public RecordResponse.MemoRecordDto createMemoRecord(Long userId, RecordRequest.RecordDto recordDto) { + User user = userDbService.findUserById(userId); + String title = recordDto.getTitle(); + String content = recordDto.getContent(); + Folder folder = folderDbService.findFolderById(recordDto.getFolderId()); + + // 제목, 본문 글자 수 검사 + validTextLength(title, content); + + // 경험 기록 종류에 따른 Record 생성 + Record record = createRecordBasedOnType(recordDto, user, folder); + + // 역량 분석 레포트 생성 + analysisService.createAnalysis(record, user); + recordDbService.saveRecord(record); + + return RecordConverter.toMemoRecordDto(record); + } + + private Record createRecordBasedOnType(RecordRequest.RecordDto recordDto, User user, Folder folder) { + if (recordDto.getRecordType() == RecordType.MEMO) + return RecordConverter.toMemoRecordEntity(recordDto.getTitle(), recordDto.getContent(), user, folder); + + ChatRoom chatRoom = chatDbService.findChatRoomById(recordDto.getChatRoomId(), user.getUserId()); + return RecordConverter.toChatRecordEntity(recordDto.getTitle(), recordDto.getContent(), user, folder, chatRoom); + } + + /** + * recordId를 받아 MEMO ver. 경험 기록의 상세 정보를 반환합니다. + * + * @param userId, recordId + * @return recordId, title, content, folderName, createdAt + */ + @Override + @Transactional(readOnly = true) + public RecordResponse.MemoRecordDto getMemoRecordDetail(Long userId, Long recordId) { + Record record = recordDbService.findRecordById(recordId); + validIsUserAuthorizedForRecord(userId, record); + return RecordConverter.toMemoRecordDto(record); + } + + private void validIsUserAuthorizedForRecord(Long userId, Record record) { + if (!record.getUser().getUserId().equals(userId)) + throw new RecordException(RecordErrorStatus.USER_RECORD_UNAUTHORIZED); + } + + /** + * title, content를 받아 Record 객체를 생성한 후, recordId를 User.tmpMemo에 저장합니다. + * + * @param userId, tmpMemoRecordDto + */ + @Transactional + public void createTmpMemoRecord(Long userId, RecordRequest.TmpMemoRecordDto tmpMemoRecordDto) { + User user = userDbService.findUserById(userId); + String title = tmpMemoRecordDto.getTitle(); + String content = tmpMemoRecordDto.getContent(); + + // User의 임시 메모 저장 유무 확인 + validHasUserTmpMemo(user); + + validTextLength(title, content); + + // Record entity 생성 후 user.tmpMemo 필드에 recordId 저장 + Record record = RecordConverter.toMemoRecordEntity(title, content, user, null); + Record tmpRecord = recordDbService.saveRecord(record); + user.updateTmpMemo(tmpRecord.getRecordId()); + } + + private void validHasUserTmpMemo(User user) { + if (user.getTmpMemo() != null) + throw new RecordException(RecordErrorStatus.ALREADY_TMP_MEMO); + } + + /** + * user의 임시 저장된 메모 기록 정보를 반환합니다. + * 해당 Record data와 tmpMemo 필드 정보를 제거합니다. + * + * @param userId + * @return 임시 메모 존재 여부, 메모 title, content + */ + @Override + @Transactional + public RecordResponse.TmpMemoRecordDto getTmpMemoRecord(Long userId) { + User user = userDbService.findUserById(userId); + Long tmpMemoRecordId = user.getTmpMemo(); + + // 임시 저장 내역이 없는 경우 isExist=false 반환 + if (tmpMemoRecordId == null) { + return RecordConverter.toNotExistingTmpMemoRecordDto(); + } + + // 임시 저장 내역이 있는 경우 결과 조회 + Record tmpMemoRecord = recordDbService.findTmpRecordById(tmpMemoRecordId); + + // 기존 데이터 제거 후 결과 반환 + user.deleteTmpMemo(); + recordDbService.deleteRecord(tmpMemoRecord); + return RecordConverter.toExistingTmpMemoRecordDto(tmpMemoRecord); + } + + /** + * 폴더별 경험 기록 리스트를 반환합니다. + * folder의 default value: 'all' + * + * @param userId, folderName + * @return + */ + @Override + @Transactional(readOnly = true) + public RecordResponse.RecordListDto getRecordListByFolder(Long userId, String folderName, Long lastRecordId) { + List recordList = fetchRecords(userId, folderName, lastRecordId); + + // 다음 조회할 데이터가 남아있는지 확인 + boolean hasNext = recordList.size() == listSize + 1; + if (hasNext) + recordList = recordList.subList(0, listSize); + + return RecordConverter.toRecordListDto(recordList, hasNext); + } + + private List fetchRecords(Long userId, String folderName, Long lastRecordId) { + if (folderName.equals("all")) { + return recordDbService.findRecordList(userId, lastRecordId); + } + Folder folder = folderDbService.findFolderByTitle(userId, folderName); + return recordDbService.findRecordListByFolder(userId, folder, lastRecordId); + } + + /** + * keyword를 받아 해당 키워드를 가진 역량 분석 정보와 경험 기록 정보를 반환합니다. + * + * @param userId + * @param keywordValue + * @param lastRecordId + * @return + */ + @Override + @Transactional(readOnly = true) + public RecordResponse.RecordListDto getRecordListByKeyword(Long userId, String keywordValue, Long lastRecordId) { + // 해당 keyword를 가진 ability 객체 조회 후 맵핑된 Record 객체 리스트 조회 + Keyword keyword = getKeyword(keywordValue); + List recordList = recordDbService.findRecordListByKeyword(userId, keyword, lastRecordId); + + // 다음 조회할 데이터가 남아있는지 확인 + boolean hasNext = recordList.size() == listSize + 1; + if (hasNext) + recordList = recordList.subList(0, listSize); + + return RecordConverter.toRecordListDto(recordList, hasNext); + } + + private Keyword getKeyword(String keywordValue) { + return Optional.ofNullable(Keyword.getName(keywordValue)) + .orElseThrow(() -> new AbilityException(AbilityErrorStatus.INVALID_KEYWORD)); + } + + /** + * 최근 생성된 경험 기록 리스트 6개를 반환합니다. + * + * @param userId + * @return RecordListDto + */ + public RecordResponse.RecordListDto getRecentRecordList(Long userId) { + List recordList = recordDbService.findRecordList(userId, -1L); + return RecordConverter.toRecordListDto(recordList, false); + } + + private void validTextLength(String title, String content) { + if (title != null && title.length() > 50) + throw new RecordException(RecordErrorStatus.OVERFLOW_MEMO_RECORD_TITLE); + + if (content != null && content.length() < 50) + throw new RecordException(RecordErrorStatus.NOT_ENOUGH_MEMO_RECORD_CONTENT); + + if (content != null && content.length() > 500) { + throw new RecordException(RecordErrorStatus.OVERFLOW_MEMO_RECORD_CONTENT); + } + } + + /** + * record가 속한 폴더를 변경합니다. + * + * @param userId + * @param updateFolderDto + */ + @Transactional + public void updateFolderOfRecord(Long userId, RecordRequest.UpdateFolderDto updateFolderDto) { + Record record = recordDbService.findRecordById(updateFolderDto.getRecordId()); + Folder folder = folderDbService.findFolderByTitle(userId, updateFolderDto.getFolder()); + + record.updateFolder(folder); + } +} diff --git a/src/main/java/corecord/dev/domain/record/domain/repository/RecordRepository.java b/src/main/java/corecord/dev/domain/record/domain/repository/RecordRepository.java index f0fa903..1bcfa45 100644 --- a/src/main/java/corecord/dev/domain/record/domain/repository/RecordRepository.java +++ b/src/main/java/corecord/dev/domain/record/domain/repository/RecordRepository.java @@ -22,12 +22,13 @@ public interface RecordRepository extends JpaRepository { "JOIN FETCH r.analysis a " + "JOIN FETCH r.folder f " + "JOIN FETCH a.abilityList al " + - "WHERE r.user = :user " + + "JOIN r.user u " + + "WHERE u.userId = :userId " + "AND (:last_record_id = 0 OR r.recordId < :last_record_id) " + // 제일 마지막에 읽은 데이터 이후부터 가져옴 "AND r.folder is not null AND r.folder = :folder") // 임시 저장 기록 제외 List findRecordsByFolder( @Param(value = "folder") Folder folder, - @Param(value = "user") User user, + @Param(value = "userId") Long userId, @Param(value = "last_record_id") Long lastRecordId, Pageable pageable); @@ -35,11 +36,12 @@ List findRecordsByFolder( "JOIN FETCH r.analysis a " + "JOIN FETCH r.folder f " + "JOIN FETCH a.abilityList al " + - "WHERE r.user = :user " + - "AND (:last_record_id = 0 OR r.recordId < :last_record_id) " + // 제일 마지막에 읽은 데이터 이후부터 가져옴 + "JOIN r.user u " + + "WHERE u.userId = :userId " + + "AND (:last_record_id <= 0 OR r.recordId < :last_record_id) " + // 제일 마지막에 읽은 데이터 이후부터 가져옴 "AND r.folder is not null") // 임시 저장 기록 제외 List findRecords( - @Param(value = "user") User user, + @Param(value = "userId") Long userId, @Param(value = "last_record_id") Long lastRecordId, Pageable pageable); @@ -48,27 +50,17 @@ List findRecords( "JOIN a.analysis an " + "JOIN an.record r " + "JOIN FETCH r.folder f " + - "WHERE a.user = :user " + + "WHERE a.user.userId = :userId " + "AND a.keyword = :keyword " + "AND (:last_record_id = 0 OR r.recordId < :last_record_id) " + // 제일 마지막에 읽은 데이터 이후부터 가져옴 "AND r.folder is not null") // 임시 저장 기록 제외 List findRecordsByKeyword( @Param(value = "keyword")Keyword keyword, - @Param(value = "user") User user, + @Param(value = "userId") Long userId, @Param(value = "last_record_id") Long lastRecordId, Pageable pageable ); - @Query("SELECT r FROM Record r " + - "JOIN FETCH r.analysis a " + - "JOIN FETCH r.folder f " + - "JOIN FETCH a.abilityList al " + - "WHERE r.user = :user " + - "AND r.folder is not null ") // 임시 저장 기록 제외 - List findRecordsOrderByCreatedAt( - @Param(value = "user") User user, - Pageable pageable); - @Query("SELECT r FROM Record r " + "JOIN FETCH r.analysis a " + "JOIN FETCH r.folder f " + diff --git a/src/main/java/corecord/dev/domain/record/presentation/RecordController.java b/src/main/java/corecord/dev/domain/record/presentation/RecordController.java index e93cb96..c45c14e 100644 --- a/src/main/java/corecord/dev/domain/record/presentation/RecordController.java +++ b/src/main/java/corecord/dev/domain/record/presentation/RecordController.java @@ -58,7 +58,7 @@ public ResponseEntity> getRecordListBy @RequestParam(name = "folder", defaultValue = "all") String folder, @RequestParam(name = "lastRecordId", defaultValue = "0") Long lastRecordId ) { - RecordResponse.RecordListDto recordResponse = recordService.getRecordList(userId, folder, lastRecordId); + RecordResponse.RecordListDto recordResponse = recordService.getRecordListByFolder(userId, folder, lastRecordId); return ApiResponse.success(RecordSuccessStatus.RECORD_LIST_GET_SUCCESS, recordResponse); } @@ -68,7 +68,7 @@ public ResponseEntity> getRecordListBy @RequestParam(name = "keyword") String keyword, @RequestParam(name = "lastRecordId", defaultValue = "0") Long lastRecordId ) { - RecordResponse.RecordListDto recordResponse = recordService.getKeywordRecordList(userId, keyword, lastRecordId); + RecordResponse.RecordListDto recordResponse = recordService.getRecordListByKeyword(userId, keyword, lastRecordId); return ApiResponse.success(RecordSuccessStatus.KEYWORD_RECORD_LIST_GET_SUCCESS, recordResponse); } @@ -77,7 +77,7 @@ public ResponseEntity> updateRecordForFolder( @UserId Long userId, @RequestBody @Valid RecordRequest.UpdateFolderDto updateFolderDto ) { - recordService.updateFolder(userId, updateFolderDto); + recordService.updateFolderOfRecord(userId, updateFolderDto); return ApiResponse.success(RecordSuccessStatus.RECORD_FOLDER_UPDATE_SUCCESS); } diff --git a/src/main/java/corecord/dev/domain/user/application/UserService.java b/src/main/java/corecord/dev/domain/user/application/UserService.java index 2b17d7b..52aa0af 100644 --- a/src/main/java/corecord/dev/domain/user/application/UserService.java +++ b/src/main/java/corecord/dev/domain/user/application/UserService.java @@ -1,164 +1,14 @@ package corecord.dev.domain.user.application; -import corecord.dev.domain.ability.application.AbilityDbService; -import corecord.dev.domain.analysis.application.AnalysisDbService; -import corecord.dev.domain.auth.jwt.JwtUtil; -import corecord.dev.domain.chat.application.ChatDbService; -import corecord.dev.domain.folder.application.FolderDbService; -import corecord.dev.domain.record.application.RecordDbService; -import corecord.dev.domain.auth.domain.entity.RefreshToken; -import corecord.dev.domain.auth.status.TokenErrorStatus; -import corecord.dev.domain.auth.exception.TokenException; -import corecord.dev.domain.auth.domain.repository.RefreshTokenRepository; -import corecord.dev.domain.user.domain.converter.UserConverter; import corecord.dev.domain.user.domain.dto.request.UserRequest; import corecord.dev.domain.user.domain.dto.response.UserResponse; -import corecord.dev.domain.user.domain.entity.Status; -import corecord.dev.domain.user.domain.entity.User; -import corecord.dev.domain.user.status.UserErrorStatus; -import corecord.dev.domain.user.exception.UserException; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import java.util.Optional; -import java.util.regex.Pattern; +public interface UserService { -@Slf4j -@Service -@RequiredArgsConstructor -public class UserService { + UserResponse.UserDto registerUser(String registerToken, UserRequest.UserRegisterDto userRegisterDto); + void logoutUser(String refreshToken); + void deleteUser(Long userId, String refreshToken); + void updateUser(Long userId, UserRequest.UserUpdateDto userUpdateDto); + UserResponse.UserInfoDto getUserInfo(Long userId); - private final JwtUtil jwtUtil; - private final RefreshTokenRepository refreshTokenRepository; - private final AnalysisDbService analysisDbService; - private final AbilityDbService abilityDbService; - private final ChatDbService chatDbService; - private final UserDbService userDbService; - private final FolderDbService folderDbService; - private final RecordDbService recordDbService; - - /** - * 회원가입 - * @param registerToken - * @param userRegisterDto - * @return - */ - @Transactional - public UserResponse.UserDto registerUser(String registerToken, UserRequest.UserRegisterDto userRegisterDto) { - validRegisterToken(registerToken); - validateUserInfo(userRegisterDto.getNickName()); - - String providerId = jwtUtil.getProviderIdFromToken(registerToken); - - checkExistUser(providerId); - - // 새로운 유저 생성 - User newUser = UserConverter.toUserEntity(userRegisterDto, providerId); - User savedUser = userDbService.saveUser(newUser); - - // RefreshToken 생성 및 저장 - String refreshToken = jwtUtil.generateRefreshToken(savedUser.getUserId()); - saveRefreshToken(refreshToken, savedUser); - - String accessToken = jwtUtil.generateAccessToken(savedUser.getUserId()); - - return UserConverter.toUserDto(savedUser, accessToken, refreshToken); - } - - private void validRegisterToken(String registerToken) { - if (!jwtUtil.isRegisterTokenValid(registerToken)) - throw new TokenException(TokenErrorStatus.INVALID_REGISTER_TOKEN); - } - - private void checkExistUser(String providerId) { - if (userDbService.IsUserExistByProviderId(providerId)) - throw new UserException(UserErrorStatus.ALREADY_EXIST_USER); - } - - private void saveRefreshToken(String refreshToken, User user) { - RefreshToken newRefreshToken = RefreshToken.of(refreshToken, user.getUserId()); - refreshTokenRepository.save(newRefreshToken); - } - - /** - * 로그아웃 - * @param refreshToken - */ - @Transactional - public void logoutUser(String refreshToken) { - deleteRefreshTokenInRedis(refreshToken); - } - - /** - * 회원 탈퇴 - * @param userId - * @param refreshToken - */ - @Transactional - public void deleteUser(Long userId, String refreshToken) { - // 연관된 데이터 삭제 - abilityDbService.deleteAbilityByUserId(userId); - analysisDbService.deleteAnalysisByUserId(userId); - chatDbService.deleteChatByUserId(userId); - recordDbService.deleteRecordByUserId(userId); - chatDbService.deleteChatRoomByUserId(userId); - folderDbService.deleteFolderByUserId(userId); - userDbService.deleteUserByUserId(userId); - deleteRefreshTokenInRedis(refreshToken); - } - - private void deleteRefreshTokenInRedis(String refreshToken) { - if (refreshToken == null || refreshToken.isEmpty()) { - log.info("쿠키에 리프레쉬 토큰 없음"); - return; - } - Optional refreshTokenOptional = refreshTokenRepository.findByRefreshToken(refreshToken); - refreshTokenOptional.ifPresent(refreshTokenRepository::delete); - } - - /** - * 유저 정보 수정 - * @param userId - * @param userUpdateDto - */ - @Transactional - public void updateUser(Long userId, UserRequest.UserUpdateDto userUpdateDto) { - User user = userDbService.getUser(userId); - - if(userUpdateDto.getNickName() != null) { - validateUserInfo(userUpdateDto.getNickName()); - user.setNickName(userUpdateDto.getNickName()); - } - - if(userUpdateDto.getStatus() != null) { - user.setStatus(Status.getStatus(userUpdateDto.getStatus())); - } - } - - private void validateUserInfo(String nickName) { - if (nickName == null || nickName.isEmpty() || nickName.length() > 10) { - throw new UserException(UserErrorStatus.INVALID_USER_NICKNAME); - } - - // 한글, 영어, 숫자, 공백만 허용 - String nicknamePattern = "^[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣\s]*$"; - if (!Pattern.matches(nicknamePattern, nickName)) { - throw new UserException(UserErrorStatus.INVALID_USER_NICKNAME); - } - } - - /** - * 유저 정보 조회 - * @param userId - * @return - */ - @Transactional - public UserResponse.UserInfoDto getUserInfo(Long userId) { - User user = userDbService.getUser(userId); - - int recordCount = recordDbService.getRecordCount(user);; - return UserConverter.toUserInfoDto(user, recordCount); - } } diff --git a/src/main/java/corecord/dev/domain/user/application/UserServiceImpl.java b/src/main/java/corecord/dev/domain/user/application/UserServiceImpl.java new file mode 100644 index 0000000..89850ad --- /dev/null +++ b/src/main/java/corecord/dev/domain/user/application/UserServiceImpl.java @@ -0,0 +1,173 @@ +package corecord.dev.domain.user.application; + +import corecord.dev.domain.ability.application.AbilityDbService; +import corecord.dev.domain.analysis.application.AnalysisDbService; +import corecord.dev.domain.auth.domain.entity.RefreshToken; +import corecord.dev.domain.auth.domain.repository.RefreshTokenRepository; +import corecord.dev.domain.auth.exception.TokenException; +import corecord.dev.domain.auth.jwt.JwtUtil; +import corecord.dev.domain.auth.status.TokenErrorStatus; +import corecord.dev.domain.chat.application.ChatDbService; +import corecord.dev.domain.folder.application.FolderDbService; +import corecord.dev.domain.record.application.RecordDbService; +import corecord.dev.domain.user.domain.converter.UserConverter; +import corecord.dev.domain.user.domain.dto.request.UserRequest; +import corecord.dev.domain.user.domain.dto.response.UserResponse; +import corecord.dev.domain.user.domain.entity.Status; +import corecord.dev.domain.user.domain.entity.User; +import corecord.dev.domain.user.exception.UserException; +import corecord.dev.domain.user.status.UserErrorStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.regex.Pattern; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final JwtUtil jwtUtil; + private final RefreshTokenRepository refreshTokenRepository; + private final AnalysisDbService analysisDbService; + private final AbilityDbService abilityDbService; + private final ChatDbService chatDbService; + private final UserDbService userDbService; + private final FolderDbService folderDbService; + private final RecordDbService recordDbService; + + /** + * 회원가입 + * + * @param registerToken + * @param userRegisterDto + * @return + */ + @Override + @Transactional + public UserResponse.UserDto registerUser(String registerToken, UserRequest.UserRegisterDto userRegisterDto) { + validRegisterToken(registerToken); + validateUserInfo(userRegisterDto.getNickName()); + + String providerId = jwtUtil.getProviderIdFromToken(registerToken); + checkExistUser(providerId); + + // 새로운 유저 생성 + User newUser = UserConverter.toUserEntity(userRegisterDto, providerId); + User savedUser = userDbService.saveUser(newUser); + + // RefreshToken 생성 및 저장 + String refreshToken = jwtUtil.generateRefreshToken(savedUser.getUserId()); + String accessToken = jwtUtil.generateAccessToken(savedUser.getUserId()); + saveRefreshToken(refreshToken, savedUser); + + return UserConverter.toUserDto(savedUser, accessToken, refreshToken); + } + + private void validRegisterToken(String registerToken) { + if (!jwtUtil.isRegisterTokenValid(registerToken)) + throw new TokenException(TokenErrorStatus.INVALID_REGISTER_TOKEN); + } + + private void checkExistUser(String providerId) { + if (userDbService.IsUserExistByProviderId(providerId)) + throw new UserException(UserErrorStatus.ALREADY_EXIST_USER); + } + + private void saveRefreshToken(String refreshToken, User user) { + RefreshToken newRefreshToken = RefreshToken.of(refreshToken, user.getUserId()); + refreshTokenRepository.save(newRefreshToken); + } + + /** + * 로그아웃 + * + * @param refreshToken + */ + @Override + @Transactional + public void logoutUser(String refreshToken) { + deleteRefreshTokenInRedis(refreshToken); + } + + /** + * 회원 탈퇴 + * + * @param userId + * @param refreshToken + */ + @Override + @Transactional + public void deleteUser(Long userId, String refreshToken) { + // 연관된 데이터 삭제 + abilityDbService.deleteAbilityByUserId(userId); + analysisDbService.deleteAnalysisByUserId(userId); + chatDbService.deleteChatByUserId(userId); + recordDbService.deleteRecordByUserId(userId); + chatDbService.deleteChatRoomByUserId(userId); + folderDbService.deleteFolderByUserId(userId); + userDbService.deleteUserByUserId(userId); + deleteRefreshTokenInRedis(refreshToken); + } + + private void deleteRefreshTokenInRedis(String refreshToken) { + if (refreshToken == null || refreshToken.isEmpty()) { + log.info("쿠키에 리프레쉬 토큰 없음"); + return; + } + Optional refreshTokenOptional = refreshTokenRepository.findByRefreshToken(refreshToken); + refreshTokenOptional.ifPresent(refreshTokenRepository::delete); + } + + /** + * 유저 정보 수정 + * + * @param userId + * @param userUpdateDto + */ + @Override + @Transactional + public void updateUser(Long userId, UserRequest.UserUpdateDto userUpdateDto) { + User user = userDbService.getUser(userId); + + if(userUpdateDto.getNickName() != null) { + validateUserInfo(userUpdateDto.getNickName()); + user.setNickName(userUpdateDto.getNickName()); + } + + if(userUpdateDto.getStatus() != null) { + user.setStatus(Status.getStatus(userUpdateDto.getStatus())); + } + } + + private void validateUserInfo(String nickName) { + if (nickName == null || nickName.isEmpty() || nickName.length() > 10) { + throw new UserException(UserErrorStatus.INVALID_USER_NICKNAME); + } + + // 한글, 영어, 숫자, 공백만 허용 + String nicknamePattern = "^[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣\s]*$"; + if (!Pattern.matches(nicknamePattern, nickName)) { + throw new UserException(UserErrorStatus.INVALID_USER_NICKNAME); + } + } + + /** + * 유저 정보 조회 + * + * @param userId + * @return + */ + @Override + @Transactional(readOnly = true) + public UserResponse.UserInfoDto getUserInfo(Long userId) { + User user = userDbService.getUser(userId); + + int recordCount = recordDbService.getRecordCount(user);; + return UserConverter.toUserInfoDto(user, recordCount); + } + +} diff --git a/src/test/java/corecord/dev/analysis/service/AnalysisServiceTest.java b/src/test/java/corecord/dev/analysis/service/AnalysisServiceTest.java index 97ba58f..f015038 100644 --- a/src/test/java/corecord/dev/analysis/service/AnalysisServiceTest.java +++ b/src/test/java/corecord/dev/analysis/service/AnalysisServiceTest.java @@ -5,6 +5,7 @@ import corecord.dev.domain.ability.status.AbilityErrorStatus; import corecord.dev.domain.ability.exception.AbilityException; import corecord.dev.domain.ability.application.AbilityService; +import corecord.dev.domain.analysis.application.AnalysisAIService; import corecord.dev.domain.analysis.application.AnalysisDbService; import corecord.dev.domain.analysis.domain.dto.request.AnalysisRequest; import corecord.dev.domain.analysis.infra.openai.dto.response.AnalysisAiResponse; @@ -13,7 +14,6 @@ import corecord.dev.domain.analysis.status.AnalysisErrorStatus; import corecord.dev.domain.analysis.exception.AnalysisException; import corecord.dev.domain.analysis.application.AnalysisService; -import corecord.dev.domain.analysis.infra.openai.application.OpenAiAnalysisAIService; import corecord.dev.domain.folder.domain.entity.Folder; import corecord.dev.domain.record.application.RecordDbService; import corecord.dev.domain.record.domain.entity.RecordType; @@ -51,7 +51,7 @@ public class AnalysisServiceTest { private UserDbService userDbService; @Mock - private OpenAiAnalysisAIService openAiAnalysisAIService; + private AnalysisAIService analysisAIService; @Mock private AbilityService abilityService; @@ -81,8 +81,8 @@ record = createMockRecord(user, folder); @DisplayName("메모 역량 분석 생성 테스트") void createMemoAnalysisTest() { // Given - when(openAiAnalysisAIService.generateMemoSummary(any(String.class))).thenReturn(testContent); - when(openAiAnalysisAIService.generateAbilityAnalysis(any(String.class))) + when(analysisAIService.generateMemoSummary(any(String.class))).thenReturn(testContent); + when(analysisAIService.generateAbilityAnalysis(any(String.class))) .thenReturn(new AnalysisAiResponse(Map.of("커뮤니케이션", "Test Keyword Content"), "Test Comment")); doNothing().when(analysisDbService).saveAnalysis(any(Analysis.class)); doNothing().when(abilityService).parseAndSaveAbilities(any(Map.class), any(Analysis.class), any(User.class)); @@ -91,8 +91,8 @@ void createMemoAnalysisTest() { Analysis response = analysisService.createAnalysis(record, user); // Then - verify(openAiAnalysisAIService).generateMemoSummary(testContent); - verify(openAiAnalysisAIService).generateAbilityAnalysis(testContent); + verify(analysisAIService).generateMemoSummary(testContent); + verify(analysisAIService).generateAbilityAnalysis(testContent); verify(analysisDbService).saveAnalysis(any(Analysis.class)); assertEquals(response.getContent(), testContent); @@ -104,7 +104,7 @@ void createMemoAnalysisTest() { void createMemoAnalysisWithNotEnoughContentTest() { // Given String overContent = "Test".repeat(500); - when(openAiAnalysisAIService.generateMemoSummary(any(String.class))).thenReturn(overContent); + when(analysisAIService.generateMemoSummary(any(String.class))).thenReturn(overContent); // When & Then AnalysisException exception = assertThrows(AnalysisException.class, @@ -117,8 +117,8 @@ void createMemoAnalysisWithNotEnoughContentTest() { void createMemoAnalysisWithNotEnoughCommentTest() { // Given String overComment = "Test".repeat(200); - when(openAiAnalysisAIService.generateMemoSummary(any(String.class))).thenReturn(testContent); - when(openAiAnalysisAIService.generateAbilityAnalysis(any(String.class))) + when(analysisAIService.generateMemoSummary(any(String.class))).thenReturn(testContent); + when(analysisAIService.generateAbilityAnalysis(any(String.class))) .thenReturn(new AnalysisAiResponse(Map.of("커뮤니케이션", "Test Keyword Content"), overComment)); // When & Then @@ -132,8 +132,8 @@ void createMemoAnalysisWithNotEnoughCommentTest() { void createMemoAnalysisWithLongKeywordCommentTest() { // Given String overKeywordComment = "Test".repeat(200); - when(openAiAnalysisAIService.generateMemoSummary(any(String.class))).thenReturn(testContent); - when(openAiAnalysisAIService.generateAbilityAnalysis(any(String.class))) + when(analysisAIService.generateMemoSummary(any(String.class))).thenReturn(testContent); + when(analysisAIService.generateAbilityAnalysis(any(String.class))) .thenReturn(new AnalysisAiResponse(Map.of("커뮤니케이션", overKeywordComment), testComment)); // When & Then diff --git a/src/test/java/corecord/dev/chat/repository/ChatRoomRepositoryTest.java b/src/test/java/corecord/dev/chat/repository/ChatRoomRepositoryTest.java index eebabcf..54005cd 100644 --- a/src/test/java/corecord/dev/chat/repository/ChatRoomRepositoryTest.java +++ b/src/test/java/corecord/dev/chat/repository/ChatRoomRepositoryTest.java @@ -39,7 +39,7 @@ void findByChatRoomIdAndUser() { ChatRoom chatRoom = createTestChatRoom(user); // When - Optional foundChatRoom = chatRoomRepository.findByChatRoomIdAndUser(chatRoom.getChatRoomId(), user); + Optional foundChatRoom = chatRoomRepository.findByChatRoomIdAndUserId(chatRoom.getChatRoomId(), user.getUserId()); // Then assertTrue(foundChatRoom.isPresent()); @@ -53,7 +53,7 @@ void findByChatRoomIdAndUser_NotFound() { User user = createTestUser(); // When - Optional foundChatRoom = chatRoomRepository.findByChatRoomIdAndUser(999L, user); + Optional foundChatRoom = chatRoomRepository.findByChatRoomIdAndUserId(999L, user.getUserId()); // Then assertFalse(foundChatRoom.isPresent()); diff --git a/src/test/java/corecord/dev/chat/service/ChatServiceTest.java b/src/test/java/corecord/dev/chat/service/ChatServiceTest.java index ddb9a56..c4daa61 100644 --- a/src/test/java/corecord/dev/chat/service/ChatServiceTest.java +++ b/src/test/java/corecord/dev/chat/service/ChatServiceTest.java @@ -78,7 +78,7 @@ void getChatList() { Chat aiChat = createTestChat("aiChat", 0); when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); when(chatDbService.findChatsByChatRoom(chatRoom)).thenReturn(List.of(userChat, aiChat)); // When @@ -104,7 +104,7 @@ void createChatWithGuide() { .build(); when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); when(chatDbService.saveChat(anyInt(), anyString(), any(ChatRoom.class))) .thenAnswer(invocation -> createTestChat(invocation.getArgument(1), invocation.getArgument(0))); @@ -118,8 +118,13 @@ void createChatWithGuide() { // Then verify(chatDbService, times(3)).saveChat(anyInt(), anyString(), eq(chatRoom)); // 사용자 입력 1개, 가이드 2개 assertEquals(result.getChats().size(), 2); // Guide 메시지는 두 개 생성 - assertEquals(result.getChats().get(0).getContent(), "걱정 마세요!\n저와 대화하다 보면 경험이 정리될 거예요\uD83D\uDCDD"); - assertEquals(result.getChats().get(1).getContent(), "오늘은 어떤 경험을 했나요?\n상황과 해결한 문제를 말해주세요!"); + assertEquals(result.getChats().get(0).getContent(), "아래 질문에 답하다 보면 경험이 정리될 거예요! \n" + + "S(상황) : 어떤 상황이었나요?\n" + + "T(과제) : 마주한 문제나 목표는 무엇이었나요?\n" + + "A(행동) : 문제를 해결하기 위해 어떻게 노력했나요?\n" + + "R(결과) : 그 결과는 어땠나요?"); + assertEquals(result.getChats().get(1).getContent(), "우선 기억나는 내용부터 가볍게 적어보세요.\n" + + "부족한 부분은 대화를 통해 모아모아가 도와줄게요! \uD83D\uDCDD"); } @Test @@ -131,7 +136,7 @@ void createChatWithSuccess() { .build(); when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); when(chatDbService.saveChat(anyInt(), anyString(), any(ChatRoom.class))) .thenAnswer(invocation -> createTestChat(invocation.getArgument(1), invocation.getArgument(0))); when(chatAIService.generateChatResponse(anyList(), anyString())).thenReturn("AI의 예상 응답"); @@ -165,7 +170,7 @@ void validAiResponse() { ); when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); when(chatDbService.findChatsByChatRoom(chatRoom)).thenReturn(chatList); when(chatAIService.generateChatSummaryResponse(anyList())) .thenReturn(new ChatSummaryAiResponse("요약 제목", "요약 내용")); @@ -188,7 +193,7 @@ void emptyAiResponse() { ); when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); when(chatDbService.findChatsByChatRoom(chatRoom)).thenReturn(chatList); when(chatAIService.generateChatSummaryResponse(anyList())) .thenReturn(new ChatSummaryAiResponse("", "")); // 빈 응답 @@ -208,7 +213,7 @@ void longAiTitle() { String longTitle = "a".repeat(51); // 51자 제목 생성 when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); when(chatDbService.findChatsByChatRoom(chatRoom)).thenReturn(chatList); when(chatAIService.generateChatSummaryResponse(anyList())) .thenReturn(new ChatSummaryAiResponse(longTitle, "정상 내용")); // 50자 초과 제목 @@ -228,7 +233,7 @@ void longAiResponse() { String longContent = "a".repeat(501); // 501자 응답 생성 when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); when(chatDbService.findChatsByChatRoom(chatRoom)).thenReturn(chatList); when(chatAIService.generateChatSummaryResponse(anyList())) .thenReturn(new ChatSummaryAiResponse("정상 제목", longContent)); // 500자 초과 내용 @@ -247,14 +252,14 @@ class ChatTmpTests { void saveChatTmp() { // Given when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); // When chatService.saveChatTmp(user.getUserId(), chatRoom.getChatRoomId()); // Then verify(userDbService).findUserById(user.getUserId()); - verify(chatDbService).findChatRoomById(chatRoom.getChatRoomId(), user); + verify(chatDbService).findChatRoomById(chatRoom.getChatRoomId(), user.getUserId()); verify(userDbService).updateUserTmpChat(user, chatRoom.getChatRoomId()); } @@ -264,7 +269,7 @@ void saveChatTmpFailsWhenTmpChatExists() { // Given user.updateTmpChat(chatRoom.getChatRoomId()); when(userDbService.findUserById(user.getUserId())).thenReturn(user); - when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user)).thenReturn(chatRoom); + when(chatDbService.findChatRoomById(chatRoom.getChatRoomId(), user.getUserId())).thenReturn(chatRoom); // When & Then assertThrows(ChatException.class, () -> chatService.saveChatTmp(user.getUserId(), chatRoom.getChatRoomId())); diff --git a/src/test/java/corecord/dev/folder/repository/FolderRepositoryTest.java b/src/test/java/corecord/dev/folder/repository/FolderRepositoryTest.java index 4964b9d..8a8c400 100644 --- a/src/test/java/corecord/dev/folder/repository/FolderRepositoryTest.java +++ b/src/test/java/corecord/dev/folder/repository/FolderRepositoryTest.java @@ -41,7 +41,7 @@ void findFolderByTitle() { Long testId = folder2.getFolderId(); // When - Optional result = folderRepository.findFolderByTitle(testTitle2, user); + Optional result = folderRepository.findFolderByTitle(testTitle2, user.getUserId()); // Then assertThat(result.isPresent()).isTrue(); diff --git a/src/test/java/corecord/dev/folder/service/FolderServiceTest.java b/src/test/java/corecord/dev/folder/service/FolderServiceTest.java index 88405cf..95d9dda 100644 --- a/src/test/java/corecord/dev/folder/service/FolderServiceTest.java +++ b/src/test/java/corecord/dev/folder/service/FolderServiceTest.java @@ -49,7 +49,7 @@ void createFolder() { when(userDbService.findUserById(testId)).thenReturn(user); doNothing().when(folderDbService).saveFolder(any(Folder.class)); - when(folderDbService.findFolderDtoList(user)).thenReturn(List.of( + when(folderDbService.findFolderDtoList(user.getUserId())).thenReturn(List.of( FolderResponse.FolderDto.builder() .folderId(testId) .title(testTitle) @@ -66,7 +66,7 @@ void createFolder() { // Then verify(userDbService).findUserById(testId); verify(folderDbService).saveFolder(any(Folder.class)); - verify(folderDbService).findFolderDtoList(user); + verify(folderDbService).findFolderDtoList(user.getUserId()); assertThat(response.getFolderDtoList()).isNotNull(); assertThat(response.getFolderDtoList().get(0).getTitle()).isEqualTo(testTitle); @@ -86,7 +86,7 @@ void updateFolder() { when(userDbService.findUserById(testId)).thenReturn(user); when(folderDbService.findFolderById(testId)).thenReturn(folder); when(folderDbService.isFolderExist(updatedTitle, user)).thenReturn(false); - when(folderDbService.findFolderDtoList(user)).thenReturn(List.of( + when(folderDbService.findFolderDtoList(user.getUserId())).thenReturn(List.of( FolderResponse.FolderDto.builder() .folderId(testId) .title(updatedTitle) @@ -104,7 +104,7 @@ void updateFolder() { // Then verify(folderDbService).findFolderById(testId); verify(folderDbService).isFolderExist(updatedTitle, user); - verify(folderDbService).findFolderDtoList(user); + verify(folderDbService).findFolderDtoList(user.getUserId()); assertThat(response.getFolderDtoList()).isNotNull(); assertThat(response.getFolderDtoList().get(0).getTitle()).isEqualTo(updatedTitle); diff --git a/src/test/java/corecord/dev/record/memo/repository/MemoRecordRepositoryTest.java b/src/test/java/corecord/dev/record/memo/repository/MemoRecordRepositoryTest.java index 1636504..cf59aa7 100644 --- a/src/test/java/corecord/dev/record/memo/repository/MemoRecordRepositoryTest.java +++ b/src/test/java/corecord/dev/record/memo/repository/MemoRecordRepositoryTest.java @@ -66,7 +66,7 @@ void findRecordByFolder() { Record record2 = createRecord("Test Record2", user, folder); // When - List result = recordRepository.findRecordsByFolder(folder, user, lastRecordId, pageable); + List result = recordRepository.findRecordsByFolder(folder, user.getUserId(), lastRecordId, pageable); // Then assertThat(result.size()).isEqualTo(2); @@ -84,7 +84,7 @@ void findRecordByFolderWhenNoRecordsExist() { Folder folder = createFolder("Test Folder", user); // When - List result = recordRepository.findRecordsByFolder(folder, user, lastRecordId, pageable); + List result = recordRepository.findRecordsByFolder(folder, user.getUserId(), lastRecordId, pageable); // Then assertEquals(result.size(), 0); @@ -118,7 +118,7 @@ void findMemoRecordListByKeywordTest() { Record record2 = createRecord("Test Record2", user, folder); // When - List result = recordRepository.findRecordsByKeyword(Keyword.COLLABORATION, user, lastRecordId, pageable); + List result = recordRepository.findRecordsByKeyword(Keyword.COLLABORATION, user.getUserId(), lastRecordId, pageable); // Then assertEquals(result.size(), 2); diff --git a/src/test/java/corecord/dev/record/memo/service/MemoRecordServiceTest.java b/src/test/java/corecord/dev/record/memo/service/MemoRecordServiceTest.java index ce6e23f..f1dfc09 100644 --- a/src/test/java/corecord/dev/record/memo/service/MemoRecordServiceTest.java +++ b/src/test/java/corecord/dev/record/memo/service/MemoRecordServiceTest.java @@ -54,7 +54,7 @@ public class MemoRecordServiceTest { private Folder folder; private String testTitle = "Test Record"; - private String testContent = "Test".repeat(10); + private String testContent = "Test!".repeat(10); @BeforeEach void setUp() {