diff --git a/src/main/kotlin/com/didit/adapter/webapi/auth/UserApi.kt b/src/main/kotlin/com/didit/adapter/webapi/auth/UserApi.kt index cf93e0ba..3be4e236 100644 --- a/src/main/kotlin/com/didit/adapter/webapi/auth/UserApi.kt +++ b/src/main/kotlin/com/didit/adapter/webapi/auth/UserApi.kt @@ -11,6 +11,7 @@ import com.didit.adapter.webapi.auth.dto.UserProfileResponse import com.didit.adapter.webapi.auth.dto.UserProfileResponseV2 import com.didit.adapter.webapi.response.SuccessResponse import com.didit.application.achievement.provided.BadgeFinder +import com.didit.application.achievement.provided.UserLevelFinder import com.didit.application.audit.Audit import com.didit.application.audit.AuditAction import com.didit.application.auth.provided.UserFinder @@ -31,6 +32,7 @@ class UserApi( private val userFinder: UserFinder, private val userRegister: UserRegister, private val badgeFinder: BadgeFinder, + private val userLevelFinder: UserLevelFinder, ) { @GetMapping("/api/v1/users/nickname/check") fun checkNickname( @@ -96,8 +98,9 @@ class UserApi( ): SuccessResponse { val user = userFinder.findByIdOrThrow(userId) val recentBadges = badgeFinder.findRecent(userId) + val currentLevel = userLevelFinder.getCurrentLevel(userId) - return SuccessResponse.of(UserProfileResponseV2.from(user, recentBadges)) + return SuccessResponse.of(UserProfileResponseV2.from(user, currentLevel, recentBadges)) } @Deprecated("Use v2 endpoint", replaceWith = ReplaceWith("updateProfileV2")) diff --git a/src/main/kotlin/com/didit/adapter/webapi/auth/dto/UserProfileResponseV2.kt b/src/main/kotlin/com/didit/adapter/webapi/auth/dto/UserProfileResponseV2.kt index f4800054..1c76a405 100644 --- a/src/main/kotlin/com/didit/adapter/webapi/auth/dto/UserProfileResponseV2.kt +++ b/src/main/kotlin/com/didit/adapter/webapi/auth/dto/UserProfileResponseV2.kt @@ -14,11 +14,13 @@ data class UserProfileResponseV2( val age: UserAge?, val experience: UserExperience?, val provider: Provider, + val currentLevel: Int, val recentBadges: List = emptyList(), ) { companion object { fun from( user: User, + currentLevel: Int, recentBadges: List = emptyList(), ) = UserProfileResponseV2( nickname = user.nickname, @@ -27,6 +29,7 @@ data class UserProfileResponseV2( age = user.age, experience = user.experience, provider = user.provider, + currentLevel = currentLevel, recentBadges = recentBadges, ) } diff --git a/src/main/kotlin/com/didit/adapter/webapi/home/HomeApi.kt b/src/main/kotlin/com/didit/adapter/webapi/home/HomeApi.kt index bd2bbc78..dc67719c 100644 --- a/src/main/kotlin/com/didit/adapter/webapi/home/HomeApi.kt +++ b/src/main/kotlin/com/didit/adapter/webapi/home/HomeApi.kt @@ -7,6 +7,7 @@ import com.didit.adapter.webapi.home.dto.HomeV2Response import com.didit.adapter.webapi.response.SuccessResponse import com.didit.adapter.webapi.retrospect.dto.RetrospectiveListItemV2Response import com.didit.application.auth.provided.UserFinder +import com.didit.application.notification.provided.NotificationHistoryFinder import com.didit.application.retrospect.provided.RetrospectiveFinder import com.didit.domain.shared.ServiceTime import org.springframework.web.bind.annotation.GetMapping @@ -17,6 +18,7 @@ import java.util.UUID class HomeApi( private val userFinder: UserFinder, private val retrospectiveFinder: RetrospectiveFinder, + private val notificationHistoryFinder: NotificationHistoryFinder, ) { @Deprecated("Use v2 endpoint", replaceWith = ReplaceWith("getHomeV2")) @RequireAuth @@ -53,6 +55,7 @@ class HomeApi( HomeV2Response( nickname = user.nickname ?: "", todayRetrospectiveCount = todayCount, + hasUnreadNotification = notificationHistoryFinder.hasUnread(userId), recentRetrospectives = recentRetros.map { RetrospectiveListItemV2Response.from(it) diff --git a/src/main/kotlin/com/didit/adapter/webapi/home/dto/HomeV2Response.kt b/src/main/kotlin/com/didit/adapter/webapi/home/dto/HomeV2Response.kt index 058382b6..7adab4e6 100644 --- a/src/main/kotlin/com/didit/adapter/webapi/home/dto/HomeV2Response.kt +++ b/src/main/kotlin/com/didit/adapter/webapi/home/dto/HomeV2Response.kt @@ -5,5 +5,6 @@ import com.didit.adapter.webapi.retrospect.dto.RetrospectiveListItemV2Response data class HomeV2Response( val nickname: String, val todayRetrospectiveCount: Int, + val hasUnreadNotification: Boolean, val recentRetrospectives: List, ) diff --git a/src/main/kotlin/com/didit/application/achievement/UserLevelQueryService.kt b/src/main/kotlin/com/didit/application/achievement/UserLevelQueryService.kt new file mode 100644 index 00000000..26c950ff --- /dev/null +++ b/src/main/kotlin/com/didit/application/achievement/UserLevelQueryService.kt @@ -0,0 +1,19 @@ +package com.didit.application.achievement + +import com.didit.application.achievement.provided.UserLevelFinder +import com.didit.application.achievement.required.UserLevelRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Transactional(readOnly = true) +@Service +class UserLevelQueryService( + private val userLevelRepository: UserLevelRepository, +) : UserLevelFinder { + override fun getCurrentLevel(userId: UUID): Int = userLevelRepository.findByUserId(userId)?.currentLevel ?: DEFAULT_LEVEL + + companion object { + private const val DEFAULT_LEVEL = 1 + } +} diff --git a/src/main/kotlin/com/didit/application/achievement/provided/UserLevelFinder.kt b/src/main/kotlin/com/didit/application/achievement/provided/UserLevelFinder.kt new file mode 100644 index 00000000..a7382267 --- /dev/null +++ b/src/main/kotlin/com/didit/application/achievement/provided/UserLevelFinder.kt @@ -0,0 +1,7 @@ +package com.didit.application.achievement.provided + +import java.util.UUID + +interface UserLevelFinder { + fun getCurrentLevel(userId: UUID): Int +} diff --git a/src/main/kotlin/com/didit/application/notification/NotificationHistoryQueryService.kt b/src/main/kotlin/com/didit/application/notification/NotificationHistoryQueryService.kt index dc945366..af785b79 100644 --- a/src/main/kotlin/com/didit/application/notification/NotificationHistoryQueryService.kt +++ b/src/main/kotlin/com/didit/application/notification/NotificationHistoryQueryService.kt @@ -18,4 +18,10 @@ class NotificationHistoryQueryService( userId = userId, createdAt = LocalDateTime.now().minusDays(NotificationHistory.RETENTION_DAYS), ) + + override fun hasUnread(userId: UUID): Boolean = + notificationHistoryRepository.existsByUserIdAndIsReadFalseAndCreatedAtAfter( + userId = userId, + createdAt = LocalDateTime.now().minusDays(NotificationHistory.RETENTION_DAYS), + ) } diff --git a/src/main/kotlin/com/didit/application/notification/provided/NotificationHistoryFinder.kt b/src/main/kotlin/com/didit/application/notification/provided/NotificationHistoryFinder.kt index 10d1adf1..22661eaa 100644 --- a/src/main/kotlin/com/didit/application/notification/provided/NotificationHistoryFinder.kt +++ b/src/main/kotlin/com/didit/application/notification/provided/NotificationHistoryFinder.kt @@ -5,4 +5,6 @@ import java.util.UUID interface NotificationHistoryFinder { fun findAllByUserId(userId: UUID): List + + fun hasUnread(userId: UUID): Boolean } diff --git a/src/main/kotlin/com/didit/application/notification/required/NotificationHistoryRepository.kt b/src/main/kotlin/com/didit/application/notification/required/NotificationHistoryRepository.kt index 84ddbe01..caf857ec 100644 --- a/src/main/kotlin/com/didit/application/notification/required/NotificationHistoryRepository.kt +++ b/src/main/kotlin/com/didit/application/notification/required/NotificationHistoryRepository.kt @@ -16,6 +16,11 @@ interface NotificationHistoryRepository : Repository createdAt: LocalDateTime, ): List + fun existsByUserIdAndIsReadFalseAndCreatedAtAfter( + userId: UUID, + createdAt: LocalDateTime, + ): Boolean + fun findByIdAndUserId( id: UUID, userId: UUID, diff --git a/src/main/resources/db/migration/V41__add_notification_unread_index.sql b/src/main/resources/db/migration/V41__add_notification_unread_index.sql new file mode 100644 index 00000000..6e7acad5 --- /dev/null +++ b/src/main/resources/db/migration/V41__add_notification_unread_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX idx_notification_histories_unread + ON notification_histories (user_id, is_read, created_at); diff --git a/src/test/kotlin/com/didit/adapter/webapi/auth/UserApiTest.kt b/src/test/kotlin/com/didit/adapter/webapi/auth/UserApiTest.kt index 65a9fb29..c6ad0032 100644 --- a/src/test/kotlin/com/didit/adapter/webapi/auth/UserApiTest.kt +++ b/src/test/kotlin/com/didit/adapter/webapi/auth/UserApiTest.kt @@ -1,6 +1,7 @@ package com.didit.adapter.webapi.auth import com.didit.application.achievement.provided.BadgeFinder +import com.didit.application.achievement.provided.UserLevelFinder import com.didit.application.auth.provided.UserFinder import com.didit.application.auth.provided.UserRegister import com.didit.docs.ApiDocumentUtils @@ -29,8 +30,9 @@ class UserApiTest : AuthenticatedRestDocsSupport() { private val userFinder: UserFinder = mock(UserFinder::class.java) private val userRegister: UserRegister = mock(UserRegister::class.java) private val badgeFinder: BadgeFinder = mock(BadgeFinder::class.java) + private val userLevelFinder: UserLevelFinder = mock(UserLevelFinder::class.java) - override fun initController() = UserApi(userFinder, userRegister, badgeFinder) + override fun initController() = UserApi(userFinder, userRegister, badgeFinder, userLevelFinder) @Test fun `닉네임 중복 확인`() { @@ -190,6 +192,7 @@ class UserApiTest : AuthenticatedRestDocsSupport() { val user = UserFixture.createOnboarded() whenever(userFinder.findByIdOrThrow(userId)).thenReturn(user) whenever(badgeFinder.findRecent(userId)).thenReturn(emptyList()) + whenever(userLevelFinder.getCurrentLevel(userId)).thenReturn(5) mockMvc .perform(get("/api/v2/users/profile")) @@ -201,6 +204,7 @@ class UserApiTest : AuthenticatedRestDocsSupport() { ApiDocumentUtils.getDocumentResponse(), responseFields( fieldWithPath("data.nickname").type(JsonFieldType.STRING).description("닉네임"), + fieldWithPath("data.currentLevel").type(JsonFieldType.NUMBER).description("현재 레벨 (1~10)"), fieldWithPath("data.job") .type(JsonFieldType.STRING) .description("직무 (DEVELOPER, PLANNER, DESIGNER)"), diff --git a/src/test/kotlin/com/didit/adapter/webapi/home/HomeApiTest.kt b/src/test/kotlin/com/didit/adapter/webapi/home/HomeApiTest.kt index 75ad786a..930635f2 100644 --- a/src/test/kotlin/com/didit/adapter/webapi/home/HomeApiTest.kt +++ b/src/test/kotlin/com/didit/adapter/webapi/home/HomeApiTest.kt @@ -1,6 +1,7 @@ package com.didit.adapter.webapi.home import com.didit.application.auth.provided.UserFinder +import com.didit.application.notification.provided.NotificationHistoryFinder import com.didit.application.retrospect.dto.RetrospectiveDetailResult import com.didit.application.retrospect.provided.RetrospectiveFinder import com.didit.docs.ApiDocumentUtils @@ -24,8 +25,9 @@ import java.util.UUID class HomeApiTest : AuthenticatedRestDocsSupport() { private val userFinder: UserFinder = mock(UserFinder::class.java) private val retrospectiveFinder: RetrospectiveFinder = mock(RetrospectiveFinder::class.java) + private val notificationHistoryFinder: NotificationHistoryFinder = mock(NotificationHistoryFinder::class.java) - override fun initController() = HomeApi(userFinder, retrospectiveFinder) + override fun initController() = HomeApi(userFinder, retrospectiveFinder, notificationHistoryFinder) @Test fun `홈 조회`() { @@ -71,6 +73,7 @@ class HomeApiTest : AuthenticatedRestDocsSupport() { whenever(userFinder.findByIdOrThrow(userId)).thenReturn(user) whenever(retrospectiveFinder.findRecentWithProjectAndTagsByUserId(userId, 5)).thenReturn(listOf(result)) whenever(retrospectiveFinder.countByUserIdAndDate(userId, ServiceTime.today())).thenReturn(1) + whenever(notificationHistoryFinder.hasUnread(userId)).thenReturn(true) mockMvc .perform(get("/api/v2/home")) @@ -83,6 +86,7 @@ class HomeApiTest : AuthenticatedRestDocsSupport() { responseFields( fieldWithPath("data.nickname").type(JsonFieldType.STRING).description("닉네임"), fieldWithPath("data.todayRetrospectiveCount").type(JsonFieldType.NUMBER).description("오늘 회고 횟수"), + fieldWithPath("data.hasUnreadNotification").type(JsonFieldType.BOOLEAN).description("안 읽은 알림 존재 여부"), fieldWithPath("data.recentRetrospectives").type(JsonFieldType.ARRAY).description("최근 회고 목록"), fieldWithPath("data.recentRetrospectives[].id").type(JsonFieldType.STRING).description("회고 ID"), fieldWithPath("data.recentRetrospectives[].title").type(JsonFieldType.STRING).description("회고 제목").optional(),