hmkim 03-25
250325 김혜민 중복로그인 yml -> db 변경
@ef0eafb7154688fe247fae31815b7addc8f80888
 
build/resources/main/mybatis/mapper/dept/dept-SQL.xml (deleted)
--- build/resources/main/mybatis/mapper/dept/dept-SQL.xml
@@ -1,343 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<!--
-    작성자 : takensoft
-    작성일 : 2024.04.24
-    내 용 : 부서 관련
--->
-<mapper namespace="com.takensoft.cms.dept.dao.DeptDAO">
-    <!-- 부서 정보 resultMap(권한포함) -->
-    <resultMap id="deptMap" type="DeptVO">
-        <result property="deptId" column="dept_id" />
-        <result property="upDeptId" column="up_dept_id" />
-        <result property="deptNm" column="dept_nm" />
-        <result property="upDeptNm" column="up_dept_nm" />
-        <result property="deptExpln" column="dept_expln" />
-        <result property="deptGrd" column="dept_grd" />
-        <result property="deptSn" column="dept_sn" />
-        <result property="useYn" column="use_yn" />
-        <result property="rgtr" column="rgtr" />
-        <result property="regDt" column="reg_dt" />
-        <result property="mdfr" column="mdfr" />
-        <result property="mdfcnDt" column="mdfcn_dt" />
-        <collection property="authrtList" column="{deptId = dept_id}" javaType="java.util.ArrayList" ofType="DeptAuthrtVO" select="findByDeptAuthrt" />
-    </resultMap>
-
-    <!-- 부서, 권한 매핑 -->
-    <resultMap id="authMap" type="DeptAuthrtVO">
-        <result property="deptId" column="dept_id" />
-        <result property="authrtCd" column="authrt_cd" />
-        <result property="rgtr" column="rgtr" />
-        <result property="regDt" column="reg_dt" />
-    </resultMap>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.25
-        내 용 : 부서 등록
-    -->
-    <insert id="save" parameterType="DeptVO">
-        INSERT INTO dept_info (
-            dept_id
-            , up_dept_id
-            , dept_nm
-            , dept_expln
-            , dept_grd
-            , dept_sn
-            , use_yn
-            , rgtr
-            , reg_dt
-        ) VALUES (
-            #{deptId}
-            , #{upDeptId}
-            , #{deptNm}
-            , #{deptExpln}
-            , #{deptGrd}
-            , #{deptSn}
-            , 'Y'
-            , #{rgtr}
-            , NOW()
-        )
-    </insert>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.25
-        내 용 : 부서 권한 등록
-    -->
-    <insert id="authrtSave" parameterType="DeptAuthrtVO">
-        INSERT INTO dept_authrt_info (
-            authrt_cd
-            , dept_id
-            , rgtr
-            , reg_dt
-        ) VALUES (
-            #{authrtCd}
-            , #{deptId}
-            , #{rgtr}
-            , NOW()
-        )
-    </insert>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.29
-        내 용 : 부서 사용자 등록
-    -->
-    <insert id="deptMbrSave" parameterType="DeptMbrVO">
-        INSERT INTO dept_mbr_info (
-            dept_id
-            , mbr_id
-            , rgtr
-            , reg_dt
-        ) VALUES (
-            #{deptId}
-            , #{mbrId}
-            , #{rgtr}
-            , NOW()
-        )
-    </insert>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.25
-        내 용 : 부서 깊이(레벨) 조회
-    -->
-    <select id="findByDeptGrd" parameterType="String" resultType="Integer">
-        SELECT dept_grd
-          FROM dept_info
-         WHERE dept_id = #{upDeptId}
-    </select>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.25
-        내 용 : 부서 순서 조회
-    -->
-    <select id="findByDeptSn" parameterType="String" resultType="Integer">
-        SELECT COALESCE(MAX(dept_sn), 0)
-          FROM dept_info
-         WHERE use_yn = 'Y'
-         <choose>
-             <when test="upDeptId != null and upDeptId != ''">
-           AND up_dept_id = #{upDeptId}
-             </when>
-             <otherwise>
-           AND up_dept_id IS NULL
-             </otherwise>
-         </choose>
-    </select>
-
-    <!--  부서 관리(관리자 페이지 Tree 호출용)   -->
-    <sql id="selecteNode">
-        SELECT dept_id AS id
-             , up_dept_id AS up_id
-             , dept_nm AS nm
-          FROM dept_info
-    </sql>
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.25
-        내 용 : 상위 부서 조회
-    -->
-    <select id="findByTopNode" resultType="HierachyVO">
-        <include refid="selecteNode" />
-        WHERE up_dept_id IS NULL
-          AND use_yn = 'Y'
-     ORDER BY dept_sn
-    </select>
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.25
-        내 용 : 하위 부서 조회
-    -->
-    <select id="findChildNode" parameterType="String" resultType="HierachyVO">
-        <include refid="selecteNode" />
-        WHERE up_dept_id = #{id}
-          AND use_yn = 'Y'
-     ORDER BY dept_sn
-    </select>
-    <!--  부서 관리(관리자 페이지 Tree 호출용)   -->
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 부서정보 조회
-    -->
-    <select id="findByDept" parameterType="String" resultMap="deptMap">
-        SELECT di.dept_id
-             , di.up_dept_id
-             , di.dept_nm
-             , di2.dept_nm AS up_dept_nm
-             , di.dept_expln
-             , di.dept_grd
-             , di.dept_sn
-             , di.use_yn
-             , di.rgtr
-             , TO_CHAR(di.reg_dt, 'YYYY-MM-DD HH24:MI') AS reg_dt
-             , di.mdfr
-             , TO_CHAR(di.mdfcn_dt, 'YYYY-MM-DD HH24:MI') AS mdfcn_dt
-          FROM dept_info di
-     LEFT JOIN dept_info di2
-            ON di.up_dept_id = di2.dept_id
-         WHERE di.dept_id = #{deptId}
-    </select>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 부서 권한 정보 조회
-    -->
-    <select id="findByDeptAuthrt" parameterType="DeptVO" resultMap="authMap">
-        SELECT dai.authrt_cd
-             , dai.dept_id
-             , dai.rgtr
-             , TO_CHAR(dai.reg_dt, 'YYYY-MM-DD HH24:MI') AS reg_dt
-          FROM dept_authrt_info dai
-         WHERE dai.dept_id = #{deptId}
-    </select>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 부서정보 수정
-    -->
-    <update id="update" parameterType="DeptVO">
-        UPDATE dept_info
-           SET up_dept_id = #{upDeptId}
-             , dept_nm = #{deptNm}
-             , dept_expln = #{deptExpln}
-             , dept_grd = #{deptGrd}
-             , dept_sn = #{deptSn}
-             , use_yn = #{useYn}
-             , mdfr = #{mdfr}
-             , mdfcn_dt = NOW()
-         WHERE dept_id = #{deptId}
-    </update>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 부서권한 삭제
-    -->
-    <delete id="deleteAuth" parameterType="String">
-        DELETE
-          FROM dept_authrt_info
-         WHERE dept_id = #{deptId}
-    </delete>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 부서 사용자 등록을 위한 사용자 정보 목록 조회 (부서에 등록되지 않은 사용자 목록)
-    -->
-    <select id="findByMber" parameterType="Map" resultType="DeptMbrVO">
-        SELECT mi.mbr_id
-             , mi.lgn_id
-             , mi.mbr_nm
-             , mi.ncnm
-             , mi.eml
-             , mi.mbl_telno
-          FROM mbr_info mi
-         WHERE NOT EXISTS (
-            SELECT 1
-              FROM dept_mbr_info dmi
-             WHERE dmi.mbr_id = mi.mbr_id
-         )
-           AND mi.use_yn = 'Y'
-           AND mi.mbr_stts = '1'
-         <if test="searchText != null and searchText != ''">
-           AND mi.mbr_nm LIKE '%' || #{searchText}  || '%'
-         </if>
-    </select>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 부서에 등록된 사용자 목록 조회
-    -->
-    <select id="findByDeptMber" parameterType="String" resultType="DeptMbrVO">
-        SELECT dmi.dept_id
-             , mi.mbr_id
-             , mi.lgn_id
-             , mi.mbr_nm
-             , mi.ncnm
-             , mi.eml
-             , mi.mbl_telno
-             , dmi.rgtr
-             , TO_CHAR(dmi.reg_dt, 'YYYY-MM-DD HH24:MI') AS reg_dt
-          FROM dept_mbr_info dmi
-     LEFT JOIN mbr_info mi
-            ON dmi.mbr_id = mi.mbr_id
-         WHERE dmi.dept_id = #{deptId}
-    </select>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 상위,하위 부서 삭제
-    -->
-    <update id="deleteDept" parameterType="String">
-        WITH RECURSIVE DeptHierarchy AS (
-            SELECT dept_id
-                 , up_dept_id
-              FROM dept_info
-             WHERE dept_id = #{deptId}
-         UNION ALL
-            SELECT di.dept_id
-                 , di.up_dept_id
-              FROM dept_info di
-              JOIN DeptHierarchy dh
-                ON di.up_dept_id = dh.dept_id
-        )
-        UPDATE dept_info SET use_yn = 'N' WHERE dept_id IN (SELECT dept_id FROM DeptHierarchy);
-    </update>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 상위, 하위 부서 삭제에 따른 부서의 사용자 삭제
-    -->
-    <delete id="deleteDeptInDeptMbr" parameterType="String">
-         WITH RECURSIVE DeptHierarchy AS (
-            SELECT dept_id
-                 , up_dept_id
-              FROM dept_info
-             WHERE dept_id = #{deptId}
-         UNION ALL
-            SELECT di.dept_id
-                 , di.up_dept_id
-              FROM dept_info di
-              JOIN DeptHierarchy dh
-                ON di.up_dept_id = dh.dept_id
-        )
-        DELETE FROM dept_mbr_info WHERE dept_id IN (SELECT dept_id FROM DeptHierarchy);
-    </delete>
-
-    <!--
-        작성자 : takensoft
-        작성일 : 2024.04.26
-        내 용 : 부서에 등록된 사용자 제거
-    -->
-    <delete id="deleteDeptMbr" parameterType="Map">
-        DELETE
-          FROM dept_mbr_info
-         WHERE 1 = 1
-           AND mbr_id = #{mbrId}
-    </delete>
-
-    <!--
-        작성자 : 박정하
-        작성일 : 2024.05.10
-        내 용 : 부서 정보 수정 (hierachyVO 사용)
-    -->
-    <update id="deptUpdateByHierachy" parameterType="DeptVO">
-        UPDATE dept_info
-        SET up_dept_id = #{upDeptId}
-          , dept_grd = #{deptGrd}
-          , dept_sn = #{deptSn}
-          , mdfr = #{mdfr}
-          , mdfcn_dt = NOW()
-        WHERE dept_id = #{deptId}
-    </update>
-</mapper>(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/cms/loginPolicy/dao/LoginPolicyDAO.java (added)
+++ src/main/java/com/takensoft/cms/loginPolicy/dao/LoginPolicyDAO.java
@@ -0,0 +1,36 @@
+package com.takensoft.cms.loginPolicy.dao;
+
+import com.takensoft.cms.accesCtrl.vo.AccesCtrlVO;
+import com.takensoft.cms.loginPolicy.vo.LoginPolicyVO;
+import com.takensoft.common.Pagination;
+import org.egovframe.rte.psl.dataaccess.mapper.Mapper;
+
+import java.util.List;
+
+/**
+ * @author 김혜민
+ * @since 2025.03.22
+ * @modification
+ *     since    |    author    | description
+ *  2025.03.22  |    김혜민     | 최초 등록
+ *
+ * 중복로그인 허용 관련 DAO
+ */
+@Mapper("loginPolicyDAO")
+public interface LoginPolicyDAO {
+
+    /**
+     * @return Boolean - 중복로그인 여부
+     *
+     * 중복로그인 조회
+     */
+    Boolean selectLatestPolicy();
+
+    /**
+     * @param loginPolicyVO - 중복로그인 정보
+     * @return int - 중복로그인 저장 결과
+     *
+     * 중복로그인 저장
+     */
+    int insertPolicy(LoginPolicyVO loginPolicyVO);
+}
 
src/main/java/com/takensoft/cms/loginPolicy/service/LoginPolicyService.java (added)
+++ src/main/java/com/takensoft/cms/loginPolicy/service/LoginPolicyService.java
@@ -0,0 +1,40 @@
+package com.takensoft.cms.loginPolicy.service;
+
+import com.takensoft.cms.bbs.vo.BbsCnVO;
+import com.takensoft.cms.loginPolicy.vo.LoginPolicyVO;
+import com.takensoft.common.Pagination;
+import org.springframework.dao.DataAccessException;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author 김혜민
+ * @since 2024.05.21
+ * @modification
+ *     since    |    author    | description
+ *  2024.05.21  |    김혜민     | 최초 등록
+ *
+ * 중복로그인 관련 인터페이스
+ */
+public interface LoginPolicyService {
+
+    /**
+     *
+     * @return LoginPolicyVO - 중복로그인 여부 조회
+     *
+     * 중복로그인 조회
+     */
+    public boolean getPolicy();
+    /**
+     * @param loginPolicyVO - 접근 제어 정보
+     * @return ResponseEntity - 접근 제어 수정 결과를 포함하는 응답
+     *
+     * 중복로그인 수정
+     */
+    public int insertPolicy(LoginPolicyVO loginPolicyVO);
+
+
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/cms/loginPolicy/service/impl/LoginPolicyServiceImpl.java (added)
+++ src/main/java/com/takensoft/cms/loginPolicy/service/impl/LoginPolicyServiceImpl.java
@@ -0,0 +1,72 @@
+package com.takensoft.cms.loginPolicy.service.impl;
+
+import com.takensoft.cms.bbs.dao.BbsCnDAO;
+import com.takensoft.cms.bbs.dao.BbsMngDAO;
+import com.takensoft.cms.bbs.dao.WordMngDAO;
+import com.takensoft.cms.bbs.service.BbsCnService;
+import com.takensoft.cms.bbs.vo.BbsCnVO;
+import com.takensoft.cms.bbs.vo.BbsMngVO;
+import com.takensoft.cms.loginPolicy.dao.LoginPolicyDAO;
+import com.takensoft.cms.loginPolicy.service.LoginPolicyService;
+import com.takensoft.cms.loginPolicy.vo.LoginPolicyVO;
+import com.takensoft.common.Pagination;
+import com.takensoft.common.exception.*;
+import com.takensoft.common.file.dao.FileDAO;
+import com.takensoft.common.file.service.FileMngService;
+import com.takensoft.common.file.vo.FileMngVO;
+import com.takensoft.common.idgen.service.IdgenService;
+import com.takensoft.common.util.JWTUtil;
+import lombok.RequiredArgsConstructor;
+import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.DataAccessException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author 김혜민
+ * @since 2024.05.21
+ * @modification
+ *     since    |    author    | description
+ *  2024.05.21  |    김혜민     | 최초 등록
+ *
+ * EgovAbstractServiceImpl : 전자정부 상속
+ * LoginPolicyService : 중복 로그인 관련 인터페이스 상속
+ *
+ * 중복 로그인 관련 인터페이스 구현체
+ */
+@Service("loginPolicyService")
+@RequiredArgsConstructor
+public class LoginPolicyServiceImpl extends EgovAbstractServiceImpl implements LoginPolicyService {
+
+    private final LoginPolicyDAO loginPolicyDAO;
+    private final IdgenService loginPolicyIdgn;
+
+    /**
+     * @return Boolean - 중복로그인 여부
+     *
+     * 중복로그인 조회
+     */
+    @Override
+    public boolean getPolicy() {
+        return loginPolicyDAO.selectLatestPolicy();
+    }
+
+    /**
+     * @param loginPolicyVO - 중복로그인 정보
+     * @return int - 중복로그인 저장 결과
+     *
+     * 중복로그인 저장
+     */
+    @Override
+    public int insertPolicy(LoginPolicyVO loginPolicyVO) {
+        loginPolicyVO.setId(loginPolicyIdgn.getNextStringId()); // ID 자동 생성
+        return loginPolicyDAO.insertPolicy(loginPolicyVO);
+    }
+
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/cms/loginPolicy/vo/LoginPolicyVO.java (added)
+++ src/main/java/com/takensoft/cms/loginPolicy/vo/LoginPolicyVO.java
@@ -0,0 +1,26 @@
+package com.takensoft.cms.loginPolicy.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author  : 김혜민
+ * @since   : 2025.03.22
+ * @modification
+ *     since    |    author    | description
+ *  2025.03.22  |    김혜민     | 최초 등록
+ *
+ * 중복로그인 허용 관련 VO
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoginPolicyVO {
+
+    private String id;                  // 중복로그인 ID
+    private boolean allowMultipleLogin;  // 중복 로그인 허용 여부
+    private String mbrId;            // 수정한 관리자 ID
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/cms/loginPolicy/web/LoginPolicyController.java (added)
+++ src/main/java/com/takensoft/cms/loginPolicy/web/LoginPolicyController.java
@@ -0,0 +1,85 @@
+package com.takensoft.cms.loginPolicy.web;
+
+import com.takensoft.cms.loginPolicy.service.LoginPolicyService;
+import com.takensoft.cms.loginPolicy.vo.LoginPolicyVO;
+import com.takensoft.common.message.MessageCode;
+import com.takensoft.common.util.JWTUtil;
+import com.takensoft.common.util.ResponseUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * @author 김혜민
+ * @since 2025.03.22
+ * @modification
+ *     since    |    author    | description
+ *  2025.03.22  |    김혜민     | 최초 등록
+ *
+ * 중복로그인 허용 관련 컨트롤러
+ */
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping(value = "/admin/allowMultipleLogin")
+public class LoginPolicyController {
+
+    private final LoginPolicyService loginPolicyService;
+    private final ResponseUtil resUtil;
+    private final JWTUtil jwtUtil;
+
+    /**
+     *
+     * @return ResponseEntity - 접근 제어 목록 조회 결과를 포함하는 응답
+     *
+     * 중복로그인 조회
+     */
+    @GetMapping("/login-policy")
+    public ResponseEntity<?> getPolicy() {
+        Boolean isAllowed = loginPolicyService.getPolicy();
+        return resUtil.successRes(isAllowed, MessageCode.COMMON_SUCCESS);
+    }
+
+    /**
+     * @param params - 접근 제어 정보
+     * @return ResponseEntity - 접근 제어 수정 결과를 포함하는 응답
+     *
+     * 중복로그인 수정
+     */
+    @PostMapping("/login-policy")
+    public ResponseEntity<?> updatePolicy(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        try {
+            boolean allow = (Boolean) params.get("allowMultipleLogin");
+
+            String token = request.getHeader("Authorization");
+            String adminId = (String) jwtUtil.getClaim(token, "mbrId");
+
+            if (adminId == null || adminId.isBlank()) {
+                return resUtil.errorRes(MessageCode.COMMON_BAD_REQUEST);
+            }
+
+            LoginPolicyVO loginPolicyVO = new LoginPolicyVO();
+            loginPolicyVO.setAllowMultipleLogin(allow);
+            loginPolicyVO.setMbrId(adminId);
+
+            int result = loginPolicyService.insertPolicy(loginPolicyVO);
+
+            if (result > 0) {
+                return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
+            } else {
+                return resUtil.errorRes(MessageCode.COMMON_INSERT_FAIL); // 저장 실패
+            }
+
+        } catch (DuplicateKeyException e) {
+            return resUtil.errorRes(MessageCode.COMMON_DUPLICATION_DATA); // 중복 저장
+        } catch (Exception e) {
+            return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); // 기타 예외
+        }
+    }
+
+}
src/main/java/com/takensoft/common/config/RedisConfig.java
--- src/main/java/com/takensoft/common/config/RedisConfig.java
+++ src/main/java/com/takensoft/common/config/RedisConfig.java
@@ -1,5 +1,6 @@
 package com.takensoft.common.config;
 
+import com.takensoft.cms.loginPolicy.dao.LoginPolicyDAO;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
@@ -27,8 +28,11 @@
     @Value("${redis.port}")
     private int redisPort;
 
-    @Value("${config.allow-multiple-logins}") // 기본값 false (중복 로그인 비허용)
-    private boolean allowMultipleLogin;
+    private final LoginPolicyDAO loginPolicyDAO;
+
+    public RedisConfig(LoginPolicyDAO loginPolicyDAO) {
+        this.loginPolicyDAO = loginPolicyDAO;
+    }
 
     @Bean
     @ConditionalOnProperty(name = "config.allow-multiple-logins", havingValue = "false", matchIfMissing = true) //redis 사용 안 할 경우 빈 등록x
@@ -51,15 +55,8 @@
      * 중복 로그인 허용 여부를 반환하는 메서드
      */
     public boolean isAllowMultipleLogin() {
-        return allowMultipleLogin;
-    }
-    /**
-     * @return allowMultipleLogin - 중복로그인 허용/비허용 반환
-     *
-     * 관리자가 설정을 변경할 수 있도록 Setter 추가
-     */
-    public void setAllowMultipleLogin(boolean allowMultipleLogin) {
-        this.allowMultipleLogin = allowMultipleLogin;
+        Boolean result = loginPolicyDAO.selectLatestPolicy();
+        return Boolean.TRUE.equals(result);
     }
 
 }
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
@@ -4,6 +4,7 @@
 import com.takensoft.cms.mber.vo.MberVO;
 import com.takensoft.common.config.AppConfig;
 import com.takensoft.common.config.RedisConfig;
+import com.takensoft.common.exception.FilterExceptionHandler;
 import com.takensoft.common.util.ErrorResponse;
 import com.takensoft.common.util.JWTUtil;
 import io.jsonwebtoken.ExpiredJwtException;
@@ -37,6 +38,7 @@
  */
 public class JWTFilter extends OncePerRequestFilter {
 
+    private static final String AUTHORIZATION_HEADER = "Authorization";
     private final JWTUtil jwtUtil;
     private final AppConfig appConfig;
     private final RedisConfig redisConfig;
@@ -65,7 +67,7 @@
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         try {
             // 헤더에서 access에 대한 토큰을 꺼냄
-            String accessToken = request.getHeader("Authorization");
+            String accessToken = request.getHeader(AUTHORIZATION_HEADER);
             // 토큰이 없다면 다음 필터로 넘김
             if(accessToken == null) {
                 filterChain.doFilter(request, response);
@@ -73,8 +75,19 @@
             }
 
             // 토큰 만료 여부 검증
-            if( (Boolean) jwtUtil.getClaim(accessToken, "isExpired")) {
-                throw new JwtException("Token expired");
+            if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) {
+                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+                response.setStatus(HttpStatus.UNAUTHORIZED.value());
+
+                ErrorResponse errorResponse = new ErrorResponse();
+                errorResponse.setMessage("Token expired");
+                errorResponse.setPath(request.getRequestURI());
+                errorResponse.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase());
+                errorResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
+                errorResponse.setTimestamp(LocalDateTime.now());
+
+                response.getOutputStream().write(appConfig.getObjectMapper().writeValueAsBytes(errorResponse));
+                return;
             }
             // 토큰에서 페이로드 확인[ 발급시 명시 ]
             String category = (String) jwtUtil.getClaim(accessToken, "category");
@@ -99,7 +112,7 @@
                     response.setStatus(HttpStatus.UNAUTHORIZED.value());
 
                     ErrorResponse errorResponse = new ErrorResponse();
-                    errorResponse.setMessage("다른 기기에서 로그인되었습니다.");
+                    errorResponse.setMessage("Token expired");
                     errorResponse.setPath(request.getRequestURI());
                     errorResponse.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase());
                     errorResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
@@ -116,24 +129,14 @@
             SecurityContextHolder.getContext().setAuthentication(authToken);
             // 다음 필터로 이동
             filterChain.doFilter(request, response);
-        } catch(JwtException | InsufficientAuthenticationException e) {
-            // 토큰 검증 실패 시 클라이언트에게 에러 응답을 위한 객체 생성
-            ErrorResponse errorResponse = new ErrorResponse();
-            // 에러 응답 메시지 설정
-            if(e instanceof ExpiredJwtException) {
-                errorResponse.setMessage("Token expired");
-            } else {
-                errorResponse.setMessage(e.getMessage());
-            }
-            errorResponse.setPath(request.getRequestURI()); // 오류 경로 설정
-            errorResponse.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase()); // 오류 원인 설정
-            errorResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); // 상태코드 설정
-            errorResponse.setTimestamp(LocalDateTime.now()); // 응답 시간 설정
-
-            // 응답 헤더 설정 및 json 응답 전송
-            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
-            response.setStatus(HttpStatus.UNAUTHORIZED.value());
-            response.getOutputStream().write(appConfig.getObjectMapper().writeValueAsBytes(errorResponse));
+        } catch (ExpiredJwtException e) {
+            FilterExceptionHandler.jwtError(response, e);
+        } catch (JwtException e) {
+            FilterExceptionHandler.jwtError(response, e);
+        } catch (InsufficientAuthenticationException e) {
+            FilterExceptionHandler.jwtError(response, e);
+        } catch (Exception e) {
+            FilterExceptionHandler.jwtError(response, e);
         }
     }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/idgen/context/ContextIdgen.java
