소셜 로그인 & 소셜 회원가입 절차 도식화

⚠️ 보안 주의사항

중요: 이 문서의 모든 API 키와 시크릿 값들은 예시용 플레이스홀더입니다.

📋 목차

  1. 전체 아키텍처 개요
  2. 백엔드 구현 세부사항
  3. 프론트엔드 구현 가이드
  4. 플랫폼별 상세 플로우
  5. 데이터 구조 및 API 스펙
  6. 테스트 및 디버깅 가이드

🏗️ 전체 아키텍처 개요

시스템 구조

graph TB
    subgraph "프론트엔드 계층"
        WEB[웹 브라우저]
        MOBILE[모바일 앱]
        ADMIN[관리자 페이지]
    end
    
    subgraph "소셜 플랫폼"
        GOOGLE[Google OAuth 2.0]
        KAKAO[카카오 로그인]
        NAVER[네이버 로그인]
    end
    
    subgraph "AuthService (포트: 8082)"
        CONTROLLER[SocialAuthController]
        SERVICE[SocialAuthService]
        GOOGLE_SVC[GoogleAuthService]
        KAKAO_SVC[KakaoAuthService]
        NAVER_SVC[NaverAuthService]
        USER_SVC[SocialUserService]
    end
    
    subgraph "데이터 저장소"
        MYSQL[(MySQL Database)]
        REDIS[(Redis Cache)]
    end
    
    WEB --> CONTROLLER
    MOBILE --> CONTROLLER
    ADMIN --> CONTROLLER
    
    CONTROLLER --> SERVICE
    SERVICE --> GOOGLE_SVC
    SERVICE --> KAKAO_SVC
    SERVICE --> NAVER_SVC
    SERVICE --> USER_SVC
    
    GOOGLE_SVC --> GOOGLE
    KAKAO_SVC --> KAKAO
    NAVER_SVC --> NAVER
    
    USER_SVC --> MYSQL
    SERVICE --> REDIS

핵심 개념: 통합 소셜 인증 시스템


🔧 백엔드 구현 세부사항

1. 컨트롤러 계층 (SocialAuthController)

파일: com.akc.b2c.authservice.controller.SocialAuthController

주요 엔드포인트:

@RestController
@RequestMapping("/api/auth/social")
public class SocialAuthController {
    
    // 1. 통합 API 엔드포인트들
    @PostMapping("/google")    // Google ID Token 처리
    @PostMapping("/kakao")     // Kakao Access Token 처리  
    @PostMapping("/naver")     // Naver Access Token 처리
    @PostMapping("/signup")    // 소셜 회원가입 통합 처리
    
    // 2. 네이버 OAuth 플로우 (팝업 전용)
    @GetMapping("/naver/login")      // 네이버 인증 시작
    @GetMapping("/naver/callback")   // 네이버 콜백 처리
    
    // 3. 헬스체크 & 메타데이터
    @GetMapping("/providers")  // 지원 플랫폼 목록
    @GetMapping("/health")     // 서비스 상태
}

의존성 주입:

private final SocialAuthService socialAuthService;      // 통합 로직 처리
private final NaverAuthService naverAuthService;        // 네이버 OAuth 전용
private final SocialUserService socialUserService;     // DB 사용자 관리
private final JwtTokenProvider jwtTokenProvider;       // JWT 토큰 생성

2. 서비스 계층 구조

2.1 통합 서비스 (SocialAuthService)

파일: com.akc.b2c.authservice.service.social.SocialAuthService

핵심 메서드:

public class SocialAuthService {
    // 각 플랫폼별 로그인 처리 (동일한 구조)
    public SocialLoginResponse googleLogin(GoogleLoginRequest request)
    public SocialLoginResponse kakaoLogin(KakaoLoginRequest request)  
    public SocialLoginResponse naverLogin(NaverLoginRequest request)
    
    // 소셜 회원가입 통합 처리
    public SocialSignUpResponse socialSignUp(SocialSignUpRequest request)
}

공통 처리 로직:

  1. 소셜 플랫폼에서 사용자 정보 추출
  2. SocialUserService.findOrCreateUserBySocial() 호출
  3. JWT 토큰 생성 및 Redis 캐싱
  4. 통일된 응답 구조 반환

2.2 Google 인증 서비스 (GoogleAuthService)

파일: com.akc.b2c.authservice.service.social.GoogleAuthService

특징:

핵심 메서드:

public class GoogleAuthService {
    public SocialUserInfo getUserInfo(String idToken) {
        // 1. 개발모드 또는 더미토큰 체크
        if (devMode || isDummyToken(idToken)) {
            return createDummyGoogleUserInfo(idToken);
        }
        
        // 2. 실제 Google API 토큰 검증
        Map<String, Object> tokenInfo = verifyGoogleIdToken(idToken);
        
        // 3. 사용자 정보 추출 및 변환
        return extractUserInfoFromGoogle(tokenInfo);
    }
}

2.3 카카오 인증 서비스 (KakaoAuthService)

파일: com.akc.b2c.authservice.service.social.KakaoAuthService

특징:

핵심 메서드:

public class KakaoAuthService {
    public SocialUserInfo getUserInfo(String accessToken) {
        // 1. 개발모드 체크
        if (devMode) {
            return createDevModeUserInfo();
        }
        
        // 2. 카카오 User Info API 호출
        Map<String, Object> userInfo = callKakaoUserInfoApi(accessToken);
        
        // 3. 카카오 특유의 중첩 구조 파싱
        return parseKakaoUserInfo(userInfo);
    }
    
    private Map<String, Object> callKakaoUserInfoApi(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + accessToken);
        headers.set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
        
        // 카카오 API: https://kapi.kakao.com/v2/user/me
        ResponseEntity<Map> response = restTemplate.exchange(kakaoUserInfoUrl, HttpMethod.GET, entity, Map.class);
    }
}

2.4 네이버 인증 서비스 (NaverAuthService)

파일: com.akc.b2c.authservice.service.social.NaverAuthService

특징:

핵심 메서드:

public class NaverAuthService {
    // 1. OAuth 인증 URL 생성
    public String buildAuthorizationUrl(String redirectUri) {
        String state = UUID.randomUUID().toString();
        return UriComponentsBuilder.fromHttpUrl(naverAuthUrl)
                .queryParam("response_type", "code")
                .queryParam("client_id", naverClientId)
                .queryParam("redirect_uri", naverRedirectUri)
                .queryParam("scope", "name email profile_image")
                .queryParam("state", state)
                .build().toUriString();
    }
    
    // 2. Authorization Code → Access Token 교환
    public String getAccessToken(String authorizationCode, String state) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", naverClientId);
        params.add("client_secret", naverClientSecret);
        params.add("redirect_uri", naverRedirectUri);
        params.add("code", authorizationCode);
        params.add("state", state);
        
        // 네이버 Token API: https://nid.naver.com/oauth2.0/token
        ResponseEntity<Map> response = restTemplate.postForEntity(naverTokenUrl, params, Map.class);
    }
    
    // 3. Access Token으로 사용자 정보 조회
    public SocialUserInfo getUserInfo(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + accessToken);
        
        // 네이버 User Info API: https://openapi.naver.com/v1/nid/me
        ResponseEntity<Map> response = restTemplate.exchange(naverUserInfoUrl, HttpMethod.GET, entity, Map.class);
    }
}

2.5 사용자 관리 서비스 (SocialUserService)

파일: com.akc.b2c.authservice.service.SocialUserService

3단계 자동 판단 로직:

@Transactional
public UserSocialResult findOrCreateUserBySocial(SocialUserInfo socialUserInfo) {
    
    // 1단계: 소셜 ID로 기존 계정 찾기 (Provider + Social ID)
    Optional<MemberSocialAccount> existingSocialAccount = 
        memberSocialAccountRepository.findByProviderAndSocialId(provider, socialUserInfo.getSocialId());
    
    if (existingSocialAccount.isPresent()) {
        // 🟢 기존 소셜 계정 → 즉시 로그인
        return UserSocialResult.builder()
                .member(socialAccount.getMember())
                .isNewUser(false)
                .isNewSocialAccount(false)
                .build();
    }
    
    // 2단계: 이메일로 기존 사용자 찾기
    Optional<Member> existingMember = memberRepository.findActiveUserByEmail(socialUserInfo.getEmail());
    
    if (existingMember.isPresent()) {
        // 🔗 기존 사용자에게 소셜 계정 연결
        Member member = existingMember.get();
        MemberSocialAccount newSocialAccount = createSocialAccount(member, socialUserInfo, provider);
        member.addSocialAccount(newSocialAccount);
        
        return UserSocialResult.builder()
                .member(member)
                .isNewUser(false)
                .isNewSocialAccount(true)
                .build();
    }
    
    // 3단계: 완전히 새로운 사용자 생성
    Member newMember = createMember(socialUserInfo);
    Member savedMember = memberRepository.save(newMember);
    MemberSocialAccount newSocialAccount = createSocialAccount(savedMember, socialUserInfo, provider);
    
    return UserSocialResult.builder()
            .member(newMember)
            .isNewUser(true)
            .isNewSocialAccount(true)
            .build();
}

🖥️ 프론트엔드 구현 가이드

1. 프로젝트 설정 및 환경 구성

1.1 패키지 설치 (React/Next.js)

# TypeScript를 사용하는 경우
npm install --save-dev @types/google.accounts

# 상태 관리 (선택사항)
npm install zustand  # 또는 redux-toolkit

1.2 환경 변수 설정

# .env.local (Next.js) 또는 .env (React)
NEXT_PUBLIC_GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com
NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY=YOUR_KAKAO_JAVASCRIPT_KEY
NEXT_PUBLIC_AUTH_SERVICE_URL=http://localhost:8082
NEXT_PUBLIC_ENVIRONMENT=development  # development | production

1.3 TypeScript 타입 정의

// types/social-login.ts
export interface SocialLoginRequest {
  idToken?: string;
  accessToken?: string;
  deviceInfo: {
    platform: 'WEB_BROWSER' | 'MOBILE_APP' | 'ADMIN_WEB';
    deviceId: string;
    appVersion: string;
  };
}

export interface SocialLoginResponse {
  success: boolean;
  accessToken?: string;
  refreshToken?: string;
  tokenType?: string;
  expiresIn?: number;
  userInfo?: UserInfo;
  needsAdditionalInfo?: boolean;
  needsUsernameInput?: boolean;
  needsAccountLink?: boolean;
  message: string;
  error?: string;
}

export interface UserInfo {
  id: string;
  email: string;
  name: string;
  profileImageUrl?: string;
  role: string;
  isNewUser: boolean;
}

// Global type definitions for Google GSI
declare global {
  interface Window {
    google: {
      accounts: {
        id: {
          initialize: (config: GoogleConfig) => void;
          prompt: (callback?: (notification: any) => void) => void;
          renderButton: (element: HTMLElement, config: GoogleButtonConfig) => void;
          disableAutoSelect: () => void;
          cancel: () => void;
        };
      };
    };
    Kakao: {
      init: (key: string) => void;
      isInitialized: () => boolean;
      Auth: {
        login: (config: KakaoLoginConfig) => void;
        logout: (callback?: () => void) => void;
        getAccessToken: () => string;
      };
    };
  }
}

2. HTML 구조 및 SDK 설정

2.1 Next.js Script 컴포넌트 활용

// components/SocialLoginProvider.tsx
import Script from 'next/script';
import { useEffect, useState } from 'react';

export const SocialLoginProvider = ({ children }: { children: React.ReactNode }) => {
  const [isGoogleLoaded, setIsGoogleLoaded] = useState(false);
  const [isKakaoLoaded, setIsKakaoLoaded] = useState(false);

  return (
    <>
      {/* Google GSI SDK */}
      <Script
        src="https://accounts.google.com/gsi/client"
        strategy="afterInteractive"
        onLoad={() => {
          console.log('Google GSI SDK 로드 완료');
          setIsGoogleLoaded(true);
        }}
        onError={() => {
          console.error('Google GSI SDK 로드 실패');
        }}
      />

      {/* Kakao JavaScript SDK */}
      <Script
        src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.5/kakao.min.js"
        strategy="afterInteractive"
        onLoad={() => {
          console.log('Kakao SDK 로드 완료');
          setIsKakaoLoaded(true);
          // Kakao SDK 초기화
          if (window.Kakao && !window.Kakao.isInitialized()) {
            window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY);
          }
        }}
        onError={() => {
          console.error('Kakao SDK 로드 실패');
        }}
      />

      {children}
    </>
  );
};

2.2 SDK 로딩 상태 관리

// hooks/useSocialSDK.ts
import { useState, useEffect } from 'react';

