diff --git a/src/main/java/com/dnd/accompany/domain/user/api/UserProfileController.java b/src/main/java/com/dnd/accompany/domain/user/api/UserProfileController.java new file mode 100644 index 0000000..640c344 --- /dev/null +++ b/src/main/java/com/dnd/accompany/domain/user/api/UserProfileController.java @@ -0,0 +1,39 @@ +package com.dnd.accompany.domain.user.api; + +import com.dnd.accompany.domain.auth.dto.jwt.JwtAuthentication; +import com.dnd.accompany.domain.user.dto.CreateUserProfileRequest; +import com.dnd.accompany.domain.user.service.UserProfileService; +import io.swagger.v3.oas.annotations.Operation; +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.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Onboarding") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/profiles") +public class UserProfileController { + + private final UserProfileService userProfileService; + + @Operation(summary = "온보딩 정보 저장") + @PostMapping + public void createUserProfile(@AuthenticationPrincipal JwtAuthentication user, + @Valid CreateUserProfileRequest createUserProfileRequest + ) { + userProfileService.createUserProfile(user.getId(), createUserProfileRequest); + } + + @Operation(summary = "온보딩 여부 조회") + @GetMapping("/exist") + public ResponseEntity existUserProfile(@AuthenticationPrincipal JwtAuthentication user) { + boolean result = userProfileService.existByUserId(user.getId()); + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/com/dnd/accompany/domain/user/dto/CreateUserProfileRequest.java b/src/main/java/com/dnd/accompany/domain/user/dto/CreateUserProfileRequest.java new file mode 100644 index 0000000..5c72af9 --- /dev/null +++ b/src/main/java/com/dnd/accompany/domain/user/dto/CreateUserProfileRequest.java @@ -0,0 +1,27 @@ +package com.dnd.accompany.domain.user.dto; + +import com.dnd.accompany.domain.user.entity.enums.FoodPreference; +import com.dnd.accompany.domain.user.entity.enums.Gender; +import com.dnd.accompany.domain.user.entity.enums.TravelPreference; +import com.dnd.accompany.domain.user.entity.enums.TravelStyle; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public record CreateUserProfileRequest( + int birthYear, + + @NotNull + Gender gender, + + @NotEmpty + List travelPreferences, + + @NotEmpty + List travelStyles, + + @NotEmpty + List foodPreferences +) { +} diff --git a/src/main/java/com/dnd/accompany/domain/user/exception/UserProfileAlreadyExistsException.java b/src/main/java/com/dnd/accompany/domain/user/exception/UserProfileAlreadyExistsException.java new file mode 100644 index 0000000..89ddb69 --- /dev/null +++ b/src/main/java/com/dnd/accompany/domain/user/exception/UserProfileAlreadyExistsException.java @@ -0,0 +1,10 @@ +package com.dnd.accompany.domain.user.exception; + +import com.dnd.accompany.global.common.exception.BusinessException; +import com.dnd.accompany.global.common.response.ErrorCode; + +public class UserProfileAlreadyExistsException extends BusinessException { + public UserProfileAlreadyExistsException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/dnd/accompany/domain/user/service/UserProfileService.java b/src/main/java/com/dnd/accompany/domain/user/service/UserProfileService.java new file mode 100644 index 0000000..887141d --- /dev/null +++ b/src/main/java/com/dnd/accompany/domain/user/service/UserProfileService.java @@ -0,0 +1,44 @@ +package com.dnd.accompany.domain.user.service; + +import com.dnd.accompany.domain.user.dto.CreateUserProfileRequest; +import com.dnd.accompany.domain.user.entity.UserProfile; +import com.dnd.accompany.domain.user.exception.UserProfileAlreadyExistsException; +import com.dnd.accompany.domain.user.infrastructure.UserProfileRepository; +import com.dnd.accompany.global.common.response.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserProfileService { + + private final UserProfileRepository userProfileRepository; + + @Transactional + public void createUserProfile(Long userId, CreateUserProfileRequest createUserProfileRequest) { + validateDuplicateProfile(userId); + + UserProfile userProfile = UserProfile.builder() + .userId(userId) + .birthYear(createUserProfileRequest.birthYear()) + .gender(createUserProfileRequest.gender()) + .travelPreferences(createUserProfileRequest.travelPreferences()) + .travelStyles(createUserProfileRequest.travelStyles()) + .foodPreferences(createUserProfileRequest.foodPreferences()) + .build(); + + userProfileRepository.save(userProfile); + } + + @Transactional(readOnly = true) + public boolean existByUserId(Long userId) { + return userProfileRepository.existsById(userId); + } + + private void validateDuplicateProfile(Long userId) { + if (userProfileRepository.existsById(userId)) { + throw new UserProfileAlreadyExistsException(ErrorCode.PROFILE_ALREADY_EXISTS); + } + } +} diff --git a/src/main/java/com/dnd/accompany/global/common/response/ErrorCode.java b/src/main/java/com/dnd/accompany/global/common/response/ErrorCode.java index 8dfc1f7..556e097 100644 --- a/src/main/java/com/dnd/accompany/global/common/response/ErrorCode.java +++ b/src/main/java/com/dnd/accompany/global/common/response/ErrorCode.java @@ -24,6 +24,9 @@ public enum ErrorCode { INVALID_PROVIDER(MatripConstant.BAD_REQUEST, "LOGIN-001", "유효하지 않은 로그인 수단입니다."), INVALID_OAUTH_TOKEN(MatripConstant.BAD_REQUEST, "LOGIN-002", "유효하지 않은 OAuth 토큰입니다."), + // ---- 프로필 ---- // + PROFILE_ALREADY_EXISTS(MatripConstant.BAD_REQUEST, "PROFILE-001", "이미 프로필 정보가 존재합니다."), + // ---- 네트워크 ---- // HTTP_CLIENT_REQUEST_FAILED(MatripConstant.INTERNAL_SERVER_ERROR, "NETWORK-001", "서버 요청에 실패하였습니다."); diff --git a/src/test/java/com/dnd/accompany/domain/user/service/UserProfileServiceTest.java b/src/test/java/com/dnd/accompany/domain/user/service/UserProfileServiceTest.java new file mode 100644 index 0000000..f6c321b --- /dev/null +++ b/src/test/java/com/dnd/accompany/domain/user/service/UserProfileServiceTest.java @@ -0,0 +1,84 @@ +package com.dnd.accompany.domain.user.service; + +import com.dnd.accompany.domain.user.dto.CreateUserProfileRequest; +import com.dnd.accompany.domain.user.entity.UserProfile; +import com.dnd.accompany.domain.user.entity.enums.FoodPreference; +import com.dnd.accompany.domain.user.entity.enums.Gender; +import com.dnd.accompany.domain.user.entity.enums.TravelPreference; +import com.dnd.accompany.domain.user.entity.enums.TravelStyle; +import com.dnd.accompany.domain.user.exception.UserProfileAlreadyExistsException; +import com.dnd.accompany.domain.user.infrastructure.UserProfileRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static com.dnd.accompany.domain.user.entity.enums.FoodPreference.*; +import static com.dnd.accompany.domain.user.entity.enums.TravelPreference.*; +import static com.dnd.accompany.domain.user.entity.enums.TravelStyle.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserProfileServiceTest { + + @Mock + private UserProfileRepository userProfileRepository; + + @InjectMocks + private UserProfileService userProfileService; + + @DisplayName("유저 프로필을 생성할 때") + @Nested + class profile { + + private Long userId = 100L; + private CreateUserProfileRequest createUserProfileRequest; + + @BeforeEach + void setup() { + createUserProfileRequest = new CreateUserProfileRequest( + 2000, + Gender.MALE, + List.of(DRAWN_TO, PUBLIC_MONEY, QUICKLY, LEISURELY), + List.of(ACTIVITY, HEALING, CAFE_TOUR, SHOPPING), + List.of(MEAT, RICE, COFFEE, FAST_FOOD) + ); + } + + @DisplayName("신규 생성인 경우 정상 생성된다.") + @Test + void success() { + //given + given(userProfileRepository.existsById(anyLong())) + .willReturn(false); + + //when + userProfileService.createUserProfile(userId, createUserProfileRequest); + + //then + verify(userProfileRepository).save(any(UserProfile.class)); + } + + @DisplayName("이미 프로필이 존재하는 경우 예외가 발생한다.") + @Test + void fail() { + //given + given(userProfileRepository.existsById(anyLong())) + .willReturn(true); + + //when & then + assertThrows(UserProfileAlreadyExistsException.class, + () -> userProfileService.createUserProfile(userId, createUserProfileRequest)); + } + } +} \ No newline at end of file