hmkim 05-29
250529 김혜민 시스템, 소셜 통합 로그인 기능 추가 및 변경
@3d66e578250082b3c2ed3470e05d1c761a048f6b
src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
--- src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
+++ src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
@@ -3,27 +3,31 @@
 import com.takensoft.cms.mber.dto.JoinDTO;
 import com.takensoft.cms.mber.dto.PasswordDTO;
 import com.takensoft.cms.mber.vo.MberAuthorVO;
+import com.takensoft.cms.mber.vo.MberSocialAccountVO;
 import com.takensoft.cms.mber.vo.MberVO;
 import com.takensoft.common.Pagination;
 import org.egovframe.rte.psl.dataaccess.mapper.Mapper;
 
 import java.util.*;
+
 /**
  * @author takensoft
  * @since 2024.04.01
  * @modification
  *     since    |    author    | description
  *  2024.04.01  |  takensoft   | 최초 등록
+ *  2025.05.29  |  takensoft   | 통합 로그인 기능 추가
  *
- * 회원 정보 관련 Mapper
+ * 회원 정보 관련 Mapper - 통합 로그인 시스템 지원
  */
 @Mapper("mberDAO")
 public interface MberDAO {
+
     /**
      * @param lgnId - 로그인 아이디
      * @return MberVO - 사용자 정보 조회 결과
      *
-     * 사용자 정보 조회 [security 용]
+     * 사용자 정보 조회 [security 용] - 통합 로그인 대응
      */
     MberVO findByMberSecurity(String lgnId);
 
@@ -31,7 +35,7 @@
      * @param lgnId - 로그인 아이디
      * @return boolean - 아이디 중복 여부
      *
-     * 아이디 중복 검사
+     * 아이디 중복 검사 - 통합 로그인 대응
      */
     boolean findByCheckLoginId(String lgnId);
 
@@ -115,4 +119,92 @@
      * 회원 ID로 권한 목록 조회
      */
     List<MberAuthorVO> findAuthoritiesByMbrId(String mbrId);
+
+    // ===================================
+    // 통합 로그인을 위한 새로운 메서드들
+    // ===================================
+
+    /**
+     * @param params - 제공자 타입과 식별자를 포함한 Map
+     *               - providerType: 제공자 타입 (SYSTEM, KAKAO, NAVER, GOOGLE)
+     *               - identifier: 식별자 (로그인ID 또는 소셜ID)
+     * @return MberVO - 통합 로그인으로 조회된 사용자 정보
+     *
+     * 통합 로그인: 제공자별 사용자 조회
+     */
+    MberVO findByUnifiedLogin(HashMap<String, Object> params);
+
+    /**
+     * @param email - 이메일
+     * @return MberVO - 이메일로 조회된 사용자 정보 (첫 번째 계정)
+     *
+     * 이메일로 모든 연동 계정 조회 (통합용)
+     */
+    MberVO findAllAccountsByEmail(String email);
+
+    /**
+     * @param mbrId - 회원 ID
+     * @return List<MberSocialAccountVO> - 소셜 계정 목록
+     *
+     * 회원 ID로 소셜 계정 목록 조회
+     */
+    List<MberSocialAccountVO> findSocialAccountsByMbrId(String mbrId);
+
+    /**
+     * @param params - 회원 ID와 제공자 타입을 포함한 Map
+     *               - mbrId: 회원 ID
+     *               - providerType: 제공자 타입
+     * @return MberSocialAccountVO - 특정 제공자의 소셜 계정 정보
+     *
+     * 특정 제공자로 소셜 계정 조회
+     */
+    MberSocialAccountVO findSocialAccountByProvider(HashMap<String, Object> params);
+
+    /**
+     * @param socialAccount - 소셜 계정 정보
+     * @return int - 저장 결과
+     *
+     * 소셜 계정 정보 저장
+     */
+    int saveSocialAccount(MberSocialAccountVO socialAccount);
+
+    /**
+     * @param socialAccount - 소셜 계정 연동 정보
+     * @return int - 연동 결과
+     *
+     * 소셜 계정 연동 (중복 시 업데이트)
+     */
+    int linkSocialAccount(MberSocialAccountVO socialAccount);
+
+    /**
+     * @param params - 연동 해지 정보를 포함한 Map
+     *               - mbrId: 회원 ID
+     *               - providerType: 제공자 타입
+     *               - mdfr: 수정자
+     * @return int - 해지 결과
+     *
+     * 소셜 계정 연동 해지
+     */
+    int unlinkSocialAccount(HashMap<String, Object> params);
+
+    /**
+     * @param params - 메인 프로필 설정 정보를 포함한 Map
+     *               - mbrId: 회원 ID
+     *               - providerType: 제공자 타입
+     *               - mdfr: 수정자
+     * @return int - 설정 결과
+     *
+     * 메인 프로필 설정
+     */
+    int setPrimaryProfile(HashMap<String, Object> params);
+
+    /**
+     * @param params - 연동 가능한 계정 검색 정보를 포함한 Map
+     *               - email: 이메일
+     *               - providerType: 제외할 제공자 타입
+     * @return MberVO - 연동 가능한 계정 정보
+     *
+     * 연동 가능한 계정 조회 (이메일로 검색, 특정 제공자 제외)
+     */
+    MberVO findLinkableAccount(HashMap<String, Object> params);
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
--- src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
@@ -5,6 +5,7 @@
 import com.takensoft.cms.mber.dto.PasswordDTO;
 import com.takensoft.cms.mber.service.MberService;
 import com.takensoft.cms.mber.vo.MberAuthorVO;
+import com.takensoft.cms.mber.vo.MberSocialAccountVO;
 import com.takensoft.cms.mber.vo.MberVO;
 import com.takensoft.common.exception.*;
 import com.takensoft.common.idgen.service.IdgenService;
@@ -12,6 +13,7 @@
 import com.takensoft.common.util.HttpRequestUtil;
 import com.takensoft.common.util.Secret;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
 import org.springframework.dao.DataAccessException;
 import org.springframework.security.core.userdetails.UserDetails;
@@ -33,6 +35,7 @@
  * @modification
  *     since    |    author    | description
  *  2024.04.01  |  takensoft   | 최초 등록
+ *  2025.05.29  |  takensoft   | 통합 로그인 기능 추가
  *
  * 회원 정보 관련 구현체
  * EgovAbstractServiceImpl : 전자정부 상속
@@ -41,6 +44,7 @@
  */
 @Service("mberService")
 @RequiredArgsConstructor
+@Slf4j
 public class MberServiceImpl extends EgovAbstractServiceImpl implements UserDetailsService, MberService {
     private final MberDAO mberDAO;
     private final IdgenService mberIdgn;
@@ -54,12 +58,13 @@
      * @throws UsernameNotFoundException - 가입하지 않은 계정으로 로그인 시도 시
      * @throws Exception - 그 외 예외 발생 시
      *
-     * security 상속 시 Override 하는 메소드
+     * security 상속 시 Override 하는 메소드 - 통합 로그인 대응
      */
     @Override
     @Transactional(readOnly = true)
     public UserDetails loadUserByUsername(String username){
         try {
+            // 통합 로그인: 모든 제공자에서 사용자 검색
             UserDetails userDetails = mberDAO.findByMberSecurity(username);
 
             return userDetails;
@@ -69,6 +74,7 @@
             throw e;
         }
     }
+
     /**
      * @param lgnId - 로그인 아이디
      * @return boolean - 아이디 아이디 중복 여부
@@ -76,7 +82,7 @@
      * @throws DataAccessException - db 관련 예외 발생 시
      * @throws Exception - 그 외 예외 발생 시
      *
-     * 아이디 중복 검사
+     * 아이디 중복 검사 - 통합 로그인 대응
      */
     @Override
     public boolean findByCheckLoginId(String lgnId) {
@@ -104,82 +110,171 @@
      * @throws DataAccessException - db 관련 예외 발생 시
      * @throws Exception - 그 외 예외 발생 시
      *
-     * 회원가입
+     * 회원가입 - 통합 로그인 대응
      */
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public HashMap<String, Object> userJoin(HttpServletRequest req,  JoinDTO joinDTO){
+    public HashMap<String, Object> userJoin(HttpServletRequest req, JoinDTO joinDTO){
         try {
-            // 회원 아이디 생성 (이미 설정된 경우 건너뛰기)
-            if (joinDTO.getMbrId() == null || joinDTO.getMbrId().isEmpty()) {
-                String mbrId = mberIdgn.getNextStringId();
-                joinDTO.setMbrId(mbrId);
-            }
-
-            // 아이디 소문자 변환
-            if (joinDTO.getLgnId() != null && !joinDTO.getLgnId().isEmpty()) {
-                joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase());
-            }
-
-            // 비밀번호 암호화 (OAuth2는 비밀번호 없음)
-            if (joinDTO.getPswd() != null && !joinDTO.getPswd().isEmpty()) {
-                joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd()));
-            }
-
-            // 연락처 암호화
-            if(joinDTO.getMblTelno() != null && !joinDTO.getMblTelno().equals("")) {
-                joinDTO.setMblTelno(Secret.encrypt(joinDTO.getMblTelno()));
-            }
-            if(joinDTO.getTelno() != null && !joinDTO.getTelno().equals("")) {
-                joinDTO.setTelno(Secret.encrypt(joinDTO.getTelno()));
-            }
-            //멤버타입 없을시 default "S" 고정
-            if (joinDTO.getMbrType() == null || joinDTO.getMbrType().isEmpty()) {
-                joinDTO.setMbrType("S");
-            }
-            // 아이피 조회 및 등록
-            joinDTO.setFrstRegIp(httpRequestUtil.getIp(req));
-
-            // 등록된 토큰에서 사용자 정보 조회
-            String writer = joinDTO.getRgtr();
-            if (writer == null || writer.isEmpty()) {
-                writer = verificationService.getCurrentUserId();
-                if (writer == null || writer.isEmpty()) {
-                    throw new CustomNotFoundException("사용자 정보 조회에 실패했습니다.");
-                }
-                joinDTO.setRgtr(writer);
-            }
-
-            // 회원정보 등록
-            HashMap<String, Object> result = new HashMap<>();
-            int saveResult = mberDAO.save(joinDTO);
-            if(saveResult == 0) {
-                throw new CustomInsertFailException("회원 정보 등록에 실패했습니다.");
-            }
-
-            result.put("mbrId", joinDTO.getMbrId());
-
-            // 권한 등록
-            int authorResult = 0;
-            if(joinDTO.getAuthorList().size() > 0) {
-                for(MberAuthorVO vo : joinDTO.getAuthorList()) {
-                    vo.setMbrId(joinDTO.getMbrId());
-                    // 작성자 등록
-                    vo.setRgtr(writer);
-
-                    authorResult += mberDAO.authorSave(vo);
-                    if(authorResult == 0) {
-                        throw new CustomInsertFailException("회원 권한 등록에 실패했습니다.");
-                    }
+            // 이메일로 기존 계정 확인 (기본 검사만 수행)
+            if (joinDTO.getEml() != null && !joinDTO.getEml().isEmpty()) {
+                MberVO existingUser = mberDAO.findByEmail(joinDTO.getEml());
+                if (existingUser != null) {
+                    throw new CustomIdTakenException("해당 이메일로 이미 계정이 등록되어 있습니다.");
                 }
             }
-            result.put("result", saveResult + authorResult);
 
-            return result;
+            // 기존 회원가입 로직 실행
+            return performStandardJoin(req, joinDTO);
+
         } catch (DataAccessException dae) {
             throw dae;
         } catch (Exception e) {
             throw e;
+        }
+    }
+
+    /**
+     * 표준 회원가입 처리 (기존 로직)
+     */
+    private HashMap<String, Object> performStandardJoin(HttpServletRequest req, JoinDTO joinDTO) {
+        // 회원 아이디 생성 (이미 설정된 경우 건너뛰기)
+        if (joinDTO.getMbrId() == null || joinDTO.getMbrId().isEmpty()) {
+            String mbrId = mberIdgn.getNextStringId();
+            joinDTO.setMbrId(mbrId);
+        }
+
+        // 아이디 소문자 변환
+        if (joinDTO.getLgnId() != null && !joinDTO.getLgnId().isEmpty()) {
+            joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase());
+        }
+
+        // 비밀번호 암호화 (OAuth2는 비밀번호 없음)
+        if (joinDTO.getPswd() != null && !joinDTO.getPswd().isEmpty()) {
+            joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd()));
+        }
+
+        // 연락처 암호화
+        if(joinDTO.getMblTelno() != null && !joinDTO.getMblTelno().equals("")) {
+            joinDTO.setMblTelno(Secret.encrypt(joinDTO.getMblTelno()));
+        }
+        if(joinDTO.getTelno() != null && !joinDTO.getTelno().equals("")) {
+            joinDTO.setTelno(Secret.encrypt(joinDTO.getTelno()));
+        }
+        //멤버타입 없을시 default "S" 고정
+        if (joinDTO.getMbrType() == null || joinDTO.getMbrType().isEmpty()) {
+            joinDTO.setMbrType("S");
+        }
+        // 아이피 조회 및 등록
+        joinDTO.setFrstRegIp(httpRequestUtil.getIp(req));
+
+        // 등록된 토큰에서 사용자 정보 조회
+        String writer = joinDTO.getRgtr();
+        if (writer == null || writer.isEmpty()) {
+            writer = verificationService.getCurrentUserId();
+            if (writer == null || writer.isEmpty()) {
+                writer = "SYSTEM"; // 자체 회원가입인 경우
+            }
+            joinDTO.setRgtr(writer);
+        }
+
+        // 회원정보 등록
+        HashMap<String, Object> result = new HashMap<>();
+        int saveResult = mberDAO.save(joinDTO);
+        if(saveResult == 0) {
+            throw new CustomInsertFailException("회원 정보 등록에 실패했습니다.");
+        }
+
+        result.put("mbrId", joinDTO.getMbrId());
+
+        // 권한 등록
+        int authorResult = 0;
+        if(joinDTO.getAuthorList().size() > 0) {
+            for(MberAuthorVO vo : joinDTO.getAuthorList()) {
+                vo.setMbrId(joinDTO.getMbrId());
+                // 작성자 등록
+                vo.setRgtr(writer);
+
+                authorResult += mberDAO.authorSave(vo);
+                if(authorResult == 0) {
+                    throw new CustomInsertFailException("회원 권한 등록에 실패했습니다.");
+                }
+            }
+        }
+
+        // 시스템 로그인 계정 정보 소셜 계정 테이블에도 저장
+        MberSocialAccountVO systemAccount = new MberSocialAccountVO();
+        systemAccount.setMbrId(joinDTO.getMbrId());
+        systemAccount.setProviderType("S");
+        systemAccount.setLoginId(joinDTO.getLgnId());
+        systemAccount.setSocialEmail(joinDTO.getEml());
+        systemAccount.setSocialName(joinDTO.getMbrNm());
+        systemAccount.setIsPrimaryProfile(true);
+        systemAccount.setIsActive(true);
+        systemAccount.setRgtr(writer);
+
+        mberDAO.saveSocialAccount(systemAccount);
+
+        result.put("result", saveResult + authorResult);
+
+        return result;
+    }
+
+    /**
+     * 기존 계정에 시스템 로그인 연동
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public HashMap<String, Object> linkSystemLogin(String mbrId, String loginId, String password, String rgtr) {
+        try {
+            // 회원 정보 조회
+            MberVO user = mberDAO.findByMber(mbrId);
+            if (user == null) {
+                throw new CustomNotFoundException("회원 정보를 찾을 수 없습니다.");
+            }
+
+            // 비밀번호 설정
+            PasswordDTO passwordDTO = new PasswordDTO();
+            passwordDTO.setMbrId(mbrId);
+            passwordDTO.setNewPswd(password);
+            passwordDTO.setMdfr(rgtr);
+            updatePassword(passwordDTO);
+
+            // 시스템 로그인 연동
+            boolean linkResult = linkSystemLoginAccount(
+                    mbrId, loginId, user.getEml(), user.getMbrNm(), rgtr
+            );
+
+            HashMap<String, Object> result = new HashMap<>();
+            result.put("success", linkResult);
+            result.put("mbrId", mbrId);
+            result.put("message", "시스템 로그인이 성공적으로 연동되었습니다.");
+
+            return result;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 시스템 로그인 계정 연동 (내부 메서드)
+     */
+    private boolean linkSystemLoginAccount(String mbrId, String loginId, String email, String name, String rgtr) {
+        try {
+            MberSocialAccountVO systemAccount = new MberSocialAccountVO();
+            systemAccount.setMbrId(mbrId);
+            systemAccount.setProviderType("S");
+            systemAccount.setLoginId(loginId);
+            systemAccount.setSocialEmail(email);
+            systemAccount.setSocialName(name);
+            systemAccount.setIsPrimaryProfile(false);
+            systemAccount.setIsActive(true);
+            systemAccount.setRgtr(rgtr);
+
+            mberDAO.linkSocialAccount(systemAccount);
+            return true;
+        } catch (Exception e) {
+            log.error("시스템 로그인 연동 실패", e);
+            return false;
         }
     }
 
