Skip to content

[Feat] 카카오/구글 소셜 로그인 구현#20

Merged
kallin1 merged 11 commits into
developfrom
feat/#15-implement-social-login
Jul 22, 2026
Merged

[Feat] 카카오/구글 소셜 로그인 구현#20
kallin1 merged 11 commits into
developfrom
feat/#15-implement-social-login

Conversation

@kallin1

@kallin1 kallin1 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

체크리스트

  • 병합 대상 브랜치가 올바른지 확인했습니다. (develop 또는 팀에서 합의한 개발 브랜치)
  • PR 제목을 컨벤션에 맞게 작성했습니다. 예: [Feat] 프롬프트 언락 기능 추가
  • 브랜치명을 컨벤션에 맞게 작성했습니다. 예: feat/#2-entity-config-setting
  • 관련 이슈를 연결했습니다.
  • 로컬에서 ./gradlew test 또는 ./gradlew build를 실행했습니다.
  • API 응답에 민감 정보, 토큰, 유료 프롬프트 원문이 노출되지 않는지 확인했습니다.

연관 이슈

작업 내용

카카오/구글 계정으로 회원가입·로그인할 수 있는 소셜 로그인 기능을 구현했습니다. 기존 AccessTokenProvider/RefreshTokenProvider를 그대로 재사용해 일반 로그인과 동일한 LoginInfo/LoginResponse 포맷으로 응답합니다.

  1. SocialLoginClient 포트 + KakaoSocialLoginClient/GoogleSocialLoginClient 어댑터로 인가 코드 → 액세스 토큰 → 사용자 정보 조회를 구현 (Spring Security OAuth2 Client 대신 RestClient 기반 커스텀 어댑터 방식 채택).
  2. SocialAccount 애그리거트(social_accounts 테이블, provider+providerUserId unique 제약)로 소셜 계정 연동 정보를 분리 관리. Userprovider 컬럼을 직접 추가하지 않고, 소셜 가입 시 랜덤 placeholder 비밀번호를 인코딩해 저장하는 방식으로 처리.
  3. SocialAuthCommandService에서 최초 로그인 시 신규 User 생성 + 계정 연동, 이후 재로그인 시 기존 User로 로그인되도록 분기 처리.
  4. 동일 이메일로 이미 일반(비밀번호) 가입된 계정이 있는 경우, 자동 연동하지 않고 DUPLICATE_EMAIL로 차단하는 정책을 적용.
  5. AuthErrorCodeUNSUPPORTED_OAUTH_PROVIDER, OAUTH_AUTHENTICATION_FAILED, OAUTH_EMAIL_NOT_AVAILABLE, SOCIAL_ACCOUNT_ALREADY_LINKED를 추가해 실패 케이스를 명시적으로 응답.
  6. (보강) 카카오 is_email_valid/is_email_verified, 구글 email_verified 플래그를 확인하지 않아 미검증 이메일로도 계정이 생성될 수 있던 문제를 수정.
  7. (보강) OAuth 클라이언트 RestClient에 connect/read 타임아웃이 없어 provider 응답 지연 시 요청 스레드가 무한 대기할 수 있던 문제를 수정 (OAuthRestClientFactory).
  8. SocialAuthCommandService에 대한 유닛 테스트가 없어 신규가입/재로그인/비활성 유저 차단/미지원 provider/클라이언트 예외 변환 케이스를 추가.

변경 범위

  • auth
  • user
  • prompt
  • commerce
  • community
  • moderation
  • tracking
  • admin
  • common
  • global
  • 문서 / 설정 / 배포 (.env.example, 배포 환경변수 수정)

리뷰 중점사항

  • 동일 이메일 충돌 시 "자동 연동하지 않고 차단"하는 정책이 맞는지 (다른 정책이 필요하면 별도 이슈로 분리 제안)
  • 아직 실서비스 도메인이 없어 배포용 Redirect URI 등록이 안 된 상태입니다. 도메인 구매 후 카카오/구글 개발자 콘솔의 Redirect URI 및 .env 값을 갱신할 예정입니다.

