hmkim 06-09
250609 김혜민 로그아웃 세션 및 토큰 제거 수정
@65a7a6aa742fc22ef181c75beffd7ed8bac2040a
src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java
--- src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java
@@ -100,7 +100,7 @@
                 // 2-1. 기존 계정 있음 - 해당 소셜이 이미 연동되어 있는지 확인
                 HashMap<String, Object> params = new HashMap<>();
                 params.put("mbrId", existingUser.getMbrId());
-                params.put("providerType", providerType);
+                params.put("providerType", convertProviderToMbrType(providerType));
 
                 MberSocialAccountVO existingSocial = mberDAO.findSocialAccountByProvider(params);
 
@@ -133,6 +133,7 @@
         try {
             // 회원 ID 생성
             String mbrId = mberIdgn.getNextStringId();
+            String lowProviderType = convertProviderToMbrType(providerType);
 
             // 새 사용자 정보 설정
             MberVO newUser = new MberVO();
@@ -141,7 +142,7 @@
             newUser.setLgnId(email.toLowerCase());
             newUser.setMbrNm(name);
             newUser.setNcnm(name);
-            newUser.setMbrType(providerType);
+            newUser.setMbrType(lowProviderType);
             newUser.setMbrStts("1");
             newUser.setUseYn(true);
             newUser.setSysPvsnYn("1");
@@ -163,7 +164,7 @@
             // 소셜 계정 정보 저장
             MberSocialAccountVO socialAccount = new MberSocialAccountVO();
             socialAccount.setMbrId(mbrId);
-            socialAccount.setProviderType(providerType);
+            socialAccount.setProviderType(lowProviderType);
             socialAccount.setSocialId(socialId);
             socialAccount.setSocialEmail(email);
             socialAccount.setSocialName(name);
@@ -189,7 +190,7 @@
             // 중복 연동 확인
             HashMap<String, Object> params = new HashMap<>();
             params.put("mbrId", mbrId);
-            params.put("providerType", providerType);
+            params.put("providerType", convertProviderToMbrType(providerType));
 
             MberSocialAccountVO existing = mberDAO.findSocialAccountByProvider(params);
             if (existing != null && existing.getIsActive()) {
@@ -198,7 +199,7 @@
 
             MberSocialAccountVO socialAccount = new MberSocialAccountVO();
             socialAccount.setMbrId(mbrId);
-            socialAccount.setProviderType(providerType);
+            socialAccount.setProviderType(convertProviderToMbrType(providerType));
             socialAccount.setSocialId(socialId);
             socialAccount.setLoginId(loginId);
             socialAccount.setSocialEmail(email);
@@ -231,21 +232,21 @@
 
             HashMap<String, Object> params = new HashMap<>();
             params.put("mbrId", mbrId);
-            params.put("providerType", providerType);
+            params.put("providerType", convertProviderToMbrType(providerType));
             params.put("mdfr", "UNLINK_SYSTEM");
 
             mberDAO.unlinkSocialAccount(params);
 
             // 해지된 계정이 메인 프로필이었다면 다른 계정을 메인으로 설정
             MberSocialAccountVO unlinkedAccount = linkedAccounts.stream()
-                    .filter(acc -> acc.getProviderType().equals(providerType))
+                    .filter(acc -> acc.getProviderType().equals(convertProviderToMbrType(providerType)))
                     .findFirst()
                     .orElse(null);
 
             if (unlinkedAccount != null && unlinkedAccount.getIsPrimaryProfile()) {
                 // 첫 번째 활성 계정을 메인으로 설정
                 MberSocialAccountVO newPrimary = linkedAccounts.stream()
-                        .filter(acc -> !acc.getProviderType().equals(providerType))
+                        .filter(acc -> !acc.getProviderType().equals(convertProviderToMbrType(providerType)))
                         .findFirst()
                         .orElse(null);
 
@@ -286,7 +287,7 @@
         try {
             HashMap<String, Object> params = new HashMap<>();
             params.put("mbrId", mbrId);
-            params.put("providerType", providerType);
+            params.put("providerType", convertProviderToMbrType(providerType));
             params.put("mdfr", "PROFILE_UPDATE");
 
             mberDAO.setPrimaryProfile(params);
@@ -341,10 +342,20 @@
      * 소셜 계정 정보 업데이트
      */
     private void updateSocialAccountInfo(MberSocialAccountVO socialAccount) {
-        try {
             mberDAO.linkSocialAccount(socialAccount);
-        } catch (Exception e) {
-            log.warn("소셜 계정 정보 업데이트 실패", e);
-        }
+    }
+
+    /**
+     * 제공자명을 회원타입으로 변환
+     */
+    @Override
+    public String convertProviderToMbrType(String provider) {
+        return switch (provider.toLowerCase()) {
+            case "kakao" -> "K";
+            case "naver" -> "N";
+            case "google" -> "G";
+            case "facebook" -> "F";
+            default -> "S";
+        };
     }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/cms/mber/service/UnifiedLoginService.java
--- src/main/java/com/takensoft/cms/mber/service/UnifiedLoginService.java
+++ src/main/java/com/takensoft/cms/mber/service/UnifiedLoginService.java
@@ -80,4 +80,11 @@
      * @return HashMap<String, Object> 통합 제안 정보
      */
     HashMap<String, Object> suggestAccountMerge(String email, String newProviderType);
+
+    /**
+     * 제공자명을 회원타입으로 변환
+     * @param provider 제공자명
+     * @return String 변환된 제공자 명
+     */
+    String convertProviderToMbrType(String provider);
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
--- src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
+++ src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
@@ -6,16 +6,12 @@
 import com.takensoft.cms.token.service.RefreshTokenService;
 import com.takensoft.cms.token.vo.RefreshTknVO;
 import com.takensoft.common.message.MessageCode;
-import com.takensoft.common.util.ResponseData;
 import com.takensoft.common.util.ResponseUtil;
 import com.takensoft.common.util.SessionUtil;
 import jakarta.servlet.http.HttpSession;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -25,7 +21,7 @@
 import jakarta.servlet.http.Cookie;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import java.nio.charset.Charset;
+import java.util.Set;
 
 /**
  * @author takensoft
@@ -54,67 +50,221 @@
      * @param res - HTTP 응답 객체
      * @return ResponseEntity - 로그아웃 응답 결과
      *
-     * 로그아웃 - 세션/JWT 모드 통합 처리
+     * 로그아웃 - 세션/JWT 모드 통합 처리 + 완전 정리
      */
     @PostMapping(value = "/mbr/logout.json")
     public ResponseEntity<?> logout(HttpServletRequest req, HttpServletResponse res){
-        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        String mbrId = null;
+        String loginMode = loginModeService.getLoginMode();
 
-        if (auth != null && auth.getPrincipal() instanceof MberVO) {
-            MberVO mber = (MberVO) auth.getPrincipal();
-            String mbrId = mber.getMbrId();
-            String loginMode = loginModeService.getLoginMode(); // J or S
-
-            // Refresh 토큰 삭제 (DB)
-            RefreshTknVO refresh = new RefreshTknVO();
-            refresh.setMbrId(mbrId);
-            int result = refreshTokenService.delete(req, refresh);
-
-            if (loginMode.equals("S")) {
-                HttpSession session = req.getSession(false);
-                if (session != null) {
-                    session.invalidate();
-                }
-
-                // SessionUtil에서 제거
-                sessionUtil.removeSession(mbrId);
-
-                // Redis에서 세션 정보 삭제 (중복로그인 관리용)
-                String sessionKey = "session:" + mbrId;
-                try {
-                    redisTemplate.delete(sessionKey);
-                } catch (Exception e) {
-                }
-
-                // JSESSIONID 쿠키 제거
-                Cookie cookie = new Cookie("JSESSIONID", null);
-                cookie.setMaxAge(0); // 삭제
-                cookie.setPath("/");
-                res.addCookie(cookie);
-
-            } else {
-                // JWT 방식: Redis에서 삭제
-                if (!loginPolicyService.getPolicy()) {
-                    try {
-                        redisTemplate.delete("jwt:" + mbrId);
-                    } catch (Exception e) {
-                    }
-                }
-
-                // refresh 쿠키 제거
-                Cookie cookie = new Cookie("refresh", null);
-                cookie.setMaxAge(0);
-                cookie.setHttpOnly(true);
-                cookie.setPath("/");
-                res.addCookie(cookie);
+        try {
+            // 1. 인증 정보에서 사용자 ID 추출
+            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+            if (auth != null && auth.getPrincipal() instanceof MberVO) {
+                MberVO mber = (MberVO) auth.getPrincipal();
+                mbrId = mber.getMbrId();
             }
 
-            // SecurityContext 제거
+            // 2. 세션에서 사용자 ID 추출 (인증 정보가 없는 경우)
+            if (mbrId == null) {
+                HttpSession session = req.getSession(false);
+                if (session != null) {
+                    mbrId = (String) session.getAttribute("mbrId");
+                }
+            }
+
+            log.info("로그아웃 시작 - 사용자: {}, 모드: {}", mbrId, loginMode);
+
+            // 3. DB에서 Refresh 토큰 삭제
+            int dbResult = 0;
+            if (mbrId != null) {
+                RefreshTknVO refresh = new RefreshTknVO();
+                refresh.setMbrId(mbrId);
+                dbResult = refreshTokenService.delete(req, refresh);
+            }
+
+            // 4. 로그인 모드별 처리
+            if ("S".equals(loginMode)) {
+                handleSessionLogout(req, res, mbrId);
+            } else {
+                handleJWTLogout(req, res, mbrId);
+            }
+
+            // 5. 공통 정리 작업
+            performCommonCleanup(req, res);
+
+            log.info("로그아웃 완료 - 사용자: {}", mbrId);
+            return resUtil.successRes(dbResult, MessageCode.LOGOUT_SUCCESS);
+
+        } catch (Exception e) {
+            log.error("로그아웃 처리 중 오류 발생 - 사용자: {}, 오류: {}", mbrId, e.getMessage(), e);
+
+            // 오류가 발생해도 기본 정리는 수행
+            try {
+                performCommonCleanup(req, res);
+            } catch (Exception cleanupError) {
+                log.error("정리 작업 중 오류: {}", cleanupError.getMessage());
+            }
+
+            return resUtil.successRes(0, MessageCode.LOGOUT_SUCCESS); // 클라이언트에는 성공으로 응답
+        }
+    }
+
+    /**
+     * 세션 모드 로그아웃 처리
+     */
+    private void handleSessionLogout(HttpServletRequest req, HttpServletResponse res, String mbrId) {
+        try {
+            // 1. 현재 세션 무효화
+            HttpSession session = req.getSession(false);
+            if (session != null) {
+                try {
+                    session.invalidate();
+                    log.debug("세션 무효화 완료: {}", session.getId());
+                } catch (IllegalStateException e) {
+                    log.debug("이미 무효화된 세션: {}", e.getMessage());
+                }
+            }
+
+            // 2. SessionUtil에서 제거
+            if (mbrId != null) {
+                sessionUtil.removeSession(mbrId);
+            }
+
+            // 3. Redis에서 세션 관련 정보 삭제
+            if (mbrId != null) {
+                cleanupSessionRedisData(mbrId);
+            }
+
+            // 4. 세션 쿠키 제거
+            removeSessionCookies(res);
+
+        } catch (Exception e) {
+            log.error("세션 모드 로그아웃 처리 중 오류: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * JWT 모드 로그아웃 처리
+     */
+    private void handleJWTLogout(HttpServletRequest req, HttpServletResponse res, String mbrId) {
+        try {
+            // 1. Redis에서 JWT 정보 삭제 (중복로그인 관리용)
+            if (mbrId != null && !loginPolicyService.getPolicy()) {
+                redisTemplate.delete("jwt:" + mbrId);
+                log.debug("Redis JWT 토큰 삭제: jwt:{}", mbrId);
+            }
+
+            // 2. JWT 관련 쿠키 제거
+            removeJWTCookies(res);
+
+        } catch (Exception e) {
+            log.error("JWT 모드 로그아웃 처리 중 오류: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Redis 세션 데이터 정리
+     */
+    private void cleanupSessionRedisData(String mbrId) {
+        try {
+            // 세션 토큰 키 삭제
+            String sessionTokenKey = "session_token:" + mbrId;
+            redisTemplate.delete(sessionTokenKey);
+
+            // 세션 키 삭제
+            String sessionKey = "session:" + mbrId;
+            redisTemplate.delete(sessionKey);
+
+            // 기타 사용자별 Redis 키 패턴 삭제
+            Set<String> userKeys = redisTemplate.keys("*:" + mbrId);
+            if (userKeys != null && !userKeys.isEmpty()) {
+                redisTemplate.delete(userKeys);
+                log.debug("사용자별 Redis 키 삭제: {}", userKeys);
+            }
+
+        } catch (Exception e) {
+            log.error("Redis 세션 데이터 정리 중 오류: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 세션 관련 쿠키 제거
+     */
+    private void removeSessionCookies(HttpServletResponse res) {
+        String[] sessionCookies = {"JSESSIONID", "SESSION"};
+
+        for (String cookieName : sessionCookies) {
+            // 기본 경로
+            Cookie cookie = new Cookie(cookieName, null);
+            cookie.setMaxAge(0);
+            cookie.setPath("/");
+            res.addCookie(cookie);
+
+            // 도메인별 쿠키도 삭제 시도
+            Cookie domainCookie = new Cookie(cookieName, null);
+            domainCookie.setMaxAge(0);
+            domainCookie.setPath("/");
+            // domainCookie.setDomain(".example.com"); // 필요시 도메인 설정
+            res.addCookie(domainCookie);
+        }
+    }
+
+    /**
+     * JWT 관련 쿠키 제거
+     */
+    private void removeJWTCookies(HttpServletResponse res) {
+        String[] jwtCookies = {"refresh", "Authorization", "access_token"};
+
+        for (String cookieName : jwtCookies) {
+            Cookie cookie = new Cookie(cookieName, null);
+            cookie.setMaxAge(0);
+            cookie.setHttpOnly(true);
+            cookie.setPath("/");
+            res.addCookie(cookie);
+        }
+    }
+
+    /**
+     * OAuth2 관련 쿠키 제거
+     */
+    private void removeOAuth2Cookies(HttpServletResponse res) {
+        String[] oauthCookies = {
+                "oauth_access_token", "oauth_refresh_token", "oauth_state",
+                "OAUTH2_AUTHORIZATION_REQUEST", "oauth2_auth_request"
+        };
+
+        for (String cookieName : oauthCookies) {
+            Cookie cookie = new Cookie(cookieName, null);
+            cookie.setMaxAge(0);
+            cookie.setPath("/");
+            res.addCookie(cookie);
+        }
+    }
+
+    /**
+     * 공통 정리 작업
+     */
+    private void performCommonCleanup(HttpServletRequest req, HttpServletResponse res) {
+        try {
+            // 1. SecurityContext 제거
             SecurityContextHolder.clearContext();
 
-            return resUtil.successRes(result, MessageCode.LOGOUT_SUCCESS);
+            // 2. OAuth2 쿠키 제거
+            removeOAuth2Cookies(res);
+
+            // 3. 응답 헤더에서 인증 정보 제거
+            res.setHeader("Authorization", "");
+            res.setHeader("loginMode", "");
+
+            // 4. 캐시 무효화 헤더 설정
+            res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+            res.setHeader("Pragma", "no-cache");
+            res.setHeader("Expires", "0");
+
+        } catch (Exception e) {
+            log.error("공통 정리 작업 중 오류: {}", e.getMessage(), e);
         }
-        return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
     }
 
     /**
@@ -126,19 +276,17 @@
      */
     @PostMapping("/refresh/tokenReissue.json")
     public ResponseEntity<?> tokenReissue(HttpServletRequest req, HttpServletResponse res) {
-        int result = refreshTokenService.tokenReissueProc(req, res);
+        try {
+            int result = refreshTokenService.tokenReissueProc(req, res);
 
-        // 응답 처리
-        HttpHeaders headers = new HttpHeaders();
-        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
-        ResponseData responseData = new ResponseData();
-        if(result > 0) {
-            return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
-        } else {
-            responseData.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
-            responseData.setStatusText(HttpStatus.INTERNAL_SERVER_ERROR);
-            responseData.setMessage("로그인을 다시해주시기 바랍니다.");
-            return new ResponseEntity<>(responseData, headers, HttpStatus.INTERNAL_SERVER_ERROR);
+            if(result > 0) {
+                return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
+            } else {
+                return resUtil.errorRes(MessageCode.JWT_EXPIRED);
+            }
+        } catch (Exception e) {
+            log.error("토큰 재발급 중 오류: {}", e.getMessage(), e);
+            return resUtil.errorRes(MessageCode.JWT_EXPIRED);
         }
     }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/config/SecurityConfig.java
--- src/main/java/com/takensoft/common/config/SecurityConfig.java
+++ src/main/java/com/takensoft/common/config/SecurityConfig.java
@@ -195,7 +195,7 @@
         http.addFilterBefore(new AccesFilter(accesCtrlService, httpRequestUtil, appConfig), UsernamePasswordAuthenticationFilter.class);
 
         // 로그인 필터
-        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil, email2ndAuth, unifiedLoginService), UsernamePasswordAuthenticationFilter.class);
+        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil, email2ndAuth), UsernamePasswordAuthenticationFilter.class);
 
         // JWT 토큰 검증 필터
         http.addFilterAfter(new JWTFilter(jwtUtil, appConfig, loginModeService, loginPolicyService, redisTemplate), UsernamePasswordAuthenticationFilter.class);
src/main/java/com/takensoft/common/filter/JWTFilter.java
--- src/main/java/com/takensoft/common/filter/JWTFilter.java
+++ src/main/java/com/takensoft/common/filter/JWTFilter.java
@@ -10,8 +10,12 @@
 import com.takensoft.common.util.JWTUtil;
 import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.JwtException;
-import jakarta.servlet.http.HttpSession;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpSession;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.http.HttpStatus;
@@ -22,14 +26,10 @@
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.filter.OncePerRequestFilter;
 
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import java.time.Duration;
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /**
  * @author takensoft
@@ -40,8 +40,9 @@
  *  2025.05.30  |  takensoft   | 모드별 명확한 분기 처리, Redis 통합
  *  2025.06.02  |  takensoft   | 세션 모드 중복로그인 검증 개선, 401 에러 통일
  *  2025.06.04  |  takensoft   | 세션에서 JWT 토큰 추출하여 통합 처리
+ *  2025.06.09  |  takensoft   | 중복로그인 검증 로직 개선, 안정성 향상
  *
- * JWT 토큰 검증 Filter
+ * JWT 토큰 검증 Filter - 중복로그인 검증 개선
  */
 @Slf4j
 public class JWTFilter extends OncePerRequestFilter {
@@ -64,7 +65,8 @@
     }
 
     @Override
-    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
         String requestURI = request.getRequestURI();
 
         // OAuth2 관련 경로 및 로그인 요청은 검증 제외
@@ -75,20 +77,22 @@
 
         try {
             String loginMode = loginModeService.getLoginMode();
-
             if ("S".equals(loginMode)) {
                 handleSessionMode(request, response, filterChain);
             } else {
                 handleJwtMode(request, response, filterChain);
             }
-
         } catch (ExpiredJwtException e) {
+            log.debug("JWT 토큰 만료: {}", e.getMessage());
             FilterExceptionHandler.jwtError(response, e);
         } catch (JwtException e) {
+            log.debug("JWT 토큰 오류: {}", e.getMessage());
             FilterExceptionHandler.jwtError(response, e);
         } catch (InsufficientAuthenticationException e) {
+            log.debug("인증 부족: {}", e.getMessage());
             FilterExceptionHandler.jwtError(response, e);
         } catch (Exception e) {
+            log.error("JWT 필터 처리 중 예상치 못한 오류: {}", e.getMessage(), e);
             FilterExceptionHandler.jwtError(response, e);
         }
     }
@@ -96,7 +100,8 @@
     /**
      * 세션 모드 처리 - 세션에서 JWT 토큰을 꺼내서 JWT 검증 로직 재사용
      */
-    private void handleSessionMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+    private void handleSessionMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
         HttpSession session = request.getSession(false);
 
         if (session == null) {
@@ -111,32 +116,25 @@
         }
 
         // 세션에서 JWT 토큰 꺼내기
-        String sessionToken = (String) session.getAttribute("JWT_TOKEN");
+        String sessionToken = (String) session.getAttribute(SESSION_JWT_KEY);
         if (sessionToken == null) {
-            sendSessionExpiredResponse(response, request);
+            log.debug("세션에 JWT 토큰이 없음 - 사용자: {}", mbrId);
+            // 토큰이 없어도 세션 자체는 유효할 수 있으므로 계속 진행
+            filterChain.doFilter(request, response);
             return;
         }
 
-
+        // 중복 로그인 검증 (비허용 모드일 때만)
         if (!loginPolicyService.getPolicy()) {
-            String tokenKey = "session_token:" + mbrId;
-            String storedToken = redisTemplate.opsForValue().get(tokenKey);
-
-            if (storedToken != null && !storedToken.equals(sessionToken)) {
-                // 토큰이 다르면 바로 차단
-                try {
-                    session.invalidate();
-                } catch (IllegalStateException e) {
-                    // 이미 무효화됨
-                }
-                sendSessionExpiredResponse(response, request);
+            if (!validateSessionToken(mbrId, sessionToken, session)) {
+                log.info("세션 중복로그인 감지 - 사용자: {}", mbrId);
+                sendTokenExpiredResponse(response, request);
                 return;
             }
         }
 
         // 세션에서 꺼낸 JWT 토큰을 헤더에 설정하고 JWT 검증 로직 재사용
         try {
-            // Authorization 헤더 설정
             HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(request) {
                 @Override
                 public String getHeader(String name) {
@@ -147,18 +145,57 @@
                 }
             };
 
-            // 기존 JWT 검증 로직 재사용
-            handleJwtMode(wrappedRequest, response, filterChain);
+            // 기존 JWT 검증 로직 재사용 (단, 재귀 호출 방지)
+            processJwtToken(wrappedRequest, response, filterChain, sessionToken);
 
         } catch (Exception e) {
-            sendSessionExpiredResponse(response, request);
+            log.debug("세션 모드 JWT 검증 중 오류 - 사용자: {}, 오류: {}", mbrId, e.getMessage());
+            // JWT 토큰에 문제가 있어도 세션은 유효할 수 있으므로 계속 진행
+            filterChain.doFilter(request, response);
+        }
+    }
+
+    /**
+     * 세션 토큰 유효성 검증
+     */
+    private boolean validateSessionToken(String mbrId, String sessionToken, HttpSession session) {
+        try {
+            String tokenKey = "session_token:" + mbrId;
+            String storedToken = redisTemplate.opsForValue().get(tokenKey);
+
+            if (storedToken == null) {
+                log.debug("Redis에 저장된 토큰이 없음 - 사용자: {}", mbrId);
+                return false;
+            }
+
+            if (!storedToken.equals(sessionToken)) {
+                log.info("토큰 불일치로 인한 중복로그인 감지 - 사용자: {}", mbrId);
+
+                // 세션 무효화
+                try {
+                    session.invalidate();
+                } catch (IllegalStateException e) {
+                    log.debug("이미 무효화된 세션: {}", e.getMessage());
+                }
+
+                return false;
+            }
+
+            // 토큰 갱신 (활성 상태 유지)
+            redisTemplate.expire(tokenKey, session.getMaxInactiveInterval(), TimeUnit.SECONDS);
+            return true;
+
+        } catch (Exception e) {
+            log.error("세션 토큰 검증 중 오류 - 사용자: {}, 오류: {}", mbrId, e.getMessage(), e);
+            return false;
         }
     }
 
     /**
      * JWT 모드 처리
      */
-    private void handleJwtMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+    private void handleJwtMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
         String accessToken = request.getHeader(AUTHORIZATION_HEADER);
 
         if (accessToken == null) {
@@ -167,24 +204,85 @@
         }
 
         // JWT 토큰 만료 체크
-        if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) {
+        Boolean isExpired = (Boolean) jwtUtil.getClaim(accessToken, "isExpired");
+        if (isExpired != null && isExpired) {
+            log.debug("JWT 토큰 만료됨");
             sendTokenExpiredResponse(response, request);
             return;
         }
 
         // 토큰에서 사용자 정보 추출
-        MberVO mber = new MberVO(
-                (String) jwtUtil.getClaim(accessToken, "mbrId"),
-                (String) jwtUtil.getClaim(accessToken, "lgnId"),
-                (List<MberAuthorVO>) jwtUtil.getClaim(accessToken, "roles")
-        );
+        String mbrId = (String) jwtUtil.getClaim(accessToken, "mbrId");
+        String lgnId = (String) jwtUtil.getClaim(accessToken, "lgnId");
+        List<MberAuthorVO> roles = (List<MberAuthorVO>) jwtUtil.getClaim(accessToken, "roles");
 
-        // 중복로그인 체크
+        MberVO mber = new MberVO(mbrId, lgnId, roles);
+
+        // 중복로그인 체크 (JWT 모드 + 비허용일 때만)
         String loginMode = loginModeService.getLoginMode();
-        if ("J".equals(loginMode) && !loginPolicyService.getPolicy() && !isTokenValid(mber.getMbrId(), accessToken)) {
-            sendTokenExpiredResponse(response, request);
+        if ("J".equals(loginMode) && !loginPolicyService.getPolicy()) {
+            if (!validateJwtToken(mbrId, accessToken)) {
+                log.info("JWT 중복로그인 감지 - 사용자: {}", mbrId);
+                sendTokenExpiredResponse(response, request);
+                return;
+            }
+        }
+
+        // Authentication 설정
+        Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities());
+        SecurityContextHolder.getContext().setAuthentication(authToken);
+
+        filterChain.doFilter(request, response);
+    }
+
+    /**
+     * JWT 토큰 유효성 검증 (개선된 로직)
+     */
+    private boolean validateJwtToken(String mbrId, String accessToken) {
+        try {
+            String tokenKey = "jwt:" + mbrId;
+            String storedToken = redisTemplate.opsForValue().get(tokenKey);
+
+            if (storedToken == null) {
+                log.debug("Redis에 저장된 JWT 토큰이 없음 - 사용자: {}", mbrId);
+                return false;
+            }
+
+            String currentToken = jwtUtil.extractToken(accessToken);
+
+            if (!storedToken.equals(currentToken)) {
+                log.info("JWT 토큰 불일치로 인한 중복로그인 감지 - 사용자: {}", mbrId);
+                return false;
+            }
+
+            return true;
+
+        } catch (Exception e) {
+            log.error("JWT 토큰 검증 중 오류 - 사용자: {}, 오류: {}", mbrId, e.getMessage(), e);
+            return false;
+        }
+    }
+
+    /**
+     * JWT 토큰 처리 (재귀 호출 방지)
+     */
+    private void processJwtToken(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, String accessToken)
+            throws ServletException, IOException {
+
+        // JWT 토큰 만료 체크
+        Boolean isExpired = (Boolean) jwtUtil.getClaim("Bearer " + accessToken, "isExpired");
+        if (isExpired != null && isExpired) {
+            log.debug("JWT 토큰 만료됨");
+            filterChain.doFilter(request, response);
             return;
         }
+
+        // 토큰에서 사용자 정보 추출
+        String mbrId = (String) jwtUtil.getClaim("Bearer " + accessToken, "mbrId");
+        String lgnId = (String) jwtUtil.getClaim("Bearer " + accessToken, "lgnId");
+        List<MberAuthorVO> roles = (List<MberAuthorVO>) jwtUtil.getClaim("Bearer " + accessToken, "roles");
+
+        MberVO mber = new MberVO(mbrId, lgnId, roles);
 
         // Authentication 설정
         Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities());
@@ -215,35 +313,11 @@
     }
 
     /**
-     * JWT 토큰 유효성 검증 (기존 로직)
-     */
-    private boolean isTokenValid(String mbrId, String accessToken) {
-        String storedToken = redisTemplate.opsForValue().get("jwt:" + mbrId);
-        return storedToken == null || storedToken.equals(jwtUtil.extractToken(accessToken));
-    }
-
-    /**
-     * 토큰 만료 응답
+     * 토큰 만료 응답 전송
      */
     private void sendTokenExpiredResponse(HttpServletResponse response, HttpServletRequest request) throws IOException {
         ErrorResponse errorResponse = new ErrorResponse();
         errorResponse.setMessage("Token expired");
-        errorResponse.setPath(request.getRequestURI());
-        errorResponse.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase());
-        errorResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
-        errorResponse.setTimestamp(LocalDateTime.now());
-
-        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
-        response.setStatus(HttpStatus.UNAUTHORIZED.value());
-        response.getOutputStream().write(appConfig.getObjectMapper().writeValueAsBytes(errorResponse));
-    }
-
-    /**
-     * 세션 만료 응답 - JWT와 동일한 메시지로 통일
-     */
-    private void sendSessionExpiredResponse(HttpServletResponse response, HttpServletRequest request) throws IOException {
-        ErrorResponse errorResponse = new ErrorResponse();
-        errorResponse.setMessage("Token expired"); // JWT와 동일한 메시지
         errorResponse.setPath(request.getRequestURI());
         errorResponse.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase());
         errorResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
src/main/java/com/takensoft/common/filter/LoginFilter.java
--- src/main/java/com/takensoft/common/filter/LoginFilter.java
+++ src/main/java/com/takensoft/common/filter/LoginFilter.java
@@ -3,7 +3,6 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.takensoft.cms.loginPolicy.service.Email2ndAuthService;
 import com.takensoft.cms.mber.dto.LoginDTO;
-import com.takensoft.cms.mber.service.UnifiedLoginService;
 import com.takensoft.cms.mber.vo.MberVO;
 import com.takensoft.common.exception.FilterExceptionHandler;
 import com.takensoft.common.util.LoginUtil;
@@ -34,8 +33,9 @@
  *  2024.04.01  |  takensoft   | 최초 등록
  *  2025.05.28  |  takensoft   | 통합 로그인 적용
  *  2025.05.29  |  takensoft   | OAuth2 통합 개선
+ *  2025.06.09  |  takensoft   | 기존 시스템 로그인 방식 복원
  *
- * 사용자 로그인 요청을 처리하는 Filter - 통합 로그인 시스템 적용
+ * 사용자 로그인 요청을 처리하는 Filter - 기존 방식 복원
  */
 public class LoginFilter extends UsernamePasswordAuthenticationFilter {
 
@@ -43,15 +43,13 @@
     private final EmailServiceImpl emailServiceImpl;
     private final LoginUtil loginUtil;
     private final Email2ndAuthService email2ndAuth;
-    private final UnifiedLoginService unifiedLoginService;
 
     public LoginFilter(AuthenticationManager authenticationManager, EmailServiceImpl emailServiceImpl,
-                       LoginUtil loginUtil, Email2ndAuthService email2ndAuth, UnifiedLoginService unifiedLoginService) {
+                       LoginUtil loginUtil, Email2ndAuthService email2ndAuth) {
         this.authenticationManager = authenticationManager;
         this.emailServiceImpl = emailServiceImpl;
         this.loginUtil = loginUtil;
         this.email2ndAuth = email2ndAuth;
-        this.unifiedLoginService = unifiedLoginService;
 
         this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/mbr/loginProc.json","POST"));
     }
@@ -67,15 +65,7 @@
         req.setAttribute("lgnReqPage", login.getLgnReqPage());
         req.setAttribute("loginMode", "S"); // 시스템 로그인 표시
 
-        // 통합 로그인 시스템을 통한 사용자 검증
-        try {
-            MberVO authenticatedUser = unifiedLoginService.authenticateUser("S", lgnId, pswd);
-            req.setAttribute("authenticatedUser", authenticatedUser);
-        } catch (Exception e) {
-            // 기존 Spring Security 방식으로 폴백
-        }
-
-        // 스프링 시큐리티 인증 토큰 생성
+        // 기존 Spring Security 방식으로 인증 (MberServiceImpl.loadUserByUsername 사용)
         UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(lgnId, pswd, null);
 
         return authenticationManager.authenticate(authToken);
@@ -85,7 +75,6 @@
     @Override
     protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authentication) throws IOException {
         Map<String, Object> result = new HashMap<>();
-
         MberVO mber = (MberVO) authentication.getPrincipal();
         boolean isAdmin = mber.getAuthorities().stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN"));
         String lgnReqPage = (String) req.getAttribute("lgnReqPage");
src/main/java/com/takensoft/common/message/MessageCode.java
--- src/main/java/com/takensoft/common/message/MessageCode.java
+++ src/main/java/com/takensoft/common/message/MessageCode.java
@@ -74,7 +74,14 @@
     EMAIL_VERIFY_SUCCESS("email.verify_success", HttpStatus.OK), // 이메일 인증 성공
     EMAIL_VERIFY_EXPIRED("email.verify_expired", HttpStatus.UNAUTHORIZED), // 이메일 인증 만료
     EMAIL_VERIFY_FAIL("email.verify_fail", HttpStatus.UNAUTHORIZED), // 이메일 인증 실패
-    CODE_NOT_MATCH("email.code_not_match", HttpStatus.UNAUTHORIZED); // 인증 코드 불일치
+    CODE_NOT_MATCH("email.code_not_match", HttpStatus.UNAUTHORIZED), // 인증 코드 불일치
+
+    //소셜 로그인 관련
+    OAUTH2_LOGIN_ERROR("oauth2.login_error", HttpStatus.INTERNAL_SERVER_ERROR), //소셜 로그인 실패
+    OAUTH2_ACCESS_DENIED("oauth2.access_denied", HttpStatus.FORBIDDEN), // 사용자 인증 취소
+    OAUTH2_INVALID_REQUEST("oauth2.invalid_request", HttpStatus.BAD_REQUEST), // 잘못된 소셜 로그인 요청
+    OAUTH2_UNAUTHORIZED_CLIENT("oauth2.unauthorized_client", HttpStatus.UNAUTHORIZED), // 인증 되지 않은 클라이언트
+    OAUTH2_SERVER_ERROR("oauth2.server_error", HttpStatus.INTERNAL_SERVER_ERROR); // 소셜로그인 서버 오류
 
     private final String messageKey;   //  메시지
     private final HttpStatus status;    // HTTP 상태
src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationFailureHandler.java
--- src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationFailureHandler.java
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationFailureHandler.java
@@ -32,8 +32,7 @@
     private String FRONT_URL;
 
     @Override
-    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
-                                        AuthenticationException exception) throws IOException, ServletException {
+    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException{
 
         String errorMessage = mapErrorMessage(exception);
         String encodedMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8);
src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
--- src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
@@ -15,11 +15,14 @@
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.OAuth2User;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
 import java.net.URLEncoder;
+import java.util.Map;
 
 /**
  * @author takensoft
@@ -30,8 +33,9 @@
  *  2025.05.28  |  takensoft   | 통합 로그인 적용
  *  2025.05.29  |  takensoft   | OAuth2 통합 문제 해결
  *  2025.06.02  |  takensoft   | 세션 모드 중복로그인 처리 개선
+ *  2025.06.09  |  takensoft   | OIDC 타입 캐스팅 문제 해결
  *
- * OAuth2 로그인 성공 핸들러 - 세션 모드 중복로그인 처리 개선
+ * OAuth2 로그인 성공 핸들러
  */
 @Slf4j
 @Component
@@ -48,25 +52,30 @@
     private String frontUrl;
 
     @Override
-    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
-
-        CustomOAuth2UserVO oAuth2User = (CustomOAuth2UserVO) authentication.getPrincipal();
-
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
         try {
+            // OAuth2User 타입 확인 및 정보 추출
+            OAuth2UserInfo userInfo = extractUserInfo(authentication);
+
+            if (userInfo == null) {
+                handleOAuth2Error(response, new Exception("사용자 정보 추출 실패"));
+                return;
+            }
+
             // 현재 설정된 로그인 모드 확인
             String currentLoginMode = loginModeService.getLoginMode();
 
             // 통합 로그인 서비스를 통한 OAuth2 사용자 처리
             MberVO mber = unifiedLoginService.processOAuth2User(
-                    oAuth2User.getEmail(),
-                    convertProviderToMbrType(oAuth2User.getProvider()),
-                    oAuth2User.getId(),
-                    oAuth2User.getName(),
+                    userInfo.email,
+                    unifiedLoginService.convertProviderToMbrType(userInfo.provider),
+                    userInfo.id,
+                    userInfo.name,
                     request
             );
 
             // OAuth2 로그인 이력 저장
-            saveLoginHistory(request, mber, oAuth2User.getProvider());
+            saveLoginHistory(request, mber, userInfo.provider);
 
             request.setAttribute("loginType", "OAUTH2");
 
@@ -74,7 +83,7 @@
             loginUtil.successLogin(mber, request, response);
 
             // OAuth2 성공 후 프론트엔드로 리다이렉트
-            String redirectUrl = String.format("%s/login.page?oauth_success=true&loginMode=%s",frontUrl, currentLoginMode);
+            String redirectUrl = String.format("%s/login.page?oauth_success=true&loginMode=%s", frontUrl, currentLoginMode);
             getRedirectStrategy().sendRedirect(request, response, redirectUrl);
 
         } catch (Exception e) {
@@ -83,37 +92,137 @@
     }
 
     /**
+     * OAuth2User에서 사용자 정보 추출 (타입별 처리)
+     */
+    private OAuth2UserInfo extractUserInfo(Authentication authentication) {
+        Object principal = authentication.getPrincipal();
+
+        try {
+            if (principal instanceof CustomOAuth2UserVO) {
+                // 커스텀 OAuth2 사용자
+                CustomOAuth2UserVO customUser = (CustomOAuth2UserVO) principal;
+                return new OAuth2UserInfo(
+                        customUser.getProvider(),
+                        customUser.getId(),
+                        customUser.getName(),
+                        customUser.getEmail()
+                );
+
+            } else if (principal instanceof OidcUser) {
+                // OIDC 사용자 (구글)
+                OidcUser oidcUser = (OidcUser) principal;
+                return extractOidcUserInfo(oidcUser, authentication);
+
+            } else if (principal instanceof OAuth2User) {
+                // 일반 OAuth2 사용자
+                OAuth2User oauth2User = (OAuth2User) principal;
+                return extractOAuth2UserInfo(oauth2User, authentication);
+            } else {
+                return null;
+            }
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * OIDC 사용자 정보 추출 (구글)
+     */
+    private OAuth2UserInfo extractOidcUserInfo(OidcUser oidcUser, Authentication authentication) {
+        try {
+            String provider = determineProvider(authentication);
+            Map<String, Object> attributes = oidcUser.getAttributes();
+
+            return new OAuth2UserInfo(
+                    provider,
+                    oidcUser.getSubject(), // OIDC의 subject가 사용자 ID
+                    oidcUser.getFullName() != null ? oidcUser.getFullName() : oidcUser.getGivenName(),
+                    oidcUser.getEmail()
+            );
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 일반 OAuth2 사용자 정보 추출
+     */
+    private OAuth2UserInfo extractOAuth2UserInfo(OAuth2User oauth2User, Authentication authentication) {
+        try {
+            String provider = determineProvider(authentication);
+            Map<String, Object> attributes = oauth2User.getAttributes();
+
+            String id = null;
+            String name = null;
+            String email = null;
+
+            // 제공자별 정보 추출
+            switch (provider.toLowerCase()) {
+                case "kakao":
+                    id = String.valueOf(attributes.get("id"));
+                    Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
+                    if (kakaoAccount != null) {
+                        email = (String) kakaoAccount.get("email");
+                        Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
+                        if (profile != null) {
+                            name = (String) profile.get("nickname");
+                        }
+                    }
+                    break;
+
+                case "naver":
+                    Map<String, Object> naverResponse = (Map<String, Object>) attributes.get("response");
+                    if (naverResponse != null) {
+                        id = (String) naverResponse.get("id");
+                        name = (String) naverResponse.get("name");
+                        email = (String) naverResponse.get("email");
+                    }
+                    break;
+
+                case "google":
+                    id = (String) attributes.get("sub");
+                    name = (String) attributes.get("name");
+                    email = (String) attributes.get("email");
+                    break;
+            }
+
+            return new OAuth2UserInfo(provider, id, name, email);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 제공자 결정
+     */
+    private String determineProvider(Authentication authentication) {
+        // 클라이언트 등록 ID에서 제공자 결정
+        String name = authentication.getName();
+        if (name != null) {
+            if (name.contains("google")) return "google";
+            if (name.contains("kakao")) return "kakao";
+            if (name.contains("naver")) return "naver";
+        }
+
+        // 기본값
+        return "google";
+    }
+
+    /**
      * 로그인 이력 저장 - OAuth2 전용
      */
     private void saveLoginHistory(HttpServletRequest request, MberVO mber, String provider) {
-        try {
             String userAgent = httpRequestUtil.getUserAgent(request);
 
             LgnHstryVO loginHistory = new LgnHstryVO();
             loginHistory.setLgnId(mber.getLgnId());
-            loginHistory.setLgnType(mber.getAuthorities().stream()
-                    .anyMatch(r -> r.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1");
+            loginHistory.setLgnType(mber.getAuthorities().stream().anyMatch(r -> r.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1");
             loginHistory.setCntnIp(httpRequestUtil.getIp(request));
             loginHistory.setCntnOperSys(httpRequestUtil.getOS(userAgent));
             loginHistory.setDeviceNm(httpRequestUtil.getDevice(userAgent));
             loginHistory.setBrwsrNm(httpRequestUtil.getBrowser(userAgent));
 
             lgnHstryService.LgnHstrySave(loginHistory);
-        } catch (Exception e) {
-        }
-    }
-
-    /**
-     * 제공자명을 회원타입으로 변환
-     */
-    private String convertProviderToMbrType(String provider) {
-        return switch (provider.toLowerCase()) {
-            case "kakao" -> "K";
-            case "naver" -> "N";
-            case "google" -> "G";
-            case "facebook" -> "F";
-            default -> "S";
-        };
     }
 
     /**
@@ -124,4 +233,21 @@
         String errorUrl = String.format("%s/login.page?error=oauth2_failed&message=%s", frontUrl, message);
         getRedirectStrategy().sendRedirect(null, response, errorUrl);
     }
+
+    /**
+     * OAuth2 사용자 정보 내부 클래스
+     */
+    private static class OAuth2UserInfo {
+        final String provider;
+        final String id;
+        final String name;
+        final String email;
+
+        OAuth2UserInfo(String provider, String id, String name, String email) {
+            this.provider = provider;
+            this.id = id;
+            this.name = name;
+            this.email = email;
+        }
+    }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/oauth/service/Impl/CustomOAuth2UserServiceImpl.java
--- src/main/java/com/takensoft/common/oauth/service/Impl/CustomOAuth2UserServiceImpl.java
+++ src/main/java/com/takensoft/common/oauth/service/Impl/CustomOAuth2UserServiceImpl.java
@@ -72,7 +72,6 @@
             OAuth2UserInfoVO oAuth2UserInfo = new OAuth2UserInfoVO(registrationId, oAuth2User.getAttributes());
 
             // DB 작업 없이 단순히 CustomOAuth2User 객체만 반환
-            // 실제 사용자 저장/업데이트는 OAuth2SuccessHandler에서 처리
             return new CustomOAuth2UserVO(oAuth2UserInfo, oAuth2User.getAttributes());
         } catch (DataAccessException dae) {
             throw dae;
src/main/java/com/takensoft/common/oauth/vo/OAuth2UserInfoVO.java
--- src/main/java/com/takensoft/common/oauth/vo/OAuth2UserInfoVO.java
+++ src/main/java/com/takensoft/common/oauth/vo/OAuth2UserInfoVO.java
@@ -4,6 +4,7 @@
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 
 import java.util.Map;
 
@@ -48,28 +49,101 @@
     }
 
     private void setKakaoUserInfo(Map<String, Object> attributes) {
-        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
-        Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
+        try {
+            Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
+            if (kakaoAccount == null) {
+                throw new IllegalArgumentException("kakao_account 정보가 없습니다.");
+            }
 
-        this.id = String.valueOf(attributes.get("id"));
-        this.name = (String) profile.get("nickname");
-        this.email = (String) kakaoAccount.get("email");
-        this.imageUrl = (String) profile.get("profile_image_url");
+            Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
+            if (profile == null) {
+                throw new IllegalArgumentException("profile 정보가 없습니다.");
+            }
+
+            this.id = String.valueOf(attributes.get("id"));
+            this.name = (String) profile.get("nickname");
+            this.email = (String) kakaoAccount.get("email");
+            this.imageUrl = (String) profile.get("profile_image_url");
+
+            // 필수 정보 검증
+            validateRequiredFields("kakao");
+        } catch (Exception e) {
+            throw new RuntimeException("카카오 로그인 정보 처리 실패", e);
+        }
     }
 
     private void setNaverUserInfo(Map<String, Object> attributes) {
-        Map<String, Object> response = (Map<String, Object>) attributes.get("response");
+        try {
+            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
+            if (response == null) {
+                throw new IllegalArgumentException("response 정보가 없습니다.");
+            }
 
-        this.id = (String) response.get("id");
-        this.name = (String) response.get("name");
-        this.email = (String) response.get("email");
-        this.imageUrl = (String) response.get("profile_image");
+            this.id = (String) response.get("id");
+            this.name = (String) response.get("name");
+            this.email = (String) response.get("email");
+            this.imageUrl = (String) response.get("profile_image");
+
+            // 필수 정보 검증
+            validateRequiredFields("naver");
+        } catch (Exception e) {
+            throw new RuntimeException("네이버 로그인 정보 처리 실패", e);
+        }
     }
 
     private void setGoogleUserInfo(Map<String, Object> attributes) {
-        this.id = (String) attributes.get("sub");
-        this.name = (String) attributes.get("name");
-        this.email = (String) attributes.get("email");
-        this.imageUrl = (String) attributes.get("picture");
+        try {
+            // 구글은 직접 attributes에서 정보 추출
+            this.id = (String) attributes.get("sub");
+            this.name = (String) attributes.get("name");
+            this.email = (String) attributes.get("email");
+            this.imageUrl = (String) attributes.get("picture");
+
+            // 구글 특정 필드 검증 및 대안 처리
+            if (this.id == null) {
+                this.id = (String) attributes.get("id"); // 대안 필드
+            }
+
+            if (this.name == null) {
+                // given_name과 family_name 조합
+                String givenName = (String) attributes.get("given_name");
+                String familyName = (String) attributes.get("family_name");
+                if (givenName != null || familyName != null) {
+                    this.name = (givenName != null ? givenName : "") +
+                            (familyName != null ? " " + familyName : "").trim();
+                }
+            }
+            // 이메일 검증 상태 확인
+            Boolean emailVerified = (Boolean) attributes.get("email_verified");
+            // 필수 정보 검증
+            validateRequiredFields("google");
+        } catch (Exception e) {
+            throw new RuntimeException("구글 로그인 정보 처리 실패", e);
+        }
+    }
+
+    /**
+     * 필수 필드 검증
+     */
+    private void validateRequiredFields(String provider) {
+        if (this.id == null || this.id.trim().isEmpty()) {
+            throw new IllegalArgumentException(provider + " 사용자 ID가 없습니다.");
+        }
+        if (this.email == null || this.email.trim().isEmpty()) {
+            throw new IllegalArgumentException(provider + " 이메일 정보가 없습니다.");
+        }
+        if (this.name == null || this.name.trim().isEmpty()) {
+            this.name = this.email.split("@")[0];
+        }
+    }
+
+    /**
+     * 파싱 실패 시 기본값 설정
+     */
+    private void setDefaultValues() {
+        if (this.id == null) this.id = "unknown_" + System.currentTimeMillis();
+        if (this.name == null) this.name = "Unknown User";
+        if (this.email == null) this.email = this.id + "@unknown.com";
+        if (this.imageUrl == null) this.imageUrl = "";
     }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/util/LoginUtil.java
--- src/main/java/com/takensoft/common/util/LoginUtil.java
+++ src/main/java/com/takensoft/common/util/LoginUtil.java
@@ -12,6 +12,7 @@
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpSession;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.http.HttpStatus;
@@ -22,6 +23,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -33,11 +35,13 @@
  *  2025.05.28  |  takensoft   | 통합 로그인 적용, 문제 해결
  *  2025.05.29  |  takensoft   | Redis 통합 중복로그인 관리
  *  2025.06.04  |  takensoft   | Redis 트랜잭션 및 타이밍 이슈 해결
+ *  2025.06.09  |  takensoft   | 중복로그인 처리 로직 개선
  *
- * 통합 로그인 유틸리티
+ * 통합 로그인 유틸리티 - 중복로그인 처리 개선
  */
 @Component
 @RequiredArgsConstructor
+@Slf4j
 public class LoginUtil {
     private final LgnHstryService lgnHstryService;
     private final HttpRequestUtil httpRequestUtil;
@@ -58,94 +62,82 @@
     /**
      * 통합 로그인 성공 처리
      */
-    public void successLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) {
-        // 로그인 방식 확인
+    public void successLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
         String loginMode = loginModeService.getLoginMode();
-
         res.setHeader("loginMode", loginMode);
-        try {
             // 로그인 이력 등록
             String loginType = (String) req.getAttribute("loginType");
             if (!"OAUTH2".equals(loginType)) {
                 saveLoginHistory(mber, req);
             }
+
             if ("S".equals(loginMode)) {
-                // Redis 기반 중복로그인 관리 적용
                 handleSessionLogin(mber, req, res);
             } else {
-                // 기존 Redis 기반 관리 유지
                 handleJwtLogin(mber, req, res);
             }
-        }
-        catch (IOException ioe) {
-            throw new RuntimeException(ioe);
-        }
-        catch (Exception e) {
-            throw e;
-        }
     }
 
     /**
-     * 세션 모드 로그인 처리 - Redis 트랜잭션 개선
+     * 세션 모드 로그인 처리 - 중복로그인 개선
      */
     private void handleSessionLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
+        String mbrId = mber.getMbrId();
+
+        // 중복로그인 비허용 시 기존 세션 정리
+        if (!loginPolicyService.getPolicy()) {
+            forceLogoutExistingSession(mbrId);
+        }
 
         // JWT 토큰은 생성하되 세션에만 저장
-        String accessToken = jwtUtil.createJwt("Authorization",
-                mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(),
-                (List) mber.getAuthorities(), JWT_ACCESSTIME);
+        String accessToken = jwtUtil.createJwt("Authorization", mbrId, mber.getLgnId(),
+                mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME);
 
         // 세션 생성 및 정보 저장
         HttpSession session = req.getSession(true);
         session.setAttribute("JWT_TOKEN", accessToken);
-        session.setAttribute("mbrId", mber.getMbrId());
+        session.setAttribute("mbrId", mbrId);
         session.setAttribute("mbrNm", mber.getMbrNm());
         session.setAttribute("lgnId", mber.getLgnId());
         session.setAttribute("roles", mber.getAuthorList());
-        session.setAttribute("loginType", req.getAttribute("loginType") != null ?
-                req.getAttribute("loginType") : "S");
+        session.setAttribute("loginType", req.getAttribute("loginType") != null ? req.getAttribute("loginType") : "S");
 
-        // 토큰만 저장
+        // Redis에 세션 토큰 저장 (중복로그인 관리용)
         if (!loginPolicyService.getPolicy()) {
-            String tokenKey = "session_token:" + mber.getMbrId();
-            // 기존 토큰 삭제 후 새 토큰 저장
-            redisTemplate.delete(tokenKey);
-            redisTemplate.opsForValue().set(tokenKey, accessToken, Duration.ofSeconds(session.getMaxInactiveInterval()));
+            saveSessionTokenToRedis(mbrId, accessToken, session.getMaxInactiveInterval());
         }
+
+        // SessionUtil에 등록
+        sessionUtil.registerSession(mbrId, session);
 
         // OAuth2가 아닌 경우 JSON 응답 전송
         String loginType = (String) req.getAttribute("loginType");
         if (!"OAUTH2".equals(loginType)) {
-            try {
-                Map<String, Object> result = new HashMap<>();
-                result.put("mbrId", mber.getMbrId());
-                result.put("mbrNm", mber.getMbrNm());
-                result.put("roles", mber.getAuthorList());
-
-                res.setContentType("application/json;charset=UTF-8");
-                res.setCharacterEncoding("UTF-8");
-                res.setStatus(HttpStatus.OK.value());
-
-                String jsonResponse = new ObjectMapper().writeValueAsString(result);
-                res.getWriter().write(jsonResponse);
-                res.getWriter().flush();
-            } catch (Exception e) {
-                throw e;
-            }
+            sendSessionLoginResponse(res, mber);
         }
+
     }
 
     /**
-     * JWT 모드 로그인 처리
+     * JWT 모드 로그인 처리 - 중복로그인 개선
      */
     private void handleJwtLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
+        String mbrId = mber.getMbrId();
+
+        // 중복로그인 비허용 시 기존 토큰 정리
+        if (!loginPolicyService.getPolicy()) {
+            forceLogoutExistingJWT(mbrId);
+        }
+
         // JWT 토큰 생성
-        String accessToken = jwtUtil.createJwt("Authorization", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME);
-        String refreshToken = jwtUtil.createJwt("refresh", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_REFRESHTIME);
+        String accessToken = jwtUtil.createJwt("Authorization", mbrId, mber.getLgnId(),
+                mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME);
+        String refreshToken = jwtUtil.createJwt("refresh", mbrId, mber.getLgnId(),
+                mber.getMbrNm(), (List) mber.getAuthorities(), JWT_REFRESHTIME);
 
         // Refresh 토큰 처리
         RefreshTknVO refresh = new RefreshTknVO();
-        refresh.setMbrId(mber.getMbrId());
+        refresh.setMbrId(mbrId);
 
         // 기존 refresh 토큰 삭제
         if (refreshTokenService.findByCheckRefresh(req, refresh)) {
@@ -157,10 +149,9 @@
         res.setHeader("Authorization", accessToken);
         res.addCookie(jwtUtil.createCookie("refresh", refreshToken, COOKIE_TIME));
 
-        // 중복 로그인 비허용일 때 Redis 저장
+        // Redis에 JWT 저장 (중복로그인 관리용)
         if (!loginPolicyService.getPolicy()) {
-            redisTemplate.delete("jwt:" + mber.getMbrId());
-            redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS);
+            saveJWTTokenToRedis(mbrId, accessToken);
         }
 
         // Refresh 토큰 저장
@@ -171,13 +162,87 @@
         if (!"OAUTH2".equals(loginType)) {
             res.setStatus(HttpStatus.OK.value());
         }
+
+    }
+
+    /**
+     * 기존 세션 강제 로그아웃
+     */
+    private void forceLogoutExistingSession(String mbrId) {
+            // 1. SessionUtil에서 기존 세션 제거
+            sessionUtil.removeSession(mbrId);
+
+            // 2. Redis에서 기존 세션 토큰 삭제
+            String sessionTokenKey = "session_token:" + mbrId;
+            String existingToken = redisTemplate.opsForValue().get(sessionTokenKey);
+
+            if (existingToken != null) {
+                redisTemplate.delete(sessionTokenKey);
+            }
+            // 3. 기타 세션 관련 키 정리
+            cleanupSessionRedisKeys(mbrId);
+    }
+
+    /**
+     * 기존 JWT 강제 로그아웃
+     */
+    private void forceLogoutExistingJWT(String mbrId) {
+            String jwtKey = "jwt:" + mbrId;
+            String existingToken = redisTemplate.opsForValue().get(jwtKey);
+
+            if (existingToken != null) {
+                redisTemplate.delete(jwtKey);
+            }
+    }
+
+    /**
+     * Redis에 세션 토큰 저장
+     */
+    private void saveSessionTokenToRedis(String mbrId, String accessToken, int sessionTimeout) {
+            String tokenKey = "session_token:" + mbrId;
+            redisTemplate.opsForValue().set(tokenKey, accessToken, Duration.ofSeconds(sessionTimeout));
+    }
+
+    /**
+     * Redis에 JWT 토큰 저장
+     */
+    private void saveJWTTokenToRedis(String mbrId, String accessToken) {
+            String jwtKey = "jwt:" + mbrId;
+            redisTemplate.opsForValue().set(jwtKey, accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 세션 관련 Redis 키 정리
+     */
+    private void cleanupSessionRedisKeys(String mbrId) {
+        Set<String> sessionKeys = redisTemplate.keys("session*:" + mbrId);
+        if (sessionKeys != null && !sessionKeys.isEmpty()) {
+            redisTemplate.delete(sessionKeys);
+        }
+    }
+
+    /**
+     * 세션 로그인 응답 전송
+     */
+    private void sendSessionLoginResponse(HttpServletResponse res, MberVO mber) throws IOException {
+            Map<String, Object> result = new HashMap<>();
+            result.put("mbrId", mber.getMbrId());
+            result.put("mbrNm", mber.getMbrNm());
+            result.put("roles", mber.getAuthorList());
+
+            res.setContentType("application/json;charset=UTF-8");
+            res.setCharacterEncoding("UTF-8");
+            res.setStatus(HttpStatus.OK.value());
+
+            String jsonResponse = new ObjectMapper().writeValueAsString(result);
+            res.getWriter().write(jsonResponse);
+            res.getWriter().flush();
     }
 
     /**
      * 로그인 이력 저장
      */
     private void saveLoginHistory(MberVO mber, HttpServletRequest req) {
-        try {
             String userAgent = httpRequestUtil.getUserAgent(req);
 
             LgnHstryVO lgnHstryVO = new LgnHstryVO();
@@ -190,7 +255,5 @@
             lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(userAgent));
 
             lgnHstryService.LgnHstrySave(lgnHstryVO);
-        } catch (Exception e) {
-        }
     }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/util/SessionUtil.java
--- src/main/java/com/takensoft/common/util/SessionUtil.java
+++ src/main/java/com/takensoft/common/util/SessionUtil.java
@@ -7,6 +7,7 @@
 
 import java.time.Duration;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -16,6 +17,7 @@
  *     since    |    author    | description
  *  2025.03.21  |  takensoft   | 최초 등록
  *  2025.05.29  |  takensoft   | Redis 통합 중복로그인 관리
+ *  2025.06.09  |  takensoft   | 세션 관리 로직 개선, 안정성 향상
  *
  * 세션 로그인 방식의 유틸리티
  */
@@ -31,110 +33,136 @@
     }
 
     /**
-     * 세션 등록 - Redis 연동
-     * 기존 세션 있으면 강제 로그아웃 후 새 세션 등록
+     * 세션 등록 - 중복로그인 처리 개선
      */
     public synchronized void registerSession(String mbrId, HttpSession newSession) {
-        try {
-            // 1. 기존 메모리 세션 처리
-            HttpSession oldSession = sessionMap.get(mbrId);
-            if (oldSession != null && oldSession != newSession) {
-                try {
-                    oldSession.invalidate();
-                } catch (IllegalStateException e) {
 
-                }
+            // 1. 기존 세션 확인 및 무효화
+            HttpSession oldSession = sessionMap.get(mbrId);
+            if (oldSession != null && !oldSession.getId().equals(newSession.getId())) {
+                    oldSession.invalidate();
             }
             // 2. 새 세션을 메모리에 등록
             sessionMap.put(mbrId, newSession);
 
-            // 3. Redis 동기화는 LoginUtil에서 처리됨
-
-        } catch (Exception e) {
-        }
+            // 3. Redis에 세션 정보 저장 (세션 ID 저장)
+            String sessionKey = "session:" + mbrId;
+            redisTemplate.opsForValue().set(sessionKey, newSession.getId(),
+                    Duration.ofSeconds(newSession.getMaxInactiveInterval()));
     }
 
     /**
      * 세션 ID로 세션 무효화
      */
     public void invalidateSessionById(String sessionId) {
-        try {
-            boolean found = false;
             // 메모리에서 해당 세션 ID를 가진 세션 찾아서 무효화
+            String targetMbrId = null;
             sessionMap.entrySet().removeIf(entry -> {
                 HttpSession session = entry.getValue();
                 if (session != null && session.getId().equals(sessionId)) {
-                    try {
                         session.invalidate();
                         return true;
-                    } catch (IllegalStateException e) {
-                        return true;
-                    }
                 }
                 return false;
             });
-
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
     }
 
     /**
-     * 사용자별 세션 제거 - Redis 연동
+     * 사용자별 세션 제거 - Redis 연동 개선
      */
     public void removeSession(String mbrId) {
-        try {
             // 1. 메모리 세션 무효화
             HttpSession session = sessionMap.get(mbrId);
             if (session != null) {
-                try {
                     session.invalidate();
-                } catch (IllegalStateException e) {
-
-                }
             }
             sessionMap.remove(mbrId);
 
-            // 2. Redis에서도 제거
-            String sessionKey = "session:" + mbrId;
-            String deletedSessionId = redisTemplate.opsForValue().get(sessionKey);
-            if (deletedSessionId != null) {
-                redisTemplate.delete(sessionKey);
-            }
-
-        } catch (Exception e) {
-        }
+            // 2. Redis에서 모든 관련 키 제거
+            cleanupRedisSessionData(mbrId);
     }
 
     /**
-     * 전체 세션 무효화 - Redis 연동
+     * 전체 세션 무효화 - Redis 연동 개선
      */
     public void invalidateAllSessions() {
-        try {
+
             // 1. 모든 메모리 세션 무효화
+            int invalidatedCount = 0;
             for (Map.Entry<String, HttpSession> entry : sessionMap.entrySet()) {
                 HttpSession session = entry.getValue();
                 if (session != null) {
-                    try {
                         session.invalidate();
-                    } catch (IllegalStateException e) {
-                        e.printStackTrace();
-                    }
+                        invalidatedCount++;
                 }
             }
             sessionMap.clear();
 
-            // 2. Redis에서 모든 세션 키 삭제
-            try {
-                var sessionKeys = redisTemplate.keys("session:*");
-                if (sessionKeys != null && !sessionKeys.isEmpty()) {
-                    redisTemplate.delete(sessionKeys);
-                }
-            } catch (Exception e) {
-                e.printStackTrace();
+            // 2. Redis에서 모든 세션 관련 키 삭제
+            cleanupAllRedisSessionData();
+    }
+
+    /**
+     * 특정 사용자의 Redis 세션 데이터 정리
+     */
+    private void cleanupRedisSessionData(String mbrId) {
+            // 세션 관련 키들 삭제
+            String sessionKey = "session:" + mbrId;
+            String sessionTokenKey = "session_token:" + mbrId;
+
+            redisTemplate.delete(sessionKey);
+            redisTemplate.delete(sessionTokenKey);
+
+            // 패턴 매칭으로 사용자 관련 모든 세션 키 찾아서 삭제
+            Set<String> userSessionKeys = redisTemplate.keys("session*:" + mbrId);
+            if (userSessionKeys != null && !userSessionKeys.isEmpty()) {
+                redisTemplate.delete(userSessionKeys);
             }
-        } catch (Exception e) {
-            e.printStackTrace();
+    }
+
+    /**
+     * 모든 Redis 세션 데이터 정리
+     */
+    private void cleanupAllRedisSessionData() {
+            // 모든 세션 관련 키 패턴들
+            String[] sessionKeyPatterns = {
+                    "session:*",
+                    "session_token:*"
+            };
+
+            for (String pattern : sessionKeyPatterns) {
+                Set<String> keys = redisTemplate.keys(pattern);
+                if (keys != null && !keys.isEmpty()) {
+                    redisTemplate.delete(keys);
+                }
+            }
+    }
+
+    /**
+     * 활성 세션 수 조회
+     */
+    public int getActiveSessionCount() {
+        return sessionMap.size();
+    }
+
+    /**
+     * 특정 사용자의 세션 활성 상태 확인
+     */
+    public boolean isSessionActive(String mbrId) {
+        HttpSession session = sessionMap.get(mbrId);
+        if (session == null) {
+            return false;
+        }
+
+        try {
+            // 세션 접근을 통한 유효성 확인
+            session.getLastAccessedTime();
+            return true;
+        } catch (IllegalStateException e) {
+            // 무효화된 세션이면 맵에서 제거
+            sessionMap.remove(mbrId);
+            return false;
         }
     }
+
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/resources/application.yml
--- src/main/resources/application.yml
+++ src/main/resources/application.yml
@@ -95,8 +95,12 @@
             client-secret: ${GOOGLE_CLIENT_SECRET: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}
             redirect-uri: ${GOOGLE_REDIRECT_URI:http://localhost:9090/login/oauth2/code/google}
             authorization-grant-type: authorization_code
-            scope: profile,email
+            scope:
+              - openid
+              - profile
+              - email
             client-name: Google
+            client-authentication-method: client_secret_basic
 
         provider:
           # 카카오 제공업체 설정
@@ -113,6 +117,16 @@
             user-info-uri: https://openapi.naver.com/v1/nid/me
             user-name-attribute: response
 
+          # 구글 제공업체 설정
+          google:
+            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
+            token-uri: https://www.googleapis.com/oauth2/v4/token
+            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
+            user-name-attribute: sub
+            # OIDC 지원을 위한 설정
+            jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
+            issuer-uri: https://accounts.google.com
+
 
 # Mybatis settings
 #mybatis:
src/main/resources/message/messages_en.yml
--- src/main/resources/message/messages_en.yml
+++ src/main/resources/message/messages_en.yml
@@ -66,4 +66,12 @@
   verify_success: "Email verification completed successfully."
   verify_expired: "Email verification has expired."
   verify_fail: "Email verification failed."
-  code_not_match: "verification code does not match."
(파일 끝에 줄바꿈 문자 없음)
+  code_not_match: "verification code does not match."
+
+# 소셜로그인 관련
+oauth2:
+  login_error: "Social sign-in failed."
+  access_denied: "User cancelled authentication."
+  invalid_request: "Invalid social sign-in request."
+  unauthorized_client: "Unauthorized client."
+  server_error: "Social authentication server error occurred."
(파일 끝에 줄바꿈 문자 없음)
src/main/resources/message/messages_ko.yml
--- src/main/resources/message/messages_ko.yml
+++ src/main/resources/message/messages_ko.yml
@@ -68,3 +68,11 @@
   verify_expired: "인증 시간이 만료되었습니다."
   verify_fail: "이메일 인증에 실패했습니다."
   code_not_match: "인증 코드가 일치하지 않습니다."
+
+# 소셜로그인 관련
+oauth2:
+    login_error: "소셜 로그인에 실패했습니다."
+    access_denied: "사용자가 인증을 취소했습니다."
+    invalid_request: "잘못된 소셜 로그인 요청입니다."
+    unauthorized_client: "인증 되지 않은 클라이언트입니다."
+    server_error: "소셜 로그인 서버 오류가 발생했습니다."
src/main/resources/mybatis/mapper/loginPolicy/loginMode-SQL.xml
--- src/main/resources/mybatis/mapper/loginPolicy/loginMode-SQL.xml
+++ src/main/resources/mybatis/mapper/loginPolicy/loginMode-SQL.xml
@@ -15,7 +15,7 @@
     <select id="selectLatestLoginMode" resultType="String">
        SELECT lgn_mode
         FROM lgn_mode_hstry
-        ORDER BY reg_dt DESC
+        ORDER BY reg_dt ASC
         LIMIT 1
     </select>
 
src/main/resources/mybatis/mapper/loginPolicy/loginPolicy-SQL.xml
--- src/main/resources/mybatis/mapper/loginPolicy/loginPolicy-SQL.xml
+++ src/main/resources/mybatis/mapper/loginPolicy/loginPolicy-SQL.xml
@@ -15,7 +15,7 @@
     <select id="selectLatestPolicy" resultType="String">
         SELECT allow_multiple_login
         FROM lgn_policy_hstry
-        ORDER BY reg_dt DESC
+        ORDER BY reg_dt ASC
         LIMIT 1
     </select>
 
src/main/resources/mybatis/mapper/mber/mber-SQL.xml
--- src/main/resources/mybatis/mapper/mber/mber-SQL.xml
+++ src/main/resources/mybatis/mapper/mber/mber-SQL.xml
@@ -122,11 +122,11 @@
         WHERE EXISTS (
         SELECT 1 FROM mbr_social_accounts msa
         WHERE msa.mbr_id = mi.mbr_id
-        <if test="providerType == 'S'">
+        <if test="providerType != null and 'S'.equals(providerType)">
             AND msa.provider_type = 'S'
             AND msa.login_id = #{identifier}
         </if>
-        <if test="providerType != 'S'">
+        <if test="providerType != null and !'S'.equals(providerType)">
             AND msa.provider_type = #{providerType}
             AND msa.social_id = #{identifier}
         </if>
Add a comment
List