Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/main/kotlin/com/didit/adapter/webapi/auth/UserApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -96,8 +98,9 @@ class UserApi(
): SuccessResponse<UserProfileResponseV2> {
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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ data class UserProfileResponseV2(
val age: UserAge?,
val experience: UserExperience?,
val provider: Provider,
val currentLevel: Int,
val recentBadges: List<BadgeResponse> = emptyList(),
) {
companion object {
fun from(
user: User,
currentLevel: Int,
recentBadges: List<BadgeResponse> = emptyList(),
) = UserProfileResponseV2(
nickname = user.nickname,
Expand All @@ -27,6 +29,7 @@ data class UserProfileResponseV2(
age = user.age,
experience = user.experience,
provider = user.provider,
currentLevel = currentLevel,
recentBadges = recentBadges,
)
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/com/didit/adapter/webapi/home/HomeApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -53,6 +55,7 @@ class HomeApi(
HomeV2Response(
nickname = user.nickname ?: "",
todayRetrospectiveCount = todayCount,
hasUnreadNotification = notificationHistoryFinder.hasUnread(userId),
recentRetrospectives =
recentRetros.map {
RetrospectiveListItemV2Response.from(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RetrospectiveListItemV2Response>,
)
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.didit.application.achievement.provided

import java.util.UUID

interface UserLevelFinder {
fun getCurrentLevel(userId: UUID): Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ import java.util.UUID

interface NotificationHistoryFinder {
fun findAllByUserId(userId: UUID): List<NotificationHistory>

fun hasUnread(userId: UUID): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ interface NotificationHistoryRepository : Repository<NotificationHistory, UUID>
createdAt: LocalDateTime,
): List<NotificationHistory>

fun existsByUserIdAndIsReadFalseAndCreatedAtAfter(
userId: UUID,
createdAt: LocalDateTime,
): Boolean
Comment on lines +19 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add an index for the unread lookup

Checked src/main/resources/db/migration/V2__create_notification_tables.sql lines 1-10: notification_histories only has the primary key, so this new existsByUserIdAndIsReadFalseAndCreatedAtAfter predicate cannot use an index on user/read/time. Because it is now executed from GET /api/v2/home on every home load, users with no unread notifications, or mostly read/old rows, can force full table scans as histories accumulate; add a composite index such as (user_id, is_read, created_at) with the new query.

Useful? React with 👍 / 👎.


fun findByIdAndUserId(
id: UUID,
userId: UUID,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE INDEX idx_notification_histories_unread
ON notification_histories (user_id, is_read, created_at);
6 changes: 5 additions & 1 deletion src/test/kotlin/com/didit/adapter/webapi/auth/UserApiTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 `닉네임 중복 확인`() {
Expand Down Expand Up @@ -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"))
Expand All @@ -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)"),
Expand Down
6 changes: 5 additions & 1 deletion src/test/kotlin/com/didit/adapter/webapi/home/HomeApiTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 `홈 조회`() {
Expand Down Expand Up @@ -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"))
Expand All @@ -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(),
Expand Down