diff --git a/src/main/java/com/dnd/accompany/domain/auth/api/AuthController.java b/src/main/java/com/dnd/accompany/domain/auth/api/AuthController.java index cc640e7..2493b26 100644 --- a/src/main/java/com/dnd/accompany/domain/auth/api/AuthController.java +++ b/src/main/java/com/dnd/accompany/domain/auth/api/AuthController.java @@ -12,7 +12,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; +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; @@ -26,10 +27,9 @@ public class AuthController { private final OAuthService oAuthService; private final UserService userService; - @Operation(summary = "로그인") - @GetMapping("/sign-in") - public ResponseEntity signIn(LoginRequest loginRequest) { + @PostMapping("/sign-in") + public ResponseEntity signIn(@RequestBody LoginRequest loginRequest) { OAuthUserDataResponse oAuthUserData = oAuthService.login(loginRequest); OAuthUserInfo oAuthUserInfo = OAuthUserInfo.from(oAuthUserData); diff --git a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/KakaoUserData.java b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/KakaoUserData.java index 81e53c6..ada6356 100644 --- a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/KakaoUserData.java +++ b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/KakaoUserData.java @@ -16,7 +16,6 @@ public class KakaoUserData { @Getter @NoArgsConstructor static class KakaoAccount { - private String email; private KakaoProfile profile; } @@ -24,13 +23,16 @@ static class KakaoAccount { @NoArgsConstructor static class KakaoProfile { private String nickname; - } - public String getEmail() { - return kakaoAccount.getEmail(); + @JsonProperty("thumbnail_image_url") + private String profileImage; } public String getNickname() { return kakaoAccount.getProfile().getNickname(); } + + public String getProfileImage() { + return kakaoAccount.getProfile().getProfileImage(); + } } diff --git a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/LoginRequest.java b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/LoginRequest.java index 8a62d14..bb73a73 100644 --- a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/LoginRequest.java +++ b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/LoginRequest.java @@ -1,9 +1,7 @@ package com.dnd.accompany.domain.auth.oauth.dto; -import com.dnd.accompany.domain.auth.oauth.service.OAuthProvider; - public record LoginRequest( - OAuthProvider provider, + String provider, String accessToken ) { } diff --git a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserDataResponse.java b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserDataResponse.java index cfbf4bd..0bfd608 100644 --- a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserDataResponse.java +++ b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserDataResponse.java @@ -12,6 +12,6 @@ public class OAuthUserDataResponse { private String provider; private String oauthId; - private String email; + private String profileImageUrl; private String nickname; } diff --git a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserInfo.java b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserInfo.java index 75b0794..9f43d99 100644 --- a/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserInfo.java +++ b/src/main/java/com/dnd/accompany/domain/auth/oauth/dto/OAuthUserInfo.java @@ -13,15 +13,14 @@ public class OAuthUserInfo { private String provider; private String oauthId; private String nickname; - private String email; - private String appleRefreshToken; + private String profileImageUrl; public static OAuthUserInfo from(OAuthUserDataResponse oAuthUserDataResponse) { return OAuthUserInfo.builder() .provider(oAuthUserDataResponse.getProvider()) .oauthId(oAuthUserDataResponse.getOauthId()) .nickname(oAuthUserDataResponse.getNickname()) - .email(oAuthUserDataResponse.getEmail()) + .profileImageUrl(oAuthUserDataResponse.getProfileImageUrl()) .build(); } } diff --git a/src/main/java/com/dnd/accompany/domain/auth/oauth/handler/KakaoService.java b/src/main/java/com/dnd/accompany/domain/auth/oauth/handler/KakaoService.java index 4a84ea2..12b8664 100644 --- a/src/main/java/com/dnd/accompany/domain/auth/oauth/handler/KakaoService.java +++ b/src/main/java/com/dnd/accompany/domain/auth/oauth/handler/KakaoService.java @@ -48,22 +48,23 @@ public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { try { ResponseEntity response = restTemplate.exchange( - url, - HttpMethod.GET, - httpRequest, - KakaoUserData.class + url, + HttpMethod.GET, + httpRequest, + KakaoUserData.class ); assert response.getBody() != null; KakaoUserData userData = response.getBody(); + return OAuthUserDataResponse.builder() - .provider(getAuthProvider().toString()) - .oauthId(userData.getId().toString()) - .email(userData.getEmail()) - .nickname(userData.getNickname()) - .build(); + .provider(getAuthProvider().toString()) + .profileImageUrl(userData.getProfileImage()) + .oauthId(userData.getId().toString()) + .nickname(userData.getNickname()) + .build(); - } catch (RestClientException e) { + } catch (Exception e) { log.warn("[KakaoService] failed to get OAuth User Data = {}", request.getAccessToken()); if (e instanceof RestClientResponseException) { diff --git a/src/main/java/com/dnd/accompany/domain/auth/oauth/service/OAuthProvider.java b/src/main/java/com/dnd/accompany/domain/auth/oauth/service/OAuthProvider.java index dfcce5f..20b715f 100644 --- a/src/main/java/com/dnd/accompany/domain/auth/oauth/service/OAuthProvider.java +++ b/src/main/java/com/dnd/accompany/domain/auth/oauth/service/OAuthProvider.java @@ -16,9 +16,9 @@ public enum OAuthProvider { private final String name; - public static OAuthProvider get(OAuthProvider oAuthProvider) { + public static OAuthProvider get(String name) { return Arrays.stream(OAuthProvider.values()) - .filter(provider -> provider.equals(oAuthProvider)) + .filter(provider -> provider.getName().equals(name)) .findAny() .orElseThrow(() -> new NotFoundException(ErrorCode.INVALID_PROVIDER)); } 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..b494d8b --- /dev/null +++ b/src/main/java/com/dnd/accompany/domain/user/api/UserProfileController.java @@ -0,0 +1,40 @@ +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.RequestBody; +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, + @RequestBody @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/entity/User.java b/src/main/java/com/dnd/accompany/domain/user/entity/User.java index ffa9621..3d50a2b 100644 --- a/src/main/java/com/dnd/accompany/domain/user/entity/User.java +++ b/src/main/java/com/dnd/accompany/domain/user/entity/User.java @@ -31,9 +31,6 @@ public class User extends TimeBaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private String email; - @Column(nullable = false) private String nickname; @@ -46,14 +43,17 @@ public class User extends TimeBaseEntity { ) private String oauthId; - private boolean deleted = Boolean.FALSE; + @Column(length = 1000) + private String profileImageUrl; + + private boolean deleted = false; - public static User of(String email, String nickname, String provider, String oauthId) { + public static User of(String nickname, String provider, String oauthId, String profileImageUrl) { return User.builder() - .email(email) - .nickname(nickname) - .provider(provider) - .oauthId(oauthId) - .build(); + .nickname(nickname) + .provider(provider) + .oauthId(oauthId) + .profileImageUrl(profileImageUrl) + .build(); } } 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/domain/user/service/UserService.java b/src/main/java/com/dnd/accompany/domain/user/service/UserService.java index 510d35a..c3d8dcf 100644 --- a/src/main/java/com/dnd/accompany/domain/user/service/UserService.java +++ b/src/main/java/com/dnd/accompany/domain/user/service/UserService.java @@ -25,10 +25,10 @@ public AuthUserInfo getOrRegister(OAuthUserInfo oauthUserInfo) { @Transactional public User registerUser(OAuthUserInfo oauthUserInfo) { return userRepository.save(User.of( - oauthUserInfo.getEmail(), oauthUserInfo.getNickname(), oauthUserInfo.getProvider(), - oauthUserInfo.getOauthId() + oauthUserInfo.getOauthId(), + oauthUserInfo.getProfileImageUrl() )); } } 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 3f785b0..2cb099d 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 diff --git a/src/test/java/com/dnd/accompany/domain/user/service/UserServiceTest.java b/src/test/java/com/dnd/accompany/domain/user/service/UserServiceTest.java index fb21892..43e4681 100644 --- a/src/test/java/com/dnd/accompany/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/dnd/accompany/domain/user/service/UserServiceTest.java @@ -39,7 +39,7 @@ void setup() { .provider("KAKAO") .nickname("TESTER1") .oauthId("KA-123") - .email("test@gmail.com") + .profileImageUrl("https://") .build(); oauthUserInfo = OAuthUserInfo.from(oAuthUserDataResponse); @@ -50,10 +50,10 @@ void setup() { void success() { //given User newUser = User.of( - oauthUserInfo.getEmail(), oauthUserInfo.getNickname(), oauthUserInfo.getProvider(), - oauthUserInfo.getOauthId() + oauthUserInfo.getOauthId(), + oauthUserInfo.getProfileImageUrl() ); ReflectionTestUtils.setField(newUser, "id", 1L); @@ -74,10 +74,10 @@ void success() { void success2() { //given User existingUser = User.of( - oauthUserInfo.getEmail(), oauthUserInfo.getNickname(), oauthUserInfo.getProvider(), - oauthUserInfo.getOauthId() + oauthUserInfo.getOauthId(), + oauthUserInfo.getProfileImageUrl() ); ReflectionTestUtils.setField(existingUser, "id", 1L);