From 9fb8821de51923158bd381e1ba7092a074ac1cf5 Mon Sep 17 00:00:00 2001 From: larama-C Date: Wed, 17 Dec 2025 14:52:01 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 6 +++++- src/main/resources/application.yml | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 329002f0..4f3b62e7 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 74a888c1..7ea0b2db 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 \ No newline at end of file From 20f29f5f8094eacbb27327dfcb85341b9a353627 Mon Sep 17 00:00:00 2001 From: larama-C Date: Wed, 17 Dec 2025 14:52:21 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=83=9D=EC=84=B1=EA=B8=B0=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=83=9D=EC=84=B1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/users/util/NicknameGenerator.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/users/util/NicknameGenerator.java diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/users/util/NicknameGenerator.java b/src/main/java/com/back/web7_9_codecrete_be/domain/users/util/NicknameGenerator.java new file mode 100644 index 00000000..be2fdcd8 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/users/util/NicknameGenerator.java @@ -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 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); + } +} From b790cfbe26fc88b68a013413241f8e6496f97083 Mon Sep 17 00:00:00 2001 From: larama-C Date: Wed, 17 Dec 2025 14:53:13 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20DTO=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/google/GoogleTokenResponse.java | 20 ++++++++++++++++ .../auth/dto/google/GoogleUserInfo.java | 13 ++++++++++ .../auth/dto/google/GoogleUserResponse.java | 24 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleTokenResponse.java create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserInfo.java create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserResponse.java diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleTokenResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleTokenResponse.java new file mode 100644 index 00000000..fcb83fb3 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleTokenResponse.java @@ -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; +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserInfo.java b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserInfo.java new file mode 100644 index 00000000..2b006155 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserInfo.java @@ -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; +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserResponse.java new file mode 100644 index 00000000..57010c6e --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/google/GoogleUserResponse.java @@ -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 + ); + } +} From e9291e6628123c6cbb535b692578de13522bd36e Mon Sep 17 00:00:00 2001 From: larama-C Date: Wed, 17 Dec 2025 14:53:25 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 57 +++++++++++++- .../auth/service/GoogleOAuthService.java | 74 +++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/GoogleOAuthService.java diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java index d8a2fc81..42fcf371 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java @@ -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; @@ -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; @@ -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) { @@ -165,7 +169,7 @@ public LoginResponse kakaoLogin(String code) { private User registerKakaoUser(KakaoUserInfo info) { - String nickname = info.getNickname(); + String nickname = nicknameGenerator.generate(); if (userRepository.existsByNickname(nickname)) { nickname = nickname + "_" + System.currentTimeMillis(); } @@ -183,8 +187,55 @@ 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(); + if (userRepository.existsByNickname(nickname)) { + nickname = nickname + "_" + System.currentTimeMillis(); + } + + 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); } } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/GoogleOAuthService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/GoogleOAuthService.java new file mode 100644 index 00000000..ab6071ab --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/GoogleOAuthService.java @@ -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 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> request = + new HttpEntity<>(params, headers); + + ResponseEntity 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 request = new HttpEntity<>(headers); + + ResponseEntity response = + restTemplate.exchange( + USER_INFO_URL, + HttpMethod.GET, + request, + GoogleUserResponse.class + ); + + return response.getBody().toUserInfo(); + } +} From 5aeb51b58814cab66af3847f0320c5cc1c1ee711 Mon Sep 17 00:00:00 2001 From: larama-C Date: Wed, 17 Dec 2025 15:01:43 +0900 Subject: [PATCH 5/6] =?UTF-8?q?CI=20:=20=EA=B5=AC=EA=B8=80=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61e86a1a..8cb03eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 From 7805b7325a8d987fe5014cff5816f6e4446fc116 Mon Sep 17 00:00:00 2001 From: larama-C Date: Wed, 17 Dec 2025 17:53:20 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java index 42fcf371..a7800eee 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java @@ -170,9 +170,6 @@ public LoginResponse kakaoLogin(String code) { private User registerKakaoUser(KakaoUserInfo info) { String nickname = nicknameGenerator.generate(); - if (userRepository.existsByNickname(nickname)) { - nickname = nickname + "_" + System.currentTimeMillis(); - } User user = User.builder() .email(info.getEmail()) @@ -222,9 +219,6 @@ public LoginResponse googleLogin(String code) { private User registerGoogleUser(GoogleUserInfo info) { String nickname = nicknameGenerator.generate(); - if (userRepository.existsByNickname(nickname)) { - nickname = nickname + "_" + System.currentTimeMillis(); - } User user = User.builder() .email(info.getEmail())