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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ jobs:
KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }}
KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }}

GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }}

steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.google;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
public class GoogleTokenResponse {

@JsonProperty("access_token")
private String accessToken;

@JsonProperty("expires_in")
private int expiresIn;

@JsonProperty("token_type")
private String tokenType;

@JsonProperty("scope")
private String scope;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.google;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class GoogleUserInfo {
private String socialId;
private String email;
private String nickname;
private String profileImageUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.google;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
public class GoogleUserResponse {

private String id;
private String email;
private String name;

@JsonProperty("picture")
private String profileImageUrl;

public GoogleUserInfo toUserInfo() {
return new GoogleUserInfo(
id,
email,
name,
profileImageUrl
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.back.web7_9_codecrete_be.domain.auth.service;

import com.back.web7_9_codecrete_be.domain.auth.dto.google.GoogleUserInfo;
import com.back.web7_9_codecrete_be.domain.auth.dto.kakao.KakaoUserInfo;
import com.back.web7_9_codecrete_be.domain.auth.dto.request.LoginRequest;
import com.back.web7_9_codecrete_be.domain.auth.dto.request.SignupRequest;
Expand All @@ -8,6 +9,7 @@
import com.back.web7_9_codecrete_be.domain.users.entity.SocialType;
import com.back.web7_9_codecrete_be.domain.users.entity.User;
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
import com.back.web7_9_codecrete_be.domain.users.util.NicknameGenerator;
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
import com.back.web7_9_codecrete_be.global.error.code.UserErrorCode;
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
Expand All @@ -28,6 +30,8 @@ public class AuthService {
private final EmailService emailService;
private final TokenService tokenService;
private final KakaoOAuthService kakaoOAuthService;
private final GoogleOAuthService googleOAuthService;
private final NicknameGenerator nicknameGenerator;

// 회원가입
public void signUp(SignupRequest req) {
Expand Down Expand Up @@ -165,10 +169,7 @@ public LoginResponse kakaoLogin(String code) {

private User registerKakaoUser(KakaoUserInfo info) {

String nickname = info.getNickname();
if (userRepository.existsByNickname(nickname)) {
nickname = nickname + "_" + System.currentTimeMillis();
}
String nickname = nicknameGenerator.generate();

User user = User.builder()
.email(info.getEmail())
Expand All @@ -183,8 +184,52 @@ private User registerKakaoUser(KakaoUserInfo info) {
return userRepository.save(user);
}

@Transactional
public LoginResponse googleLogin(String code) {
// TODO: 구글 인가 코드 → 사용자 정보 → 로그인 처리
throw new UnsupportedOperationException("구글 로그인 미구현");

// 1. 인가 코드 → 구글 Access Token
String googleAccessToken = googleOAuthService.getAccessToken(code);

// 2. Access Token → 사용자 정보
GoogleUserInfo googleUserInfo = googleOAuthService.getUserInfo(googleAccessToken);

if (googleUserInfo.getEmail() == null) {
throw new BusinessException(AuthErrorCode.SOCIAL_EMAIL_NOT_PROVIDED);
}

// 3. 소셜 ID 기준 사용자 조회
User user = userRepository
.findBySocialTypeAndSocialId(
SocialType.GOOGLE,
googleUserInfo.getSocialId()
)
.orElseGet(() -> registerGoogleUser(googleUserInfo));

// 4. 탈퇴 사용자 체크
if (user.getIsDeleted()) {
throw new BusinessException(UserErrorCode.USER_DELETED);
}

// 5. 토큰 발급
tokenService.issueTokens(user);

return new LoginResponse(user.getId(), user.getNickname());
}

private User registerGoogleUser(GoogleUserInfo info) {

String nickname = nicknameGenerator.generate();

User user = User.builder()
.email(info.getEmail())
.nickname(nickname)
.password(null)
.birth(null)
.profileImage(info.getProfileImageUrl())
.socialType(SocialType.GOOGLE)
.socialId(info.getSocialId())
.build();

return userRepository.save(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.back.web7_9_codecrete_be.domain.auth.service;

import com.back.web7_9_codecrete_be.domain.auth.dto.google.GoogleTokenResponse;
import com.back.web7_9_codecrete_be.domain.auth.dto.google.GoogleUserInfo;
import com.back.web7_9_codecrete_be.domain.auth.dto.google.GoogleUserResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

@Service
@RequiredArgsConstructor
public class GoogleOAuthService {

@Value("${oauth.google.client-id}")
private String clientId;

@Value("${oauth.google.client-secret}")
private String clientSecret;

@Value("${oauth.google.redirect-uri}")
private String redirectUri;

private static final String TOKEN_URL = "https://oauth2.googleapis.com/token";
private static final String USER_INFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";

private final RestTemplate restTemplate = new RestTemplate();

// 인가 코드 → 액세스 토큰
public String getAccessToken(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("redirect_uri", redirectUri);
params.add("code", code);

HttpEntity<MultiValueMap<String, String>> request =
new HttpEntity<>(params, headers);

ResponseEntity<GoogleTokenResponse> response =
restTemplate.postForEntity(
TOKEN_URL,
request,
GoogleTokenResponse.class
);

return response.getBody().getAccessToken();
}

// 액세스 토큰 → 구글 사용자 정보
public GoogleUserInfo getUserInfo(String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);

HttpEntity<Void> request = new HttpEntity<>(headers);

ResponseEntity<GoogleUserResponse> response =
restTemplate.exchange(
USER_INFO_URL,
HttpMethod.GET,
request,
GoogleUserResponse.class
);

return response.getBody().toUserInfo();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.back.web7_9_codecrete_be.domain.users.util;

import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
import java.util.List;

@Component
@RequiredArgsConstructor
public class NicknameGenerator {

private final UserRepository userRepository;
private final SecureRandom random = new SecureRandom();

// 접두어 후보들
private static final List<String> PREFIXES = List.of(
"공연덕후",
"덕질러",
"콘덕",
"라이브덕후",
"공연러"
);

public String generate() {
while (true) {
String prefix = PREFIXES.get(random.nextInt(PREFIXES.size()));
String nickname = prefix + "_" + randomNumber6Digits();

if (!userRepository.existsByNickname(nickname)) {
return nickname;
}
}
}

private long randomNumber6Digits() {
// 100000 ~ 999999
return 100_000L + random.nextInt(900_000);
}
}
6 changes: 5 additions & 1 deletion src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ oauth:
kakao:
client-id: ${KAKAO_REST_API_KEY} # 카카오 REST API 키
client-secret: ${KAKAO_CLIENT_SECRET} # 카카오 Client Secret
redirect-uri: http://localhost:3000/oauth/kakao # 카카오 로그인 Redirect URI
redirect-uri: http://localhost:3000/oauth/kakao # 카카오 로그인 Redirect URI
google:
client-id: ${GOOGLE_CLIENT_ID} # 구글 클라이언트 아이디
client-secret: ${GOOGLE_CLIENT_SECRET} # 구글 클라이언트 시크릿
redirect-uri: http://localhost:3000/oauth/google # 구글 로그인 Redirect URI
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ oauth:
client-id: ${KAKAO_REST_API_KEY} # 카카오 REST API 키
client-secret: ${KAKAO_CLIENT_SECRET} # 카카오 Client Secret
redirect-uri: ${KAKAO_REDIRECT_URI} # 카카오 로그인 Redirect URI
google:
client-id: ${GOOGLE_CLIENT_ID} # 구글 클라이언트 아이디
client-secret: ${GOOGLE_CLIENT_SECRET} # 구글 클라이언트 시크릿
redirect-uri: ${GOOGLE_REDIRECT_URI} # 구글 로그인 Redirect URI