export const useSocialSDK = () => {
  const [sdkStatus, setSdkStatus] = useState({
    google: { loaded: false, initialized: false },
    kakao: { loaded: false, initialized: false }
  });

  useEffect(() => {
    // Google SDK 상태 체크
    const checkGoogleSDK = () => {
      if (typeof window !== 'undefined' && window.google?.accounts?.id) {
        setSdkStatus(prev => ({
          ...prev,
          google: { loaded: true, initialized: true }
        }));
      }
    };

    // Kakao SDK 상태 체크  
    const checkKakaoSDK = () => {
      if (typeof window !== 'undefined' && window.Kakao?.isInitialized()) {
        setSdkStatus(prev => ({
          ...prev,
          kakao: { loaded: true, initialized: true }
        }));
      }
    };

    const interval = setInterval(() => {
      checkGoogleSDK();
      checkKakaoSDK();
    }, 100);

    // 5초 후 체크 중단
    setTimeout(() => clearInterval(interval), 5000);

    return () => clearInterval(interval);
  }, []);

  return sdkStatus;
};

3. 플랫폼별 프론트엔드 구현

3.1 Google 로그인 (ID Token 방식)

// hooks/useGoogleLogin.ts
import { useCallback, useEffect, useState } from 'react';
import { SocialLoginResponse } from '../types/social-login';

export const useGoogleLogin = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Google GSI 초기화
  const initializeGoogleLogin = useCallback(() => {
    if (!window.google?.accounts?.id) {
      setError('Google SDK가 로드되지 않았습니다.');
      return false;
    }

    try {
      window.google.accounts.id.initialize({
        client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
        callback: handleCredentialResponse,
        auto_select: false,
        cancel_on_tap_outside: true,
        use_fedcm_for_prompt: false // FedCM 비활성화로 팝업 보장
      });

      return true;
    } catch (error) {
      setError('Google 초기화 실패');
      console.error('Google 초기화 오류:', error);
      return false;
    }
  }, []);

  // Google 콜백 처리
  const handleCredentialResponse = useCallback(async (response: any) => {
    setIsLoading(true);
    setError(null);

    try {
      const deviceId = localStorage.getItem('deviceId') || generateDeviceId();
      
      const result = await fetch(`${process.env.NEXT_PUBLIC_AUTH_SERVICE_URL}/api/auth/social/google`, {
        method: 'POST',
        headers: { 
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        },
        credentials: 'include', // CORS 쿠키 허용
        body: JSON.stringify({
          idToken: response.credential,
          deviceInfo: {
            platform: 'WEB_BROWSER',
            deviceId: deviceId,
            appVersion: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0'
          }
        })
      });

      if (!result.ok) {
        throw new Error(`HTTP ${result.status}: ${result.statusText}`);
      }

      const data: SocialLoginResponse = await result.json();
      
      if (data.success) {
        handleSocialLoginSuccess(data, 'Google');
      } else {
        throw new Error(data.error || 'Google 로그인 실패');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
      setError(errorMessage);
      console.error('Google 로그인 오류:', error);
    } finally {
      setIsLoading(false);
    }
  }, []);

  // 로그인 버튼 렌더링
  const renderGoogleButton = useCallback((elementId: string, config?: Partial<GoogleButtonConfig>) => {
    const element = document.getElementById(elementId);
    if (!element || !window.google?.accounts?.id) return;

    const defaultConfig = {
      theme: 'outline' as const,
      size: 'large' as const,
      type: 'standard' as const,
      text: 'signin_with' as const,
      locale: 'ko',
      width: 300
    };

    window.google.accounts.id.renderButton(element, { ...defaultConfig, ...config });
  }, []);

  return {
    isLoading,
    error,
    initializeGoogleLogin,
    renderGoogleButton
  };
};

