Skip to content

Commit 9abab3f

Browse files
committed
feat[OAuth]: 소셜 간편 로그인 구현 및 토큰 생성, 검증 로직 수정
1 parent c2b2ef4 commit 9abab3f

18 files changed

+666
-540
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.ai.lawyer.domain.auth.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class OAuth2LoginResponse {
9+
private boolean success;
10+
private String message;
11+
}

backend/src/main/java/com/ai/lawyer/domain/member/controller/MemberController.java

Lines changed: 132 additions & 261 deletions
Large diffs are not rendered by default.
Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
package com.ai.lawyer.domain.member.dto;
22

3-
import lombok.AllArgsConstructor;
4-
import lombok.Getter;
5-
63
import java.time.LocalDateTime;
74

8-
@Getter
9-
@AllArgsConstructor
10-
public class MemberErrorResponse {
11-
private final String message;
12-
private final int status;
13-
private final String error;
14-
private final LocalDateTime timestamp;
15-
5+
public record MemberErrorResponse(
6+
String message,
7+
int status,
8+
String error,
9+
LocalDateTime timestamp
10+
) {
1611
public static MemberErrorResponse of(String message, int status, String error) {
1712
return new MemberErrorResponse(message, status, error, LocalDateTime.now());
1813
}
19-
}
14+
}

backend/src/main/java/com/ai/lawyer/domain/member/dto/MemberResponse.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ public static MemberResponse from(Member member) {
3939
}
4040

4141
public static MemberResponse from(MemberAdapter memberAdapter) {
42-
if (memberAdapter instanceof Member) {
43-
return from((Member) memberAdapter);
44-
} else if (memberAdapter instanceof OAuth2Member oauth2Member) {
45-
return MemberResponse.builder()
42+
return switch (memberAdapter) {
43+
case null -> throw new IllegalArgumentException("MemberAdapter cannot be null");
44+
case Member member -> from(member);
45+
case OAuth2Member oauth2Member -> MemberResponse.builder()
4646
.memberId(oauth2Member.getMemberId())
4747
.loginId(oauth2Member.getLoginId())
4848
.email(oauth2Member.getEmail()) // OAuth2Member의 email 컬럼
@@ -53,7 +53,8 @@ public static MemberResponse from(MemberAdapter memberAdapter) {
5353
.createdAt(oauth2Member.getCreatedAt())
5454
.updatedAt(oauth2Member.getUpdatedAt())
5555
.build();
56-
}
57-
throw new IllegalArgumentException("Unsupported member type");
56+
default ->
57+
throw new IllegalArgumentException("Unsupported member type: " + memberAdapter.getClass().getName());
58+
};
5859
}
59-
}
60+
}

