From c484a172e70b1d3ac90d831993e7d7b2380e868c Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 26 Jul 2025 19:48:42 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20=EB=82=B4=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/OAuthController.java | 2 +- .../flipnote/common/config/AppConfig.java | 7 ------ .../user/controller/UserController.java | 10 ++++++++ .../flipnote/user/entity/UserOAuthLink.java | 12 +++++++++- .../user/model/SocialLinkResponse.java | 23 +++++++++++++++++++ .../user/model/SocialLinksResponse.java | 18 +++++++++++++++ .../repository/UserOAuthLinkRepository.java | 4 ++++ .../flipnote/user/service/UserService.java | 12 +++++++++- 8 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 src/main/java/project/flipnote/user/model/SocialLinkResponse.java create mode 100644 src/main/java/project/flipnote/user/model/SocialLinksResponse.java diff --git a/src/main/java/project/flipnote/auth/controller/OAuthController.java b/src/main/java/project/flipnote/auth/controller/OAuthController.java index 4e7811c1..39fe90d7 100644 --- a/src/main/java/project/flipnote/auth/controller/OAuthController.java +++ b/src/main/java/project/flipnote/auth/controller/OAuthController.java @@ -37,7 +37,7 @@ public ResponseEntity redirectToProviderAuthorization( HttpServletRequest request, @AuthenticationPrincipal UserAuth userAuth ) { - AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, userAuth.userId()); + AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, 1L); return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.SET_COOKIE, authRedirect.cookie().toString()) 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)); From 97004b8bdc6b5ed71d6e0b39ca2a0c5c3b6376b1 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 26 Jul 2025 19:53:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Test:=20=EB=82=B4=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/UserServiceTest.java | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index 1afe5071..d4de0a46 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,24 @@ void fail_incorrectCurrentPassword() { verify(tokenVersionRedisRepository, never()).deleteTokenVersion(anyLong()); } } + + @DisplayName("내 소셜 계정 목록 조회 테스트") + @Nested + class GetSocialLinks { + + @DisplayName("성공") + @Test + void success() { + long userId = 1L; + + List links = List.of(new UserOAuthLink("google", "providerId1", null)); + + given(userOAuthLinkRepository.findByUser_Id(userId)).willReturn(links); + + SocialLinksResponse res = userService.getSocialLinks(userId); + + assertThat(res.socialLinks()).isNotNull(); + assertThat(res.socialLinks().size()).isEqualTo(links.size()); + } + } } \ No newline at end of file From 609246c14d8aabc42b0cfa1fd1469955b03a4ae3 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 26 Jul 2025 20:26:47 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20=EC=9E=84=EC=8B=9C=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/auth/controller/OAuthController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/auth/controller/OAuthController.java b/src/main/java/project/flipnote/auth/controller/OAuthController.java index 39fe90d7..4e7811c1 100644 --- a/src/main/java/project/flipnote/auth/controller/OAuthController.java +++ b/src/main/java/project/flipnote/auth/controller/OAuthController.java @@ -37,7 +37,7 @@ public ResponseEntity redirectToProviderAuthorization( HttpServletRequest request, @AuthenticationPrincipal UserAuth userAuth ) { - AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, 1L); + AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, userAuth.userId()); return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.SET_COOKIE, authRedirect.cookie().toString()) From 2bbeaa62229a1592e1a0bac027e8f868971071c5 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 26 Jul 2025 20:27:08 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Test:=20=EB=82=B4=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/user/service/UserServiceTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index d4de0a46..1084f18e 100644 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -397,16 +397,17 @@ class GetSocialLinks { @DisplayName("성공") @Test void success() { - long userId = 1L; + User user = UserFixture.createActiveUser(); - List links = List.of(new UserOAuthLink("google", "providerId1", null)); + List links = List.of(new UserOAuthLink("google", "providerId1", user)); - given(userOAuthLinkRepository.findByUser_Id(userId)).willReturn(links); + given(userOAuthLinkRepository.findByUser_Id(user.getId())).willReturn(links); - SocialLinksResponse res = userService.getSocialLinks(userId); + SocialLinksResponse res = userService.getSocialLinks(user.getId()); assertThat(res.socialLinks()).isNotNull(); - assertThat(res.socialLinks().size()).isEqualTo(links.size()); + assertThat(res.socialLinks().size()).isEqualTo(1); + assertThat(res.socialLinks().get(0).provider()).isEqualTo("google"); } } } \ No newline at end of file