// 유틸리티 함수
const generateDeviceId = (): string => {
  const id = `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  localStorage.setItem('deviceId', id);
  return id;
};

3.2 카카오 로그인 (Access Token 방식)

// hooks/useKakaoLogin.ts
import { useCallback, useState } from 'react';

export const useKakaoLogin = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // 카카오 로그인 실행
  const loginWithKakao = useCallback(async () => {
    if (!window.Kakao?.Auth) {
      setError('카카오 SDK가 로드되지 않았습니다.');
      return;
    }

    setIsLoading(true);
    setError(null);

    try {
      // 카카오 로그인 팝업
      window.Kakao.Auth.login({
        success: async (kakaoResponse: any) => {
          try {
            const deviceId = localStorage.getItem('deviceId') || generateDeviceId();
            
            const result = await fetch(`${process.env.NEXT_PUBLIC_AUTH_SERVICE_URL}/api/auth/social/kakao`, {
              method: 'POST',
              headers: { 
                'Content-Type': 'application/json',
                'Accept': 'application/json'
              },
              credentials: 'include',
              body: JSON.stringify({
                accessToken: kakaoResponse.access_token,
                deviceInfo: {
                  platform: 'WEB_BROWSER',
                  deviceId: deviceId,
                  appVersion: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0'
                }
              })
            });

            if (!result.ok) {
              throw new Error(`HTTP ${result.status}: ${result.statusText}`);
            }

            const data: SocialLoginResponse = await result.json();
            
            if (data.success) {
              handleSocialLoginSuccess(data, 'Kakao');
            } else {
              throw new Error(data.error || '카카오 로그인 실패');
            }
          } catch (error) {
            const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
            setError(errorMessage);
            console.error('카카오 백엔드 통신 오류:', error);
          } finally {
            setIsLoading(false);
          }
        },
        fail: (kakaoError: any) => {
          setIsLoading(false);
          setError('카카오 인증 실패');
          console.error('카카오 로그인 실패:', kakaoError);
        }
      });
    } catch (error) {
      setIsLoading(false);
      setError('카카오 로그인 초기화 실패');
      console.error('카카오 로그인 오류:', error);
    }
  }, []);

  return {
    isLoading,
    error,
    loginWithKakao
  };
};

3.3 네이버 로그인 (팝업 방식)

// hooks/useNaverLogin.ts
import { useCallback, useEffect, useState } from 'react';

export const useNaverLogin = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // 네이버 로그인 팝업 실행
  const loginWithNaver = useCallback(() => {
    setIsLoading(true);
    setError(null);

    try {
      const popup = window.open(
        `${process.env.NEXT_PUBLIC_AUTH_SERVICE_URL}/api/auth/social/naver/login`,
        'naverLogin',
        'width=500,height=600,scrollbars=yes,resizable=yes,top=50,left=50'
      );

      if (!popup) {
        throw new Error('팝업이 차단되었습니다. 브라우저의 팝업 설정을 확인해주세요.');
      }

      // 팝업 창 닫힘 감지
      const popupTimer = setInterval(() => {
        if (popup.closed) {
          clearInterval(popupTimer);
          setIsLoading(false);
        }
      }, 1000);

      // 30초 후 자동 정리
      setTimeout(() => {
        if (!popup.closed) {
          popup.close();
          clearInterval(popupTimer);
          setIsLoading(false);
          setError('로그인 시간이 초과되었습니다.');
        }
      }, 30000);

    } catch (error) {
      setIsLoading(false);
      const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
      setError(errorMessage);
      console.error('네이버 로그인 오류:', error);
    }
  }, []);

  // 팝업 메시지 리스너 설정
  useEffect(() => {
    const handlePopupMessage = (event: MessageEvent) => {
      // 보안: AuthService 도메인만 허용
      if (event.origin !== process.env.NEXT_PUBLIC_AUTH_SERVICE_URL) {
        console.warn('허용되지 않은 도메인에서 메시지:', event.origin);
        return;
      }

      const { type, result, error } = event.data;

      switch (type) {
        case 'NAVER_LOGIN_SUCCESS':
          console.log('네이버 로그인 성공:', result);
          handleSocialLoginSuccess(result, 'Naver');
          setIsLoading(false);
          break;

        case 'NAVER_SIGNUP_NEEDED':
          console.log('네이버 회원가입 필요:', result);
          handleNaverSignupNeeded(result);
          setIsLoading(false);
          break;

        case 'NAVER_LOGIN_ERROR':
          console.error('네이버 로그인 오류:', error);
          setError(error?.message || '네이버 로그인 중 오류가 발생했습니다.');
          setIsLoading(false);
          break;

        default:
          console.log('알 수 없는 메시지 타입:', type);
      }
    };

    window.addEventListener('message', handlePopupMessage);
    return () => window.removeEventListener('message', handlePopupMessage);
  }, []);

  return {
    isLoading,
    error,
    loginWithNaver
  };
};

// 네이버 회원가입 필요 처리
const handleNaverSignupNeeded = (result: any) => {
  const { needsUsernameInput, needsAdditionalInfo, tempUserInfo } = result;
  
  // 회원가입 모달 표시 또는 페이지 이동
  // 상태 관리 라이브러리를 통해 전역 상태 업데이트
  console.log('추가 정보 입력 필요:', {
    needsUsernameInput,
    needsAdditionalInfo,
    tempUserInfo
  });
};

4. 통합 응답 처리 및 사용자 경험(UX) 최적화

4.1 로딩 상태 관리

// components/SocialLoginButton.tsx
import React, { useState } from 'react';
import { Loader, CheckCircle, AlertCircle } from 'lucide-react';

interface SocialLoginButtonProps {
  provider: 'google' | 'kakao' | 'naver';
  onLogin: () => Promise<void>;
  disabled?: boolean;
}

export const SocialLoginButton: React.FC<SocialLoginButtonProps> = ({
  provider,
  onLogin,
  disabled = false
}) => {
  const [isLoading, setIsLoading] = useState(false);
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  const handleClick = async () => {
    if (isLoading || disabled) return;

    setIsLoading(true);
    setStatus('loading');

    try {
      await onLogin();
      setStatus('success');
      
      // 성공 상태를 잠시 보여준 후 초기화
      setTimeout(() => {
        setStatus('idle');
        setIsLoading(false);
      }, 2000);
    } catch (error) {
      setStatus('error');
      setTimeout(() => {
        setStatus('idle');
        setIsLoading(false);
      }, 3000);
    }
  };

  const getButtonContent = () => {
    switch (status) {
      case 'loading':
        return (
          <>
            <Loader className="w-4 h-4 animate-spin" />
            <span>{provider.toUpperCase()} 로그인 중...</span>
          </>
        );
      case 'success':
        return (
          <>
            <CheckCircle className="w-4 h-4 text-green-500" />
            <span>로그인 성공!</span>
          </>
        );
      case 'error':
        return (
          <>
            <AlertCircle className="w-4 h-4 text-red-500" />
            <span>다시 시도해주세요</span>
          </>
        );
      default:
        return (
          <>
            <img src={`/icons/${provider}.svg`} alt={provider} className="w-4 h-4" />
            <span>{provider.toUpperCase()}로 시작하기</span>
          </>
        );
    }
  };

  return (
    <button
      onClick={handleClick}
      disabled={isLoading || disabled}
      className={`
        flex items-center justify-center gap-3 w-full px-4 py-3 rounded-lg border
        transition-all duration-200 font-medium
        ${status === 'error' ? 'border-red-300 bg-red-50 text-red-700' :
          status === 'success' ? 'border-green-300 bg-green-50 text-green-700' :
          'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'}
        ${(isLoading || disabled) && 'cursor-not-allowed opacity-60'}
      `}
    >
      {getButtonContent()}
    </button>
  );
};

4.2 에러 처리 및 사용자 피드백

// hooks/useErrorHandler.ts
import { useState } from 'react';

interface ErrorInfo {
  code?: string;
  message: string;
  provider?: string;
  retry?: () => void;
}

export const useErrorHandler = () => {
  const [error, setError] = useState<ErrorInfo | null>(null);

  const handleSocialLoginError = (error: any, provider: string, retryFn?: () => void) => {
    let errorInfo: ErrorInfo;

    if (error.message?.includes('팝업이 차단')) {
      errorInfo = {
        code: 'POPUP_BLOCKED',
        message: '팝업이 차단되었습니다. 브라우저 설정을 확인하고 다시 시도해주세요.',
        provider,
        retry: retryFn
      };
    } else if (error.message?.includes('HTTP 403')) {
      errorInfo = {
        code: 'FORBIDDEN',
        message: '로그인 권한이 없습니다. 관리자에게 문의해주세요.',
        provider
      };
    } else if (error.message?.includes('HTTP 500')) {
      errorInfo = {
        code: 'SERVER_ERROR',
        message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
        provider,
        retry: retryFn
      };
    } else if (error.message?.includes('Network')) {
      errorInfo = {
        code: 'NETWORK_ERROR',
        message: '네트워크 연결을 확인해주세요.',
        provider,
        retry: retryFn
      };
    } else {
      errorInfo = {
        code: 'UNKNOWN',
        message: error.message || '알 수 없는 오류가 발생했습니다.',
        provider,
        retry: retryFn
      };
    }

    setError(errorInfo);
    
    // 에러 로깅 (선택사항)
    console.error(`${provider} 소셜 로그인 오류:`, error);
    
    // 에러 분석을 위한 로그 전송 (선택사항)
    if (process.env.NODE_ENV === 'production') {
      // sendErrorLog(errorInfo);
    }
  };

  const clearError = () => setError(null);

  return {
    error,
    handleSocialLoginError,
    clearError
  };
};

4.3 통합 소셜 로그인 응답 처리

// utils/socialLoginHandler.ts
import { SocialLoginResponse } from '../types/social-login';

// 토큰 저장 및 관리
export const handleSocialLoginSuccess = async (
  response: SocialLoginResponse, 
  provider: string
) => {
  try {
    // JWT 토큰 저장 (httpOnly 쿠키 권장, 여기서는 localStorage 예시)
    if (response.accessToken) {
      localStorage.setItem('accessToken', response.accessToken);
    }
    if (response.refreshToken) {
      localStorage.setItem('refreshToken', response.refreshToken);
    }

    // 사용자 정보 저장
    if (response.userInfo) {
      localStorage.setItem('userInfo', JSON.stringify(response.userInfo));
    }

    // 추가 정보 입력이 필요한 경우
    if (response.needsAdditionalInfo || response.needsUsernameInput) {
      // 회원가입 폼으로 이동하거나 모달 표시
      showSignUpModal(provider, response);
      return;
    }

    // 로그인 성공 알림
    showSuccessNotification(`${provider} 로그인이 완료되었습니다!`);

    // 홈 페이지로 이동 또는 이전 페이지로 복귀
    const returnUrl = localStorage.getItem('returnUrl') || '/dashboard';
    window.location.href = returnUrl;

  } catch (error) {
    console.error('로그인 성공 처리 중 오류:', error);
    throw new Error('로그인 처리 중 오류가 발생했습니다.');
  }
};

// 회원가입 모달 표시
const showSignUpModal = (provider: string, response: SocialLoginResponse) => {
  // 상태 관리 라이브러리를 사용하여 모달 상태 업데이트
  // 예: Zustand, Redux Toolkit 등
  
  const signUpData = {
    provider,
    socialToken: response.accessToken, // 또는 임시 토큰
    userInfo: response.userInfo,
    requirements: {
      needsUsernameInput: response.needsUsernameInput,
      needsAdditionalInfo: response.needsAdditionalInfo,
      needsAccountLink: response.needsAccountLink
    }
  };

  // 전역 상태 업데이트
  // useSignUpStore.getState().setSignUpData(signUpData);
  // useUIStore.getState().setModalOpen('signup');
};

// 성공 알림 표시
const showSuccessNotification = (message: string) => {
  // 토스트 알림 라이브러리 사용 (예: react-hot-toast, sonner 등)
  // toast.success(message);
  
  // 또는 브라우저 기본 알림
  if ('Notification' in window && Notification.permission === 'granted') {
    new Notification('로그인 성공', { body: message });
  }
};

4.4 회원가입 폼 컴포넌트

// components/SocialSignUpForm.tsx
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const signUpSchema = z.object({
  userId: z.string()
    .min(4, '아이디는 4자 이상이어야 합니다.')
    .max(20, '아이디는 20자 이하여야 합니다.')
    .regex(/^[a-zA-Z0-9_]+$/, '아이디는 영문, 숫자, 밑줄만 사용 가능합니다.'),
  phone: z.string()
    .regex(/^010-\d{4}-\d{4}$/, '휴대폰 번호 형식이 올바르지 않습니다.'),
  termsAgreed: z.boolean().refine(val => val, '이용약관에 동의해주세요.'),
  privacyAgreed: z.boolean().refine(val => val, '개인정보 처리방침에 동의해주세요.'),
  marketingAgreed: z.boolean().optional()
});

type SignUpFormData = z.infer<typeof signUpSchema>;

interface SocialSignUpFormProps {
  provider: string;
  socialToken: string;
  userInfo: any;
  requirements: {
    needsUsernameInput: boolean;
    needsAdditionalInfo: boolean;
  };
  onSubmit: (data: SignUpFormData) => Promise<void>;
  onCancel: () => void;
}

export const SocialSignUpForm: React.FC<SocialSignUpFormProps> = ({
  provider,
  userInfo,
  requirements,
  onSubmit,
  onCancel
}) => {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [userIdAvailable, setUserIdAvailable] = useState<boolean | null>(null);

  const { register, handleSubmit, watch, formState: { errors } } = useForm<SignUpFormData>({
    resolver: zodResolver(signUpSchema),
    defaultValues: {
      marketingAgreed: false
    }
  });

  const userId = watch('userId');

  // 아이디 중복 확인
  const checkUserIdAvailability = async (userId: string) => {
    if (!userId || userId.length < 4) return;

    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_AUTH_SERVICE_URL}/api/auth/check-userid`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId })
      });

      const data = await response.json();
      setUserIdAvailable(data.available);
    } catch (error) {
      console.error('아이디 중복 확인 오류:', error);
    }
  };

  const handleFormSubmit = async (data: SignUpFormData) => {
    setIsSubmitting(true);
    try {
      await onSubmit(data);
    } catch (error) {
      console.error('회원가입 오류:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
      <div className="bg-white p-6 rounded-lg max-w-md w-full mx-4">
        <h2 className="text-xl font-bold mb-4">
          {provider.toUpperCase()} 회원가입
        </h2>

        <form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
          {/* 기본 정보 표시 */}
          <div className="p-3 bg-gray-50 rounded">
            <p className="text-sm text-gray-600">
              <strong>이메일:</strong> {userInfo.email}
            </p>
            <p className="text-sm text-gray-600">
              <strong>이름:</strong> {userInfo.name}
            </p>
          </div>

          {/* 아이디 입력 (필요한 경우) */}
          {requirements.needsUsernameInput && (
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                아이디
              </label>
              <div className="relative">
                <input
                  {...register('userId')}
                  type="text"
                  className={`w-full px-3 py-2 border rounded-md ${
                    errors.userId ? 'border-red-300' : 'border-gray-300'
                  }`}
                  placeholder="아이디를 입력하세요"
                  onBlur={() => checkUserIdAvailability(userId)}
                />
                {userIdAvailable === false && (
                  <p className="text-sm text-red-600 mt-1">이미 사용 중인 아이디입니다.</p>
                )}
                {userIdAvailable === true && (
                  <p className="text-sm text-green-600 mt-1">사용 가능한 아이디입니다.</p>
                )}
              </div>
              {errors.userId && (
                <p className="text-sm text-red-600 mt-1">{errors.userId.message}</p>
              )}
            </div>
          )}

          {/* 휴대폰 번호 (필요한 경우) */}
          {requirements.needsAdditionalInfo && (
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                휴대폰 번호
              </label>
              <input
                {...register('phone')}
                type="tel"
                className={`w-full px-3 py-2 border rounded-md ${
                  errors.phone ? 'border-red-300' : 'border-gray-300'
                }`}
                placeholder="010-0000-0000"
              />
              {errors.phone && (
                <p className="text-sm text-red-600 mt-1">{errors.phone.message}</p>
              )}
            </div>
          )}

          {/* 약관 동의 */}
          <div className="space-y-2">
            <label className="flex items-center">
              <input
                {...register('termsAgreed')}
                type="checkbox"
                className="mr-2"
              />
              <span className="text-sm">이용약관에 동의합니다 (필수)</span>
            </label>
            {errors.termsAgreed && (
              <p className="text-sm text-red-600">{errors.termsAgreed.message}</p>
            )}

            <label className="flex items-center">
              <input
                {...register('privacyAgreed')}
                type="checkbox"
                className="mr-2"
              />
              <span className="text-sm">개인정보 처리방침에 동의합니다 (필수)</span>
            </label>
            {errors.privacyAgreed && (
              <p className="text-sm text-red-600">{errors.privacyAgreed.message}</p>
            )}

            <label className="flex items-center">
              <input
                {...register('marketingAgreed')}
                type="checkbox"
                className="mr-2"
              />
              <span className="text-sm">마케팅 정보 수신에 동의합니다 (선택)</span>
            </label>
          </div>

          {/* 버튼 */}
          <div className="flex gap-2 pt-4">
            <button
              type="button"
              onClick={onCancel}
              className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50"
            >
              취소
            </button>
            <button
              type="submit"
              disabled={isSubmitting || userIdAvailable === false}
              className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
            >
              {isSubmitting ? '처리 중...' : '회원가입'}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

🔄 플랫폼별 상세 플로우

1. Google 로그인 플로우

sequenceDiagram
    participant User as 👤 사용자
    participant Frontend as 🌐 프론트엔드
    participant GSI as 🔍 Google GSI
    participant AuthService as 🔐 AuthService
    participant GoogleAPI as 📡 Google API
    participant DB as 🗄️ Database
    
    User->>Frontend: "Google로 시작하기" 클릭
    Frontend->>GSI: google.accounts.id.initialize()
    GSI->>User: Google 로그인 팝업 표시
    User->>GSI: 계정 선택 및 로그인
    GSI->>Frontend: handleCredentialResponse(idToken)
    
    Frontend->>AuthService: POST /api/auth/social/google<br/>{idToken, deviceInfo}
    AuthService->>GoogleAPI: GET /oauth2/v3/tokeninfo?id_token=...
    GoogleAPI->>AuthService: 사용자 정보 응답
    AuthService->>DB: 3단계 자동 판단 로직 실행
    DB->>AuthService: 사용자 정보 반환
    AuthService->>Frontend: JWT + 사용자 정보 응답
    Frontend->>User: 로그인 완료 또는 회원가입 폼

2. 카카오 로그인 플로우

sequenceDiagram
    participant User as 👤 사용자
    participant Frontend as 🌐 프론트엔드
    participant KakaoSDK as 🍊 Kakao SDK
    participant AuthService as 🔐 AuthService
    participant KakaoAPI as 📡 Kakao API
    participant DB as 🗄️ Database
    
    User->>Frontend: "카카오로 시작하기" 클릭
    Frontend->>KakaoSDK: Kakao.Auth.createLoginButton()
    KakaoSDK->>User: 카카오 로그인 페이지
    User->>KakaoSDK: 카카오 계정 로그인
    KakaoSDK->>Frontend: success(accessToken)
    
    Frontend->>AuthService: POST /api/auth/social/kakao<br/>{accessToken, deviceInfo}
    AuthService->>KakaoAPI: GET /v2/user/me<br/>Authorization: Bearer {accessToken}
    KakaoAPI->>AuthService: 사용자 정보 응답<br/>{id, kakao_account: {email, profile}}
    AuthService->>DB: 3단계 자동 판단 로직 실행
    DB->>AuthService: 사용자 정보 반환
    AuthService->>Frontend: JWT + 사용자 정보 응답
    Frontend->>User: 로그인 완료 또는 회원가입 폼

3. 네이버 로그인 플로우 (팝업 방식)

sequenceDiagram
    participant User as 👤 사용자
    participant Frontend as 🌐 프론트엔드
    participant Popup as 🔗 팝업창
    participant AuthService as 🔐 AuthService
    participant NaverOAuth as 🟢 네이버 OAuth
    participant NaverAPI as 📡 네이버 API
    participant DB as 🗄️ Database
    
    User->>Frontend: "네이버로 시작하기" 클릭
    Frontend->>Popup: window.open('/api/auth/social/naver/login')
    Popup->>AuthService: GET /api/auth/social/naver/login
    AuthService->>NaverOAuth: redirect to 네이버 인증 페이지
    NaverOAuth->>User: 네이버 로그인 페이지 표시
    User->>NaverOAuth: 네이버 계정 로그인
    NaverOAuth->>AuthService: GET /api/auth/social/naver/callback?code=...&state=...
    
    AuthService->>NaverOAuth: POST /oauth2.0/token<br/>{code, client_id, client_secret}
    NaverOAuth->>AuthService: {access_token}
    AuthService->>NaverAPI: GET /v1/nid/me<br/>Authorization: Bearer {access_token}
    NaverAPI->>AuthService: 사용자 정보 응답<br/>{response: {id, email, name}}
    AuthService->>DB: 3단계 자동 판단 로직 실행
    DB->>AuthService: 사용자 정보 반환
    
    alt 로그인 성공
        AuthService->>Popup: HTML + postMessage('NAVER_LOGIN_SUCCESS')
        Popup->>Frontend: window.opener.postMessage({type: 'NAVER_LOGIN_SUCCESS'})
        Frontend->>User: 로그인 완료
    else 회원가입 필요
        AuthService->>Popup: HTML + postMessage('NAVER_SIGNUP_NEEDED')
        Popup->>Frontend: window.opener.postMessage({type: 'NAVER_SIGNUP_NEEDED'})
        Frontend->>User: 회원가입 폼 표시
    end


🔐 소셜 로그인 절차

1. Google 소셜 로그인 플로우

sequenceDiagram
    participant U as 사용자
    participant C as 클라이언트(앱/웹)
    participant A as AuthService
    participant G as Google API
    participant DB as Database
    participant R as Redis
    
    U->>C: 1. Google 로그인 버튼 클릭
    C->>G: 2. Google OAuth 인증 요청
    G->>U: 3. Google 로그인 페이지 표시
    U->>G: 4. 계정 정보 입력 & 동의
    G->>C: 5. ID Token 발급
    
    C->>A: 6. POST /api/auth/social/google
    Note over C,A: { "idToken": "google_id_token" }
    
    A->>G: 7. ID Token 검증
    G->>A: 8. 사용자 정보 반환
    Note over G,A: { sub, email, name, picture, email_verified }
    
    A->>DB: 9. 소셜 계정 조회/생성
    DB->>A: 10. 사용자 정보 반환
    
    A->>A: 11. JWT 토큰 생성
    A->>R: 12. 토큰 캐싱
    
    A->>C: 13. 로그인 응답
    Note over A,C: { accessToken, refreshToken, userInfo }
    
    C->>U: 14. 로그인 완료 화면

2. Kakao 소셜 로그인 플로우

sequenceDiagram
    participant U as 사용자
    participant C as 클라이언트
    participant A as AuthService
    participant K as Kakao API
    participant DB as Database
    participant R as Redis
    
    U->>C: 1. 카카오 로그인 버튼 클릭
    C->>K: 2. 카카오 OAuth 인증 요청
    K->>U: 3. 카카오 로그인 페이지
    U->>K: 4. 로그인 & 동의
    K->>C: 5. Access Token 발급
    
    C->>A: 6. POST /api/auth/social/kakao
    Note over C,A: { "accessToken": "kakao_access_token" }
    
    A->>K: 7. 사용자 정보 조회
    K->>A: 8. 사용자 정보 반환
    Note over K,A: { id, kakao_account: { email, profile: { nickname, profile_image_url } } }
    
    A->>DB: 9. 소셜 계정 처리
    A->>A: 10. JWT 토큰 생성
    A->>R: 11. 토큰 캐싱
    A->>C: 12. 로그인 응답
    C->>U: 13. 로그인 완료

3. Naver 소셜 로그인 플로우

sequenceDiagram
    participant U as 사용자
    participant C as 클라이언트
    participant A as AuthService
    participant N as Naver API
    participant DB as Database
    participant R as Redis
    
    U->>C: 1. 네이버 로그인 버튼 클릭
    C->>N: 2. 네이버 OAuth 인증 요청
    N->>U: 3. 네이버 로그인 페이지
    U->>K: 4. 로그인 & 동의
    N->>C: 5. Access Token 발급
    
    C->>A: 6. POST /api/auth/social/naver
    Note over C,A: { "accessToken": "naver_access_token" }
    
    A->>N: 7. 사용자 정보 조회
    N->>A: 8. 사용자 정보 반환
    Note over N,A: { resultcode: "00", response: { id, email, name, nickname, profile_image } }
    
    A->>DB: 9. 소셜 계정 처리
    A->>A: 10. JWT 토큰 생성
    A->>R: 11. 토큰 캐싱
    A->>C: 12. 로그인 응답
    C->>U: 13. 로그인 완료

👤 소셜 회원가입 절차

소셜 회원가입 통합 플로우

sequenceDiagram
    participant U as 사용자
    participant C as 클라이언트
    participant A as AuthService
    participant S as 소셜 플랫폼
    participant DB as Database
    participant R as Redis
    
    U->>C: 1. 소셜 회원가입 선택
    C->>S: 2. 소셜 인증 요청
    S->>U: 3. 로그인 페이지 표시
    U->>S: 4. 인증 & 동의
    S->>C: 5. 소셜 토큰 발급
    
    C->>U: 6. 추가 정보 입력 폼 표시
    Note over C,U: userId, 전화번호, 약관 동의
    U->>C: 7. 추가 정보 입력
    
    C->>A: 8. POST /api/auth/social/signup
    Note over C,A: { provider, socialToken, userId, phone, termsAgreed, privacyAgreed }
    
    A->>S: 9. 소셜 토큰으로 사용자 정보 조회
    S->>A: 10. 소셜 사용자 정보 반환
    
    A->>DB: 11. userId 중복 확인
    DB->>A: 12. 중복 여부 응답
    
    alt userId 중복인 경우
        A->>C: 13. 중복 오류 응답
        C->>U: 14. 다른 아이디 입력 요청
    else userId 사용 가능
        A->>DB: 15. 새 사용자 생성
        A->>DB: 16. 소셜 계정 연결
        A->>A: 17. JWT 토큰 생성
        A->>R: 18. 토큰 캐싱
        A->>C: 19. 회원가입 성공 응답
        C->>U: 20. 회원가입 완료 화면
    end

판단 플로우 다이어그램

flowchart TD
    A[소셜 토큰 수신] --> B[소셜 사용자 정보 추출]
    B --> C{1️⃣ 소셜 ID로 기존 계정 찾기}
    
    C -->|찾음| D[🟢 즉시 로그인]
    C -->|못찾음| E{2️⃣ 이메일로 기존 사용자 찾기}
    
    E -->|찾음| F[🔗 소셜 계정 연결]
    F --> G[🟢 연결 후 로그인]
    
    E -->|못찾음| H[🆕 신규 사용자 생성]
    H --> I{3️⃣ 추가 정보 충분한가?}
    
    I -->|충분함| J[🟢 자동 생성 후 로그인]
    I -->|부족함| K[⚠️ 소셜 회원가입 필요]
    
    K --> L[사용자가 추가 정보 입력]
    L --> M[🟢 회원가입 완료 후 로그인]

� 데이터 구조 및 API 스펙

1. 요청 데이터 구조

Google 로그인 요청 (GoogleLoginRequest)

{
  "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "deviceInfo": {
    "platform": "WEB_BROWSER",  // WEB_BROWSER, MOBILE_APP, ADMIN_WEB
    "deviceId": "browser-fingerprint-uuid",
    "appVersion": "1.0.0"
  }
}

카카오 로그인 요청 (KakaoLoginRequest)

{
  "accessToken": "kEWG7tLkJc3eDpNpOo8TCCoTsCYadJ5F_access_token",
  "deviceInfo": {
    "platform": "MOBILE_APP",
    "deviceId": "device-unique-id",
    "appVersion": "2.1.0"
  }
}

네이버 로그인 요청 (NaverLoginRequest)

{
  "accessToken": "AAAANVl6Y8PgC8HsssEQutadsd_access_token"
}

소셜 회원가입 요청 (SocialSignUpRequest)

{
  "provider": "GOOGLE",  // GOOGLE, KAKAO, NAVER
  "socialToken": "social_platform_token",
  "userId": "user_unique_id",
  "phone": "010-1234-5678",
  "termsAgreed": true,
  "privacyAgreed": true,
  "marketingAgreed": false
}

2. 응답 데이터 구조

통합 소셜 로그인 응답 (SocialLoginResponse)

{
  "success": true,
  "accessToken": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOiI...",
  "refreshToken": "4f8b1c2e-3d4a-5b6c-7d8e-9f0a1b2c3d4e",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "userInfo": {
    "id": "member-uuid-1234",
    "email": "user@gmail.com", 
    "name": "홍길동",
    "profileImageUrl": "https://profile-image-url",
    "role": "USER",
    "isNewUser": false
  },
  "needsAdditionalInfo": false,
  "needsUsernameInput": false, 
  "needsAccountLink": false,
  "message": "Google 로그인이 완료되었습니다!"
}

소셜 회원가입 응답 (SocialSignUpResponse)

{
  "success": true,
  "message": "회원가입이 완료되었습니다!",
  "accessToken": "eyJhbGciOiJIUzUxMiJ9...",
  "refreshToken": "refresh-token-uuid",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "userInfo": {
    "id": "new-member-uuid",
    "email": "newuser@gmail.com",
    "name": "김신규", 
    "profileImageUrl": "https://profile-image-url",
    "provider": "GOOGLE",
    "role": "USER",
    "needsAdditionalInfo": false
  }
}

3. 중간 처리 데이터 (SocialUserInfo)

플랫폼별 사용자 정보 통합 클래스:

{
  "provider": "google",  // google, kakao, naver
  "socialId": "108234567890123456789",
  "email": "user@gmail.com",
  "name": "홍길동",
  "profileImageUrl": "https://profile-image-url",
  "emailVerified": true,
  "phoneNumber": "010-1234-5678",
  "locale": "ko_KR",
  "collectionChannel": "mobile_app",
  "providedInfoMask": 37  // 비트마스크로 제공된 정보 표시
}

4. 데이터베이스 구조

members 테이블

CREATE TABLE members (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    member_uuid VARCHAR(36) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE,
    name VARCHAR(50),
    user_id VARCHAR(30) UNIQUE,
    phone VARCHAR(20),
    profile_image_url TEXT,
    role VARCHAR(20) DEFAULT 'USER',
    status VARCHAR(20) DEFAULT 'ACTIVE',
    last_login_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

member_social_accounts 테이블

CREATE TABLE member_social_accounts (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    member_id BIGINT NOT NULL,
    provider VARCHAR(20) NOT NULL,  -- GOOGLE, KAKAO, NAVER
    social_id VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    name VARCHAR(50),
    profile_image_url TEXT,
    is_primary BOOLEAN DEFAULT FALSE,
    last_used_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (member_id) REFERENCES members(id),
    UNIQUE KEY unique_social_account (provider, social_id)
);

🧪 테스트 및 디버깅 가이드

3. 백엔드 개발 모드 설정

3.1 application.yml 개발 설정

# AuthService의 application-dev.yml
spring:
  profiles:
    active: dev
    
social:
  google:
    client-id: ${GOOGLE_CLIENT_ID:YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com}
    dev-mode: true  # 더미 데이터 사용
    token-info-url: https://oauth2.googleapis.com/tokeninfo
    
  kakao:
    client-id: ${KAKAO_REST_API_KEY:your-kakao-rest-api-key}
    user-info-url: https://kapi.kakao.com/v2/user/me
    dev-mode: true  # 더미 데이터 사용
    
  naver:
    client-id: ${NAVER_CLIENT_ID:YOUR_NAVER_CLIENT_ID}
    client-secret: ${NAVER_CLIENT_SECRET:your-naver-client-secret}
    redirect-uri: http://localhost:8082/api/auth/social/naver/callback
    auth-url: https://nid.naver.com/oauth2.0/authorize
    token-url: https://nid.naver.com/oauth2.0/token
    user-info-url: https://openapi.naver.com/v1/nid/me
    dev-mode: true  # 더미 데이터 사용

jwt:
  secret: ${JWT_SECRET:YOUR_JWT_SECRET_KEY_HERE}
  access-token-expiry: 3600  # 1시간
  refresh-token-expiry: 86400  # 24시간

logging:
  level:
    com.akc.b2c.authservice.service.social: DEBUG
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG

3.2 개발 모드 더미 데이터

// GoogleAuthService.java의 개발 모드 더미 데이터
private SocialUserInfo createDummyGoogleUserInfo(String idToken) {
    return SocialUserInfo.builder()
            .provider("google")
            .socialId("dummy_google_" + System.currentTimeMillis())
            .email("dev.google@test.com")
            .name("구글 테스트 사용자")
            .profileImageUrl("https://lh3.googleusercontent.com/a/default-user=s96-c")
            .emailVerified(true)
            .locale("ko_KR")
            .build();
}

// KakaoAuthService.java의 개발 모드 더미 데이터
private SocialUserInfo createDevModeUserInfo() {
    return SocialUserInfo.builder()
            .provider("kakao")
            .socialId("dummy_kakao_" + System.currentTimeMillis())
            .email("dev.kakao@test.com")
            .name("카카오 테스트 사용자")
            .profileImageUrl("https://k.kakaocdn.net/dn/default_profile.png")
            .emailVerified(true)
            .build();
}

// NaverAuthService.java의 개발 모드 더미 데이터
private SocialUserInfo createDummyNaverUserInfo() {
    return SocialUserInfo.builder()
            .provider("naver")
            .socialId("dummy_naver_" + System.currentTimeMillis())
            .email("dev.naver@test.com")
            .name("네이버 테스트 사용자")
            .profileImageUrl("https://ssl.pstatic.net/static/pwe/address/img_profile.png")
            .emailVerified(true)
            .build();
}

4. 플랫폼별 테스트 시나리오

4.1 Google 로그인 테스트

// 개발 모드 테스트 (더미 토큰)
const testGoogleLogin = () => {
    fetch('http://localhost:8082/api/auth/social/google', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            idToken: 'dummy_google_id_token_for_dev',
            deviceInfo: {
                platform: 'WEB_BROWSER',
                deviceId: 'test-device-' + Date.now()
            }
        })
    });
};

