diff --git a/src/main/java/project/flipnote/common/config/AppConfig.java b/src/main/java/project/flipnote/common/config/AppConfig.java index d765410e..dc321c4f 100644 --- a/src/main/java/project/flipnote/common/config/AppConfig.java +++ b/src/main/java/project/flipnote/common/config/AppConfig.java @@ -5,8 +5,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import com.fasterxml.jackson.databind.ObjectMapper; - @EnableConfigurationProperties({AsyncProperties.class, ClientProperties.class, OAuthProperties.class}) @Configuration public class AppConfig { @@ -15,9 +13,4 @@ public class AppConfig { public RestClient restClient() { return RestClient.create(); } - - @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); - } } diff --git a/src/main/java/project/flipnote/user/controller/UserController.java b/src/main/java/project/flipnote/user/controller/UserController.java index 512dac73..1a55ae5d 100644 --- a/src/main/java/project/flipnote/user/controller/UserController.java +++ b/src/main/java/project/flipnote/user/controller/UserController.java @@ -18,6 +18,7 @@ import project.flipnote.common.security.dto.UserAuth; import project.flipnote.user.model.MyInfoResponse; import project.flipnote.user.model.ChangePasswordRequest; +import project.flipnote.user.model.SocialLinksResponse; import project.flipnote.user.model.UserInfoResponse; import project.flipnote.user.model.UserRegisterRequest; import project.flipnote.user.model.UserRegisterResponse; @@ -77,4 +78,13 @@ public ResponseEntity updatePassword( userService.changePassword(userAuth.userId(), req); return ResponseEntity.noContent().build(); } + + @GetMapping("/me/social-links") + public ResponseEntity getSocialLinks( + @AuthenticationPrincipal UserAuth userAuth + ) { + SocialLinksResponse res = userService.getSocialLinks(userAuth.userId()); + + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/project/flipnote/user/entity/UserOAuthLink.java b/src/main/java/project/flipnote/user/entity/UserOAuthLink.java index 2d2f894d..4ed6cdee 100644 --- a/src/main/java/project/flipnote/user/entity/UserOAuthLink.java +++ b/src/main/java/project/flipnote/user/entity/UserOAuthLink.java @@ -1,7 +1,13 @@ package project.flipnote.user.entity; +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -9,7 +15,6 @@ import jakarta.persistence.Index; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -20,6 +25,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) @Table( name = "user_oauth_link", indexes = { @@ -43,6 +49,10 @@ public class UserOAuthLink { @JoinColumn(name = "user_id", nullable = false) private User user; + @CreatedDate + @Column(updatable = false) + private LocalDateTime linkedAt; + @Builder public UserOAuthLink(String provider, String providerId, User user) { this.provider = provider; diff --git a/src/main/java/project/flipnote/user/model/SocialLinkResponse.java b/src/main/java/project/flipnote/user/model/SocialLinkResponse.java new file mode 100644 index 00000000..c3293c6d --- /dev/null +++ b/src/main/java/project/flipnote/user/model/SocialLinkResponse.java @@ -0,0 +1,23 @@ +package project.flipnote.user.model; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import project.flipnote.user.entity.UserOAuthLink; + +public record SocialLinkResponse( + + String provider, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime linkedAt +) { + + public static SocialLinkResponse from(UserOAuthLink link) { + return new SocialLinkResponse( + link.getProvider(), + link.getLinkedAt() + ); + } +} diff --git a/src/main/java/project/flipnote/user/model/SocialLinksResponse.java b/src/main/java/project/flipnote/user/model/SocialLinksResponse.java new file mode 100644 index 00000000..593219d5 --- /dev/null +++ b/src/main/java/project/flipnote/user/model/SocialLinksResponse.java @@ -0,0 +1,18 @@ +package project.flipnote.user.model; + +import java.util.List; + +import project.flipnote.user.entity.UserOAuthLink; + +public record SocialLinksResponse( + List socialLinks +) { + + public static SocialLinksResponse from(List links) { + List socialLinks = links.stream() + .map(SocialLinkResponse::from) + .toList(); + + return new SocialLinksResponse(socialLinks); + } +} diff --git a/src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java b/src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java index 6a48bd48..23acd9c5 100644 --- a/src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java @@ -1,5 +1,7 @@ package project.flipnote.user.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import project.flipnote.user.entity.UserOAuthLink; @@ -7,4 +9,6 @@ public interface UserOAuthLinkRepository extends JpaRepository { boolean existsByUser_IdAndProviderId(Long userId, String providerId); + + List findByUser_Id(Long userId); } diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 930de5ba..595c56e7 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -1,5 +1,6 @@ package project.flipnote.user.service; +import java.util.List; import java.util.Objects; import org.springframework.security.crypto.password.PasswordEncoder; @@ -7,21 +8,23 @@ import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.auth.service.AuthService; import project.flipnote.auth.service.EmailVerificationService; import project.flipnote.auth.service.TokenVersionService; import project.flipnote.common.exception.BizException; import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserOAuthLink; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.model.MyInfoResponse; import project.flipnote.user.model.ChangePasswordRequest; +import project.flipnote.user.model.SocialLinksResponse; import project.flipnote.user.model.UserInfoResponse; import project.flipnote.user.model.UserRegisterRequest; import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.model.UserUpdateRequest; import project.flipnote.user.model.UserUpdateResponse; +import project.flipnote.user.repository.UserOAuthLinkRepository; import project.flipnote.user.repository.UserRepository; @RequiredArgsConstructor @@ -34,6 +37,7 @@ public class UserService { private final AuthService authService; private final TokenVersionService tokenVersionService; private final EmailVerificationService emailVerificationService; + private final UserOAuthLinkRepository userOAuthLinkRepository; @Transactional public UserRegisterResponse register(UserRegisterRequest req) { @@ -104,6 +108,12 @@ public void changePassword(Long userId, ChangePasswordRequest req) { tokenVersionService.incrementTokenVersion(userId); } + public SocialLinksResponse getSocialLinks(Long userId) { + List links = userOAuthLinkRepository.findByUser_Id(userId); + + return SocialLinksResponse.from(links); + } + private User findActiveUserById(Long userId) { return userRepository.findByIdAndStatus(userId, UserStatus.ACTIVE) .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index 1afe5071..1084f18e 100644 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -16,7 +17,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import project.flipnote.auth.exception.AuthErrorCode; -import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.auth.repository.TokenVersionRedisRepository; import project.flipnote.auth.service.AuthService; import project.flipnote.auth.service.EmailVerificationService; @@ -24,15 +24,18 @@ import project.flipnote.common.exception.BizException; import project.flipnote.fixture.UserFixture; import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserOAuthLink; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.model.MyInfoResponse; import project.flipnote.user.model.ChangePasswordRequest; +import project.flipnote.user.model.SocialLinksResponse; import project.flipnote.user.model.UserInfoResponse; import project.flipnote.user.model.UserRegisterRequest; import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.model.UserUpdateRequest; import project.flipnote.user.model.UserUpdateResponse; +import project.flipnote.user.repository.UserOAuthLinkRepository; import project.flipnote.user.repository.UserRepository; @DisplayName("회원 서비스 단위 테스트") @@ -51,9 +54,6 @@ class UserServiceTest { @Mock TokenVersionRedisRepository tokenVersionRedisRepository; - @Mock - EmailVerificationRedisRepository emailVerificationRedisRepository; - @Mock AuthService authService; @@ -63,6 +63,9 @@ class UserServiceTest { @Mock EmailVerificationService emailVerificationService; + @Mock + UserOAuthLinkRepository userOAuthLinkRepository; + @DisplayName("회원가입 테스트") @Nested class Register { @@ -386,4 +389,25 @@ void fail_incorrectCurrentPassword() { verify(tokenVersionRedisRepository, never()).deleteTokenVersion(anyLong()); } } + + @DisplayName("내 소셜 계정 목록 조회 테스트") + @Nested + class GetSocialLinks { + + @DisplayName("성공") + @Test + void success() { + User user = UserFixture.createActiveUser(); + + List links = List.of(new UserOAuthLink("google", "providerId1", user)); + + given(userOAuthLinkRepository.findByUser_Id(user.getId())).willReturn(links); + + SocialLinksResponse res = userService.getSocialLinks(user.getId()); + + assertThat(res.socialLinks()).isNotNull(); + assertThat(res.socialLinks().size()).isEqualTo(1); + assertThat(res.socialLinks().get(0).provider()).isEqualTo("google"); + } + } } \ No newline at end of file