테스트

  • ./gradlew test (auth 도메인 전체 + 전체 스위트 통과)
  • ./gradlew build
  • 별도 확인: 로컬에서 localhost Redirect URI로 카카오/구글 인가 코드 발급 → /api/v1/auth/oauth/{provider} 호출까지 수동 검증

스크린샷 / 응답 예시

없음 (실제 Client ID/Secret은 각자 로컬 .env에 설정 필요)

kallin1 added 7 commits July 13, 2026 23:37
프론트에서 전달받은 인가 코드로 카카오 사용자 정보를 조회하고, 최초 로그인 시
자동 회원가입 후 기존 로그인과 동일한 JWT 발급 파이프라인으로 토큰을 발급한다.
카카오 소셜 로그인과 동일한 SocialLoginClient 포트 구조를 재사용해
구글 인가 코드 교환 및 사용자 정보 조회를 추가한다.
카카오/구글 소셜 로그인 추가로 KAKAO_CLIENT_ID, GOOGLE_CLIENT_ID 등
필수 환경변수가 늘어나 로컬 부팅에 필요한 값을 한눈에 정리한다.
docker run에 -e 플래그가 전혀 없어 JWT/Swagger/카카오/구글 시크릿이 컨테이너에
전달되지 않았다. GitHub Secrets를 SSH로 전달해 컨테이너 실행 시 주입하고,
SPRING_PROFILES_ACTIVE=prod도 함께 활성화한다. 로컬에서는 .env가 있으면
bootRun에만 자동 주입하도록 build.gradle에 보조 설정을 추가한다.
카카오 is_email_valid/is_email_verified, 구글 email_verified 플래그를 확인하지 않아
미검증 이메일로도 계정이 생성될 수 있던 문제를 수정하고, OAuth 클라이언트 호출에
connect/read 타임아웃이 없어 provider 응답 지연 시 스레드가 무한 대기하던 문제를 해결.
신규 가입/기존 계정 재로그인/비활성 유저 차단/미지원 provider/클라이언트 예외 변환 등
소셜 로그인 usecase의 핵심 분기에 대한 테스트 커버리지가 없던 것을 보완.
@kallin1
kallin1 changed the base branch from main to develop July 17, 2026 15:05

@Hanharam Hanharam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

고생하셨습니다! 👍

추후에 계정연동 로직 추가한다면 이렇게 하면 좋을 것 같습니다!