4.2 카카오 로그인 테스트

// 개발 모드 테스트 (더미 토큰)  
const testKakaoLogin = () => {
    fetch('http://localhost:8082/api/auth/social/kakao', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            accessToken: 'dummy_kakao_access_token_for_dev',
            deviceInfo: {
                platform: 'WEB_BROWSER',
                deviceId: 'test-device-' + Date.now()
            }
        })
    });
};

4.3 네이버 로그인 테스트

// 팝업 방식 테스트
const testNaverLogin = () => {
    const popup = window.open(
        'http://localhost:8082/api/auth/social/naver/login',
        'naverTest',
        'width=500,height=600'
    );
    
    window.addEventListener('message', (event) => {
        if (event.origin !== 'http://localhost:8082') return;
        console.log('네이버 로그인 결과:', event.data);
    });
};

5. 백엔드 로그 모니터링

5.1 주요 로그 포인트

# AuthService 로그 실시간 모니터링
tail -f /path/to/authservice.log | grep -E "(Google|Kakao|Naver|소셜)"

# 특정 패턴 필터링
tail -f /path/to/authservice.log | grep -E "(로그인 처리 시작|사용자 정보|토큰 생성)"

5.2 에러 디버깅

# 토큰 검증 실패 로그
grep "토큰 검증 실패" /path/to/authservice.log

