diff --git a/src/main/java/com/Timo/Timo/domain/user/controller/OnboardingController.java b/src/main/java/com/Timo/Timo/domain/user/controller/OnboardingController.java new file mode 100644 index 00000000..3e182760 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/user/controller/OnboardingController.java @@ -0,0 +1,39 @@ +package com.Timo.Timo.domain.user.controller; + +import com.Timo.Timo.domain.user.docs.OnboardingControllerDocs; +import com.Timo.Timo.domain.user.dto.request.OnboardingRequest; +import com.Timo.Timo.domain.user.dto.response.OnboardingResponse; +import com.Timo.Timo.domain.user.factory.OnboardingResponseFactory; +import com.Timo.Timo.domain.user.service.OnboardingService; +import com.Timo.Timo.global.auth.principal.CustomUserDetails; +import com.Timo.Timo.global.response.BaseResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +@Tag(name = "Onboarding", description = "온보딩 API") +public class OnboardingController implements OnboardingControllerDocs { + + private final OnboardingService onboardingService; + + @Override + @PostMapping("/onboarding") + public ResponseEntity> completeOnboarding( + @AuthenticationPrincipal CustomUserDetails userDetails, + @Valid @RequestBody OnboardingRequest request + ){ + Long userId = userDetails.getUserId(); + OnboardingResponse response = onboardingService.completeOnboarding(userId, request); + + return OnboardingResponseFactory.completed(response); + } +} diff --git a/src/main/java/com/Timo/Timo/domain/user/docs/OnboardingControllerDocs.java b/src/main/java/com/Timo/Timo/domain/user/docs/OnboardingControllerDocs.java new file mode 100644 index 00000000..5fb72056 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/user/docs/OnboardingControllerDocs.java @@ -0,0 +1,17 @@ +package com.Timo.Timo.domain.user.docs; + +import com.Timo.Timo.domain.user.dto.request.OnboardingRequest; +import com.Timo.Timo.domain.user.dto.response.OnboardingResponse; +import com.Timo.Timo.global.auth.principal.CustomUserDetails; +import com.Timo.Timo.global.response.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.http.ResponseEntity; + +public interface OnboardingControllerDocs { + + @Operation(summary = "온보딩 완료", description = "언어, 예측 정확도, 기상/취침 시간을 저장하고 온보딩을 완료 처리합니다.") + ResponseEntity> completeOnboarding( + CustomUserDetails userDetails, + OnboardingRequest request + ); +} diff --git a/src/main/java/com/Timo/Timo/domain/user/dto/request/OnboardingRequest.java b/src/main/java/com/Timo/Timo/domain/user/dto/request/OnboardingRequest.java new file mode 100644 index 00000000..7b11a4dc --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/user/dto/request/OnboardingRequest.java @@ -0,0 +1,27 @@ +package com.Timo.Timo.domain.user.dto.request; + +import com.Timo.Timo.domain.user.enums.Language; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +public record OnboardingRequest( + + @NotNull(message = "language는 필수입니다.") + Language language, + + @NotNull(message = "predictionAccuracy는 필수입니다.") + @Min(value = 1, message = "predictionAccuracy는 1~4 사이여야 합니다.") + @Max(value = 4, message = "predictionAccuracy는 1~4 사이여야 합니다.") + Long predictionAccuracy, + + @NotNull(message = "wakeupTime은 필수입니다.") + @Pattern(regexp = "^([01]\\d|2[0-3]):[0-5]\\d$", message = "wakeUpTime 형식은 HH:MM 이어야 합니다.") + String wakeUpTime, + + @NotNull(message = "bedTime은 필수입니다.") + @Pattern(regexp = "^([01]\\d|2[0-3]):[0-5]\\d$", message = "bedTime 형식은 HH:MM 이어야 합니다.") + String bedTime +){ +} diff --git a/src/main/java/com/Timo/Timo/domain/user/dto/response/OnboardingResponse.java b/src/main/java/com/Timo/Timo/domain/user/dto/response/OnboardingResponse.java new file mode 100644 index 00000000..de52f7e4 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/user/dto/response/OnboardingResponse.java @@ -0,0 +1,8 @@ +package com.Timo.Timo.domain.user.dto.response; + +import lombok.Builder; + +@Builder +public record OnboardingResponse( + boolean onboardingCompleted +) {} diff --git a/src/main/java/com/Timo/Timo/domain/user/entity/User.java b/src/main/java/com/Timo/Timo/domain/user/entity/User.java index 763e0be4..4752022c 100644 --- a/src/main/java/com/Timo/Timo/domain/user/entity/User.java +++ b/src/main/java/com/Timo/Timo/domain/user/entity/User.java @@ -76,7 +76,16 @@ public void update(String name, String profileImageUrl) { this.profileImageUrl = profileImageUrl; } - public void completeOnboarding() { + public void completeOnboarding( + Language language, + Long predictionAccuracy, + LocalTime wakeUpTime, + LocalTime bedTime + ) { + this.language = language; + this.predictionAccuracy = predictionAccuracy; + this.wakeUpTime = wakeUpTime; + this.bedTime = bedTime; this.onboardingCompleted = true; } diff --git a/src/main/java/com/Timo/Timo/domain/user/exception/UserErrorCode.java b/src/main/java/com/Timo/Timo/domain/user/exception/UserErrorCode.java index 5ddbf59f..e1ba79c5 100644 --- a/src/main/java/com/Timo/Timo/domain/user/exception/UserErrorCode.java +++ b/src/main/java/com/Timo/Timo/domain/user/exception/UserErrorCode.java @@ -9,6 +9,7 @@ @RequiredArgsConstructor public enum UserErrorCode implements BaseErrorCode { + USER_400_INVALID_TIME(HttpStatus.BAD_REQUEST, "USER_400", "취침 시간은 기상 시간보다 이후여야 합니다."), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404", "존재하지 않는 사용자입니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/Timo/Timo/domain/user/exception/UserSuccessCode.java b/src/main/java/com/Timo/Timo/domain/user/exception/UserSuccessCode.java index 61c25b7d..0dba1e92 100644 --- a/src/main/java/com/Timo/Timo/domain/user/exception/UserSuccessCode.java +++ b/src/main/java/com/Timo/Timo/domain/user/exception/UserSuccessCode.java @@ -12,7 +12,8 @@ public enum UserSuccessCode implements BaseSuccessCode { PROFILE_RETRIEVED(HttpStatus.OK, "프로필을 조회했습니다."), - LANGUAGE_UPDATED(HttpStatus.OK, "언어설정이 수정되었습니다."); + LANGUAGE_UPDATED(HttpStatus.OK, "언어설정이 수정되었습니다."), + ONBOARDING_COMPLETED(HttpStatus.OK, "온보딩이 완료되었습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/Timo/Timo/domain/user/factory/OnboardingResponseFactory.java b/src/main/java/com/Timo/Timo/domain/user/factory/OnboardingResponseFactory.java new file mode 100644 index 00000000..dd0d2cdb --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/user/factory/OnboardingResponseFactory.java @@ -0,0 +1,17 @@ +package com.Timo.Timo.domain.user.factory; + +import com.Timo.Timo.domain.user.dto.response.OnboardingResponse; +import com.Timo.Timo.domain.user.exception.UserSuccessCode; +import com.Timo.Timo.global.response.BaseResponse; +import org.springframework.http.ResponseEntity; + +public class OnboardingResponseFactory { + + public static ResponseEntity> completed( + OnboardingResponse response + ){ + return ResponseEntity.ok( + BaseResponse.onSuccess(UserSuccessCode.ONBOARDING_COMPLETED, response) + ); + } +} diff --git a/src/main/java/com/Timo/Timo/domain/user/service/OnboardingService.java b/src/main/java/com/Timo/Timo/domain/user/service/OnboardingService.java new file mode 100644 index 00000000..2ed7f29a --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/user/service/OnboardingService.java @@ -0,0 +1,44 @@ +package com.Timo.Timo.domain.user.service; + +import com.Timo.Timo.domain.user.dto.request.OnboardingRequest; +import com.Timo.Timo.domain.user.dto.response.OnboardingResponse; +import com.Timo.Timo.domain.user.entity.User; +import com.Timo.Timo.domain.user.exception.UserErrorCode; +import com.Timo.Timo.domain.user.repository.UserRepository; +import com.Timo.Timo.global.exception.CustomException; +import java.time.LocalTime; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class OnboardingService { + + private final UserRepository userRepository; + + @Transactional + public OnboardingResponse completeOnboarding(Long userId, OnboardingRequest request){ + + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); + + LocalTime wakeUpTime = LocalTime.parse(request.wakeUpTime()); + LocalTime bedTime = LocalTime.parse(request.bedTime()); + + if (!bedTime.isAfter(wakeUpTime)) { + throw new CustomException(UserErrorCode.USER_400_INVALID_TIME); + } + + user.completeOnboarding( + request.language(), + request.predictionAccuracy(), + wakeUpTime, + bedTime + ); + + return OnboardingResponse.builder() + .onboardingCompleted(true) + .build(); + } +} diff --git a/src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java b/src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java index f1536cab..0fff9596 100644 --- a/src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java +++ b/src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java @@ -18,7 +18,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.CookieValue; diff --git a/src/main/java/com/Timo/Timo/global/auth/docs/AuthControllerDocs.java b/src/main/java/com/Timo/Timo/global/auth/docs/AuthControllerDocs.java index b941270f..dcb4fad6 100644 --- a/src/main/java/com/Timo/Timo/global/auth/docs/AuthControllerDocs.java +++ b/src/main/java/com/Timo/Timo/global/auth/docs/AuthControllerDocs.java @@ -13,9 +13,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; import jakarta.servlet.http.HttpServletRequest; -import java.util.Map; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestBody; @@ -32,7 +31,8 @@ public interface AuthControllerDocs { - RefreshToken과 sessionId는 Set-Cookie 헤더로 전달되며, 응답 바디에는 포함되지 않음 """ ) - @io.swagger.v3.oas.annotations.parameters.RequestBody( + @SecurityRequirements + @io.swagger.v3.oas.annotations.parameters.RequestBody( content = @Content( mediaType = "application/json", schema = @Schema(implementation = AuthTokenRequest.class), @@ -98,6 +98,7 @@ ResponseEntity> token( """ ) + @SecurityRequirements @ApiResponses({ @ApiResponse( responseCode = "200", @@ -139,8 +140,7 @@ ResponseEntity> reissue( description = """ 현재 세션을 로그아웃하고 RefreshToken 및 sessionId 쿠키를 만료시킵니다. Swagger UI 오른쪽 위의 Authorize 버튼을 눌러 유효한 Access Token을 입력해야 합니다. - """, - security = @SecurityRequirement(name = "bearerAuth") + """ ) @ApiResponses({ @ApiResponse( @@ -177,8 +177,7 @@ ResponseEntity> logout( 회원 탈퇴를 진행하며, 사용자와 관련된 모든 데이터를 영구 삭제합니다. Swagger UI 오른쪽 위의 Authorize 버튼을 눌러 유효한 Access Token을 입력해야 합니다. 이 작업은 되돌릴 수 없습니다. - """, - security = @SecurityRequirement(name = "bearerAuth") + """ ) @ApiResponses({ @ApiResponse(