sequenceDiagram
    participant B as "Browser"
    participant N as "Next.js Route Handler"
    participant O as "Google/Kakao"
    participant S as "Spring API"
    participant D as "Database / Redis"

    B->>N: GET /api/auth/oauth/google/start
    N->>N: state + PKCE 생성
    N->>D: state·codeVerifier·만료시간 저장
    N-->>B: Provider authorize URL로 redirect

    B->>O: 로그인 및 동의
    O-->>N: GET callback?code=...&state=...

    N->>D: 저장된 OAuth transaction 조회
    N->>N: state·만료·일회성 검증
    N->>S: code + codeVerifier + 고정된 redirectUri

    S->>O: Authorization Code 토큰 교환
    O-->>S: Provider Access Token

    S->>O: Provider 사용자 정보 요청
    O-->>S: providerUserId + 검증된 email + name

    S->>D: social_accounts에서 provider 계정 조회

    alt 이미 소셜 계정이 연동됨
        D-->>S: 연결된 userId
        S->>D: User 상태 조회
        S->>S: PromSearch Access/Refresh Token 발급
        S-->>N: 로그인 성공 + 서비스 토큰
        N-->>B: HttpOnly 쿠키 설정 후 서비스로 redirect

    else 연동된 소셜 계정 없음
        S->>D: 검증된 email로 기존 User 조회

        alt 동일 이메일 User도 없음
            S->>D: User + SocialAccount 트랜잭션 생성
            S->>S: PromSearch Access/Refresh Token 발급
            S-->>N: 신규 가입·로그인 성공
            N-->>B: HttpOnly 쿠키 설정 후 온보딩으로 redirect

        else 동일 이메일의 기존 User 존재
            S->>D: 일회성 AccountLinkTicket 저장
            Note over S,D: existingUserId, provider,<br/>providerUserId, email,<br/>expiresAt, used=false
            S-->>N: ACCOUNT_LINK_REQUIRED + linkTicket
            N-->>B: 임시 HttpOnly 쿠키 저장 후<br/>기존 계정 로그인 화면으로 redirect

            B->>N: 기존 이메일·비밀번호 제출
            N->>S: POST /api/v1/auth/login
            S->>D: 기존 계정 자격 증명 검증

            alt 기존 계정 인증 실패
                S-->>N: AUTH-001
                N-->>B: 로그인 실패 응답

            else 기존 계정 인증 성공
                S-->>N: 기존 계정 서비스 토큰
                Note over N: 아직 Browser 쿠키로 확정하지 않고<br/>연동 요청에 사용

                N->>S: POST /api/v1/auth/social-accounts/link<br/>Bearer Access Token + linkTicket
                S->>D: AccountLinkTicket 조회 및 잠금
                S->>S: 만료·일회성·사용자 일치 검증

                alt 연동 검증 실패
                    S-->>N: 만료 또는 잘못된 연동 요청
                    N-->>B: 소셜 로그인 재시작 안내

                else 연동 검증 성공
                    S->>D: SocialAccount 생성 + ticket 사용 처리
                    Note over S,D: 하나의 트랜잭션으로 처리
                    S-->>N: 계정 연동 성공
                    N-->>B: HttpOnly 서비스 쿠키 설정 후 redirect
                end
            end
        end
    end
Loading

연동 이후에는 그냥 로그인 가능합니다

sequenceDiagram
    participant B as "Browser"
    participant N as "Next.js Route Handler"
    participant O as "Google/Kakao"
    participant S as "Spring API"
    participant D as "Database"

    B->>N: 소셜 로그인 시작
    N-->>B: Provider로 redirect
    B->>O: 로그인 및 동의
    O-->>N: code + state
    N->>S: code + codeVerifier
    S->>O: 토큰 교환 및 사용자 정보 조회
    O-->>S: providerUserId + email

    S->>D: provider + providerUserId 조회
    D-->>S: 기존 userId 반환

    S->>D: User 상태 확인
    S->>S: PromSearch Access/Refresh Token 발급
    S-->>N: 로그인 성공
    N-->>B: HttpOnly 쿠키 설정 후 redirect
Loading

private final TokenHasher tokenHasher;

@Override
@Transactional

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

현재는 카카오·구글 API 응답을 기다리는 동안에도 DB 트랜잭션이 계속 유지됩니다. 외부 API 호출은 트랜잭션 없이 먼저 완료하고, 회원 생성·소셜 계정 연결·Refresh Token 저장처럼 실제 DB 작업만 별도 서비스의 @transactional 메서드에서 처리하면 좋을 것 같습니다!
같은 클래스의 private 메서드로 분리하면 @transactional이 적용되지 않을 수 있으므로, DB 저장 로직은 별도 Spring Bean으로 분리하는 방식이 안전합니다. 이 구조로 변경한다면 클래스 레벨의 @transactional(readOnly = true)도 없애면 좋을 것 같습니다!

@Service
@RequiredArgsConstructor
public class SocialLoginTransactionService {

    @Transactional
    public TokenInfo completeLogin(
            SocialProvider provider,
            SocialUserInfo userInfo
    ) {
        // 회원 조회 또는 생성
        // 소셜 계정 연결
        // 토큰 생성
        // Refresh Token 세션 저장

        return tokenInfo;
    }
}

이런식으로 분리하면 좋을 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

말씀하신 대로 외부 API 호출까지 트랜잭션 안에서 처리되면, 카카오·구글 응답을 기다리는 동안 DB 커넥션이 계속 점유되어 커넥션 풀 고갈로 이어질 수 있는 문제가 있어 아래와 같이 분리했습니다.