# DB 연결 문제 로그  
grep "DB 처리 실패" /path/to/authservice.log

# JWT 토큰 생성 오류
grep "JWT.*실패" /path/to/authservice.log

6. API 테스트 스크립트

6.1 cURL을 이용한 API 테스트

# Google 로그인 테스트
curl -X POST http://localhost:8082/api/auth/social/google \
  -H "Content-Type: application/json" \
  -d '{
    "idToken": "dummy_google_id_token",
    "deviceInfo": {
      "platform": "WEB_BROWSER",
      "deviceId": "test-device"
    }
  }'

# 카카오 로그인 테스트
curl -X POST http://localhost:8082/api/auth/social/kakao \
  -H "Content-Type: application/json" \
  -d '{
    "accessToken": "dummy_kakao_access_token",
    "deviceInfo": {
      "platform": "WEB_BROWSER",
      "deviceId": "test-device"
    }
  }'

# 네이버 로그인 테스트 (Access Token 방식)
curl -X POST http://localhost:8082/api/auth/social/naver \
  -H "Content-Type: application/json" \
  -d '{
    "accessToken": "dummy_naver_access_token"
  }'

# 지원 플랫폼 확인
curl -X GET http://localhost:8082/api/auth/social/providers

# 헬스체크
curl -X GET http://localhost:8082/api/auth/social/health

7. 일반적인 문제 및 해결방법

7.1 문제: CORS 오류

Access to fetch at 'http://localhost:8082/api/auth/social/google' 
from origin 'null' has been blocked by CORS policy

백엔드 해결방법: SecurityConfig에서 CORS 설정 확인

// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList(
            "http://localhost:3011", 
            "http://localhost:3010", 
            "http://localhost:3000",  // Next.js 개발 서버
            "file://*"  // 로컬 HTML 파일 허용
        ));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);
        return source;
    }
}

프론트엔드 해결방법: 개발 환경에서 프록시 설정

// next.config.js (Next.js)
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/auth/:path*',
        destination: 'http://localhost:8082/api/auth/:path*'
      }
    ];
  }
};

7.2 문제: 네이버 팝업 차단

해결방법: 브라우저 팝업 허용 설정 또는 사용자 액션에서 팝업 호출

7.3 문제: JWT 토큰 만료

백엔드 해결방법: RefreshToken을 이용한 토큰 갱신 로직 구현

// JwtTokenProvider.java
@Component
public class JwtTokenProvider {
    
    @Value("${jwt.access-token-expiry:3600}")
    private long accessTokenExpiry;
    
    @Value("${jwt.refresh-token-expiry:86400}")
    private long refreshTokenExpiry;
    
    public TokenResponse refreshAccessToken(String refreshToken) {
        if (validateToken(refreshToken)) {
            String userId = getUserIdFromToken(refreshToken);
            String newAccessToken = generateAccessToken(userId);
            
            return TokenResponse.builder()
                    .accessToken(newAccessToken)
                    .refreshToken(refreshToken)  // RefreshToken은 재사용
                    .tokenType("Bearer")
                    .expiresIn(accessTokenExpiry)
                    .build();
        }
        throw new InvalidTokenException("Invalid refresh token");
    }
}

1. 로컬 테스트 환경 설정

1.1 개발 환경 준비

# AuthService 실행
cd /Users/sangyonghan/development/Amano/akc-b2c/AuthService
./gradlew bootRun

# 프론트엔드 개발 서버 실행 (Next.js 예시)
cd /path/to/frontend
npm run dev

1.2 환경별 설정

// config/social-config.ts
export const getSocialConfig = () => {
  const environment = process.env.NODE_ENV;
  
  const configs = {
    development: {
      authServiceUrl: 'http://localhost:8082',
      googleClientId: 'YOUR_GOOGLE_CLIENT_ID',
      kakaoJsKey: 'YOUR_KAKAO_JAVASCRIPT_KEY',
      enableDevMode: true,
      enableConsoleLogging: true
    },
    staging: {
      authServiceUrl: 'https://staging-auth.example.com',
      googleClientId: 'YOUR_STAGING_GOOGLE_CLIENT_ID',
      kakaoJsKey: 'YOUR_STAGING_KAKAO_JS_KEY',
      enableDevMode: false,
      enableConsoleLogging: true
    },
    production: {
      authServiceUrl: 'https://auth.example.com',
      googleClientId: 'YOUR_PROD_GOOGLE_CLIENT_ID',
      kakaoJsKey: 'YOUR_PROD_KAKAO_JS_KEY',
      enableDevMode: false,
      enableConsoleLogging: false
    }
  };

  return configs[environment as keyof typeof configs] || configs.development;
};

2.1 Google 로그인 디버깅

// utils/googleDebugger.ts
export const debugGoogleLogin = {
  // Google SDK 상태 확인
  checkSDKStatus: () => {
    console.log('Google SDK 상태:', {
      sdkLoaded: typeof window.google !== 'undefined',
      accountsAvailable: typeof window.google?.accounts !== 'undefined',
      idAvailable: typeof window.google?.accounts?.id !== 'undefined'
    });
  },

  // ID Token 디코딩 (개발 환경에서만)
  decodeIdToken: (idToken: string) => {
    if (process.env.NODE_ENV !== 'development') return;
    
    try {
      const payload = JSON.parse(atob(idToken.split('.')[1]));
      console.log('Google ID Token 페이로드:', {
        iss: payload.iss,
        aud: payload.aud,
        sub: payload.sub,
        email: payload.email,
        email_verified: payload.email_verified,
        exp: new Date(payload.exp * 1000),
        iat: new Date(payload.iat * 1000)
      });
    } catch (error) {
      console.error('ID Token 디코딩 실패:', error);
    }
  },

  // 네트워크 요청 로깅
  logNetworkRequest: async (idToken: string) => {
    const startTime = Date.now();
    console.log('Google API 요청 시작:', new Date().toISOString());
    
    try {
      const response = await fetch('/api/auth/social/google', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ idToken })
      });
      
      const endTime = Date.now();
      console.log('Google API 응답:', {
        status: response.status,
        statusText: response.statusText,
        duration: `${endTime - startTime}ms`,
        headers: Object.fromEntries(response.headers.entries())
      });
      
      return response;
    } catch (error) {
      console.error('Google API 요청 실패:', error);
      throw error;
    }
  }
};

2.2 카카오 로그인 디버깅

// utils/kakaoDebugger.ts
export const debugKakaoLogin = {
  // Kakao SDK 상태 확인
  checkSDKStatus: () => {
    console.log('Kakao SDK 상태:', {
      sdkLoaded: typeof window.Kakao !== 'undefined',
      initialized: window.Kakao?.isInitialized(),
      version: window.Kakao?.VERSION
    });
  },

  // 액세스 토큰 정보 확인
  checkAccessToken: () => {
    if (!window.Kakao?.Auth) return;
    
    const accessToken = window.Kakao.Auth.getAccessToken();
    console.log('Kakao Access Token:', {
      hasToken: !!accessToken,
      tokenLength: accessToken?.length,
      tokenPreview: accessToken ? `${accessToken.substring(0, 10)}...` : null
    });
  },

  // 로그인 상태 확인
  checkLoginStatus: () => {
    if (!window.Kakao?.Auth) return;
    
    window.Kakao.Auth.getStatusInfo((statusInfo: any) => {
      console.log('Kakao 로그인 상태:', statusInfo);
    });
  }
};

2.3 네이버 로그인 디버깅

// utils/naverDebugger.ts
export const debugNaverLogin = {
  // 팝업 상태 모니터링
  monitorPopup: (popup: Window) => {
    const checkInterval = setInterval(() => {
      if (popup.closed) {
        console.log('네이버 팝업이 닫혔습니다.');
        clearInterval(checkInterval);
        return;
      }
      
      try {
        console.log('팝업 URL:', popup.location.href);
      } catch (error) {
        // Cross-origin 오류는 정상 (다른 도메인으로 이동 중)
        console.log('팝업이 네이버 도메인으로 이동 중...');
      }
    }, 1000);
    
    // 30초 후 모니터링 중단
    setTimeout(() => clearInterval(checkInterval), 30000);
  },

  // 메시지 이벤트 로깅
  logPopupMessages: () => {
    const originalAddEventListener = window.addEventListener;
    
    window.addEventListener = function(type, listener, options) {
      if (type === 'message') {
        const wrappedListener = (event: MessageEvent) => {
          console.log('팝업 메시지 수신:', {
            origin: event.origin,
            data: event.data,
            timestamp: new Date().toISOString()
          });
          
          return listener.call(this, event);
        };
        
        return originalAddEventListener.call(this, type, wrappedListener, options);
      }
      
      return originalAddEventListener.call(this, type, listener, options);
    };
  }
};

3. 일반적인 문제 및 해결방법

3.1 CORS 오류

문제:

Access to fetch at 'http://localhost:8082/api/auth/social/google' 
from origin 'http://localhost:3000' has been blocked by CORS policy

해결방법:

// 개발 환경에서 프록시 설정 (Next.js)
// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/auth/:path*',
        destination: 'http://localhost:8082/api/auth/:path*'
      }
    ];
  }
};

// 또는 백엔드 CORS 설정 확인
// SecurityConfig.java에서 allowedOrigins 확인

3.2 SDK 로딩 실패

문제: Google GSI 또는 Kakao SDK가 로드되지 않음

해결방법:

// SDK 로딩 재시도 로직
const loadSDKWithRetry = async (url: string, checkFn: () => boolean, maxRetries = 3) => {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await loadScript(url);
      
      // SDK 로드 확인을 위해 잠시 대기
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      if (checkFn()) {
        console.log(`SDK 로드 성공 (${i + 1}번째 시도)`);
        return true;
      }
    } catch (error) {
      console.warn(`SDK 로드 실패 (${i + 1}번째 시도):`, error);
    }
    
    if (i < maxRetries - 1) {
      await new Promise(resolve => setTimeout(resolve, 2000)); // 2초 대기
    }
  }
  
  throw new Error('SDK 로드 최대 재시도 횟수 초과');
};

// 사용 예시
try {
  await loadSDKWithRetry(
    'https://accounts.google.com/gsi/client',
    () => typeof window.google?.accounts?.id !== 'undefined'
  );
} catch (error) {
  // Fallback UI 표시 또는 대체 로그인 방법 제공
  showFallbackLoginOptions();
}

3.3 팝업 차단 문제

문제: 브라우저에서 네이버 로그인 팝업이 차단됨

해결방법:

