Skip to content

Commit f0256ff

Browse files
authored
Merge pull request #32 from UruruLab/feat/24-social-login-common-structure
소셜 로그인 공통 구조 및 추상화 레이어 구현
2 parents 1e85bff + 4118e0f commit f0256ff

14 files changed

+667
-3
lines changed

src/main/java/com/ururulab/ururu/global/auth/dto/OAuthUserInfo.java renamed to src/main/java/com/ururulab/ururu/global/auth/dto/info/OAuthUserInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.ururulab.ururu.global.auth.dto;
1+
package com.ururulab.ururu.global.auth.dto.info;
22

33
import com.ururulab.ururu.member.domain.entity.enumerated.SocialProvider;
44

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.ururulab.ururu.global.auth.dto.info;
2+
3+
import com.ururulab.ururu.member.domain.entity.enumerated.SocialProvider;
4+
5+
/**
6+
* 소셜 제공자별 회원 정보를 통일된 형태로 표현하는 DTO.
7+
*
8+
* <p>각 소셜 제공자의 응답 형식을 표준화하여 일관된 회원 정보 처리가 가능하도록 합니다.</p>
9+
*/
10+
public record SocialMemberInfo(
11+
String socialId,
12+
String email,
13+
String nickname,
14+
String profileImage,
15+
SocialProvider provider
16+
) {
17+
18+
/**
19+
* 완전한 회원 정보로 SocialMemberInfo 생성 (이펙티브 자바 아이템 1).
20+
*
21+
* @param socialId 소셜 제공자 고유 ID
22+
* @param email 회원 이메일 (null 가능, 제공자가 이메일을 제공하지 않는 경우)
23+
* @param nickname 회원 닉네임 (null 가능, 제공자가 닉네임을 제공하지 않는 경우)
24+
* @param profileImage 프로필 이미지 URL (null 가능, 이미지가 없는 경우)
25+
* @param provider 소셜 제공자
26+
* @return 검증된 SocialMemberInfo 객체
27+
* @throws IllegalArgumentException socialId 또는 provider가 null인 경우
28+
*/
29+
public static SocialMemberInfo of(
30+
final String socialId,
31+
final String email,
32+
final String nickname,
33+
final String profileImage,
34+
final SocialProvider provider
35+
) {
36+
validateRequiredFields(socialId, provider);
37+
return new SocialMemberInfo(socialId, email, nickname, profileImage, provider);
38+
}
39+
40+
/**
41+
* 최소 정보로 SocialMemberInfo 생성 (socialId와 provider만 필수).
42+
*
43+
* @param socialId 소셜 제공자 고유 ID
44+
* @param provider 소셜 제공자
45+
* @return 최소 정보만 포함된 SocialMemberInfo 객체
46+
*/
47+
public static SocialMemberInfo withMinimalInfo(
48+
final String socialId,
49+
final SocialProvider provider
50+
) {
51+
return of(socialId, null, null, null, provider);
52+
}
53+
54+
/**
55+
* 카카오 API 응답으로부터 SocialMemberInfo 생성.
56+
*
57+
* @param attributes 카카오 API 응답 Map
58+
* @return 카카오 회원 정보가 파싱된 SocialMemberInfo 객체
59+
* @throws IllegalArgumentException 필수 정보가 누락된 경우
60+
*/
61+
public static SocialMemberInfo fromKakaoAttributes(final java.util.Map<String, Object> attributes) {
62+
if (attributes == null) {
63+
throw new IllegalArgumentException("카카오 회원 정보가 없습니다");
64+
}
65+
66+
final Object idObj = attributes.get("id");
67+
if (idObj == null) {
68+
throw new IllegalArgumentException("카카오 회원 ID가 없습니다");
69+
}
70+
final String socialId = String.valueOf(idObj);
71+
72+
@SuppressWarnings("unchecked")
73+
final java.util.Map<String, Object> kakaoAccount =
74+
(java.util.Map<String, Object>) attributes.get("kakao_account");
75+
final String email = kakaoAccount != null ? (String) kakaoAccount.get("email") : null;
76+
77+
@SuppressWarnings("unchecked")
78+
final java.util.Map<String, Object> profile = kakaoAccount != null ?
79+
(java.util.Map<String, Object>) kakaoAccount.get("profile") : null;
80+
final String nickname = profile != null ? (String) profile.get("nickname") : null;
81+
final String profileImage = profile != null ? (String) profile.get("profile_image_url") : null;
82+
83+
return of(socialId, email, nickname, profileImage, SocialProvider.KAKAO);
84+
}
85+
86+
/**
87+
* 구글 API 응답으로부터 SocialMemberInfo 생성.
88+
*
89+
* @param attributes 구글 API 응답 Map
90+
* @return 구글 회원 정보가 파싱된 SocialMemberInfo 객체
91+
* @throws IllegalArgumentException 필수 정보가 누락된 경우
92+
*/
93+
public static SocialMemberInfo fromGoogleAttributes(final java.util.Map<String, Object> attributes) {
94+
if (attributes == null) {
95+
throw new IllegalArgumentException("구글 회원 정보가 없습니다");
96+
}
97+
98+
final String socialId = (String) attributes.get("sub");
99+
if (socialId == null || socialId.isBlank()) {
100+
throw new IllegalArgumentException("구글 회원 ID가 없습니다");
101+
}
102+
103+
final String email = (String) attributes.get("email");
104+
final String nickname = (String) attributes.get("name");
105+
final String profileImage = (String) attributes.get("picture");
106+
107+
return of(socialId, email, nickname, profileImage, SocialProvider.GOOGLE);
108+
}
109+
110+
private static void validateRequiredFields(final String socialId, final SocialProvider provider) {
111+
if (socialId == null || socialId.isBlank()) {
112+
throw new IllegalArgumentException("소셜 ID는 필수입니다.");
113+
}
114+
if (provider == null) {
115+
throw new IllegalArgumentException("소셜 제공자는 필수입니다.");
116+
}
117+
}
118+
}

