diff --git a/src/main/kotlin/app/ieoseo/server/global/security/UserProvisioningFilter.kt b/src/main/kotlin/app/ieoseo/server/global/security/UserProvisioningFilter.kt index a2abe7c..743ff5c 100644 --- a/src/main/kotlin/app/ieoseo/server/global/security/UserProvisioningFilter.kt +++ b/src/main/kotlin/app/ieoseo/server/global/security/UserProvisioningFilter.kt @@ -24,6 +24,9 @@ class UserProvisioningFilter( if (user == null) { // 이메일은 nullable(미제공 provider 지원, ADR-0017) — placeholder 합성하지 않는다. repo.save(User(id = principal.userId, email = principal.email, nickname = defaultNickname(principal))) + // 이번 요청에 막 생성된 신규 사용자임을 표시 → AuthController.me 가 isNew 로 반환 + // (client 가 닉네임 설정 화면을 띄우는 신호). provisioning 은 보통 첫 /auth/me 요청. + request.setAttribute(NEW_USER_REQUEST_ATTR, true) } else if (!user.isActive) { // 탈퇴 사용자는 토큰 만료 전까지 유효한 Supabase JWT 로 접근할 수 있으므로 명시 차단한다. response.sendError(HttpServletResponse.SC_FORBIDDEN, "비활성화된 계정입니다") @@ -39,5 +42,9 @@ class UserProvisioningFilter( ?: principal.email?.substringBefore('@')?.take(NICKNAME_MAX_LENGTH)?.ifBlank { null } ?: "user" - private companion object { const val NICKNAME_MAX_LENGTH = 20 } + companion object { + /** 신규 provisioning 표시용 요청 attribute 키(AuthController.me 가 읽어 isNew 반환). */ + const val NEW_USER_REQUEST_ATTR = "app.ieoseo.newUser" + private const val NICKNAME_MAX_LENGTH = 20 + } } diff --git a/src/main/kotlin/app/ieoseo/server/user/controller/AuthController.kt b/src/main/kotlin/app/ieoseo/server/user/controller/AuthController.kt index 7743206..de0b60a 100644 --- a/src/main/kotlin/app/ieoseo/server/user/controller/AuthController.kt +++ b/src/main/kotlin/app/ieoseo/server/user/controller/AuthController.kt @@ -3,8 +3,10 @@ package app.ieoseo.server.user.controller import app.ieoseo.server.user.dto.* import app.ieoseo.server.global.security.AuthPrincipal +import app.ieoseo.server.global.security.UserProvisioningFilter import app.ieoseo.server.user.service.AuthService import app.ieoseo.server.settings.service.UserSettingsService +import jakarta.servlet.http.HttpServletRequest import app.ieoseo.server.global.common.ApiResponse import app.ieoseo.server.settings.dto.SettingsResponse import app.ieoseo.server.settings.dto.UpdateSettingsRequest @@ -34,8 +36,15 @@ class AuthController( private val userSettingsService: UserSettingsService, ) { @GetMapping("/me") - fun me(@AuthenticationPrincipal principal: AuthPrincipal): ApiResponse = - ApiResponse.ok(UserResponse.from(authService.me(principal.userId))) + fun me( + @AuthenticationPrincipal principal: AuthPrincipal, + request: HttpServletRequest, + ): ApiResponse { + // UserProvisioningFilter 가 이번 요청에 사용자를 막 생성했으면 isNew=true 로 알린다 + // (client 가 닉네임 설정 화면을 띄움). 기존 사용자면 attribute 없음 → false. + val isNew = request.getAttribute(UserProvisioningFilter.NEW_USER_REQUEST_ATTR) == true + return ApiResponse.ok(UserResponse.from(authService.me(principal.userId), isNew)) + } /** 프로필(닉네임) 수정(이슈 #56). 인증 필요. 검증 실패 → 400, 사용자 없음 → 404. */ @PatchMapping("/me") diff --git a/src/main/kotlin/app/ieoseo/server/user/dto/AuthDtos.kt b/src/main/kotlin/app/ieoseo/server/user/dto/AuthDtos.kt index 8a3a549..2d2fa4e 100644 --- a/src/main/kotlin/app/ieoseo/server/user/dto/AuthDtos.kt +++ b/src/main/kotlin/app/ieoseo/server/user/dto/AuthDtos.kt @@ -13,8 +13,11 @@ data class UserResponse( val id: UUID, val email: String?, val nickname: String, + /** 이번 요청에 막 provisioning 된 신규 사용자인지(client 닉네임 설정 화면 트리거용). */ + val isNew: Boolean = false, ) { companion object { - fun from(user: User): UserResponse = UserResponse(id = user.id, email = user.email, nickname = user.nickname) + fun from(user: User, isNew: Boolean = false): UserResponse = + UserResponse(id = user.id, email = user.email, nickname = user.nickname, isNew = isNew) } } diff --git a/src/test/kotlin/app/ieoseo/server/global/security/UserProvisioningFilterTest.kt b/src/test/kotlin/app/ieoseo/server/global/security/UserProvisioningFilterTest.kt index a2c3c89..2c2a52a 100644 --- a/src/test/kotlin/app/ieoseo/server/global/security/UserProvisioningFilterTest.kt +++ b/src/test/kotlin/app/ieoseo/server/global/security/UserProvisioningFilterTest.kt @@ -96,6 +96,33 @@ class UserProvisioningFilterTest { assertEquals("user", user.nickname) } + @Test + fun `신규 생성 시 요청에 신규 표시 attribute 를 설정한다`() { + authenticate(AuthPrincipal(userId, email = "jiwoo@ieoseo.app")) + `when`(repo.findById(userId)).thenReturn(Optional.empty()) + + filter.doFilter(request, response, chain) + + verify(request).setAttribute( + eq(UserProvisioningFilter.NEW_USER_REQUEST_ATTR), + eq(true), + ) + } + + @Test + fun `기존 사용자면 신규 표시 attribute 를 설정하지 않는다`() { + authenticate(AuthPrincipal(userId, email = "jiwoo@ieoseo.app")) + `when`(repo.findById(userId)) + .thenReturn(Optional.of(User(id = userId, email = "jiwoo@ieoseo.app", nickname = "jiwoo"))) + + filter.doFilter(request, response, chain) + + verify(request, never()).setAttribute( + eq(UserProvisioningFilter.NEW_USER_REQUEST_ATTR), + org.mockito.ArgumentMatchers.any(), + ) + } + @Test fun `활성 사용자가 이미 존재하면 저장하지 않고 체인을 진행한다`() { authenticate(AuthPrincipal(userId, email = "jiwoo@ieoseo.app")) diff --git a/src/test/kotlin/app/ieoseo/server/user/controller/AuthControllerWebMvcTest.kt b/src/test/kotlin/app/ieoseo/server/user/controller/AuthControllerWebMvcTest.kt index 6fc455c..6dca709 100644 --- a/src/test/kotlin/app/ieoseo/server/user/controller/AuthControllerWebMvcTest.kt +++ b/src/test/kotlin/app/ieoseo/server/user/controller/AuthControllerWebMvcTest.kt @@ -3,6 +3,7 @@ package app.ieoseo.server.user.controller import app.ieoseo.server.user.domain.User import app.ieoseo.server.global.security.AuthPrincipal import app.ieoseo.server.global.security.SecurityConfig +import app.ieoseo.server.global.security.UserProvisioningFilter import app.ieoseo.server.user.service.AuthService import app.ieoseo.server.settings.service.UpdateUserSettingsCommand import app.ieoseo.server.settings.service.UserSettingsService @@ -97,6 +98,20 @@ class AuthControllerWebMvcTest { .andExpect(jsonPath("$.data.id").value(userId.toString())) .andExpect(jsonPath("$.data.email").value("jiwoo@ieoseo.app")) .andExpect(jsonPath("$.data.nickname").value("지우")) + .andExpect(jsonPath("$.data.isNew").value(false)) + } + + @Test + fun `me 는 신규 provisioning 요청이면 isNew true 를 반환한다`() { + `when`(authService.me(userId)).thenReturn(user()) + + mockMvc.perform( + get("/api/v1/auth/me") + .with(authentication(asUser())) + .requestAttr(UserProvisioningFilter.NEW_USER_REQUEST_ATTR, true), + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.isNew").value(true)) } @Test