// 사용자 인터랙션 기반 팝업 열기
const openPopupWithFallback = (url: string, name: string, features: string) => {
  const popup = window.open(url, name, features);
  
  if (!popup || popup.closed) {
    // 팝업이 차단된 경우 사용자에게 알림
    const userConfirmed = confirm(
      '팝업이 차단되었습니다. 브라우저 설정에서 팝업을 허용하고 다시 시도하시겠습니까?'
    );
    
    if (userConfirmed) {
      // 새 창으로 직접 이동
      window.location.href = url;
    }
    
    return null;
  }
  
  return popup;
};

3.4 네트워크 타임아웃

문제: API 요청이 너무 오래 걸리거나 응답하지 않음

해결방법:

// 타임아웃이 있는 fetch 래퍼
const fetchWithTimeout = async (url: string, options: RequestInit, timeout = 10000) => {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error instanceof Error && error.name === 'AbortError') {
      throw new Error(`요청 시간 초과 (${timeout}ms)`);
    }
    
    throw error;
  }
};

4. 로깅 및 모니터링

4.1 구조화된 로깅

// utils/logger.ts
interface LogEvent {
  level: 'debug' | 'info' | 'warn' | 'error';
  message: string;
  context?: Record<string, any>;
  timestamp?: string;
  provider?: string;
  userId?: string;
}

class SocialLoginLogger {
  private isDevelopment = process.env.NODE_ENV === 'development';
  
  private log(event: LogEvent) {
    const logEntry = {
      ...event,
      timestamp: event.timestamp || new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href
    };
    
    // 개발 환경에서는 콘솔에 출력
    if (this.isDevelopment) {
      console[event.level](event.message, logEntry);
    }
    
    // 프로덕션에서는 로그 수집 서비스로 전송
    if (!this.isDevelopment && event.level !== 'debug') {
      this.sendToLogService(logEntry);
    }
  }
  
  private async sendToLogService(logEntry: LogEvent) {
    try {
      await fetch('/api/logs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(logEntry)
      });
    } catch (error) {
      // 로그 전송 실패는 조용히 처리
      console.warn('로그 전송 실패:', error);
    }
  }
  
  debug(message: string, context?: Record<string, any>) {
    this.log({ level: 'debug', message, context });
  }
  
  info(message: string, context?: Record<string, any>) {
    this.log({ level: 'info', message, context });
  }
  
  warn(message: string, context?: Record<string, any>) {
    this.log({ level: 'warn', message, context });
  }
  
  error(message: string, context?: Record<string, any>) {
    this.log({ level: 'error', message, context });
  }
  
  socialLoginStart(provider: string) {
    this.info(`${provider} 로그인 시작`, { provider });
  }
  
  socialLoginSuccess(provider: string, isNewUser: boolean) {
    this.info(`${provider} 로그인 성공`, { provider, isNewUser });
  }
  
  socialLoginError(provider: string, error: Error) {
    this.error(`${provider} 로그인 실패`, {
      provider,
      error: error.message,
      stack: error.stack
    });
  }
}

export const logger = new SocialLoginLogger();

4.2 성능 모니터링

// utils/performanceMonitor.ts
export const performanceMonitor = {
  measureSocialLogin: (provider: string, operation: () => Promise<any>) => {
    const startTime = performance.now();
    const startTimestamp = Date.now();
    
    logger.debug(`${provider} 성능 측정 시작`);
    
    return operation().finally(() => {
      const duration = performance.now() - startTime;
      const endTimestamp = Date.now();
      
      logger.info(`${provider} 성능 측정 완료`, {
        provider,
        duration: Math.round(duration),
        startTimestamp,
        endTimestamp
      });
      
      // 성능 임계값 확인 (5초 이상이면 경고)
      if (duration > 5000) {
        logger.warn(`${provider} 로그인이 예상보다 오래 걸림`, {
          provider,
          duration: Math.round(duration)
        });
      }
    });
  }
};

🎯 핵심 특징 요약

통합 인증 시스템의 장점

  1. 일관된 사용자 경험: 모든 소셜 플랫폼에서 동일한 플로우
  2. 자동 계정 연결: 같은 이메일로 여러 소셜 계정 통합 관리
  3. 유연한 회원가입: 필요시에만 추가 정보 수집
  4. 개발자 친화적: 개발 모드 지원으로 쉬운 테스트

6. 보안 및 모범 사례 가이드

6.1 프론트엔드 보안 고려사항

토큰 저장 방식:

// 보안 등급별 토큰 저장 방법

// 1. 최고 보안 (권장) - httpOnly 쿠키
// 백엔드에서 Set-Cookie 헤더로 설정
// 프론트엔드에서는 접근 불가능하지만 자동으로 요청에 포함

// 2. 높은 보안 - Secure Storage (모바일)
// React Native의 Keychain (iOS) / KeyStore (Android)

// 3. 중간 보안 - sessionStorage (웹)
const storeTokenSecurely = (accessToken: string, refreshToken: string) => {
  // 세션 스토리지는 탭 닫히면 자동 삭제
  sessionStorage.setItem('accessToken', accessToken);
  sessionStorage.setItem('refreshToken', refreshToken);
  
  // 토큰 만료 시간 저장
  const expiresAt = Date.now() + (3600 * 1000); // 1시간
  sessionStorage.setItem('tokenExpiresAt', expiresAt.toString());
};

// 4. 낮은 보안 - localStorage (개발 환경에만 사용)
// 프로덕션에서는 사용 금지
if (process.env.NODE_ENV === 'development') {
  localStorage.setItem('accessToken', accessToken);
}

CSRF 방지:

// CSRF 토큰 처리
export const apiClient = {
  async request(url: string, options: RequestInit = {}) {
    // CSRF 토큰을 헤더에 추가
    const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
    
    const defaultOptions: RequestInit = {
      credentials: 'include', // 쿠키 자동 포함
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest', // CSRF 방지
        ...(csrfToken && { 'X-CSRF-Token': csrfToken }),
        ...options.headers
      }
    };

    return fetch(url, { ...defaultOptions, ...options });
  }
};

개인정보 보호:

// 민감한 정보 로깅 방지
const sanitizeUserInfo = (userInfo: any) => {
  const { email, phone, ...safeInfo } = userInfo;
  
  return {
    ...safeInfo,
    email: email ? `${email.substring(0, 3)}***@${email.split('@')[1]}` : undefined,
    phone: phone ? `${phone.substring(0, 3)}****${phone.substring(7)}` : undefined
  };
};

// 개발 환경에서만 상세 로그
const logSocialLogin = (provider: string, userInfo: any) => {
  if (process.env.NODE_ENV === 'development') {
    console.log(`${provider} 로그인:`, sanitizeUserInfo(userInfo));
  } else {
    // 프로덕션에서는 최소한의 로그만
    console.log(`${provider} 로그인 성공`);
  }
};

6.2 에러 처리 및 사용자 프라이버시

// 보안을 고려한 에러 메시지
const getSecureErrorMessage = (error: any, provider: string) => {
  // 내부 오류 정보는 숨기고 사용자 친화적인 메시지만 표시
  const publicErrors: Record<string, string> = {
    'INVALID_TOKEN': '로그인 정보가 올바르지 않습니다. 다시 시도해주세요.',
    'EXPIRED_TOKEN': '로그인이 만료되었습니다. 다시 로그인해주세요.',
    'NETWORK_ERROR': '네트워크 연결을 확인해주세요.',
    'POPUP_BLOCKED': '팝업이 차단되었습니다. 브라우저 설정을 확인해주세요.',
    'USER_DENIED': '로그인이 취소되었습니다.',
    'SERVER_ERROR': '일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'
  };

  const errorCode = error.code || 'UNKNOWN_ERROR';
  
  // 개발 환경에서는 상세 오류 정보 로그
  if (process.env.NODE_ENV === 'development') {
    console.error(`${provider} 상세 오류:`, error);
  }

  return publicErrors[errorCode] || '알 수 없는 오류가 발생했습니다.';
};

6.3 Content Security Policy (CSP) 설정

<!-- Next.js의 경우 next.config.js에서 설정 -->
<!-- 또는 HTML 헤더에서 설정 -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 
    https://accounts.google.com 
    https://t1.kakaocdn.net 
    https://developers.kakao.com
    'unsafe-inline';
  connect-src 'self' 
    https://accounts.google.com 
    https://kapi.kakao.com 
    https://openapi.naver.com 
    http://localhost:8082;
  img-src 'self' 
    https://*.googleusercontent.com 
    https://*.kakaocdn.net 
    https://*.pstatic.net 
    data:;
  style-src 'self' 'unsafe-inline';
  frame-src 'self' 
    https://accounts.google.com;
">

7. 통합 인증 시스템의 장점

  1. 일관된 사용자 경험: 모든 소셜 플랫폼에서 동일한 플로우
  2. 자동 계정 연결: 같은 이메일로 여러 소셜 계정 통합 관리
  3. 유연한 회원가입: 필요시에만 추가 정보 수집
  4. 개발자 친화적: 개발 모드 지원으로 쉬운 테스트
  5. 향상된 보안: 토큰 기반 인증과 프론트엔드 보안 모범 사례 적용
  6. 사용자 프라이버시 보호: 민감한 정보 최소 수집 및 안전한 처리

8. 프론트엔드 보안 체크리스트

이 문서는 실제 구현된 코드를 기반으로 작성되었으며, 프론트엔드와 백엔드 모두에서 참고할 수 있는 완전한 가이드입니다.

통합 소셜 인증 프로세스 (로그인/회원가입 자동 판단)

🎯 핵심 개념: 하나의 버튼, 스마트한 처리

사용자 경험

[구글로 시작하기] 버튼 → 구글 인증 → 자동으로 로그인/가입 판단 → 서비스 이용

통합 프로세스 플로우

flowchart TD
    A[사용자가 '구글로 시작하기' 클릭] --> B[구글 OAuth 인증]
    B --> C[ID Token 획득]
    C --> D[AuthService 호출<br/>POST /api/auth/social/google]
    
    D --> E{서버가 자동 판단}
    
    E -->|기존 소셜 계정| F1[즉시 로그인 완료]
    E -->|기존 이메일 사용자| F2[소셜 계정 연결 후 로그인]
    E -->|완전 신규 + 정보 충분| F3[자동 회원가입 후 로그인]
    E -->|완전 신규 + 정보 부족| F4[추가 정보 수집 필요]
    
    F4 --> G[회원가입 폼 표시]
    G --> H[사용자가 추가 정보 입력]
    H --> I[POST /api/auth/social/signup]
    I --> J[회원가입 완료 후 로그인]
    
    F1 --> K[서비스 이용]
    F2 --> K
    F3 --> K  
    J --> K

📊 데이터 흐름도

1. 소셜 로그인 데이터 처리 과정

graph TD
    A[소셜 토큰] --> B{토큰 검증}
    B -->|성공| C[소셜 사용자 정보 추출]
    B -->|실패| D[인증 실패]
    
    C --> E[SocialUserInfo 생성]
    E --> F{기존 소셜 계정 존재?}
    
    F -->|예| G[기존 사용자 로그인]
    F -->|아니오| H{이메일로 기존 사용자 존재?}
    
    H -->|예| I[소셜 계정 연결]
    H -->|아니오| J[신규 사용자 생성]
    
    G --> K[JWT 토큰 생성]
    I --> K
    J --> K
    
    K --> L[Redis 토큰 캐싱]
    L --> M[로그인 응답]

2. 소셜 회원가입 데이터 처리 과정

graph TD
    A[소셜 회원가입 요청] --> B[소셜 토큰 검증]
    B --> C[사용자 정보 추출]
    C --> D[추가 정보 결합]
    
    D --> E{userId 중복 확인}
    E -->|중복| F[중복 오류]
    E -->|사용가능| G[신규 사용자 생성]
    
    G --> H[소셜 계정 연결]
    H --> I[JWT 토큰 생성]
    I --> J[회원가입 완료]
    
    F --> K[다른 아이디 요청]

🔄 상세 시퀀스 다이어그램

기존 사용자 소셜 계정 연결 시나리오

sequenceDiagram
    participant C as Client
    participant A as AuthService
    participant DB as Database
    participant SA as SocialAccount
    
    C->>A: 소셜 로그인 요청
    A->>A: 소셜 토큰 검증
    
    A->>SA: 소셜 ID로 계정 조회
    SA->>A: 계정 없음
    
    A->>DB: 이메일로 사용자 조회
    DB->>A: 기존 사용자 존재
    
    Note over A: 기존 사용자에게 새 소셜 계정 연결
    
    A->>SA: 새 소셜 계정 생성
    A->>DB: 사용자 정보 업데이트
    
    A->>A: JWT 토큰 생성
    A->>C: 연결 완료 응답
    Note over A,C: "기존 계정에 Google 소셜 계정이 연결되었습니다!"

