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
7 changes: 0 additions & 7 deletions src/main/java/project/flipnote/common/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,9 +13,4 @@ public class AppConfig {
public RestClient restClient() {
return RestClient.create();
}

@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
10 changes: 10 additions & 0 deletions src/main/java/project/flipnote/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -77,4 +78,13 @@ public ResponseEntity<Void> updatePassword(
userService.changePassword(userAuth.userId(), req);
return ResponseEntity.noContent().build();
}

@GetMapping("/me/social-links")
public ResponseEntity<SocialLinksResponse> getSocialLinks(
@AuthenticationPrincipal UserAuth userAuth
) {
SocialLinksResponse res = userService.getSocialLinks(userAuth.userId());

return ResponseEntity.ok(res);
}
}
12 changes: 11 additions & 1 deletion src/main/java/project/flipnote/user/entity/UserOAuthLink.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
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;
import jakarta.persistence.Id;
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;
Expand All @@ -20,6 +25,7 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Table(
name = "user_oauth_link",
indexes = {
Expand All @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/project/flipnote/user/model/SocialLinkResponse.java
Original file line number Diff line number Diff line change
@@ -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()
);
}
Comment on lines +17 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

팩토리 메서드에 null 안전성을 개선하세요.

link 파라미터와 link.getLinkedAt() 값에 대한 null 체크가 없어 런타임에 NullPointerException이 발생할 수 있습니다.

 public static SocialLinkResponse from(UserOAuthLink link) {
+    if (link == null) {
+        throw new IllegalArgumentException("UserOAuthLink cannot be null");
+    }
     return new SocialLinkResponse(
         link.getProvider(),
         link.getLinkedAt()
     );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static SocialLinkResponse from(UserOAuthLink link) {
return new SocialLinkResponse(
link.getProvider(),
link.getLinkedAt()
);
}
public static SocialLinkResponse from(UserOAuthLink link) {
if (link == null) {
throw new IllegalArgumentException("UserOAuthLink cannot be null");
}
return new SocialLinkResponse(
link.getProvider(),
link.getLinkedAt()
);
}
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/user/model/SocialLinkResponse.java around
lines 17 to 22, the from() factory method lacks null safety checks for the link
parameter and its getLinkedAt() value, which can cause NullPointerException at
runtime. Add null checks to verify that link is not null before accessing its
methods, and also check if link.getLinkedAt() is null before passing it to the
constructor, handling these cases appropriately to prevent exceptions.

}
18 changes: 18 additions & 0 deletions src/main/java/project/flipnote/user/model/SocialLinksResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package project.flipnote.user.model;

import java.util.List;

import project.flipnote.user.entity.UserOAuthLink;

public record SocialLinksResponse(
List<SocialLinkResponse> socialLinks
) {

public static SocialLinksResponse from(List<UserOAuthLink> links) {
List<SocialLinkResponse> socialLinks = links.stream()
.map(SocialLinkResponse::from)
.toList();

return new SocialLinksResponse(socialLinks);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package project.flipnote.user.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import project.flipnote.user.entity.UserOAuthLink;

public interface UserOAuthLinkRepository extends JpaRepository<UserOAuthLink, Long> {

boolean existsByUser_IdAndProviderId(Long userId, String providerId);

List<UserOAuthLink> findByUser_Id(Long userId);
}
12 changes: 11 additions & 1 deletion src/main/java/project/flipnote/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
package project.flipnote.user.service;

import java.util.List;
import java.util.Objects;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
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
Expand All @@ -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) {
Expand Down Expand Up @@ -104,6 +108,12 @@ public void changePassword(Long userId, ChangePasswordRequest req) {
tokenVersionService.incrementTokenVersion(userId);
}

public SocialLinksResponse getSocialLinks(Long userId) {
List<UserOAuthLink> 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));
Expand Down
32 changes: 28 additions & 4 deletions src/test/java/project/flipnote/user/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,23 +17,25 @@
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;
import project.flipnote.auth.service.TokenVersionService;
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("회원 서비스 단위 테스트")
Expand All @@ -51,9 +54,6 @@ class UserServiceTest {
@Mock
TokenVersionRedisRepository tokenVersionRedisRepository;

@Mock
EmailVerificationRedisRepository emailVerificationRedisRepository;

@Mock
AuthService authService;

Expand All @@ -63,6 +63,9 @@ class UserServiceTest {
@Mock
EmailVerificationService emailVerificationService;

@Mock
UserOAuthLinkRepository userOAuthLinkRepository;

@DisplayName("회원가입 테스트")
@Nested
class Register {
Expand Down Expand Up @@ -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<UserOAuthLink> 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");
}
}
}
Loading