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
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import com.zimdugo.core.exception.BusinessException;
import com.zimdugo.core.exception.ErrorCode;
import com.zimdugo.user.application.UserQueryService;
import com.zimdugo.user.domain.SocialAccountStore;
import com.zimdugo.user.domain.User;
import com.zimdugo.user.domain.UserStatus;
import com.zimdugo.user.domain.UserStore;
import com.zimdugo.user.infrastructure.SocialAccountJpaRepository;
import com.zimdugo.user.infrastructure.UserJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -20,8 +20,8 @@ public class AccountWithdrawalService {
private final AccessTokenValidationService accessTokenValidationService;
private final JwtTokenProvider jwtTokenProvider;
private final UserQueryService userQueryService;
private final UserStore userStore;
private final SocialAccountStore socialAccountStore;
private final UserJpaRepository userJpaRepository;
private final SocialAccountJpaRepository socialAccountJpaRepository;
private final RefreshTokenRepository refreshTokenRepository;

public void withdraw(String accessToken) {
Expand All @@ -39,9 +39,9 @@ public void withdraw(String accessToken) {
}

user.changeStatus(UserStatus.DELETED);
userStore.store(user);
userJpaRepository.save(user);

socialAccountStore.deleteAllByUserId(userId);
socialAccountJpaRepository.deleteAllByUserId(userId);
refreshTokenRepository.deleteAllByUserId(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import com.zimdugo.auth.domain.OAuth2UserInfo;
import com.zimdugo.auth.domain.OAuth2UserInfoFactory;
import com.zimdugo.user.domain.SocialAccount;
import com.zimdugo.user.domain.SocialAccountReader;
import com.zimdugo.user.domain.SocialAccountStore;
import com.zimdugo.user.domain.User;
import com.zimdugo.user.domain.UserRole;
import com.zimdugo.user.domain.UserStatus;
import com.zimdugo.user.domain.UserStore;
import com.zimdugo.user.infrastructure.SocialAccountJpaRepository;
import com.zimdugo.user.infrastructure.UserJpaRepository;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
Expand All @@ -21,18 +23,13 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@RequiredArgsConstructor
@Transactional
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

private final UserStore userStore;
private final SocialAccountReader socialAccountReader;
private final SocialAccountStore socialAccountStore;
private final UserJpaRepository userJpaRepository;
private final SocialAccountJpaRepository socialAccountJpaRepository;
Comment on lines +31 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Repository 네이밍에 JPA라는 기술을 사용한다는걸 명시적으로 알려줄 필요는 없을 것 같아요


@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Expand All @@ -48,67 +45,51 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic

Map<String, Object> attributes = new HashMap<>(oAuth2User.getAttributes());
attributes.put("userId", user.getId());
attributes.put("email", user.getEmail()); // null 가능
attributes.put("email", user.getEmail());
attributes.put("nickname", user.getNickname());
attributes.put("role", user.getRoleOrDefault().name());

String nameAttributeKey = resolveNameAttributeKey(user, userInfo);
String nameAttributeKey = resolveNameAttributeKey(user);

return new DefaultOAuth2User(
List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))),
attributes,
nameAttributeKey
List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))),
attributes,
nameAttributeKey
);
}

