Skip to content

Commit dc3a59e

Browse files
committed
feat: 카카오 소셜 로그인 구현
1 parent 2696912 commit dc3a59e

4 files changed

Lines changed: 156 additions & 0 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/auth/controller/AuthController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,18 @@ public RsData<?> refresh() {
9090
String newAccessToken = tokenService.reissueAccessToken();
9191
return RsData.success("토큰 재발급 완료", newAccessToken);
9292
}
93+
94+
@Operation(summary = "카카오 소셜 로그인", description = "카카오 OAuth 인가 코드를 이용해 로그인/회원가입을 진행합니다.")
95+
@GetMapping("/login/kakao")
96+
public RsData<?> kakaoLogin(@RequestParam String code) {
97+
LoginResponse response = authService.kakaoLogin(code);
98+
return RsData.success("카카오 로그인 성공", response);
99+
}
100+
101+
@Operation(summary = "구글 소셜 로그인", description = "구글 OAuth 인가 코드를 이용해 로그인/회원가입을 진행합니다.")
102+
@GetMapping("/login/google")
103+
public RsData<?> googleLogin(@RequestParam String code) {
104+
LoginResponse response = authService.googleLogin(code);
105+
return RsData.success("구글 로그인 성공", response);
106+
}
93107
}

src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.back.web7_9_codecrete_be.domain.auth.service;
22