src/main/java/com/ururulab/ururu/global/auth/dto/KakaoOAuthRequest.java renamed to src/main/java/com/ururulab/ururu/global/auth/dto/request/KakaoOAuthRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.ururulab.ururu.global.auth.dto;
1+
package com.ururulab.ururu.global.auth.dto.request;
22

33
import jakarta.validation.constraints.NotBlank;
44

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.ururulab.ururu.global.auth.dto.request;
2+
3+
import com.ururulab.ururu.member.domain.entity.enumerated.SocialProvider;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.NotNull;
6+
7+
/**
8+
* 소셜 로그인 요청 DTO.
9+
*
10+
* <p>클라이언트로부터 받은 소셜 로그인 요청을 표준화된 형태로 처리합니다.</p>
11+
*/
12+
public record SocialLoginRequest(
13+
@NotNull(message = "소셜 제공자는 필수입니다")
14+
SocialProvider provider,
15+
16+
@NotBlank(message = "인증 코드는 필수입니다")
17+
String code,
18+
19+
String state // null 가능, CSRF 방지용 상태값 (선택적)
20+
) {
21+
22+
/**
23+
* SocialLoginRequest 생성을 위한 정적 팩토리 메서드.
24+
*
25+
* @param provider 소셜 제공자
26+
* @param code 인증 코드
27+
* @param state CSRF 방지용 상태값 (null 가능)
28+
* @return 검증된 SocialLoginRequest 객체
29+
*/
30+
public static SocialLoginRequest of(
31+
final SocialProvider provider,
32+
final String code,
33+
final String state
34+
) {
35+
return new SocialLoginRequest(provider, code, state);
36+
}
37+
38+
/**
39+
* state 없이 요청 생성 (일부 제공자에서 선택적).
40+
*
41+
* @param provider 소셜 제공자
42+
* @param code 인증 코드
43+
* @return state가 null인 SocialLoginRequest 객체
44+
*/
45+
public static SocialLoginRequest withoutState(
46+
final SocialProvider provider,
47+
final String code
48+
) {
49+
return of(provider, code, null);
50+
}
51+
}

src/main/java/com/ururulab/ururu/global/auth/dto/JwtTokenResponse.java renamed to src/main/java/com/ururulab/ururu/global/auth/dto/response/JwtTokenResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.ururulab.ururu.global.auth.dto;
1+
package com.ururulab.ururu.global.auth.dto.response;
22