private void validateRequiredFields(OAuth2UserInfo userInfo, String registrationId) {
if (userInfo.getProviderUserId() == null || userInfo.getProviderUserId().isBlank()) {
throw new OAuth2AuthenticationException(
new OAuth2Error("invalid_user_info"),
registrationId + " 사용자 식별값(providerUserId) 가져오지 못했습니다."
new OAuth2Error("invalid_user_info"),
registrationId + " 사용자 식별자(providerUserId) 가져오지 못했습니다."
Comment on lines +64 to +65
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이부분은 사용자에게 어떻게 핸들링되서 내려가게 되나요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

현재 OAuth2 실패 핸들러는 별도 등록하지 않아 Spring Security 기본 failure flow(리다이렉트)로 처리되고 있습니다.
반면 성공은 OAuth2SuccessHandler에서 JSON을 직접 내려주고 있어 성공/실패 응답 방식이 혼재된 상태입니다.

응답 일관성을 위해 OAuth2 성공/실패를 한 방식으로 통일하는 게 맞다고 생각합니다.. 놓친부분 확인해 주셔서 감사합니다! 현재 브라우저 기반 소셜 로그인 흐름을 고려하면 리다이렉트 방식으로 맞추는 방향이 더 자연스럽다고 보고 있습니다.
리뷰 의견 주시면 성공 처리도 리다이렉트 기반으로 정리해 반영하겠습니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

넵 그리고 리다이렉트시에 어디로 보낼지는 프론트와 협의가 필요한 부분이니 이야기 나눠보시면 좋을 것 같습니다.

);
}
}

private User findOrCreateUser(OAuth2UserInfo userInfo) {
return socialAccountReader
return socialAccountJpaRepository
.findByProviderAndProviderUserId(userInfo.getProvider(), userInfo.getProviderUserId())
.map(socialAccount -> syncAndGetUser(socialAccount, userInfo))
.map(SocialAccount::getUser)
.orElseGet(() -> createNewUser(userInfo));
}

private User syncAndGetUser(SocialAccount socialAccount, OAuth2UserInfo userInfo) {
socialAccount.updateProviderProfile(
normalize(userInfo.getEmail()),
normalize(userInfo.getProfileImageUrl())
);
SocialAccount saved = socialAccountStore.store(socialAccount);
return saved.getUser();
}

private User createNewUser(OAuth2UserInfo userInfo) {
String email = normalize(userInfo.getEmail());
String nickname = resolveNickname(userInfo);
String profileImageUrl = normalize(userInfo.getProfileImageUrl());

User user = new User(
email, // 카카오는 null일 수 있음
nickname,
profileImageUrl,
UserStatus.ACTIVE
);

User savedUser = userStore.store(user);
User user = new User(email, nickname, profileImageUrl, UserStatus.ACTIVE);
User savedUser = userJpaRepository.save(user);

SocialAccount socialAccount = new SocialAccount(
savedUser,
userInfo.getProvider(),
userInfo.getProviderUserId(),
email, // null 가능
profileImageUrl
savedUser,
userInfo.getProvider(),
userInfo.getProviderUserId(),
email,
profileImageUrl
);

socialAccountStore.store(socialAccount);
socialAccountJpaRepository.save(socialAccount);

return savedUser;
}
Expand All @@ -135,7 +116,7 @@ private String normalize(String value) {
return trimmed.isBlank() ? null : trimmed;
}

