|
2 | 2 |
|
3 | 3 | import com.example.booklog.domain.users.entity.AuthAccounts; |
4 | 4 | import com.example.booklog.domain.users.entity.AuthProvider; |
| 5 | +import com.example.booklog.domain.users.entity.UserSettings; |
| 6 | +import com.example.booklog.domain.users.entity.UserStatus; |
| 7 | +import com.example.booklog.domain.users.entity.Users; |
| 8 | +import com.example.booklog.domain.users.repository.UserSettingsRepository; |
| 9 | +import com.example.booklog.domain.users.repository.UsersRepository; |
5 | 10 | import com.example.booklog.global.auth.entity.RefreshToken; |
6 | 11 | import com.example.booklog.global.auth.repository.AuthAccountsRepository; |
7 | 12 | import com.example.booklog.global.auth.repository.RefreshTokenRepository; |
8 | 13 | import com.example.booklog.global.auth.converter.AuthConverter; |
9 | 14 | import com.example.booklog.global.auth.dto.AuthReqDTO; |
10 | 15 | import com.example.booklog.global.auth.dto.AuthResDTO; |
| 16 | +import com.example.booklog.global.auth.enums.Role; |
11 | 17 | import com.example.booklog.global.auth.exception.AuthErrorCode; |
12 | 18 | import com.example.booklog.global.auth.exception.AuthException; |
13 | 19 | import com.example.booklog.global.auth.security.CustomUserDetails; |
14 | 20 | import com.example.booklog.global.auth.security.JwtUtil; |
15 | 21 | import jakarta.validation.Valid; |
16 | 22 | import lombok.RequiredArgsConstructor; |
| 23 | +import lombok.extern.slf4j.Slf4j; |
| 24 | +import org.springframework.http.*; |
17 | 25 | import org.springframework.security.crypto.password.PasswordEncoder; |
18 | 26 | import org.springframework.stereotype.Service; |
19 | 27 | import org.springframework.transaction.annotation.Transactional; |
| 28 | +import org.springframework.web.client.RestTemplate; |
20 | 29 |
|
| 30 | +import java.util.Map; |
| 31 | + |
| 32 | +@Slf4j |
21 | 33 | @Service |
22 | 34 | @RequiredArgsConstructor |
23 | 35 | public class AuthQueryServiceImpl implements AuthQueryService { |
24 | 36 |
|
25 | 37 | private final AuthAccountsRepository authAccountsRepository; |
26 | 38 | private final RefreshTokenRepository refreshTokenRepository; |
| 39 | + private final UsersRepository usersRepository; |
| 40 | + private final UserSettingsRepository userSettingsRepository; |
27 | 41 | private final JwtUtil jwtUtil; |
28 | 42 | private final PasswordEncoder encoder; |
| 43 | + private final RestTemplate restTemplate = new RestTemplate(); |
29 | 44 |
|
30 | 45 | @Override |
31 | 46 | @Transactional |
@@ -107,4 +122,143 @@ public AuthResDTO.LoginDTO refreshToken(AuthReqDTO.@Valid RefreshTokenDTO dto) { |
107 | 122 | // 9. 응답 DTO 반환 |
108 | 123 | return AuthConverter.toLoginDTO(account, newAccessToken, newRefreshToken, jwtUtil.getAccessExpirationMillis() / 1000); |
109 | 124 | } |
| 125 | + |
| 126 | + @Override |
| 127 | + @Transactional |
| 128 | + public AuthResDTO.LoginDTO kakaoLogin(AuthReqDTO.@Valid KakaoLoginDTO dto) { |
| 129 | + log.info("=== 카카오 로그인 API 호출 시작 ==="); |
| 130 | + log.info("받은 카카오 토큰 (앞 20자): {}", dto.kakaoAccessToken().substring(0, Math.min(20, dto.kakaoAccessToken().length()))); |
| 131 | + |
| 132 | + // 1. 카카오 API로 사용자 정보 조회 |
| 133 | + String kakaoUserInfoUrl = "https://kapi.kakao.com/v2/user/me"; |
| 134 | + |
| 135 | + HttpHeaders headers = new HttpHeaders(); |
| 136 | + headers.setBearerAuth(dto.kakaoAccessToken()); |
| 137 | + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| 138 | + |
| 139 | + HttpEntity<String> entity = new HttpEntity<>(headers); |
| 140 | + |
| 141 | + ResponseEntity<Map<String, Object>> response; |
| 142 | + try { |
| 143 | + log.info("카카오 API 호출 시도: {}", kakaoUserInfoUrl); |
| 144 | + @SuppressWarnings("unchecked") |
| 145 | + ResponseEntity<Map<String, Object>> temp = (ResponseEntity<Map<String, Object>>) (ResponseEntity<?>) restTemplate.exchange(kakaoUserInfoUrl, HttpMethod.GET, entity, Map.class); |
| 146 | + response = temp; |
| 147 | + |
| 148 | + log.info("카카오 API 응답 상태: {}", response.getStatusCode()); |
| 149 | + |
| 150 | + // 응답 상태 코드 확인 |
| 151 | + if (!response.getStatusCode().is2xxSuccessful()) { |
| 152 | + log.error("카카오 API 호출 실패: HTTP Status {}, Body: {}", response.getStatusCode(), response.getBody()); |
| 153 | + throw new AuthException(AuthErrorCode.KAKAO_API_ERROR); |
| 154 | + } |
| 155 | + } catch (AuthException e) { |
| 156 | + throw e; |
| 157 | + } catch (org.springframework.web.client.HttpClientErrorException e) { |
| 158 | + log.error("카카오 API 호출 실패 (HTTP 에러): Status={}, Message={}, Body={}", |
| 159 | + e.getStatusCode(), e.getMessage(), e.getResponseBodyAsString()); |
| 160 | + throw new AuthException(AuthErrorCode.KAKAO_TOKEN_INVALID); |
| 161 | + } catch (Exception e) { |
| 162 | + log.error("카카오 API 호출 실패 (예외): Type={}, Message={}", e.getClass().getSimpleName(), e.getMessage(), e); |
| 163 | + throw new AuthException(AuthErrorCode.KAKAO_TOKEN_INVALID); |
| 164 | + } |
| 165 | + |
| 166 | + Map<String, Object> attributes = response.getBody(); |
| 167 | + if (attributes == null || attributes.isEmpty()) { |
| 168 | + log.error("카카오 API 응답이 비어있습니다."); |
| 169 | + throw new AuthException(AuthErrorCode.KAKAO_API_ERROR); |
| 170 | + } |
| 171 | + |
| 172 | + log.info("카카오 API 응답 데이터: {}", attributes); |
| 173 | + |
| 174 | + // 2. 카카오 사용자 정보 파싱 |
| 175 | + String providerId = String.valueOf(attributes.get("id")); |
| 176 | + @SuppressWarnings("unchecked") |
| 177 | + Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account"); |
| 178 | + @SuppressWarnings("unchecked") |
| 179 | + Map<String, Object> profile = kakaoAccount != null ? (Map<String, Object>) kakaoAccount.get("profile") : null; |
| 180 | + |
| 181 | + String email = kakaoAccount != null ? (String) kakaoAccount.get("email") : null; |
| 182 | + String nickname = profile != null ? (String) profile.get("nickname") : null; |
| 183 | + String profileImageUrl = profile != null ? (String) profile.get("profile_image_url") : null; |
| 184 | + |
| 185 | + log.info("카카오 로그인 시도: providerId={}, email={}, nickname={}", providerId, email, nickname); |
| 186 | + |
| 187 | + if (email == null || email.isEmpty()) { |
| 188 | + log.error("카카오 계정에 이메일 정보가 없습니다. providerId={}", providerId); |
| 189 | + throw new AuthException(AuthErrorCode.KAKAO_API_ERROR); |
| 190 | + } |
| 191 | + |
| 192 | + // ...existing code... |
| 193 | + |
| 194 | + // 3. 기존 계정 조회 또는 신규 생성 |
| 195 | + AuthAccounts authAccount = authAccountsRepository |
| 196 | + .findByProviderIdAndProvider(providerId, AuthProvider.KAKAO) |
| 197 | + .orElseGet(() -> createKakaoAccount(providerId, email, nickname, profileImageUrl)); |
| 198 | + |
| 199 | + // 4. 기존 계정이면 마지막 로그인 시간 및 프로필 업데이트 |
| 200 | + authAccount.updateLastLogin(); |
| 201 | + authAccount.updateProfile(email, nickname, profileImageUrl); |
| 202 | + authAccountsRepository.save(authAccount); |
| 203 | + |
| 204 | + // 5. CustomUserDetails 생성 |
| 205 | + CustomUserDetails userDetails = new CustomUserDetails(authAccount); |
| 206 | + |
| 207 | + // 6. JWT 액세스 토큰 발급 |
| 208 | + String accessToken = jwtUtil.createAccessToken(userDetails); |
| 209 | + |
| 210 | + // 7. JWT 리프레시 토큰 발급 |
| 211 | + String refreshToken = jwtUtil.createRefreshToken(userDetails); |
| 212 | + |
| 213 | + // 8. 기존 RefreshToken 삭제 후 새로 저장 |
| 214 | + refreshTokenRepository.findByEmail(email) |
| 215 | + .ifPresent(refreshTokenRepository::delete); |
| 216 | + |
| 217 | + RefreshToken newRefreshToken = RefreshToken.builder() |
| 218 | + .token(refreshToken) |
| 219 | + .email(email) |
| 220 | + .expiryDate(java.time.LocalDateTime.now().plus(jwtUtil.getRefreshExpiration())) |
| 221 | + .build(); |
| 222 | + refreshTokenRepository.save(newRefreshToken); |
| 223 | + |
| 224 | + // 9. 응답 DTO 반환 |
| 225 | + return AuthConverter.toLoginDTO(authAccount, accessToken, refreshToken, jwtUtil.getAccessExpirationMillis() / 1000); |
| 226 | + } |
| 227 | + |
| 228 | + /** |
| 229 | + * 카카오 신규 계정 생성 |
| 230 | + */ |
| 231 | + private AuthAccounts createKakaoAccount(String providerId, String email, String nickname, String profileImageUrl) { |
| 232 | + log.info("신규 카카오 사용자 등록: providerId={}", providerId); |
| 233 | + |
| 234 | + // Users 엔티티 생성 |
| 235 | + Users newUser = Users.builder() |
| 236 | + .nickname(nickname) |
| 237 | + .profileImageUrl(profileImageUrl) |
| 238 | + .status(UserStatus.ACTIVE) |
| 239 | + .build(); |
| 240 | + usersRepository.save(newUser); |
| 241 | + |
| 242 | + // AuthAccounts 생성 |
| 243 | + AuthAccounts newAuthAccount = AuthAccounts.builder() |
| 244 | + .user(newUser) |
| 245 | + .provider(AuthProvider.KAKAO) |
| 246 | + .providerId(providerId) |
| 247 | + .email(email) |
| 248 | + .displayName(nickname) |
| 249 | + .profileImageUrl(profileImageUrl) |
| 250 | + .role(Role.ROLE_USER) |
| 251 | + .build(); |
| 252 | + authAccountsRepository.save(newAuthAccount); |
| 253 | + |
| 254 | + // UserSettings 생성 (서재/북로그 공개 설정 기본값: true) |
| 255 | + UserSettings userSettings = UserSettings.builder() |
| 256 | + .user(newUser) |
| 257 | + .isShelfPublic(true) |
| 258 | + .isPostPublic(true) |
| 259 | + .build(); |
| 260 | + userSettingsRepository.save(userSettings); |
| 261 | + |
| 262 | + return newAuthAccount; |
| 263 | + } |
110 | 264 | } |
0 commit comments