diff --git a/Api-Module/src/docs/asciidoc/Tag.adoc b/Api-Module/src/docs/asciidoc/Tag.adoc index 9f670411..b4a3549e 100644 --- a/Api-Module/src/docs/asciidoc/Tag.adoc +++ b/Api-Module/src/docs/asciidoc/Tag.adoc @@ -14,7 +14,18 @@ operation::TagControllerTest/createChildTagTest/[snippets='http-request,path-par [[GetParentTagTest]] === 상위 태그 조회 API -operation::TagControllerTest/getAllParentTagTest/[snippets='http-request,http-response,response-fields'] +operation::TagControllerTest/getAllParentTagByUserRegisterTest/[snippets='http-request,request-headers,http-response,response-fields'] + +[[GetTopRankTagTest]] +=== 연도 내 경험 최근 추가 순 태그 조회 API + +operation::TagControllerTest/getTopRankParentTagTest/[snippets='http-request,request-headers,http-response,response-fields'] + +[[GetParentTagsByFilter]] +=== 연도 내 상위 태그 조회 API + +operation::TagControllerTest/getParentTagsByFilter/[snippets='http-request,http-response,response-fields'] + [[GetChildTagTest]] === 하위 태그 조회 API diff --git a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/dto/GetTag.kt b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/dto/GetTag.kt index d55015d9..f709ac04 100644 --- a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/dto/GetTag.kt +++ b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/dto/GetTag.kt @@ -11,4 +11,16 @@ class GetTag { val id: UUID, val name: String ) + + data class TotalTagInfo( + val totalExperienceCount: Int, + val tagInfos : List + ) + + data class TagSummary( + val id: UUID, + val name: String, + val strongPointCount: Int, + val experienceCount: Int + ) } diff --git a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/service/TagGetService.kt b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/service/TagGetService.kt index 4cce6a06..17983516 100644 --- a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/service/TagGetService.kt +++ b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/application/service/TagGetService.kt @@ -2,14 +2,18 @@ package com.bamyanggang.apimodule.domain.tag.application.service import com.bamyanggang.apimodule.common.getAuthenticationPrincipal import com.bamyanggang.apimodule.domain.tag.application.dto.GetTag +import com.bamyanggang.domainmodule.domain.experience.service.ExperienceReader import com.bamyanggang.domainmodule.domain.tag.service.TagReader import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import java.util.* @Service class TagGetService( - private val tagReader: TagReader + private val tagReader: TagReader, + private val experienceReader: ExperienceReader ) { + @Transactional(readOnly = true) fun getAllParentTagByUserId(): GetTag.Response { val tagDetails = getAuthenticationPrincipal().let { tagReader.readAllParentTagsByUserId(it).map { tag -> @@ -20,6 +24,7 @@ class TagGetService( return GetTag.Response(tagDetails) } + @Transactional(readOnly = true) fun getAllChildTagsByParentTagId(parentTagId: UUID): GetTag.Response { val tagDetails = getAuthenticationPrincipal().let { tagReader.readAllChildTagsByUserId(it, parentTagId).map { tag -> @@ -29,4 +34,50 @@ class TagGetService( return GetTag.Response(tagDetails) } + + @Transactional(readOnly = true) + fun getParentTagsByYearAndLimit(year: Int, limit: Int): GetTag.Response { + val currentUserId = getAuthenticationPrincipal() + val topParentTagIds = experienceReader.readByYearDesc(year, currentUserId) + .distinctBy { it.parentTagId } + .take(limit) + .map { it.parentTagId } + + return tagReader.readByIds(topParentTagIds).map { + GetTag.TagDetail(it.id, it.name) + }.let { + GetTag.Response(it) + } + } + + @Transactional(readOnly = true) + fun getAllParentTagsByYear(year: Int): GetTag.TotalTagInfo { + val currentUserId = getAuthenticationPrincipal() + val experiences = experienceReader.readByYearDesc(year, currentUserId) + + val experienceGroup = experiences.groupBy { it.parentTagId } + + val tagSummaries = experienceGroup.map { + val parentTag = tagReader.readById(it.key) + val strongPoints = TreeSet() + + it.value.forEach { experience -> + experience.strongPoints.forEach { strongPoint -> + strongPoints.add(strongPoint.id) + } + } + + GetTag.TagSummary( + parentTag.id, + parentTag.name, + strongPoints.size, + it.value.size + ) + } + + return GetTag.TotalTagInfo( + experiences.size, + tagSummaries + ) + } } diff --git a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagApi.kt b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagApi.kt index 14256b51..ebe9ada4 100644 --- a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagApi.kt +++ b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagApi.kt @@ -2,5 +2,7 @@ package com.bamyanggang.apimodule.domain.tag.presentation object TagApi { const val BASE_URL = "/api/tags" + const val MY_TAG_URL = "$BASE_URL/my" + const val TOP_RANK_TAG_URL = "$BASE_URL/top-rank" const val TAG_PATH_VARIABLE_URL = "$BASE_URL/{tagId}" } diff --git a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagController.kt b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagController.kt index 08b3b77d..390b8dbb 100644 --- a/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagController.kt +++ b/Api-Module/src/main/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagController.kt @@ -15,7 +15,20 @@ class TagController( private val tagGetService: TagGetService ) { @GetMapping(TagApi.BASE_URL) - fun getAllParentTags(): GetTag.Response { + fun getParentTagsByYear(@RequestParam("year") year: Int): GetTag.TotalTagInfo { + return tagGetService.getAllParentTagsByYear(year) + } + + @GetMapping(TagApi.TOP_RANK_TAG_URL) + fun getTopRankTagsByLimit( + @RequestParam("year") year: Int, + @RequestParam("limit") limit: Int + ): GetTag.Response { + return tagGetService.getParentTagsByYearAndLimit(year, limit) + } + + @GetMapping(TagApi.MY_TAG_URL) + fun getUserParentTags(): GetTag.Response { return tagGetService.getAllParentTagByUserId() } diff --git a/Api-Module/src/test/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagControllerTest.kt b/Api-Module/src/test/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagControllerTest.kt index 65d46bb3..674ce59b 100644 --- a/Api-Module/src/test/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagControllerTest.kt +++ b/Api-Module/src/test/kotlin/com/bamyanggang/apimodule/domain/tag/presentation/TagControllerTest.kt @@ -17,8 +17,7 @@ import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName import org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders import org.springframework.restdocs.payload.PayloadDocumentation.* -import org.springframework.restdocs.request.RequestDocumentation.parameterWithName -import org.springframework.restdocs.request.RequestDocumentation.pathParameters +import org.springframework.restdocs.request.RequestDocumentation.* import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* @@ -249,8 +248,8 @@ class TagControllerTest : BaseRestDocsTest() { } @Test - @DisplayName("상위 태그를 전체조회한다.") - fun getAllParentTagTest() { + @DisplayName("유저가 등록한 상위 태그를 전체 조회한다.") + fun getAllParentTagByUserRegisterTest() { //given val tagDetails = arrayListOf( GetTag.TagDetail(generateFixture(), "상위 태그 이름 1"), @@ -259,9 +258,9 @@ class TagControllerTest : BaseRestDocsTest() { val tagResponse = GetTag.Response(tagDetails) - given(tagController.getAllParentTags()).willReturn(tagResponse) + given(tagController.getUserParentTags()).willReturn(tagResponse) - val request = RestDocumentationRequestBuilders.get(TagApi.BASE_URL) + val request = RestDocumentationRequestBuilders.get(TagApi.MY_TAG_URL) .header("Authorization", "Bearer Access Token") //when @@ -282,6 +281,103 @@ class TagControllerTest : BaseRestDocsTest() { ) } + @Test + @DisplayName("최근에 추가된 경험이 있는 순으로 상위 태그 정보를 반환한다.") + fun getTopRankParentTagTest() { + //given + val tagDetails = arrayListOf( + GetTag.TagDetail(generateFixture(), "상위 태그 이름 1"), + GetTag.TagDetail(generateFixture(), "상위 태그 이름 2") + ) + + val year = 2024 + val limit = 6 + val tagResponse = GetTag.Response(tagDetails) + + given(tagController.getTopRankTagsByLimit(year, limit)).willReturn(tagResponse) + + val request = RestDocumentationRequestBuilders.get(TagApi.TOP_RANK_TAG_URL) + .header("Authorization", "Bearer Access Token") + .queryParam("year", year.toString()) + .queryParam("limit", limit.toString()) + + //when + val result = mockMvc.perform(request) + + //then + result.andExpect(status().isOk) + .andDo(resultHandler.document( + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), + responseFields( + fieldWithPath("tags").description("상위 태그 리스트"), + fieldWithPath("tags[].id").description("태그 id"), + fieldWithPath("tags[].name").description("태그 이름"), + ) + ) + ) + } + + @Test + @DisplayName("필터에 의해 걸러진 태그 정보를 반환한다.") + fun getParentTagsByFilter() { + //given +// val tagDetails = arrayListOf( +// GetTag.TagDetail(generateFixture(), "상위 태그 이름 1"), +// GetTag.TagDetail(generateFixture(), "상위 태그 이름 2") +// ) + + val tagSummaries = arrayListOf( + GetTag.TagSummary( + UUID.randomUUID(), + "상위 태그 정보 1", + 3, + 14 + ), + GetTag.TagSummary( + UUID.randomUUID(), + "상위 태그 정보 2", + 1, + 7 + ) + ) + + val tagResponse = GetTag.TotalTagInfo( + 21, + tagSummaries) + + val year = 2024 + + given(tagController.getParentTagsByYear(year)).willReturn(tagResponse) + + val request = RestDocumentationRequestBuilders.get(TagApi.BASE_URL) + .header("Authorization", "Bearer Access Token") + .queryParam("year", year.toString()) + + //when + val result = mockMvc.perform(request) + + //then + result.andExpect(status().isOk) + .andDo(resultHandler.document( + requestHeaders( + headerWithName("Authorization").description("엑세스 토큰") + ), queryParameters( + parameterWithName("year").description("검색 연도") + ), + responseFields( + fieldWithPath("totalExperienceCount").description("연도 내 총 경험 개수"), + fieldWithPath("tagInfos").description("상위 태그 정보 배열"), + fieldWithPath("tagInfos[].id").description("상위 태그 id"), + fieldWithPath("tagInfos[].name").description("상위 태그 이름"), + fieldWithPath("tagInfos[].strongPointCount").description("상위 태그 내 역량 키워드 개수"), + fieldWithPath("tagInfos[].experienceCount").description("상위 태그 내 경험 개수"), + ) + ) + ) + } + @Test @DisplayName("태그를 삭제한다.") fun deleteTagTest() { diff --git a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/repository/ExperienceRepository.kt b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/repository/ExperienceRepository.kt index c2e7e164..7c3a5f4b 100644 --- a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/repository/ExperienceRepository.kt +++ b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/repository/ExperienceRepository.kt @@ -8,4 +8,5 @@ interface ExperienceRepository { fun deleteByExperienceId(experienceId: UUID) fun findByExperienceId(id: UUID): Experience fun findAllByUserId(userId: UUID): List + fun findByUserIdAndYearDesc(year: Int, userId: UUID): List } diff --git a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/service/ExperienceReader.kt b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/service/ExperienceReader.kt index ee591048..f0b2754e 100644 --- a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/service/ExperienceReader.kt +++ b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/experience/service/ExperienceReader.kt @@ -15,14 +15,18 @@ class ExperienceReader( fun readAllYearsByExistExperience(userId: UUID): List { val yearSet : TreeSet = TreeSet() - readAllExperienceByUserId(userId).forEach { + readAllByUserId(userId).forEach { yearSet.add(it.startedAt.year) } return yearSet.toList() } - fun readAllExperienceByUserId(userId: UUID): List { + fun readAllByUserId(userId: UUID): List { return experienceRepository.findAllByUserId(userId) } + + fun readByYearDesc(year: Int, userId: UUID): List { + return experienceRepository.findByUserIdAndYearDesc(year, userId) + } } diff --git a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/repository/TagRepository.kt b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/repository/TagRepository.kt index 874c65a6..98db685d 100644 --- a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/repository/TagRepository.kt +++ b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/repository/TagRepository.kt @@ -5,8 +5,10 @@ import java.util.* interface TagRepository { fun save(newTag : Tag) + fun findById(id : UUID) : Tag fun findAllParentTagsByUserId(userId: UUID): List fun findAllChildTagsByUserId(userId: UUID, parentId: UUID): List fun deleteByTagId(tagId: UUID) fun isExistById(tagId: UUID): Boolean + fun findByParentTagIds(tagParentTagIds: List): List } diff --git a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/service/TagReader.kt b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/service/TagReader.kt index ca5fbe97..238de612 100644 --- a/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/service/TagReader.kt +++ b/Domain-Module/src/main/kotlin/com/bamyanggang/domainmodule/domain/tag/service/TagReader.kt @@ -16,4 +16,12 @@ class TagReader( fun readAllChildTagsByUserId(userId: UUID, parentId: UUID): List { return tagRepository.findAllChildTagsByUserId(userId, parentId) } + + fun readById(tagId: UUID): Tag { + return tagRepository.findById(tagId) + } + + fun readByIds(parentTagIds: List): List { + return tagRepository.findByParentTagIds(parentTagIds) + } } diff --git a/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/ExperienceRepositoryImpl.java b/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/ExperienceRepositoryImpl.java index 0762c363..48f5aa44 100644 --- a/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/ExperienceRepositoryImpl.java +++ b/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/ExperienceRepositoryImpl.java @@ -6,6 +6,8 @@ import com.bamyanggang.persistence.experience.jpa.entity.ExperienceJpaEntity; import com.bamyanggang.persistence.experience.jpa.repository.ExperienceJpaRepository; import com.bamyanggang.persistence.experience.mapper.ExperienceMapper; +import com.bamyanggang.persistence.user.UserRepositoryImpl; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -16,6 +18,7 @@ public class ExperienceRepositoryImpl implements ExperienceRepository { private final ExperienceJpaRepository experienceJpaRepository; private final ExperienceMapper experienceMapper; + private final UserRepositoryImpl userRepositoryImpl; @Override public void save(Experience experience) { @@ -41,4 +44,14 @@ public List findAllByUserId(UUID userId) { List userExperienceJpaEntities = experienceJpaRepository.findAllByUserId(userId); return userExperienceJpaEntities.stream().map(experienceMapper::toExperienceDomainEntity).toList(); } + + @Override + public List findByUserIdAndYearDesc(int year, UUID userId) { + LocalDateTime startYear = LocalDateTime.of(year, 1, 1, 0, 0); + LocalDateTime endYear = LocalDateTime.of(year, 12, 31, 23, 59); + List experienceJpaEntities = experienceJpaRepository + .findByUserIdAndCreatedAtBetweenOrderByCreatedAtDesc(userId, startYear, endYear); + + return experienceJpaEntities.stream().map(experienceMapper::toExperienceDomainEntity).toList(); + } } diff --git a/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/jpa/repository/ExperienceJpaRepository.java b/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/jpa/repository/ExperienceJpaRepository.java index f69c547a..5e2f9694 100644 --- a/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/jpa/repository/ExperienceJpaRepository.java +++ b/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/experience/jpa/repository/ExperienceJpaRepository.java @@ -1,10 +1,12 @@ package com.bamyanggang.persistence.experience.jpa.repository; import com.bamyanggang.persistence.experience.jpa.entity.ExperienceJpaEntity; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; public interface ExperienceJpaRepository extends JpaRepository { List findAllByUserId(UUID userId); + List findByUserIdAndCreatedAtBetweenOrderByCreatedAtDesc(UUID userId, LocalDateTime startYear, LocalDateTime endYear); } diff --git a/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/tag/TagRepositoryImpl.java b/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/tag/TagRepositoryImpl.java index 9de7d7ca..22cd8ff1 100644 --- a/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/tag/TagRepositoryImpl.java +++ b/Infrastructure-Module/persistence/src/main/java/com/bamyanggang/persistence/tag/TagRepositoryImpl.java @@ -2,6 +2,7 @@ import com.bamyanggang.domainmodule.domain.tag.aggregate.Tag; import com.bamyanggang.domainmodule.domain.tag.repository.TagRepository; +import com.bamyanggang.persistence.common.exception.PersistenceException.NotFound; import com.bamyanggang.persistence.tag.jpa.entity.TagJpaEntity; import com.bamyanggang.persistence.tag.jpa.repository.TagJpaRepository; import com.bamyanggang.persistence.tag.mapper.TagMapper; @@ -43,4 +44,16 @@ public void deleteByTagId(UUID tagId) { public boolean isExistById(UUID tagId) { return tagJpaRepository.existsById(tagId); } + + @Override + public List findByParentTagIds(List parentTagIds) { + List tagJpaEntities = tagJpaRepository.findAllById(parentTagIds); + return tagJpaEntities.stream().map(tagMapper::toDomainEntity).toList(); + } + + @Override + public Tag findById(UUID tagId) { + TagJpaEntity tagJpaEntity = tagJpaRepository.findById(tagId).orElseThrow(NotFound::new); + return tagMapper.toDomainEntity(tagJpaEntity); + } }