diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/OnboardingController.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/OnboardingController.kt index 511ffb7..e6accc4 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/OnboardingController.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/OnboardingController.kt @@ -5,6 +5,7 @@ import noweekend.core.api.controller.v1.request.LeaveInputRequest import noweekend.core.api.controller.v1.request.ProfileRequest import noweekend.core.api.controller.v1.request.TagRequest import noweekend.core.api.controller.v1.response.DefaultTags +import noweekend.core.api.controller.v1.response.OnboardingStatusResponse import noweekend.core.api.security.annotations.CurrentUserId import noweekend.core.domain.user.UserService import noweekend.core.support.response.ApiResponse @@ -62,4 +63,13 @@ class OnboardingController( "연차 정보가 성공적으로 저장되었습니다.", ) } + + @GetMapping("/onboarding/status") + override fun getOnboardingStatus( + @CurrentUserId userId: String, + ): ApiResponse { + return ApiResponse.success( + userService.getOnboardingStatus(userId), + ) + } } diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/OnboardingControllerDocs.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/OnboardingControllerDocs.kt index 5970938..f4a6b46 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/OnboardingControllerDocs.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/docs/OnboardingControllerDocs.kt @@ -11,6 +11,7 @@ import noweekend.core.api.controller.v1.request.LeaveInputRequest import noweekend.core.api.controller.v1.request.ProfileRequest import noweekend.core.api.controller.v1.request.TagRequest import noweekend.core.api.controller.v1.response.DefaultTags +import noweekend.core.api.controller.v1.response.OnboardingStatusResponse import noweekend.core.api.security.annotations.CurrentUserId import noweekend.core.support.response.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse @@ -340,4 +341,72 @@ interface OnboardingControllerDocs { fun getDefaultTag( @Parameter(hidden = true) @CurrentUserId userId: String, ): ApiResponse + + @Operation( + summary = "온보딩: 사용자 진행 상태 조회", + description = """ + 온보딩 플로우(이름/생년월일, 연차, 일정 태그) 입력 진행 상황을 반환합니다. + + status 값 설명: + - NONE: 이름/생년월일 미입력 상태 + - NAME_AND_BIRTHDAY: 이름/생년월일 입력만 완료 + - ANNUAL_LEAVE: 연차 입력까지 완료 + - DONE: 모든 온보딩 절차 완료 + """, + responses = [ + SwaggerApiResponse( + responseCode = "200", + description = "온보딩 상태 조회 성공", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ApiResponse::class), + examples = [ + ExampleObject( + name = "예시 응답", + value = """ +{ + "result": "SUCCESS", + "data": { + "status": "DONE" + }, + "error": null +} +""", + ), + ], + ), + ], + ), + SwaggerApiResponse( + responseCode = "400", + description = "온보딩 순서에 맞지 않게 데이터가 입력된 경우", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ApiResponse::class), + examples = [ + ExampleObject( + name = "온보딩 순서 오류 예시", + value = """ +{ + "result": "ERROR", + "data": null, + "error": { + "code": "INVALID_ONBOARD_STATUS", + "message": "온보딩 순서에 맞게 요청하지 않은 상태입니다. 사용자 정보조회하여 어떤 값이 입력되지 않았는지 확인해주세요.", + "data": {} + } +} +""", + ), + ], + ), + ], + ), + ], + ) + fun getOnboardingStatus( + @Parameter(hidden = true) @CurrentUserId userId: String, + ): ApiResponse } diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/OnboardingStatusResponse.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/OnboardingStatusResponse.kt new file mode 100644 index 0000000..a9f8e9a --- /dev/null +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/OnboardingStatusResponse.kt @@ -0,0 +1,27 @@ +package noweekend.core.api.controller.v1.response + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "온보딩 진행 상태 응답") +data class OnboardingStatusResponse( + @Schema( + description = "온보딩 단계 상태 (NONE: 이름/생년월일 미입력, NAME_AND_BIRTHDAY: 이름/생년월일만 입력, ANNUAL_LEAVE: 연차까지 입력, DONE: 모든 온보딩 완료)", + example = "ANNUAL_LEAVE", + ) + val status: OnboardingStatus, +) + +@Schema(description = "온보딩 상태 Enum") +enum class OnboardingStatus { + @Schema(description = "이름/생년월일 입력 전 상태") + NONE, + + @Schema(description = "이름/생년월일 입력 완료 상태") + NAME_AND_BIRTHDAY, + + @Schema(description = "연차 입력까지 완료된 상태") + ANNUAL_LEAVE, + + @Schema(description = "모든 온보딩 완료 상태") + DONE, +} diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/UserInformationResponse.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/UserInformationResponse.kt index 87da3f1..8490101 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/UserInformationResponse.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v1/response/UserInformationResponse.kt @@ -20,7 +20,7 @@ data class UserInformationResponse( val revocableToken: String?, val role: Role, val birthDate: LocalDate, - val remainingAnnualLeave: Double, + val remainingAnnualLeave: Double?, val createdAt: LocalDateTime, val updatedAt: LocalDateTime?, var location: Location?, diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserService.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserService.kt index 4dfd688..b462eaa 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserService.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserService.kt @@ -4,6 +4,7 @@ import noweekend.core.api.controller.v1.request.LeaveInputRequest import noweekend.core.api.controller.v1.request.LocationRequest import noweekend.core.api.controller.v1.request.ProfileRequest import noweekend.core.api.controller.v1.request.TagUpdateRequest +import noweekend.core.api.controller.v1.response.OnboardingStatusResponse import noweekend.core.api.controller.v1.response.UserInformationResponse import noweekend.core.domain.tag.BasicTag import noweekend.core.domain.tag.UserTags @@ -17,4 +18,5 @@ interface UserService { fun getStateTags(userId: String): UserTags fun updateLocation(request: LocationRequest, userId: String) fun getUserInformationById(userId: String): UserInformationResponse + fun getOnboardingStatus(userId: String): OnboardingStatusResponse } diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserServiceImpl.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserServiceImpl.kt index a5abbb4..85a5a04 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserServiceImpl.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/domain/user/UserServiceImpl.kt @@ -4,6 +4,8 @@ import noweekend.core.api.controller.v1.request.LeaveInputRequest import noweekend.core.api.controller.v1.request.LocationRequest import noweekend.core.api.controller.v1.request.ProfileRequest import noweekend.core.api.controller.v1.request.TagUpdateRequest +import noweekend.core.api.controller.v1.response.OnboardingStatus +import noweekend.core.api.controller.v1.response.OnboardingStatusResponse import noweekend.core.api.controller.v1.response.UserInformationResponse import noweekend.core.domain.enumerate.ScheduleCategory import noweekend.core.domain.schedule.ScheduleReader @@ -66,7 +68,7 @@ class UserServiceImpl( daysToAdd += 0.5 } val updatedUser = user.copy( - remainingAnnualLeave = user.remainingAnnualLeave + daysToAdd, + remainingAnnualLeave = (user.remainingAnnualLeave ?: 0.0) + daysToAdd, ) userWriter.upsert(updatedUser) } @@ -119,4 +121,36 @@ class UserServiceImpl( return UserInformationResponse.of(user, averageTemperature) } + + override fun getOnboardingStatus(userId: String): OnboardingStatusResponse { + val user = userReader.findUserById(userId) ?: throw CoreException(ErrorType.USER_NOT_FOUND_INTERNAL) + + val nameAndBirthdayEntered = user.birthDate != null && user.name != null + val annualLeaveEntered = user.remainingAnnualLeave != null + val userTags = tagReader.getUserTags(userId) + val selectedTagsCount = (userTags.selectedBasicTags + userTags.unselectedBasicTags + userTags.selectedCustomTags + userTags.unselectedCustomTags).count { it.selected } + val tagEntered = selectedTagsCount > 0 + + // 1. 이름/생년월일 안됨 -> NONE + if (!nameAndBirthdayEntered && !annualLeaveEntered && !tagEntered) { + return OnboardingStatusResponse(OnboardingStatus.NONE) + } + + // 2. 이름/생년월일만 됨 -> NAME_AND_BIRTHDAY + if (nameAndBirthdayEntered && !annualLeaveEntered && !tagEntered) { + return OnboardingStatusResponse(OnboardingStatus.NAME_AND_BIRTHDAY) + } + + // 3. 연차까지 됨(이름/생년월일, 연차 ok, 태그 없음) -> ANNUAL_LEAVE + if (nameAndBirthdayEntered && annualLeaveEntered && !tagEntered) { + return OnboardingStatusResponse(OnboardingStatus.ANNUAL_LEAVE) + } + + // 4. 모든게 정상적으로 입력됨 -> DONE + if (nameAndBirthdayEntered && annualLeaveEntered && tagEntered) { + return OnboardingStatusResponse(OnboardingStatus.DONE) + } + + throw CoreException(ErrorType.INVALID_ONBOARD_STATUS) + } } diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/support/error/ErrorType.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/support/error/ErrorType.kt index a047d2e..b5cee9d 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/support/error/ErrorType.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/support/error/ErrorType.kt @@ -25,4 +25,5 @@ enum class ErrorType( MCP_SERVER_SANDWICH_ERROR(HttpStatus.GATEWAY_TIMEOUT, ErrorCode.E504, "MCP 추천 서버의 응답이 없습니다. 잠시 후 다시 시도해주세요.", LogLevel.ERROR), INVALID_LOCATION(HttpStatus.BAD_REQUEST, ErrorCode.E400, "사용자가 한국 위치가 아니기 때문에 날씨를 추천할 수 없습니다.", LogLevel.WARN), INVALID_SCHEDULE_TAG(HttpStatus.BAD_REQUEST, ErrorCode.E400, "유효하지 않은 태그가 포함되어 있습니다.", LogLevel.WARN), + INVALID_ONBOARD_STATUS(HttpStatus.BAD_REQUEST, ErrorCode.E400, "온보딩 순서에 맞게 요청하지 않은 상태입니다. 사용자 정보조회하여 어떤 값이 입력되지 않았는지 확인해주세요.", LogLevel.WARN), } diff --git a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/auth/TestUserService.kt b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/auth/TestUserService.kt index d7934b0..d713eb3 100644 --- a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/auth/TestUserService.kt +++ b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/auth/TestUserService.kt @@ -84,7 +84,7 @@ data class UserWithToken( revocableToken = user.revocableToken, role = user.role, birthDate = user.birthDate, - remainingAnnualLeave = user.remainingAnnualLeave, + remainingAnnualLeave = 10.0, createdAt = user.createdAt, updatedAt = user.updatedAt, ) diff --git a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/user/User.kt b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/user/User.kt index 67ef9b5..18a7ce0 100644 --- a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/user/User.kt +++ b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/user/User.kt @@ -17,7 +17,7 @@ data class User( val revocableToken: String?, val role: Role, val birthDate: LocalDate?, - val remainingAnnualLeave: Double = 0.0, + val remainingAnnualLeave: Double?, val createdAt: LocalDateTime?, val updatedAt: LocalDateTime?, var deleted: Boolean, @@ -41,6 +41,7 @@ data class User( revocableToken = revocableToken, role = role, birthDate = null, + remainingAnnualLeave = null, createdAt = null, updatedAt = null, deleted = false, diff --git a/noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/user/UserEntity.kt b/noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/user/UserEntity.kt index 47c2cd7..11b4253 100644 --- a/noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/user/UserEntity.kt +++ b/noweekend-storage/db-core/src/main/kotlin/noweekend/storage/db/core/user/UserEntity.kt @@ -56,7 +56,7 @@ class UserEntity( val birthDate: LocalDate? = null, @Column(name = "remaining_annual_leave", nullable = true) - val remainingAnnualLeave: Double = 0.0, + val remainingAnnualLeave: Double?, @Embedded var location: LocationEmbeddable?,