33
import com.fasterxml.jackson.annotation.JsonProperty;
44

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.ururulab.ururu.global.auth.dto.response;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
/**
6+
* 소셜 로그인 응답 DTO.
7+
*
8+
* <p>소셜 로그인 성공 시 JWT 토큰과 회원 정보를 포함한 응답을 제공합니다.</p>
9+
*/
10+
public record SocialLoginResponse(
11+
@JsonProperty("access_token") String accessToken,
12+
@JsonProperty("refresh_token") String refreshToken,
13+
@JsonProperty("token_type") String tokenType,
14+
@JsonProperty("expires_in") Long expiresIn,
15+
@JsonProperty("member_info") MemberInfo memberInfo
16+
) {
17+
18+
private static final String BEARER_TYPE = "Bearer";
19+
20+
/**
21+
* 완전한 토큰 정보로 소셜 로그인 응답 생성 (이펙티브 자바 아이템 1).
22+
*
23+
* @param accessToken JWT 액세스 토큰
24+
* @param refreshToken JWT 리프레시 토큰 (null 가능)
25+
* @param expiresIn 토큰 만료 시간 (초)
26+
* @param memberInfo 회원 정보
27+
* @return 소셜 로그인 응답
28+
* @throws IllegalArgumentException 필수 파라미터가 유효하지 않은 경우
29+
*/
30+
public static SocialLoginResponse of(
31+
final String accessToken,
32+
final String refreshToken,
33+
final Long expiresIn,
34+
final MemberInfo memberInfo
35+
) {
36+
validateAccessToken(accessToken);
37+
validateExpiresIn(expiresIn);
38+
validateMemberInfo(memberInfo);
39+
40+
return new SocialLoginResponse(accessToken, refreshToken, BEARER_TYPE, expiresIn, memberInfo);
41+
}
42+
43+
/**
44+
* 액세스 토큰만으로 응답 생성 (리프레시 토큰 없는 경우).
45+
*
46+
* @param accessToken JWT 액세스 토큰
47+
* @param expiresIn 토큰 만료 시간 (초)
48+
* @param memberInfo 회원 정보
49+
* @return 액세스 토큰만 포함된 소셜 로그인 응답
50+
*/
51+
public static SocialLoginResponse withAccessTokenOnly(
52+
final String accessToken,
53+
final Long expiresIn,
54+
final MemberInfo memberInfo
55+
) {
56+
return of(accessToken, null, expiresIn, memberInfo);
57+
}
58+
59+
private static void validateAccessToken(final String accessToken) {
60+
if (accessToken == null || accessToken.isBlank()) {
61+
throw new IllegalArgumentException("액세스 토큰은 필수입니다.");
62+
}
63+
}
64+
65+
private static void validateExpiresIn(final Long expiresIn) {
66+
if (expiresIn == null || expiresIn <= 0) {
67+
throw new IllegalArgumentException("만료 시간은 0보다 커야 합니다.");
68+
}
69+
}
70+
71+
private static void validateMemberInfo(final MemberInfo memberInfo) {
72+
if (memberInfo == null) {
73+
throw new IllegalArgumentException("회원 정보는 필수입니다.");
74+
}
75+
}
76+
77+
/**
78+
* 응답에 포함될 회원 정보 DTO (user → member 변경 반영).
79+
*/
80+
public record MemberInfo(
81+
@JsonProperty("member_id") Long memberId,
82+
String email,
83+
String nickname,
84+
@JsonProperty("profile_image") String profileImage
85+
) {
86+
87+
/**
88+
* 완전한 회원 정보로 MemberInfo 생성.
89+
*
90+
* @param memberId 회원 ID
91+
* @param email 이메일 (null 가능)
92+
* @param nickname 닉네임 (null 가능)
93+
* @param profileImage 프로필 이미지 URL (null 가능)
94+
* @return MemberInfo 객체
95+
* @throws IllegalArgumentException memberId가 null인 경우
96+
*/
97+
public static MemberInfo of(
98+
final Long memberId,
99+
final String email,
100+
final String nickname,
101+
final String profileImage
102+
) {
103+
if (memberId == null) {
104+
throw new IllegalArgumentException("회원 ID는 필수입니다.");
105+
}
106+
return new MemberInfo(memberId, email, nickname, profileImage);
107+
}
108+
109+
/**
110+
* 최소 정보로 MemberInfo 생성 (memberId만 필수).
111+
*
112+
* @param memberId 회원 ID
113+
* @return 최소 정보만 포함된 MemberInfo 객체
114+
*/
115+
public static MemberInfo withMinimalInfo(final Long memberId) {
116+
return of(memberId, null, null, null);
117+
}
118+
}
119+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.ururulab.ururu.global.auth.exception;
2+
3+
/**
4+
* 소셜 로그인 관련 최상위 예외 클래스.
5+
*/
6+
public class SocialLoginException extends RuntimeException {
7+
8+
public SocialLoginException(final String message) {
9+
super(message);
10+
}
11+
12+
public SocialLoginException(final String message, final Throwable cause) {
13+
super(message, cause);
14+
}
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ururulab.ururu.global.auth.exception;
2+
3+
/**
4+
* 소셜 회원 정보 조회 실패 예외.
5+
*
6+
* <p>액세스 토큰을 통해 회원 정보를 조회하는 과정에서 실패할 때 발생합니다.</p>
7+
*/
8+
public final class SocialMemberInfoException extends SocialLoginException {
9+
10+
public SocialMemberInfoException(final String message) {
11+
super(message);
12+
}
13+
14+
public SocialMemberInfoException(final String message, final Throwable cause) {
15+
super(message, cause);
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ururulab.ururu.global.auth.exception;
2+
3+
/**
4+
* 소셜 토큰 교환 실패 예외.
5+
*
6+
* <p>인증 코드를 액세스 토큰으로 교환하는 과정에서 실패할 때 발생합니다.</p>
7+
*/
8+
public final class SocialTokenExchangeException extends SocialLoginException {
9+
10+
public SocialTokenExchangeException(final String message) {
11+
super(message);
12+
}
13+
14+
public SocialTokenExchangeException(final String message, final Throwable cause) {
15+
super(message, cause);
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.ururulab.ururu.global.auth.exception;
2+
3+
/**
4+
* 지원하지 않는 소셜 제공자 예외.
5+
*/
6+
public final class UnsupportedSocialProviderException extends SocialLoginException {
7+
8+
public UnsupportedSocialProviderException(final String message) {
9+
super(message);
10+
}
11+
}

0 commit comments

Comments
 (0)