SocialAuthCommandService: 트랜잭션 없이 client.exchangeCodeAndFetchUserInfo(...)로 외부 API 호출을 먼저 완료하고, 그 결과(SocialUserInfo)만 넘기도록 했습니다. 더 이상 DB 트랜잭션이 필요 없어 클래스 레벨 @transactional(readOnly = true)도 제거했습니다.
SocialLoginTransactionService: 회원 조회/생성, 소셜 계정 연결, Refresh Token 저장처럼 실제 DB 작업만 담당하는 별도 Spring Bean으로 분리하고, completeLogin()에 @transactional을 적용했습니다. 말씀하신 대로 private 메서드 분리는 self-invocation 문제로 트랜잭션이 적용되지 않을 수 있어, 별도 클래스로 분리하는 방식을 택했습니다.

확인 부탁드립니다!

Long userId = socialAccountRepository
.findByProviderAndProviderUserId(command.provider(), socialUserInfo.providerUserId())
.map(SocialAccount::getUserId)
.orElseGet(() -> provisionSocialUser(command.provider(), socialUserInfo));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

동일 소셜 계정의 최초 로그인 요청이 동시에 들어오면 두 요청 모두 연동 계정 조회에 실패하고 신규 회원 생성을 시도할 수 있습니다. 이때 DB unique 제약으로 중복 데이터는 방지되지만, 한 요청은 이메일·닉네임 또는 (provider, providerUserId) 충돌로 실패하게 됩니다.

  1. A와 B 모두 SocialAccount 조회 결과가 없음
  2. 두 요청 모두 신규 회원 생성을 시도
  3. A가 User와 SocialAccount 생성에 성공
  4. B는 users.email, users.nickname 또는 (provider, providerUserId) unique 제약에서 충돌

DB unique 제약으로 중복 데이터는 생기지 않지만, B 요청은 로그인 실패 응답을 받게 됩니다.

생성 충돌이 발생하면 해당 트랜잭션을 rollback한 뒤 새로운 트랜잭션에서 (provider, providerUserId)를 다시 조회하고, 다른 요청이 생성한 연동 계정이 확인되면 해당 User로 로그인을 이어가는 재시도 경로를 고려하면 좋을 것 같습니다! 재조회 결과가 없다면 실제 중복 이메일/닉네임이므로 기존 오류를 반환해야 할 것 같습니다.

  • 재조회 결과가 있으면 다른 동시 요청이 계정을 생성한 것이므로 해당 User로 로그인
  • 재조회 결과가 없으면 실제 기존 이메일/닉네임 중복이므로 원래 오류 반환

이렇게 하면 DB에는 User와 SocialAccount가 하나만 생성되면서, 동시에 들어온 로그인 요청도 실패 화면 대신 정상 로그인을 띄워줄 수 있습니다!

그래서 결국 사용자 UX를 생각한건데 이런 상황은 거의 없을 것 같아서 추후에 필요하면 수정해도 좋을 것 같습니다!