📊 데이터 구조 분석

1. 입력 데이터 (Request)

Google 로그인 요청

{
  "idToken": "eyJhbGciOiJSUzI1NiIs...",
  "deviceInfo": {
    "platform": "MOBILE_APP",
    "deviceId": "device-uuid",
    "appVersion": "1.0.0"
  }
}

Kakao 로그인 요청

{
  "accessToken": "kEWG7tLkJc3eDpNp...",
  "deviceInfo": {
    "platform": "WEB_BROWSER",
    "deviceId": "browser-fingerprint"
  }
}
{
  "accessToken": "AAAANVl6Y8PgC8H..."
}

소셜 회원가입 요청

{
  "provider": "GOOGLE",
  "socialToken": "eyJhbGciOiJSUzI1NiIs...",
  "userId": "user123",
  "phone": "010-1234-5678",
  "termsAgreed": true,
  "privacyAgreed": true,
  "marketingAgreed": false
}

2. 중간 처리 데이터 (SocialUserInfo)

{
  "provider": "google",
  "socialId": "108234567890123456789",
  "email": "user@gmail.com",
  "name": "홍길동",
  "profileImageUrl": "https://lh3.googleusercontent.com/...",
  "emailVerified": true,
  "phoneNumber": "010-1234-5678",
  "locale": "ko_KR",
  "collectionChannel": "mobile_app",
  "providedInfoMask": 37
}

3. 출력 데이터 (Response)

소셜 로그인 응답

{
  "success": true,
  "accessToken": "eyJhbGciOiJIUzUxMiJ9...",
  "refreshToken": "4f8b1c2e-3d4a-5b6c-7d8e-9f0a1b2c3d4e",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "userInfo": {
    "id": "member-uuid-1234",
    "email": "user@gmail.com",
    "name": "홍길동",
    "profileImageUrl": "https://lh3.googleusercontent.com/...",
    "role": "USER",
    "isNewUser": false
  },
  "needsAdditionalInfo": false,
  "needsUsernameInput": false,
  "needsAccountLink": false,
  "message": "Google 로그인이 완료되었습니다!"
}

소셜 회원가입 응답

{
  "success": true,
  "message": "Google 회원가입이 완료되었습니다!",
  "accessToken": "eyJhbGciOiJIUzUxMiJ9...",
  "refreshToken": "4f8b1c2e-3d4a-5b6c-7d8e-9f0a1b2c3d4e",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "userInfo": {
    "id": "new-member-uuid-5678",
    "email": "newuser@gmail.com",
    "name": "김신규",
    "profileImageUrl": "https://lh3.googleusercontent.com/...",
    "provider": "GOOGLE",
    "role": "USER",
    "needsAdditionalInfo": false
  }
}

🎯 핵심 데이터 처리 로직

1. 사용자 식별 우선순위

  1. 소셜 ID + 플랫폼 (기존 소셜 계정)
  2. 이메일 (기존 일반 사용자 또는 다른 소셜 계정)
  3. 신규 생성 (완전히 새로운 사용자)

2. 정보 병합 전략

3. 보안 검증 단계

  1. 소셜 토큰 검증 (플랫폼 API 호출)
  2. 클라이언트 ID 확인 (Google)
  3. 이메일 인증 상태 확인
  4. 계정 상태 확인 (활성/비활성/삭제)

📋 시나리오별 처리 결과

시나리오 결과 needsAdditionalInfo needsUsernameInput isNewUser
완전 신규 사용자 회원가입 + 로그인 true true true
기존 일반 사용자 소셜 계정 연결 false false false
기존 소셜 사용자 로그인 false false false
정보 부족 신규 사용자 임시 토큰 발급 true true true

🔧 백엔드 실제 구현 정보 (프론트엔드 팀 요청사항)

1. application.yml 설정 파일 상세

1.1 실제 운영 설정 값

# AuthService 포트 및 기본 설정
server:
  port: 8082

spring:
  application:
    name: auth-service
  profiles:
    active: local  # local | docker | dev 환경별 프로파일

# JWT 토큰 설정 (실제 값)
jwt:
  secret: akc-b2c-jwt-secret-key-2024-very-long-and-secure
  access-token-expiration: 3600000   # 1시간 (밀리초)
  refresh-token-expiration: 604800000  # 7일 (밀리초)
  issuer: akc-b2c-auth-service

# 소셜 로그인 실제 설정 값
social:
  google:
    client-id: your-google-client-id.apps.googleusercontent.com
    client-secret: ${GOOGLE_CLIENT_SECRET:your-google-client-secret}
    token-info-url: https://www.googleapis.com/oauth2/v3/tokeninfo
    user-info-url: https://www.googleapis.com/oauth2/v2/userinfo
    scope: openid,email,profile
    dev-mode: false  # 프로덕션: false, 개발: true
    
  kakao:
    client-id: your-kakao-javascript-key  # JavaScript 키
    user-info-url: https://kapi.kakao.com/v2/user/me
    token-url: https://kauth.kakao.com/oauth/token
    scope: profile_nickname,account_email
    dev-mode: false
    
  naver:
    client-id: your-naver-client-id
    client-secret: ${NAVER_CLIENT_SECRET:your-naver-client-secret}
    redirect-uri: http://localhost:8082/api/auth/social/naver/callback
    auth-url: https://nid.naver.com/oauth2.0/authorize
    token-url: https://nid.naver.com/oauth2.0/token
    user-info-url: https://openapi.naver.com/v1/nid/me
    scope: name,email,profile_image
    dev-mode: false

# 보안 및 제한 설정
social:
  security:
    max-login-attempts: 5
    account-lock-duration: 1800000  # 30분
  settings:
    allow-account-linking: true
    auto-register: true
    require-phone-verification: true
    default-role: USER

1.2 환경별 설정 차이점

# Local 환경 (개발자 로컬)
spring.profiles.active: local
datasource.url: jdbc:mysql://localhost:3306/db_akc_b2c
redis.host: localhost
dev-mode: true  # 모든 소셜 플랫폼

# Docker 환경 (컨테이너)
spring.profiles.active: docker  
datasource.url: jdbc:mysql://host.docker.internal:3306/db_akc_b2c
redis.host: redis
dev-mode: false

# Development 환경 (최소 의존성)
spring.profiles.active: dev
datasource.url: jdbc:h2:mem:testdb  # 인메모리 DB
redis: disabled
dev-mode: true

2. 개발 모드 더미 데이터 로직

2.1 Google 개발 모드 로직

// GoogleAuthService.java 실제 구현
public SocialUserInfo getUserInfo(String idToken) {
    // 개발 모드 또는 더미 토큰 체크
    if (devMode || isDummyToken(idToken)) {
        log.info("🧪 Google 개발 모드 - 더미 데이터 반환");
        return createDummyGoogleUserInfo(idToken);
    }
    
    // 실제 Google API 토큰 검증
    return verifyGoogleIdToken(idToken);
}

private boolean isDummyToken(String idToken) {
    return idToken != null && (
        idToken.startsWith("dummy") ||
        idToken.startsWith("test") ||
        idToken.equals("dev_google_token")
    );
}

private SocialUserInfo createDummyGoogleUserInfo(String idToken) {
    return SocialUserInfo.builder()
            .provider("google")
            .socialId("dummy_google_" + System.currentTimeMillis())
            .email("dev.google.user@test.com")
            .name("구글 개발 테스트")
            .profileImageUrl("https://lh3.googleusercontent.com/a/default-user")
            .emailVerified(true)
            .locale("ko_KR")
            .collectionChannel("web_test")
            .providedInfoMask(63) // 모든 정보 제공
            .build();
}

2.2 카카오 개발 모드 로직

// KakaoAuthService.java 실제 구현
public SocialUserInfo getUserInfo(String accessToken) {
    if (devMode) {
        log.info("🍊 카카오 개발 모드 - 더미 데이터 반환");
        return createDevModeUserInfo();
    }
    
    return callKakaoUserInfoApi(accessToken);
}

private SocialUserInfo createDevModeUserInfo() {
    return SocialUserInfo.builder()
            .provider("kakao")
            .socialId("dummy_kakao_" + System.currentTimeMillis())
            .email("dev.kakao.user@test.com")
            .name("카카오 개발 테스트")
            .profileImageUrl("https://k.kakaocdn.net/dn/default_profile.png")
            .emailVerified(true)
            .build();
}

2.3 네이버 개발 모드 로직

// NaverAuthService.java 실제 구현
public String getAccessToken(String authorizationCode, String state) {
    if (devMode) {
        log.info("🟢 네이버 개발 모드 - 더미 Access Token 반환");
        return "dev_naver_access_token_" + System.currentTimeMillis();
    }
    
    return exchangeCodeForToken(authorizationCode, state);
}

public SocialUserInfo getUserInfo(String accessToken) {
    if (devMode || accessToken.startsWith("dev_naver_")) {
        return createDummyNaverUserInfo();
    }
    
    return callNaverUserInfoApi(accessToken);
}

3. 정확한 API 엔드포인트 스펙

3.1 실제 구현된 엔드포인트

@RestController
@RequestMapping("/api/auth/social")
public class SocialAuthController {
    
    // ✅ 실제 POST 엔드포인트들
    @PostMapping("/google")      // Google ID Token 처리
    @PostMapping("/kakao")       // Kakao Access Token 처리
    @PostMapping("/naver")       // Naver Access Token 처리 (추가적)
    @PostMapping("/signup")      // 소셜 회원가입 통합
    
    // ✅ 네이버 OAuth GET 엔드포인트들 (팝업 전용)
    @GetMapping("/naver/login")     // 네이버 인증 시작
    @GetMapping("/naver/callback")  // 네이버 콜백 처리
    
    // ✅ 유틸리티 엔드포인트들
    @GetMapping("/providers")    // 지원 플랫폼 목록 반환
    @GetMapping("/health")       // 서비스 상태 체크
    
    // ⚠️ 숨겨진 엔드포인트 (내부적으로 존재할 수 있음)
    @PostMapping("/check-userid")  // 아이디 중복 확인 (추정)
}

3.2 실제 요청/응답 형식 (검증된 스펙)

// POST /api/auth/social/google 요청
{
  "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "deviceInfo": {
    "platform": "WEB_BROWSER",
    "deviceId": "browser-12345",
    "appVersion": "1.0.0"
  }
}

// POST /api/auth/social/kakao 요청  
{
  "accessToken": "kEWG7tLkJc3eDpNpOo8TCCoTs...",
  "deviceInfo": {
    "platform": "MOBILE_APP", 
    "deviceId": "device-67890",
    "appVersion": "2.1.0"
  }
}

// POST /api/auth/social/naver 요청
{
  "accessToken": "AAAANVl6Y8PgC8HsssEQutads..."
}

4. 실제 데이터베이스 스키마

4.1 members 테이블 (실제 구조)