3+
import com.back.web7_9_codecrete_be.domain.auth.dto.kakao.KakaoUserInfo;
34
import com.back.web7_9_codecrete_be.domain.auth.dto.request.LoginRequest;
45
import com.back.web7_9_codecrete_be.domain.auth.dto.request.SignupRequest;
56
import com.back.web7_9_codecrete_be.domain.auth.dto.response.LoginResponse;
67
import com.back.web7_9_codecrete_be.domain.email.service.EmailService;
8+
import com.back.web7_9_codecrete_be.domain.users.entity.SocialType;
79
import com.back.web7_9_codecrete_be.domain.users.entity.User;
810
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
911
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
@@ -25,6 +27,7 @@ public class AuthService {
2527
private final PasswordEncoder passwordEncoder;
2628
private final EmailService emailService;
2729
private final TokenService tokenService;
30+
private final KakaoOAuthService kakaoOAuthService;
2831

2932
// 회원가입
3033
public void signUp(SignupRequest req) {
@@ -50,6 +53,8 @@ public void signUp(SignupRequest req) {
5053
.password(passwordEncoder.encode(req.getPassword()))
5154
.birth(LocalDate.parse(req.getBirth()))
5255
.profileImage(req.getProfileImage())
56+
.socialType(SocialType.LOCAL)
57+
.socialId(null)
5358
.build();
5459

5560
userRepository.save(user);
@@ -66,6 +71,10 @@ public LoginResponse login(LoginRequest req) {
6671
throw new BusinessException(UserErrorCode.USER_DELETED);
6772
}
6873

74+
if (user.getSocialType() != SocialType.LOCAL) {
75+
throw new BusinessException(AuthErrorCode.SOCIAL_USER_CANNOT_LOGIN);
76+
}
77+
6978
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
7079
throw new BusinessException(AuthErrorCode.INVALID_PASSWORD);
7180
}
@@ -119,4 +128,61 @@ private String generateTempPassword() {
119128
}
120129
return sb.toString();
121130
}
131+
132+
@Transactional
133+
public LoginResponse kakaoLogin(String code) {
134+
135+
// 1. 인가 코드 → 카카오 Access Token
136+
String kakaoAccessToken = kakaoOAuthService.getAccessToken(code);
137+
138+
// 2. Access Token → 사용자 정보
139+
KakaoUserInfo kakaoUserInfo = kakaoOAuthService.getUserInfo(kakaoAccessToken);
140+
141+
if (kakaoUserInfo.getEmail() == null) {
142+
throw new BusinessException(AuthErrorCode.SOCIAL_EMAIL_NOT_PROVIDED);
143+
}
144+
145+
// 3. 소셜 ID 기준 사용자 조회
146+
User user = userRepository
147+
.findBySocialTypeAndSocialId(
148+
SocialType.KAKAO,
149+
kakaoUserInfo.getSocialId()
150+
)
151+
.orElseGet(() -> registerKakaoUser(kakaoUserInfo));
152+
153+
// 4. 탈퇴 사용자 체크
154+
if (user.getIsDeleted()) {
155+
throw new BusinessException(UserErrorCode.USER_DELETED);
156+
}
157+
158+
// 5. 토큰 발급
159+
tokenService.issueTokens(user);
160+
161+
return new LoginResponse(user.getId(), user.getNickname());
162+
}
163+
164+
private User registerKakaoUser(KakaoUserInfo info) {
165+
166+
String nickname = info.getNickname();
167+
if (userRepository.existsByNickname(nickname)) {
168+
nickname = nickname + "_" + System.currentTimeMillis();
169+
}
170+
171+
User user = User.builder()
172+
.email(info.getEmail())
173+
.nickname(nickname)
174+
.password(null)
175+
.birth(null)
176+
.profileImage(info.getProfileImageUrl())
177+
.socialType(SocialType.KAKAO)
178+
.socialId(info.getSocialId())
179+
.build();
180+
181+
return userRepository.save(user);
182+
}
183+
184+
public LoginResponse googleLogin(String code) {
185+
// TODO: 구글 인가 코드 → 사용자 정보 → 로그인 처리
186+
throw new UnsupportedOperationException("구글 로그인 미구현");
187+
}
122188
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.service;
2+
3+
import com.back.web7_9_codecrete_be.domain.auth.dto.kakao.KakaoTokenResponse;
4+
import com.back.web7_9_codecrete_be.domain.auth.dto.kakao.KakaoUserInfo;
5+
import com.back.web7_9_codecrete_be.domain.auth.dto.kakao.KakaoUserResponse;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.http.*;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.util.LinkedMultiValueMap;
11+
import org.springframework.util.MultiValueMap;
12+
import org.springframework.web.client.RestTemplate;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class KakaoOAuthService {
17+
18+
@Value("${oauth.kakao.client-id}")
19+
private String clientId;
20+
21+
@Value("${oauth.kakao.redirect-uri}")
22+
private String redirectUri;
23+
24+
@Value("${oauth.kakao.client-secret}")
25+
private String clientSecret;
26+
27+
private static final String TOKEN_URL = "https://kauth.kakao.com/oauth/token";
28+
private static final String USER_INFO_URL = "https://kapi.kakao.com/v2/user/me";
29+
30+
private final RestTemplate restTemplate = new RestTemplate();
31+
32+
// 인가 코드 → 액세스 토큰
33+
public String getAccessToken(String code) {
34+
HttpHeaders headers = new HttpHeaders();
35+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
36+
37+
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
38+
params.add("grant_type", "authorization_code");
39+
params.add("client_id", clientId);
40+
params.add("client_secret", clientSecret);
41+
params.add("redirect_uri", redirectUri);
42+
params.add("code", code);
43+
44+
HttpEntity<MultiValueMap<String, String>> request =
45+
new HttpEntity<>(params, headers);
46+
47+
ResponseEntity<KakaoTokenResponse> response =
48+
restTemplate.postForEntity(
49+
TOKEN_URL,
50+
request,
51+
KakaoTokenResponse.class
52+
);
53+
54+
return response.getBody().getAccessToken();
55+
}
56+
57+
// 인가 코드 → 카카오 사용자 정보
58+
public KakaoUserInfo getUserInfo(String accessToken) {
59+
HttpHeaders headers = new HttpHeaders();
60+
headers.setBearerAuth(accessToken);
61+
62+
HttpEntity<Void> request = new HttpEntity<>(headers);
63+
64+
ResponseEntity<KakaoUserResponse> response =
65+
restTemplate.exchange(
66+
USER_INFO_URL,
67+
HttpMethod.GET,
68+
request,
69+
KakaoUserResponse.class
70+
);
71+
72+
return response.getBody().toUserInfo();
73+
}
74+
}

src/main/java/com/back/web7_9_codecrete_be/global/error/code/AuthErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public enum AuthErrorCode implements ErrorCode {
1717
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "A-110", "존재하지 않는 이메일입니다."),
1818
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "A-111", "비밀번호가 일치하지 않습니다."),
1919
USER_INACTIVE(HttpStatus.FORBIDDEN, "A-112", "현재 비활성화된 계정입니다."),
20+
SOCIAL_USER_CANNOT_LOGIN(HttpStatus.BAD_REQUEST, "A-113", "소셜 로그인 계정은 일반 로그인으로 로그인할 수 없습니다."),
21+
SOCIAL_EMAIL_NOT_PROVIDED(HttpStatus.BAD_REQUEST, "A-114", "소셜 계정에서 이메일 정보를 제공하지 않았습니다."),
2022

2123
// 권한 관련
2224
UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "A-120", "로그인이 필요합니다."),

0 commit comments

Comments
 (0)