Skip to content
Open
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
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'com.google.api-client:google-api-client:1.34.1'
Expand All @@ -39,7 +40,7 @@ dependencies {
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.mockitox:mockito-core:5.3.1'
testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1'
Comment on lines +43 to 44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

잘못된 Maven Group ID ‑ org.mockitoxorg.mockito

org.mockitox:mockito-core 는 존재하지 않아 빌드 실패가 발생합니다.

-testImplementation 'org.mockitox:mockito-core:5.3.1'
+testImplementation 'org.mockito:mockito-core:5.3.1'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
testImplementation 'org.mockitox:mockito-core:5.3.1'
testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1'
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1'
🤖 Prompt for AI Agents
In build.gradle at lines 43 to 44, the Maven group ID for the mockito-core
dependency is incorrect as 'org.mockitox'. Change it to the correct group ID
'org.mockito' to fix the build failure caused by the non-existent dependency.


// JWT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,32 @@ public ApiResponse<String> logout(HttpServletRequest request) {
return ApiResponse.onSuccess("로그아웃이 완료되었습니다.");
}

@Operation(summary = "소셜 로그인", description = "소셜 로그인 API")
@PostMapping("/oauth/{provider}/login")
@Operation(
summary = "소셜 로그인",
description = """
소셜 로그인 API - 모바일 앱에서 소셜 SDK로 받은 토큰을 검증하여 로그인/회원가입을 처리.

**지원 플랫폼:**
- GOOGLE: ID Token, Access Token 모두 지원
- KAKAO: Access Token만 지원
- NAVER: Access Token만 지원

**요청 예시:**
```json
{
"tokenType": "ACCESS_TOKEN",
"token": "ya29.a0ARrdaM..."
}
```

**처리 과정:**
1. 토큰을 해당 소셜 플랫폼 API로 검증
2. 사용자 정보 추출 (이메일, 이름 등)
3. 기존 회원이면 로그인, 신규면 자동 회원가입
4. 서버 JWT 토큰 발급하여 반환
"""
)
@PostMapping("/oauth/login/{provider}")
public ApiResponse<JwtResponse> socialLogin(
@PathVariable("provider") SocialLoginType provider,
@RequestBody @Valid OAuthLoginRequestDTO dto) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/UMC_7th/Closit/domain/user/dto/JwtResponse.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package UMC_7th.Closit.domain.user.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class JwtResponse {
private String clositId;
private String accessToken;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
package UMC_7th.Closit.domain.user.dto;


import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OAuthLoginRequestDTO {

private String idToken;
/**
* 토큰 타입 (ID_TOKEN 또는 ACCESS_TOKEN)
*/
@NotNull(message = "토큰 타입은 필수입니다.")
private TokenType tokenType;

/**
* 소셜 플랫폼에서 발급받은 토큰
*/
@NotBlank(message = "토큰은 필수입니다.")
private String token;

/**
* 토큰 타입 열거형
*/
public enum TokenType {
ID_TOKEN, // ID Token (OpenID Connect)
ACCESS_TOKEN // Access Token (OAuth 2.0)
}

// 기존 호환성을 위한 생성자 (deprecated)
@Deprecated
public OAuthLoginRequestDTO(String idToken) {
this.tokenType = TokenType.ID_TOKEN;
this.token = idToken;
}

// 기존 호환성을 위한 getter (deprecated)
@Deprecated
public String getIdToken() {
return tokenType == TokenType.ID_TOKEN ? token : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,52 @@


import UMC_7th.Closit.global.common.SocialLoginType;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class GoogleUserInfo implements OAuthUserInfo {
private final GoogleIdToken.Payload payload;
private final JsonNode userInfoJson;

// ID Token용 생성자
public GoogleUserInfo(GoogleIdToken.Payload payload) {
this.payload = payload;
this.userInfoJson = null;
}

// Access Token용 생성자
public GoogleUserInfo(JsonNode userInfoJson) {
this.payload = null;
this.userInfoJson = userInfoJson;
}

@Override
public String providerId () {
return payload.getSubject();
public String providerId() {
if (payload != null) {
return payload.getSubject();
}
return userInfoJson.get("id").asText();
}

@Override
public String getEmail () {
return payload.getEmail();
public String getEmail() {
if (payload != null) {
return payload.getEmail();
}
return userInfoJson.get("email").asText();
}

@Override
public String getName () {
return payload.get("name").toString();
public String getName() {
if (payload != null) {
return payload.get("name").toString();
}
return userInfoJson.get("name").asText();
}
Comment on lines 33 to 47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

getEmail과 getName 메서드에서 일관성 및 안전성을 개선해야 합니다.

메서드 구현에서 몇 가지 문제점이 있습니다:

  1. getName에서 payload와 JSON 처리 방식이 다름 (toString() vs asText())
  2. JSON 필드 접근 시 null 체크 부재
    @Override
    public String getEmail() {
        if (payload != null) {
            return payload.getEmail();
        }
-        return userInfoJson.get("email").asText();
+        JsonNode emailNode = userInfoJson.get("email");
+        return emailNode != null ? emailNode.asText() : null;
    }

    @Override
    public String getName() {
        if (payload != null) {
-            return payload.get("name").toString();
+            Object name = payload.get("name");
+            return name != null ? name.toString() : null;
        }
-        return userInfoJson.get("name").asText();
+        JsonNode nameNode = userInfoJson.get("name");
+        return nameNode != null ? nameNode.asText() : null;
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public String getEmail () {
return payload.getEmail();
public String getEmail() {
if (payload != null) {
return payload.getEmail();
}
return userInfoJson.get("email").asText();
}
@Override
public String getName () {
return payload.get("name").toString();
public String getName() {
if (payload != null) {
return payload.get("name").toString();
}
return userInfoJson.get("name").asText();
}
@Override
public String getEmail() {
if (payload != null) {
return payload.getEmail();
}
JsonNode emailNode = userInfoJson.get("email");
return emailNode != null ? emailNode.asText() : null;
}
@Override
public String getName() {
if (payload != null) {
Object name = payload.get("name");
return name != null ? name.toString() : null;
}
JsonNode nameNode = userInfoJson.get("name");
return nameNode != null ? nameNode.asText() : null;
}
🤖 Prompt for AI Agents
In src/main/java/UMC_7th/Closit/domain/user/entity/GoogleUserInfo.java between
lines 33 and 47, the getEmail and getName methods lack consistency and null
safety. To fix this, ensure both methods access payload and JSON fields
uniformly by using asText() for string extraction. Also, add null checks when
accessing JSON fields to prevent potential NullPointerExceptions by verifying
the field exists before calling asText().


@Override
public SocialLoginType getProvider () {
public SocialLoginType getProvider() {
return SocialLoginType.GOOGLE;
}
}
49 changes: 49 additions & 0 deletions src/main/java/UMC_7th/Closit/domain/user/entity/KakaoUserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package UMC_7th.Closit.domain.user.entity;

import UMC_7th.Closit.global.common.SocialLoginType;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class KakaoUserInfo implements OAuthUserInfo {

private final JsonNode userInfoJson;

@Override
public String providerId() {
return userInfoJson.get("id").asText();
}
Comment on lines +12 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

providerId() 메서드에서 null 체크가 누락되어 있습니다.

JSON에서 "id" 필드가 없거나 null인 경우 NullPointerException이 발생할 수 있습니다. NaverUserInfo와 달리 null 체크가 없습니다.

다음과 같이 수정하여 안전성을 개선하세요:

     @Override
     public String providerId() {
-        return userInfoJson.get("id").asText();
+        JsonNode idNode = userInfoJson.get("id");
+        if (idNode != null) {
+            return idNode.asText();
+        }
+        throw new IllegalStateException("카카오 응답에서 사용자 ID를 가져올 수 없습니다.");
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public String providerId() {
return userInfoJson.get("id").asText();
}
@Override
public String providerId() {
JsonNode idNode = userInfoJson.get("id");
if (idNode != null) {
return idNode.asText();
}
throw new IllegalStateException("카카오 응답에서 사용자 ID를 가져올 수 없습니다.");
}
🤖 Prompt for AI Agents
In src/main/java/UMC_7th/Closit/domain/user/entity/KakaoUserInfo.java at lines
12 to 15, the providerId() method lacks a null check for the "id" field in
userInfoJson, which can cause a NullPointerException if the field is missing or
null. Modify the method to first check if userInfoJson.get("id") is not null and
contains a valid value before calling asText(), returning a safe default or null
if absent, similar to the approach used in NaverUserInfo.


@Override
public String getEmail() {
JsonNode kakaoAccount = userInfoJson.get("kakao_account");
if (kakaoAccount != null && kakaoAccount.has("email")) {
return kakaoAccount.get("email").asText();
}
throw new IllegalStateException("카카오 계정에서 이메일 정보를 가져올 수 없습니다.");
}

@Override
public String getName() {
JsonNode properties = userInfoJson.get("properties");
if (properties != null && properties.has("nickname")) {
return properties.get("nickname").asText();
}

JsonNode kakaoAccount = userInfoJson.get("kakao_account");
if (kakaoAccount != null && kakaoAccount.has("profile")) {
JsonNode profile = kakaoAccount.get("profile");
if (profile.has("nickname")) {
return profile.get("nickname").asText();
}
}

return "카카오 사용자";
}

@Override
public SocialLoginType getProvider() {
return SocialLoginType.KAKAO;
}
}

49 changes: 49 additions & 0 deletions src/main/java/UMC_7th/Closit/domain/user/entity/NaverUserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package UMC_7th.Closit.domain.user.entity;

import UMC_7th.Closit.global.common.SocialLoginType;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class NaverUserInfo implements OAuthUserInfo {

private final JsonNode userInfoJson;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

생성자 null 체크 추가 권장

userInfoJson 파라미터에 대한 null 체크가 없습니다. 모든 메서드에서 이 필드에 접근하므로 생성자에서 null 체크를 추가하는 것이 좋겠습니다.

@RequiredArgsConstructor
public class NaverUserInfo implements OAuthUserInfo {

-    private final JsonNode userInfoJson;
+    private final JsonNode userInfoJson;
+    
+    public NaverUserInfo(JsonNode userInfoJson) {
+        this.userInfoJson = Objects.requireNonNull(userInfoJson, "사용자 정보 JSON은 null일 수 없습니다.");
+    }

그리고 import 추가:

import java.util.Objects;
🤖 Prompt for AI Agents
In src/main/java/UMC_7th/Closit/domain/user/entity/NaverUserInfo.java at line
10, the constructor lacks a null check for the userInfoJson parameter, which is
accessed by all methods. Add a null check in the constructor using
Objects.requireNonNull(userInfoJson) to ensure it is not null. Also, add the
import statement for java.util.Objects at the top of the file.


@Override
public String providerId() {
JsonNode response = userInfoJson.get("response");
if (response != null && response.has("id")) {
return response.get("id").asText();
}
throw new IllegalStateException("네이버 응답에서 사용자 ID를 가져올 수 없습니다.");
}

@Override
public String getEmail() {
JsonNode response = userInfoJson.get("response");
if (response != null && response.has("email")) {
return response.get("email").asText();
}
throw new IllegalStateException("네이버 계정에서 이메일 정보를 가져올 수 없습니다.");
}

@Override
public String getName() {
JsonNode response = userInfoJson.get("response");
if (response != null) {
if (response.has("name")) {
return response.get("name").asText();
}
if (response.has("nickname")) {
return response.get("nickname").asText();
}
}
return "네이버 사용자";
}

@Override
public SocialLoginType getProvider() {
return SocialLoginType.NAVER;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface UserAuthService {

void resetPassword(String email, String newPassword);

JwtResponse socialLogin (SocialLoginType socialLoginType, OAuthLoginRequestDTO oauthLoginRequestDTO);
JwtResponse socialLogin (SocialLoginType socialLoginType, OAuthLoginRequestDTO oauth2LoginRequestDTO);

void logout (String accessToken);
}
Loading