CREATE TABLE `members` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `member_uuid` varchar(36) NOT NULL,
  `email` varchar(100) DEFAULT NULL,
  `name` varchar(50) DEFAULT NULL,
  `user_id` varchar(30) DEFAULT NULL,
  `phone` varchar(20) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,  -- 소셜 전용 회원은 NULL
  `profile_image_url` text,
  `role` varchar(20) DEFAULT 'USER',
  `member_type` varchar(20) DEFAULT 'SOCIAL',  -- GENERAL, SOCIAL
  `status` varchar(20) DEFAULT 'ACTIVE',  -- ACTIVE, INACTIVE, WITHDRAWN
  `birth_date` date DEFAULT NULL,
  `gender` varchar(10) DEFAULT NULL,
  `marketing_agreed` tinyint(1) DEFAULT 0,
  `terms_agreed` tinyint(1) DEFAULT 1,
  `privacy_agreed` tinyint(1) DEFAULT 1,
  `sms_notification_enabled` tinyint(1) DEFAULT 1,
  `email_notification_enabled` tinyint(1) DEFAULT 1,
  `last_login_at` timestamp NULL DEFAULT NULL,
  `password_changed_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `created_by` varchar(50) DEFAULT 'system',
  `updated_by` varchar(50) DEFAULT 'system',
  `withdraw_reason` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_members_member_uuid` (`member_uuid`),
  UNIQUE KEY `uk_members_email` (`email`),
  UNIQUE KEY `uk_members_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 member_social_accounts 테이블 (실제 구조)

CREATE TABLE `member_social_accounts` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `member_id` bigint NOT NULL,
  `provider` varchar(20) NOT NULL COMMENT '소셜 제공업체 (GOOGLE, KAKAO, NAVER)',
  `social_id` varchar(100) NOT NULL COMMENT '소셜 플랫폼 고유 ID',
  `email` varchar(100) DEFAULT NULL COMMENT '소셜에서 제공된 이메일',
  `name` varchar(50) DEFAULT NULL COMMENT '소셜에서 제공된 이름',
  `profile_image_url` text COMMENT '소셜 프로필 이미지 URL',
  `is_primary` tinyint(1) DEFAULT 0 COMMENT '주 소셜 계정 여부',
  `is_email_verified` tinyint(1) DEFAULT 0 COMMENT '소셜에서 이메일 인증 여부',
  `locale` varchar(10) DEFAULT NULL COMMENT '사용자 로케일 (ko_KR, en_US)',
  `last_used_at` timestamp NULL DEFAULT NULL COMMENT '마지막 로그인 시간',
  `additional_info` json DEFAULT NULL COMMENT '소셜별 추가 정보 JSON',
  `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `created_by` varchar(50) DEFAULT 'system',
  `updated_by` varchar(50) DEFAULT 'system',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_social_account` (`provider`,`social_id`),
  KEY `fk_member_social_accounts_member_id` (`member_id`),
  KEY `idx_member_social_accounts_provider` (`provider`),
  KEY `idx_member_social_accounts_email` (`email`),
  CONSTRAINT `fk_member_social_accounts_member_id` 
    FOREIGN KEY (`member_id`) REFERENCES `members` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5. 에러 응답 형식 표준화

5.1 표준 에러 응답 구조

// 성공 응답
{
  "success": true,
  "data": { /* 실제 데이터 */ },
  "message": "처리 완료",
  "timestamp": 1703123456789
}

// 실패 응답
{
  "success": false,
  "error": {
    "code": "INVALID_SOCIAL_TOKEN",
    "message": "소셜 토큰이 유효하지 않습니다",
    "details": "Google ID Token verification failed",
    "field": "idToken"  // 필드 오류인 경우
  },
  "timestamp": 1703123456789
}

5.2 소셜 로그인 관련 에러 코드 정의

// SocialAuthErrorCode.java (실제 구현 추정)
public enum SocialAuthErrorCode {
    // 토큰 관련 오류
    INVALID_SOCIAL_TOKEN("AUTH_001", "소셜 토큰이 유효하지 않습니다"),
    EXPIRED_SOCIAL_TOKEN("AUTH_002", "소셜 토큰이 만료되었습니다"),
    SOCIAL_TOKEN_VERIFICATION_FAILED("AUTH_003", "소셜 토큰 검증에 실패했습니다"),
    
    // 사용자 정보 관련 오류
    SOCIAL_USER_INFO_NOT_FOUND("AUTH_101", "소셜 사용자 정보를 찾을 수 없습니다"),
    REQUIRED_USER_INFO_MISSING("AUTH_102", "필수 사용자 정보가 누락되었습니다"),
    EMAIL_NOT_VERIFIED("AUTH_103", "이메일 인증이 완료되지 않았습니다"),
    
    // 회원가입 관련 오류
    DUPLICATE_USER_ID("AUTH_201", "이미 사용 중인 아이디입니다"),
    DUPLICATE_EMAIL("AUTH_202", "이미 가입된 이메일입니다"),
    TERMS_NOT_AGREED("AUTH_203", "필수 약관에 동의하지 않았습니다"),
    INVALID_PHONE_FORMAT("AUTH_204", "휴대폰 번호 형식이 올바르지 않습니다"),
    
    // 플랫폼별 오류
    GOOGLE_API_ERROR("AUTH_301", "Google API 호출 중 오류가 발생했습니다"),
    KAKAO_API_ERROR("AUTH_302", "카카오 API 호출 중 오류가 발생했습니다"),
    NAVER_API_ERROR("AUTH_303", "네이버 API 호출 중 오류가 발생했습니다"),
    
    // 시스템 오류
    DATABASE_ERROR("SYS_001", "데이터베이스 처리 중 오류가 발생했습니다"),
    REDIS_CONNECTION_ERROR("SYS_002", "Redis 연결 오류가 발생했습니다"),
    JWT_GENERATION_FAILED("SYS_003", "JWT 토큰 생성에 실패했습니다");
    
    private final String code;
    private final String message;
    
    SocialAuthErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }
}

6. 로깅 전략

6.1 실제 로깅 설정

# application.yml의 로깅 설정
logging:
  level:
    com.akc.b2c.authservice: DEBUG                    # AuthService 전체 DEBUG
    com.akc.b2c.authservice.service.social: INFO      # 소셜 로그인만 INFO
    org.springframework.security: DEBUG               # Spring Security DEBUG
    org.hibernate.SQL: DEBUG                          # SQL 쿼리 로깅
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE  # SQL 파라미터
    
  pattern:
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    
  file:
    name: logs/authservice.log
    max-size: 100MB
    max-history: 30

6.2 로그 레벨별 출력 내용

// 실제 로그 출력 예시 (SocialAuthService.java)
public class SocialAuthService {
    
    public SocialLoginResponse googleLogin(GoogleLoginRequest request) {
        log.info("🔍 Google 로그인 처리 시작 - Platform: {}", 
                request.getDeviceInfo().getPlatform());
        
        try {
            SocialUserInfo socialUserInfo = googleAuthService.getUserInfo(request.getIdToken());
            log.debug("Google 사용자 정보 수신: {}", socialUserInfo.toMaskedString());
            
            UserSocialResult userResult = socialUserService.findOrCreateUserBySocial(socialUserInfo);
            log.info("사용자 DB 처리 완료 - NewUser: {}, NewSocial: {}", 
                    userResult.isNewUser(), userResult.isNewSocialAccount());
            
            String accessToken = jwtTokenProvider.generateAccessToken(/*...*/);
            log.debug("JWT 토큰 생성 완료 - TokenLength: {}", accessToken.length());
            
            return buildSuccessResponse(userResult, accessToken, refreshToken);
            
        } catch (Exception e) {
            log.error("❌ Google 로그인 처리 실패 - Error: {}", e.getMessage(), e);
            throw e;
        }
    }
}

7. 배포 관련 설정

7.1 Docker 환경 설정

# docker-compose.yml (AuthService 부분)
services:
  auth-service:
    build: ./AuthService
    container_name: akc-auth-service
    ports:
      - "8082:8082"
    environment:
      SPRING_PROFILES_ACTIVE: docker
      DB_USERNAME: antsome
      DB_PASSWORD: Q1w2e3r4%^
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
      NAVER_CLIENT_SECRET: ${NAVER_CLIENT_SECRET}
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      - mysql
      - redis
    networks:
      - akc-network

7.2 환경변수 관리

# .env 파일 (실제 배포 시 필요한 환경변수)
GOOGLE_CLIENT_SECRET=your-google-client-secret
NAVER_CLIENT_SECRET=your-naver-client-secret
JWT_SECRET=akc-b2c-jwt-secret-key-2024-very-long-and-secure
DB_USERNAME=antsome
DB_PASSWORD=Q1w2e3r4%^
SMS_PROVIDER=test
AWS_ACCESS_KEY=your-aws-access-key
AWS_SECRET_KEY=your-aws-secret-key

8. 토큰 관리 방식

8.1 JWT 토큰 생성 로직 (실제 구현)

// JwtTokenProvider.java
@Component
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String secretKey;
    
    @Value("${jwt.access-token-expiration}")
    private long accessTokenExpiration;
    
    @Value("${jwt.refresh-token-expiration}")
    private long refreshTokenExpiration;
    
    public String generateAccessToken(String userId, String email, String role) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("email", email);
        claims.put("role", role);
        claims.put("type", "access");
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userId)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
                .compact();
    }
    
    public String generateRefreshToken(String userId) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("type", "refresh");
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userId)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpiration))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
                .compact();
    }
}

8.2 Redis 토큰 캐싱 방식 (실제 구현)

// RedisTokenService.java
@Service
public class RedisTokenService {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    public void cacheAccessToken(String userId, String token, long expiration) {
        String key = "access_token:" + userId;
        redisTemplate.opsForValue().set(key, token, Duration.ofMillis(expiration));
        log.debug("Access Token 캐싱 완료 - UserId: {}, TTL: {}ms", userId, expiration);
    }
    
    public void cacheRefreshToken(String userId, String token, long expiration) {
        String key = "refresh_token:" + userId;
        redisTemplate.opsForValue().set(key, token, Duration.ofMillis(expiration));
        log.debug("Refresh Token 캐싱 완료 - UserId: {}, TTL: {}ms", userId, expiration);
    }
    
    public boolean validateCachedToken(String userId, String token) {
        String cachedToken = redisTemplate.opsForValue().get("access_token:" + userId);
        return token.equals(cachedToken);
    }
    
    public void invalidateUserTokens(String userId) {
        redisTemplate.delete("access_token:" + userId);
        redisTemplate.delete("refresh_token:" + userId);
        log.info("사용자 토큰 무효화 완료 - UserId: {}", userId);
    }
}

9. 프론트엔드-백엔드 인터페이스 명확화

9.1 개발 환경에서의 CORS 및 통신 설정

// SecurityConfig.java에서 CORS 설정
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    
    // 프론트엔드 개발 서버 허용 (실제 값)
    configuration.setAllowedOriginPatterns(Arrays.asList(
        "http://localhost:3000",    // Next.js 개발 서버
        "http://localhost:3011",    // 기존 개발 서버
        "http://localhost:3010",    // 기존 개발 서버
        "http://192.168.0.9:3011",  // 로컬 네트워크
        "file://*"                  // 로컬 HTML 파일
    ));
    
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);
    
    // 프론트엔드에서 사용할 수 있도록 노출할 헤더
    configuration.setExposedHeaders(Arrays.asList(
        "X-User-Id", "X-User-Email", "X-User-Role", "Authorization"
    ));
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", configuration);
    return source;
}

9.2 실제 네이버 팝업 통신 방식 (postMessage)

<!-- 네이버 콜백에서 실제 생성되는 HTML -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>네이버 로그인 처리</title>
</head>
<body>
    <script>
        try {
            // 실제 백엔드에서 생성하는 메시지 형식
            window.opener.postMessage({
                type: 'NAVER_LOGIN_SUCCESS',  // 또는 'NAVER_SIGNUP_NEEDED', 'NAVER_LOGIN_ERROR'
                result: {
                    member: {
                        id: '<%=member.getId()%>',
                        email: '<%=member.getEmail()%>',
                        name: '<%=member.getName()%>',
                        profileImageUrl: '<%=member.getProfileImageUrl()%>'
                    },
                    token: '<%=jwtToken%>',
                    isNewMember: <%=userResult.isNewUser()%>
                }
            }, '*');
            window.close();
        } catch (error) {
            console.error('부모 창으로 메시지 전송 실패:', error);
            document.body.innerHTML = '<h2>처리 완료</h2><p>이 창을 닫고 메인 페이지로 돌아가세요.</p>';
        }
    </script>
</body>
</html>

10. 실제 개발 환경 테스트 방법

10.1 개발 모드 활성화 방법

# application-local.yml에서 개발 모드 설정
social:
  google:
    dev-mode: true     # Google 개발 모드 활성화
  kakao:
    dev-mode: true     # 카카오 개발 모드 활성화  
  naver:
    dev-mode: true     # 네이버 개발 모드 활성화

10.2 개발 환경 테스트용 더미 토큰

// 프론트엔드에서 사용할 수 있는 개발용 토큰들
const DEV_TOKENS = {
  google: 'dummy_google_token_dev_mode',
  kakao: 'dummy_kakao_token_dev_mode', 
  naver: 'dummy_naver_token_dev_mode'
};

// 개발 환경에서 실제 소셜 로그인 없이 테스트
const testSocialLogin = (provider) => {
  const token = DEV_TOKENS[provider];
  
  fetch(`http://localhost:8082/api/auth/social/${provider}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      idToken: provider === 'google' ? token : undefined,
      accessToken: provider !== 'google' ? token : undefined,
      deviceInfo: {
        platform: 'WEB_BROWSER',
        deviceId: 'test-device-' + Date.now(),
        appVersion: '1.0.0'
      }
    })
  })
  .then(response => response.json())
  .then(data => {
    console.log(`${provider} 개발 모드 테스트 결과:`, data);
  });
};

이 문서는 실제 구현된 백엔드 코드를 기반으로 작성되었으며, 프론트엔드 개발자들이 정확한 API 스펙과 설정 정보를 참고할 수 있도록 상세히 기술되었습니다.

이 도식화를 통해 소셜 로그인 및 회원가입의 전체적인 흐름과 데이터 처리 과정을 명확하게 이해할 수 있습니다. 각 단계에서 필요한 데이터와 처리 결과를 확인하여 테스트 시나리오를 작성할 수 있습니다.