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
Original file line number Diff line number Diff line change
@@ -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<BaseResponse<OnboardingResponse>> completeOnboarding(
@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @RequestBody OnboardingRequest request
){
Long userId = userDetails.getUserId();
OnboardingResponse response = onboardingService.completeOnboarding(userId, request);

return OnboardingResponseFactory.completed(response);
}
}
Original file line number Diff line number Diff line change
@@ -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<BaseResponse<OnboardingResponse>> completeOnboarding(
CustomUserDetails userDetails,
OnboardingRequest request
);
}
Original file line number Diff line number Diff line change
@@ -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
){
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.Timo.Timo.domain.user.dto.response;

import lombok.Builder;

@Builder
public record OnboardingResponse(
boolean onboardingCompleted
) {}
11 changes: 10 additions & 1 deletion src/main/java/com/Timo/Timo/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BaseResponse<OnboardingResponse>> completed(
OnboardingResponse response
){
return ResponseEntity.ok(
BaseResponse.onSuccess(UserSuccessCode.ONBOARDING_COMPLETED, response)
);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand Down Expand Up @@ -98,6 +98,7 @@ ResponseEntity<BaseResponse<AuthTokenResponse>> token(
"""

)
@SecurityRequirements
@ApiResponses({
@ApiResponse(
responseCode = "200",
Expand Down Expand Up @@ -139,8 +140,7 @@ ResponseEntity<BaseResponse<AuthReissueResponse>> reissue(
description = """
현재 세션을 로그아웃하고 RefreshToken 및 sessionId 쿠키를 만료시킵니다.
Swagger UI 오른쪽 위의 Authorize 버튼을 눌러 유효한 Access Token을 입력해야 합니다.
""",
security = @SecurityRequirement(name = "bearerAuth")
"""
)
@ApiResponses({
@ApiResponse(
Expand Down Expand Up @@ -177,8 +177,7 @@ ResponseEntity<BaseResponse<Void>> logout(
회원 탈퇴를 진행하며, 사용자와 관련된 모든 데이터를 영구 삭제합니다.
Swagger UI 오른쪽 위의 Authorize 버튼을 눌러 유효한 Access Token을 입력해야 합니다.
이 작업은 되돌릴 수 없습니다.
""",
security = @SecurityRequirement(name = "bearerAuth")
"""
)
@ApiResponses({
@ApiResponse(
Expand Down