--- src/main/java/com/takensoft/common/idgen/context/ContextIdgen.java
+++ src/main/java/com/takensoft/common/idgen/context/ContextIdgen.java
@@ -3,7 +3,15 @@
 import com.takensoft.common.idgen.service.IdgenService;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-
+/**
+ * @author takensoft
+ * @since 2025.01.22
+ * @modification
+ *     since    |    author    | description
+ *  2025.01.22  |  takensoft   | 최초 등록
+ *
+ * 고유 아이디 생성 클래스
+ */
 @Configuration
 public class ContextIdgen {
 
@@ -149,4 +157,15 @@
         idgenServiceImpl.setTblNm("POPUP_MNG_ID");
         return idgenServiceImpl;
     }
+
+    // 중복 로그인 정책 이력 ID
+    @Bean(name = "loginPolicyIdgn")
+    public IdgenService loginPolicyIdgn() {
+        IdgenService idgenServiceImpl = new IdgenService();
+        idgenServiceImpl.setCipers(15);                 // 총 자릿수
+        idgenServiceImpl.setFillChar('0');              // 0으로 채움
+        idgenServiceImpl.setPrefix("LOGIN_POL_");       // 접두사
+        idgenServiceImpl.setTblNm("LOGIN_POLICY_ID");   // 시퀀스 테이블명
+        return idgenServiceImpl;
+    }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/idgen/vo/IdgenVO.java
--- src/main/java/com/takensoft/common/idgen/vo/IdgenVO.java
+++ src/main/java/com/takensoft/common/idgen/vo/IdgenVO.java
@@ -22,6 +22,6 @@
 @Setter
 public class IdgenVO implements Serializable {
 
-    private String tblNm; // 테이블명
+    private String tblNm;   // 테이블명
     private int aftrId;     // ID값
 }
