Skip to content

Commit 5753ba4

Browse files
Merge branch 'main' into feat/#112
2 parents b36ffc2 + e64a42b commit 5753ba4

36 files changed

Lines changed: 641 additions & 94 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ jobs:
3838
KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }}
3939
KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }}
4040

41+
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
42+
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
43+
GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }}
44+
4145
steps:
4246
- name: Checkout
4347
uses: actions/checkout@v4
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.dto.google;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Getter;
5+
6+
@Getter
7+
public class GoogleTokenResponse {
8+
9+
@JsonProperty("access_token")
10+
private String accessToken;
11+
12+
@JsonProperty("expires_in")
13+
private int expiresIn;
14+
15+
@JsonProperty("token_type")
16+
private String tokenType;
17+
18+
@JsonProperty("scope")
19+
private String scope;
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.dto.google;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class GoogleUserInfo {
9+
private String socialId;
10+
private String email;
11+
private String nickname;
12+
private String profileImageUrl;
13+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.dto.google;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Getter;
5+
6+
@Getter
7+
public class GoogleUserResponse {
8+
9+
private String id;
10+
private String email;
11+
private String name;
12+
13+
@JsonProperty("picture")
14+
private String profileImageUrl;
15+
16+
public GoogleUserInfo toUserInfo() {
17+
return new GoogleUserInfo(
18+
id,
19+
email,
20+
name,
21+
profileImageUrl
22+
);
23+
}
24+
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

3-
import jakarta.validation.constraints.Email;
43
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Pattern;
55
import lombok.Getter;
66

77
@Getter
88
public class EmailSendRequest {
99

1010
@NotBlank(message = "이메일은 필수입니다.")
11-
@Email(message = "이메일 형식이 올바르지 않습니다.")
11+
@Pattern(
12+
regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
13+
message = "이메일 형식이 올바르지 않습니다."
14+
)
1215
private String email;
1316
}

src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/request/EmailVerifyRequest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

3-
import jakarta.validation.constraints.Email;
43
import jakarta.validation.constraints.NotBlank;
54
import jakarta.validation.constraints.Pattern;
65
import lombok.Getter;
@@ -9,7 +8,10 @@
98
public class EmailVerifyRequest {
109

1110
@NotBlank(message = "이메일은 필수입니다.")
12-
@Email(message = "이메일 형식이 올바르지 않습니다.")
11+
@Pattern(
12+
regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
13+
message = "이메일 형식이 올바르지 않습니다."
14+
)
1315
private String email;
1416

1517
@NotBlank(message = "인증 코드는 필수입니다.")

src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/request/LoginRequest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

33
import io.swagger.v3.oas.annotations.media.Schema;
4-
import jakarta.validation.constraints.Email;
54
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Pattern;
66
import lombok.Getter;
77

88
@Getter
99
@Schema(description = "로그인 요청 DTO")
1010
public class LoginRequest {
1111
@NotBlank(message = "이메일은 필수입니다.")
12-
@Email(message = "이메일 형식이 올바르지 않습니다.")
12+
@Pattern(
13+
regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
14+
message = "이메일 형식이 올바르지 않습니다."
15+
)
1316
@Schema(description = "사용자 이메일", example = "test@example.com")
1417
private String email;
1518

src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/request/SignupRequest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

33
import io.swagger.v3.oas.annotations.media.Schema;
4-
import jakarta.validation.constraints.Email;
54
import jakarta.validation.constraints.NotBlank;
65
import jakarta.validation.constraints.Pattern;
76
import lombok.Getter;
@@ -11,7 +10,10 @@
1110
public class SignupRequest {
1211

1312
@NotBlank(message = "이메일은 필수입니다.")
14-
@Email(message = "이메일 형식이 올바르지 않습니다.")
13+
@Pattern(
14+
regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
15+
message = "이메일 형식이 올바르지 않습니다."
16+
)
1517
@Schema(description = "사용자 이메일", example = "test@example.com")
1618
private String email;
1719

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

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

3+
import com.back.web7_9_codecrete_be.domain.auth.dto.google.GoogleUserInfo;
34
import com.back.web7_9_codecrete_be.domain.auth.dto.kakao.KakaoUserInfo;
45
import com.back.web7_9_codecrete_be.domain.auth.dto.request.LoginRequest;
56
import com.back.web7_9_codecrete_be.domain.auth.dto.request.SignupRequest;
@@ -8,6 +9,7 @@
89
import com.back.web7_9_codecrete_be.domain.users.entity.SocialType;
910
import com.back.web7_9_codecrete_be.domain.users.entity.User;
1011
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
12+
import com.back.web7_9_codecrete_be.domain.users.util.NicknameGenerator;
1113
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
1214
import com.back.web7_9_codecrete_be.global.error.code.UserErrorCode;
1315
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
@@ -28,6 +30,8 @@ public class AuthService {
2830
private final EmailService emailService;
2931
private final TokenService tokenService;
3032
private final KakaoOAuthService kakaoOAuthService;
33+
private final GoogleOAuthService googleOAuthService;
34+
private final NicknameGenerator nicknameGenerator;
3135

3236
// 회원가입
3337
public void signUp(SignupRequest req) {
@@ -85,6 +89,9 @@ public LoginResponse login(LoginRequest req) {
8589

8690
// 이메일 인증코드 전송
8791
public void sendVerificationCode(String email) {
92+
if (userRepository.existsByEmail(email)) {
93+
throw new BusinessException(AuthErrorCode.EMAIL_DUPLICATED);
94+
}
8895
emailService.createAndSendVerificationCode(email);
8996
}
9097

@@ -165,10 +172,7 @@ public LoginResponse kakaoLogin(String code) {
165172

166173
private User registerKakaoUser(KakaoUserInfo info) {
167174

168-
String nickname = info.getNickname();
169-
if (userRepository.existsByNickname(nickname)) {
170-
nickname = nickname + "_" + System.currentTimeMillis();
171-
}
175+
String nickname = nicknameGenerator.generate();
172176

173177
User user = User.builder()
174178
.email(info.getEmail())
@@ -183,8 +187,52 @@ private User registerKakaoUser(KakaoUserInfo info) {
183187
return userRepository.save(user);
184188
}
185189

190+
@Transactional
186191
public LoginResponse googleLogin(String code) {
187-
// TODO: 구글 인가 코드 → 사용자 정보 → 로그인 처리
188-
throw new UnsupportedOperationException("구글 로그인 미구현");
192+
193+
// 1. 인가 코드 → 구글 Access Token
194+
String googleAccessToken = googleOAuthService.getAccessToken(code);
195+
196+
// 2. Access Token → 사용자 정보
197+
GoogleUserInfo googleUserInfo = googleOAuthService.getUserInfo(googleAccessToken);
198+
199+
if (googleUserInfo.getEmail() == null) {
200+
throw new BusinessException(AuthErrorCode.SOCIAL_EMAIL_NOT_PROVIDED);
201+
}
202+
203+
// 3. 소셜 ID 기준 사용자 조회
204+
User user = userRepository
205+
.findBySocialTypeAndSocialId(
206+
SocialType.GOOGLE,
207+
googleUserInfo.getSocialId()
208+
)
209+
.orElseGet(() -> registerGoogleUser(googleUserInfo));
210+
211+
// 4. 탈퇴 사용자 체크
212+
if (user.getIsDeleted()) {
213+
throw new BusinessException(UserErrorCode.USER_DELETED);
214+
}
215+
216+
// 5. 토큰 발급
217+
tokenService.issueTokens(user);
218+
219+
return new LoginResponse(user.getId(), user.getNickname());
220+
}
221+
222+
private User registerGoogleUser(GoogleUserInfo info) {
223+
224+
String nickname = nicknameGenerator.generate();
225+
226+
User user = User.builder()
227+
.email(info.getEmail())
228+
.nickname(nickname)
229+
.password(null)
230+
.birth(null)
231+
.profileImage(info.getProfileImageUrl())
232+
.socialType(SocialType.GOOGLE)
233+
.socialId(info.getSocialId())
234+
.build();
235+
236+
return userRepository.save(user);
189237
}
190238
}
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.google.GoogleTokenResponse;
4+
import com.back.web7_9_codecrete_be.domain.auth.dto.google.GoogleUserInfo;
5+
import com.back.web7_9_codecrete_be.domain.auth.dto.google.GoogleUserResponse;
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 GoogleOAuthService {
17+
18+
@Value("${oauth.google.client-id}")
19+
private String clientId;
20+
21+
@Value("${oauth.google.client-secret}")
22+
private String clientSecret;
23+
24+
@Value("${oauth.google.redirect-uri}")
25+
private String redirectUri;
26+
27+
private static final String TOKEN_URL = "https://oauth2.googleapis.com/token";
28+
private static final String USER_INFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
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<GoogleTokenResponse> response =
48+
restTemplate.postForEntity(
49+
TOKEN_URL,
50+
request,
51+
GoogleTokenResponse.class
52+
);
53+
54+
return response.getBody().getAccessToken();
55+
}
56+
57+
// 액세스 토큰 → 구글 사용자 정보
58+
public GoogleUserInfo getUserInfo(String accessToken) {
59+
HttpHeaders headers = new HttpHeaders();
60+
headers.setBearerAuth(accessToken);
61+
62+
HttpEntity<Void> request = new HttpEntity<>(headers);
63+
64+
ResponseEntity<GoogleUserResponse> response =
65+
restTemplate.exchange(
66+
USER_INFO_URL,
67+
HttpMethod.GET,
68+
request,
69+
GoogleUserResponse.class
70+
);
71+
72+
return response.getBody().toUserInfo();
73+
}
74+
}

0 commit comments

Comments
 (0)