private String resolveNameAttributeKey(User user, OAuth2UserInfo userInfo) {
private String resolveNameAttributeKey(User user) {
if (user.getEmail() != null && !user.getEmail().isBlank()) {
return "email";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.zimdugo.auth.domain;

import com.zimdugo.identity.domain.AuthProvider;
import com.zimdugo.user.domain.AuthProvider;

import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.zimdugo.auth.domain;

import com.zimdugo.identity.domain.AuthProvider;
import com.zimdugo.user.domain.AuthProvider;

import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.zimdugo.auth.domain;

import com.zimdugo.identity.domain.AuthProvider;
import com.zimdugo.user.domain.AuthProvider;

import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.zimdugo.auth.domain;

import com.zimdugo.identity.domain.AuthProvider;
import com.zimdugo.user.domain.AuthProvider;

import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.zimdugo.auth.domain;

import com.zimdugo.identity.domain.AuthProvider;
import com.zimdugo.user.domain.AuthProvider;

public interface OAuth2UserInfo {

Expand Down
37 changes: 19 additions & 18 deletions src/main/java/com/zimdugo/user/application/UserQueryService.java
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
package com.zimdugo.user.application;

import com.zimdugo.user.domain.SocialAccount;
import com.zimdugo.user.domain.SocialAccountReader;
import com.zimdugo.user.domain.User;
import com.zimdugo.user.domain.UserReader;
import com.zimdugo.core.exception.BusinessException;
import com.zimdugo.core.exception.ErrorCode;
import java.util.List;
import com.zimdugo.user.infrastructure.SocialAccountJpaRepository;
import com.zimdugo.user.infrastructure.UserJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserQueryService {

private final UserReader userReader;
private final SocialAccountReader socialAccountReader;
private final UserJpaRepository userJpaRepository;
private final SocialAccountJpaRepository socialAccountJpaRepository;

public UserProfileResponse getProfile(Long userId) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

아키텍처 규칙상 Application Layer에서 바로 Response를 반환하면 안됩니다.

별도의 DTO로 변환 후 컨트롤러에서 Response로 변환해주세요.

User user = findById(userId);

List<SocialAccount> socialAccounts = socialAccountReader.findAllByUserId(userId);
List<SocialAccount> socialAccounts =
socialAccountJpaRepository.findAllByUserId(userId);

List<String> providers = socialAccounts.stream()
.map(sa -> sa.getProvider().name().toLowerCase())
.toList();
.map(sa -> sa.getProvider().name().toLowerCase())
.toList();

return new UserProfileResponse(
user.getId(),
user.getEmail(),
user.getNickname(),
user.getProfileImageUrl(),
user.getStatus().name(),
providers
user.getId(),
user.getEmail(),
user.getNickname(),
user.getProfileImageUrl(),
user.getStatus().name(),
providers
);
}

// AuthController에서 재발급 시 User 조회용
public User findById(Long userId) {
return userReader.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
return userJpaRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("user not found. id=" + userId));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.zimdugo.identity.domain;
package com.zimdugo.user.domain;

public enum AuthProvider {
GOOGLE,
KAKAO,
NAVER,
FACEBOOK
}
}
77 changes: 52 additions & 25 deletions src/main/java/com/zimdugo/user/domain/SocialAccount.java
Original file line number Diff line number Diff line change
@@ -1,51 +1,78 @@
package com.zimdugo.user.domain;

import com.zimdugo.identity.domain.AuthProvider;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(
name = "social_account",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_provider_provider_user_id",
columnNames = {"provider", "provider_user_id"}
)
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SocialAccount {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@JoinColumn(name = "user_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private User user;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private AuthProvider provider;

@Column(name = "provider_user_id", nullable = false, length = 100)
private String providerUserId;

@Column(length = 100)
private String providerEmail;

@Column(length = 255)
private String providerProfileImageUrl;
private LocalDateTime linkedAt;

public SocialAccount(
User user,
AuthProvider provider,
String providerUserId,
String providerEmail,
String providerProfileImageUrl
) {
this(null, user, provider, providerUserId, providerEmail, providerProfileImageUrl, null);
}
@Column(nullable = false)
private LocalDateTime linkedAt;

@SuppressWarnings("checkstyle:ParameterNumber")
public SocialAccount(
Long id,
User user,
AuthProvider provider,
String providerUserId,
String providerEmail,
String providerProfileImageUrl,
LocalDateTime linkedAt
User user,
AuthProvider provider,
String providerUserId,
String providerEmail,
String providerProfileImageUrl
) {
this.id = id;
this.user = user;
this.provider = provider;
this.providerUserId = providerUserId;
this.providerEmail = providerEmail;
this.providerProfileImageUrl = providerProfileImageUrl;
this.linkedAt = linkedAt;
}

public void updateProviderProfile(String providerEmail, String providerProfileImageUrl) {
this.providerEmail = providerEmail;
this.providerProfileImageUrl = providerProfileImageUrl;
@PrePersist
protected void onCreate() {
this.linkedAt = LocalDateTime.now();
}
}
Loading
Loading