return client.fetchUserInfo(command.authorizationCode(), command.redirectUri());
} catch (AuthDomainException e) {
throw e;
} catch (RuntimeException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

외부 API 오류는 실제 HTTP 응답을 확인할 수 있는 GoogleSocialLoginClient와 KakaoSocialLoginClient에서 반환하는 것이 역할상 자연스러워 보입니다.

지금은 Service에서 모든 RuntimeException을 인증 실패로 변환하면, 만료된 인가 코드뿐 아니라 제공자 장애·타임아웃·응답 변환 실패와 NPE 같은 내부 코드 오류까지 모두 401로 처리됩니다.

GoogleSocialLoginClientKakaoSocialLoginClient는 실제 외부 API를 호출하기 때문에 이런 예외들을 알고 있습니다.

  • 인가 코드가 잘못됐는지
  • 인가 코드가 만료됐는지
  • 구글·카카오 서버에 장애가 발생했는지
  • 연결 시간이 초과됐는지
  • 응답 형식이 예상과 다른지

그래서 SocialAuthCommandServicefetchUserInfo()가 실패했을 때 예외만 알고 있으면 좋을 것 같습니다.

SocialAuthCommandService
  → 소셜 로그인 흐름만 조율

Google/KakaoSocialLoginClient
  → 외부 API 호출
  → HTTP 상태와 타임아웃을 해석
  → 서비스에서 이해할 수 있는 예외로 변환

이런식으로 책임을 분리하면 좋을 것 같습니다!

try {
    return requestUserInfo();
} catch (HttpClientErrorException e) {
    if (e.getStatusCode().value() == 429) {
        throw new AuthDomainException(OAUTH_PROVIDER_RATE_LIMITED);
    }

    if (Set.of(400, 401, 403).contains(e.getStatusCode().value())) {
        throw new AuthDomainException(OAUTH_AUTHENTICATION_FAILED);
    }

    throw new AuthDomainException(OAUTH_PROVIDER_BAD_RESPONSE);
} catch (HttpServerErrorException | ResourceAccessException e) {
    throw new AuthDomainException(OAUTH_PROVIDER_UNAVAILABLE);
} catch (RestClientException e) {
    throw new AuthDomainException(OAUTH_PROVIDER_BAD_RESPONSE);
}

소셜 로그인 클라이언트에서는 이런식으로 예외를 잡으면 좋습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

리뷰 감사합니다! HTTP 상태 코드나 타임아웃 여부 같은 정보는 실제로 외부 API를 호출하는 Google/KakaoSocialLoginClient만 알 수 있으니, 말씀하신 대로 예외 변환 책임을 Client로 옮기는 방향으로 수정했습니다.

SocialAuthCommandService: client.exchangeCodeAndFetchUserInfo()를 호출만 하고, 예외를 별도로 감싸지 않도록 try-catch를 제거했습니다. (내부에서 발생한 예외가 AuthDomainException이면 그대로 전파됩니다.)
Google/KakaoSocialLoginClient: 제안해주신 것처럼 HttpClientErrorException, HttpServerErrorException, ResourceAccessException, RestClientException을 구분해서 각각 OAUTH_PROVIDER_RATE_LIMITED, OAUTH_AUTHENTICATION_FAILED, OAUTH_PROVIDER_UNAVAILABLE, OAUTH_PROVIDER_BAD_RESPONSE로 변환해서 던지도록 반영했습니다.

덕분에 책임 분리가 명확해진 것 같습니다. 감사합니다!


@Override
public SignupInfo registerSocialUser(RegisterSocialUserCommand command) {
validateDuplicateEmail(command.email());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

추후 계정 연동 기능을 넣는다면 기존 계정 재인증을 하고 소셜 로그인 전용 409 응답과 함께 “기존 방식으로 로그인 후 계정 연결” 같은 다음 행동을 안내하면 좋을 것 같습니다!

public SocialAccount save(SocialAccount socialAccount) {
try {
return socialAccountJpaRepository.saveAndFlush(SocialAccountJpaEntity.from(socialAccount)).toDomain();
} catch (DataIntegrityViolationException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

DataIntegrityViolationException은 소셜 계정 중복 외에도 FK, NOT NULL, 컬럼 길이 위반 등 여러 DB 오류에서 발생할 수 있습니다. 지금처럼 모든 경우를 SOCIAL_ACCOUNT_ALREADY_LINKED로 변환하면 실제 코드나 데이터 문제가 모두 소셜 계정 중복으로 반환될 것 같습니다.

(provider, provider_user_id)의 UNIQUE 제약조건 이름을 확인해 해당 충돌만 “이미 연결된 계정”으로 변환하고, 나머지 DB 오류는 그대로 전달하는 것이 좋을 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

리뷰 감사합니다! 말씀하신 제약조건 이름으로 구분하는 방법도 고려해봤는데, DB 벤더마다 제약조건 이름 규칙이 다르고 이름이 바뀌면 코드가 깨질 수 있을 것 같아서, 실제 데이터 조회로 원인을 검증하는 방식으로 수정해봤습니다.

DataIntegrityViolationException 발생 시 (provider, providerUserId) 조합이 실제로 DB에 존재하는지 existsByProviderAndProviderUserId로 재조회해서,

존재하면 유니크 제약 위반이 맞으므로 SOCIAL_ACCOUNT_ALREADY_LINKED로 변환해서 던지고
존재하지 않으면 다른 원인(FK, NOT NULL 등)일 수 있으므로 원본 예외를 그대로 던지도록 했습니다.

이 방향에 대해 어떻게 생각하시는지 의견 부탁드립니다!


SocialProvider provider();

SocialUserInfo fetchUserInfo(String authorizationCode, String redirectUri);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fetchUserInfo는 실제로 인가 코드→토큰 교환과 사용자 정보 조회를 모두 수행하고 있어서, exchangeCodeAndFetchUserInfo처럼 전체 동작을 알 수 있는 메서드명이면 더 좋을 것 같습니다!

@kallin1 kallin1 Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

확실히 수정하는게 더 명확한 것 같습니다 반영하겠습니다!

kallin1 added 4 commits July 20, 2026 23:15
…fo로 이름 변경

인가 코드 교환과 사용자 정보 조회를 모두 수행하는 메서드임을 명확히 드러내기 위해 이름을 변경했습니다.
SocialAuthCommandService가 모든 RuntimeException을 일괄적으로 인증 실패(401)로 변환하던 방식은
제공자 장애·타임아웃·응답 형식 오류 같은 서로 다른 원인을 구분하지 못하고, 코드 버그로 인한
예외까지 감춰버리는 문제가 있었습니다.

실제 HTTP 응답을 다루는 Kakao/GoogleSocialLoginClient에서 HttpClientErrorException(429는
OAUTH_PROVIDER_RATE_LIMITED, 400/401/403은 OAUTH_AUTHENTICATION_FAILED, 그 외는
OAUTH_PROVIDER_BAD_RESPONSE), HttpServerErrorException/ResourceAccessException은
OAUTH_PROVIDER_UNAVAILABLE로 번역하도록 OAuthExceptionTranslator를 도입했습니다.
Service는 더 이상 클라이언트 예외를 가로채지 않고, 알 수 없는 런타임 예외는 그대로 전파됩니다.
DataIntegrityViolationException은 (provider, provider_user_id) 유니크 제약 위반 외에도
FK, NOT NULL, 컬럼 길이 위반 등 다양한 원인으로 발생할 수 있습니다. 모든 경우를 무조건
SOCIAL_ACCOUNT_ALREADY_LINKED로 변환하면 실제 데이터/코드 문제가 가려집니다.

UserPersistenceAdapter와 동일하게, 예외 발생 시 (provider, providerUserId) 존재 여부를
다시 조회해 실제 중복 연동인 경우에만 SOCIAL_ACCOUNT_ALREADY_LINKED로 변환하고,
그 외에는 원본 예외를 그대로 전파하도록 수정했습니다.
기존에는 SocialAuthCommandService 클래스 레벨에 @transactional(readOnly = true)가 걸려 있어,
카카오/구글 API 응답을 기다리는 동안에도 DB 트랜잭션이 유지되는 문제가 있었습니다.

외부 API 호출(exchangeCodeAndFetchUserInfo)은 트랜잭션 없이 먼저 수행하고, 회원 조회/생성,
소셜 계정 연동, 토큰 생성, Refresh Token 세션 저장처럼 실제 DB 작업만 별도 Bean인
SocialLoginTransactionService의 @transactional 메서드에서 처리하도록 분리했습니다.
SocialAuthCommandService에는 더 이상 트랜잭션이 걸리지 않습니다.

@Hanharam Hanharam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍 고생하셨습니다! 추후에 소셜 로그인 방식은 프론트 로그인 구조 보고 변경해야할 수도 있을 것 같습니다!

@kallin1
kallin1 merged commit 41e0b35 into develop Jul 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 소셜로그인 구현

2 participants