@@ -206,10 +301,8 @@
 
             // 비밀번호 비교 후 성공 시 비밀번호 수정 후 true 반환
             if (bCryptPasswordEncoder.matches(passwordDTO.getPswd(), mbr.getPassword())) {
-//                passwordDTO.setNewPswd(bCryptPasswordEncoder.encode(passwordDTO.getNewPswd()));
                 passwordDTO.setMbrId(mbr.getMbrId());
                 passwordDTO.setMdfr(writer);
-//                mberDAO.updatePassword(passwordDTO);
                 int result = updatePassword(passwordDTO);
                 return true;
                 // 기존 비밀번호와 입력한 비밀번호가 서로 다를 경우 false 반환
@@ -222,6 +315,7 @@
             throw e;
         }
     }
+
     /**
      * @param params
      *  - 회원 아이디
@@ -280,9 +374,7 @@
         }
     }
 
-
-
-    // OAuth2 관련 메서드 구현
+    // OAuth2 관련 메서드 구현 (기존 메서드들 유지)
 
     /**
      * @param email - 이메일
@@ -317,7 +409,7 @@
     @Transactional(readOnly = true)
     public MberVO findByEmailAndProvider(String email, String mbrType) {
         try {
-            MberVO user = mberDAO.findByEmailAndProvider(email, mbrType); // 파라미터 직접 전달
+            MberVO user = mberDAO.findByEmailAndProvider(email, mbrType);
 
             // 권한 정보도 함께 조회
             if (user != null) {
@@ -348,8 +440,8 @@
             // OAuth2 사용자를 JoinDTO로 변환하여 기존 검증된 로직 활용
             JoinDTO oauthJoinDTO = createOAuthJoinDTO(user);
 
-            // 기존 userJoin 메서드 활용 (검증된 로직)
-            HashMap<String, Object> result = userJoin(request, oauthJoinDTO);
+            // 기존 userJoin 메서드 활용하되, 통합 검사는 우회
+            HashMap<String, Object> result = performStandardJoin(request, oauthJoinDTO);
 
             // 생성된 회원 ID 설정
             user.setMbrId(result.get("mbrId").toString());
 
src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java (added)
+++ src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java
@@ -0,0 +1,350 @@
+package com.takensoft.cms.mber.service.Impl;
+
+import com.takensoft.cms.mber.dao.MberDAO;
+import com.takensoft.cms.mber.service.UnifiedLoginService;
+import com.takensoft.cms.mber.vo.MberAuthorVO;
+import com.takensoft.cms.mber.vo.MberSocialAccountVO;
+import com.takensoft.cms.mber.vo.MberVO;
+import com.takensoft.common.exception.*;
+import com.takensoft.common.idgen.service.IdgenService;
+import com.takensoft.common.util.HttpRequestUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
+import org.springframework.dao.DataAccessException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author takensoft
+ * @since 2025.05.29
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.29  |  takensoft   | 최초 등록
+ *
+ * 통합 로그인 서비스 구현체 - 순환 의존성 해결
+ * EgovAbstractServiceImpl : 전자정부 상속
+ * UnifiedLoginService : 통합 로그인 인터페이스 상속
+ */
+@Service("unifiedLoginService")
+@RequiredArgsConstructor
+@Slf4j
+public class UnifiedLoginServiceImpl extends EgovAbstractServiceImpl implements UnifiedLoginService {
+
+    private final MberDAO mberDAO;
+    private final IdgenService mberIdgn;
+    private final HttpRequestUtil httpRequestUtil;
+
+    // BCryptPasswordEncoder는 필요할 때 ApplicationContext에서 가져오도록 수정
+    private BCryptPasswordEncoder passwordEncoder;
+
+    /**
+     * BCryptPasswordEncoder 지연 초기화 (순환 의존성 해결)
+     */
+    private BCryptPasswordEncoder getPasswordEncoder() {
+        if (passwordEncoder == null) {
+            passwordEncoder = new BCryptPasswordEncoder();
+        }
+        return passwordEncoder;
+    }
+
+    /**
+     * 통합 로그인 인증
+     */
+    @Override
+    @Transactional(readOnly = true)
+    public MberVO authenticateUser(String providerType, String identifier, String password) {
+        try {
+            HashMap<String, Object> params = new HashMap<>();
+            params.put("providerType", providerType);
+            params.put("identifier", identifier);
+
+            MberVO user = mberDAO.findByUnifiedLogin(params);
+
+            if (user == null) {
+                throw new CustomNotFoundException("사용자를 찾을 수 없습니다.");
+            }
+
+            // 시스템 로그인의 경우 비밀번호 검증
+            if ("S".equals(providerType)) {
+                if (password == null || !getPasswordEncoder().matches(password, user.getPassword())) {
+                    throw new CustomPasswordComparisonException("비밀번호가 일치하지 않습니다.");
+                }
+            }
+
+            return user;
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * OAuth2 사용자 처리 (가입 또는 연동)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public MberVO processOAuth2User(String email, String providerType, String socialId, String name, HttpServletRequest request) {
+        try {
+            // 1. 이메일로 기존 계정 검색
+            MberVO existingUser = mberDAO.findAllAccountsByEmail(email);
+
+            if (existingUser != null) {
+                // 2-1. 기존 계정 있음 - 해당 소셜이 이미 연동되어 있는지 확인
+                HashMap<String, Object> params = new HashMap<>();
+                params.put("mbrId", existingUser.getMbrId());
+                params.put("providerType", providerType);
+
+                MberSocialAccountVO existingSocial = mberDAO.findSocialAccountByProvider(params);
+
+                if (existingSocial != null) {
+                    // 이미 연동되어 있음 - 정보만 업데이트
+                    existingSocial.setSocialName(name);
+                    existingSocial.setMdfr("OAUTH2_UPDATE");
+                    updateSocialAccountInfo(existingSocial);
+                    return existingUser;
+                } else {
+                    // 새로운 소셜 계정 연동
+                    linkAccount(existingUser.getMbrId(), providerType, socialId, null, email, name);
+                    return existingUser;
+                }
+            } else {
+                // 2-2. 기존 계정 없음 - 새 계정 생성
+                return createNewOAuth2User(email, providerType, socialId, name, request);
+            }
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 새로운 OAuth2 사용자 생성
+     */
+    private MberVO createNewOAuth2User(String email, String providerType, String socialId, String name, HttpServletRequest request) {
+        try {
+            // 회원 ID 생성
+            String mbrId = mberIdgn.getNextStringId();
+
+            // 새 사용자 정보 설정
+            MberVO newUser = new MberVO();
+            newUser.setMbrId(mbrId);
+            newUser.setEml(email);
+            newUser.setLgnId(email.toLowerCase());
+            newUser.setMbrNm(name);
+            newUser.setNcnm(name);
+            newUser.setMbrType(providerType);
+            newUser.setMbrStts("1");
+            newUser.setUseYn(true);
+            newUser.setSysPvsnYn("1");
+            newUser.setRgtr("OAUTH2_SYSTEM");
+
+            // 회원 정보 저장
+            int result = mberDAO.saveOAuthUser(newUser);
+            if (result == 0) {
+                throw new CustomInsertFailException("OAuth2 사용자 저장에 실패했습니다.");
+            }
+
+            // 기본 권한 등록
+            MberAuthorVO userRole = new MberAuthorVO();
+            userRole.setMbrId(mbrId);
+            userRole.setAuthrtCd("ROLE_USER");
+            userRole.setRgtr("OAUTH2_SYSTEM");
+            mberDAO.authorSave(userRole);
+
+            // 소셜 계정 정보 저장
+            MberSocialAccountVO socialAccount = new MberSocialAccountVO();
+            socialAccount.setMbrId(mbrId);
+            socialAccount.setProviderType(providerType);
+            socialAccount.setSocialId(socialId);
+            socialAccount.setSocialEmail(email);
+            socialAccount.setSocialName(name);
+            socialAccount.setIsPrimaryProfile(true);
+            socialAccount.setIsActive(true);
+            socialAccount.setRgtr("OAUTH2_SYSTEM");
+
+            mberDAO.saveSocialAccount(socialAccount);
+
+            return newUser;
+        } catch (Exception e) {
+            throw new CustomInsertFailException("OAuth2 사용자 생성에 실패했습니다: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 계정 연동
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean linkAccount(String mbrId, String providerType, String socialId, String loginId, String email, String name) {
+        try {
+            // 중복 연동 확인
+            HashMap<String, Object> params = new HashMap<>();
+            params.put("mbrId", mbrId);
+            params.put("providerType", providerType);
+
+            MberSocialAccountVO existing = mberDAO.findSocialAccountByProvider(params);
+            if (existing != null && existing.getIsActive()) {
+                throw new CustomIdTakenException("이미 연동된 계정입니다.");
+            }
+
+            MberSocialAccountVO socialAccount = new MberSocialAccountVO();
+            socialAccount.setMbrId(mbrId);
+            socialAccount.setProviderType(providerType);
+            socialAccount.setSocialId(socialId);
+            socialAccount.setLoginId(loginId);
+            socialAccount.setSocialEmail(email);
+            socialAccount.setSocialName(name);
+            socialAccount.setIsPrimaryProfile(false);
+            socialAccount.setIsActive(true);
+            socialAccount.setRgtr("LINK_SYSTEM");
+
+            mberDAO.linkSocialAccount(socialAccount);
+            return true;
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 계정 연동 해지
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean unlinkAccount(String mbrId, String providerType) {
+        try {
+            // 연동된 계정 개수 확인 (최소 1개는 유지해야 함)
+            List<MberSocialAccountVO> linkedAccounts = getLinkedAccounts(mbrId);
+            if (linkedAccounts.size() <= 1) {
+                throw new CustomNotFoundException("최소 하나의 로그인 방법은 유지해야 합니다.");
+            }
+
+            HashMap<String, Object> params = new HashMap<>();
+            params.put("mbrId", mbrId);
+            params.put("providerType", providerType);
+            params.put("mdfr", "UNLINK_SYSTEM");
+
+            mberDAO.unlinkSocialAccount(params);
+
+            // 해지된 계정이 메인 프로필이었다면 다른 계정을 메인으로 설정
+            MberSocialAccountVO unlinkedAccount = linkedAccounts.stream()
+                    .filter(acc -> acc.getProviderType().equals(providerType))
+                    .findFirst()
+                    .orElse(null);
+
+            if (unlinkedAccount != null && unlinkedAccount.getIsPrimaryProfile()) {
+                // 첫 번째 활성 계정을 메인으로 설정
+                MberSocialAccountVO newPrimary = linkedAccounts.stream()
+                        .filter(acc -> !acc.getProviderType().equals(providerType))
+                        .findFirst()
+                        .orElse(null);
+
+                if (newPrimary != null) {
+                    setPrimaryProfile(mbrId, newPrimary.getProviderType());
+                }
+            }
+
+            return true;
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 연동된 계정 목록 조회
+     */
+    @Override
+    @Transactional(readOnly = true)
+    public List<MberSocialAccountVO> getLinkedAccounts(String mbrId) {
+        try {
+            return mberDAO.findSocialAccountsByMbrId(mbrId);
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 메인 프로필 설정
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean setPrimaryProfile(String mbrId, String providerType) {
+        try {
+            HashMap<String, Object> params = new HashMap<>();
+            params.put("mbrId", mbrId);
+            params.put("providerType", providerType);
+            params.put("mdfr", "PROFILE_UPDATE");
+
+            mberDAO.setPrimaryProfile(params);
+            return true;
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 계정 통합 제안
+     */
+    @Override
+    @Transactional(readOnly = true)
+    public HashMap<String, Object> suggestAccountMerge(String email, String newProviderType) {
+        try {
+            HashMap<String, Object> result = new HashMap<>();
+
+            // 이메일로 기존 계정 검색
+            MberVO existingUser = mberDAO.findAllAccountsByEmail(email);
+
+            if (existingUser != null) {
+                // 기존 계정의 연동 정보 조회
+                List<MberSocialAccountVO> linkedAccounts = getLinkedAccounts(existingUser.getMbrId());
+
+                // 새로운 제공자가 이미 연동되어 있는지 확인
+                boolean alreadyLinked = linkedAccounts.stream()
+                        .anyMatch(acc -> acc.getProviderType().equals(newProviderType));
+
+                result.put("hasExistingAccount", true);
+                result.put("mbrId", existingUser.getMbrId());
+                result.put("mbrNm", existingUser.getMbrNm());
+                result.put("linkedAccounts", linkedAccounts);
+                result.put("alreadyLinked", alreadyLinked);
+                result.put("canLink", !alreadyLinked);
+            } else {
+                result.put("hasExistingAccount", false);
+                result.put("canCreateNew", true);
+            }
+
+            return result;
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 소셜 계정 정보 업데이트
+     */
+    private void updateSocialAccountInfo(MberSocialAccountVO socialAccount) {
+        try {
+            mberDAO.linkSocialAccount(socialAccount);
+        } catch (Exception e) {
+            log.warn("소셜 계정 정보 업데이트 실패", e);
+        }
+    }
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/cms/mber/service/UnifiedLoginService.java (added)
+++ src/main/java/com/takensoft/cms/mber/service/UnifiedLoginService.java
@@ -0,0 +1,83 @@
+package com.takensoft.cms.mber.service;
+
+import com.takensoft.cms.mber.vo.MberSocialAccountVO;
+import com.takensoft.cms.mber.vo.MberVO;
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author takensoft
+ * @since 2025.05.29
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.29  |  takensoft   | 최초 등록
+ *
+ * 통합 로그인 서비스 인터페이스
+ */
+public interface UnifiedLoginService {
+
+    /**
+     * 통합 로그인 인증
+     * @param providerType 제공자 타입 (SYSTEM, KAKAO, NAVER, GOOGLE)
+     * @param identifier 식별자 (로그인ID 또는 소셜ID)
+     * @param password 비밀번호 (시스템 로그인시만 필요)
+     * @return MberVO 인증된 사용자 정보
+     */
+    MberVO authenticateUser(String providerType, String identifier, String password);
+
+    /**
+     * OAuth2 사용자 처리 (가입 또는 연동)
+     * @param email 이메일
+     * @param providerType 제공자 타입
+     * @param socialId 소셜 고유 ID
+     * @param name 이름
+     * @param request HTTP 요청 객체
+     * @return MberVO 처리된 사용자 정보
+     */
+    MberVO processOAuth2User(String email, String providerType, String socialId, String name, HttpServletRequest request);
+
+    /**
+     * 계정 연동
+     * @param mbrId 회원 ID
+     * @param providerType 제공자 타입
+     * @param socialId 소셜 ID (시스템일 경우 null)
+     * @param loginId 로그인 ID (소셜일 경우 null)
+     * @param email 이메일
+     * @param name 이름
+     * @return boolean 연동 성공 여부
+     */
+    boolean linkAccount(String mbrId, String providerType, String socialId, String loginId, String email, String name);
+
+    /**
+     * 계정 연동 해지
+     * @param mbrId 회원 ID
+     * @param providerType 제공자 타입
+     * @return boolean 해지 성공 여부
+     */
+    boolean unlinkAccount(String mbrId, String providerType);
+
+    /**
+     * 연동된 계정 목록 조회
+     * @param mbrId 회원 ID
+     * @return List<MberSocialAccountVO> 연동된 계정 목록
+     */
+    List<MberSocialAccountVO> getLinkedAccounts(String mbrId);
+
+    /**
+     * 메인 프로필 설정
+     * @param mbrId 회원 ID
+     * @param providerType 제공자 타입
+     * @return boolean 설정 성공 여부
+     */
+    boolean setPrimaryProfile(String mbrId, String providerType);
+
+    /**
+     * 계정 통합 제안
+     * @param email 이메일
+     * @param newProviderType 새로운 제공자 타입
+     * @return HashMap<String, Object> 통합 제안 정보
+     */
+    HashMap<String, Object> suggestAccountMerge(String email, String newProviderType);
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/cms/mber/vo/MberSocialAccountVO.java (added)
+++ src/main/java/com/takensoft/cms/mber/vo/MberSocialAccountVO.java
@@ -0,0 +1,42 @@
+package com.takensoft.cms.mber.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author takensoft
+ * @since 2025.05.29
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.29  |  takensoft   | 최초 등록
+ *
+ * 소셜 계정 연동 정보 VO
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class MberSocialAccountVO {
+
+    private Long id;                    // 연동 ID
+    private String mbrId;               // 회원 ID
+    private String providerType;        // 제공자 타입 (SYSTEM, KAKAO, NAVER, GOOGLE)
+    private String socialId;            // 소셜 로그인 고유 ID
+    private String loginId;             // 시스템 로그인 ID
+    private String socialEmail;         // 소셜 계정 이메일
+    private String socialName;          // 소셜 계정 이름
+    private Boolean isPrimaryProfile;   // 메인 프로필 여부
+    private Boolean isActive;           // 연동 활성화 여부
+    private LocalDateTime linkedDt;     // 연동일시
+    private LocalDateTime unlinkedDt;   // 연동 해지일시
+    private String rgtr;                // 등록자
+    private LocalDateTime regDt;        // 등록일시
+    private String mdfr;                // 수정자
+    private LocalDateTime mdfcnDt;      // 수정일시
+
+
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/cms/mber/web/UnifiedLoginController.java (added)
+++ src/main/java/com/takensoft/cms/mber/web/UnifiedLoginController.java
@@ -0,0 +1,133 @@
+package com.takensoft.cms.mber.web;
+
+import com.takensoft.cms.mber.service.UnifiedLoginService;
+import com.takensoft.cms.mber.vo.MberSocialAccountVO;
+import com.takensoft.common.message.MessageCode;
+import com.takensoft.common.service.VerificationService;
+import com.takensoft.common.util.ResponseUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author takensoft
+ * @since 2025.05.29
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.29  |  takensoft   | 최초 등록
+ *
+ * 통합 로그인 관련 컨트롤러
+ */
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping(value = "/mbr/unified")
+public class UnifiedLoginController {
+
+    private final UnifiedLoginService unifiedLoginService;
+    private final VerificationService verificationService;
+    private final ResponseUtil resUtil;
+
+    /**
+     * 연동된 계정 목록 조회
+     */
+    @PostMapping("/linkedAccounts.json")
+    public ResponseEntity<?> getLinkedAccounts() {
+        try {
+            String currentUserId = verificationService.getCurrentUserId();
+            List<MberSocialAccountVO> linkedAccounts = unifiedLoginService.getLinkedAccounts(currentUserId);
+
+            return resUtil.successRes(linkedAccounts, MessageCode.COMMON_SUCCESS);
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+        }
+    }
+
+    /**
+     * 계정 연동
+     */
+    @PostMapping("/linkAccount.json")
+    public ResponseEntity<?> linkAccount(@RequestBody HashMap<String, String> params) {
+        try {
+            String currentUserId = verificationService.getCurrentUserId();
+            String providerType = params.get("providerType");
+            String socialId = params.get("socialId");
+            String loginId = params.get("loginId");
+            String email = params.get("email");
+            String name = params.get("name");
+
+            boolean success = unifiedLoginService.linkAccount(currentUserId, providerType, socialId, loginId, email, name);
+
+            if (success) {
+                return resUtil.successRes("계정 연동이 완료되었습니다.", MessageCode.COMMON_SUCCESS);
+            } else {
+                return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+            }
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+        }
+    }
+
+    /**
+     * 계정 연동 해지
+     */
+    @PostMapping("/unlinkAccount.json")
+    public ResponseEntity<?> unlinkAccount(@RequestBody HashMap<String, String> params) {
+        try {
+            String currentUserId = verificationService.getCurrentUserId();
+            String providerType = params.get("providerType");
+
+            boolean success = unifiedLoginService.unlinkAccount(currentUserId, providerType);
+
+            if (success) {
+                return resUtil.successRes("계정 연동이 해지되었습니다.", MessageCode.COMMON_SUCCESS);
+            } else {
+                return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+            }
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+        }
+    }
+
+    /**
+     * 메인 프로필 설정
+     */
+    @PostMapping("/setPrimaryProfile.json")
+    public ResponseEntity<?> setPrimaryProfile(@RequestBody HashMap<String, String> params) {
+        try {
+            String currentUserId = verificationService.getCurrentUserId();
+            String providerType = params.get("providerType");
+
+            boolean success = unifiedLoginService.setPrimaryProfile(currentUserId, providerType);
+
+            if (success) {
+                return resUtil.successRes("메인 프로필이 설정되었습니다.", MessageCode.COMMON_SUCCESS);
+            } else {
+                return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+            }
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+        }
+    }
+
+    /**
+     * 계정 통합 제안 조회
+     */
+    @PostMapping("/suggestMerge.json")
+    public ResponseEntity<?> suggestAccountMerge(@RequestBody HashMap<String, String> params) {
+        try {
+            String email = params.get("email");
+            String newProviderType = params.get("providerType");
+
+            HashMap<String, Object> suggestion = unifiedLoginService.suggestAccountMerge(email, newProviderType);
+
+            return resUtil.successRes(suggestion, MessageCode.COMMON_SUCCESS);
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+        }
+    }
+}(파일 끝에 줄바꿈 문자 없음)
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
@@ -5,6 +5,7 @@
 import com.takensoft.cms.loginPolicy.service.Email2ndAuthService;
 import com.takensoft.cms.loginPolicy.service.LoginModeService;
 import com.takensoft.cms.loginPolicy.service.LoginPolicyService;
+import com.takensoft.cms.mber.service.UnifiedLoginService;
 import com.takensoft.common.filter.*;
 import com.takensoft.common.util.HttpRequestUtil;
 import com.takensoft.common.exception.CustomAccessDenieHandler;
@@ -43,8 +44,9 @@
  *  2025.01.22  |  takensoft   | 최초 등록
  *  2025.05.22  |  takensoft   | OAuth2 로그인 추가
  *  2025.05.26  |    하석형     | 로그인 유틸 추가
+ *  2025.05.29  |  takensoft   | 통합 로그인 시스템 적용
  *
- * Spring Security 설정을 위한 Config
+ * Spring Security 설정을 위한 Config - 통합 로그인 시스템 적용
  */
 @Configuration
 @EnableWebSecurity
@@ -64,6 +66,7 @@
     private final EmailServiceImpl emailServiceImpl;
     private final LoginUtil loginUtil;
     private final Email2ndAuthService email2ndAuth;
+    private final UnifiedLoginService unifiedLoginService;
 
     @Autowired
     private CustomOAuth2UserServiceImpl customOAuth2UserServiceImpl;
@@ -82,7 +85,7 @@
      * SecurityConfig 생성자
      */
     public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, CntxtPthService cntxtPthService, AccesCtrlService accesCtrlService, AppConfig appConfig, CustomAuthenticationEntryPoint authenticationEntryPoint, CustomAccessDenieHandler accessDenieHandler,
-                          HttpRequestUtil httpRequestUtil, LoginModeService loginModeService, LoginPolicyService loginPolicyService, EmailServiceImpl emailServiceImpl, @Value("${front.url}") String fUrl, RedisTemplate<String, String> redisTemplate, LoginUtil loginUtil, Email2ndAuthService email2ndAuth) {
+                          HttpRequestUtil httpRequestUtil, LoginModeService loginModeService, LoginPolicyService loginPolicyService, EmailServiceImpl emailServiceImpl, @Value("${front.url}") String fUrl, RedisTemplate<String, String> redisTemplate, LoginUtil loginUtil, Email2ndAuthService email2ndAuth, UnifiedLoginService unifiedLoginService) {
         this.authenticationConfiguration = authenticationConfiguration;
         this.cntxtPthService = cntxtPthService;
         this.accesCtrlService = accesCtrlService;
@@ -98,6 +101,7 @@
         this.emailServiceImpl = emailServiceImpl;
         this.loginUtil = loginUtil;
         this.email2ndAuth = email2ndAuth;
+        this.unifiedLoginService = unifiedLoginService;
     }
 
     /**
@@ -192,8 +196,7 @@
         http.addFilterBefore(new AccesFilter(accesCtrlService, httpRequestUtil, appConfig), JWTFilter.class);
 
         // 로그인 필터
-        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil, email2ndAuth), UsernamePasswordAuthenticationFilter.class);
-
+        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil, email2ndAuth, unifiedLoginService), UsernamePasswordAuthenticationFilter.class);
 
         return http.build();
     }
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
@@ -27,16 +27,16 @@
 import java.io.IOException;
 import java.time.LocalDateTime;
 import java.util.List;
+
 /**
  * @author takensoft
  * @since 2024.04.01
  * @modification
  *     since    |    author    | description
  *  2024.04.01  |  takensoft   | 최초 등록
+ *  2025.05.30  |  takensoft   | 모드별 명확한 분기 처리, Redis 통합
  *
- * OncePerRequestFilter - 한 번의 요청마다 단 한 번만 필터링 작업을 수행하는 필터를 제공하는 클래스
- *
- * JWT 토큰 검증 Filter
+ * JWT 토큰 검증 Filter - 모드별 명확한 분기 처리
  */
 public class JWTFilter extends OncePerRequestFilter {
 
@@ -48,65 +48,36 @@
     private final RedisTemplate<String, String> redisTemplate;
     private final LoginModeService loginModeService;
 
-    /**
-     * @param jwtUtil JWT 유틸리티 클래스의 인스턴스
-     *
-     * JWTFilter 생성자
-     */
-    public JWTFilter(JWTUtil jwtUtil, AppConfig appConfig, LoginModeService loginModeService, LoginPolicyService loginPolicyService, RedisTemplate<String, String> redisTemplate) {
+    public JWTFilter(JWTUtil jwtUtil, AppConfig appConfig, LoginModeService loginModeService,
+                     LoginPolicyService loginPolicyService, RedisTemplate<String, String> redisTemplate) {
         this.jwtUtil = jwtUtil;
         this.appConfig = appConfig;
         this.loginModeService = loginModeService;
         this.loginPolicyService = loginPolicyService;
         this.redisTemplate = redisTemplate;
     }
-    /**
-     * @param request HttpServletRequest 객체
-     * @param response HttpServletResponse 객체
-     * @param filterChain 필터 체인을 통해 다음 필터로 요청을 전달
-     * @throws ServletException 필터 처리 중 발생한 서블릿 예외
-     * @throws IOException 필터 처리 중 발생한 IO 예외
-     *
-     * JWT 토큰 검증
-     */
+
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         String requestURI = request.getRequestURI();
 
-        // OAuth2 관련 경로는 JWT 검증 제외
+        // OAuth2 관련 경로는 검증 제외
         if (isOAuth2Request(requestURI)) {
             filterChain.doFilter(request, response);
             return;
         }
+
         try {
             String loginMode = loginModeService.getLoginMode();
-            String accessToken = resolveToken(request, loginMode);
 
-            if (accessToken == null) {
-                filterChain.doFilter(request, response);
-                return;
+            if ("S".equals(loginMode)) {
+                // 세션 모드: 세션 기반 검증 및 Redis 중복로그인 체크
+                handleSessionMode(request, response, filterChain);
+            } else {
+                // JWT 모드: 토큰 기반 검증
+                handleJwtMode(request, response, filterChain);
             }
 
-            if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) {
-                sendTokenExpiredResponse(response, request);
-                return;
-            }
-
-            MberVO mber = new MberVO(
-                    (String) jwtUtil.getClaim(accessToken, "mbrId"),
-                    (String) jwtUtil.getClaim(accessToken, "lgnId"),
-                    (List<MberAuthorVO>) jwtUtil.getClaim(accessToken, "roles")
-            );
-
-            if (!loginPolicyService.getPolicy() && !isTokenValid(mber.getMbrId(), accessToken)) {
-                sendTokenExpiredResponse(response, request);
-                return;
-            }
-
-            Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities());
-            SecurityContextHolder.getContext().setAuthentication(authToken);
-
-            filterChain.doFilter(request, response);
         } catch (ExpiredJwtException e) {
             FilterExceptionHandler.jwtError(response, e);
         } catch (JwtException e) {
@@ -118,13 +89,90 @@
         }
     }
 
-    private String resolveToken(HttpServletRequest request, String loginMode) {
-        if ("S".equals(loginMode)) {
-            HttpSession session = request.getSession(false);
-            return session != null ? (String) session.getAttribute(SESSION_JWT_KEY) : null;
-        } else {
-            return request.getHeader(AUTHORIZATION_HEADER);
+    /**
+     * 세션 모드 처리 - Redis 기반 중복로그인 체크
+     */
+    private void handleSessionMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+        HttpSession session = request.getSession(false);
+
+        if (session == null) {
+            filterChain.doFilter(request, response);
+            return;
         }
+
+        String mbrId = (String) session.getAttribute("mbrId");
+        if (mbrId == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        // Redis 기반 중복로그인 체크 (세션 모드)
+        if (!loginPolicyService.getPolicy()) {
+            String sessionKey = "session:" + mbrId;
+            String storedSessionId = redisTemplate.opsForValue().get(sessionKey);
+
+            if (storedSessionId == null || !storedSessionId.equals(session.getId())) {
+                // 다른 곳에서 로그인했거나 세션이 만료됨
+                try {
+                    session.invalidate();
+                } catch (IllegalStateException e) {
+                    // 이미 무효화된 세션
+                }
+                sendSessionExpiredResponse(response, request);
+                return;
+            }
+        }
+
+        // 세션 정보로 Authentication 설정
+        try {
+            List<MberAuthorVO> roles = (List<MberAuthorVO>) session.getAttribute("roles");
+            String lgnId = (String) session.getAttribute("lgnId");
+            String mbrNm = (String) session.getAttribute("mbrNm");
+
+            MberVO mber = new MberVO(mbrId, lgnId, roles);
+            mber.setMbrNm(mbrNm);
+
+            Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities());
+            SecurityContextHolder.getContext().setAuthentication(authToken);
+
+            filterChain.doFilter(request, response);
+        } catch (Exception e) {
+            sendSessionExpiredResponse(response, request);
+        }
+    }
+
+    /**
+     * JWT 모드 처리 - 기존 로직 유지
+     */
+    private void handleJwtMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+        String accessToken = request.getHeader(AUTHORIZATION_HEADER);
+
+        if (accessToken == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) {
+            sendTokenExpiredResponse(response, request);
+            return;
+        }
+
+        MberVO mber = new MberVO(
+                (String) jwtUtil.getClaim(accessToken, "mbrId"),
+                (String) jwtUtil.getClaim(accessToken, "lgnId"),
+                (List<MberAuthorVO>) jwtUtil.getClaim(accessToken, "roles")
+        );
+
+        // JWT 모드에서 중복로그인 체크
+        if (!loginPolicyService.getPolicy() && !isTokenValid(mber.getMbrId(), accessToken)) {
+            sendTokenExpiredResponse(response, request);
+            return;
+        }
+
+        Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities());
+        SecurityContextHolder.getContext().setAuthentication(authToken);
+
+        filterChain.doFilter(request, response);
     }
 
     /**
@@ -136,13 +184,17 @@
                 requestURI.startsWith("/oauth/");
     }
 
-
+    /**
+     * JWT 토큰 유효성 검증 (기존 로직)
+     */
     private boolean isTokenValid(String mbrId, String accessToken) {
         String storedToken = redisTemplate.opsForValue().get("jwt:" + mbrId);
-        return storedToken == null || storedToken.equals(accessToken);
+        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");
@@ -155,4 +207,20 @@
         response.setStatus(HttpStatus.UNAUTHORIZED.value());
         response.getOutputStream().write(appConfig.getObjectMapper().writeValueAsBytes(errorResponse));
     }
+
+    /**
+     * 세션 만료 응답
+     */
+    private void sendSessionExpiredResponse(HttpServletResponse response, HttpServletRequest request) throws IOException {
+        ErrorResponse errorResponse = new ErrorResponse();
+        errorResponse.setMessage("Session expired or duplicate login detected");
+        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));
+    }
 }
(파일 끝에 줄바꿈 문자 없음)
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,12 +3,14 @@
 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;
 import com.takensoft.common.verify.service.Impl.EmailServiceImpl;
 import com.takensoft.common.verify.vo.EmailVO;
 import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -31,119 +33,96 @@
  * @modification
  *     since    |    author    | description
  *  2024.04.01  |  takensoft   | 최초 등록
+ *  2025.05.28  |  takensoft   | 통합 로그인 적용
+ *  2025.05.29  |  takensoft   | OAuth2 통합 개선
  *
- * UsernamePasswordAuthenticationFilter - Spring Security에서 사용자 로그인 요청을 처리하는 필터 클래스
- *
- * 사용자 로그인 요청을 처리하는 Filter
+ * 사용자 로그인 요청을 처리하는 Filter - 통합 로그인 시스템 적용
  */
+@Slf4j
 public class LoginFilter extends UsernamePasswordAuthenticationFilter {
 
     private final AuthenticationManager authenticationManager;
     private final EmailServiceImpl emailServiceImpl;
     private final LoginUtil loginUtil;
     private final Email2ndAuthService email2ndAuth;
-    /**
-     * LoginFilter 생성자
-     */
-    public LoginFilter(AuthenticationManager authenticationManager, EmailServiceImpl emailServiceImpl, LoginUtil loginUtil, Email2ndAuthService email2ndAuth) {
+    private final UnifiedLoginService unifiedLoginService;
+
+    public LoginFilter(AuthenticationManager authenticationManager, EmailServiceImpl emailServiceImpl,
+                       LoginUtil loginUtil, Email2ndAuthService email2ndAuth, UnifiedLoginService unifiedLoginService) {
         this.authenticationManager = authenticationManager;
         this.emailServiceImpl = emailServiceImpl;
         this.loginUtil = loginUtil;
         this.email2ndAuth = email2ndAuth;
+        this.unifiedLoginService = unifiedLoginService;
 
         this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/mbr/loginProc.json","POST"));
     }
 
-    /**
-     * @param req - HTTP 요청 객체
-     * @param res - HTTP 응답 객체
-     * @return 인증 정보
-     * @throws AuthenticationException - 인증 예외
-     *
-     * 로그인 요청을 처리 ( 사용자가 입력한 로그인 정보로 인증 시도 )
-     */
     @SneakyThrows
     @Override
     public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
         ObjectMapper mapper = new ObjectMapper();
         LoginDTO login = mapper.readValue(req.getInputStream(), LoginDTO.class);
-        // 클라이언트에서 요청한 아이디와 비밀번호 추출
+
         String lgnId = login.getLgnId();
         String pswd = login.getPswd();
         req.setAttribute("lgnReqPage", login.getLgnReqPage());
+        req.setAttribute("loginType", "S"); // 시스템 로그인 표시
 
-        // 스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함
+        // 통합 로그인 시스템을 통한 사용자 검증
+        try {
+            MberVO authenticatedUser = unifiedLoginService.authenticateUser("S", lgnId, pswd);
+            req.setAttribute("authenticatedUser", authenticatedUser);
+        } catch (Exception e) {
+            // 기존 Spring Security 방식으로 폴백
+        }
+
+        // 스프링 시큐리티 인증 토큰 생성
         UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(lgnId, pswd, null);
 
-        // token에 담은 검증을 위한 AuthenticationManager로 전달
         return authenticationManager.authenticate(authToken);
     }
 
-    /**
-     * @param req - HTTP 요청 객체
-     * @param res - HTTP 응답 객체
-     * @param chain - 필터 체인
-     * @param authentication - 인증된 사용자 정보
-     * @throws IOException - IO 예외
-     * @throws ServletException - 서블릿 예외
-     *
-     * 로그인 인증 성공 시
-     */
     @SneakyThrows
     @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"); // 로그인 요청 페이지 정보 (A: 관리자, U: 사용자)
-        boolean use2ndAuth = email2ndAuth.findByEmail2ndAuth(); // 이메일 2차 인증 여부 확인
+        boolean isAdmin = mber.getAuthorities().stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN"));
+        String lgnReqPage = (String) req.getAttribute("lgnReqPage");
+        boolean use2ndAuth = email2ndAuth.findByEmail2ndAuth();
 
-        // 관리자일 경우 2차 인증(이메일 인증) 코드 발송
-        if(isAdmin) {
-            if(use2ndAuth) {
-                EmailVO emailVO = new EmailVO().builder()
-                        .email(mber.getEml())
-                        .build();
-                emailServiceImpl.sendEmailVerifyCode(emailVO);
-                res.setContentType("application/json;charset=UTF-8");
-                res.setStatus(HttpStatus.OK.value());
-                result.put("mbrId", mber.getMbrId());
-                result.put("email", mber.getEml());
+        // 관리자 2차 인증 처리
+        if(isAdmin && use2ndAuth) {
+            EmailVO emailVO = new EmailVO().builder()
+                    .email(mber.getEml())
+                    .build();
+            emailServiceImpl.sendEmailVerifyCode(emailVO);
 
-                res.setContentType("application/json;charset=UTF-8");
-                res.setStatus(HttpStatus.OK.value());
+            result.put("mbrId", mber.getMbrId());
+            result.put("email", mber.getEml());
 
-                new ObjectMapper().writeValue(res.getOutputStream(), result);
-            } else {
-                loginUtil.successLogin(mber, req, res); // 로그인 성공 처리
-            }
-        // 사용자일 경우
-        } else {
-            // 사용자가 관리자 로그인 페이지로 접근할 경우
-            if("A".equals(lgnReqPage)) {
-                res.setContentType("application/json;charset=UTF-8");
-                res.setStatus(HttpStatus.FORBIDDEN.value());
-
-                result.put("message", "접근 권한이 없습니다.");
-                new ObjectMapper().writeValue(res.getOutputStream(), result);
-            } else {
-                loginUtil.successLogin(mber, req, res); // 로그인 성공 처리
-            }
+            res.setContentType("application/json;charset=UTF-8");
+            res.setStatus(HttpStatus.OK.value());
+            new ObjectMapper().writeValue(res.getOutputStream(), result);
+            return;
         }
+
+        // 사용자 권한 체크
+        if (!isAdmin && "A".equals(lgnReqPage)) {
+            res.setContentType("application/json;charset=UTF-8");
+            res.setStatus(HttpStatus.FORBIDDEN.value());
+            result.put("message", "접근 권한이 없습니다.");
+            new ObjectMapper().writeValue(res.getOutputStream(), result);
+            return;
+        }
+
+        loginUtil.successLogin(mber, req, res);
     }
 
-    /**
-     * @param req - HTTP 요청 객체
-     * @param res - HTTP 응답 객체
-     * @param failed - 인증 예외
-     * @throws IOException - IO 예외
-     * @throws ServletException - 서블릿 예외
-     *
-     * 로그인 인증 실패 시
-     */
     @Override
     protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException {
         FilterExceptionHandler.loginError(res, failed);
     }
-}
+}
(파일 끝에 줄바꿈 문자 없음)
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
@@ -1,62 +1,47 @@
 package com.takensoft.common.oauth.handler;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.takensoft.cms.loginPolicy.service.LoginModeService;
-import com.takensoft.cms.loginPolicy.service.LoginPolicyService;
 import com.takensoft.cms.mber.service.LgnHstryService;
-import com.takensoft.cms.mber.service.MberService;
+import com.takensoft.cms.mber.service.UnifiedLoginService;
 import com.takensoft.cms.mber.vo.LgnHstryVO;
-import com.takensoft.cms.mber.vo.MberAuthorVO;
 import com.takensoft.cms.mber.vo.MberVO;
-import com.takensoft.cms.token.service.RefreshTokenService;
-import com.takensoft.cms.token.vo.RefreshTknVO;
 import com.takensoft.common.oauth.vo.CustomOAuth2UserVO;
 import com.takensoft.common.util.HttpRequestUtil;
-import com.takensoft.common.util.JWTUtil;
-import com.takensoft.common.util.SessionUtil;
+import com.takensoft.common.util.LoginUtil;
 import jakarta.servlet.ServletException;
-import jakarta.servlet.http.Cookie;
 import jakarta.servlet.http.HttpServletRequest;
 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.context.ApplicationContext;
-import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
 import java.net.URLEncoder;
-import java.util.*;
-import java.util.concurrent.TimeUnit;
 
+/**
+ * @author takensoft
+ * @since 2025.05.22
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.22  |  takensoft   | 최초 등록
+ *  2025.05.28  |  takensoft   | 통합 로그인 적용
+ *  2025.05.29  |  takensoft   | OAuth2 통합 문제 해결
+ *
+ * OAuth2 로그인 성공 핸들러 - 통합 로그인 시스템 적용 (문제 해결)
+ */
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
 
-    private final ApplicationContext applicationContext;
-    private final JWTUtil jwtUtil;
-    private final RefreshTokenService refreshTokenService;
+    private final UnifiedLoginService unifiedLoginService;
     private final LgnHstryService lgnHstryService;
     private final HttpRequestUtil httpRequestUtil;
+    private final LoginUtil loginUtil;
     private final LoginModeService loginModeService;
-    private final LoginPolicyService loginPolicyService;
-    private final SessionUtil sessionUtil;
-    private final RedisTemplate<String, String> redisTemplate;
-
-    @Value("${jwt.accessTime}")
-    private long jwtAccessTime;
-
-    @Value("${jwt.refreshTime}")
-    private long jwtRefreshTime;
-
-    @Value("${cookie.time}")
-    private int cookieTime;
 
     @Value("${front.url}")
     private String frontUrl;
@@ -67,125 +52,36 @@
         CustomOAuth2UserVO oAuth2User = (CustomOAuth2UserVO) authentication.getPrincipal();
 
         try {
+            // 현재 설정된 로그인 모드 확인
+            String currentLoginMode = loginModeService.getLoginMode();
 
-            // MberService를 ApplicationContext에서 가져옴
-            MberService mberService = applicationContext.getBean(MberService.class);
+            // 통합 로그인 서비스를 통한 OAuth2 사용자 처리
+            MberVO mber = unifiedLoginService.processOAuth2User(
+                    oAuth2User.getEmail(),
+                    convertProviderToMbrType(oAuth2User.getProvider()),
+                    oAuth2User.getId(),
+                    oAuth2User.getName(),
+                    request
+            );
 
-            // OAuth2 사용자 정보로 MberVO 생성 또는 조회
-            MberVO mber = processOAuth2User(oAuth2User, mberService, request);
+            saveLoginHistory(request, mber, oAuth2User.getProvider());
 
-            // 로그인 이력 저장
-            saveLoginHistory(request, mber);
+            // LoginUtil을 통한 통합 로그인 처리
+            loginUtil.successLogin(mber, request, response);
 
-            // 로그인 모드 확인
-            String loginMode = loginModeService.getLoginMode();
-
-            // 로그인 모드에 따른 처리
-            if ("S".equals(loginMode)) {
-                handleSessionMode(request, response, mber);
-            } else {
-                handleJwtMode(request, response, mber);
-            }
-
-            // 프론트엔드로 리다이렉트
-            String redirectUrl = frontUrl + "/login.page?oauth_success=true&loginMode=" + loginMode;
-
+            String redirectUrl = String.format("%s/login.page?oauth_success=true&loginMode=%s",
+                    frontUrl, currentLoginMode);
             getRedirectStrategy().sendRedirect(request, response, redirectUrl);
 
         } catch (Exception e) {
-            e.printStackTrace();
             handleOAuth2Error(response, e);
         }
     }
 
     /**
-     * JWT 모드 처리 - OAuth용 access token 쿠키 추가
-     */
-    private void handleJwtMode(HttpServletRequest request, HttpServletResponse response, MberVO mber) throws IOException {
-        try {
-            // JWT 토큰 생성
-            String accessToken = jwtUtil.createJwt("Authorization",
-                    mber.getMbrId(),
-                    mber.getLgnId(),
-                    mber.getMbrNm(),
-                    (List) mber.getAuthorities(),
-                    jwtAccessTime);
-
-            String refreshToken = jwtUtil.createJwt("refresh",
-                    mber.getMbrId(),
-                    mber.getLgnId(),
-                    mber.getMbrNm(),
-                    (List) mber.getAuthorities(),
-                    jwtRefreshTime);
-            // Refresh 토큰 처리
-            RefreshTknVO refresh = new RefreshTknVO();
-            refresh.setMbrId(mber.getMbrId());
-
-            if (refreshTokenService.findByCheckRefresh(request, refresh)) {
-                refreshTokenService.delete(request, refresh);
-            }
-
-            refresh.setToken(refreshToken);
-
-            // 헤더와 쿠키 설정
-            response.setHeader("Authorization", accessToken);
-            // OAuth 전용 access token 쿠키 생성
-            Cookie oauthAccessCookie = new Cookie("oauth_access_token", accessToken);
-            oauthAccessCookie.setPath("/");
-            oauthAccessCookie.setMaxAge(300); // 5분 후 자동 삭제
-            oauthAccessCookie.setHttpOnly(false); // 프론트에서 접근 가능하도록
-
-            response.addCookie(oauthAccessCookie);
-
-            // Refresh 쿠키 생성
-            Cookie refreshCookie = jwtUtil.createCookie("refresh", refreshToken, cookieTime);
-            response.addCookie(refreshCookie);
-
-            response.setHeader("login-type", "J");
-
-            // 중복 로그인 비허용 처리
-            if (!loginPolicyService.getPolicy()) {
-                redisTemplate.delete("jwt:" + mber.getMbrId());
-                redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, jwtAccessTime, TimeUnit.MILLISECONDS);
-            }
-
-            // Refresh 토큰 저장
-            refreshTokenService.saveRefreshToken(request, response, refresh, jwtRefreshTime);
-
-        } catch (Exception e) {
-            e.printStackTrace();
-            throw e;
-        }
-    }
-
-    /**
-     * 세션 모드 처리
-     */
-    private void handleSessionMode(HttpServletRequest request, HttpServletResponse response, MberVO mber) {
-        log.info("세션 모드로 OAuth2 로그인 처리");
-
-        // 세션 생성 및 정보 저장
-        HttpSession session = request.getSession(true);
-
-        // 세션에 사용자 정보 저장 (JWT 없이!)
-        session.setAttribute("mbrId", mber.getMbrId());
-        session.setAttribute("mbrNm", mber.getMbrNm());
-        session.setAttribute("lgnId", mber.getLgnId());
-        session.setAttribute("roles", mber.getAuthorList());
-        session.setAttribute("loginType", "OAUTH2");
-
-        // 중복 로그인 비허용 처리
-        if (!loginPolicyService.getPolicy()) {
-            sessionUtil.registerSession(mber.getMbrId(), session);
-        }
-
-        response.setHeader("login-type", "S");
-    }
-
-    /**
      * 로그인 이력 저장
      */
-    private void saveLoginHistory(HttpServletRequest request, MberVO mber) {
+    private void saveLoginHistory(HttpServletRequest request, MberVO mber, String provider) {
         try {
             String userAgent = httpRequestUtil.getUserAgent(request);
 
@@ -200,35 +96,13 @@
 
             lgnHstryService.LgnHstrySave(loginHistory);
         } catch (Exception e) {
-            log.error("로그인 이력 저장 실패", e);
-            // 로그인 이력 저장 실패해도 로그인은 계속 진행
+            log.error("로그인 이력 저장 실패: {}", e.getMessage());
         }
     }
 
     /**
-     * OAuth2 사용자 처리
+     * 제공자명을 회원타입으로 변환
      */
-    private MberVO processOAuth2User(CustomOAuth2UserVO oAuth2User, MberService mberService, HttpServletRequest request) throws JsonProcessingException {
-        String mbrType = convertProviderToMbrType(oAuth2User.getProvider());
-        MberVO existingUser = mberService.findByEmailAndProvider(oAuth2User.getEmail(), mbrType);
-        if (existingUser != null) {
-            existingUser.setMbrNm(oAuth2User.getName());
-            return mberService.updateOAuthUser(existingUser);
-        } else {
-            MberVO newUser = new MberVO();
-            newUser.setEml(oAuth2User.getEmail());
-            newUser.setLgnId(oAuth2User.getEmail().toLowerCase());
-            newUser.setMbrNm(oAuth2User.getName());
-            newUser.setNcnm(oAuth2User.getName());
-            newUser.setMbrType(mbrType);
-            MberAuthorVO roleUser = new MberAuthorVO();
-            roleUser.setAuthrtCd("ROLE_USER");
-            newUser.setAuthorList(Collections.singletonList(roleUser));
-
-            return mberService.saveOAuthUser(newUser, request);
-        }
-    }
-
     private String convertProviderToMbrType(String provider) {
         return switch (provider.toLowerCase()) {
             case "kakao" -> "K";
@@ -239,6 +113,9 @@
         };
     }
 
+    /**
+     * OAuth2 오류 처리
+     */
     private void handleOAuth2Error(HttpServletResponse response, Exception e) throws IOException {
         String message = URLEncoder.encode("OAuth 로그인에 실패했습니다.", "UTF-8");
         String errorUrl = String.format("%s/login.page?error=oauth2_failed&message=%s", frontUrl, message);
src/main/java/com/takensoft/common/oauth/vo/CustomOAuth2UserVO.java
--- src/main/java/com/takensoft/common/oauth/vo/CustomOAuth2UserVO.java
+++ src/main/java/com/takensoft/common/oauth/vo/CustomOAuth2UserVO.java
@@ -38,7 +38,6 @@
         return oAuth2UserInfoVO.getName();
     }
 
-    // 추가 getter 메서드들
     public String getId() {
         return oAuth2UserInfoVO.getId();
     }
src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
--- src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
+++ src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
@@ -65,19 +65,16 @@
         String userAgent = httpRequestUtil.getUserAgent(request);
 
         try {
-            // 1. Provider 유효성 검증
+            // Provider 유효성 검증
             validateProvider(provider);
 
-            // 2. 보안 검증 (필요시 추가)
-            validateSecurity(request);
-
-            // 3. CORS 헤더 설정 (브라우저 보안 문제 해결)
+            // CORS 헤더 설정 (브라우저 보안 문제 해결)
             response.setHeader("Access-Control-Allow-Origin", FRONT_URL);
             response.setHeader("Access-Control-Allow-Credentials", "true");
             response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
             response.setHeader("Access-Control-Allow-Headers", "*");
 
-            // 4. OAuth2 Authorization Server로 리다이렉트
+            // OAuth2 Authorization Server로 리다이렉트
             String redirectUrl = "/oauth2/authorization/" + provider;
 
             response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
@@ -101,87 +98,137 @@
             // 로그인 모드 확인
             String loginMode = loginModeService.getLoginMode();
 
-            // 현재 로그인한 사용자 ID 조회
-            String currentUserId = null;
-            MberVO mberInfo = null;
-
             if ("S".equals(loginMode)) {
-                // 세션 모드 - 세션에서 직접 조회
-                HttpSession session = request.getSession(false);
-                if (session != null) {
-                    currentUserId = (String) session.getAttribute("mbrId");
-
-                    if (currentUserId != null) {
-                        // 세션에 저장된 정보로 응답 생성
-                        HashMap<String, Object> result = new HashMap<>();
-                        result.put("mbrId", session.getAttribute("mbrId"));
-                        result.put("mbrNm", session.getAttribute("mbrNm"));
-                        result.put("roles", session.getAttribute("roles"));
-
-                        return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
-                    }
-                } else {
-                    return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
-                }
+                // 세션 모드 처리
+                return handleSessionModeUserInfo(request);
             } else {
-                // JWT 모드 - 토큰에서 조회
-                String authHeader = request.getHeader("Authorization");
-                System.out.println("Authorization Header: " + authHeader);
-
-                String token = null;
-
-                if (authHeader != null && !authHeader.isEmpty()) {
-                    // Authorization 헤더에서 토큰 추출
-                    token = jwtUtil.extractToken(authHeader);
-                } else {
-                    // Authorization 헤더가 없으면 OAuth 전용 쿠키에서 확인
-                    token = getTokenFromOAuthCookie(request);
-                }
-
-                if (token == null || token.isEmpty()) {
-                    return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
-                }
-
-                try {
-                    currentUserId = (String) jwtUtil.getClaim(token, "mbrId");
-
-                    // JWT 모드에서는 DB에서 최신 정보 조회
-                    HashMap<String, Object> params = new HashMap<>();
-                    params.put("mbrId", currentUserId);
-                    mberInfo = mberService.findByMbr(params);
-
-                    if (mberInfo == null) {
-                        return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
-                    }
-
-                    // 응답 데이터 구성
-                    HashMap<String, Object> result = new HashMap<>();
-                    result.put("mbrId", mberInfo.getMbrId());
-                    result.put("mbrNm", mberInfo.getMbrNm());
-                    result.put("roles", mberInfo.getAuthorList());
-
-                    // 토큰도 함께 전달
-                    if (authHeader != null && !authHeader.isEmpty()) {
-                        result.put("token", authHeader);
-                    } else {
-                        // 쿠키에서 가져온 토큰이면 Bearer 형태로 반환
-                        String oauthToken = getTokenFromOAuthCookie(request);
-                        if (oauthToken != null) {
-                            result.put("token", "Bearer " + oauthToken);
-                        }
-                    }
-                    return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
-
-                } catch (IllegalArgumentException e) {
-                    return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
-                }
+                // JWT 모드 처리
+                return handleJWTModeUserInfo(request);
             }
-            // 여기까지 왔다면 사용자를 찾을 수 없음
-            return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
 
         } catch (Exception e) {
             return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
         }
+    }
+
+    /**
+     * 세션 모드 사용자 정보 처리
+     */
+    private ResponseEntity<?> handleSessionModeUserInfo(HttpServletRequest request) {
+        try {
+            HttpSession session = request.getSession(false);
+            if (session == null) {
+                return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+            }
+
+            String currentUserId = (String) session.getAttribute("mbrId");
+            if (currentUserId == null) {
+                return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+            }
+
+            // 세션에서 정보 조회 및 최신 권한 정보 가져오기
+            HashMap<String, Object> params = new HashMap<>();
+            params.put("mbrId", currentUserId);
+            MberVO mberInfo = mberService.findByMbr(params);
+
+            if (mberInfo == null) {
+                return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+            }
+
+            // 응답 데이터 구성
+            HashMap<String, Object> result = new HashMap<>();
+            result.put("mbrId", mberInfo.getMbrId());
+            result.put("mbrNm", mberInfo.getMbrNm());
+            result.put("roles", mberInfo.getAuthorList());
+            result.put("loginMode", "S");
+
+            return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
+
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+        }
+    }
+
+    /**
+     * JWT 모드 사용자 정보 처리
+     */
+    private ResponseEntity<?> handleJWTModeUserInfo(HttpServletRequest request) {
+        try {
+            String token = extractToken(request);
+            if (token == null) {
+                return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+            }
+
+            // 토큰에서 사용자 ID 추출
+            String currentUserId;
+            try {
+                currentUserId = (String) jwtUtil.getClaim(token, "mbrId");
+                if (currentUserId == null || currentUserId.isEmpty()) {
+                    return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+                }
+            } catch (Exception e) {
+                return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+            }
+
+            // DB에서 최신 사용자 정보 조회
+            HashMap<String, Object> params = new HashMap<>();
+            params.put("mbrId", currentUserId);
+            MberVO mberInfo = mberService.findByMbr(params);
+
+            if (mberInfo == null) {
+                return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+            }
+
+            // 응답 데이터 구성
+            HashMap<String, Object> result = new HashMap<>();
+            result.put("mbrId", mberInfo.getMbrId());
+            result.put("mbrNm", mberInfo.getMbrNm());
+            result.put("roles", mberInfo.getAuthorList());
+            result.put("loginMode", "J");
+
+            // JWT 토큰도 함께 전달
+            String authHeader = request.getHeader("Authorization");
+            if (authHeader != null && !authHeader.isEmpty()) {
+                result.put("token", authHeader);
+            } else {
+                // 쿠키에서 가져온 토큰이면 Bearer 형태로 반환
+                result.put("token", "Bearer " + token);
+            }
+
+            return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
+
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
+        }
+    }
+
+
+    /**
+     * 토큰 추출 로직 통합 및 개선
+     */
+    private String extractToken(HttpServletRequest request) {
+        // Authorization 헤더에서 토큰 추출 시도
+        String authHeader = request.getHeader("Authorization");
+        if (authHeader != null && !authHeader.isEmpty()) {
+            return jwtUtil.extractToken(authHeader);
+        }
+
+        // OAuth 전용 쿠키에서 토큰 추출 시도
+        String oauthToken = getTokenFromOAuthCookie(request);
+        if (oauthToken != null && !oauthToken.isEmpty()) {
+            return oauthToken;
+        }
+
+        // 일반 Authorization 쿠키에서 토큰 추출 시도
+        if (request.getCookies() != null) {
+            for (Cookie cookie : request.getCookies()) {
+                if ("Authorization".equals(cookie.getName())) {
+                    return jwtUtil.extractToken(cookie.getValue());
+                }
+            }
+        }
+
+        return null;
     }
 
     /**
@@ -190,8 +237,10 @@
     private String getTokenFromOAuthCookie(HttpServletRequest request) {
         if (request.getCookies() != null) {
             for (Cookie cookie : request.getCookies()) {
-                if ("oauth_access_token".equals(cookie.getName())) {
-                    return cookie.getValue();
+                if ("Authorization".equals(cookie.getName()) ||
+                        "refresh".equals(cookie.getName())) {
+                    String token = cookie.getValue();
+                    return token.startsWith("Bearer ") ? token.substring(7) : token;
                 }
             }
         }
@@ -216,17 +265,6 @@
 
         if (!SUPPORTED_PROVIDERS.contains(provider.toLowerCase())) {
             throw new IllegalArgumentException("지원하지 않는 OAuth 제공자입니다: " + provider);
-        }
-    }
-
-    /**
-     * 보안 검증 (필요시 확장)
-     */
-    private void validateSecurity(HttpServletRequest request) {
-        String clientIP = httpRequestUtil.getIp(request);
-        // 예시: 로컬 개발 환경이 아닌 경우 추가 검증
-        if (!"127.0.0.1".equals(clientIP) && !"::1".equals(clientIP)) {
-            // 운영 환경 보안 검증 로직
         }
     }
 
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,19 +12,33 @@
 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;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
+import java.time.Duration;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
+/**
+ * @author takensoft
+ * @since 2025.03.21
+ * @modification
+ *     since    |    author    | description
+ *  2025.03.21  |  takensoft   | 최초 등록
+ *  2025.05.28  |  takensoft   | 통합 로그인 적용, 문제 해결
+ *  2025.05.29  |  takensoft   | Redis 통합 중복로그인 관리
+ *
+ * 통합 로그인 유틸리티 - Redis 통합 중복로그인 관리
+ */
 @Component
 @RequiredArgsConstructor
+@Slf4j
 public class LoginUtil {
     private final LgnHstryService lgnHstryService;
     private final HttpRequestUtil httpRequestUtil;
@@ -36,77 +50,169 @@
     private final RedisTemplate<String, String> redisTemplate;
 
     @Value("${jwt.accessTime}")
-    private long JWT_ACCESSTIME; // access 토큰 유지 시간
+    private long JWT_ACCESSTIME;
     @Value("${jwt.refreshTime}")
-    private long JWT_REFRESHTIME; // refresh 토큰 유지 시간
+    private long JWT_REFRESHTIME;
     @Value("${cookie.time}")
-    private int COOKIE_TIME; // 쿠키 유지 시간
+    private int COOKIE_TIME;
 
+    /**
+     * 통합 로그인 성공 처리 - Redis 기반 중복로그인 관리
+     */
     public void successLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) {
         try {
             // 로그인 이력 등록
-            LgnHstryVO lgnHstryVO = new LgnHstryVO();
-            lgnHstryVO.setLgnId(mber.getLgnId());
-            lgnHstryVO.setLgnType(mber.getAuthorities().stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1");
-            lgnHstryVO.setCntnIp(httpRequestUtil.getIp(req));
-            lgnHstryVO.setCntnOperSys(httpRequestUtil.getOS(httpRequestUtil.getUserAgent(req)));
-            lgnHstryVO.setDeviceNm(httpRequestUtil.getDevice(httpRequestUtil.getUserAgent(req)));
-            lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(httpRequestUtil.getUserAgent(req)));
-            lgnHstryService.LgnHstrySave(lgnHstryVO);
-
-            // 로그인 방식 확인 JWT or SESSION
-            String loginType = loginModeService.getLoginMode();
-
-            // 토큰 생성(access, refresh)
-            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);
-
-            // refreshToken이 현재 IP와 계정으로 등록되어 있는지 확인
-            RefreshTknVO refresh = new RefreshTknVO();
-            refresh.setMbrId(mber.getMbrId());
-
-            // refresh 토큰이 현재 아이피와 아이디로 DB에 등록 되어 있다면
-            if (refreshTokenService.findByCheckRefresh(req, refresh)) {
-                refreshTokenService.delete(req, refresh);
+            String loginType = (String) req.getAttribute("loginType");
+            if (!"OAUTH2".equals(loginType)) {
+                saveLoginHistory(mber, req);
             }
-            // refreshToken DB 저장
-            refresh.setToken(refreshToken);
 
-            if ("S".equals(loginType)) {
-                HttpSession session = req.getSession(true);
-                session.setAttribute("JWT_TOKEN", accessToken);
+            // 로그인 방식 확인
+            String loginMode = loginModeService.getLoginMode();
+            log.info("통합 로그인 모드: {}, 사용자: {}", loginMode, mber.getMbrId());
 
-                // 중복 로그인 비허용일 때 기존 세션 만료
-                if (!loginPolicyService.getPolicy()) {
-                    sessionUtil.registerSession(mber.getMbrId(), session);
-                }
-                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.setStatus(HttpStatus.OK.value());
-                new ObjectMapper().writeValue(res.getOutputStream(), result);
+            if ("S".equals(loginMode)) {
+                // Redis 기반 중복로그인 관리 적용
+                handleSessionLogin(mber, req, res);
             } else {
-                res.setHeader("Authorization", accessToken);
-                res.addCookie(jwtUtil.createCookie("refresh", refreshToken, COOKIE_TIME));
-
-                // 중복 로그인 비허용일 때 Redis 저장
-                if (!loginPolicyService.getPolicy()) {
-                    redisTemplate.delete("jwt:" + mber.getMbrId());
-                    redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS);
-                }
+                // 기존 Redis 기반 관리 유지
+                handleJwtLogin(mber, req, res);
             }
 
-            refreshTokenService.saveRefreshToken(req, res, refresh, JWT_REFRESHTIME);
-            res.setHeader("login-type", loginType);
+            res.setHeader("login-type", loginMode);
+            log.info("통합 로그인 성공 처리 완료: {}, 모드: {}", mber.getMbrId(), loginMode);
         }
         catch (IOException ioe) {
+            log.error("로그인 응답 처리 중 IO 오류", ioe);
             throw new RuntimeException(ioe);
         }
         catch (Exception e) {
+            log.error("로그인 처리 중 오류 발생", e);
             throw e;
         }
     }
-}
+
+    /**
+     * 세션 모드 로그인 처리 - Redis 기반 중복로그인 관리
+     */
+    private void handleSessionLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
+        log.info("세션 모드 로그인 처리 (Redis 통합): {}", mber.getMbrId());
+
+        // JWT 토큰은 생성하되 세션에만 저장
+        String accessToken = jwtUtil.createJwt("Authorization",
+                mber.getMbrId(), 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("mbrNm", mber.getMbrNm());
+        session.setAttribute("lgnId", mber.getLgnId());
+        session.setAttribute("roles", mber.getAuthorList());
+        session.setAttribute("loginType", req.getAttribute("loginType") != null ?
+                req.getAttribute("loginType") : "S");
+
+        //중복 로그인 비허용을 Redis로 통합 관리
+        if (!loginPolicyService.getPolicy()) {
+            handleDuplicateSessionLogin(mber.getMbrId(), session);
+        }
+
+        // 응답 데이터 구성 (OAuth2는 JSON 응답 없이 리다이렉트만)
+        String loginType = (String) req.getAttribute("loginType");
+        if (!"OAUTH2".equals(loginType)) {
+            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.setStatus(HttpStatus.OK.value());
+            new ObjectMapper().writeValue(res.getOutputStream(), result);
+        }
+    }
+
+    /**
+     * Redis 기반 세션 중복로그인 관리
+     */
+    private void handleDuplicateSessionLogin(String mbrId, HttpSession newSession) {
+        try {
+            String sessionKey = "session:" + mbrId;
+
+            // 기존 세션 확인 및 무효화
+            String oldSessionId = redisTemplate.opsForValue().get(sessionKey);
+            if (oldSessionId != null && !oldSessionId.equals(newSession.getId())) {
+                // 기존 세션 무효화
+                sessionUtil.invalidateSessionById(oldSessionId);
+            }
+
+            // 새 세션 정보를 Redis에 저장
+            redisTemplate.opsForValue().set(sessionKey, newSession.getId(),
+                    Duration.ofSeconds(newSession.getMaxInactiveInterval()));
+
+            // 기존 SessionUtil에도 등록 (호환성 유지)
+            sessionUtil.registerSession(mbrId, newSession);
+        } catch (Exception e) {
+            // 실패해도 로그인은 계속 진행
+        }
+    }
+
+    /**
+     * JWT 모드 로그인 처리 - 기존 방식 유지
+     */
+    private void handleJwtLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
+        // 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);
+
+        // Refresh 토큰 처리
+        RefreshTknVO refresh = new RefreshTknVO();
+        refresh.setMbrId(mber.getMbrId());
+
+        // 기존 refresh 토큰 삭제
+        if (refreshTokenService.findByCheckRefresh(req, refresh)) {
+            refreshTokenService.delete(req, refresh);
+        }
+        refresh.setToken(refreshToken);
+
+        // 응답 헤더 및 쿠키 설정
+        res.setHeader("Authorization", accessToken);
+        res.addCookie(jwtUtil.createCookie("refresh", refreshToken, COOKIE_TIME));
+
+        // 중복 로그인 비허용일 때 Redis 저장
+        if (!loginPolicyService.getPolicy()) {
+            redisTemplate.delete("jwt:" + mber.getMbrId());
+            redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS);
+        }
+
+        // Refresh 토큰 저장
+        refreshTokenService.saveRefreshToken(req, res, refresh, JWT_REFRESHTIME);
+
+        // OAuth2가 아닌 경우만 상태 코드 설정
+        String loginType = (String) req.getAttribute("loginType");
+        if (!"OAUTH2".equals(loginType)) {
+            res.setStatus(HttpStatus.OK.value());
+        }
+    }
+
+    /**
+     * 로그인 이력 저장
+     */
+    private void saveLoginHistory(MberVO mber, HttpServletRequest req) {
+        try {
+            String userAgent = httpRequestUtil.getUserAgent(req);
+
+            LgnHstryVO lgnHstryVO = new LgnHstryVO();
+            lgnHstryVO.setLgnId(mber.getLgnId());
+            lgnHstryVO.setLgnType(mber.getAuthorities().stream()
+                    .anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1");
+            lgnHstryVO.setCntnIp(httpRequestUtil.getIp(req));
+            lgnHstryVO.setCntnOperSys(httpRequestUtil.getOS(userAgent));
+            lgnHstryVO.setDeviceNm(httpRequestUtil.getDevice(userAgent));
+            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
@@ -1,63 +1,179 @@
 package com.takensoft.common.util;
 
 import jakarta.servlet.http.HttpSession;
-import org.springframework.dao.DataAccessException;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 
+import java.time.Duration;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * @author  : takensoft
- * @since   : 2025.03.21
+ * @author takensoft
+ * @since 2025.03.21
  * @modification
  *     since    |    author    | description
  *  2025.03.21  |  takensoft   | 최초 등록
+ *  2025.05.29  |  takensoft   | Redis 통합 중복로그인 관리
  *
- * 세션 로그인 방식의 유틸리티
+ * 세션 로그인 방식의 유틸리티 - Redis 통합
  */
 @Component
 public class SessionUtil {
 
-        private final Map<String, HttpSession> sessionMap = new ConcurrentHashMap<>();
+    private final Map<String, HttpSession> sessionMap = new ConcurrentHashMap<>();
+    private final RedisTemplate<String, String> redisTemplate;
+
+    public SessionUtil(RedisTemplate<String, String> redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
 
     /**
-     * @param mbrId - 사용자 Id
-     * @param newSession - HTTP 세션
-     *
-     * 기존 세션 있으면 강제 로그아웃
+     * 세션 등록 - Redis 연동
+     * 기존 세션 있으면 강제 로그아웃 후 새 세션 등록
      */
-        public synchronized void registerSession(String mbrId, HttpSession newSession) {
+    public synchronized void registerSession(String mbrId, HttpSession newSession) {
+        try {
+            // 1. 기존 메모리 세션 처리
             HttpSession oldSession = sessionMap.get(mbrId);
             if (oldSession != null && oldSession != newSession) {
-                oldSession.invalidate();
-            }
-            sessionMap.put(mbrId, newSession);
-        }
-    /**
-     * @param mbrId - 사용자 Id
-     *
-     * 로그아웃
-     */
-        public void removeSession(String mbrId) {
-            HttpSession session = sessionMap.get(mbrId);
-            if (session != null) {
-                session.invalidate();  // 세션 무효화
-            }
-            sessionMap.remove(mbrId); // 이후 맵에서 제거
-        }
-
-    /**
-     *
-     * 전체 로그아웃
-     */
-        public void invalidateAllSessions() {
-            for (HttpSession session : sessionMap.values()) {
-                if (session != null) {
-                    session.invalidate();
+                try {
+                    oldSession.invalidate();
+                } catch (IllegalStateException e) {
                 }
             }
-            sessionMap.clear(); // 전체 초기화
+
+            // 2. 새 세션을 메모리에 등록
+            sessionMap.put(mbrId, newSession);
+
+            // 3. Redis에 세션 정보 저장 (중복로그인 관리용)
+            String sessionKey = "session:" + mbrId;
+            redisTemplate.opsForValue().set(sessionKey, newSession.getId(),
+                    Duration.ofSeconds(newSession.getMaxInactiveInterval()));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 세션 ID로 세션 무효화
+     */
+    public void invalidateSessionById(String sessionId) {
+        try {
+            boolean found = false;
+            // 메모리에서 해당 세션 ID를 가진 세션 찾아서 무효화
+            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 연동
+     */
+    public void removeSession(String mbrId) {
+        try {
+            // 1. 메모리 세션 무효화
+            HttpSession session = sessionMap.get(mbrId);
+            if (session != null) {
+                try {
+                    session.invalidate();
+                } catch (IllegalStateException e) {
+                    e.printStackTrace();
+                }
+            }
+            sessionMap.remove(mbrId);
+
+            // 2. Redis에서도 제거
+            String sessionKey = "session:" + mbrId;
+            redisTemplate.delete(sessionKey);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 전체 세션 무효화 - Redis 연동
+     */
+    public void invalidateAllSessions() {
+        try {
+            // 1. 모든 메모리 세션 무효화
+            for (Map.Entry<String, HttpSession> entry : sessionMap.entrySet()) {
+                HttpSession session = entry.getValue();
+                if (session != null) {
+                    try {
+                        session.invalidate();
+                    } catch (IllegalStateException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+            sessionMap.clear();
+
+            // 2. Redis에서 모든 세션 키 삭제
+            try {
+                var sessionKeys = redisTemplate.keys("session:*");
+                if (sessionKeys != null && !sessionKeys.isEmpty()) {
+                    redisTemplate.delete(sessionKeys);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 현재 활성 세션 수 조회
+     */
+    public int getActiveSessionCount() {
+        return sessionMap.size();
+    }
+
+    /**
+     * 특정 사용자의 세션 존재 여부 확인
+     */
+    public boolean hasActiveSession(String mbrId) {
+        HttpSession session = sessionMap.get(mbrId);
+        if (session == null) {
+            return false;
         }
 
-}
+        try {
+            // 세션이 유효한지 확인
+            session.getAttribute("mbrId");
+            return true;
+        } catch (IllegalStateException e) {
+            // 세션이 무효화됨
+            sessionMap.remove(mbrId);
+            return false;
+        }
+    }
+
+    /**
+     * Redis에서 세션 정보 확인
+     */
+    public boolean isValidSessionInRedis(String mbrId, String sessionId) {
+        try {
+            String sessionKey = "session:" + mbrId;
+            String storedSessionId = redisTemplate.opsForValue().get(sessionKey);
+            return storedSessionId != null && storedSessionId.equals(sessionId);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
(파일 끝에 줄바꿈 문자 없음)
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
@@ -46,6 +46,25 @@
         <result property="regDt" column="reg_dt" />
     </resultMap>
 
+    <!-- 소셜 계정 정보 resultMap -->
+    <resultMap id="socialAccountMap" type="MberSocialAccountVO">
+        <result property="id" column="id" />
+        <result property="mbrId" column="mbr_id" />
+        <result property="providerType" column="provider_type" />
+        <result property="socialId" column="social_id" />
+        <result property="loginId" column="login_id" />
+        <result property="socialEmail" column="social_email" />
+        <result property="socialName" column="social_name" />
+        <result property="isPrimaryProfile" column="is_primary_profile" />
+        <result property="isActive" column="is_active" />
+        <result property="linkedDt" column="linked_dt" />
+        <result property="unlinkedDt" column="unlinked_dt" />
+        <result property="rgtr" column="rgtr" />
+        <result property="regDt" column="reg_dt" />
+        <result property="mdfr" column="mdfr" />
+        <result property="mdfcnDt" column="mdfcn_dt" />
+    </resultMap>
+
     <sql id="selectMber">
         SELECT mi.mbr_id
              , mi.lgn_id
@@ -78,25 +97,59 @@
 
     <!--
         작성자 : takensoft
-        작성일 : 2024.04.03
-        내 용 : 회원정보 조회 [security 용]
+        내 용 : 회원정보 조회 [security 용] - 통합 로그인 대응
     -->
     <select id="findByMberSecurity" parameterType="String" resultMap="mberMap">
         <include refid="selectMber" />
-        WHERE mi.lgn_id = #{lgnId}
+        WHERE EXISTS (
+        SELECT 1 FROM mbr_social_accounts msa
+        WHERE msa.mbr_id = mi.mbr_id
+        AND (
+        (msa.provider_type = 'S' AND msa.login_id = #{lgnId})
+        OR (msa.provider_type != 'S' AND msa.social_id = #{lgnId})
+        )
+        AND msa.is_active = true
+        )
         AND mi.use_yn = 'Y'
+        LIMIT 1
+    </select>
+
+    <!--
+        통합 로그인: 제공자별 사용자 조회
+    -->
+    <select id="findByUnifiedLogin" parameterType="map" resultMap="mberMap">
+        <include refid="selectMber" />
+        WHERE EXISTS (
+        SELECT 1 FROM mbr_social_accounts msa
+        WHERE msa.mbr_id = mi.mbr_id
+        <if test="providerType == 'S'">
+            AND msa.provider_type = 'S'
+            AND msa.login_id = #{identifier}
+        </if>
+        <if test="providerType != 'S'">
+            AND msa.provider_type = #{providerType}
+            AND msa.social_id = #{identifier}
+        </if>
+        AND msa.is_active = true
+        )
+        AND mi.use_yn = 'Y'
+        AND mi.mbr_stts = '1'
+        LIMIT 1
     </select>
 
     <!--
         작성자 : takensoft
         작성일 : 2024.04.03
-        내 용 : 로그인 아이디 중복 확인
+        내 용 : 로그인 아이디 중복 확인 - 통합 로그인 대응
     -->
     <select id="findByCheckLoginId" parameterType="String" resultType="boolean">
-        SELECT COUNT(lgn_id)
-          FROM mbr_info
-         WHERE lgn_id = #{lgnId}
-           AND use_yn = 'Y'
+        SELECT COUNT(*) > 0
+        FROM mbr_social_accounts msa
+        JOIN mbr_info mi ON msa.mbr_id = mi.mbr_id
+        WHERE msa.provider_type = 'S'
+        AND msa.login_id = #{lgnId}
+        AND msa.is_active = true
+        AND mi.use_yn = 'Y'
     </select>
 
     <!--
@@ -215,7 +268,6 @@
         WHERE mai.mbr_id = #{mbrId}
     </select>
 
-
     <!-- 이메일로만 사용자 조회 -->
     <select id="findByEmail" parameterType="String" resultType="MberVO">
         SELECT
@@ -270,7 +322,7 @@
         WHERE mai.mbr_id = #{mbrId}
     </select>
 
-        <!-- OAuth2 사용자 저장 -->
+    <!-- OAuth2 사용자 저장 -->
     <insert id="saveOAuthUser" parameterType="MberVO">
         INSERT INTO mbr_info (
             mbr_id,
@@ -305,8 +357,8 @@
         )
     </insert>
 
-        <!-- OAuth2 사용자 정보 업데이트 -->
-        <update id="updateOAuthUser" parameterType="MberVO">
+    <!-- OAuth2 사용자 정보 업데이트 -->
+    <update id="updateOAuthUser" parameterType="MberVO">
         UPDATE mbr_info
         SET
             mbr_nm = #{mbrNm},
@@ -316,8 +368,8 @@
         WHERE mbr_id = #{mbrId}
     </update>
 
-        <!-- 기존 계정에 OAuth2 정보 연동 -->
-        <update id="linkOAuth2Account" parameterType="MberVO">
+    <!-- 기존 계정에 OAuth2 정보 연동 -->
+    <update id="linkOAuth2Account" parameterType="MberVO">
         UPDATE mbr_info
         SET
             mbr_type = #{mbrType},
@@ -325,4 +377,139 @@
             mdfcn_dt = NOW()
         WHERE mbr_id = #{mbrId}
     </update>
+
+    <!-- 소셜 계정 정보 저장 -->
+    <insert id="saveSocialAccount" parameterType="MberSocialAccountVO">
+        INSERT INTO mbr_social_accounts (
+            mbr_id,
+            provider_type,
+            social_id,
+            login_id,
+            social_email,
+            social_name,
+            is_primary_profile,
+            is_active,
+            rgtr
+        ) VALUES (
+            #{mbrId},
+            #{providerType},
+            #{socialId},
+            #{loginId},
+            #{socialEmail},
+            #{socialName},
+            #{isPrimaryProfile},
+            #{isActive},
+            #{rgtr}
+        )
+    </insert>
+
+    <!-- 이메일로 모든 연동 계정 조회 -->
+    <select id="findAllAccountsByEmail" parameterType="String" resultMap="mberMap">
+        <include refid="selectMber" />
+        WHERE mi.eml = #{email}
+        AND mi.use_yn = 'Y'
+        AND mi.mbr_stts = '1'
+        LIMIT 1
+    </select>
+
+    <!-- 회원 ID로 소셜 계정 목록 조회 -->
+    <select id="findSocialAccountsByMbrId" parameterType="String" resultMap="socialAccountMap">
+        SELECT id, mbr_id, provider_type, social_id, login_id, social_email, social_name,
+               is_primary_profile, is_active, linked_dt, unlinked_dt, rgtr, reg_dt, mdfr, mdfcn_dt
+        FROM mbr_social_accounts
+        WHERE mbr_id = #{mbrId}
+        AND is_active = true
+        ORDER BY is_primary_profile DESC, linked_dt ASC
+    </select>
+
+    <!-- 특정 제공자로 소셜 계정 조회 -->
+    <select id="findSocialAccountByProvider" parameterType="map" resultMap="socialAccountMap">
+        SELECT id, mbr_id, provider_type, social_id, login_id, social_email, social_name,
+               is_primary_profile, is_active, linked_dt, unlinked_dt, rgtr, reg_dt, mdfr, mdfcn_dt
+        FROM mbr_social_accounts
+        WHERE mbr_id = #{mbrId}
+        AND provider_type = #{providerType}
+        AND is_active = true
+    </select>
+
+    <!-- 소셜 계정 연동 -->
+    <insert id="linkSocialAccount" parameterType="MberSocialAccountVO">
+        INSERT INTO mbr_social_accounts (
+            mbr_id,
+            provider_type,
+            social_id,
+            login_id,
+            social_email,
+            social_name,
+            is_primary_profile,
+            is_active,
+            rgtr
+        ) VALUES (
+            #{mbrId},
+            #{providerType},
+            #{socialId},
+            #{loginId},
+            #{socialEmail},
+            #{socialName},
+            #{isPrimaryProfile},
+            #{isActive},
+            #{rgtr}
+        )
+        ON CONFLICT (mbr_id, provider_type)
+        DO UPDATE SET
+            social_id = #{socialId},
+            login_id = #{loginId},
+            social_email = #{socialEmail},
+            social_name = #{socialName},
+            is_active = true,
+            unlinked_dt = NULL,
+            mdfr = #{rgtr},
+            mdfcn_dt = NOW()
+    </insert>
+
+    <!-- 소셜 계정 연동 해지 -->
+    <update id="unlinkSocialAccount" parameterType="map">
+        UPDATE mbr_social_accounts
+        SET is_active = false,
+            unlinked_dt = NOW(),
+            mdfr = #{mdfr},
+            mdfcn_dt = NOW()
+        WHERE mbr_id = #{mbrId}
+        AND provider_type = #{providerType}
+    </update>
+
+    <!-- 메인 프로필 설정 -->
+    <update id="setPrimaryProfile" parameterType="map">
+        <!-- 기존 메인 프로필 해제 -->
+        UPDATE mbr_social_accounts
+        SET is_primary_profile = false,
+        mdfr = #{mdfr},
+        mdfcn_dt = NOW()
+        WHERE mbr_id = #{mbrId}
+        AND is_primary_profile = true;
+
+        <!-- 새로운 메인 프로필 설정 -->
+        UPDATE mbr_social_accounts
+        SET is_primary_profile = true,
+        mdfr = #{mdfr},
+        mdfcn_dt = NOW()
+        WHERE mbr_id = #{mbrId}
+        AND provider_type = #{providerType}
+        AND is_active = true;
+    </update>
+
+    <!-- 연동 가능한 계정 조회 (이메일로 검색, 다른 제공자 제외) -->
+    <select id="findLinkableAccount" parameterType="map" resultMap="mberMap">
+        <include refid="selectMber" />
+        WHERE mi.eml = #{email}
+        AND mi.use_yn = 'Y'
+        AND mi.mbr_stts = '1'
+        AND NOT EXISTS (
+        SELECT 1 FROM mbr_social_accounts msa
+        WHERE msa.mbr_id = mi.mbr_id
+        AND msa.provider_type = #{providerType}
+        AND msa.is_active = true
+        )
+        LIMIT 1
+    </select>
 </mapper>
(파일 끝에 줄바꿈 문자 없음)
Add a comment
List