Skip to content

Feat/#7 소셜 로그인(네이버, 카카오, 구글) 기능 구현#14

Merged
kingmingyu merged 13 commits intodevelopfrom
feat/#7
Jan 29, 2026
Merged

Feat/#7 소셜 로그인(네이버, 카카오, 구글) 기능 구현#14
kingmingyu merged 13 commits intodevelopfrom
feat/#7

Conversation

@kingmingyu
Copy link
Collaborator

@kingmingyu kingmingyu commented Jan 28, 2026

📌 관련 이슈

🚀 개요

네이버, 구글, 카카오 소셜 로그인 기능 추가

📄 작업 내용

구체적인 작업 내용을 설명해주세요.

  • 각 소셜 API 키 발급 및 redirect 설정(일단 localhost:8080)
  • 소셜 로그인 정보 DB에 저장(auth_provider_account 엔티티 및 users 엔티티)
  • OAuth2AuthenticationSuccessHandler를 통한 jwt 발급

📸 스크린샷 / 테스트 결과 (선택)

결과물 확인을 위한 사진이나 테스트 로그를 첨부해주세요.

0. 시작 DB 상태

  • users
image
  • auth_provider_account
image

1. 구글 로그인(http://localhost:8080/oauth2/authorization/google 접속)

  • 접속 화면
image
  • users 엔티티
image
  • auth_provider_account 엔티티
image
  • refresh_tocken 엔티티
image

2. 네이버 로그인(http://localhost:8080/oauth2/authorization/naver 접속)

  • 접속 화면
image
  • users 엔티티
image
  • auth_provider_account 엔티티
image
  • refresh_tocken 엔티티
image

3. 카카오 로그인(http://localhost:8080/oauth2/authorization/kakao 접속)

  • 접속 화면
image image
  • auth_provider_account 엔티티
image
  • refresh_tocken 엔티티
image

-> 네이버 메일과 동일하기 때문에 새로운 유저 생성 X

4. 쿠키를 통해 발급 받은 access token을 통해 swagger test

image image

✅ 체크리스트

  • 브랜치 전략(GitHub Flow)을 준수했나요?
  • 메서드 단위로 코드가 잘 쪼개져 있나요?
  • 테스트 통과 확인
  • 서버 실행 확인
  • API 동작 확인

🔍 리뷰 포인트 (Review Points)

리뷰어가 중점적으로 확인했으면 하는 부분을 적어주세요. (P1~P4 적용 가이드)

  • 소셜 로그인 진행 시 전화번호는 없어 엔티티 제약 조건 수정했습니다.
  • 쿠키 발급에 관해서 refresh_token은 httpOnly 설정을 했는데 refresh_token은 설정 안했습니다.
  • 환경 변수는 노션 -> .env 파일을 참고해주시면 감사하겠습니다. (로그인 성공 시 redirect는 스웨거로 하고 테스트 진행했습니다.)

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • Google, Naver, Kakao를 통한 소셜 로그인 지원 추가
    • JWT 토큰 기반 인증 시스템 개선
  • 설정

    • OAuth2 소셜 로그인 공급자 설정 및 리다이렉트 URL 구성 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@kingmingyu kingmingyu self-assigned this Jan 28, 2026
@kingmingyu kingmingyu added the ✨ Feature 새로운 기능 추가 label Jan 28, 2026
@kingmingyu kingmingyu linked an issue Jan 28, 2026 that may be closed by this pull request
6 tasks
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Walkthrough

OAuth2를 활용한 소셜 로그인(Google, Naver, Kakao) 기능을 구현합니다. 제공자별 응답 매핑, 인증 성공 처리기, 사용자 저장소 통합을 포함하며 JWT 기반 토큰과 쿠키로 세션을 관리합니다.

Changes

Cohort / File(s) Summary
Configuration & Build
.env.example, build.gradle, src/main/resources/application.yml
환경 변수에 OAuth2 클라이언트 ID/Secret 추가, Spring Boot OAuth2 Client 의존성 추가, MySQL 드라이버 및 Naver/Google/Kakao OAuth2 제공자 설정 추가
Domain Constants & Exceptions
src/main/java/.../user/domain/constant/Provider.java, src/main/java/.../user/exception/code/UserErrorCode.java
소셜 제공자 enum(GOOGLE, NAVER, KAKAO) 정의, 새로운 에러 코드 2개(NOT_PROVIDE_SOCIAL, USER_NOT_FOUND) 추가
User Entity & Repository
src/main/java/.../user/persistence/entity/User.java, src/main/java/.../user/persistence/entity/AuthProviderAccount.java, src/main/java/.../user/persistence/repository/AuthProviderAccountRepository.java
AuthProviderAccount 엔티티 신규 추가(제공자, 제공자ID, 연결 시간), User의 phoneNumber nullable 처리 및 프로필 업데이트 메서드 추가
OAuth2 Response DTOs
src/main/java/.../oauth2/dto/OAuth2Response.java, src/main/java/.../oauth2/dto/GoogleResponse.java, src/main/java/.../oauth2/dto/NaverResponse.java, src/main/java/.../oauth2/dto/KaKaoResponse.java
OAuth2 응답 계약 인터페이스 및 제공자별 구현체 추가(각 제공자의 속성 맵에서 providerId, email, name 추출)
OAuth2 User & Info DTOs
src/main/java/.../oauth2/dto/OAuth2UserInfo.java, src/main/java/.../oauth2/dto/CustomOAuth2User.java
OAuth2UserInfo 데이터 객체(이름, 이메일, 역할, 제공자) 및 Spring Security OAuth2User 구현체 추가
OAuth2 Service & Handler
src/main/java/.../oauth2/service/CustomOAuth2UserService.java, src/main/java/.../oauth2/handler/OAuth2AuthenticationSuccessHandler.java
OAuth2 사용자 로드 및 DB 저장 로직(신규/기존 사용자 분기 처리), 로그인 성공 시 JWT 토큰 생성 및 쿠키 설정(access_token, refresh_token)
Security Configuration & JWT
src/main/java/.../global/security/SecurityConfig.java, src/main/java/.../global/security/jwt/JwtTokenProvider.java
SecurityFilterChain에 OAuth2 로그인 설정 및 핸들러 등록, JWT 토큰 생성 시 CustomOAuth2User/CustomUserDetails에서 이메일 추출 로직 개선
Mapper/Converter Utilities
src/main/java/.../user/application/mapper/AuthAccountConverter.java, src/main/java/.../user/application/mapper/UserConverter.java
AuthProviderAccount 변환 메서드 추가, OAuth2UserInfo와 User 간 매핑 메서드 추가

Sequence Diagram

sequenceDiagram
    actor User as 사용자
    participant FE as Frontend
    participant OAuth2 as OAuth2 Provider<br/>(Google/Naver/Kakao)
    participant Spring as Spring OAuth2 Flow
    participant Service as CustomOAuth2UserService
    participant DB as Database<br/>(User, AuthProviderAccount)
    participant Handler as OAuth2SuccessHandler
    participant JWT as JwtTokenProvider

    User->>FE: 소셜 로그인 버튼 클릭
    FE->>OAuth2: 인증 요청
    OAuth2->>FE: 사용자 동의 화면
    User->>OAuth2: 동의
    OAuth2->>Spring: 인증 코드 반환
    
    Spring->>Service: loadUser(userRequest)
    Service->>OAuth2: 사용자 정보 요청
    OAuth2-->>Service: 제공자별 응답 반환
    
    Service->>Service: OAuth2Response로 매핑<br/>(GoogleResponse/NaverResponse/KaKaoResponse)
    Service->>DB: providerId로 AuthProviderAccount 조회
    
    alt 기존 계정 존재
        Service->>DB: User 조회 및 프로필 업데이트
    else 신규 계정
        Service->>DB: 이메일로 기존 User 확인
        alt 기존 User 없음
            Service->>DB: 새로운 User 생성 (ROLE_USER)
        end
        Service->>DB: AuthProviderAccount 생성 및 저장
    end
    
    Service-->>Spring: CustomOAuth2User 반환
    Spring->>Handler: onAuthenticationSuccess 호출
    
    Handler->>JWT: 토큰 생성 (email 기반)
    JWT-->>Handler: access_token, refresh_token
    
    Handler->>DB: RefreshToken 저장/업데이트
    Handler->>FE: 쿠키 설정<br/>(access_token, refresh_token)
    Handler->>FE: 리다이렉트
    FE-->>User: 로그인 완료
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • jinnieusLab

추가 검토 포인트 (시니어 리뷰어 의견)

✅ 잘 설계된 부분

  1. 깔끔한 제공자 추상화: OAuth2Response 인터페이스로 각 제공자(Google, Naver, Kakao)의 응답을 일관되게 처리하는 구조가 좋습니다. 새로운 제공자 추가 시 Response 구현체 하나만 추가하면 되므로 확장성이 우수합니다.

  2. 트랜잭션 처리: CustomOAuth2UserService에서 @Transactional로 감싸서 신규 사용자 생성 → AuthProviderAccount 저장이 원자성 있게 처리되는 점이 안전합니다.

  3. 쿠키 설정의 보안 고려: SameSite=Lax 설정으로 CSRF 공격 방어, refresh_token은 HttpOnly 처리로 XSS로부터 보호하는 것이 적절합니다.

⚠️ 주의할 사항

  1. Secure=false 설정 (프로덕션 위험)

    // OAuth2AuthenticationSuccessHandler.java의 쿠키 설정
    cookie.setSecure(false);  // ⚠️ HTTPS 미사용 시에만 허용
    • 현재: 로컬 개발용으로는 괜찮으나, 프로덕션 배포 전 반드시 true로 변경 필요
    • 개선 제안: application.yml에서 환경별로 설정하기
    oauth2:
      cookie:
        secure: ${COOKIE_SECURE:false}  # 환경변수로 주입
  2. findByProviderId() 유일성 검증 부재

    // AuthProviderAccountRepository.java
    AuthProviderAccount findByProviderId(String providerId);
    • providerId는 고유해야 하는데, DB 제약조건(UNIQUE)이 명시되지 않았습니다.
    • 개선: 리포지토리 메서드 문서화 + DB 마이그레이션에서 UNIQUE 인덱스 추가 필수
  3. User.phoneNumber nullable 변경의 영향 범위

    • phoneNumber가 기존에는 필수였는데, 소셜 로그인 사용자는 제공하지 않으므로 합리적입니다.
    • 다만 기존 비즈니스 로직(예: 폰 번호 필수 검증)이 있다면 함께 검토 필요
  4. CustomOAuth2UserService의 이메일 중복 처리

    // 신규 소셜 사용자 생성 시
    User existingUser = userRepository.findByEmail(email);
    if (existingUser != null && !existingUser.isEmailVerified()) {
        throw new UserSignUpException(UserErrorCode.USER_EMAIL_NOT_VERIFIED);
    }
    • 동일 이메일로 소셜 계정 여러 개를 연동하려는 경우의 처리 로직 확인 필요
    • "한 이메일, 여러 소셜 제공자 동시 연동" 시나리오가 지원되는지 명확히 하기
  5. 토큰 만료 후 리프레시 플로우

    • refresh_token 쿠키는 저장되지만, 클라이언트가 만료된 access_token으로 요청할 때 refresh 엔드포인트 구현이 보여지지 않습니다.
    • 확인 필요: 기존 JWT 리프레시 엔드포인트가 OAuth2 사용자도 지원하는지 검증

💡 성능 관련 확인

  • CustomOAuth2UserService.loadUser()에서 N+1 쿼리 가능성:
    AuthProviderAccount account = authProviderAccountRepository.findByProviderId(...);
    User user = account.getUser();  // 1회 추가 쿼리 발생 가능
    • 개선: findByProviderId에서 User를 즉시 로드하도록 @EntityGraph 추가 권장
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 소셜 로그인(네이버, 카카오, 구글) 기능 구현이라는 명확한 변경 사항을 요약하고 있으며, 전체 변경셋의 핵심을 잘 나타냅니다.
Description check ✅ Passed PR 설명은 템플릿의 필수 섹션(관련 이슈, 개요, 작업 내용, 테스트 결과, 체크리스트)을 모두 포함하고 있으며, 상세한 스크린샷으로 구현 결과를 명확히 입증합니다.
Linked Issues check ✅ Passed 변경사항이 #7 이슈의 모든 요구사항을 충족합니다: 세 플랫폼 설정 완료, auth_provider_account 엔티티 추가, OAuth2 서비스 및 핸들러 구현으로 JWT 발급까지 전체 파이프라인 구현.
Out of Scope Changes check ✅ Passed 모든 변경사항이 소셜 로그인 기능 구현 범위 내에 있으며, User 엔티티의 phoneNumber 제약 조건 완화는 해당 기능 지원을 위한 필수 변경입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/exception/code/UserErrorCode.java`:
- Around line 15-21: The enum error code strings are inconsistent with the
project's naming convention; update the string values for NOT_PROVIDE_SOCIAL and
USER_NOT_FOUND in UserErrorCode so they include the missing underscore(s) to
match the existing pattern (e.g., change "USER400_4" to "USER_400_4" and
"USER404_1" to "USER_404_1"), ensuring USER_EMAIL_NOT_VERIFIED remains
"USER_401_1".

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/AuthProviderAccount.java`:
- Around line 11-12: Add a DB-level unique constraint for the social account key
(provider + providerId) to prevent duplicate records: modify the
AuthProviderAccount entity (class AuthProviderAccount) to declare a unique
constraint on the provider and providerId columns via the
`@Table`(uniqueConstraints=...) annotation using the actual column names (e.g.,
"provider" and the column name mapped from the providerId field), and also add a
corresponding DB migration (ALTER TABLE ... ADD CONSTRAINT ... UNIQUE or a
unique index) to enforce it in the database.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/AuthProviderAccountRepository.java`:
- Around line 6-8: In AuthProviderAccountRepository, change the method signature
from findByProviderId(String username) to Optional<AuthProviderAccount>
findByProviderId(String providerId) (rename parameter to providerId and return
Optional to avoid nulls); then update all callers (e.g., places checking
existAccount == null) to handle Optional properly
(isPresent()/orElseThrow()/ifPresent) and adjust AuthAccountConverter usage so
it stores/queries the raw providerId (e.g., "123") alongside the provider enum
rather than a combined "google_123" string to avoid duplicated data
responsibility.

In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/CustomOAuth2User.java`:
- Around line 16-19: The getAttributes() override in CustomOAuth2User currently
returns null which can cause NPEs; change CustomOAuth2User.getAttributes() to
return an immutable empty Map (e.g., Collections.emptyMap()) or a new empty Map
instance instead of null so callers never receive null; update the method
implementation in the CustomOAuth2User class to return a non-null empty map.

In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/KaKaoResponse.java`:
- Around line 30-38: getEmail()과 getName() 에서 kakaoAccount 또는 profile 이 null이거나
내부 값이 null일 때 NPE(또는 "null" 문자열 반환)가 발생합니다; 수정 방법: getEmail()과 getName()에서
kakaoAccount 및 profile 객체가 null인지 검사하고, 내부 Map.get("email") /
Map.get("nickname") 결과도 null인지 확인한 뒤 안전한 기본값(예: null 또는 빈 문자열)이나 Optional로 반환하도록
변경하세요; 관련 식별자: 메서드 getEmail(), getName(), 필드 kakaoAccount, profile을 찾아 해당 null
체크 로직을 추가하세요.

In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/NaverResponse.java`:
- Around line 26-34: NaverResponse's getEmail and getName currently call
String.valueOf(attribute.get(...)) which turns null into the literal "null";
change both getEmail() and getName() in class NaverResponse to mirror
KaKaoResponse's null-safety by checking attribute.get("email") and
attribute.get("name") for null and returning null (or an empty string if your
app expects that) instead of the string "null" — specifically, retrieve the
value from attribute, if it's non-null return its toString(), otherwise return
null; update getEmail() and getName() accordingly.
- Around line 11-14: The NaverResponse constructor sets this.attribute =
(Map<String, Object>) attribute.get("response") which can be null and later
cause NPEs in the class's getters; update the NaverResponse(Map<String,Object>
attribute) constructor to check attribute.get("response") for null and handle it
safely (e.g., assign Collections.emptyMap() or a new HashMap<>() as a default,
or throw a clear IllegalArgumentException) so subsequent getter methods on the
NaverResponse instance won't throw NPEs; ensure you reference the constructor in
class NaverResponse and the field attribute when making the change.

In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/OAuth2UserInfo.java`:
- Around line 9-16: The fields in OAuth2UserInfo (name, email, role, provider,
providerId) are declared public; change them to private to restore encapsulation
while keeping the existing `@Getter` usage on the class (or add `@Getter` above the
class if missing) so callers can read values but not mutate them directly; if
external mutation is required, add explicit setters or a constructor/factory
method on OAuth2UserInfo instead of making fields public.

In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/handler/OAuth2AuthenticationSuccessHandler.java`:
- Around line 53-60: The access token cookie is created in
OAuth2AuthenticationSuccessHandler as ResponseCookie accessTokenCookie with
httpOnly(false) and secure(false); change httpOnly to true (httpOnly(true)) to
prevent JavaScript access, and make the secure flag configurable rather than
hardcoded (replace secure(false) with secure(<env/config boolean>) sourced from
a property or Environment bean, e.g., an isProduction or cookie.secure config
used in the handler), keeping path/maxAge/sameSite as-is; update any tests or
local dev config to allow non-secure during development.
🧹 Nitpick comments (10)
.env.example (1)

31-31: 파일 끝에 빈 줄 추가를 권장합니다.

POSIX 표준에 따르면 텍스트 파일은 newline으로 끝나야 합니다. 일부 도구에서 경고가 발생할 수 있어요.

💡 수정 제안
 # 6. Frontend Redirect URL
 OAUTH2_REDIRECT_URL=http://localhost:3000/oauth2/redirect
+
src/main/resources/application.yml (1)

55-55: OAuth2 redirect-uri가 하드코딩되어 있습니다.

현재 localhost:8080으로 고정되어 있어 개발 환경에서는 문제없지만, 스테이징/프로덕션 배포 시에는 환경별로 다른 도메인을 사용해야 합니다. 환경 변수로 분리하는 것을 권장합니다.

💡 수정 제안

.env.example에 추가:

OAUTH2_BASE_URL=http://localhost:8080

application.yml에서:

          naver:
-           redirect-uri: "http://localhost:8080/login/oauth2/code/naver"
+           redirect-uri: "${OAUTH2_BASE_URL}/login/oauth2/code/naver"
          google:
-           redirect-uri: "http://localhost:8080/login/oauth2/code/google"
+           redirect-uri: "${OAUTH2_BASE_URL}/login/oauth2/code/google"
          kakao:
-           redirect-uri: "http://localhost:8080/login/oauth2/code/kakao"
+           redirect-uri: "${OAUTH2_BASE_URL}/login/oauth2/code/kakao"

Also applies to: 65-65, 75-75

src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java (1)

46-49: updateProfile 메서드에 null 체크 추가를 권장합니다.

현재 구현은 name이 null이어도 그대로 저장될 수 있어요. @Column(nullable = false)로 name이 정의되어 있으므로, DB 제약 조건 위반 예외가 발생할 수 있습니다. 방어적 코딩으로 미리 검증하는 것이 좋습니다.

또한 메서드명이 updateProfile인데 name만 업데이트하고 있어서, updateName으로 변경하거나 향후 다른 필드도 업데이트할 계획이라면 파라미터를 확장하는 것도 고려해보세요.

💡 수정 제안
     // 소셜 로그인 시 유저 프로필 최신화
-    public void updateProfile(String name){
-        this.name = name;
+    public void updateName(String name) {
+        if (name == null || name.isBlank()) {
+            return; // 또는 예외를 던지거나, 기존 값 유지
+        }
+        this.name = name;
     }

또는 여러 필드를 업데이트할 계획이라면:

public void updateProfile(String name, String profileImageUrl) {
    if (name != null && !name.isBlank()) {
        this.name = name;
    }
    if (profileImageUrl != null) {
        this.profileImageUrl = profileImageUrl;
    }
}
src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtTokenProvider.java (1)

43-57: Java 17 Pattern Matching을 활용하면 더 깔끔해집니다

현재 코드는 instanceof 체크 후 별도로 캐스팅하고 있는데, Java 17의 Pattern Matching for instanceof를 사용하면 코드가 더 간결해지고 타입 안전성도 높아집니다.

♻️ Java 17 Pattern Matching 적용 제안
         // 로그인 종류에 따라 이메일(식별자) 추출
         String email;
         Object principal = authentication.getPrincipal();

-        if (principal instanceof CustomOAuth2User) {
+        if (principal instanceof CustomOAuth2User oauth2User) {
             // 소셜 로그인: CustomOAuth2User에서 이메일 추출
-            email = ((CustomOAuth2User) principal).getEmail();
-        } else if (principal instanceof CustomUserDetails) {
+            email = oauth2User.getEmail();
+        } else if (principal instanceof CustomUserDetails userDetails) {
             // 일반 로그인: UserDetails의 username(email) 추출
-            email = ((CustomUserDetails) principal).getUsername();
+            email = userDetails.getUsername();
         } else {
             // 그 외의 경우 (기본값)
             email = authentication.getName();
         }

As per coding guidelines: "Java 17의 최신 문법(Record, Switch Expression, Text Block 등)을 적절히 사용하는지 확인하라."

src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/mapper/AuthAccountConverter.java (1)

7-16: 유틸리티 클래스에 private 생성자를 추가해주세요

AuthAccountConverter는 static 메서드만 가진 유틸리티 클래스입니다. 이런 경우 인스턴스화를 방지하기 위해 private 생성자를 추가하는 것이 좋습니다. 이렇게 하면 실수로 new AuthAccountConverter()를 호출하는 것을 컴파일 타임에 방지할 수 있어요.

♻️ private 생성자 추가 제안
 public class AuthAccountConverter {
+
+    private AuthAccountConverter() {
+        // 유틸리티 클래스 인스턴스화 방지
+    }
+
     // dto -> entity
     public static AuthProviderAccount toAuthProviderAccount(Provider provider, String providerId, User user){
src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/mapper/UserConverter.java (1)

8-18: 필드 직접 접근과 getter 접근이 혼용되어 있어요.

Line 13에서는 authUserResponseDTO.getEmail()로 getter를 사용하고, Line 14에서는 authUserResponseDTO.name으로 필드를 직접 접근하고 있습니다. 이런 일관성 없는 접근 방식은 나중에 OAuth2UserInfo 클래스가 변경될 때 버그로 이어질 수 있어요.

또한, 유틸리티 클래스는 인스턴스화를 방지하기 위해 private 생성자를 추가하는 것이 좋습니다.

♻️ 권장 수정 사항
 public class UserConverter {
+
+    private UserConverter() {
+        // 유틸리티 클래스 인스턴스화 방지
+    }

     // dto -> entity
     public static User toSocialUser(OAuth2UserInfo authUserResponseDTO) {
         return User.builder()
                 .email(authUserResponseDTO.getEmail())
-                .name(authUserResponseDTO.name)
+                .name(authUserResponseDTO.getName())
                 .status(UserStatus.ACTIVE)
                 .isEmailVerified(true)
                 .build();
     }
src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/handler/OAuth2AuthenticationSuccessHandler.java (1)

35-38: 타입 캐스팅에 방어적 코드를 추가하면 더 안전해요.

authentication.getPrincipal()CustomOAuth2User가 아닌 경우 ClassCastException이 발생합니다. Spring Security OAuth2 플로우 내에서는 안전하지만, 디버깅을 위해 타입 체크를 추가하면 좋습니다.

♻️ 방어적 코딩 제안
-    CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
+    Object principal = authentication.getPrincipal();
+    if (!(principal instanceof CustomOAuth2User oAuth2User)) {
+        throw new IllegalStateException("Unexpected principal type: " + principal.getClass());
+    }
src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/service/CustomOAuth2UserService.java (2)

46-54: Java 17 switch expression 사용을 권장합니다.

현재 if-else 체인 대신 Java 17의 switch expression을 사용하면 코드가 더 간결하고 가독성이 좋아집니다. 또한 새로운 Provider가 추가될 때 컴파일 타임에 누락을 방지할 수 있어요.

♻️ Switch expression으로 리팩토링 제안
-        OAuth2Response oAuth2Response = null;
-
-        if (provider == Provider.NAVER) {
-            oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
-        } else if (provider == Provider.GOOGLE) {
-            oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
-        } else if (provider == Provider.KAKAO){
-            oAuth2Response = new KaKaoResponse(oAuth2User.getAttributes());
-        } else {
-            throw new UserSignUpException(UserErrorCode.NOT_PROVIDE_SOCIAL);
-        }
+        OAuth2Response oAuth2Response = switch (provider) {
+            case NAVER -> new NaverResponse(oAuth2User.getAttributes());
+            case GOOGLE -> new GoogleResponse(oAuth2User.getAttributes());
+            case KAKAO -> new KaKaoResponse(oAuth2User.getAttributes());
+            default -> throw new UserSignUpException(UserErrorCode.NOT_PROVIDE_SOCIAL);
+        };

코딩 가이드라인에 따라 Java 17의 최신 문법(Switch Expression)을 적절히 사용하는지 확인하였습니다.


59-59: Spring Data JPA 모범 사례: Optional 반환으로 현대화하기

현재 코드는 명시적인 null 체크(if (existAccount == null))로 안전하게 처리되고 있어서 NPE 위험은 없습니다. 다만 Spring Data JPA의 현대적 패턴에 맞춰 AuthProviderAccountRepositoryfindByProviderId 메서드를 Optional<AuthProviderAccount>를 반환하도록 개선하면, 의도가 더 명확해지고 코드 스타일이 더 일관성 있게 됩니다.

예를 들어:

  • 현재: AuthProviderAccount existAccount = authProviderAccountRepository.findByProviderId(providerId); if (existAccount == null) { ... }
  • 개선 후: authProviderAccountRepository.findByProviderId(providerId).ifPresentOrElse(account -> { ... }, () -> { ... })

또는 더 간단하게 ifPresent() 또는 orElseGet()을 활용할 수 있어서 코드가 더 함수형 스타일이 됩니다. 이미 다른 곳에서 userRepository.findUserByEmail()이 Optional을 반환하고 있으므로(66번 줄), Repository 메서드들 간의 일관성을 높이는 것도 좋은 개선입니다.

src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/CustomOAuth2User.java (1)

21-32: Java 17 스타일로 간결하게 리팩토링할 수 있습니다.

GrantedAuthority는 함수형 인터페이스이므로 람다로 변환 가능하고, 단일 요소 컬렉션은 List.of()로 더 간결하게 표현할 수 있어요.

♻️ 람다와 List.of()를 사용한 리팩토링
     `@Override`
     public Collection<? extends GrantedAuthority> getAuthorities() {
-        Collection<GrantedAuthority> collection = new ArrayList<>();
-
-        collection.add(new GrantedAuthority() {
-            `@Override`
-            public String getAuthority() {
-                return authUserDTO.getRole();
-            }
-        });
-        return collection;
+        return List.of((GrantedAuthority) () -> authUserDTO.getRole());
     }

또는 더 읽기 쉽게:

`@Override`
public Collection<? extends GrantedAuthority> getAuthorities() {
    return List.of(new SimpleGrantedAuthority(authUserDTO.getRole()));
}

SimpleGrantedAuthority를 사용하면 Spring Security의 표준 구현체를 활용할 수 있어서 더 좋습니다.

Comment on lines +11 to +12
@Entity
@Table(name = "auth_provider_account")
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

provider + providerId 조합에 Unique 제약 조건 추가를 권장합니다

동일한 소셜 계정(provider + providerId 조합)은 DB에서 유일해야 합니다. 현재는 테이블 레벨에서 이 제약이 없어서 동시성 이슈나 버그로 인해 중복 레코드가 생성될 수 있습니다.

예를 들어, Google 계정 "12345"가 두 번 저장되는 상황을 방지해야 합니다.

🔧 Unique 제약 조건 추가 제안
 `@Entity`
-@Table(name = "auth_provider_account")
+@Table(name = "auth_provider_account",
+       uniqueConstraints = `@UniqueConstraint`(
+           name = "uk_provider_provider_id",
+           columnNames = {"provider", "provider_id"}
+       ))
 `@Getter`

이렇게 하면 DB 레벨에서 데이터 무결성이 보장되고, 중복 삽입 시도 시 예외가 발생하여 문제를 조기에 발견할 수 있습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/AuthProviderAccount.java`
around lines 11 - 12, Add a DB-level unique constraint for the social account
key (provider + providerId) to prevent duplicate records: modify the
AuthProviderAccount entity (class AuthProviderAccount) to declare a unique
constraint on the provider and providerId columns via the
`@Table`(uniqueConstraints=...) annotation using the actual column names (e.g.,
"provider" and the column name mapped from the providerId field), and also add a
corresponding DB migration (ALTER TABLE ... ADD CONSTRAINT ... UNIQUE or a
unique index) to enforce it in the database.

Comment on lines +6 to +8
public interface AuthProviderAccountRepository extends JpaRepository<AuthProviderAccount, Long> {
AuthProviderAccount findByProviderId(String username);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# AuthProviderAccountRepository 파일 위치 및 내용 확인
find . -name "AuthProviderAccountRepository.java" -type f

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 185


🏁 Script executed:

# 해당 파일의 전체 내용 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/AuthProviderAccountRepository.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 508


🏁 Script executed:

# findByProviderId 사용처 확인
rg -n "findByProviderId" --type=java -B2 -A2

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1423


🏁 Script executed:

# AuthProviderAccount 도메인 모델 확인
find . -name "AuthProviderAccount.java" -type f

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 171


🏁 Script executed:

# Provider enum/class 구조 확인
find . -name "Provider.java" -type f

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 157


🏁 Script executed:

# AuthProviderAccount 엔티티 전체 내용 확인
cat -n ./src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/AuthProviderAccount.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1476


🏁 Script executed:

# Provider enum 확인
cat -n ./src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/constant/Provider.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1171


🏁 Script executed:

# AuthProviderAccount 엔티티에 `@Unique` 또는 제약조건 확인
rg -n "AuthProviderAccount" --type=java -A20 | head -60

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 8292


🏁 Script executed:

# CustomOAuth2UserService에서 findByProviderId 사용 후 처리 방식 확인
rg -n "findByProviderId" --type=java -A5 | head -30

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1185


🏁 Script executed:

# CustomOAuth2UserService의 전체 loadUser 메서드 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/service/CustomOAuth2UserService.java | head -100

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 5351


🏁 Script executed:

# providerId 생성 및 저장 방식 전체 확인
rg -n "provider_" --type=java -B3 -A3

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 2677


🏁 Script executed:

# AuthProviderAccount에 복합 유니크 제약이 있는지 확인
rg -n "@UniqueConstraint|@Unique" --type=java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

# 데이터베이스 스키마 또는 마이그레이션 파일 확인
find . -name "*.sql" -o -name "*migration*" | head -20

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

# Liquibase 또는 Flyway 마이그레이션 파일 확인
find . -path "*/db/migration/*" -o -path "*/db/changelog/*" 2>/dev/null

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

# 전체 프로젝트 구조에서 SQL 파일 확인
find . -name "*.xml" -o -name "*.sql" | grep -i "liqui\|flyway\|migration" | head -20

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

# AuthAccountConverter의 저장 로직이 providerId를 어떻게 처리하는지 다시 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/mapper/AuthAccountConverter.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 832


🏁 Script executed:

# 혹시 다른 repository 메서드나 쿼리 메서드가 있는지 확인
rg -n "findBy" src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/ --type=java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 421


🏁 Script executed:

# 마지막 확인: providerId에 실제로 "google_123" 형태로 저장되는지 User entity도 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java | head -50

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1862


🏁 Script executed:

# OAuth2UserInfo 구조를 확인하여 providerId가 어떤 값인지 확인
find . -name "OAuth2UserInfo.java" -type f

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 161


🏁 Script executed:

# AuthAccountConverter 저장할 때 provider와 providerId가 어떻게 저장되는지 다시 확인
rg -n "AuthAccountConverter.toAuthProviderAccount" --type=java -B2 -A5

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1240


🏁 Script executed:

# 마지막 확인: OAuth2UserInfo에서 providerId가 combined string인지 확인
cat -n ./src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/OAuth2UserInfo.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 548


파라미터명과 반환 타입을 개선해야 합니다

  1. 파라미터명 오류: findByProviderId(String username)에서 파라미터명이 username으로 되어 있는데, 실제로는 "google_123" 형태의 combined providerId입니다. 파라미터명을 providerId로 수정해주세요.

  2. Optional 반환 권장: 현재는 null을 반환하는데, 59번 라인의 if (existAccount == null) 체크로 NPE 위험이 있습니다. Optional<AuthProviderAccount> findByProviderId(String providerId)로 변경하면 null safe한 코드가 됩니다.

  3. 설계 개선 제안: 현재 providerId 컬럼에 "google_123" 같은 combined 문자열을 저장하고 있는데, 엔티티는 provider(enum)도 따로 가지고 있어서 데이터 중복이 발생합니다. 다음처럼 개선하면 더 명확합니다:

🔧 개선된 Repository 방식
 public interface AuthProviderAccountRepository extends JpaRepository<AuthProviderAccount, Long> {
-    AuthProviderAccount findByProviderId(String username);
+    Optional<AuthProviderAccount> findByProviderAndProviderId(Provider provider, String providerId);
 }

그러면 저장할 때도:

// 현재 (combined string)
AuthProviderAccount account = AuthAccountConverter.toAuthProviderAccount(
    provider, "google_123", user
);

// 개선된 방식 (분리된 데이터)
AuthProviderAccount account = AuthAccountConverter.toAuthProviderAccount(
    provider, "123", user  // providerId는 순수 ID만
);

이렇게 하면 데이터베이스 정규화가 개선되고, provider와 providerId의 책임이 명확해집니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/AuthProviderAccountRepository.java`
around lines 6 - 8, In AuthProviderAccountRepository, change the method
signature from findByProviderId(String username) to
Optional<AuthProviderAccount> findByProviderId(String providerId) (rename
parameter to providerId and return Optional to avoid nulls); then update all
callers (e.g., places checking existAccount == null) to handle Optional properly
(isPresent()/orElseThrow()/ifPresent) and adjust AuthAccountConverter usage so
it stores/queries the raw providerId (e.g., "123") alongside the provider enum
rather than a combined "google_123" string to avoid duplicated data
responsibility.

Comment on lines +16 to +19
@Override
public Map<String, Object> getAttributes() {
return null;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getAttributes()가 null을 반환하면 NPE 위험이 있습니다.

Spring Security 내부 또는 다른 컴포넌트에서 getAttributes()를 호출할 때 null을 반환하면 NullPointerException이 발생할 수 있어요. 빈 Map을 반환하는 것이 더 안전합니다.

🐛 수정 제안
     `@Override`
     public Map<String, Object> getAttributes() {
-        return null;
+        return Map.of();
     }
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/CustomOAuth2User.java`
around lines 16 - 19, The getAttributes() override in CustomOAuth2User currently
returns null which can cause NPEs; change CustomOAuth2User.getAttributes() to
return an immutable empty Map (e.g., Collections.emptyMap()) or a new empty Map
instance instead of null so callers never receive null; update the method
implementation in the CustomOAuth2User class to return a non-null empty map.

Comment on lines +30 to +38
@Override
public String getEmail() {
return String.valueOf(kakaoAccount.get("email"));
}

@Override
public String getName() {
return String.valueOf(profile.get("nickname"));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

NullPointerException 위험이 있어요! 🚨

kakaoAccountprofile이 null일 경우 getEmail()getName()에서 NPE가 발생합니다. 카카오 로그인에서 사용자가 이메일 제공에 동의하지 않으면 kakaoAccount.get("email")이 null을 반환하거나, kakaoAccount 자체가 없을 수 있어요.

예를 들어, 사용자가 카카오 로그인 시 "이메일 제공 선택 안함"을 선택하면 서버가 500 에러를 반환하게 됩니다.

🐛 Null 안전성 개선 제안
 `@Override`
 public String getEmail() {
+    if (kakaoAccount == null) {
+        return null;
+    }
+    Object email = kakaoAccount.get("email");
+    return email != null ? String.valueOf(email) : null;
-    return String.valueOf(kakaoAccount.get("email"));
 }

 `@Override`
 public String getName() {
+    if (profile == null) {
+        return null;
+    }
+    Object nickname = profile.get("nickname");
+    return nickname != null ? String.valueOf(nickname) : null;
-    return String.valueOf(profile.get("nickname"));
 }
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/KaKaoResponse.java`
around lines 30 - 38, getEmail()과 getName() 에서 kakaoAccount 또는 profile 이 null이거나
내부 값이 null일 때 NPE(또는 "null" 문자열 반환)가 발생합니다; 수정 방법: getEmail()과 getName()에서
kakaoAccount 및 profile 객체가 null인지 검사하고, 내부 Map.get("email") /
Map.get("nickname") 결과도 null인지 확인한 뒤 안전한 기본값(예: null 또는 빈 문자열)이나 Optional로 반환하도록
변경하세요; 관련 식별자: 메서드 getEmail(), getName(), 필드 kakaoAccount, profile을 찾아 해당 null
체크 로직을 추가하세요.

Comment on lines +11 to +14
// 네이버는 response라는 안에 실제 데이터 있어서 추출해서 저장
public NaverResponse(Map<String, Object> attribute) {
this.attribute = (Map<String, Object>) attribute.get("response");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

생성자에서 NPE 위험이 있어요! 🚨

attribute.get("response")가 null을 반환하면 this.attribute가 null이 되어, 이후 모든 getter 호출에서 NPE가 발생합니다. 네이버 API 응답이 예상과 다른 형식일 경우 로그인 전체가 실패하게 됩니다.

🐛 Null 체크 추가 제안
 // 네이버는 response라는 안에 실제 데이터 있어서 추출해서 저장
 public NaverResponse(Map<String, Object> attribute) {
-    this.attribute = (Map<String, Object>) attribute.get("response");
+    Object response = attribute.get("response");
+    if (response == null) {
+        throw new IllegalArgumentException("Naver OAuth2 response is missing 'response' field");
+    }
+    this.attribute = (Map<String, Object>) response;
 }
📝 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
// 네이버는 response라는 안에 실제 데이터 있어서 추출해서 저장
public NaverResponse(Map<String, Object> attribute) {
this.attribute = (Map<String, Object>) attribute.get("response");
}
// 네이버는 response라는 안에 실제 데이터 있어서 추출해서 저장
public NaverResponse(Map<String, Object> attribute) {
Object response = attribute.get("response");
if (response == null) {
throw new IllegalArgumentException("Naver OAuth2 response is missing 'response' field");
}
this.attribute = (Map<String, Object>) response;
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/NaverResponse.java`
around lines 11 - 14, The NaverResponse constructor sets this.attribute =
(Map<String, Object>) attribute.get("response") which can be null and later
cause NPEs in the class's getters; update the NaverResponse(Map<String,Object>
attribute) constructor to check attribute.get("response") for null and handle it
safely (e.g., assign Collections.emptyMap() or a new HashMap<>() as a default,
or throw a clear IllegalArgumentException) so subsequent getter methods on the
NaverResponse instance won't throw NPEs; ensure you reference the constructor in
class NaverResponse and the field attribute when making the change.

Comment on lines +26 to +34
@Override
public String getEmail() {
return String.valueOf(attribute.get("email"));
}

@Override
public String getName() {
return String.valueOf(attribute.get("name"));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getter 메서드들도 null 안전성 고려가 필요해요.

KaKaoResponse와 동일하게, attribute.get("email")이나 attribute.get("name")이 null일 경우 "null" 문자열이 반환됩니다. 네이버에서 이메일 제공 동의를 하지 않은 사용자의 경우 DB에 "null"이라는 문자열이 저장될 수 있어요.

🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/NaverResponse.java`
around lines 26 - 34, NaverResponse's getEmail and getName currently call
String.valueOf(attribute.get(...)) which turns null into the literal "null";
change both getEmail() and getName() in class NaverResponse to mirror
KaKaoResponse's null-safety by checking attribute.get("email") and
attribute.get("name") for null and returning null (or an empty string if your
app expects that) instead of the string "null" — specifically, retrieve the
value from attribute, if it's non-null return its toString(), otherwise return
null; update getEmail() and getName() accordingly.

import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import com.whereyouad.WhereYouAd.global.security.oauth2.dto.OAuth2Response;

public class UserConverter {
Copy link
Collaborator

Choose a reason for hiding this comment

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

P4: domains/user/application/mapper 밑에 제가 회원가입 DTO 변환할 때 사용한 UserConverter 클래스랑 이름이 겹쳐서 헷갈릴 수 있을 것 같은데 OAuthConverter 등으로 변경하면 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아 그렇군요 제가 위치를 착각했던 것 같습니다! 수정하겠습니다! 감사합니다!


// 로그인 종류에 따라 이메일(식별자) 추출
String email;
Object principal = authentication.getPrincipal();
Copy link
Collaborator

Choose a reason for hiding this comment

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

P4: 기존 로그인이랑 소셜 로그인 구현 구분 꼼꼼히 잘해주신거 같아요!!

Copy link
Collaborator

@ojy0903 ojy0903 left a comment

Choose a reason for hiding this comment

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

P4: 전체적으로 꼼꼼하게 잘 해주신 것 같습니다! User 전화번호 nullable 허용한거도 기존 회원가입이나 로그인 로직에 전화번호 관한 검증은 없어서 오류 없을 거 같아요. 로그인 성공 후 Redirect 경로는 이후에 프론트쪽이랑 연결할때 수정할 필요 있으면 진행하고 지금은 이대로 해도 좋을 것 같아요! 고생하셨습니다

Copy link
Collaborator

@jinnieusLab jinnieusLab left a comment

Choose a reason for hiding this comment

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

P4: 소셜 로그인 로직이 깔끔하게 짜져 있어서 흐름이 잘 이해되네요!!
이후에 조직 관리 구현 시 회원 탈퇴 부분 함께 구현하면서 소셜 revoke 하는 부분만 추가하면 될 듯 합니다!
준영님이 말씀해주신 부분 참고해주시고 수정 사항 반영 후 머지하면 될 것 같습니다! 고생하셨습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

P4: yml 설정도 깔끔하네요!

@kingmingyu kingmingyu merged commit deeee7a into develop Jan 29, 2026
2 checks passed
This was referenced Jan 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 소셜 로그인 구현 - 네이버, 카카오, 구글

3 participants