From 2963569daa3ef62ed2cb33ea89ee7dd6390454bc Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 7 Feb 2026 15:53:57 +0900 Subject: [PATCH 1/4] refactor AuthenticatedMemberResolver to enhance JWT token validation and error handling --- .../com/moa/common/auth/AuthConstants.kt | 5 -- .../auth/AuthenticatedMemberResolver.kt | 29 +++++-- .../common/filter/JwtAuthenticationFilter.kt | 79 ------------------- 3 files changed, 21 insertions(+), 92 deletions(-) delete mode 100644 src/main/kotlin/com/moa/common/auth/AuthConstants.kt delete mode 100644 src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt diff --git a/src/main/kotlin/com/moa/common/auth/AuthConstants.kt b/src/main/kotlin/com/moa/common/auth/AuthConstants.kt deleted file mode 100644 index d7460a5..0000000 --- a/src/main/kotlin/com/moa/common/auth/AuthConstants.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.moa.common.auth - -object AuthConstants { - const val CURRENT_MEMBER_ID = "currentMemberId" -} diff --git a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt b/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt index 93a0f00..79e7d37 100644 --- a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt +++ b/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt @@ -1,17 +1,21 @@ package com.moa.common.auth -import com.moa.common.exception.BadRequestException import com.moa.common.exception.ErrorCode +import com.moa.common.exception.UnauthorizedException +import io.jsonwebtoken.ExpiredJwtException +import jakarta.servlet.http.HttpServletRequest import org.springframework.core.MethodParameter import org.springframework.stereotype.Component import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest -import org.springframework.web.context.request.RequestAttributes import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer @Component -class AuthenticatedMemberResolver : HandlerMethodArgumentResolver { +class AuthenticatedMemberResolver( + private val jwtTokenProvider: JwtTokenProvider, + private val request: HttpServletRequest, +) : HandlerMethodArgumentResolver { override fun supportsParameter(parameter: MethodParameter): Boolean { return parameter.hasParameterAnnotation(Auth::class.java) && @@ -24,11 +28,20 @@ class AuthenticatedMemberResolver : HandlerMethodArgumentResolver { webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory?, ): AuthenticatedMemberInfo { - val memberId = webRequest.getAttribute( - AuthConstants.CURRENT_MEMBER_ID, - RequestAttributes.SCOPE_REQUEST - ) as? Long ?: throw BadRequestException(ErrorCode.INVALID_ID_TOKEN) + val token = jwtTokenProvider.extractToken(request) + ?: throw UnauthorizedException(ErrorCode.UNAUTHORIZED) - return AuthenticatedMemberInfo(id = memberId) + try { + jwtTokenProvider.validateToken(token) + + val memberId = jwtTokenProvider.getUserIdFromToken(token) + ?: throw UnauthorizedException(ErrorCode.UNAUTHORIZED) + + return AuthenticatedMemberInfo(id = memberId) + } catch (ex: ExpiredJwtException) { + throw UnauthorizedException(ErrorCode.EXPIRED_TOKEN) + } catch (ex: Exception) { + throw UnauthorizedException(ErrorCode.UNAUTHORIZED) + } } } diff --git a/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt deleted file mode 100644 index 4cb875f..0000000 --- a/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.moa.common.filter - -import com.moa.common.auth.AuthConstants -import com.moa.common.auth.JwtTokenProvider -import com.moa.common.exception.ErrorCode -import io.jsonwebtoken.ExpiredJwtException -import jakarta.servlet.FilterChain -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.springframework.http.MediaType -import org.springframework.stereotype.Component -import org.springframework.web.filter.OncePerRequestFilter -import tools.jackson.databind.ObjectMapper - -@Component -class JwtAuthenticationFilter( - private val jwtTokenProvider: JwtTokenProvider, - private val objectMapper: ObjectMapper, -) : OncePerRequestFilter() { - - companion object { - private val EXCLUDED_PATHS = listOf( - "/api/v1/auth", - "/h2-console", - "/api-docs-ui", - "/swagger-ui", - "/swagger-resources", - "/v3/api-docs", - ) - } - - override fun shouldNotFilter(request: HttpServletRequest): Boolean { - val path = request.requestURI - return EXCLUDED_PATHS.any { path.startsWith(it) } - } - - override fun doFilterInternal( - request: HttpServletRequest, - response: HttpServletResponse, - filterChain: FilterChain, - ) { - val token = jwtTokenProvider.extractToken(request) - - if (token == null) { - writeErrorResponse(response, ErrorCode.UNAUTHORIZED) - return - } - - try { - jwtTokenProvider.validateToken(token) - - val memberId = jwtTokenProvider.getUserIdFromToken(token) - if (memberId == null) { - writeErrorResponse(response, ErrorCode.UNAUTHORIZED) - return - } - request.setAttribute(AuthConstants.CURRENT_MEMBER_ID, memberId) - filterChain.doFilter(request, response) - } catch (ex: ExpiredJwtException) { - writeErrorResponse(response, ErrorCode.EXPIRED_TOKEN) - } catch (ex: Exception) { - writeErrorResponse(response, ErrorCode.UNAUTHORIZED) - } - } - - private fun writeErrorResponse(response: HttpServletResponse, errorCode: ErrorCode) { - response.status = HttpServletResponse.SC_UNAUTHORIZED - response.contentType = MediaType.APPLICATION_JSON_VALUE - response.characterEncoding = "UTF-8" - - val errorResponse = mapOf( - "code" to errorCode.code, - "message" to errorCode.message, - "content" to null - ) - - objectMapper.writeValue(response.writer, errorResponse) - } -} From fbc1620231e442af4d82816ce0a5c70b9d4df72d Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 7 Feb 2026 15:56:49 +0900 Subject: [PATCH 2/4] refactor onboarding authentication to use OnboardingAuth annotation and update related components --- .../moa/common/auth/{Auth.kt => OnboardingAuth.kt} | 2 +- ...mberResolver.kt => OnboardingMemberResolver.kt} | 4 ++-- .../kotlin/com/moa/common/config/SwaggerConfig.kt | 4 ++-- src/main/kotlin/com/moa/common/config/WebConfig.kt | 6 +++--- .../com/moa/controller/OnboardingController.kt | 14 +++++++------- 5 files changed, 15 insertions(+), 15 deletions(-) rename src/main/kotlin/com/moa/common/auth/{Auth.kt => OnboardingAuth.kt} (84%) rename src/main/kotlin/com/moa/common/auth/{AuthenticatedMemberResolver.kt => OnboardingMemberResolver.kt} (94%) diff --git a/src/main/kotlin/com/moa/common/auth/Auth.kt b/src/main/kotlin/com/moa/common/auth/OnboardingAuth.kt similarity index 84% rename from src/main/kotlin/com/moa/common/auth/Auth.kt rename to src/main/kotlin/com/moa/common/auth/OnboardingAuth.kt index 3cc9d19..dc96905 100644 --- a/src/main/kotlin/com/moa/common/auth/Auth.kt +++ b/src/main/kotlin/com/moa/common/auth/OnboardingAuth.kt @@ -2,7 +2,7 @@ package com.moa.common.auth @Target(AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) -annotation class Auth +annotation class OnboardingAuth data class AuthenticatedMemberInfo( val id: Long, diff --git a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt b/src/main/kotlin/com/moa/common/auth/OnboardingMemberResolver.kt similarity index 94% rename from src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt rename to src/main/kotlin/com/moa/common/auth/OnboardingMemberResolver.kt index 79e7d37..d86d533 100644 --- a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt +++ b/src/main/kotlin/com/moa/common/auth/OnboardingMemberResolver.kt @@ -12,13 +12,13 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer @Component -class AuthenticatedMemberResolver( +class OnboardingMemberResolver( private val jwtTokenProvider: JwtTokenProvider, private val request: HttpServletRequest, ) : HandlerMethodArgumentResolver { override fun supportsParameter(parameter: MethodParameter): Boolean { - return parameter.hasParameterAnnotation(Auth::class.java) && + return parameter.hasParameterAnnotation(OnboardingAuth::class.java) && parameter.parameterType == AuthenticatedMemberInfo::class.java } diff --git a/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt b/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt index 23646ab..8183f4d 100644 --- a/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt +++ b/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt @@ -1,6 +1,6 @@ package com.moa.common.config -import com.moa.common.auth.Auth +import com.moa.common.auth.OnboardingAuth import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info @@ -15,7 +15,7 @@ import org.springframework.context.annotation.Configuration class SwaggerConfig { init { - SpringDocUtils.getConfig().addAnnotationsToIgnore(Auth::class.java) + SpringDocUtils.getConfig().addAnnotationsToIgnore(OnboardingAuth::class.java) } @Bean diff --git a/src/main/kotlin/com/moa/common/config/WebConfig.kt b/src/main/kotlin/com/moa/common/config/WebConfig.kt index 64b3c8c..e797f25 100644 --- a/src/main/kotlin/com/moa/common/config/WebConfig.kt +++ b/src/main/kotlin/com/moa/common/config/WebConfig.kt @@ -1,16 +1,16 @@ package com.moa.common.config -import com.moa.common.auth.AuthenticatedMemberResolver +import com.moa.common.auth.OnboardingMemberResolver import org.springframework.context.annotation.Configuration import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration class WebConfig( - private val authenticatedMemberResolver: AuthenticatedMemberResolver, + private val onboardingMemberResolver: OnboardingMemberResolver, ) : WebMvcConfigurer { override fun addArgumentResolvers(resolvers: MutableList) { - resolvers.add(authenticatedMemberResolver) + resolvers.add(onboardingMemberResolver) } } diff --git a/src/main/kotlin/com/moa/controller/OnboardingController.kt b/src/main/kotlin/com/moa/controller/OnboardingController.kt index d8a38d8..80a519a 100644 --- a/src/main/kotlin/com/moa/controller/OnboardingController.kt +++ b/src/main/kotlin/com/moa/controller/OnboardingController.kt @@ -1,7 +1,7 @@ package com.moa.controller -import com.moa.common.auth.Auth import com.moa.common.auth.AuthenticatedMemberInfo +import com.moa.common.auth.OnboardingAuth import com.moa.common.response.ApiResponse import com.moa.service.* import com.moa.service.dto.PayrollUpsertRequest @@ -22,24 +22,24 @@ class OnboardingController( ) { @GetMapping("/status") - fun status(@Auth member: AuthenticatedMemberInfo) = + fun status(@OnboardingAuth member: AuthenticatedMemberInfo) = ApiResponse.success(onboardingStatusService.getStatus(member.id)) @PatchMapping("/profile") fun upsertProfile( - @Auth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthenticatedMemberInfo, @RequestBody @Valid req: ProfileUpsertRequest, ) = ApiResponse.success(profileService.upsertProfile(member.id, req)) @PatchMapping("/payroll") fun upsertPayroll( - @Auth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthenticatedMemberInfo, @RequestBody @Valid req: PayrollUpsertRequest, ) = ApiResponse.success(payrollService.upsert(member.id, req)) @PatchMapping("/work-policy") fun upsertWorkPolicy( - @Auth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthenticatedMemberInfo, @RequestBody @Valid req: WorkPolicyUpsertRequest, ) = ApiResponse.success(workPolicyService.upsert(member.id, req)) @@ -48,12 +48,12 @@ class OnboardingController( ApiResponse.success(termsService.getTerms()) @GetMapping("/terms/agreements") - fun agreements(@Auth member: AuthenticatedMemberInfo) = + fun agreements(@OnboardingAuth member: AuthenticatedMemberInfo) = ApiResponse.success(termsService.getAgreements(member.id)) @PutMapping("/terms/agreements") fun agree( - @Auth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthenticatedMemberInfo, @RequestBody @Valid req: TermsAgreementRequest, ) = ApiResponse.success(termsService.upsertAgreements(member.id, req)) } From 150119fffd1972e2bb58ec53ed576aa0b163e8b1 Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 7 Feb 2026 16:06:30 +0900 Subject: [PATCH 3/4] implement onboarding authentication resolver and related classes --- src/main/kotlin/com/moa/common/auth/Auth.kt | 5 ++ .../com/moa/common/auth/AuthMemberResolver.kt | 88 +++++++++++++++++++ .../common/auth/AuthenticatedMemberInfo.kt | 5 ++ .../com/moa/common/auth/OnboardingAuth.kt | 4 - ...ver.kt => OnboardingAuthMemberResolver.kt} | 8 +- .../com/moa/common/config/SwaggerConfig.kt | 5 +- .../kotlin/com/moa/common/config/WebConfig.kt | 9 +- .../com/moa/common/exception/ErrorCode.kt | 2 +- .../common/exception/ForbiddenException.kt | 2 +- 9 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/com/moa/common/auth/Auth.kt create mode 100644 src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt create mode 100644 src/main/kotlin/com/moa/common/auth/AuthenticatedMemberInfo.kt rename src/main/kotlin/com/moa/common/auth/{OnboardingMemberResolver.kt => OnboardingAuthMemberResolver.kt} (87%) diff --git a/src/main/kotlin/com/moa/common/auth/Auth.kt b/src/main/kotlin/com/moa/common/auth/Auth.kt new file mode 100644 index 0000000..58095db --- /dev/null +++ b/src/main/kotlin/com/moa/common/auth/Auth.kt @@ -0,0 +1,5 @@ +package com.moa.common.auth + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class Auth diff --git a/src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt b/src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt new file mode 100644 index 0000000..d11a0bb --- /dev/null +++ b/src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt @@ -0,0 +1,88 @@ +package com.moa.common.auth + +import com.moa.common.exception.ErrorCode +import com.moa.common.exception.ForbiddenException +import com.moa.common.exception.UnauthorizedException +import com.moa.repository.* +import io.jsonwebtoken.ExpiredJwtException +import jakarta.servlet.http.HttpServletRequest +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import java.time.LocalDate + +@Component +class AuthMemberResolver( + private val jwtTokenProvider: JwtTokenProvider, + private val request: HttpServletRequest, + private val termRepository: TermRepository, + private val termAgreementRepository: TermAgreementRepository, + private val profileRepository: ProfileRepository, + private val payrollVersionRepository: PayrollVersionRepository, + private val workPolicyVersionRepository: WorkPolicyVersionRepository, +) : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(Auth::class.java) && + parameter.parameterType == AuthenticatedMemberInfo::class.java + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): AuthenticatedMemberInfo { + val token = jwtTokenProvider.extractToken(request) + ?: throw UnauthorizedException() + + val memberId = try { + jwtTokenProvider.validateToken(token) + jwtTokenProvider.getUserIdFromToken(token) + } catch (ex: ExpiredJwtException) { + throw UnauthorizedException(ErrorCode.EXPIRED_TOKEN) + } catch (ex: Exception) { + throw UnauthorizedException() + } ?: throw UnauthorizedException() + + val today = LocalDate.now() + + val profileCompleted = profileRepository.findByMemberId(memberId) + ?.let { it.nickname.isNotBlank() && it.workplace.isNotBlank() } + ?: false + + val payrollCompleted = + payrollVersionRepository + .findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(memberId, today) != null + + val workPolicyCompleted = + workPolicyVersionRepository + .findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(memberId, today) + ?.workdays + ?.isNotEmpty() + ?: false + + val requiredCodes = termRepository.findAll() + .asSequence() + .filter { it.required } + .map { it.code } + .toSet() + + val agreements = termAgreementRepository.findAllByMemberId(memberId) + .associate { it.termCode to it.agreed } + + val hasRequiredTermsAgreed = requiredCodes.all { agreements[it] == true } + + val onboardingCompleted = + profileCompleted && payrollCompleted && workPolicyCompleted && hasRequiredTermsAgreed + + if (!onboardingCompleted) { + throw ForbiddenException(ErrorCode.ONBOARDING_INCOMPLETE) + } + + return AuthenticatedMemberInfo(id = memberId) + } +} diff --git a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberInfo.kt b/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberInfo.kt new file mode 100644 index 0000000..c0a7038 --- /dev/null +++ b/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberInfo.kt @@ -0,0 +1,5 @@ +package com.moa.common.auth + +data class AuthenticatedMemberInfo( + val id: Long, +) diff --git a/src/main/kotlin/com/moa/common/auth/OnboardingAuth.kt b/src/main/kotlin/com/moa/common/auth/OnboardingAuth.kt index dc96905..c43d1e5 100644 --- a/src/main/kotlin/com/moa/common/auth/OnboardingAuth.kt +++ b/src/main/kotlin/com/moa/common/auth/OnboardingAuth.kt @@ -3,7 +3,3 @@ package com.moa.common.auth @Target(AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) annotation class OnboardingAuth - -data class AuthenticatedMemberInfo( - val id: Long, -) diff --git a/src/main/kotlin/com/moa/common/auth/OnboardingMemberResolver.kt b/src/main/kotlin/com/moa/common/auth/OnboardingAuthMemberResolver.kt similarity index 87% rename from src/main/kotlin/com/moa/common/auth/OnboardingMemberResolver.kt rename to src/main/kotlin/com/moa/common/auth/OnboardingAuthMemberResolver.kt index d86d533..653e01a 100644 --- a/src/main/kotlin/com/moa/common/auth/OnboardingMemberResolver.kt +++ b/src/main/kotlin/com/moa/common/auth/OnboardingAuthMemberResolver.kt @@ -12,7 +12,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer @Component -class OnboardingMemberResolver( +class OnboardingAuthMemberResolver( private val jwtTokenProvider: JwtTokenProvider, private val request: HttpServletRequest, ) : HandlerMethodArgumentResolver { @@ -29,19 +29,19 @@ class OnboardingMemberResolver( binderFactory: WebDataBinderFactory?, ): AuthenticatedMemberInfo { val token = jwtTokenProvider.extractToken(request) - ?: throw UnauthorizedException(ErrorCode.UNAUTHORIZED) + ?: throw UnauthorizedException() try { jwtTokenProvider.validateToken(token) val memberId = jwtTokenProvider.getUserIdFromToken(token) - ?: throw UnauthorizedException(ErrorCode.UNAUTHORIZED) + ?: throw UnauthorizedException() return AuthenticatedMemberInfo(id = memberId) } catch (ex: ExpiredJwtException) { throw UnauthorizedException(ErrorCode.EXPIRED_TOKEN) } catch (ex: Exception) { - throw UnauthorizedException(ErrorCode.UNAUTHORIZED) + throw UnauthorizedException() } } } diff --git a/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt b/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt index 8183f4d..8d81df2 100644 --- a/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt +++ b/src/main/kotlin/com/moa/common/config/SwaggerConfig.kt @@ -1,5 +1,6 @@ package com.moa.common.config +import com.moa.common.auth.Auth import com.moa.common.auth.OnboardingAuth import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI @@ -15,7 +16,9 @@ import org.springframework.context.annotation.Configuration class SwaggerConfig { init { - SpringDocUtils.getConfig().addAnnotationsToIgnore(OnboardingAuth::class.java) + SpringDocUtils.getConfig() + .addAnnotationsToIgnore(OnboardingAuth::class.java) + .addAnnotationsToIgnore(Auth::class.java) } @Bean diff --git a/src/main/kotlin/com/moa/common/config/WebConfig.kt b/src/main/kotlin/com/moa/common/config/WebConfig.kt index e797f25..e3b0c08 100644 --- a/src/main/kotlin/com/moa/common/config/WebConfig.kt +++ b/src/main/kotlin/com/moa/common/config/WebConfig.kt @@ -1,16 +1,19 @@ package com.moa.common.config -import com.moa.common.auth.OnboardingMemberResolver +import com.moa.common.auth.AuthMemberResolver +import com.moa.common.auth.OnboardingAuthMemberResolver import org.springframework.context.annotation.Configuration import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration class WebConfig( - private val onboardingMemberResolver: OnboardingMemberResolver, + private val onboardingAuthMemberResolver: OnboardingAuthMemberResolver, + private val authMemberResolver: AuthMemberResolver, ) : WebMvcConfigurer { override fun addArgumentResolvers(resolvers: MutableList) { - resolvers.add(onboardingMemberResolver) + resolvers.add(onboardingAuthMemberResolver) + resolvers.add(authMemberResolver) } } diff --git a/src/main/kotlin/com/moa/common/exception/ErrorCode.kt b/src/main/kotlin/com/moa/common/exception/ErrorCode.kt index 64cdfcd..4b37e72 100644 --- a/src/main/kotlin/com/moa/common/exception/ErrorCode.kt +++ b/src/main/kotlin/com/moa/common/exception/ErrorCode.kt @@ -10,7 +10,7 @@ enum class ErrorCode( // 4xx BAD_REQUEST("BAD_REQUEST", "잘못된 요청입니다."), UNAUTHORIZED("UNAUTHORIZED", "인증되지 않은 사용자입니다"), - FORBIDDEN("FORBIDDEN", "권한이 없습니다"), + ONBOARDING_INCOMPLETE("ONBOARDING_INCOMPLETE", "온보딩이 완료되지 않았습니다"), RESOURCE_NOT_FOUND("RESOURCE_NOT_FOUND", "리소스를 찾을 수 없습니다"), INVALID_PAYROLL_INPUT("INVALID_PAYROLL_INPUT", "급여 입력값이 유효하지 않습니다"), diff --git a/src/main/kotlin/com/moa/common/exception/ForbiddenException.kt b/src/main/kotlin/com/moa/common/exception/ForbiddenException.kt index d65846a..9c59d73 100644 --- a/src/main/kotlin/com/moa/common/exception/ForbiddenException.kt +++ b/src/main/kotlin/com/moa/common/exception/ForbiddenException.kt @@ -1,5 +1,5 @@ package com.moa.common.exception class ForbiddenException( - val errorCode: ErrorCode = ErrorCode.FORBIDDEN, + val errorCode: ErrorCode, ) : RuntimeException(errorCode.message) From 3f8a8af87b2c6d9158651927710165bf1177df4a Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 7 Feb 2026 16:08:18 +0900 Subject: [PATCH 4/4] refactor onboarding member information handling to use AuthMemberInfo class --- ...ticatedMemberInfo.kt => AuthMemberInfo.kt} | 2 +- .../com/moa/common/auth/AuthMemberResolver.kt | 63 ++++++++++++------- .../auth/OnboardingAuthMemberResolver.kt | 6 +- .../moa/controller/OnboardingController.kt | 14 ++--- 4 files changed, 51 insertions(+), 34 deletions(-) rename src/main/kotlin/com/moa/common/auth/{AuthenticatedMemberInfo.kt => AuthMemberInfo.kt} (57%) diff --git a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberInfo.kt b/src/main/kotlin/com/moa/common/auth/AuthMemberInfo.kt similarity index 57% rename from src/main/kotlin/com/moa/common/auth/AuthenticatedMemberInfo.kt rename to src/main/kotlin/com/moa/common/auth/AuthMemberInfo.kt index c0a7038..afcf1fb 100644 --- a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberInfo.kt +++ b/src/main/kotlin/com/moa/common/auth/AuthMemberInfo.kt @@ -1,5 +1,5 @@ package com.moa.common.auth -data class AuthenticatedMemberInfo( +data class AuthMemberInfo( val id: Long, ) diff --git a/src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt b/src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt index d11a0bb..9bb825f 100644 --- a/src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt +++ b/src/main/kotlin/com/moa/common/auth/AuthMemberResolver.kt @@ -27,7 +27,7 @@ class AuthMemberResolver( override fun supportsParameter(parameter: MethodParameter): Boolean { return parameter.hasParameterAnnotation(Auth::class.java) && - parameter.parameterType == AuthenticatedMemberInfo::class.java + parameter.parameterType == AuthMemberInfo::class.java } override fun resolveArgument( @@ -35,11 +35,17 @@ class AuthMemberResolver( mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory?, - ): AuthenticatedMemberInfo { + ): AuthMemberInfo { + val memberId = resolveMemberId() + validateOnboardingCompleted(memberId) + return AuthMemberInfo(id = memberId) + } + + private fun resolveMemberId(): Long { val token = jwtTokenProvider.extractToken(request) ?: throw UnauthorizedException() - val memberId = try { + return try { jwtTokenProvider.validateToken(token) jwtTokenProvider.getUserIdFromToken(token) } catch (ex: ExpiredJwtException) { @@ -47,24 +53,44 @@ class AuthMemberResolver( } catch (ex: Exception) { throw UnauthorizedException() } ?: throw UnauthorizedException() + } + private fun validateOnboardingCompleted(memberId: Long) { val today = LocalDate.now() - val profileCompleted = profileRepository.findByMemberId(memberId) + val profileCompleted = isProfileCompleted(memberId) + val payrollCompleted = isPayrollCompleted(memberId, today) + val workPolicyCompleted = isWorkPolicyCompleted(memberId, today) + val hasRequiredTermsAgreed = hasRequiredTermsAgreed(memberId) + + val onboardingCompleted = + profileCompleted && payrollCompleted && workPolicyCompleted && hasRequiredTermsAgreed + + if (!onboardingCompleted) { + throw ForbiddenException(ErrorCode.ONBOARDING_INCOMPLETE) + } + } + + private fun isProfileCompleted(memberId: Long): Boolean { + return profileRepository.findByMemberId(memberId) ?.let { it.nickname.isNotBlank() && it.workplace.isNotBlank() } ?: false + } - val payrollCompleted = - payrollVersionRepository - .findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(memberId, today) != null + private fun isPayrollCompleted(memberId: Long, today: LocalDate): Boolean { + return payrollVersionRepository + .findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(memberId, today) != null + } - val workPolicyCompleted = - workPolicyVersionRepository - .findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(memberId, today) - ?.workdays - ?.isNotEmpty() - ?: false + private fun isWorkPolicyCompleted(memberId: Long, today: LocalDate): Boolean { + return workPolicyVersionRepository + .findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(memberId, today) + ?.workdays + ?.isNotEmpty() + ?: false + } + private fun hasRequiredTermsAgreed(memberId: Long): Boolean { val requiredCodes = termRepository.findAll() .asSequence() .filter { it.required } @@ -74,15 +100,6 @@ class AuthMemberResolver( val agreements = termAgreementRepository.findAllByMemberId(memberId) .associate { it.termCode to it.agreed } - val hasRequiredTermsAgreed = requiredCodes.all { agreements[it] == true } - - val onboardingCompleted = - profileCompleted && payrollCompleted && workPolicyCompleted && hasRequiredTermsAgreed - - if (!onboardingCompleted) { - throw ForbiddenException(ErrorCode.ONBOARDING_INCOMPLETE) - } - - return AuthenticatedMemberInfo(id = memberId) + return requiredCodes.all { agreements[it] == true } } } diff --git a/src/main/kotlin/com/moa/common/auth/OnboardingAuthMemberResolver.kt b/src/main/kotlin/com/moa/common/auth/OnboardingAuthMemberResolver.kt index 653e01a..6a8db67 100644 --- a/src/main/kotlin/com/moa/common/auth/OnboardingAuthMemberResolver.kt +++ b/src/main/kotlin/com/moa/common/auth/OnboardingAuthMemberResolver.kt @@ -19,7 +19,7 @@ class OnboardingAuthMemberResolver( override fun supportsParameter(parameter: MethodParameter): Boolean { return parameter.hasParameterAnnotation(OnboardingAuth::class.java) && - parameter.parameterType == AuthenticatedMemberInfo::class.java + parameter.parameterType == AuthMemberInfo::class.java } override fun resolveArgument( @@ -27,7 +27,7 @@ class OnboardingAuthMemberResolver( mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory?, - ): AuthenticatedMemberInfo { + ): AuthMemberInfo { val token = jwtTokenProvider.extractToken(request) ?: throw UnauthorizedException() @@ -37,7 +37,7 @@ class OnboardingAuthMemberResolver( val memberId = jwtTokenProvider.getUserIdFromToken(token) ?: throw UnauthorizedException() - return AuthenticatedMemberInfo(id = memberId) + return AuthMemberInfo(id = memberId) } catch (ex: ExpiredJwtException) { throw UnauthorizedException(ErrorCode.EXPIRED_TOKEN) } catch (ex: Exception) { diff --git a/src/main/kotlin/com/moa/controller/OnboardingController.kt b/src/main/kotlin/com/moa/controller/OnboardingController.kt index 80a519a..b555aaf 100644 --- a/src/main/kotlin/com/moa/controller/OnboardingController.kt +++ b/src/main/kotlin/com/moa/controller/OnboardingController.kt @@ -1,6 +1,6 @@ package com.moa.controller -import com.moa.common.auth.AuthenticatedMemberInfo +import com.moa.common.auth.AuthMemberInfo import com.moa.common.auth.OnboardingAuth import com.moa.common.response.ApiResponse import com.moa.service.* @@ -22,24 +22,24 @@ class OnboardingController( ) { @GetMapping("/status") - fun status(@OnboardingAuth member: AuthenticatedMemberInfo) = + fun status(@OnboardingAuth member: AuthMemberInfo) = ApiResponse.success(onboardingStatusService.getStatus(member.id)) @PatchMapping("/profile") fun upsertProfile( - @OnboardingAuth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthMemberInfo, @RequestBody @Valid req: ProfileUpsertRequest, ) = ApiResponse.success(profileService.upsertProfile(member.id, req)) @PatchMapping("/payroll") fun upsertPayroll( - @OnboardingAuth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthMemberInfo, @RequestBody @Valid req: PayrollUpsertRequest, ) = ApiResponse.success(payrollService.upsert(member.id, req)) @PatchMapping("/work-policy") fun upsertWorkPolicy( - @OnboardingAuth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthMemberInfo, @RequestBody @Valid req: WorkPolicyUpsertRequest, ) = ApiResponse.success(workPolicyService.upsert(member.id, req)) @@ -48,12 +48,12 @@ class OnboardingController( ApiResponse.success(termsService.getTerms()) @GetMapping("/terms/agreements") - fun agreements(@OnboardingAuth member: AuthenticatedMemberInfo) = + fun agreements(@OnboardingAuth member: AuthMemberInfo) = ApiResponse.success(termsService.getAgreements(member.id)) @PutMapping("/terms/agreements") fun agree( - @OnboardingAuth member: AuthenticatedMemberInfo, + @OnboardingAuth member: AuthMemberInfo, @RequestBody @Valid req: TermsAgreementRequest, ) = ApiResponse.success(termsService.upsertAgreements(member.id, req)) }