backend/src/main/java/com/ai/lawyer/domain/member/entity/Member.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@Entity
1212
@Table(name = "member",
1313
indexes = {
14-
@Index(name = "idx_member_loginid", columnList = "loginid")
14+
@Index(name = "idx_member_login_id", columnList = "login_id")
1515
})
1616
@Getter
1717
@Setter
@@ -27,7 +27,7 @@ public class Member implements MemberAdapter {
2727
@Column(name = "member_id", nullable = false)
2828
private Long memberId;
2929

30-
@Column(name = "loginid", nullable = false, unique = true, length = 100)
30+
@Column(name = "login_id", nullable = false, unique = true, length = 100)
3131
@Email(message = "올바른 이메일 형식이 아닙니다")
3232
@NotBlank(message = "이메일(로그인 ID)은 필수입니다")
3333
private String loginId;

backend/src/main/java/com/ai/lawyer/domain/member/entity/OAuth2Member.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@Entity
1212
@Table(name = "oauth2_member",
1313
indexes = {
14-
@Index(name = "idx_oauth2_member_loginid", columnList = "loginid"),
14+
@Index(name = "idx_oauth2_member_login_id", columnList = "login_id"),
1515
@Index(name = "idx_oauth2_member_provider", columnList = "provider, provider_id")
1616
})
1717
@Getter
@@ -20,15 +20,15 @@
2020
@AllArgsConstructor
2121
@Builder
2222
@ToString
23-
@EqualsAndHashCode(of = "memberId")
23+
@EqualsAndHashCode()
2424
public class OAuth2Member implements MemberAdapter {
2525

2626
@Id
2727
@GeneratedValue(strategy = GenerationType.IDENTITY)
2828
@Column(name = "member_id", nullable = false)
2929
private Long memberId;
3030

31-
@Column(name = "loginid", nullable = false, unique = true, length = 100)
31+
@Column(name = "login_id", nullable = false, unique = true, length = 100)
3232
@Email(message = "올바른 이메일 형식이 아닙니다")
3333
@NotBlank(message = "이메일(로그인 ID)은 필수입니다")
3434
private String loginId;
@@ -80,8 +80,4 @@ public enum Provider {
8080
private final String description;
8181
Provider(String description) { this.description = description; }
8282
}
83-
84-
public String getProvider() {
85-
return provider != null ? provider.name() : null;
86-
}
8783
}

backend/src/main/java/com/ai/lawyer/domain/member/service/MemberService.java

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,53 @@
99
import com.ai.lawyer.global.jwt.CookieUtil;
1010
import com.ai.lawyer.global.email.service.EmailService;
1111
import com.ai.lawyer.global.email.service.EmailAuthService;
12-
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
1313
import org.springframework.security.crypto.password.PasswordEncoder;
1414
import org.springframework.stereotype.Service;
1515
import org.springframework.transaction.annotation.Transactional;
1616
import jakarta.servlet.http.HttpServletResponse;
1717

18+
@Slf4j
1819
@Service
19-
@RequiredArgsConstructor
2020
@Transactional(readOnly = true)
2121
public class MemberService {
2222

2323
private final MemberRepository memberRepository;
24-
private final OAuth2MemberRepository oauth2MemberRepository;
24+
private OAuth2MemberRepository oauth2MemberRepository;
2525
private final PasswordEncoder passwordEncoder;
2626
private final TokenProvider tokenProvider;
2727
private final CookieUtil cookieUtil;
2828
private final EmailService emailService;
2929
private final EmailAuthService emailAuthService;
3030

31+
public MemberService(
32+
MemberRepository memberRepository,
33+
PasswordEncoder passwordEncoder,
34+
TokenProvider tokenProvider,
35+
CookieUtil cookieUtil,
36+
EmailService emailService,
37+
EmailAuthService emailAuthService) {
38+
this.memberRepository = memberRepository;
39+
this.passwordEncoder = passwordEncoder;
40+
this.tokenProvider = tokenProvider;
41+
this.cookieUtil = cookieUtil;
42+
this.emailService = emailService;
43+
this.emailAuthService = emailAuthService;
44+
}
45+
46+
@org.springframework.beans.factory.annotation.Autowired(required = false)
47+
public void setOauth2MemberRepository(OAuth2MemberRepository oauth2MemberRepository) {
48+
this.oauth2MemberRepository = oauth2MemberRepository;
49+
}
50+
51+
// 에러 메시지 상수
52+
private static final String ERR_DUPLICATE_EMAIL = "이미 존재하는 이메일입니다.";
53+
private static final String ERR_MEMBER_NOT_FOUND = "존재하지 않는 회원입니다.";
54+
private static final String ERR_PASSWORD_MISMATCH = "비밀번호가 일치하지 않습니다.";
55+
private static final String ERR_INVALID_REFRESH_TOKEN = "유효하지 않은 리프레시 토큰입니다.";
56+
private static final String ERR_MEMBER_NOT_FOUND_BY_LOGIN_ID = "해당 로그인 ID의 회원이 없습니다.";
57+
private static final String ERR_EMAIL_VERIFICATION_REQUIRED = "이메일 인증을 완료해야 비밀번호를 재설정할 수 있습니다.";
58+
3159
@Transactional
3260
public MemberResponse signup(MemberSignupRequest request, HttpServletResponse response) {
3361
validateDuplicateLoginId(request.getLoginId());
@@ -52,10 +80,10 @@ public MemberResponse signup(MemberSignupRequest request, HttpServletResponse re
5280

5381
public MemberResponse login(MemberLoginRequest request, HttpServletResponse response) {
5482
Member member = memberRepository.findByLoginId(request.getLoginId())
55-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
83+
.orElseThrow(() -> new IllegalArgumentException(ERR_MEMBER_NOT_FOUND));
5684

5785
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
58-
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
86+
throw new IllegalArgumentException(ERR_PASSWORD_MISMATCH);
5987
}
6088

6189
String accessToken = tokenProvider.generateAccessToken(member);
@@ -74,74 +102,96 @@ public void logout(String loginId, HttpServletResponse response) {
74102
}
75103

76104
public MemberResponse refreshToken(String refreshToken, HttpServletResponse response) {
105+
log.debug("토큰 재발급 시작: refreshToken={}", refreshToken.substring(0, Math.min(10, refreshToken.length())) + "...");
106+
107+
// 1. 리프레시 토큰으로 loginId 찾기
77108
String loginId = tokenProvider.findUsernameByRefreshToken(refreshToken);
78109
if (loginId == null) {
79-
throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
110+
log.warn("Redis에서 리프레시 토큰을 찾을 수 없습니다.");
111+
throw new IllegalArgumentException(ERR_INVALID_REFRESH_TOKEN);
80112
}
113+
log.debug("리프레시 토큰으로 찾은 loginId: {}", loginId);
81114

115+
// 2. 리프레시 토큰 검증
82116
if (!tokenProvider.validateRefreshToken(loginId, refreshToken)) {
83-
throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
117+
log.warn("리프레시 토큰 검증 실패: loginId={}", loginId);
118+
throw new IllegalArgumentException(ERR_INVALID_REFRESH_TOKEN);
84119
}
85120

86-
// Member 또는 OAuth2Member 조회
87-
com.ai.lawyer.domain.member.entity.MemberAdapter member = memberRepository.findByLoginId(loginId)
88-
.map(m -> (com.ai.lawyer.domain.member.entity.MemberAdapter) m)
89-
.orElse(oauth2MemberRepository.findByLoginId(loginId).orElse(null));
121+
// 3. Member 또는 OAuth2Member 조회
122+
com.ai.lawyer.domain.member.entity.MemberAdapter member = memberRepository.findByLoginId(loginId).orElse(null);
123+
124+
if (member != null) {
125+
log.info("로컬 회원 찾음: loginId={}, memberId={}", loginId, member.getMemberId());
126+
} else if (oauth2MemberRepository != null) {
127+
member = oauth2MemberRepository.findByLoginId(loginId).orElse(null);
128+
if (member != null) {
129+
log.info("OAuth2 회원 찾음: loginId={}, memberId={}", loginId, member.getMemberId());
130+
}
131+
}
90132

91133
if (member == null) {
92-
throw new IllegalArgumentException("존재하지 않는 회원입니다.");
134+
log.error("회원을 찾을 수 없습니다: loginId={}", loginId);
135+
throw new IllegalArgumentException(ERR_MEMBER_NOT_FOUND);
93136
}
94137

138+
// 4. 기존 토큰 삭제
95139
tokenProvider.deleteAllTokens(loginId);
140+
log.debug("기존 토큰 삭제 완료: loginId={}", loginId);
96141

142+
// 5. 새 토큰 생성
97143
String newAccessToken = tokenProvider.generateAccessToken(member);
98144
String newRefreshToken = tokenProvider.generateRefreshToken(member);
145+
log.debug("새 토큰 생성 완료: loginId={}", loginId);
99146

147+
// 6. 쿠키 설정
100148
cookieUtil.setTokenCookies(response, newAccessToken, newRefreshToken);
149+
log.info("토큰 재발급 성공: loginId={}, memberId={}, memberType={}",
150+
loginId, member.getMemberId(), member.getClass().getSimpleName());
101151

102152
return MemberResponse.from(member);
103153
}
104154

105155
@Transactional
106156
public void withdraw(Long memberId) {
107157
Member member = memberRepository.findById(memberId)
108-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
158+
.orElseThrow(() -> new IllegalArgumentException(ERR_MEMBER_NOT_FOUND));
109159

110160
memberRepository.delete(member);
111161
}
112162

113163
public MemberResponse getMemberById(Long memberId) {
114164
// Member 또는 OAuth2Member 조회
115-
com.ai.lawyer.domain.member.entity.MemberAdapter member = memberRepository.findById(memberId)
116-
.map(m -> (com.ai.lawyer.domain.member.entity.MemberAdapter) m)
117-
.orElse(oauth2MemberRepository.findById(memberId)
118-
.map(m -> (com.ai.lawyer.domain.member.entity.MemberAdapter) m)
119-
.orElse(null));
165+
com.ai.lawyer.domain.member.entity.MemberAdapter member = memberRepository.findById(memberId).orElse(null);
166+
167+
if (member == null && oauth2MemberRepository != null) {
168+
member = oauth2MemberRepository.findById(memberId).orElse(null);
169+
}
120170

121171
if (member == null) {
122-
throw new IllegalArgumentException("존재하지 않는 회원입니다.");
172+
throw new IllegalArgumentException(ERR_MEMBER_NOT_FOUND);
123173
}
124174

125175
return MemberResponse.from(member);
126176
}
127177

128178
public void sendCodeToEmailByLoginId(String loginId) {
129179
Member member = memberRepository.findByLoginId(loginId)
130-
.orElseThrow(() -> new IllegalArgumentException("해당 로그인 ID의 회원이 없습니다."));
180+
.orElseThrow(() -> new IllegalArgumentException(ERR_MEMBER_NOT_FOUND_BY_LOGIN_ID));
131181
String email = member.getLoginId();
132182
emailService.sendVerificationCode(email, loginId);
133183
}
134184

135185
public boolean verifyAuthCode(String loginId, String verificationCode) {
136186
memberRepository.findByLoginId(loginId)
137-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
187+
.orElseThrow(() -> new IllegalArgumentException(ERR_MEMBER_NOT_FOUND));
138188

139189
return emailAuthService.verifyAuthCode(loginId, verificationCode);
140190
}
141191

142192
public boolean verifyPassword(String loginId, String password) {
143193
Member member = memberRepository.findByLoginId(loginId)
144-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
194+
.orElseThrow(() -> new IllegalArgumentException(ERR_MEMBER_NOT_FOUND));
145195

146196
boolean isValid = passwordEncoder.matches(password, member.getPassword());
147197

@@ -156,17 +206,17 @@ public boolean verifyPassword(String loginId, String password) {
156206
@Transactional
157207
public void resetPassword(String loginId, String newPassword, Boolean success) {
158208
Member member = memberRepository.findByLoginId(loginId)
159-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
209+
.orElseThrow(() -> new IllegalArgumentException(ERR_MEMBER_NOT_FOUND));
160210

161211
boolean clientSuccess = Boolean.TRUE.equals(success);
162212

163213
if (!clientSuccess) {
164-
throw new IllegalArgumentException("이메일 인증을 완료해야 비밀번호를 재설정할 수 있습니다.");
214+
throw new IllegalArgumentException(ERR_EMAIL_VERIFICATION_REQUIRED);
165215
}
166216

167217
boolean redisVerified = emailAuthService.isEmailVerified(loginId);
168218
if (!redisVerified) {
169-
throw new IllegalArgumentException("이메일 인증을 완료해야 비밀번호를 재설정할 수 있습니다.");
219+
throw new IllegalArgumentException(ERR_EMAIL_VERIFICATION_REQUIRED);
170220
}
171221

172222
String encodedPassword = passwordEncoder.encode(newPassword);
@@ -184,6 +234,10 @@ public String extractLoginIdFromToken(String token) {
184234

185235
@Transactional
186236
public MemberResponse oauth2LoginTest(OAuth2LoginTestRequest request, HttpServletResponse response) {
237+
if (oauth2MemberRepository == null) {
238+
throw new IllegalStateException("OAuth2 기능이 비활성화되어 있습니다.");
239+
}
240+
187241
// 기존 OAuth2 회원 조회
188242
OAuth2Member oauth2Member = oauth2MemberRepository.findByLoginId(request.getEmail()).orElse(null);
189243

@@ -212,7 +266,7 @@ public MemberResponse oauth2LoginTest(OAuth2LoginTestRequest request, HttpServle
212266

213267
private void validateDuplicateLoginId(String loginId) {
214268
if (memberRepository.existsByLoginId(loginId)) {
215-
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
269+
throw new IllegalArgumentException(ERR_DUPLICATE_EMAIL);
216270
}
217271
}
218272
}

backend/src/main/java/com/ai/lawyer/global/config/EmbeddedRedisConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public void startRedis() {
5252
try {
5353
redisServer = RedisServer.builder()
5454
.port(redisPort)
55-
.setting("maxmemory 128M")
55+
.setting("max_memory 128M")
5656
.build();
5757

5858
if (!redisServer.isActive()) {

0 commit comments

Comments
 (0)