src/main/java/com/takensoft/common/util/JWTUtil.java
--- src/main/java/com/takensoft/common/util/JWTUtil.java
+++ src/main/java/com/takensoft/common/util/JWTUtil.java
@@ -3,7 +3,7 @@
 import com.takensoft.cms.mber.vo.MberAuthorVO;
 import com.takensoft.cms.mber.vo.MberVO;
 import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.Jwts;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.core.Authentication;
@@ -100,11 +100,19 @@
      * 클레임 조회
      */
     public Object getClaim(String tkn, String knd) {
-        Claims claims = Jwts.parser()
-                .verifyWith(JWT_SECRET_KEY)
-                .build()
-                .parseSignedClaims(tkn)
-                .getPayload();
+        Claims claims;
+        try {
+            claims = Jwts.parser()
+                    .clockSkewSeconds(60)
+                    .verifyWith(JWT_SECRET_KEY)
+                    .build()
+                    .parseSignedClaims(tkn)
+                    .getPayload();
+        } catch (ExpiredJwtException e) {
+            // 만료된 토큰이라도 claims 꺼내기 가능
+            claims = e.getClaims();
+        }
+
         switch (knd) {
             case "category":
                 return claims.get("category", String.class);
@@ -115,24 +123,20 @@
             case "mbrNm":
                 return claims.get("mbrNm", String.class);
             case "roles":
-                // roles 클레임에서 사용자 권한 정보를 파싱
                 List<Map<String, Object>> roles = claims.get("roles", List.class);
                 List<MberAuthorVO> authorList = new ArrayList<>();
-                if(roles != null && !roles.isEmpty()) {
-                    for(Map<String, Object> role : roles) {
+                if (roles != null && !roles.isEmpty()) {
+                    for (Map<String, Object> role : roles) {
                         MberAuthorVO userAuthor = new MberAuthorVO(role.get("authority").toString());
                         authorList.add(userAuthor);
                     }
                 }
                 return authorList;
             case "isExpired":
-                // 토큰 만료 여부 반환
                 return claims.getExpiration().before(new Date());
             case "expired":
-                // 토큰 만료 시간 반환
                 return claims.getExpiration();
             default:
-                // 유효하지 않는 knd 처리
                 throw new IllegalArgumentException("Invalid knd : " + knd);
         }
     }
 
src/main/resources/mybatis/mapper/loginPolicy/loginPolicy-SQL.xml (added)
+++ src/main/resources/mybatis/mapper/loginPolicy/loginPolicy-SQL.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<!--
+    작성자 : takensoft
+    작성일 : 2025.03.22
+    내 용 : 중복로그인 관련
+-->
+<mapper namespace="com.takensoft.cms.logionPolicy.dao.LogionPolicyDAO">
+
+    <select id="selectLatestPolicy" resultType="boolean">
+        SELECT allow_multiple_login
+        FROM login_policy_history
+        ORDER BY updated_at DESC
+        LIMIT 1
+    </select>
+
+    <!-- 새로운 설정 삽입 -->
+    <insert id="insertPolicy" parameterType="LoginPolicyVO">
+        INSERT INTO login_policy_history (
+            allow_multiple_login,
+            updated_by,
+            updated_at
+        )
+        VALUES (
+            #{allowMultipleLogin},
+            #{updatedBy},
+            NOW()
+        )
+    </insert>
+
+</mapper>(파일 끝에 줄바꿈 문자 없음)
Add a comment
List