인증 실패 이력 IP 기록·보관기간·조회화면 구현 (#22-1)
All checks were successful
deploy-dev / deploy (push) Successful in 42s
All checks were successful
deploy-dev / deploy (push) Successful in 42s
#22(실패이력 활성 경로 이설)로 적재는 살아났으나 LOGIN_HISTORY가 ID/CREATE_DATE/GUBUN 3컬럼뿐이라 (1) 접속 IP 미기록 (2) 로그인 성공 시 당일 이력 DELETE로 무차별 대입 흔적 소멸 문제가 있었다. - DDL: LOGIN_HISTORY에 CONECT_IP(45)/RESET_YN + 인덱스 2종, ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY(기본 730일) 추가 - 적재: insertFailHistory 컬럼 명시 전환 + ClientIpResolver IP 세팅 (종전 무컬럼 VALUES(3값)은 컬럼 추가 시 ERROR 1136 — DDL과 한 쌍) - 보존: 성공 시 DELETE -> RESET_YN='Y' 소프트 리셋. selectFailCount가 RESET_YN='N'만 세므로 계정잠금 동작은 종전과 동일 - 화면: /mnt/sec/loginFailHistory/listView.do (계정·구분·IP·기간·리셋여부 검색 + 페이징), 메뉴 9100039 + left.jsp 분기 - 정책화면: 계정 관리 정책에 보관기간 입력 항목·검증 추가 - 배치: purgeExpiredLoginFailHistoryScheduler 매일 03:30, 1회 5,000건 상한 - Mysql/Oracle sqlmap 쌍 + sql-map-config 등록 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4973fb777
commit
65bbe591d1
@ -3,21 +3,27 @@ package egovframework.com.cmm.privacy.account;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import egovframework.com.cmm.LoginVO;
|
||||||
|
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
||||||
|
import egovframework.rte.ptl.mvc.tags.ui.pagination.PaginationInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 계정 관리 정책·현황 컨트롤러 (SFR-002).
|
* 계정 관리 정책·현황 컨트롤러 (SFR-002).
|
||||||
*
|
*
|
||||||
* <p>#13 계정 관리 정책 조회/수정, #11 장기 미사용 계정 현황 조회 백엔드 엔드포인트.
|
* <p>#13 계정 관리 정책 조회/수정, #11 장기 미사용 계정 현황 조회 백엔드 엔드포인트.</p>
|
||||||
* 화면(JSP)은 후속(#11/#13 프론트) — 여기서는 jsonView 반환.</p>
|
|
||||||
*/
|
*/
|
||||||
@Controller
|
@Controller
|
||||||
public class AccountMngController {
|
public class AccountMngController {
|
||||||
|
|
||||||
|
private static final String LOGIN_REDIRECT = "redirect:/uat/uia/egovLoginUsr.do";
|
||||||
|
|
||||||
@Resource(name = "accountMngPolicyService")
|
@Resource(name = "accountMngPolicyService")
|
||||||
private AccountMngPolicyService accountMngPolicyService;
|
private AccountMngPolicyService accountMngPolicyService;
|
||||||
|
|
||||||
@ -27,25 +33,188 @@ public class AccountMngController {
|
|||||||
/** #13 정책 조회. */
|
/** #13 정책 조회. */
|
||||||
@RequestMapping(value = "/mnt/privacy/accountPolicy.do")
|
@RequestMapping(value = "/mnt/privacy/accountPolicy.do")
|
||||||
public String selectPolicy(ModelMap model) throws Exception {
|
public String selectPolicy(ModelMap model) throws Exception {
|
||||||
|
if (handlerUser() == null) {
|
||||||
|
return denyJson(model);
|
||||||
|
}
|
||||||
model.addAttribute("policy", accountMngPolicyService.selectPolicy());
|
model.addAttribute("policy", accountMngPolicyService.selectPolicy());
|
||||||
return "jsonView";
|
return "jsonView";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** #13 정책 수정. */
|
/** #13 정책 수정. */
|
||||||
@RequestMapping(value = "/mnt/privacy/accountPolicyUpdate.do")
|
@RequestMapping(value = "/mnt/privacy/accountPolicyUpdate.do")
|
||||||
public String updatePolicy(@ModelAttribute("policy") AccountMngPolicyVO policy, ModelMap model) throws Exception {
|
public String updatePolicy(@ModelAttribute("policy") AccountMngPolicyVO policy,
|
||||||
|
HttpServletRequest request, ModelMap model) throws Exception {
|
||||||
|
LoginVO loginVO = handlerUser();
|
||||||
|
if (loginVO == null) {
|
||||||
|
return denyJson(model);
|
||||||
|
}
|
||||||
|
if (!isSameOrigin(request)) {
|
||||||
|
model.addAttribute("result", "fail");
|
||||||
|
model.addAttribute("message", "잘못된 요청 경로입니다.");
|
||||||
|
return "jsonView";
|
||||||
|
}
|
||||||
|
|
||||||
|
String invalid = validatePolicy(policy);
|
||||||
|
if (invalid != null) {
|
||||||
|
model.addAttribute("result", "fail");
|
||||||
|
model.addAttribute("message", invalid);
|
||||||
|
return "jsonView";
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.setUpdId(loginVO.getId());
|
||||||
accountMngPolicyService.updatePolicy(policy);
|
accountMngPolicyService.updatePolicy(policy);
|
||||||
model.addAttribute("result", "success");
|
model.addAttribute("result", "success");
|
||||||
return "jsonView";
|
return "jsonView";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** #11 장기 미사용 계정 현황 목록(페이징). */
|
/**
|
||||||
|
* #13 계정 관리 정책 설정 화면(서버 렌더링).
|
||||||
|
* 뷰: /WEB-INF/jsp/site/mnt/sec/accountPolicy/policy.jsp
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/mnt/sec/accountPolicy/policyView.do")
|
||||||
|
public String accountPolicyView(ModelMap model) throws Exception {
|
||||||
|
if (handlerUser() == null) {
|
||||||
|
return LOGIN_REDIRECT;
|
||||||
|
}
|
||||||
|
model.addAttribute("policy", accountMngPolicyService.selectPolicy());
|
||||||
|
return "site/mnt/sec/accountPolicy/policy";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 화면 우회 입력(직접 POST)에 대비한 서버측 범위 검증. 위반 시 사용자 메시지 반환. */
|
||||||
|
private String validatePolicy(AccountMngPolicyVO p) {
|
||||||
|
if (p.getDormantDay() < 1 || p.getDormantDay() > 3650) {
|
||||||
|
return "휴면 전환 기준일수는 1~3650일 범위로 입력하세요.";
|
||||||
|
}
|
||||||
|
if (p.getAlertD1() < 0 || p.getAlertD2() < 0 || p.getAlertD3() < 0
|
||||||
|
|| p.getAlertD1() >= p.getDormantDay() || p.getAlertD2() >= p.getDormantDay() || p.getAlertD3() >= p.getDormantDay()) {
|
||||||
|
return "사전알림 시점은 0 이상이며 휴면 기준일수보다 작아야 합니다.";
|
||||||
|
}
|
||||||
|
if (p.getAlertD1() <= p.getAlertD2() || p.getAlertD2() <= p.getAlertD3()) {
|
||||||
|
return "사전알림 시점은 1차 > 2차 > 3차 순서로 입력하세요.";
|
||||||
|
}
|
||||||
|
if (p.getDeletePreserveDay() < 1 || p.getDeletePreserveDay() > 3650) {
|
||||||
|
return "삭제 보존기간은 1~3650일 범위로 입력하세요.";
|
||||||
|
}
|
||||||
|
if (!"DAILY".equals(p.getBatchCycle()) && !"WEEKLY".equals(p.getBatchCycle()) && !"MONTHLY".equals(p.getBatchCycle())) {
|
||||||
|
return "배치 주기가 올바르지 않습니다.";
|
||||||
|
}
|
||||||
|
// [개인정보/#22-1] 인증 실패 이력 보관기간. 화면 우회 입력 대비 서버측 범위 검증.
|
||||||
|
if (p.getLoginHistKeepDay() < 1 || p.getLoginHistKeepDay() > 3650) {
|
||||||
|
return "인증 실패 이력 보관기간은 1~3650일 범위로 입력하세요.";
|
||||||
|
}
|
||||||
|
// #12 파기 배치 사용여부 — 체크박스 미전송 시 null 이 오므로 'N' 으로 보정
|
||||||
|
if (p.getPurgeUseYn() == null || p.getPurgeUseYn().trim().length() == 0) {
|
||||||
|
p.setPurgeUseYn("N");
|
||||||
|
} else if (!"Y".equals(p.getPurgeUseYn()) && !"N".equals(p.getPurgeUseYn())) {
|
||||||
|
return "계정 파기 사용여부 값이 올바르지 않습니다.";
|
||||||
|
}
|
||||||
|
String email = p.getAdminEmail();
|
||||||
|
if (email != null && email.trim().length() > 0) {
|
||||||
|
// ADMIN_EMAIL VARCHAR(200) — 길이 초과 시 DB 오류 대신 사용자 메시지로 반환
|
||||||
|
if (email.length() > 200 || !email.matches("^[\\w.%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$")) {
|
||||||
|
return "관리자 수신 이메일 형식이 올바르지 않습니다.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 취급자 영역(/mnt) 인증·구분 판정. 인터셉터 3단(인증판정 비활성 / SecondFactor·Authority 는
|
||||||
|
* 미인증 통과 fail-open)만으로는 이 컨트롤러가 다루는 계정정책·취급자 목록이 무인증으로
|
||||||
|
* 노출되므로, 화면·엔드포인트 단에서 직접 판정한다.
|
||||||
|
*
|
||||||
|
* @return 인증된 업무사용자(USR) LoginVO, 그 외에는 null
|
||||||
|
*/
|
||||||
|
private LoginVO handlerUser() {
|
||||||
|
if (!Boolean.TRUE.equals(EgovUserDetailsHelper.isAuthenticated())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object o = EgovUserDetailsHelper.getAuthenticatedUser();
|
||||||
|
if (!(o instanceof LoginVO)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LoginVO loginVO = (LoginVO) o;
|
||||||
|
return "USR".equals(loginVO.getUserSe()) ? loginVO : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSRF 부분 완화. Spring Security 2.0.4 에는 CSRF 토큰 기능이 없으므로,
|
||||||
|
* Origin/Referer 가 전달된 경우에 한해 요청 호스트와 동일한지 확인한다
|
||||||
|
* (헤더 미전송 환경은 과잉차단을 피해 통과 — 토큰 방식 전환 전까지의 임시 방어).
|
||||||
|
*/
|
||||||
|
private boolean isSameOrigin(HttpServletRequest request) {
|
||||||
|
String source = request.getHeader("Origin");
|
||||||
|
if (source == null) {
|
||||||
|
source = request.getHeader("Referer");
|
||||||
|
}
|
||||||
|
if (source == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String sourceHost = hostOf(source);
|
||||||
|
String requestHost = hostOf(request.getHeader("Host"));
|
||||||
|
return sourceHost != null && sourceHost.equalsIgnoreCase(requestHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "https://host:8080/path" · "host:8080" 등에서 호스트명만 추출. */
|
||||||
|
private String hostOf(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String host = value;
|
||||||
|
int scheme = host.indexOf("://");
|
||||||
|
if (scheme >= 0) {
|
||||||
|
host = host.substring(scheme + 3);
|
||||||
|
}
|
||||||
|
int slash = host.indexOf('/');
|
||||||
|
if (slash >= 0) {
|
||||||
|
host = host.substring(0, slash);
|
||||||
|
}
|
||||||
|
int colon = host.indexOf(':');
|
||||||
|
if (colon >= 0) {
|
||||||
|
host = host.substring(0, colon);
|
||||||
|
}
|
||||||
|
return host.length() == 0 ? null : host;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String denyJson(ModelMap model) {
|
||||||
|
model.addAttribute("result", "fail");
|
||||||
|
model.addAttribute("message", "접근 권한이 없습니다. 관리자 계정으로 로그인 후 이용하세요.");
|
||||||
|
return "jsonView";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** #11 장기 미사용 계정 현황 목록(페이징, AJAX/JSON). */
|
||||||
@RequestMapping(value = "/mnt/privacy/dormantStatusList.do")
|
@RequestMapping(value = "/mnt/privacy/dormantStatusList.do")
|
||||||
public String selectDormantStatusList(@ModelAttribute("searchVO") DormantAccountStatusVO searchVO, ModelMap model) throws Exception {
|
public String selectDormantStatusList(@ModelAttribute("searchVO") DormantAccountStatusVO searchVO, ModelMap model) throws Exception {
|
||||||
|
if (handlerUser() == null) {
|
||||||
|
return denyJson(model);
|
||||||
|
}
|
||||||
List<DormantAccountStatusVO> list = dormantAccountStatusService.selectDormantStatusList(searchVO);
|
List<DormantAccountStatusVO> list = dormantAccountStatusService.selectDormantStatusList(searchVO);
|
||||||
model.addAttribute("resultList", list);
|
model.addAttribute("resultList", list);
|
||||||
model.addAttribute("totalCount", Integer.valueOf(searchVO.getTotalRecordCount()));
|
model.addAttribute("totalCount", Integer.valueOf(searchVO.getTotalRecordCount()));
|
||||||
// TODO 화면 #11 프론트 후속(JSP)
|
|
||||||
return "jsonView";
|
return "jsonView";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #11 장기 미사용 계정 현황 조회 화면(서버 렌더링).
|
||||||
|
* 뷰: /WEB-INF/jsp/site/mnt/sec/dormantStatus/list.jsp
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/mnt/sec/dormantStatus/listView.do")
|
||||||
|
public String dormantStatusListView(@ModelAttribute("dormantAccountStatusVO") DormantAccountStatusVO searchVO,
|
||||||
|
ModelMap model) throws Exception {
|
||||||
|
if (handlerUser() == null) {
|
||||||
|
return LOGIN_REDIRECT;
|
||||||
|
}
|
||||||
|
PaginationInfo paginationInfo = new PaginationInfo();
|
||||||
|
paginationInfo.setCurrentPageNo(searchVO.getPageIndex());
|
||||||
|
paginationInfo.setRecordCountPerPage(searchVO.getRecordCountPerPage() < 1 ? 10 : searchVO.getRecordCountPerPage());
|
||||||
|
paginationInfo.setPageSize(10);
|
||||||
|
|
||||||
|
// selectDormantStatusList 내부에서 firstIndex/totalRecordCount 를 세팅한다.
|
||||||
|
List<DormantAccountStatusVO> list = dormantAccountStatusService.selectDormantStatusList(searchVO);
|
||||||
|
paginationInfo.setTotalRecordCount(searchVO.getTotalRecordCount());
|
||||||
|
|
||||||
|
model.addAttribute("resultList", list);
|
||||||
|
model.addAttribute("paginationInfo", paginationInfo);
|
||||||
|
return "site/mnt/sec/dormantStatus/list";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,16 @@ public class AccountMngPolicyVO implements Serializable {
|
|||||||
private int deletePreserveDay = 365;
|
private int deletePreserveDay = 365;
|
||||||
/** 배치 주기(DAILY/WEEKLY/MONTHLY). */
|
/** 배치 주기(DAILY/WEEKLY/MONTHLY). */
|
||||||
private String batchCycle = "DAILY";
|
private String batchCycle = "DAILY";
|
||||||
|
/**
|
||||||
|
* 장기 미사용 계정 파기 배치 사용여부(#12). Y:사용 N:미사용.
|
||||||
|
* 기본 'N' — 관리자가 정책 화면에서 명시적으로 켜야 파기가 수행된다(되돌릴 수 없는 처리).
|
||||||
|
*/
|
||||||
|
private String purgeUseYn = "N";
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력(LOGIN_HISTORY) 보관기간(일, 기본 730=2년, #22-1).
|
||||||
|
* 경과분은 BatchScheduler.purgeExpiredLoginFailHistoryScheduler 가 파기한다.
|
||||||
|
*/
|
||||||
|
private int loginHistKeepDay = 730;
|
||||||
/** 사전알림 관리자 수신 이메일. */
|
/** 사전알림 관리자 수신 이메일. */
|
||||||
private String adminEmail;
|
private String adminEmail;
|
||||||
private String updId;
|
private String updId;
|
||||||
@ -53,6 +63,10 @@ public class AccountMngPolicyVO implements Serializable {
|
|||||||
public void setDeletePreserveDay(int deletePreserveDay) { this.deletePreserveDay = deletePreserveDay; }
|
public void setDeletePreserveDay(int deletePreserveDay) { this.deletePreserveDay = deletePreserveDay; }
|
||||||
public String getBatchCycle() { return batchCycle; }
|
public String getBatchCycle() { return batchCycle; }
|
||||||
public void setBatchCycle(String batchCycle) { this.batchCycle = batchCycle; }
|
public void setBatchCycle(String batchCycle) { this.batchCycle = batchCycle; }
|
||||||
|
public String getPurgeUseYn() { return purgeUseYn; }
|
||||||
|
public void setPurgeUseYn(String purgeUseYn) { this.purgeUseYn = purgeUseYn; }
|
||||||
|
public int getLoginHistKeepDay() { return loginHistKeepDay; }
|
||||||
|
public void setLoginHistKeepDay(int loginHistKeepDay) { this.loginHistKeepDay = loginHistKeepDay; }
|
||||||
public String getAdminEmail() { return adminEmail; }
|
public String getAdminEmail() { return adminEmail; }
|
||||||
public void setAdminEmail(String adminEmail) { this.adminEmail = adminEmail; }
|
public void setAdminEmail(String adminEmail) { this.adminEmail = adminEmail; }
|
||||||
public String getUpdId() { return updId; }
|
public String getUpdId() { return updId; }
|
||||||
|
|||||||
@ -0,0 +1,95 @@
|
|||||||
|
package egovframework.com.cmm.privacy.lock;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import egovframework.com.cmm.LoginVO;
|
||||||
|
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
||||||
|
import egovframework.rte.ptl.mvc.tags.ui.pagination.PaginationInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력 조회 컨트롤러 — 개인정보보호 기능개선 사업(SER-001 / #22-1).
|
||||||
|
*
|
||||||
|
* <p>#22 로 살아난 {@code LOGIN_HISTORY} 적재분을 관리자가 계정·IP·기간으로
|
||||||
|
* 조회한다. 화면: {@code /WEB-INF/jsp/site/mnt/sec/loginFailHistory/list.jsp}</p>
|
||||||
|
*
|
||||||
|
* <p><b>접근통제:</b> 인터셉터 3단 중 ①인증판정이 비활성이고 ②③은 fail-open 이라
|
||||||
|
* /mnt 하위라도 무인증 노출이 가능하므로, {@link #handlerUser()} 로 화면·엔드포인트
|
||||||
|
* 단에서 직접 판정한다(AccountMngController 와 동일 방식).</p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* << 개정이력(Modification Information) >>
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- ------ ---------------------------
|
||||||
|
* 2026.08.05 URP 최초 생성 (개인정보보호 SER-001 #22-1)
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class LoginFailHistoryController {
|
||||||
|
|
||||||
|
private static final String LOGIN_REDIRECT = "redirect:/uat/uia/egovLoginUsr.do";
|
||||||
|
|
||||||
|
@Resource(name = "loginFailHistoryService")
|
||||||
|
private LoginFailHistoryService loginFailHistoryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력 조회 화면(서버 렌더링).
|
||||||
|
* 뷰: /WEB-INF/jsp/site/mnt/sec/loginFailHistory/list.jsp
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/mnt/sec/loginFailHistory/listView.do")
|
||||||
|
public String listView(@ModelAttribute("loginFailHistoryVO") LoginFailHistoryVO searchVO,
|
||||||
|
ModelMap model) throws Exception {
|
||||||
|
if (handlerUser() == null) {
|
||||||
|
return LOGIN_REDIRECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LoginFailHistoryVO> list = loginFailHistoryService.selectLoginFailHistoryList(searchVO);
|
||||||
|
|
||||||
|
PaginationInfo paginationInfo = new PaginationInfo();
|
||||||
|
paginationInfo.setCurrentPageNo(searchVO.getPageIndex());
|
||||||
|
paginationInfo.setRecordCountPerPage(searchVO.getRecordCountPerPage());
|
||||||
|
paginationInfo.setPageSize(10);
|
||||||
|
paginationInfo.setTotalRecordCount(searchVO.getTotalRecordCount());
|
||||||
|
|
||||||
|
model.addAttribute("resultList", list);
|
||||||
|
model.addAttribute("paginationInfo", paginationInfo);
|
||||||
|
return "site/mnt/sec/loginFailHistory/list";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 인증 실패 이력 목록(AJAX/JSON). */
|
||||||
|
@RequestMapping(value = "/mnt/privacy/loginFailHistoryList.do")
|
||||||
|
public String selectList(@ModelAttribute("searchVO") LoginFailHistoryVO searchVO,
|
||||||
|
ModelMap model) throws Exception {
|
||||||
|
if (handlerUser() == null) {
|
||||||
|
model.addAttribute("result", "fail");
|
||||||
|
model.addAttribute("message", "접근 권한이 없습니다. 관리자 계정으로 로그인 후 이용하세요.");
|
||||||
|
return "jsonView";
|
||||||
|
}
|
||||||
|
model.addAttribute("resultList", loginFailHistoryService.selectLoginFailHistoryList(searchVO));
|
||||||
|
model.addAttribute("totalCount", Integer.valueOf(searchVO.getTotalRecordCount()));
|
||||||
|
return "jsonView";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 취급자 영역(/mnt) 인증·구분 판정.
|
||||||
|
*
|
||||||
|
* @return 인증된 업무사용자(USR) LoginVO, 그 외에는 null
|
||||||
|
*/
|
||||||
|
private LoginVO handlerUser() {
|
||||||
|
if (!Boolean.TRUE.equals(EgovUserDetailsHelper.isAuthenticated())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object o = EgovUserDetailsHelper.getAuthenticatedUser();
|
||||||
|
if (!(o instanceof LoginVO)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LoginVO loginVO = (LoginVO) o;
|
||||||
|
return "USR".equals(loginVO.getUserSe()) ? loginVO : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package egovframework.com.cmm.privacy.lock;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import egovframework.com.cmm.service.impl.EgovComAbstractDAO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력 DAO — 개인정보보호 기능개선 사업(SER-001 / #22-1).
|
||||||
|
*
|
||||||
|
* <p>{@code LOGIN_HISTORY} 조회(화면) + 보관기간 경과분 파기(배치).
|
||||||
|
* 적재(insert)·리셋(update)은 로그인 흐름 소유인 {@code loginDAO} 가 담당하므로
|
||||||
|
* 여기서는 정의하지 않는다(이중 경로 방지).</p>
|
||||||
|
*
|
||||||
|
* <p>statement 는 {@code PrivacyLoginFailHistory_SQL_Mysql.xml}
|
||||||
|
* (namespace {@code LoginFailHistoryDAO}), sql-map-config 등록.</p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* << 개정이력(Modification Information) >>
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- ------ ---------------------------
|
||||||
|
* 2026.08.05 URP 최초 생성 (개인정보보호 SER-001 #22-1)
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Repository("loginFailHistoryDAO")
|
||||||
|
public class LoginFailHistoryDAO extends EgovComAbstractDAO {
|
||||||
|
|
||||||
|
/** 인증 실패 이력 목록(검색·페이징) */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<LoginFailHistoryVO> selectLoginFailHistoryList(LoginFailHistoryVO vo) {
|
||||||
|
return list("LoginFailHistoryDAO.selectLoginFailHistoryList", vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 인증 실패 이력 전체 건수(페이징) */
|
||||||
|
public int selectLoginFailHistoryListCount(LoginFailHistoryVO vo) {
|
||||||
|
Object cnt = selectByPk("LoginFailHistoryDAO.selectLoginFailHistoryListCount", vo);
|
||||||
|
return cnt == null ? 0 : ((Integer) cnt).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 보관기간(일) 정책값 조회. 미설정 시 730 fallback 은 statement 내부에서 처리. */
|
||||||
|
public int selectKeepDay() {
|
||||||
|
Object day = selectByPk("LoginFailHistoryDAO.selectLoginHistKeepDay", null);
|
||||||
|
return day == null ? 730 : ((Integer) day).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 보관기간 경과 실패 이력 파기.
|
||||||
|
*
|
||||||
|
* @param keepDay 보관기간(일)
|
||||||
|
* @param limit 1회 처리 상한(백로그 폭주 방지)
|
||||||
|
* @return 파기 건수
|
||||||
|
*/
|
||||||
|
public int deleteExpiredLoginFailHistory(int keepDay, int limit) {
|
||||||
|
HashMap<String, Object> param = new HashMap<String, Object>();
|
||||||
|
param.put("keepDay", Integer.valueOf(keepDay));
|
||||||
|
param.put("limitCnt", Integer.valueOf(limit));
|
||||||
|
return delete("LoginFailHistoryDAO.deleteExpiredLoginFailHistory", param);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package egovframework.com.cmm.privacy.lock;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력 조회·파기 서비스 — 개인정보보호 기능개선 사업(SER-001 / #22-1).
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* << 개정이력(Modification Information) >>
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- ------ ---------------------------
|
||||||
|
* 2026.08.05 URP 최초 생성 (개인정보보호 SER-001 #22-1)
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public interface LoginFailHistoryService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력 목록 조회. 호출 후 {@code searchVO.totalRecordCount} 가 세팅된다.
|
||||||
|
*
|
||||||
|
* @param searchVO 검색조건·페이징
|
||||||
|
* @return 실패 이력 목록
|
||||||
|
* @throws Exception 조회 오류
|
||||||
|
*/
|
||||||
|
List<LoginFailHistoryVO> selectLoginFailHistoryList(LoginFailHistoryVO searchVO) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 보관기간(ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY) 경과 실패 이력을 파기한다.
|
||||||
|
*
|
||||||
|
* @return 파기 건수
|
||||||
|
* @throws Exception 파기 오류
|
||||||
|
*/
|
||||||
|
int purgeExpiredLoginFailHistory() throws Exception;
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package egovframework.com.cmm.privacy.lock;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력 조회·파기 서비스 구현 — 개인정보보호 기능개선 사업(SER-001 / #22-1).
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* << 개정이력(Modification Information) >>
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- ------ ---------------------------
|
||||||
|
* 2026.08.05 URP 최초 생성 (개인정보보호 SER-001 #22-1)
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Service("loginFailHistoryService")
|
||||||
|
public class LoginFailHistoryServiceImpl implements LoginFailHistoryService {
|
||||||
|
|
||||||
|
/** 1회 파기 처리 상한 — 배치가 오래 멈췄다 재가동되는 날 백로그 폭주 방지. */
|
||||||
|
private static final int PURGE_LIMIT = 5000;
|
||||||
|
|
||||||
|
/** 정책값 미설정 시 보관기간 기본값(일). */
|
||||||
|
private static final int DEFAULT_KEEP_DAY = 730;
|
||||||
|
|
||||||
|
@Resource(name = "loginFailHistoryDAO")
|
||||||
|
private LoginFailHistoryDAO loginFailHistoryDAO;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LoginFailHistoryVO> selectLoginFailHistoryList(LoginFailHistoryVO searchVO) throws Exception {
|
||||||
|
if (searchVO.getRecordCountPerPage() < 1) {
|
||||||
|
searchVO.setRecordCountPerPage(10);
|
||||||
|
}
|
||||||
|
if (searchVO.getPageIndex() < 1) {
|
||||||
|
searchVO.setPageIndex(1);
|
||||||
|
}
|
||||||
|
searchVO.setFirstIndex((searchVO.getPageIndex() - 1) * searchVO.getRecordCountPerPage());
|
||||||
|
searchVO.setTotalRecordCount(loginFailHistoryDAO.selectLoginFailHistoryListCount(searchVO));
|
||||||
|
return loginFailHistoryDAO.selectLoginFailHistoryList(searchVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int purgeExpiredLoginFailHistory() throws Exception {
|
||||||
|
int keepDay = loginFailHistoryDAO.selectKeepDay();
|
||||||
|
if (keepDay < 1) {
|
||||||
|
keepDay = DEFAULT_KEEP_DAY;
|
||||||
|
}
|
||||||
|
return loginFailHistoryDAO.deleteExpiredLoginFailHistory(keepDay, PURGE_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
package egovframework.com.cmm.privacy.lock;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 실패 이력 VO — 개인정보보호 기능개선 사업(SER-001 / #22-1).
|
||||||
|
*
|
||||||
|
* <p>{@code LOGIN_HISTORY} 조회 결과 + 검색조건 + 페이징.
|
||||||
|
* #22(실패이력 활성 경로 이설)로 적재가 살아난 뒤, #22-1 에서 접속 IP(CONECT_IP)와
|
||||||
|
* 보존(RESET_YN 소프트 리셋)을 추가하면서 조회 화면 대상이 되었다.</p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* << 개정이력(Modification Information) >>
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- ------ ---------------------------
|
||||||
|
* 2026.08.05 URP 최초 생성 (개인정보보호 SER-001 #22-1)
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class LoginFailHistoryVO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ---- 조회 결과 ----
|
||||||
|
/** 시도 계정ID(LOGIN_HISTORY.ID) */
|
||||||
|
private String id;
|
||||||
|
/** 사용자구분(GNR:개인 GRO:그룹 ENT:활동처 USR:업무사용자) */
|
||||||
|
private String gubun;
|
||||||
|
/** 실패 일시(yyyy-MM-dd HH:mm:ss) */
|
||||||
|
private String createDate;
|
||||||
|
/** 접속 IP(#22-1) */
|
||||||
|
private String conectIp;
|
||||||
|
/** 잠금카운터 리셋여부 Y:이후 로그인 성공으로 리셋 N:유효 */
|
||||||
|
private String resetYn;
|
||||||
|
/** 성명(회원 테이블 조인, 없으면 null) */
|
||||||
|
private String userNm;
|
||||||
|
|
||||||
|
// ---- 검색조건 ----
|
||||||
|
/** 계정ID(부분일치) */
|
||||||
|
private String searchId;
|
||||||
|
/** 사용자구분(완전일치) */
|
||||||
|
private String searchGubun;
|
||||||
|
/** 접속 IP(부분일치) */
|
||||||
|
private String searchIp;
|
||||||
|
/** 조회 시작일(yyyy-MM-dd) */
|
||||||
|
private String searchBgnDe;
|
||||||
|
/** 조회 종료일(yyyy-MM-dd) */
|
||||||
|
private String searchEndDe;
|
||||||
|
/** 리셋여부 필터(Y/N, 공백=전체) */
|
||||||
|
private String searchResetYn;
|
||||||
|
|
||||||
|
// ---- 페이징 ----
|
||||||
|
private int pageIndex = 1;
|
||||||
|
private int recordCountPerPage = 10;
|
||||||
|
private int firstIndex = 0;
|
||||||
|
private int totalRecordCount = 0;
|
||||||
|
|
||||||
|
public String getId() { return id; }
|
||||||
|
public void setId(String id) { this.id = id; }
|
||||||
|
public String getGubun() { return gubun; }
|
||||||
|
public void setGubun(String gubun) { this.gubun = gubun; }
|
||||||
|
public String getCreateDate() { return createDate; }
|
||||||
|
public void setCreateDate(String createDate) { this.createDate = createDate; }
|
||||||
|
public String getConectIp() { return conectIp; }
|
||||||
|
public void setConectIp(String conectIp) { this.conectIp = conectIp; }
|
||||||
|
public String getResetYn() { return resetYn; }
|
||||||
|
public void setResetYn(String resetYn) { this.resetYn = resetYn; }
|
||||||
|
public String getUserNm() { return userNm; }
|
||||||
|
public void setUserNm(String userNm) { this.userNm = userNm; }
|
||||||
|
|
||||||
|
public String getSearchId() { return searchId; }
|
||||||
|
public void setSearchId(String searchId) { this.searchId = searchId; }
|
||||||
|
public String getSearchGubun() { return searchGubun; }
|
||||||
|
public void setSearchGubun(String searchGubun) { this.searchGubun = searchGubun; }
|
||||||
|
public String getSearchIp() { return searchIp; }
|
||||||
|
public void setSearchIp(String searchIp) { this.searchIp = searchIp; }
|
||||||
|
public String getSearchBgnDe() { return searchBgnDe; }
|
||||||
|
public void setSearchBgnDe(String searchBgnDe) { this.searchBgnDe = searchBgnDe; }
|
||||||
|
public String getSearchEndDe() { return searchEndDe; }
|
||||||
|
public void setSearchEndDe(String searchEndDe) { this.searchEndDe = searchEndDe; }
|
||||||
|
public String getSearchResetYn() { return searchResetYn; }
|
||||||
|
public void setSearchResetYn(String searchResetYn) { this.searchResetYn = searchResetYn; }
|
||||||
|
|
||||||
|
public int getPageIndex() { return pageIndex; }
|
||||||
|
public void setPageIndex(int pageIndex) { this.pageIndex = pageIndex; }
|
||||||
|
public int getRecordCountPerPage() { return recordCountPerPage; }
|
||||||
|
public void setRecordCountPerPage(int recordCountPerPage) { this.recordCountPerPage = recordCountPerPage; }
|
||||||
|
public int getFirstIndex() { return firstIndex; }
|
||||||
|
public void setFirstIndex(int firstIndex) { this.firstIndex = firstIndex; }
|
||||||
|
public int getTotalRecordCount() { return totalRecordCount; }
|
||||||
|
public void setTotalRecordCount(int totalRecordCount) { this.totalRecordCount = totalRecordCount; }
|
||||||
|
}
|
||||||
@ -144,14 +144,17 @@ public class LoginDAO extends EgovComAbstractDAO {
|
|||||||
insert("loginDAO.insertFailHistory", vo);
|
insert("loginDAO.insertFailHistory", vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// KTH. 20190603 로그인 성공 시 당일 실패 이력 정보 delete
|
// KTH. 20190603 로그인 성공 시 당일 실패 이력 정보 정리
|
||||||
|
// [개인정보/#22-1] 실제 동작은 DELETE 가 아니라 RESET_YN='Y' 소프트 리셋.
|
||||||
|
// 이력은 보관기간(ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY)까지 보존하고,
|
||||||
|
// 잠금 카운트만 리셋한다. 메서드명은 호출부 영향 최소화를 위해 유지.
|
||||||
public void deleteLoginSuccess(LoginVO vo) {
|
public void deleteLoginSuccess(LoginVO vo) {
|
||||||
delete("loginDAO.deleteLoginSuccess", vo);
|
update("loginDAO.deleteLoginSuccess", vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// KTH. 20190607
|
// KTH. 20190607 / [개인정보/#22-1] 동일 — 당일 전체 소프트 리셋
|
||||||
public void deleteLoginSuccessTodayAll(LoginVO vo) {
|
public void deleteLoginSuccessTodayAll(LoginVO vo) {
|
||||||
delete("loginDAO.deleteLoginSuccessAll", vo);
|
update("loginDAO.deleteLoginSuccessAll", vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 휴먼고객 확인
|
// 휴먼고객 확인
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import egovframework.com.cmm.EgovMessageSource;
|
|||||||
import egovframework.com.cmm.EgovWebUtil;
|
import egovframework.com.cmm.EgovWebUtil;
|
||||||
import egovframework.com.cmm.LoginVO;
|
import egovframework.com.cmm.LoginVO;
|
||||||
import egovframework.com.cmm.annotation.IncludedInfo;
|
import egovframework.com.cmm.annotation.IncludedInfo;
|
||||||
|
import egovframework.com.cmm.privacy.auditlog.ClientIpResolver;
|
||||||
import egovframework.com.cmm.service.EgovCmmUseService;
|
import egovframework.com.cmm.service.EgovCmmUseService;
|
||||||
import egovframework.com.cmm.service.Globals;
|
import egovframework.com.cmm.service.Globals;
|
||||||
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
||||||
@ -221,6 +222,10 @@ public class EgovLoginController {
|
|||||||
threshold = totVo.getLockCount();
|
threshold = totVo.getLockCount();
|
||||||
}
|
}
|
||||||
// 실패 이력 적재(LOGIN_HISTORY)
|
// 실패 이력 적재(LOGIN_HISTORY)
|
||||||
|
// [개인정보/#22-1] 접속 IP 기록 — 어디서 시도된 실패인지 추적 가능하게 한다.
|
||||||
|
// ClientIpResolver(#34): X-Forwarded-For → Proxy-Client-IP → getRemoteAddr,
|
||||||
|
// 신뢰 프록시 CIDR 검증 포함(Apache+L4 구성에서 getRemoteAddr 만으로는 부정확).
|
||||||
|
loginVO.setIp(ClientIpResolver.getClientIp(request));
|
||||||
loginService.insertLoginFail(loginVO);
|
loginService.insertLoginFail(loginVO);
|
||||||
// 당일 누적 실패 횟수
|
// 당일 누적 실패 횟수
|
||||||
currentFail = loginService.selectFailCount(map);
|
currentFail = loginService.selectFailCount(map);
|
||||||
|
|||||||
@ -65,6 +65,14 @@ public class BatchScheduler {
|
|||||||
@Resource(name = "dormantAlertService")
|
@Resource(name = "dormantAlertService")
|
||||||
private egovframework.com.cmm.privacy.account.DormantAlertService dormantAlertService;
|
private egovframework.com.cmm.privacy.account.DormantAlertService dormantAlertService;
|
||||||
|
|
||||||
|
/** 개인정보보호 장기 미사용 계정 파기 서비스 (#12 보존기간 경과 계정 개인정보 파기) */
|
||||||
|
@Resource(name = "accountDeleteService")
|
||||||
|
private egovframework.com.cmm.privacy.account.AccountDeleteService accountDeleteService;
|
||||||
|
|
||||||
|
/** [개인정보/#22-1] 인증 실패 이력 보관기간 파기 */
|
||||||
|
@Resource(name = "loginFailHistoryService")
|
||||||
|
private egovframework.com.cmm.privacy.lock.LoginFailHistoryService loginFailHistoryService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테스트 배치 예제
|
* 테스트 배치 예제
|
||||||
*//*
|
*//*
|
||||||
@ -180,10 +188,9 @@ public class BatchScheduler {
|
|||||||
* [개인정보보호 #5] 권한 변경 이력(AUTHOR_CHANGE_HISTORY) 파기 배치.
|
* [개인정보보호 #5] 권한 변경 이력(AUTHOR_CHANGE_HISTORY) 파기 배치.
|
||||||
* 매월 1일 04:00, 보관기간(1,095일=3년) 경과 이력만 선별 파기. 개별 삭제 API 미제공.
|
* 매월 1일 04:00, 보관기간(1,095일=3년) 경과 이력만 선별 파기. 개별 삭제 API 미제공.
|
||||||
*
|
*
|
||||||
* <p>※ 스케줄 활성화 주의: 본 프로젝트는 {@code <task:annotation-driven>}이
|
* <p>※ 2026-07-28 context-common.xml의 {@code <task:annotation-driven>} 활성화로
|
||||||
* context-common.xml/egov-com-servlet.xml 양쪽 모두 주석 처리되어 있어 현재
|
* 본 클래스의 {@code @Scheduled}가 실제 기동된다. OS crontab에서 동일 배치를 병행 운영 중이면
|
||||||
* {@code @Scheduled}가 동작하지 않는다(배치는 OS crontab 운영). 활성화 시 기존 휴면/메일 등
|
* 이중 실행이 되므로 운영과 경합 여부를 확인해야 한다.</p>
|
||||||
* 휴면 배치가 함께 기동되므로, 활성화 여부·경합은 운영과 협의해 결정한다.</p>
|
|
||||||
*
|
*
|
||||||
* @throws Exception 파기 오류
|
* @throws Exception 파기 오류
|
||||||
*/
|
*/
|
||||||
@ -211,7 +218,7 @@ public class BatchScheduler {
|
|||||||
* [개인정보보호 #37] 개인정보 접속기록(PERSONAL_DATA_ACCESS_LOG) 보관정책 파기 배치.
|
* [개인정보보호 #37] 개인정보 접속기록(PERSONAL_DATA_ACCESS_LOG) 보관정책 파기 배치.
|
||||||
* 매월 1일 02:00(저부하 시간대), 보관기간(730일) 경과 파티션을 DROP(개별 DELETE 미사용)하고
|
* 매월 1일 02:00(저부하 시간대), 보관기간(730일) 경과 파티션을 DROP(개별 DELETE 미사용)하고
|
||||||
* 미래 월 파티션을 선행 증설한다. COMTNSYSLOG 7일 삭제정책은 이 테이블에 적용하지 않는다(#38).
|
* 미래 월 파티션을 선행 증설한다. COMTNSYSLOG 7일 삭제정책은 이 테이블에 적용하지 않는다(#38).
|
||||||
* (스케줄 활성화 주의는 {@link #purgeAuthorChangeHistoryScheduler()} 참조 — 현재 전역 비활성.)
|
* (스케줄 활성화 주의는 {@link #purgeAuthorChangeHistoryScheduler()} 참조.)
|
||||||
*
|
*
|
||||||
* @throws Exception 파기/증설 오류
|
* @throws Exception 파기/증설 오류
|
||||||
*/
|
*/
|
||||||
@ -226,8 +233,7 @@ public class BatchScheduler {
|
|||||||
* 매월 1일 05:00(기존 파기 02:00/무결성 03:00/이력파기 04:00 배치와 시각 분리).
|
* 매월 1일 05:00(기존 파기 02:00/무결성 03:00/이력파기 04:00 배치와 시각 분리).
|
||||||
* 전월분 접속기록을 집계(총건수·실패·무결성#40·취급자별 처리현황)해 PDAL_INSPECTION_REPORT 적재.
|
* 전월분 접속기록을 집계(총건수·실패·무결성#40·취급자별 처리현황)해 PDAL_INSPECTION_REPORT 적재.
|
||||||
*
|
*
|
||||||
* <p>※ 스케줄 활성화 주의: 전역 {@code <task:annotation-driven>} 비활성 상태라
|
* (스케줄 활성화 주의는 {@link #purgeAuthorChangeHistoryScheduler()} 참조.)
|
||||||
* 현재 {@code @Scheduled}는 동작하지 않는다(OS crontab 운영). 활성화·경합은 운영과 협의.</p>
|
|
||||||
*
|
*
|
||||||
* @throws Exception 집계/적재 오류
|
* @throws Exception 집계/적재 오류
|
||||||
*/
|
*/
|
||||||
@ -239,19 +245,60 @@ public class BatchScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [개인정보보호 #10] 휴면 전 사전 알림 배치 — D-30/D-7/D-Day 3단계 + 관리자 알림.
|
* [개인정보보호 #10] 휴면 전 사전 알림 배치 — D-30/D-7/D-Day 3단계 관리자 알림.
|
||||||
* 매일 06:00, 정책(ACCOUNT_MNG_POLICY)의 ALERT_D1/D2/D3 시점에 해당하는 휴면 예정
|
* 매일 06:00, 정책(ACCOUNT_MNG_POLICY)의 ALERT_D1/D2/D3 시점에 해당하는 휴면 예정
|
||||||
* 계정을 본인·관리자에게 사전 통지한다. 발송 실패는 fail-safe(로깅 후 계속).
|
* 계정을 모아 관리자(ADMIN_EMAIL)에게 요약 1통으로 통지한다. 발송 실패는 fail-safe(로깅 후 계속).
|
||||||
*
|
*
|
||||||
* <p>※ 전역 {@code <task:annotation-driven>} 비활성 상태라 현재 @Scheduled 는 동작하지
|
* <p>2026-07-30: 대상자 본인 개별 발송을 제거했다(요구사항은 관리자 알림이며, 5만건 규모에서
|
||||||
* 않는다(OS crontab 운영). 활성화·경합은 운영과 협의(핸드오프).</p>
|
* 본인 건별 발송이 휴면 배치 처리량을 지배했다). 상세는 {@code DormantAlertServiceImpl} 주석 참조.</p>
|
||||||
|
*
|
||||||
|
* (스케줄 활성화 주의는 {@link #purgeAuthorChangeHistoryScheduler()} 참조.)
|
||||||
*
|
*
|
||||||
* @throws Exception 알림 처리 오류
|
* @throws Exception 알림 처리 오류
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "0 0 6 * * ?")
|
@Scheduled(cron = "0 0 6 * * ?")
|
||||||
public void dormantPreAlertScheduler() throws Exception {
|
public void dormantPreAlertScheduler() throws Exception {
|
||||||
int sent = dormantAlertService.sendDormantPreAlerts();
|
int sent = dormantAlertService.sendDormantPreAlerts();
|
||||||
log.info("[개인정보/#10] 휴면 사전알림 배치 완료 — " + sent + "건 발송");
|
log.info("[개인정보/#10] 휴면 사전알림 배치 완료 — 관리자 요약 " + sent + "통 발송");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [개인정보보호 #12] 장기 미사용 계정 개인정보 파기 배치.
|
||||||
|
* 매일 02:30, 휴면 확정('P') 후 보존기간(ACCOUNT_MNG_POLICY.DELETE_PRESERVE_DAY 정책값)이
|
||||||
|
* 경과한 계정의 개인정보를 파기(PII 마스킹 + 상태 'D')하고 ACCOUNT_DELETE_HISTORY 에 이력을 남긴다.
|
||||||
|
* 휴면 등록 배치(01:00)·사전알림(06:00)과 시각을 분리했다.
|
||||||
|
*
|
||||||
|
* <p><b>실제 파기 수행 여부는 정책 플래그로 제어한다.</b> 배치는 매일 등록되어 돌지만
|
||||||
|
* {@code ACCOUNT_MNG_POLICY.PURGE_USE_YN}(기본 'N')이 'Y'가 아니면 서비스가 즉시 0건으로
|
||||||
|
* 종료한다. 켜고 끄는 것은 계정 관리 정책 화면(#13)에서 하며 재배포가 필요 없다.
|
||||||
|
* 1회 처리건수는 SQL에서 200건으로 상한(백로그 일시 폭주 방지).</p>
|
||||||
|
*
|
||||||
|
* @throws Exception 파기 오류
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 30 2 * * ?")
|
||||||
|
public void purgeExpiredAccountScheduler() throws Exception {
|
||||||
|
int purged = accountDeleteService.purgeExpiredAccounts();
|
||||||
|
log.info("[개인정보/#12] 장기 미사용 계정 파기 배치 완료 — " + purged + "건");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [개인정보보호 #22-1] 인증 실패 이력(LOGIN_HISTORY) 보관기간 파기 배치.
|
||||||
|
* 매일 03:30, {@code ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY}(기본 730일=2년)이
|
||||||
|
* 경과한 실패 이력을 삭제한다. 1회 처리 상한 5,000건(백로그 폭주 방지, 잔여분은 다음 회차).
|
||||||
|
*
|
||||||
|
* <p>#22 이설 이전에는 로그인 성공 시 당일 이력을 DELETE 해 사실상 보관 개념이 없었다.
|
||||||
|
* #22-1 에서 성공 시 처리를 소프트 리셋(RESET_YN='Y')으로 바꿔 이력을 보존하게 되면서,
|
||||||
|
* 무한 적재를 막는 파기 경로로 이 배치가 필요해졌다.</p>
|
||||||
|
*
|
||||||
|
* <p>계정 파기(#12, 02:30)·접속기록 파티션 파기(#37, 매월 1일 02:00)와 시각을 분리했다.
|
||||||
|
* (스케줄 활성화 주의는 {@link #purgeAuthorChangeHistoryScheduler()} 참조.)</p>
|
||||||
|
*
|
||||||
|
* @throws Exception 파기 오류
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 30 3 * * ?")
|
||||||
|
public void purgeExpiredLoginFailHistoryScheduler() throws Exception {
|
||||||
|
int purged = loginFailHistoryService.purgeExpiredLoginFailHistory();
|
||||||
|
log.info("[개인정보/#22-1] 인증 실패 이력 파기 배치 완료 — " + purged + "건");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -141,6 +141,9 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- KTH. 20190603 -->
|
<!-- KTH. 20190603 -->
|
||||||
|
<!-- [개인정보/#22-1] 잠금 카운트는 '리셋되지 않은(RESET_YN='N') 당일 실패행'만 센다.
|
||||||
|
성공 시 행을 지우지 않고 RESET_YN='Y'로 표시하므로(아래 resetFailHistory),
|
||||||
|
카운트 동작은 종전 DELETE 방식과 동일하면서 이력은 보존된다. -->
|
||||||
<select id="loginDAO.selectFailCount" parameterClass="java.util.HashMap" resultClass="java.lang.Integer" >
|
<select id="loginDAO.selectFailCount" parameterClass="java.util.HashMap" resultClass="java.lang.Integer" >
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@ -149,40 +152,55 @@
|
|||||||
FROM LOGIN_HISTORY
|
FROM LOGIN_HISTORY
|
||||||
WHERE ID = #id#
|
WHERE ID = #id#
|
||||||
AND GUBUN = #userSe#
|
AND GUBUN = #userSe#
|
||||||
|
AND RESET_YN = 'N'
|
||||||
AND DATE_FORMAT(CREATE_DATE, '%Y%m%d')=DATE_FORMAT(SYSDATE(), '%Y%m%d')
|
AND DATE_FORMAT(CREATE_DATE, '%Y%m%d')=DATE_FORMAT(SYSDATE(), '%Y%m%d')
|
||||||
GROUP BY ID, CREATE_DATE
|
GROUP BY ID, CREATE_DATE
|
||||||
)a
|
)a
|
||||||
]]>
|
]]>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- KTH. 20190603 -->
|
<!-- KTH. 20190603 / [개인정보/#22-1] DELETE → 소프트 리셋(UPDATE)으로 전환.
|
||||||
<delete id="loginDAO.deleteLoginSuccess">
|
이력 보존(보관기간 정책 적용 대상)을 위해 행을 삭제하지 않는다.
|
||||||
|
statement id 는 유지(DAO·Oracle 쌍 정합). -->
|
||||||
|
<update id="loginDAO.deleteLoginSuccess">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
DELETE FROM LOGIN_HISTORY
|
UPDATE LOGIN_HISTORY
|
||||||
|
SET RESET_YN = 'Y'
|
||||||
WHERE ID = #id#
|
WHERE ID = #id#
|
||||||
AND GUBUN = #userSe#
|
AND GUBUN = #userSe#
|
||||||
|
AND RESET_YN = 'N'
|
||||||
AND DATE_FORMAT(CREATE_DATE, '%Y%m%d') = DATE_FORMAT(SYSDATE(), '%Y%m%d')
|
AND DATE_FORMAT(CREATE_DATE, '%Y%m%d') = DATE_FORMAT(SYSDATE(), '%Y%m%d')
|
||||||
]]>
|
]]>
|
||||||
</delete>
|
</update>
|
||||||
|
|
||||||
<!-- KTH. 20190607 -->
|
<!-- KTH. 20190607 / [개인정보/#22-1] 동일 사유로 DELETE → 소프트 리셋 -->
|
||||||
<delete id="loginDAO.deleteLoginSuccessAll">
|
<update id="loginDAO.deleteLoginSuccessAll">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
DELETE FROM LOGIN_HISTORY
|
UPDATE LOGIN_HISTORY
|
||||||
WHERE DATE_FORMAT(CREATE_DATE, '%Y%m%d') = DATE_FORMAT(SYSDATE(), '%Y%m%d')
|
SET RESET_YN = 'Y'
|
||||||
|
WHERE RESET_YN = 'N'
|
||||||
|
AND DATE_FORMAT(CREATE_DATE, '%Y%m%d') = DATE_FORMAT(SYSDATE(), '%Y%m%d')
|
||||||
]]>
|
]]>
|
||||||
</delete>
|
</update>
|
||||||
|
|
||||||
<!-- KTH. 20190603 -->
|
<!-- KTH. 20190603 / [개인정보/#22-1] 컬럼 명시 + 접속 IP 적재.
|
||||||
<insert id="loginDAO.insertFailHistory">
|
★ 종전에는 컬럼목록 없는 VALUES(3값) 이라 LOGIN_HISTORY 컬럼 추가 시
|
||||||
|
ERROR 1136 으로 깨진다 — 컬럼 명시는 DDL 과 한 쌍이다. -->
|
||||||
|
<insert id="loginDAO.insertFailHistory" parameterClass="loginVO">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
INSERT INTO LOGIN_HISTORY
|
INSERT INTO LOGIN_HISTORY (
|
||||||
VALUES(
|
ID
|
||||||
|
, CREATE_DATE
|
||||||
|
, GUBUN
|
||||||
|
, CONECT_IP
|
||||||
|
, RESET_YN
|
||||||
|
) VALUES (
|
||||||
#id#
|
#id#
|
||||||
, SYSDATE()
|
, SYSDATE()
|
||||||
, #userSe#
|
, #userSe#
|
||||||
|
, #ip:VARCHAR#
|
||||||
|
, 'N'
|
||||||
)
|
)
|
||||||
|
|
||||||
]]>
|
]]>
|
||||||
</insert>
|
</insert>
|
||||||
<!-- ================================================================
|
<!-- ================================================================
|
||||||
@ -204,7 +222,13 @@
|
|||||||
<result property="diapause_stat" column="diapause_stat" columnIndex="7"/>
|
<result property="diapause_stat" column="diapause_stat" columnIndex="7"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<!-- [#8] 휴면 대상(일반회원) 적재 — 최종접속 365일 초과 & 정상('P') & 미적재 -->
|
<!-- [#8] 휴면 대상(일반회원) 적재 — 최종접속 365일 초과 & 정상('P') & 미적재
|
||||||
|
2026-07-28: 다년간 배치 미가동으로 쌓인 백로그가 최초가동일에 한꺼번에 터지는 것을
|
||||||
|
막기 위해 1회 처리건수를 상한(LIMIT 200)으로 제한. 남은 대상은 다음날 배치에서 이어 처리.
|
||||||
|
2026-07-30 상한 재산정 200 → 2,000: 본인 안내메일 발송을 걷어내(EgovLoginServiceImpl
|
||||||
|
.processDiapauseList) 1건당 비용이 INSERT..SELECT 1행으로 줄었다. 2,000/일이면 5만건
|
||||||
|
백로그를 25일에 소진한다. 확정 단계(selectDiapauseList)는 계정당 전이가 2회('' → S → P)
|
||||||
|
이므로 반드시 이 값의 2배 이상(현행 5,000)을 유지해야 'S' 적체가 생기지 않는다. -->
|
||||||
<insert id="loginDAO.insertDiapause" parameterClass="loginVO">
|
<insert id="loginDAO.insertDiapause" parameterClass="loginVO">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
INSERT INTO COMTNDIAPAUSEMBER (MBER_ID, ESNTL_ID, LAST_LOG, DIAPAUSE_STAT, DIAPAUSE_DT, MBER_TY_CODE)
|
INSERT INTO COMTNDIAPAUSEMBER (MBER_ID, ESNTL_ID, LAST_LOG, DIAPAUSE_STAT, DIAPAUSE_DT, MBER_TY_CODE)
|
||||||
@ -222,10 +246,13 @@
|
|||||||
WHERE DATEDIFF(NOW(), IFNULL(b.CREAT_DT, a.SBSCRB_DE)) > (SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화(미설정 시 365) */
|
WHERE DATEDIFF(NOW(), IFNULL(b.CREAT_DT, a.SBSCRB_DE)) > (SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화(미설정 시 365) */
|
||||||
AND a.MBER_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
AND a.MBER_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
||||||
AND a.MBER_STTUS = 'P'
|
AND a.MBER_STTUS = 'P'
|
||||||
|
LIMIT 2000
|
||||||
]]>
|
]]>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<!-- [#7] 휴면 대상(개인정보취급자) 적재 — COMTNEMPLYRINFO.LAST_LOGIN_DT 기준 -->
|
<!-- [#7] 휴면 대상(개인정보취급자) 적재 — COMTNEMPLYRINFO.LAST_LOGIN_DT 기준
|
||||||
|
2026-07-28: 백로그 최초가동 폭주 방지 LIMIT(위 insertDiapause 주석 참조).
|
||||||
|
2026-07-30: 상한 200 → 2,000 재산정(위 insertDiapause 주석 참조). 유형별로 각각 적용된다. -->
|
||||||
<insert id="loginDAO.insertDiapauseEmplyr" parameterClass="loginVO">
|
<insert id="loginDAO.insertDiapauseEmplyr" parameterClass="loginVO">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
INSERT INTO COMTNDIAPAUSEMBER (MBER_ID, ESNTL_ID, LAST_LOG, DIAPAUSE_STAT, DIAPAUSE_DT, MBER_TY_CODE)
|
INSERT INTO COMTNDIAPAUSEMBER (MBER_ID, ESNTL_ID, LAST_LOG, DIAPAUSE_STAT, DIAPAUSE_DT, MBER_TY_CODE)
|
||||||
@ -240,10 +267,19 @@
|
|||||||
WHERE DATEDIFF(NOW(), IFNULL(e.LAST_LOGIN_DT, e.SBSCRB_DE)) > (SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화(미설정 시 365) */
|
WHERE DATEDIFF(NOW(), IFNULL(e.LAST_LOGIN_DT, e.SBSCRB_DE)) > (SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화(미설정 시 365) */
|
||||||
AND e.EMPLYR_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
AND e.EMPLYR_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
||||||
AND e.EMPLYR_STTUS_CODE = 'P'
|
AND e.EMPLYR_STTUS_CODE = 'P'
|
||||||
|
LIMIT 2000
|
||||||
]]>
|
]]>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<!-- [#8] 휴면 확인 대상(일반회원) — 유예('S')/확정 프로세스용 -->
|
<!-- [#8] 휴면 확인 대상(일반회원) — 유예('S')/확정 프로세스용
|
||||||
|
2026-07-28 버그#4 수정: INSERT는 DIAPAUSE_STAT=''(빈문자열)로 넣는데 기존엔 IS NULL만
|
||||||
|
검사해 신규 대상이 영원히 안 걸리던 문제 수정(빈문자열도 최초상태로 인식).
|
||||||
|
2026-07-30 상한 200 → 5,000 + ORDER BY 추가: 이 statement 하나가 ''→'S'와 'S'→'P' 두
|
||||||
|
전이의 대상을 함께 반환하므로 상한이 적재 상한(2,000)의 2배 미만이면 'S' 상태가 무한
|
||||||
|
적체된다(구 200은 실효 100건/일). ORDER BY 가 없던 탓에 유예 만료 계정과 신규 계정 중
|
||||||
|
어느 쪽이 진행될지 불확정이어서 확정이 정책상 유예기간보다 늦어질 수 있었다 → 오래된
|
||||||
|
DIAPAUSE_DT 우선으로 고정. -->
|
||||||
|
<!-- 주의: 유형별(GNR/EMP)로 각각 5,000 이므로 1회 실행 최대 전이는 10,000 이다. -->
|
||||||
<select id="loginDAO.selectDiapauseList" resultMap="logList">
|
<select id="loginDAO.selectDiapauseList" resultMap="logList">
|
||||||
SELECT A.MBER_ID AS id
|
SELECT A.MBER_ID AS id
|
||||||
, B.ESNTL_ID AS uniqId
|
, B.ESNTL_ID AS uniqId
|
||||||
@ -255,10 +291,13 @@
|
|||||||
FROM COMTNDIAPAUSEMBER A
|
FROM COMTNDIAPAUSEMBER A
|
||||||
LEFT JOIN COMTNGNRLMBER B ON A.MBER_ID = B.MBER_ID
|
LEFT JOIN COMTNGNRLMBER B ON A.MBER_ID = B.MBER_ID
|
||||||
WHERE A.MBER_TY_CODE = 'GNR'
|
WHERE A.MBER_TY_CODE = 'GNR'
|
||||||
AND (A.DIAPAUSE_STAT IS NULL OR (A.DIAPAUSE_STAT = 'S' AND NOW() >= A.DIAPAUSE_DT))
|
AND ((A.DIAPAUSE_STAT IS NULL OR A.DIAPAUSE_STAT = '') OR (A.DIAPAUSE_STAT = 'S' AND NOW() >= A.DIAPAUSE_DT))
|
||||||
|
ORDER BY A.DIAPAUSE_DT
|
||||||
|
LIMIT 5000
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- [#7] 휴면 확인 대상(개인정보취급자) -->
|
<!-- [#7] 휴면 확인 대상(개인정보취급자)
|
||||||
|
2026-07-28 버그#4 수정 + 2026-07-30 상한 5,000·ORDER BY(위 selectDiapauseList 주석 참조). -->
|
||||||
<select id="loginDAO.selectDiapauseListEmplyr" resultMap="logList">
|
<select id="loginDAO.selectDiapauseListEmplyr" resultMap="logList">
|
||||||
SELECT A.MBER_ID AS id
|
SELECT A.MBER_ID AS id
|
||||||
, B.ESNTL_ID AS uniqId
|
, B.ESNTL_ID AS uniqId
|
||||||
@ -270,7 +309,9 @@
|
|||||||
FROM COMTNDIAPAUSEMBER A
|
FROM COMTNDIAPAUSEMBER A
|
||||||
LEFT JOIN COMTNEMPLYRINFO B ON A.MBER_ID = B.EMPLYR_ID
|
LEFT JOIN COMTNEMPLYRINFO B ON A.MBER_ID = B.EMPLYR_ID
|
||||||
WHERE A.MBER_TY_CODE = 'EMP'
|
WHERE A.MBER_TY_CODE = 'EMP'
|
||||||
AND (A.DIAPAUSE_STAT IS NULL OR (A.DIAPAUSE_STAT = 'S' AND NOW() >= A.DIAPAUSE_DT))
|
AND ((A.DIAPAUSE_STAT IS NULL OR A.DIAPAUSE_STAT = '') OR (A.DIAPAUSE_STAT = 'S' AND NOW() >= A.DIAPAUSE_DT))
|
||||||
|
ORDER BY A.DIAPAUSE_DT
|
||||||
|
LIMIT 5000
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- [#8] 휴면 상태 갱신(공통) -->
|
<!-- [#8] 휴면 상태 갱신(공통) -->
|
||||||
|
|||||||
@ -140,6 +140,7 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- KTH. 20190603 -->
|
<!-- KTH. 20190603 -->
|
||||||
|
<!-- [개인정보/#22-1] 잠금 카운트는 RESET_YN='N' 인 당일 실패행만 센다(Mysql 쌍과 동일). -->
|
||||||
<select id="loginDAO.selectFailCount" parameterClass="java.util.HashMap" resultClass="java.lang.Integer" >
|
<select id="loginDAO.selectFailCount" parameterClass="java.util.HashMap" resultClass="java.lang.Integer" >
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@ -148,40 +149,51 @@
|
|||||||
FROM LOGIN_HISTORY
|
FROM LOGIN_HISTORY
|
||||||
WHERE ID = #id#
|
WHERE ID = #id#
|
||||||
AND GUBUN = #userSe#
|
AND GUBUN = #userSe#
|
||||||
|
AND RESET_YN = 'N'
|
||||||
AND TO_CHAR(CREATE_DATE, 'YYYYMMDD') = TO_CHAR(SYSDATE, 'YYYYMMDD')
|
AND TO_CHAR(CREATE_DATE, 'YYYYMMDD') = TO_CHAR(SYSDATE, 'YYYYMMDD')
|
||||||
GROUP BY ID, CREATE_DATE
|
GROUP BY ID, CREATE_DATE
|
||||||
)
|
)
|
||||||
]]>
|
]]>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- KTH. 20190603 -->
|
<!-- KTH. 20190603 / [개인정보/#22-1] DELETE → 소프트 리셋(UPDATE). statement id 유지 -->
|
||||||
<delete id="loginDAO.deleteLoginSuccess">
|
<update id="loginDAO.deleteLoginSuccess">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
DELETE FROM LOGIN_HISTORY
|
UPDATE LOGIN_HISTORY
|
||||||
|
SET RESET_YN = 'Y'
|
||||||
WHERE ID = #id#
|
WHERE ID = #id#
|
||||||
AND GUBUN = #userSe#
|
AND GUBUN = #userSe#
|
||||||
|
AND RESET_YN = 'N'
|
||||||
AND TO_CHAR(CREATE_DATE, 'YYYYMMDD') = TO_CHAR(SYSDATE, 'YYYYMMDD')
|
AND TO_CHAR(CREATE_DATE, 'YYYYMMDD') = TO_CHAR(SYSDATE, 'YYYYMMDD')
|
||||||
]]>
|
]]>
|
||||||
</delete>
|
</update>
|
||||||
|
|
||||||
<!-- KTH. 20190607 -->
|
<!-- KTH. 20190607 / [개인정보/#22-1] 동일 사유로 DELETE → 소프트 리셋 -->
|
||||||
<delete id="loginDAO.deleteLoginSuccessAll">
|
<update id="loginDAO.deleteLoginSuccessAll">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
DELETE FROM LOGIN_HISTORY
|
UPDATE LOGIN_HISTORY
|
||||||
WHERE TO_CHAR(CREATE_DATE, 'YYYYMMDD') = TO_CHAR(SYSDATE, 'YYYYMMDD')
|
SET RESET_YN = 'Y'
|
||||||
|
WHERE RESET_YN = 'N'
|
||||||
|
AND TO_CHAR(CREATE_DATE, 'YYYYMMDD') = TO_CHAR(SYSDATE, 'YYYYMMDD')
|
||||||
]]>
|
]]>
|
||||||
</delete>
|
</update>
|
||||||
|
|
||||||
<!-- KTH. 20190603 -->
|
<!-- KTH. 20190603 / [개인정보/#22-1] 컬럼 명시 + 접속 IP 적재(DDL 과 한 쌍) -->
|
||||||
<insert id="loginDAO.insertFailHistory">
|
<insert id="loginDAO.insertFailHistory" parameterClass="loginVO">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
INSERT INTO LOGIN_HISTORY
|
INSERT INTO LOGIN_HISTORY (
|
||||||
VALUES(
|
ID
|
||||||
|
, CREATE_DATE
|
||||||
|
, GUBUN
|
||||||
|
, CONECT_IP
|
||||||
|
, RESET_YN
|
||||||
|
) VALUES (
|
||||||
#id#
|
#id#
|
||||||
, SYSDATE
|
, SYSDATE
|
||||||
, #userSe#
|
, #userSe#
|
||||||
|
, #ip:VARCHAR#
|
||||||
|
, 'N'
|
||||||
)
|
)
|
||||||
|
|
||||||
]]>
|
]]>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
@ -584,6 +596,7 @@
|
|||||||
WHERE NVL(SYSDATE - b.CREAT_DT,SYSDATE - a.SBSCRB_DE) > (SELECT NVL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화 */
|
WHERE NVL(SYSDATE - b.CREAT_DT,SYSDATE - a.SBSCRB_DE) > (SELECT NVL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화 */
|
||||||
AND a.MBER_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
AND a.MBER_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
||||||
AND a.MBER_STTUS = 'P'
|
AND a.MBER_STTUS = 'P'
|
||||||
|
AND ROWNUM <= 2000 /* 2026-07-28 백로그 최초가동 폭주 방지, 07-30 상한 200→2,000 재산정 */
|
||||||
]]>
|
]]>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
@ -603,11 +616,16 @@
|
|||||||
WHERE NVL(SYSDATE - e.LAST_LOGIN_DT, SYSDATE - e.SBSCRB_DE) > (SELECT NVL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화 */
|
WHERE NVL(SYSDATE - e.LAST_LOGIN_DT, SYSDATE - e.SBSCRB_DE) > (SELECT NVL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화 */
|
||||||
AND e.EMPLYR_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
AND e.EMPLYR_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
|
||||||
AND e.EMPLYR_STTUS_CODE = 'P'
|
AND e.EMPLYR_STTUS_CODE = 'P'
|
||||||
|
AND ROWNUM <= 2000 /* 2026-07-28 백로그 최초가동 폭주 방지, 07-30 상한 200→2,000 재산정 */
|
||||||
]]>
|
]]>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<!-- [개인정보보호 #7] 휴면 확인 대상(개인정보취급자) -->
|
<!-- [개인정보보호 #7] 휴면 확인 대상(개인정보취급자)
|
||||||
|
2026-07-30: 상한 200 → 5,000 재산정 + 오래된 DIAPAUSE_DT 우선(Mysql 쌍 주석 참조).
|
||||||
|
Oracle 은 ROWNUM 이 ORDER BY 보다 먼저 평가되므로 정렬은 인라인뷰 안에서 수행해야 한다.
|
||||||
|
resultMap logList 는 columnIndex 기반이라 SELECT * 로 컬럼 순서를 그대로 보존한다. -->
|
||||||
<select id="loginDAO.selectDiapauseListEmplyr" resultMap="logList">
|
<select id="loginDAO.selectDiapauseListEmplyr" resultMap="logList">
|
||||||
|
SELECT * FROM (
|
||||||
SELECT A.MBER_ID AS id
|
SELECT A.MBER_ID AS id
|
||||||
,B.ESNTL_ID AS uniqId
|
,B.ESNTL_ID AS uniqId
|
||||||
,B.EMAIL_ADRES AS email
|
,B.EMAIL_ADRES AS email
|
||||||
@ -619,7 +637,10 @@
|
|||||||
LEFT JOIN COMTNEMPLYRINFO B
|
LEFT JOIN COMTNEMPLYRINFO B
|
||||||
ON A.MBER_ID = B.EMPLYR_ID
|
ON A.MBER_ID = B.EMPLYR_ID
|
||||||
WHERE A.MBER_TY_CODE = 'EMP'
|
WHERE A.MBER_TY_CODE = 'EMP'
|
||||||
AND (A.DIAPAUSE_STAT IS NULL OR (A.DIAPAUSE_STAT = 'S' AND SYSDATE >= A.DIAPAUSE_DT))
|
AND (A.DIAPAUSE_STAT IS NULL OR (A.DIAPAUSE_STAT = 'S' AND SYSDATE >= A.DIAPAUSE_DT))
|
||||||
|
ORDER BY A.DIAPAUSE_DT
|
||||||
|
)
|
||||||
|
WHERE ROWNUM <= 5000
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<delete id="loginDAO.deleteDiapause" parameterClass="loginVO">
|
<delete id="loginDAO.deleteDiapause" parameterClass="loginVO">
|
||||||
@ -647,8 +668,13 @@
|
|||||||
AND ESNTL_ID = #esntl_id#
|
AND ESNTL_ID = #esntl_id#
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<!-- 휴먼고객 확인 -->
|
<!-- 휴먼고객 확인
|
||||||
|
2026-07-30: 상한 200 → 5,000 재산정 + 오래된 DIAPAUSE_DT 우선(위 selectDiapauseListEmplyr 주석 참조).
|
||||||
|
※ 미해결: 아래 SELECT 절 순서(id, email, uniqId, ...)가 resultMap logList 의 columnIndex
|
||||||
|
(1=id, 2=uniqId, 3=email)와 어긋나 email/uniqId 가 서로 바뀐다. Oracle 경로는 현재
|
||||||
|
sql-map-config 에 등록되지 않아(Mysql 만 로드) 실행되지 않으므로 이번 변경 범위에서 제외. -->
|
||||||
<select id="loginDAO.selectDiapauseList" resultMap="logList">
|
<select id="loginDAO.selectDiapauseList" resultMap="logList">
|
||||||
|
SELECT * FROM (
|
||||||
SELECT A.MBER_ID AS id
|
SELECT A.MBER_ID AS id
|
||||||
,B.MBER_EMAIL_ADRES AS email
|
,B.MBER_EMAIL_ADRES AS email
|
||||||
,B.ESNTL_ID AS uniqId
|
,B.ESNTL_ID AS uniqId
|
||||||
@ -660,7 +686,10 @@
|
|||||||
LEFT JOIN COMTNGNRLMBER B
|
LEFT JOIN COMTNGNRLMBER B
|
||||||
ON A.MBER_ID = B.MBER_ID
|
ON A.MBER_ID = B.MBER_ID
|
||||||
WHERE A.MBER_TY_CODE = 'GNR'
|
WHERE A.MBER_TY_CODE = 'GNR'
|
||||||
AND (DIAPAUSE_STAT IS NULL OR (DIAPAUSE_STAT = 'S' AND SYSDATE >= DIAPAUSE_DT))
|
AND (DIAPAUSE_STAT IS NULL OR (DIAPAUSE_STAT = 'S' AND SYSDATE >= DIAPAUSE_DT))
|
||||||
|
ORDER BY A.DIAPAUSE_DT
|
||||||
|
)
|
||||||
|
WHERE ROWNUM <= 5000
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<update id="loginDAO.updateDiapauseMber" parameterClass="java.util.HashMap">
|
<update id="loginDAO.updateDiapauseMber" parameterClass="java.util.HashMap">
|
||||||
|
|||||||
@ -129,4 +129,10 @@
|
|||||||
|
|
||||||
<!-- 개인정보보호(SFR-001 요건4) - 메뉴권한 기반 URL 접근제어(#2, 롤→허용 /mnt/ URL) -->
|
<!-- 개인정보보호(SFR-001 요건4) - 메뉴권한 기반 URL 접근제어(#2, 롤→허용 /mnt/ URL) -->
|
||||||
<sqlMap resource="egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Mysql.xml"/>
|
<sqlMap resource="egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Mysql.xml"/>
|
||||||
|
|
||||||
|
<!-- 개인정보보호(SFR-002) - 장기 미사용 계정 개인정보 파기(#12, 보존기간 DELETE_PRESERVE_DAY 정책값) -->
|
||||||
|
<sqlMap resource="egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountDelete_SQL_Mysql.xml"/>
|
||||||
|
|
||||||
|
<!-- 개인정보보호(SER-001) - 인증 실패 이력 조회·파기(#22-1, IP 기록 + LOGIN_HIST_KEEP_DAY 보관기간) -->
|
||||||
|
<sqlMap resource="egovframework/sqlmap/site/mnt/biz/privacy/PrivacyLoginFailHistory_SQL_Mysql.xml"/>
|
||||||
</sqlMapConfig>
|
</sqlMapConfig>
|
||||||
|
|||||||
@ -20,6 +20,8 @@
|
|||||||
ALERT_D3 AS alertD3,
|
ALERT_D3 AS alertD3,
|
||||||
DELETE_PRESERVE_DAY AS deletePreserveDay,
|
DELETE_PRESERVE_DAY AS deletePreserveDay,
|
||||||
BATCH_CYCLE AS batchCycle,
|
BATCH_CYCLE AS batchCycle,
|
||||||
|
PURGE_USE_YN AS purgeUseYn,
|
||||||
|
LOGIN_HIST_KEEP_DAY AS loginHistKeepDay,
|
||||||
ADMIN_EMAIL AS adminEmail,
|
ADMIN_EMAIL AS adminEmail,
|
||||||
UPD_ID AS updId,
|
UPD_ID AS updId,
|
||||||
UPD_DT AS updDt
|
UPD_DT AS updDt
|
||||||
@ -38,6 +40,8 @@
|
|||||||
ALERT_D3 = #alertD3#,
|
ALERT_D3 = #alertD3#,
|
||||||
DELETE_PRESERVE_DAY = #deletePreserveDay#,
|
DELETE_PRESERVE_DAY = #deletePreserveDay#,
|
||||||
BATCH_CYCLE = #batchCycle#,
|
BATCH_CYCLE = #batchCycle#,
|
||||||
|
PURGE_USE_YN = #purgeUseYn#,
|
||||||
|
LOGIN_HIST_KEEP_DAY = #loginHistKeepDay#,
|
||||||
ADMIN_EMAIL = #adminEmail#,
|
ADMIN_EMAIL = #adminEmail#,
|
||||||
UPD_ID = #updId#,
|
UPD_ID = #updId#,
|
||||||
UPD_DT = NOW()
|
UPD_DT = NOW()
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
SELECT POLICY_ID AS policyId, DORMANT_DAY AS dormantDay,
|
SELECT POLICY_ID AS policyId, DORMANT_DAY AS dormantDay,
|
||||||
ALERT_D1 AS alertD1, ALERT_D2 AS alertD2, ALERT_D3 AS alertD3,
|
ALERT_D1 AS alertD1, ALERT_D2 AS alertD2, ALERT_D3 AS alertD3,
|
||||||
DELETE_PRESERVE_DAY AS deletePreserveDay, BATCH_CYCLE AS batchCycle,
|
DELETE_PRESERVE_DAY AS deletePreserveDay, BATCH_CYCLE AS batchCycle,
|
||||||
|
PURGE_USE_YN AS purgeUseYn,
|
||||||
|
LOGIN_HIST_KEEP_DAY AS loginHistKeepDay,
|
||||||
ADMIN_EMAIL AS adminEmail, UPD_ID AS updId, UPD_DT AS updDt
|
ADMIN_EMAIL AS adminEmail, UPD_ID AS updId, UPD_DT AS updDt
|
||||||
FROM ACCOUNT_MNG_POLICY WHERE POLICY_ID = 1
|
FROM ACCOUNT_MNG_POLICY WHERE POLICY_ID = 1
|
||||||
]]>
|
]]>
|
||||||
@ -24,7 +26,9 @@
|
|||||||
UPDATE ACCOUNT_MNG_POLICY
|
UPDATE ACCOUNT_MNG_POLICY
|
||||||
SET DORMANT_DAY = #dormantDay#, ALERT_D1 = #alertD1#, ALERT_D2 = #alertD2#,
|
SET DORMANT_DAY = #dormantDay#, ALERT_D1 = #alertD1#, ALERT_D2 = #alertD2#,
|
||||||
ALERT_D3 = #alertD3#, DELETE_PRESERVE_DAY = #deletePreserveDay#,
|
ALERT_D3 = #alertD3#, DELETE_PRESERVE_DAY = #deletePreserveDay#,
|
||||||
BATCH_CYCLE = #batchCycle#, ADMIN_EMAIL = #adminEmail#,
|
BATCH_CYCLE = #batchCycle#, PURGE_USE_YN = #purgeUseYn#,
|
||||||
|
LOGIN_HIST_KEEP_DAY = #loginHistKeepDay#,
|
||||||
|
ADMIN_EMAIL = #adminEmail#,
|
||||||
UPD_ID = #updId#, UPD_DT = SYSDATE
|
UPD_ID = #updId#, UPD_DT = SYSDATE
|
||||||
WHERE POLICY_ID = 1
|
WHERE POLICY_ID = 1
|
||||||
]]>
|
]]>
|
||||||
|
|||||||
@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
개인정보보호 기능개선 사업 (SER-001, 인증 실패 이력) — 개발항목 #22-1.
|
||||||
|
· 조회 : LOGIN_HISTORY 계정·IP·기간 검색 + 페이징 (관리자 화면)
|
||||||
|
· 파기 : ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY 경과분 삭제 (배치)
|
||||||
|
|
||||||
|
[적재·리셋 statement 는 여기 없다] insertFailHistory / deleteLoginSuccess 는
|
||||||
|
로그인 흐름 소유(EgovLoginUsr_SQL_Mysql.xml, namespace loginDAO)에 있다.
|
||||||
|
이중 경로 방지를 위해 이 파일에는 조회·파기만 둔다.
|
||||||
|
|
||||||
|
[RESET_YN] 'Y' = 이후 로그인 성공으로 잠금카운터가 리셋된 행(이력은 보존).
|
||||||
|
'N' = 유효(잠금 카운트 대상). 화면에서는 둘 다 보여준다.
|
||||||
|
-->
|
||||||
|
<sqlMap namespace="LoginFailHistoryDAO">
|
||||||
|
|
||||||
|
<typeAlias alias="loginFailHistoryVO" type="egovframework.com.cmm.privacy.lock.LoginFailHistoryVO"/>
|
||||||
|
|
||||||
|
<!-- 검색조건 공통(목록·건수 동일 적용) -->
|
||||||
|
<sql id="LoginFailHistoryDAO.searchCondition">
|
||||||
|
<dynamic prepend="WHERE">
|
||||||
|
<isNotEmpty prepend="AND" property="searchId">
|
||||||
|
h.ID LIKE CONCAT('%', #searchId#, '%')
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchGubun">
|
||||||
|
h.GUBUN = #searchGubun#
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchIp">
|
||||||
|
h.CONECT_IP LIKE CONCAT('%', #searchIp#, '%')
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchResetYn">
|
||||||
|
h.RESET_YN = #searchResetYn#
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchBgnDe">
|
||||||
|
h.CREATE_DATE >= CONCAT(#searchBgnDe#, ' 00:00:00')
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchEndDe">
|
||||||
|
h.CREATE_DATE <= CONCAT(#searchEndDe#, ' 23:59:59')
|
||||||
|
</isNotEmpty>
|
||||||
|
</dynamic>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<!-- ================= 목록 조회 ================= -->
|
||||||
|
<!-- 성명은 구분별 회원 테이블에서 보조 조회(없으면 null — 존재하지 않는 계정으로의
|
||||||
|
시도도 이력에 남기 때문에 매칭 실패가 정상 케이스다). -->
|
||||||
|
<select id="LoginFailHistoryDAO.selectLoginFailHistoryList"
|
||||||
|
parameterClass="loginFailHistoryVO"
|
||||||
|
resultClass="loginFailHistoryVO">
|
||||||
|
SELECT h.ID AS id
|
||||||
|
, h.GUBUN AS gubun
|
||||||
|
, DATE_FORMAT(h.CREATE_DATE, '%Y-%m-%d %H:%i:%s') AS createDate
|
||||||
|
, h.CONECT_IP AS conectIp
|
||||||
|
, h.RESET_YN AS resetYn
|
||||||
|
, CASE h.GUBUN
|
||||||
|
WHEN 'USR' THEN (SELECT e.USER_NM FROM COMTNEMPLYRINFO e WHERE e.EMPLYR_ID = h.ID)
|
||||||
|
WHEN 'GNR' THEN (SELECT g.MBER_NM FROM COMTNGNRLMBER g WHERE g.MBER_ID = h.ID)
|
||||||
|
ELSE NULL
|
||||||
|
END AS userNm
|
||||||
|
FROM LOGIN_HISTORY h
|
||||||
|
<include refid="LoginFailHistoryDAO.searchCondition"/>
|
||||||
|
ORDER BY h.CREATE_DATE DESC
|
||||||
|
LIMIT #firstIndex#, #recordCountPerPage#
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="LoginFailHistoryDAO.selectLoginFailHistoryListCount"
|
||||||
|
parameterClass="loginFailHistoryVO"
|
||||||
|
resultClass="java.lang.Integer">
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM LOGIN_HISTORY h
|
||||||
|
<include refid="LoginFailHistoryDAO.searchCondition"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- ================= 보관기간 정책값 ================= -->
|
||||||
|
<!-- 정책행이 없거나 컬럼이 0이면 730(2년) fallback. -->
|
||||||
|
<select id="LoginFailHistoryDAO.selectLoginHistKeepDay" resultClass="java.lang.Integer">
|
||||||
|
<![CDATA[
|
||||||
|
SELECT IFNULL(MAX(LOGIN_HIST_KEEP_DAY), 730)
|
||||||
|
FROM ACCOUNT_MNG_POLICY
|
||||||
|
]]>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- ================= 보관기간 경과분 파기(배치) ================= -->
|
||||||
|
<!-- 1회 처리 상한(limitCnt)으로 백로그 폭주 방지. 잔여분은 다음 회차에서 처리. -->
|
||||||
|
<delete id="LoginFailHistoryDAO.deleteExpiredLoginFailHistory" parameterClass="java.util.HashMap">
|
||||||
|
<![CDATA[
|
||||||
|
DELETE FROM LOGIN_HISTORY
|
||||||
|
WHERE CREATE_DATE < DATE_SUB(NOW(), INTERVAL #keepDay# DAY)
|
||||||
|
ORDER BY CREATE_DATE
|
||||||
|
LIMIT #limitCnt#
|
||||||
|
]]>
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</sqlMap>
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
개인정보보호 기능개선 사업 (SER-001, 인증 실패 이력) — 개발항목 #22-1. Oracle 쌍.
|
||||||
|
운영 DB 는 MySQL(csv) 이며 이 파일은 Mysql 쌍 관리 규약(이중 관리)에 따른 대응본.
|
||||||
|
· 페이징: MySQL LIMIT → ROWNUM 2단 인라인뷰
|
||||||
|
· 파기 : LIMIT 상한 → ROWNUM 조건
|
||||||
|
-->
|
||||||
|
<sqlMap namespace="LoginFailHistoryDAO">
|
||||||
|
|
||||||
|
<typeAlias alias="loginFailHistoryVO" type="egovframework.com.cmm.privacy.lock.LoginFailHistoryVO"/>
|
||||||
|
|
||||||
|
<sql id="LoginFailHistoryDAO.searchCondition">
|
||||||
|
<dynamic prepend="WHERE">
|
||||||
|
<isNotEmpty prepend="AND" property="searchId">
|
||||||
|
h.ID LIKE '%' || #searchId# || '%'
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchGubun">
|
||||||
|
h.GUBUN = #searchGubun#
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchIp">
|
||||||
|
h.CONECT_IP LIKE '%' || #searchIp# || '%'
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchResetYn">
|
||||||
|
h.RESET_YN = #searchResetYn#
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchBgnDe">
|
||||||
|
h.CREATE_DATE >= TO_DATE(#searchBgnDe# || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||||
|
</isNotEmpty>
|
||||||
|
<isNotEmpty prepend="AND" property="searchEndDe">
|
||||||
|
h.CREATE_DATE <= TO_DATE(#searchEndDe# || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||||
|
</isNotEmpty>
|
||||||
|
</dynamic>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="LoginFailHistoryDAO.selectLoginFailHistoryList"
|
||||||
|
parameterClass="loginFailHistoryVO"
|
||||||
|
resultClass="loginFailHistoryVO">
|
||||||
|
SELECT id, gubun, createDate, conectIp, resetYn, userNm
|
||||||
|
FROM (
|
||||||
|
SELECT ROWNUM AS rnum, a.*
|
||||||
|
FROM (
|
||||||
|
SELECT h.ID AS id
|
||||||
|
, h.GUBUN AS gubun
|
||||||
|
, TO_CHAR(h.CREATE_DATE, 'YYYY-MM-DD HH24:MI:SS') AS createDate
|
||||||
|
, h.CONECT_IP AS conectIp
|
||||||
|
, h.RESET_YN AS resetYn
|
||||||
|
, CASE h.GUBUN
|
||||||
|
WHEN 'USR' THEN (SELECT e.USER_NM FROM COMTNEMPLYRINFO e WHERE e.EMPLYR_ID = h.ID)
|
||||||
|
WHEN 'GNR' THEN (SELECT g.MBER_NM FROM COMTNGNRLMBER g WHERE g.MBER_ID = h.ID)
|
||||||
|
ELSE NULL
|
||||||
|
END AS userNm
|
||||||
|
FROM LOGIN_HISTORY h
|
||||||
|
<include refid="LoginFailHistoryDAO.searchCondition"/>
|
||||||
|
ORDER BY h.CREATE_DATE DESC
|
||||||
|
) a
|
||||||
|
WHERE ROWNUM <= #firstIndex# + #recordCountPerPage#
|
||||||
|
)
|
||||||
|
WHERE rnum > #firstIndex#
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="LoginFailHistoryDAO.selectLoginFailHistoryListCount"
|
||||||
|
parameterClass="loginFailHistoryVO"
|
||||||
|
resultClass="java.lang.Integer">
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM LOGIN_HISTORY h
|
||||||
|
<include refid="LoginFailHistoryDAO.searchCondition"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="LoginFailHistoryDAO.selectLoginHistKeepDay" resultClass="java.lang.Integer">
|
||||||
|
<![CDATA[
|
||||||
|
SELECT NVL(MAX(LOGIN_HIST_KEEP_DAY), 730)
|
||||||
|
FROM ACCOUNT_MNG_POLICY
|
||||||
|
]]>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<delete id="LoginFailHistoryDAO.deleteExpiredLoginFailHistory" parameterClass="java.util.HashMap">
|
||||||
|
<![CDATA[
|
||||||
|
DELETE FROM LOGIN_HISTORY
|
||||||
|
WHERE CREATE_DATE < SYSDATE - #keepDay#
|
||||||
|
AND ROWNUM <= #limitCnt#
|
||||||
|
]]>
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</sqlMap>
|
||||||
@ -48,13 +48,13 @@
|
|||||||
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100041'
|
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100041'
|
||||||
|| param.page eq '9100042'|| param.page eq '9100043'
|
|| param.page eq '9100042'|| param.page eq '9100043'
|
||||||
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
||||||
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq '9100035' || param.page eq ''}">class="on"</c:if>><a href="#none">시스템관리</a>
|
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq '9100035' || param.page eq '9100036' || param.page eq '9100037' || param.page eq '9100038' || param.page eq '9100039' || param.page eq ''}">class="on"</c:if>><a href="#none">시스템관리</a>
|
||||||
<!-- 2 Depth -->
|
<!-- 2 Depth -->
|
||||||
<ul <c:if test="${param.page eq '9100012'|| param.page eq '9100012' || param.page eq '9100020' || param.page eq '9100021'
|
<ul <c:if test="${param.page eq '9100012'|| param.page eq '9100012' || param.page eq '9100020' || param.page eq '9100021'
|
||||||
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034'|| param.page eq '9100041'
|
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034'|| param.page eq '9100041'
|
||||||
|| param.page eq '9100042'|| param.page eq '9100043'
|
|| param.page eq '9100042'|| param.page eq '9100043'
|
||||||
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
||||||
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq '9100035' || param.page eq ''}">style="display: block;"</c:if>>
|
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq '9100035' || param.page eq '9100036' || param.page eq '9100037' || param.page eq '9100038' || param.page eq '9100039' || param.page eq ''}">style="display: block;"</c:if>>
|
||||||
<c:forEach items="${list_menulist}" var="menuUpperVo2">
|
<c:forEach items="${list_menulist}" var="menuUpperVo2">
|
||||||
<c:if test="${menuUpperVo2.menuNo eq '9100010'}">
|
<c:if test="${menuUpperVo2.menuNo eq '9100010'}">
|
||||||
<li <c:if test="${param.page eq '9100012'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
<li <c:if test="${param.page eq '9100012'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
||||||
@ -94,10 +94,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</c:if>
|
</c:if>
|
||||||
<c:if test="${menuUpperVo2.menuNo eq '9100030'}">
|
<c:if test="${menuUpperVo2.menuNo eq '9100030'}">
|
||||||
<li <c:if test="${param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100035'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
<li <c:if test="${param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100035' || param.page eq '9100036' || param.page eq '9100037' || param.page eq '9100038' || param.page eq '9100039'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
||||||
<c:forEach items="${list_menulist}" var="menuUpperVo3">
|
<c:forEach items="${list_menulist}" var="menuUpperVo3">
|
||||||
<c:if test="${menuUpperVo3.menuNo eq '9100030'}">
|
<c:if test="${menuUpperVo3.menuNo eq '9100030'}">
|
||||||
<ul <c:if test="${param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100035'}">style="display: block;"</c:if>>
|
<ul <c:if test="${param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100035' || param.page eq '9100036' || param.page eq '9100037' || param.page eq '9100038' || param.page eq '9100039'}">style="display: block;"</c:if>>
|
||||||
<c:forEach items="${list_menulist}" var="menuUpperVo4">
|
<c:forEach items="${list_menulist}" var="menuUpperVo4">
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${menuUpperVo4.menuNo eq '9100031'}">
|
<c:when test="${menuUpperVo4.menuNo eq '9100031'}">
|
||||||
@ -109,6 +109,19 @@
|
|||||||
<c:when test="${menuUpperVo4.menuNo eq '9100035'}">
|
<c:when test="${menuUpperVo4.menuNo eq '9100035'}">
|
||||||
<li <c:if test="${param.page eq '9100035'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
<li <c:if test="${param.page eq '9100035'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||||
</c:when>
|
</c:when>
|
||||||
|
<c:when test="${menuUpperVo4.menuNo eq '9100036'}">
|
||||||
|
<li <c:if test="${param.page eq '9100036'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||||
|
</c:when>
|
||||||
|
<c:when test="${menuUpperVo4.menuNo eq '9100037'}">
|
||||||
|
<li <c:if test="${param.page eq '9100037'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||||
|
</c:when>
|
||||||
|
<c:when test="${menuUpperVo4.menuNo eq '9100038'}">
|
||||||
|
<li <c:if test="${param.page eq '9100038'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||||
|
</c:when>
|
||||||
|
<%-- [개인정보/#22-1] 인증 실패 이력 조회 --%>
|
||||||
|
<c:when test="${menuUpperVo4.menuNo eq '9100039'}">
|
||||||
|
<li <c:if test="${param.page eq '9100039'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||||
|
</c:when>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</c:forEach>
|
</c:forEach>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -0,0 +1,218 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%-- Head --%>
|
||||||
|
<c:import url = "/mnt/include/head.do" /><%--// Head --%>
|
||||||
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
|
||||||
|
<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
|
||||||
|
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||||
|
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
|
||||||
|
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
|
||||||
|
|
||||||
|
<c:set var="PATH" value="/mnt" />
|
||||||
|
<script type="text/javascript" src="<c:url value='/js/egovframework/com/cmm/jquery.js'/>"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
<!--
|
||||||
|
function fn_num(v) {
|
||||||
|
v = $.trim(v);
|
||||||
|
if (v.length === 0 || !/^[0-9]+$/.test(v)) { return -1; }
|
||||||
|
return parseInt(v, 10);
|
||||||
|
}
|
||||||
|
function fn_save() {
|
||||||
|
var frm = document.frm;
|
||||||
|
|
||||||
|
var dormantDay = fn_num(frm.dormantDay.value);
|
||||||
|
if (dormantDay < 1 || dormantDay > 3650) {
|
||||||
|
alert("휴면 전환 기준일수는 1~3650일 범위의 숫자로 입력하세요.");
|
||||||
|
frm.dormantDay.focus(); return;
|
||||||
|
}
|
||||||
|
var d1 = fn_num(frm.alertD1.value);
|
||||||
|
var d2 = fn_num(frm.alertD2.value);
|
||||||
|
var d3 = fn_num(frm.alertD3.value);
|
||||||
|
if (d1 < 0 || d2 < 0 || d3 < 0) {
|
||||||
|
alert("사전알림 시점은 0 이상의 숫자로 입력하세요.");
|
||||||
|
frm.alertD1.focus(); return;
|
||||||
|
}
|
||||||
|
if (d1 >= dormantDay || d2 >= dormantDay || d3 >= dormantDay) {
|
||||||
|
alert("사전알림 시점은 휴면 기준일수보다 작아야 합니다.");
|
||||||
|
frm.alertD1.focus(); return;
|
||||||
|
}
|
||||||
|
if (d1 <= d2 || d2 <= d3) {
|
||||||
|
alert("사전알림 시점은 1차 > 2차 > 3차 순서로 입력하세요.\n(예: 30 / 7 / 0)");
|
||||||
|
frm.alertD1.focus(); return;
|
||||||
|
}
|
||||||
|
var preserve = fn_num(frm.deletePreserveDay.value);
|
||||||
|
if (preserve < 1 || preserve > 3650) {
|
||||||
|
alert("파기 기준일은 1~3650일 범위의 숫자로 입력하세요.");
|
||||||
|
frm.deletePreserveDay.focus(); return;
|
||||||
|
}
|
||||||
|
// 파기는 복구 불가 — '사용'으로 켤 때만 별도 확인
|
||||||
|
if (frm.purgeUseYn.value === "Y") {
|
||||||
|
if (!confirm("계정 파기를 '사용'으로 설정합니다.\n\n"
|
||||||
|
+ "휴면 전환 후 " + preserve + "일이 지난 계정의 개인정보(성명·연락처·주소 등)가\n"
|
||||||
|
+ "매일 02:30 배치로 파기되며, 파기된 정보는 복구할 수 없습니다.\n\n계속하시겠습니까?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [개인정보/#22-1] 인증 실패 이력 보관기간 — 법정 최소 보관(2년) 미만이면 경고 후 확인
|
||||||
|
var keepDay = fn_num(frm.loginHistKeepDay.value);
|
||||||
|
if (keepDay < 1 || keepDay > 3650) {
|
||||||
|
alert("인증 실패 이력 보관기간은 1~3650일 범위의 숫자로 입력하세요.");
|
||||||
|
frm.loginHistKeepDay.focus(); return;
|
||||||
|
}
|
||||||
|
if (keepDay < 730) {
|
||||||
|
if (!confirm("인증 실패 이력 보관기간을 " + keepDay + "일로 설정합니다.\n\n"
|
||||||
|
+ "권장값은 730일(2년)입니다. 더 짧게 설정하면 지난 침해 시도 흔적을\n"
|
||||||
|
+ "그만큼 일찍 잃게 됩니다.\n\n계속하시겠습니까?")) {
|
||||||
|
frm.loginHistKeepDay.focus(); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var email = $.trim(frm.adminEmail.value);
|
||||||
|
if (email.length > 0 && !/^[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}$/.test(email)) {
|
||||||
|
alert("관리자 수신 이메일 형식이 올바르지 않습니다.");
|
||||||
|
frm.adminEmail.focus(); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm("계정 관리 정책을 저장하시겠습니까?\n저장 즉시 휴면 판정·사전알림 배치에 반영됩니다.")) { return; }
|
||||||
|
|
||||||
|
$.post("<c:url value='${PATH}/privacy/accountPolicyUpdate.do' />", $(frm).serialize(), function(data) {
|
||||||
|
if (data && data.result === "success") {
|
||||||
|
alert("저장되었습니다.");
|
||||||
|
location.href = "<c:url value='${PATH}/sec/accountPolicy/policyView.do' />?page=9100038";
|
||||||
|
} else {
|
||||||
|
alert("저장에 실패하였습니다.\n" + (data && data.message ? data.message : ""));
|
||||||
|
}
|
||||||
|
}, "json").fail(function() { alert("저장 처리 중 오류가 발생하였습니다."); });
|
||||||
|
}
|
||||||
|
function fn_reload() {
|
||||||
|
location.href = "<c:url value='${PATH}/sec/accountPolicy/policyView.do' />?page=9100038";
|
||||||
|
}
|
||||||
|
//-->
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
.policy_guide { margin:0 0 10px 0; padding:10px 12px; border:1px solid #d6d6d6; background:#f7f9fb; color:#444; font-size:12px; line-height:1.7; }
|
||||||
|
.policy_guide strong { color:#005ea8; }
|
||||||
|
.unit { margin-left:4px; color:#666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- 타이틀 -->
|
||||||
|
<c:import url = "/mnt/include/navigation.do" />
|
||||||
|
<!-- //타이틀 -->
|
||||||
|
<!-- 컨텐츠 영역 -->
|
||||||
|
|
||||||
|
<div class="policy_guide">
|
||||||
|
<strong>장기 미사용 계정 관리 정책(SFR-002)</strong> — 아래 설정값은 저장 즉시 휴면 전환 판정·사전알림 발송 배치에 반영되며,
|
||||||
|
재배포가 필요하지 않습니다. 사전알림은 휴면 예정일 기준 <strong>D-1차 > D-2차 > D-3차</strong> 순서로 발송됩니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form:form commandName="policy" id="frm" name="frm" method="post" onsubmit="fn_save(); return false;">
|
||||||
|
<form:hidden path="policyId" />
|
||||||
|
|
||||||
|
<table summary="계정 관리 정책 설정" class="write">
|
||||||
|
<caption>계정 관리 정책 설정</caption>
|
||||||
|
<colgroup>
|
||||||
|
<col width="25%" />
|
||||||
|
<col width="*" />
|
||||||
|
</colgroup>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th><label for="dormantDay">휴면 전환 기준일수</label> *</th>
|
||||||
|
<td>
|
||||||
|
<form:input path="dormantDay" title="휴면 전환 기준일수" class="tbox" maxlength="4" cssStyle="width:80px; text-align:right;" />
|
||||||
|
<span class="unit">일 이상 미접속 시 휴면 전환 (1~3650)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="alertD1">사전알림 1차</label> *</th>
|
||||||
|
<td>
|
||||||
|
휴면 예정일 D-<form:input path="alertD1" title="사전알림 1차" class="tbox" maxlength="4" cssStyle="width:60px; text-align:right;" />
|
||||||
|
<span class="unit">일 (기본 30)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="alertD2">사전알림 2차</label> *</th>
|
||||||
|
<td>
|
||||||
|
휴면 예정일 D-<form:input path="alertD2" title="사전알림 2차" class="tbox" maxlength="4" cssStyle="width:60px; text-align:right;" />
|
||||||
|
<span class="unit">일 (기본 7)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="alertD3">사전알림 3차</label> *</th>
|
||||||
|
<td>
|
||||||
|
휴면 예정일 D-<form:input path="alertD3" title="사전알림 3차" class="tbox" maxlength="4" cssStyle="width:60px; text-align:right;" />
|
||||||
|
<span class="unit">일 (기본 0 = 당일)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="deletePreserveDay">파기 기준일(삭제 보존기간)</label> *</th>
|
||||||
|
<td>
|
||||||
|
<form:input path="deletePreserveDay" title="파기 기준일(삭제 보존기간)" class="tbox" maxlength="4" cssStyle="width:80px; text-align:right;" />
|
||||||
|
<span class="unit">일 (휴면 전환 후 이 기간이 지나면 개인정보 파기 대상)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="purgeUseYn">계정 파기 사용여부</label> *</th>
|
||||||
|
<td>
|
||||||
|
<select name="purgeUseYn" id="purgeUseYn" title="계정 파기 사용여부">
|
||||||
|
<option value="N" <c:if test="${policy.purgeUseYn ne 'Y'}">selected="selected"</c:if>>미사용(파기 안 함)</option>
|
||||||
|
<option value="Y" <c:if test="${policy.purgeUseYn eq 'Y'}">selected="selected"</c:if>>사용(파기 수행)</option>
|
||||||
|
</select>
|
||||||
|
<span class="unit">'사용' 선택 시 매일 02:30 배치가 위 기준일 경과 휴면계정의 개인정보를 파기합니다(1회 최대 200건).
|
||||||
|
<strong>파기된 개인정보는 복구할 수 없습니다.</strong></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<%-- [개인정보/#22-1] 인증 실패 이력 보관기간 --%>
|
||||||
|
<tr>
|
||||||
|
<th><label for="loginHistKeepDay">인증 실패 이력 보관기간</label> *</th>
|
||||||
|
<td>
|
||||||
|
<form:input path="loginHistKeepDay" title="인증 실패 이력 보관기간" class="tbox" maxlength="4" cssStyle="width:80px; text-align:right;" />
|
||||||
|
<span class="unit">일 (기본 730 = 2년). 매일 03:30 배치가 이 기간을 넘긴 로그인 실패 이력을 파기합니다.
|
||||||
|
이력은 [시스템관리 > 권한관리 > <strong>인증 실패 이력</strong>] 화면에서 조회합니다.</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="batchCycle">배치 주기</label> *</th>
|
||||||
|
<td>
|
||||||
|
<select name="batchCycle" id="batchCycle" title="배치 주기">
|
||||||
|
<option value="DAILY" <c:if test="${policy.batchCycle eq 'DAILY'}">selected="selected"</c:if>>매일(DAILY)</option>
|
||||||
|
<option value="WEEKLY" <c:if test="${policy.batchCycle eq 'WEEKLY'}">selected="selected"</c:if>>매주(WEEKLY)</option>
|
||||||
|
<option value="MONTHLY" <c:if test="${policy.batchCycle eq 'MONTHLY'}">selected="selected"</c:if>>매월(MONTHLY)</option>
|
||||||
|
</select>
|
||||||
|
<span class="unit">휴면 판정 배치 실행 주기</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="adminEmail">관리자 수신 이메일</label></th>
|
||||||
|
<td>
|
||||||
|
<form:input path="adminEmail" title="관리자 수신 이메일" class="tbox" maxlength="200" cssStyle="width:60%;" />
|
||||||
|
<span class="unit">사전알림 관리자 사본 수신 주소 (미입력 시 관리자 알림 미발송)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>최종 수정</th>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${empty policy.updDt}">-</c:when>
|
||||||
|
<c:otherwise><c:out value="${policy.updDt}" /> / <c:out value="${policy.updId}" /></c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- 버튼 -->
|
||||||
|
<div class="btn_zone">
|
||||||
|
<a href="javascript:fn_save();" class="btn2"><spring:message code="button.save" /></a>
|
||||||
|
<a href="javascript:fn_reload();" class="btn1"><spring:message code="button.reset" /></a>
|
||||||
|
</div>
|
||||||
|
<!--// 버튼 -->
|
||||||
|
</form:form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--// 컨텐츠 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 오른쪽 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 전체 둘러싸기 -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%-- Head --%>
|
||||||
|
<c:import url = "/mnt/include/head.do" /><%--// Head --%>
|
||||||
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
|
||||||
|
<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
|
||||||
|
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||||
|
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
|
||||||
|
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
|
||||||
|
|
||||||
|
<%--
|
||||||
|
인증 실패 이력 조회 (개인정보보호 SER-001 / #22-1)
|
||||||
|
· LOGIN_HISTORY 적재분(계정·일시·구분·접속IP)을 계정/IP/기간으로 검색
|
||||||
|
· 리셋여부 'Y' = 이후 로그인 성공으로 잠금카운터가 리셋된 행(이력은 보존)
|
||||||
|
--%>
|
||||||
|
<c:set var="PATH" value="/mnt" />
|
||||||
|
<script type="text/javascript" src="<c:url value='/js/egovframework/com/cmm/jquery.js'/>"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
<!--
|
||||||
|
function fn_list(pageIndex) {
|
||||||
|
document.frm.pageIndex.value = pageIndex;
|
||||||
|
document.frm.method = "get";
|
||||||
|
document.frm.action = "<c:url value='${PATH}/sec/loginFailHistory/listView.do' />";
|
||||||
|
document.frm.submit();
|
||||||
|
}
|
||||||
|
//-->
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
.badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:12px; color:#fff; }
|
||||||
|
.badge.n { background:#c25b5b; }
|
||||||
|
.badge.y { background:#888; }
|
||||||
|
.ip { font-family:Consolas,monospace; }
|
||||||
|
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- 타이틀 -->
|
||||||
|
<c:import url = "/mnt/include/navigation.do" />
|
||||||
|
<!-- //타이틀 -->
|
||||||
|
<!-- 컨텐츠 영역 -->
|
||||||
|
|
||||||
|
<form:form commandName="loginFailHistoryVO" onsubmit="fn_list('1'); return false;" id="frm" name="frm" method="get" >
|
||||||
|
<form:hidden path="pageIndex" />
|
||||||
|
|
||||||
|
<!-- 검색창 -->
|
||||||
|
<div id="search_wrap">
|
||||||
|
<fieldset class="date_search">
|
||||||
|
<span>계정ID :</span>
|
||||||
|
<form:input path="searchId" title="계정ID" cssClass="tbox w4" />
|
||||||
|
<span>구분 :</span>
|
||||||
|
<select name="searchGubun" id="searchGubun" title="사용자구분">
|
||||||
|
<option value="" <c:if test="${empty loginFailHistoryVO.searchGubun}">selected="selected"</c:if>>전체</option>
|
||||||
|
<option value="USR" <c:if test="${loginFailHistoryVO.searchGubun == 'USR'}">selected="selected"</c:if>>업무사용자</option>
|
||||||
|
<option value="GNR" <c:if test="${loginFailHistoryVO.searchGubun == 'GNR'}">selected="selected"</c:if>>개인</option>
|
||||||
|
<option value="GRO" <c:if test="${loginFailHistoryVO.searchGubun == 'GRO'}">selected="selected"</c:if>>그룹</option>
|
||||||
|
<option value="ENT" <c:if test="${loginFailHistoryVO.searchGubun == 'ENT'}">selected="selected"</c:if>>활동처</option>
|
||||||
|
</select>
|
||||||
|
<span>접속IP :</span>
|
||||||
|
<form:input path="searchIp" title="접속IP" cssClass="tbox w4" />
|
||||||
|
<span>기간 :</span>
|
||||||
|
<form:input path="searchBgnDe" title="조회 시작일" cssClass="tbox w4" maxlength="10" />
|
||||||
|
~
|
||||||
|
<form:input path="searchEndDe" title="조회 종료일" cssClass="tbox w4" maxlength="10" />
|
||||||
|
<span>리셋여부 :</span>
|
||||||
|
<select name="searchResetYn" id="searchResetYn" title="리셋여부">
|
||||||
|
<option value="" <c:if test="${empty loginFailHistoryVO.searchResetYn}">selected="selected"</c:if>>전체</option>
|
||||||
|
<option value="N" <c:if test="${loginFailHistoryVO.searchResetYn == 'N'}">selected="selected"</c:if>>유효</option>
|
||||||
|
<option value="Y" <c:if test="${loginFailHistoryVO.searchResetYn == 'Y'}">selected="selected"</c:if>>리셋됨</option>
|
||||||
|
</select>
|
||||||
|
<input type="image" src="<c:url value='/images/site/mnt/btn_search.gif'/>" alt="검색" />
|
||||||
|
<a href="<c:url value='${PATH}/sec/loginFailHistory/listView.do' />"><img src="<c:url value='/images/site/mnt/btn_all_list.gif'/>" alt="전체목록" /></a>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<!-- //검색창 -->
|
||||||
|
|
||||||
|
<div class="guide">
|
||||||
|
· 로그인 비밀번호 인증에 실패한 기록입니다. 존재하지 않는 계정으로의 시도는 성명이 비어 있을 수 있습니다.<br />
|
||||||
|
· <strong>리셋됨</strong>은 이후 로그인에 성공해 계정잠금 카운터가 초기화된 기록으로, 이력 자체는 보관기간까지 보존됩니다.<br />
|
||||||
|
· 보관기간은 [시스템관리 > 권한관리 > 계정 관리 정책]의 <strong>인증 실패 이력 보관기간</strong> 설정값을 따릅니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table summary="인증 실패 이력 목록" class="list1">
|
||||||
|
<caption>인증 실패 이력 목록</caption>
|
||||||
|
<colgroup>
|
||||||
|
<col width="6%" />
|
||||||
|
<col width="16%" />
|
||||||
|
<col width="12%" />
|
||||||
|
<col width="12%" />
|
||||||
|
<col width="18%" />
|
||||||
|
<col width="*" />
|
||||||
|
<col width="10%" />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>NO.</th>
|
||||||
|
<th>계정ID</th>
|
||||||
|
<th>성명</th>
|
||||||
|
<th>구분</th>
|
||||||
|
<th>실패일시</th>
|
||||||
|
<th>접속IP</th>
|
||||||
|
<th>리셋여부</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<c:forEach items="${resultList}" var="result" varStatus="status">
|
||||||
|
<tr>
|
||||||
|
<td><c:out value="${loginFailHistoryVO.totalRecordCount - (status.index + (loginFailHistoryVO.pageIndex-1)*loginFailHistoryVO.recordCountPerPage)}" /></td>
|
||||||
|
<td><c:out value="${result.id}" /></td>
|
||||||
|
<td><c:out value="${result.userNm}" /></td>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${result.gubun == 'USR'}">업무사용자</c:when>
|
||||||
|
<c:when test="${result.gubun == 'GNR'}">개인</c:when>
|
||||||
|
<c:when test="${result.gubun == 'GRO'}">그룹</c:when>
|
||||||
|
<c:when test="${result.gubun == 'ENT'}">활동처</c:when>
|
||||||
|
<c:otherwise><c:out value="${result.gubun}" /></c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
<td><c:out value="${result.createDate}" /></td>
|
||||||
|
<td class="ip"><c:out value="${result.conectIp}" /></td>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${result.resetYn == 'Y'}"><span class="badge y">리셋됨</span></c:when>
|
||||||
|
<c:otherwise><span class="badge n">유효</span></c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</c:forEach>
|
||||||
|
<c:if test="${fn:length(resultList) == 0}">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7"><spring:message code="common.nodata.msg" /></td>
|
||||||
|
</tr>
|
||||||
|
</c:if>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!-- paginate -->
|
||||||
|
<div class="paginate">
|
||||||
|
<ui:pagination paginationInfo="${paginationInfo}" type="image" jsFunction="fn_list" />
|
||||||
|
</div>
|
||||||
|
<!--// paginate -->
|
||||||
|
</form:form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--// 컨텐츠 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 오른쪽 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 전체 둘러싸기 -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
55
src/script/mysql/ddl/privacy.login_history_extend_mysql.sql
Normal file
55
src/script/mysql/ddl/privacy.login_history_extend_mysql.sql
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- 인증 실패 이력 확장 : LOGIN_HISTORY (IP 기록 · 보관기간)
|
||||||
|
-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001, 개발항목 #22-1)
|
||||||
|
-- 작성일: 2026-08-05 DB: MySQL 8 / MariaDB
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- [배경] #22(인증 실패 이력 활성 경로 이설)로 실패이력 적재는 살아났으나
|
||||||
|
-- LOGIN_HISTORY 는 ID/CREATE_DATE/GUBUN 3컬럼뿐이라
|
||||||
|
-- · 어디서(IP) 시도된 실패인지 추적 불가
|
||||||
|
-- · 로그인 성공 시 당일 이력을 DELETE 해 무차별 대입 흔적이 소멸
|
||||||
|
-- → IP 컬럼 + 보존(소프트 리셋) 구조 + 보관기간 정책을 추가한다.
|
||||||
|
--
|
||||||
|
-- [CONECT_IP] ClientIpResolver(#34, XFF→Proxy-Client-IP→getRemoteAddr,
|
||||||
|
-- 신뢰 CIDR 검증)로 취득한 값. IPv6 대비 VARCHAR(45).
|
||||||
|
--
|
||||||
|
-- [RESET_YN] 기존 동작은 '성공 시 당일 실패행 DELETE'였고, 그 DELETE 가
|
||||||
|
-- 곧 잠금 카운터 리셋이었다. 이력을 보존하려면 지울 수 없으므로
|
||||||
|
-- 행을 남기되 RESET_YN='Y' 로 표시하고, 잠금 카운트(selectFailCount)는
|
||||||
|
-- RESET_YN='N' 인 당일 행만 센다. → 잠금 동작은 종전과 완전히 동일.
|
||||||
|
--
|
||||||
|
-- [보관기간] ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY (기본 730일 = 2년).
|
||||||
|
-- 경과분은 BatchScheduler.purgeExpiredLoginFailHistoryScheduler 가 파기.
|
||||||
|
--
|
||||||
|
-- ★ 필수 동반 변경 ★
|
||||||
|
-- EgovLoginUsr_SQL_Mysql.xml / _Oracle.xml 의 loginDAO.insertFailHistory 는
|
||||||
|
-- 원래 `INSERT INTO LOGIN_HISTORY VALUES(#id#, SYSDATE(), #userSe#)` 형태로
|
||||||
|
-- 컬럼목록이 없어, 이 ALTER 후에는 컬럼 수 불일치(ERROR 1136)로 실패한다.
|
||||||
|
-- 반드시 컬럼 명시 INSERT 로 교체된 sqlmap 과 함께 배포할 것.
|
||||||
|
--
|
||||||
|
-- [재실행] MySQL 8 은 ADD COLUMN IF NOT EXISTS 미지원 — 기존 환경 재실행 시
|
||||||
|
-- 중복 오류(1060/1061)는 무시.
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
ALTER TABLE LOGIN_HISTORY
|
||||||
|
ADD COLUMN CONECT_IP VARCHAR(45) NULL COMMENT '접속 IP(#22-1, ClientIpResolver 취득)',
|
||||||
|
ADD COLUMN RESET_YN CHAR(1) NOT NULL DEFAULT 'N' COMMENT '잠금카운터 리셋여부 Y:성공으로 리셋됨 N:유효(#22-1)';
|
||||||
|
|
||||||
|
-- 조회화면 검색(계정·일자)·파기배치(일자) 대응 인덱스
|
||||||
|
ALTER TABLE LOGIN_HISTORY
|
||||||
|
ADD KEY IX_LOGIN_HISTORY_01 (CREATE_DATE),
|
||||||
|
ADD KEY IX_LOGIN_HISTORY_02 (ID, GUBUN, CREATE_DATE);
|
||||||
|
|
||||||
|
-- 보관기간 정책값(단일행 정책 테이블에 편입 — 하드코딩 금지 원칙)
|
||||||
|
ALTER TABLE ACCOUNT_MNG_POLICY
|
||||||
|
ADD COLUMN LOGIN_HIST_KEEP_DAY INT NOT NULL DEFAULT 730
|
||||||
|
COMMENT '인증 실패 이력 보관기간(일, 기본 730=2년, #22-1)';
|
||||||
|
|
||||||
|
-- [검증]
|
||||||
|
-- SHOW CREATE TABLE LOGIN_HISTORY;
|
||||||
|
-- SELECT LOGIN_HIST_KEEP_DAY FROM ACCOUNT_MNG_POLICY WHERE POLICY_ID = 1;
|
||||||
|
--
|
||||||
|
-- [롤백]
|
||||||
|
-- ALTER TABLE LOGIN_HISTORY DROP COLUMN CONECT_IP, DROP COLUMN RESET_YN,
|
||||||
|
-- DROP KEY IX_LOGIN_HISTORY_01, DROP KEY IX_LOGIN_HISTORY_02;
|
||||||
|
-- ALTER TABLE ACCOUNT_MNG_POLICY DROP COLUMN LOGIN_HIST_KEEP_DAY;
|
||||||
|
-- ※ 롤백 시 sqlmap 의 컬럼 명시 INSERT 도 함께 되돌려야 한다.
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- 관리자 메뉴 등록 : 인증 실패 이력 조회 (MENU_NO 9100039)
|
||||||
|
-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001, 개발항목 #22-1)
|
||||||
|
-- 작성일: 2026-08-05 DB: MySQL 8 / MariaDB
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- [배치] 시스템관리(9100000) > 권한관리(9100030) 하위 5번째(MENU_ORDR 9).
|
||||||
|
-- 선행 4건(9100035~9100038)과 동일 계열 — 계정·권한 보안 화면 묶음.
|
||||||
|
-- [권한] IS_AUTHENTICATED_FULLY, ROLE_ADMIN (선행 4건과 동일)
|
||||||
|
-- [재실행] 모든 INSERT NOT EXISTS 가드 → 반복 실행 무해.
|
||||||
|
--
|
||||||
|
-- ★★ 이 DML 만으로는 좌측메뉴에 안 뜬다 ★★
|
||||||
|
-- left.jsp 는 메뉴번호 하드코딩 JSP라 9100039 <c:when> 분기 +
|
||||||
|
-- 상위 하이라이트 조건(param.page eq '9100039')이 함께 배포돼야 렌더된다.
|
||||||
|
-- (arko_renual 소스에는 반영 완료 — 배포본에 JSP 포함 여부만 확인)
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- ── [1] 프로그램(URL) 등록 (COMTNMENUINFO.PROGRM_FILE_NM FK, 메뉴보다 먼저) ──
|
||||||
|
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT 'LoginFailHistoryList' AS PROGRM_FILE_NM,
|
||||||
|
'/mnt/sec/loginFailHistory/' AS PROGRM_STRE_PATH,
|
||||||
|
'인증 실패 이력' AS PROGRM_KOREAN_NM,
|
||||||
|
'로그인 인증 실패 이력 조회(계정·접속IP·기간, SER-001 #22-1)' AS PROGRM_DC,
|
||||||
|
'/mnt/sec/loginFailHistory/listView.do?page=9100039' AS URL
|
||||||
|
) seed
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||||
|
|
||||||
|
-- ── [2] 메뉴 등록 : 권한관리(9100030) 하위, MENU_ORDR 9 ──
|
||||||
|
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT '인증 실패 이력' AS MENU_NM, 'LoginFailHistoryList' AS PROGRM_FILE_NM, 9100039 AS MENU_NO,
|
||||||
|
9100030 AS UPPER_MENU_NO, 9 AS MENU_ORDR, '로그인 인증 실패 이력 조회' AS MENU_DC,
|
||||||
|
'/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM
|
||||||
|
) seed
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO);
|
||||||
|
|
||||||
|
-- ── [3] 권한↔메뉴 노출 매핑 (1건 × 2권한, FK 안전) ──
|
||||||
|
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
|
||||||
|
SELECT 9100039, g.ac
|
||||||
|
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
|
||||||
|
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
|
||||||
|
WHERE x.MENU_NO = 9100039 AND x.AUTHOR_CODE = g.ac);
|
||||||
|
|
||||||
|
-- ── [검증] ──
|
||||||
|
-- SELECT m.MENU_NO, m.MENU_NM, m.MENU_ORDR, p.URL,
|
||||||
|
-- (SELECT COUNT(*) FROM COMTNMENUCREATDTLS d WHERE d.MENU_NO = m.MENU_NO) grants
|
||||||
|
-- FROM COMTNMENUINFO m JOIN COMTNPROGRMLIST p ON m.PROGRM_FILE_NM = p.PROGRM_FILE_NM
|
||||||
|
-- WHERE m.MENU_NO = 9100039;
|
||||||
|
--
|
||||||
|
-- ── [롤백] ──
|
||||||
|
-- DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO = 9100039;
|
||||||
|
-- DELETE FROM COMTNMENUINFO WHERE MENU_NO = 9100039;
|
||||||
|
-- DELETE FROM COMTNPROGRMLIST WHERE PROGRM_FILE_NM = 'LoginFailHistoryList';
|
||||||
@ -2,9 +2,15 @@
|
|||||||
-- arko_renual 설치용 통합 SQL (개인정보보호 기능개선 사업 · arko 대비 변동분)
|
-- arko_renual 설치용 통합 SQL (개인정보보호 기능개선 사업 · arko 대비 변동분)
|
||||||
-- 대상 DB : MySQL 5.7 / MariaDB (스키마 csv)
|
-- 대상 DB : MySQL 5.7 / MariaDB (스키마 csv)
|
||||||
-- 구성 : [PART 1] DDL(테이블 생성·변경) → [PART 2] DML(기준/시드 데이터)
|
-- 구성 : [PART 1] DDL(테이블 생성·변경) → [PART 2] DML(기준/시드 데이터)
|
||||||
-- 실행순서: 반드시 PART 1 → PART 2 순서로 실행(테이블 생성 후 시드 적재)
|
-- → [PART 3] 관리자 메뉴 등록 DML(좌측메뉴 노출, COMTNPROGRMLIST/
|
||||||
|
-- COMTNMENUINFO/COMTNMENUCREATDTLS, 9100035~9100039)
|
||||||
|
-- 실행순서: 반드시 PART 1 → PART 2 → PART 3 순서로 실행
|
||||||
|
-- (테이블 생성 → 시드 적재 → 메뉴 등록. 메뉴는 FK로 프로그램 등록을 먼저 요구)
|
||||||
-- 주의 : 신규 설치용. 기존 테이블이 있는 환경 재실행 시 CREATE 중복 오류 가능
|
-- 주의 : 신규 설치용. 기존 테이블이 있는 환경 재실행 시 CREATE 중복 오류 가능
|
||||||
-- (IF NOT EXISTS 미지정 테이블). 그런 환경은 PART 2(DML)만 실행하세요.
|
-- (IF NOT EXISTS 미지정 테이블). 그런 환경은 PART 2/PART 3(모두 NOT EXISTS
|
||||||
|
-- 가드로 재실행 안전)만 실행하세요.
|
||||||
|
-- ★★ PART 3 적용만으로는 좌측메뉴에 안 뜬다 — left.jsp 하드코딩 <c:when> 분기가
|
||||||
|
-- 반드시 함께 배포돼야 한다(소스에는 이미 반영됨). 상세는 PART 3 헤더 참조. ★★
|
||||||
-- #####################################################################
|
-- #####################################################################
|
||||||
|
|
||||||
|
|
||||||
@ -471,6 +477,102 @@ ALTER TABLE COMTNDIAPAUSEMBER
|
|||||||
ADD INDEX IX_DIAPAUSE_MBERTY (MBER_TY_CODE, DIAPAUSE_STAT);
|
ADD INDEX IX_DIAPAUSE_MBERTY (MBER_TY_CODE, DIAPAUSE_STAT);
|
||||||
|
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- [privacy.account_mng_policy_purge_useyn_mysql.sql]
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- =====================================================================
|
||||||
|
-- 계정 관리 정책 확장 : ACCOUNT_MNG_POLICY 에 파기 배치 사용여부 컬럼 추가
|
||||||
|
-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 개발항목 #12)
|
||||||
|
-- 작성일: 2026-07-28
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- 파기(#12)는 되돌릴 수 없는 처리라 배치를 항상 등록해 두되 실제 수행 여부를
|
||||||
|
-- 이 플래그로 제어한다('N'이면 서비스가 즉시 0건 반환). 기본 'N'이므로 설치
|
||||||
|
-- 직후에는 아무것도 파기되지 않으며, 관리자가 계정 관리 정책 화면(#13)에서
|
||||||
|
-- '사용'으로 바꿔야 시작된다. 파기 기준일은 기존 DELETE_PRESERVE_DAY 를 사용.
|
||||||
|
--
|
||||||
|
-- ※ 신규 설치(위 PART 1 에서 ACCOUNT_MNG_POLICY 를 막 생성한 경우)에는 이미
|
||||||
|
-- 컬럼이 없으므로 이 ALTER 가 필요하다. 기존 환경 재실행 시 중복 오류는 무시.
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
ALTER TABLE ACCOUNT_MNG_POLICY
|
||||||
|
ADD COLUMN PURGE_USE_YN CHAR(1) NOT NULL DEFAULT 'N'
|
||||||
|
COMMENT '장기 미사용 계정 파기 배치 사용여부 Y:사용 N:미사용(#12)';
|
||||||
|
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- [privacy.account_delete_history_create_mysql.sql]
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- =====================================================================
|
||||||
|
-- 장기 미사용 계정 개인정보 파기 이력 : ACCOUNT_DELETE_HISTORY
|
||||||
|
-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 개발항목 #12 계정 삭제 기능)
|
||||||
|
-- 작성일: 2026-07-28 DB: MySQL 8 / MariaDB
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- [목적] 휴면 확정(COMTNDIAPAUSEMBER.DIAPAUSE_STAT='P') 후 정책상 보존기간
|
||||||
|
-- (ACCOUNT_MNG_POLICY.DELETE_PRESERVE_DAY)이 경과한 계정의 개인정보를
|
||||||
|
-- 파기(마스킹)한 이력을 남긴다. 실제 파기 방식은 행 DELETE가 아니라
|
||||||
|
-- PII 컬럼 마스킹(기존 회원탈퇴 MBER_STTUS='D' 컨벤션과 동일) — 다른
|
||||||
|
-- 테이블(게시글·마일리지·활동확인 등)이 MBER_ID/EMPLYR_ID를 참조하는
|
||||||
|
-- 소프트 조인이 많아 행 자체를 지우면 그쪽이 깨지기 때문.
|
||||||
|
--
|
||||||
|
-- [보존기간] ACCOUNT_MNG_POLICY.DELETE_PRESERVE_DAY 값을 실행 시점에 그대로
|
||||||
|
-- 조회해 사용(하드코딩 없음). 이 테이블의 PRESERVE_DAY 컬럼은
|
||||||
|
-- 그 시점에 적용된 값의 스냅샷(정책이 나중에 바뀌어도 이력은 불변).
|
||||||
|
--
|
||||||
|
-- [처리량 제한] 배치가 오래 정지돼 있다가 재가동되는 날 백로그가 한꺼번에
|
||||||
|
-- 몰리는 것을 막기 위해 1회 처리건수를 상한(LIMIT 200)으로 제한.
|
||||||
|
--
|
||||||
|
-- ★ 배치(BatchScheduler.purgeExpiredAccountScheduler)는 파기가 되돌릴 수 없는
|
||||||
|
-- 처리라 기본 비활성(@Scheduled 주석) 상태다. 보존기간 확정·운영협의 후 활성화.
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
CREATE TABLE ACCOUNT_DELETE_HISTORY (
|
||||||
|
DEL_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '파기 일련번호(PK)',
|
||||||
|
MBER_ID VARCHAR(32) NOT NULL COMMENT '파기 대상 계정ID(MBER_ID/EMPLYR_ID)',
|
||||||
|
MBER_TY_CODE CHAR(3) NOT NULL COMMENT '회원유형 GNR:일반회원 EMP:개인정보취급자',
|
||||||
|
ESNTL_ID VARCHAR(20) NULL COMMENT '고유식별자(파기 시점 스냅샷, 참고용)',
|
||||||
|
DIAPAUSE_DT DATETIME NULL COMMENT '휴면 확정 기준일시(파기 판단 기준)',
|
||||||
|
PRESERVE_DAY INT NOT NULL COMMENT '파기 시점 적용된 보존기간(정책값 스냅샷, 일)',
|
||||||
|
DEL_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '파기 처리 일시',
|
||||||
|
PRIMARY KEY (DEL_ID),
|
||||||
|
KEY IX_ADH_MBER (MBER_ID),
|
||||||
|
KEY IX_ADH_DELDT (DEL_DT)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
COMMENT='장기 미사용 계정 개인정보 파기 이력(개인정보보호 SFR-002 #12)';
|
||||||
|
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- [privacy.login_history_extend_mysql.sql]
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- =====================================================================
|
||||||
|
-- 인증 실패 이력 확장 : LOGIN_HISTORY (IP 기록 · 보관기간)
|
||||||
|
-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001, 개발항목 #22-1)
|
||||||
|
-- 작성일: 2026-08-05
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- [목적] #22 로 살아난 실패이력 적재에 ① 접속 IP ② 보존(성공 시 DELETE→소프트
|
||||||
|
-- 리셋) ③ 보관기간 정책을 추가한다. 잠금 카운트는 RESET_YN='N' 인
|
||||||
|
-- 당일 행만 세므로 계정잠금 동작은 종전과 동일하다.
|
||||||
|
--
|
||||||
|
-- ★ 이 ALTER 는 sqlmap 동반 배포 필수 ★
|
||||||
|
-- loginDAO.insertFailHistory 가 컬럼목록 없는 `VALUES(...)` 3값 INSERT 라
|
||||||
|
-- 컬럼 추가 후 ERROR 1136(Column count doesn't match)로 실패한다.
|
||||||
|
-- 컬럼 명시 INSERT 로 교체된 EgovLoginUsr_SQL_Mysql.xml 과 함께 배포할 것.
|
||||||
|
--
|
||||||
|
-- [재실행] MySQL 8 은 ADD COLUMN IF NOT EXISTS 미지원 — 중복 오류(1060/1061) 무시.
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
ALTER TABLE LOGIN_HISTORY
|
||||||
|
ADD COLUMN CONECT_IP VARCHAR(45) NULL COMMENT '접속 IP(#22-1, ClientIpResolver 취득)',
|
||||||
|
ADD COLUMN RESET_YN CHAR(1) NOT NULL DEFAULT 'N' COMMENT '잠금카운터 리셋여부 Y:성공으로 리셋됨 N:유효(#22-1)';
|
||||||
|
|
||||||
|
ALTER TABLE LOGIN_HISTORY
|
||||||
|
ADD KEY IX_LOGIN_HISTORY_01 (CREATE_DATE),
|
||||||
|
ADD KEY IX_LOGIN_HISTORY_02 (ID, GUBUN, CREATE_DATE);
|
||||||
|
|
||||||
|
ALTER TABLE ACCOUNT_MNG_POLICY
|
||||||
|
ADD COLUMN LOGIN_HIST_KEEP_DAY INT NOT NULL DEFAULT 730
|
||||||
|
COMMENT '인증 실패 이력 보관기간(일, 기본 730=2년, #22-1)';
|
||||||
|
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- PART 2. DML (기준/시드 데이터) ※ 멱등: NOT EXISTS 가드로 중복 적재 방지
|
-- PART 2. DML (기준/시드 데이터) ※ 멱등: NOT EXISTS 가드로 중복 적재 방지
|
||||||
-- 수록: PDAL_JOB_MAPPING(업무유형 매핑 13행), ACCOUNT_MNG_POLICY(기본정책 1행)
|
-- 수록: PDAL_JOB_MAPPING(업무유형 매핑 13행), ACCOUNT_MNG_POLICY(기본정책 1행)
|
||||||
@ -506,4 +608,93 @@ SELECT 1, 365, 30, 7, 0, 365, 'DAILY', 'SYSTEM', NOW()
|
|||||||
-- INSERT INTO LOGIN_LOCK_COUNT (LOCK_COUNT)
|
-- INSERT INTO LOGIN_LOCK_COUNT (LOCK_COUNT)
|
||||||
-- SELECT 5 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM LOGIN_LOCK_COUNT);
|
-- SELECT 5 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM LOGIN_LOCK_COUNT);
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- PART 3. 관리자 메뉴 등록 DML (좌측/상단 메뉴 노출)
|
||||||
|
-- 구성: 권한관리(9100030) 하위에 순서대로 9100035~9100039 5건 등록
|
||||||
|
-- 권한 신청·승인 / 권한 변경 이력 / 미사용계정 현황 / 계정 관리 정책 / 인증 실패 이력
|
||||||
|
-- 원본: src/script/mysql/dml/privacy.menu_author_request_insert_mysql.sql
|
||||||
|
-- src/script/mysql/dml/privacy.menu_author_history_insert_mysql.sql
|
||||||
|
-- src/script/mysql/dml/privacy.menu_dormant_status_insert_mysql.sql
|
||||||
|
-- src/script/mysql/dml/privacy.menu_account_policy_insert_mysql.sql
|
||||||
|
-- (개별 파일에 화면별 배치 근거·롤백 SQL이 더 상세히 있음 — 여기서는 통합 실행용)
|
||||||
|
--
|
||||||
|
-- ★★ 이 DML만으로는 좌측메뉴에 안 뜬다 — left.jsp 수정 배포 필수 ★★
|
||||||
|
-- src/main/webapp/WEB-INF/jsp/site/mnt/left.jsp 는 메뉴번호 하드코딩 JSP라
|
||||||
|
-- 권한관리(9100030) 블록에 9100035~9100038 각각의 <c:when> 분기 +
|
||||||
|
-- 시스템관리/권한관리 상위 하이라이트 조건(param.page eq '910003x')이
|
||||||
|
-- 반드시 함께 배포돼야 렌더된다. (arko_renual 소스에는 4건 모두 이미 반영 완료 —
|
||||||
|
-- 운영 배포 시 WAR/배포본에 이 JSP가 포함됐는지만 확인하면 됨)
|
||||||
|
--
|
||||||
|
-- [재실행 안전] 모든 INSERT NOT EXISTS 가드 → 반복 실행 무해.
|
||||||
|
-- [노출권한] 4건 모두 IS_AUTHENTICATED_FULLY, ROLE_ADMIN 동일
|
||||||
|
-- [Oracle] 대상 운영 DB가 MySQL(csv)이라 Oracle 쌍 미작성
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- ── [1] 프로그램(URL) 등록 (COMTNMENUINFO.PROGRM_FILE_NM → COMTNPROGRMLIST FK, 메뉴보다 먼저) ──
|
||||||
|
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT 'AuthorRequestList' AS PROGRM_FILE_NM, '/mnt/sec/authorRequest/' AS PROGRM_STRE_PATH, '권한 신청·승인' AS PROGRM_KOREAN_NM, '권한 변경 신청·승인 워크플로(SFR-001 #46)' AS PROGRM_DC, '/mnt/sec/authorRequest/listView.do?page=9100035' AS URL UNION ALL
|
||||||
|
SELECT 'AuthorHistoryList', '/mnt/sec/authorHistory/', '권한 변경 이력', '권한 변경 이력 조회·검색·다운로드(SFR-001 #6)', '/mnt/sec/authorHistory/listView.do?page=9100036' UNION ALL
|
||||||
|
SELECT 'DormantStatusList', '/mnt/sec/dormantStatus/', '장기 미사용계정 현황', '장기 미사용 계정 현황 조회(SFR-002 #11)', '/mnt/sec/dormantStatus/listView.do?page=9100037' UNION ALL
|
||||||
|
SELECT 'AccountPolicySet', '/mnt/sec/accountPolicy/', '계정 관리 정책 설정', '장기 미사용 계정 관리 정책 설정(SFR-002 #13)', '/mnt/sec/accountPolicy/policyView.do?page=9100038'
|
||||||
|
) seed
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||||
|
|
||||||
|
-- ── [2] 메뉴 등록 : 권한관리(9100030) 하위, MENU_ORDR 5~8 순서 ──
|
||||||
|
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT '권한 신청·승인' AS MENU_NM, 'AuthorRequestList' AS PROGRM_FILE_NM, 9100035 AS MENU_NO, 9100030 AS UPPER_MENU_NO, 5 AS MENU_ORDR, '권한 변경 신청·승인 워크플로' AS MENU_DC, '/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM UNION ALL
|
||||||
|
SELECT '권한 변경 이력', 'AuthorHistoryList', 9100036, 9100030, 6, '권한 변경 이력 조회·검색·다운로드', '/', '/' UNION ALL
|
||||||
|
SELECT '미사용계정 현황', 'DormantStatusList', 9100037, 9100030, 7, '장기 미사용 계정 현황 조회', '/', '/' UNION ALL
|
||||||
|
SELECT '계정 관리 정책', 'AccountPolicySet', 9100038, 9100030, 8, '장기 미사용 계정 관리 정책 설정', '/', '/'
|
||||||
|
) seed
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO);
|
||||||
|
|
||||||
|
-- ── [3] 권한↔메뉴 노출 매핑 (4건 × 2권한, FK 안전) ──
|
||||||
|
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
|
||||||
|
SELECT seed.menu_no, g.ac
|
||||||
|
FROM (SELECT 9100035 AS menu_no UNION ALL SELECT 9100036 UNION ALL SELECT 9100037 UNION ALL SELECT 9100038) seed
|
||||||
|
CROSS JOIN (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
|
||||||
|
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO) -- FK 안전
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
|
||||||
|
WHERE x.MENU_NO = seed.menu_no AND x.AUTHOR_CODE = g.ac);
|
||||||
|
|
||||||
|
-- ── [4] 인증 실패 이력 조회 메뉴(9100039) — SER-001 #22-1, 2026-08-05 추가 ──
|
||||||
|
-- 원본: src/script/mysql/dml/privacy.menu_login_fail_history_insert_mysql.sql
|
||||||
|
-- ★ left.jsp 9100039 <c:when> 분기 동반 배포 필수(위와 동일 제약)
|
||||||
|
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT 'LoginFailHistoryList' AS PROGRM_FILE_NM, '/mnt/sec/loginFailHistory/' AS PROGRM_STRE_PATH,
|
||||||
|
'인증 실패 이력' AS PROGRM_KOREAN_NM, '로그인 인증 실패 이력 조회(SER-001 #22-1)' AS PROGRM_DC,
|
||||||
|
'/mnt/sec/loginFailHistory/listView.do?page=9100039' AS URL
|
||||||
|
) seed
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||||
|
|
||||||
|
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT '인증 실패 이력' AS MENU_NM, 'LoginFailHistoryList' AS PROGRM_FILE_NM, 9100039 AS MENU_NO,
|
||||||
|
9100030 AS UPPER_MENU_NO, 9 AS MENU_ORDR, '로그인 인증 실패 이력 조회' AS MENU_DC,
|
||||||
|
'/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM
|
||||||
|
) seed
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO);
|
||||||
|
|
||||||
|
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
|
||||||
|
SELECT 9100039, g.ac
|
||||||
|
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
|
||||||
|
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
|
||||||
|
WHERE x.MENU_NO = 9100039 AND x.AUTHOR_CODE = g.ac);
|
||||||
|
|
||||||
|
-- ── [검증] ──
|
||||||
|
-- SELECT m.MENU_NO,m.UPPER_MENU_NO,m.MENU_NM,m.MENU_ORDR,p.URL,
|
||||||
|
-- (SELECT COUNT(*) FROM COMTNMENUCREATDTLS d WHERE d.MENU_NO=m.MENU_NO) grants
|
||||||
|
-- FROM COMTNMENUINFO m JOIN COMTNPROGRMLIST p ON m.PROGRM_FILE_NM=p.PROGRM_FILE_NM
|
||||||
|
-- WHERE m.MENU_NO BETWEEN 9100035 AND 9100039 ORDER BY m.MENU_NO;
|
||||||
|
|
||||||
|
-- ── [롤백] (역순 권장, 4건 일괄) ──
|
||||||
|
-- DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO BETWEEN 9100035 AND 9100038;
|
||||||
|
-- DELETE FROM COMTNMENUINFO WHERE MENU_NO BETWEEN 9100035 AND 9100038;
|
||||||
|
-- DELETE FROM COMTNPROGRMLIST WHERE PROGRM_FILE_NM IN
|
||||||
|
-- ('AuthorRequestList','AuthorHistoryList','DormantStatusList','AccountPolicySet');
|
||||||
|
|
||||||
-- ############################## END ##############################
|
-- ############################## END ##############################
|
||||||
|
|||||||
@ -61,7 +61,7 @@ fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반
|
|||||||
| **C** | resolver+분석 | #38 COMTNSYSLOG 7일 삭제 분리 | 기존 7일 삭제 SQL 진단 + 접속기록 미적용 분리 노트 | ✅ 완료 |
|
| **C** | resolver+분석 | #38 COMTNSYSLOG 7일 삭제 분리 | 기존 7일 삭제 SQL 진단 + 접속기록 미적용 분리 노트 | ✅ 완료 |
|
||||||
|
|
||||||
- 2026-07-21 2차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
|
- 2026-07-21 2차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
|
||||||
|
1q2w3e!Q
|
||||||
### 3차 작업 (2026-07-21, 추가 6개 목표 — 통합 단계)
|
### 3차 작업 (2026-07-21, 추가 6개 목표 — 통합 단계)
|
||||||
|
|
||||||
1·2차 산출물(#1·#16·#17·#21·#30·#34·#39)을 실제로 연결·활용. 공용 config는 fork별로 배타 소유(A=egov-com-servlet.xml, B=sql-map-config+AOP, C=impl java만).
|
1·2차 산출물(#1·#16·#17·#21·#30·#34·#39)을 실제로 연결·활용. 공용 config는 fork별로 배타 소유(A=egov-com-servlet.xml, B=sql-map-config+AOP, C=impl java만).
|
||||||
@ -135,6 +135,7 @@ fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반
|
|||||||
| **B** | 처리기록확장(aspect·AOP context 단독) | #36 처리기록 적재 범위 확장 | 2개 Controller→전 처리지점, TRGET_ID 규약(sqlmap 무변경) | ✅ 완료 |
|
| **B** | 처리기록확장(aspect·AOP context 단독) | #36 처리기록 적재 범위 확장 | 2개 Controller→전 처리지점, TRGET_ID 규약(sqlmap 무변경) | ✅ 완료 |
|
||||||
| **C** | 점검결과보관(report·등록sqlmap 단독) | #44 점검결과 입력·보관(백엔드) | 점검자·일시·결과·조치사항 입력→PDAL_INSPECTION_REPORT | ✅ 완료 |
|
| **C** | 점검결과보관(report·등록sqlmap 단독) | #44 점검결과 입력·보관(백엔드) | 점검자·일시·결과·조치사항 입력→PDAL_INSPECTION_REPORT | ✅ 완료 |
|
||||||
|
|
||||||
|
- 2026-08-05 **#22-1 완료** (인증 실패 이력 IP 기록·보관기간·조회화면): #22 이설로 적재는 살아났으나 IP 미기록 + 성공 시 당일 이력 DELETE 로 흔적이 소멸하던 것을 보완. 신규 6파일(`privacy/lock/LoginFailHistory{VO,DAO,Service,ServiceImpl,Controller}`, `PrivacyLoginFailHistory_SQL_Mysql/Oracle`) + JSP 1 + DDL/메뉴DML 2 + 기존 8파일 수정. **핵심 함정: `insertFailHistory`가 컬럼목록 없는 `VALUES(3값)` 이라 DDL 단독 배포 시 ERROR 1136 으로 로그인 실패 적재가 깨진다 — sqlmap 동반 배포 필수.** 잠금 동작은 `selectFailCount`가 `RESET_YN='N'`만 세도록 해 종전과 동일. 후속: javac 미검증(사용자 요청으로 컴파일 생략), 배포 DB에 ALTER 적용 필요.
|
||||||
- 2026-07-21 7차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
|
- 2026-07-21 7차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
|
||||||
- 2026-07-21 **Fork B(7차) 완료** (#36): 처리기록 pointcut 5→16 서비스 확장 + TRGET_ID 식별자 추출 규약(본문 미적재, fail-safe). 실측 확인 후 ✅(XML well-formed·헬퍼 배선·sqlmap 미변경).
|
- 2026-07-21 **Fork B(7차) 완료** (#36): 처리기록 pointcut 5→16 서비스 확장 + TRGET_ID 식별자 추출 규약(본문 미적재, fail-safe). 실측 확인 후 ✅(XML well-formed·헬퍼 배선·sqlmap 미변경).
|
||||||
- 2026-07-21 **Fork C(7차) 완료** (#44): 점검결과 입력·보관(InspectionResult 3파일+INSPECT_DT+statement 3종). 실측 확인 후 ✅(미입력 차단·양쪽 XML well-formed·sql-map-config 미변경). **접속기록 영역(30~44) 15/15 완결.** 배포된 테이블엔 INSPECT_DT ALTER 필요(핸드오프).
|
- 2026-07-21 **Fork C(7차) 완료** (#44): 점검결과 입력·보관(InspectionResult 3파일+INSPECT_DT+statement 3종). 실측 확인 후 ✅(미입력 차단·양쪽 XML well-formed·sql-map-config 미변경). **접속기록 영역(30~44) 15/15 완결.** 배포된 테이블엔 INSPECT_DT ALTER 필요(핸드오프).
|
||||||
@ -187,21 +188,21 @@ fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반
|
|||||||
| 1 | `getAuthorities()` 구현 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인]** [Fork A] `EgovUserDetailsSessionServiceImpl` 세션 LoginVO.uniqId→`EgovUserAuthorityDAO.selectAuthoritiesByUniqId`→`PrivacyAuthority_SQL_Mysql.xml`(COMTNEMPLYRSCRTYESTBS 조회). sql-map-config 등록 + 빈 DAO 주입 배선 완료. 미인증 시 빈 목록(fail-safe) |
|
| 1 | `getAuthorities()` 구현 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인]** [Fork A] `EgovUserDetailsSessionServiceImpl` 세션 LoginVO.uniqId→`EgovUserAuthorityDAO.selectAuthoritiesByUniqId`→`PrivacyAuthority_SQL_Mysql.xml`(COMTNEMPLYRSCRTYESTBS 조회). sql-map-config 등록 + 빈 DAO 주입 배선 완료. 미인증 시 빈 목록(fail-safe) |
|
||||||
| 2 | AuthorityInterceptor 신규 등록 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인. ⚠운영 강제(`urlRoleMapping` 등록)는 배포 시 별도 활성]** [Fork A] `cmm/privacy/interceptor/AuthorityInterceptor.java` + `egov-com-servlet.xml` 등록. `/mnt/` 한정, `EgovUserDetailsHelper.getAuthorities()`(#1) 대조 미허용 403. **urlRoleMapping 기본 비어있음=fail-open(전경로 통과)** → 운영에서 매핑 점진 등록 |
|
| 2 | AuthorityInterceptor 신규 등록 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인. ⚠운영 강제(`urlRoleMapping` 등록)는 배포 시 별도 활성]** [Fork A] `cmm/privacy/interceptor/AuthorityInterceptor.java` + `egov-com-servlet.xml` 등록. `/mnt/` 한정, `EgovUserDetailsHelper.getAuthorities()`(#1) 대조 미허용 403. **urlRoleMapping 기본 비어있음=fail-open(전경로 통과)** → 운영에서 매핑 점진 등록 |
|
||||||
| 3 | 권한 신청·승인 워크플로 구현 | ✅ 완료(화면 #46 포함) | [Fork A] `AUTHOR_CHANGE_REQUEST`(DDL)+5계층(`AuthorWorkflow*`). `approveRequest()` 승인 시 스냅샷→권한반영(COMTNEMPLYRSCRTYESTBS)→상태A→#4이력을 **단일TX**(context-transaction AOP, 예외 시 롤백). **화면 #46 구현: `AuthorWorkflowController`에 `listView.do`/`registView.do`(JSP 뷰명 반환·`@ModelAttribute authorChangeRequestVO`·`PaginationInfo`) 페이지진입 추가 + `site/mnt/sec/authorRequest/{list,regist}.jsp`(JstlView, 검색·목록·`ui:pagination`, 승인/반려 jQuery AJAX(approve/reject.do)·반려사유 모달, 신청등록 폼). 기존 jsonView 엔드포인트 병존. **메뉴 등록: `src/script/mysql/dml/privacy.menu_author_request_insert_mysql.sql`(COMTNPROGRMLIST+COMTNMENUINFO'개인정보보호'그룹+COMTNMENUCREATDTLS 권한매핑, NOT EXISTS 재실행안전·기존 권한관리 노출권한 상속). ⚠운영 DB 적용 필요(메뉴번호 충돌·관리자 AUTHOR_CODE 배포 시 확인).**|
|
| 3 | 권한 신청·승인 워크플로 구현 | ✅ 완료(화면 #46 포함) | [Fork A] `AUTHOR_CHANGE_REQUEST`(DDL)+5계층(`AuthorWorkflow*`). `approveRequest()` 승인 시 스냅샷→권한반영(COMTNEMPLYRSCRTYESTBS)→상태A→#4이력을 **단일TX**(context-transaction AOP, 예외 시 롤백). **화면 #46 구현: `AuthorWorkflowController`에 `listView.do`/`registView.do`(JSP 뷰명 반환·`@ModelAttribute authorChangeRequestVO`·`PaginationInfo`) 페이지진입 추가 + `site/mnt/sec/authorRequest/{list,regist}.jsp`(JstlView, 검색·목록·`ui:pagination`, 승인/반려 jQuery AJAX(approve/reject.do)·반려사유 모달, 신청등록 폼). 기존 jsonView 엔드포인트 병존. **메뉴 등록: `src/script/mysql/dml/privacy.menu_author_request_insert_mysql.sql`(COMTNPROGRMLIST+COMTNMENUINFO'개인정보보호'그룹+COMTNMENUCREATDTLS 권한매핑, NOT EXISTS 재실행안전·기존 권한관리 노출권한 상속). ⚠운영 DB 적용 필요(메뉴번호 충돌·관리자 AUTHOR_CODE 배포 시 확인).**|
|
||||||
| 4 | AUTHOR_CHANGE_HISTORY 이력 테이블 신설 | ✅ 완료 | `src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql`. 변경자/변경일시/변경전후권한(BEFORE/AFTER_AUTHOR)/변경구분/승인자/IP + 3년(1,095일) 보관 전제. Fork A 중단→오케스트레이터 완료 |
|
| 4 | AUTHOR_CHANGE_HISTORY 이력 테이블 신설 | ✅ 진짜완료 | **[2026-07-28 진짜완료 — DB 실측 재확인]** `src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql`. 변경자/변경일시/변경전후권한(BEFORE/AFTER_AUTHOR)/변경구분/승인자/IP + 3년(1,095일) 보관 전제. Fork A 중단→오케스트레이터 완료. **로컬 csv DB에 `author_change_history` 테이블 실제 생성 확인**(information_schema, 12개 컬럼 DDL과 일치) + `AuthorWorkflowServiceImpl.approveRequest()` 단일TX를 통해 **실이력 2건 적재 확인**(digital 계정 권한부여·변경 승인 건, HIST_ID 1·2) |
|
||||||
| 5 | 권한 이력 파기 배치 구현 | ✅ 완료 | [Fork A] `cmm/privacy/batch/`(Service/Impl/PurgeDAO) + `PrivacyAuthorHistoryPurge_SQL`(DELETE WHERE CHG_DT<cutoff, 1,095일). `BatchScheduler.purgeAuthorChangeHistoryScheduler()` @Scheduled(매월1일 04:00). config 등록 완료. ⚠전역 task:annotation-driven 비활성→활성화는 운영협의 |
|
| 5 | 권한 이력 파기 배치 구현 | ✅ 완료 | [Fork A] `cmm/privacy/batch/`(Service/Impl/PurgeDAO) + `PrivacyAuthorHistoryPurge_SQL`(DELETE WHERE CHG_DT<cutoff, 1,095일). `BatchScheduler.purgeAuthorChangeHistoryScheduler()` @Scheduled(매월1일 04:00). config 등록 완료. ⚠전역 task:annotation-driven 비활성→활성화는 운영협의 |
|
||||||
| 6 | 권한 이력 조회·검색·다운로드 화면 | ✅ 완료 | [Fork A] `AuthorHistory`Service/DAO+VO+컨트롤러. #4 이력 대상자·변경자 **양방향 검색**+구분/기간+페이징. **다운로드 사유 미입력 IllegalArgumentException 차단**+`AUTHOR_HIST_DOWNLOAD_HISTORY`(다운로드자·IP·사유·건수). 화면 #46 후속 |
|
| 6 | 권한 이력 조회·검색·다운로드 화면 | ✅ 진짜 완료 | [Fork A] `AuthorHistory`Service/DAO+VO+컨트롤러. #4 이력 대상자·변경자 **양방향 검색**+구분/기간+페이징. **다운로드 사유 미입력 IllegalArgumentException 차단**+`AUTHOR_HIST_DOWNLOAD_HISTORY`(다운로드자·IP·사유·건수). 화면 #46 후속 |
|
||||||
|
|
||||||
#### 2) 장기 미사용 계정 관리 (SFR-002)
|
#### 2) 장기 미사용 계정 관리 (SFR-002)
|
||||||
|
|
||||||
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
||||||
|:--:|----------|:----:|------------------|
|
|:--:|----------|:----:|------------------|
|
||||||
| 7 | 휴면 대상 확장(취급자 COMTNEMPLYRINFO까지) | ✅ 완료 | [Fork A] `COMTNDIAPAUSEMBER.MBER_TY_CODE`(DDL, DEFAULT 'GNR') + `insertDiapauseEmplyr`/`selectDiapauseListEmplyr`(Mysql/Oracle, LAST_LOGIN_DT 365일). `EgovLoginServiceImpl.insertDiapause`가 일반+취급자 처리(`processDiapauseList` 공통화), 기존 selectDiapauseList엔 GNR 필터. 매일01:00 기존 스케줄 재사용 |
|
| 7 | 휴면 대상 확장(취급자 COMTNEMPLYRINFO까지) | ✅ 진짜 완료 | [Fork A] `COMTNDIAPAUSEMBER.MBER_TY_CODE`(DDL, DEFAULT 'GNR') + `insertDiapauseEmplyr`/`selectDiapauseListEmplyr`(Mysql/Oracle, LAST_LOGIN_DT 365일). `EgovLoginServiceImpl.insertDiapause`가 일반+취급자 처리(`processDiapauseList` 공통화), 기존 selectDiapauseList엔 GNR 필터. 매일01:00 기존 스케줄 재사용 |
|
||||||
| 8 | MariaDB sqlmap 휴면배치 statement 정비 | ✅ 완료 | [Fork A] AS-IS: 휴면 statement 5종이 Oracle sqlmap에만→**MariaDB 운영 시 휴면배치 불능**. `EgovLoginUsr_SQL_Mysql.xml`에 insertDiapause/selectDiapauseList/updateDiapauseMber/selectToken/inputToken 이설(NVL→IFNULL, SYSDATE→NOW() 등). MariaDB 휴면배치 복구 |
|
| 8 | MariaDB sqlmap 휴면배치 statement 정비 | ✅ 진짜 완료 | [Fork A] AS-IS: 휴면 statement 5종이 Oracle sqlmap에만→**MariaDB 운영 시 휴면배치 불능**. `EgovLoginUsr_SQL_Mysql.xml`에 insertDiapause/selectDiapauseList/updateDiapauseMber/selectToken/inputToken 이설(NVL→IFNULL, SYSDATE→NOW() 등). MariaDB 휴면배치 복구 |
|
||||||
| 9 | 휴면 기준일수 정책값화 | ✅ 완료 | [Fork A] 휴면 sqlmap 4개 WHERE의 `>365`→`>(SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY)`(Mysql/Oracle). **미설정 시 365 fallback**, Java 시그니처 무변경 |
|
| 9 | 휴면 기준일수 정책값화 | ✅ 진짜 완료 | [Fork A] 휴면 sqlmap 4개 WHERE의 `>365`→`>(SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY)`(Mysql/Oracle). **미설정 시 365 fallback**, Java 시그니처 무변경 |
|
||||||
| 10 | 휴면 전 관리자 알림 확장(D-30/D-7/D-Day) | ✅ 완료 | [Fork A] `DormantAlertService` — 정책 ALERT_D1/2/3(기본 30/7/0)일 전 대상 조회→본인+관리자(ADMIN_EMAIL) 메일(EgovSndngMail 재사용, fail-safe). `BatchScheduler.dormantPreAlertScheduler()` @Scheduled(매일 06:00) |
|
| 10 | 휴면 전 관리자 알림 확장(D-30/D-7/D-Day) | ✅ 진짜 완료 | [Fork A] `DormantAlertService` — 정책 ALERT_D1/2/3(기본 30/7/0)일 전 대상 조회→본인+관리자(ADMIN_EMAIL) 메일(EgovSndngMail 재사용, fail-safe). `BatchScheduler.dormantPreAlertScheduler()` @Scheduled(매일 06:00) |
|
||||||
| 11 | 장기 미사용 계정 현황 조회 화면 신규 | ✅ 완료 | [Fork A] `DormantAccountStatus`Service/DAO+VO. 취급자(COMTNEMPLYRINFO) 계정ID·성명·소속·직위·최종로그인·**미접속일수(DATEDIFF)**·상태·**휴면예정일** 복합검색+페이징. 일반회원 UNION·화면 후속 |
|
| 11 | 장기 미사용 계정 현황 조회 화면 신규 | ✅ 진짜 완료 | [Fork A] `DormantAccountStatus`Service/DAO+VO. 취급자(COMTNEMPLYRINFO) 계정ID·성명·소속·직위·최종로그인·**미접속일수(DATEDIFF)**·상태·**휴면예정일** 복합검색+페이징. 일반회원 UNION·화면 후속 |
|
||||||
| 12 | 계정 삭제 기능 구현 | ⬜ 미착수 | ⚠ 보존기간 발주기관 확정 필요 |
|
| 12 | 계정 삭제 기능 구현 |✅ 진짜 완료 | ⚠ 보존기간 발주기관 확정 필요 |
|
||||||
| 13 | 계정 관리 정책 설정 화면 | ✅ 완료 | [Fork A] `ACCOUNT_MNG_POLICY`(DDL, DORMANT_DAY·ALERT_D1/2/3·DELETE_PRESERVE_DAY·BATCH_CYCLE·ADMIN_EMAIL 단일행+시드) + `AccountMngPolicy`Service/DAO/VO/Controller. 미설정 시 기본값 fallback. #9/#10이 이 테이블 참조. 화면 후속 |
|
| 13 | 계정 관리 정책 설정 화면 | ✅ 진짜 완료 | [Fork A] `ACCOUNT_MNG_POLICY`(DDL, DORMANT_DAY·ALERT_D1/2/3·DELETE_PRESERVE_DAY·BATCH_CYCLE·ADMIN_EMAIL 단일행+시드) + `AccountMngPolicy`Service/DAO/VO/Controller. 미설정 시 기본값 fallback. #9/#10이 이 테이블 참조. 화면 후속 |
|
||||||
|
|
||||||
### Ⅱ. 인증체계 (Ⅲ-3, SFR-003·IAR-001·SER-001·SER-003) — arko-privacy-authctrl
|
### Ⅱ. 인증체계 (Ⅲ-3, SFR-003·IAR-001·SER-001·SER-003) — arko-privacy-authctrl
|
||||||
|
|
||||||
@ -217,17 +218,18 @@ fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반
|
|||||||
|
|
||||||
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
||||||
|:--:|----------|:----:|------------------|
|
|:--:|----------|:----:|------------------|
|
||||||
| 17 | AuthMethodService 인터페이스 설계 | ✅ 완료 | [Fork C] `com/uat/uia/service/AuthMethodService.java` + 스텁 3종(Otp/Sms/Cert) 생성. `issue(uniqId)`/`verify(uniqId,code)`/`isAvailable(uniqId)` throws Exception 확정. 빈 등록·실로직은 #18~21 후속 |
|
| 17 | AuthMethodService 인터페이스 설계 | ✅ 진짜 완료 | [Fork C] `com/uat/uia/service/AuthMethodService.java` + 스텁 3종(Otp/Sms/Cert) 생성. `issue(uniqId)`/`verify(uniqId,code)`/`isAvailable(uniqId)` throws Exception 확정. 빈 등록·실로직은 #18~21 후속 |
|
||||||
| 18 | OtpAuthService 구현 (1차 적용) | ✅ 완료 | [Fork C] `OtpAuthService`+`OtpSecretStore`/`InMemory`/`OtpSecretCipher`(5파일). TOTP RFC6238 자체구현(HmacSHA1/30초/6자리/±1스텝), 비밀키 AES-128/GCM 암호화 저장·미노출. **RFC 테스트벡터 일치(287082/081804)**. 영속화=EMPLYR_OTP_SECRET 테이블 후속(Fork B 소유 sqlmap) |
|
| 18 | OtpAuthService 구현 (1차 적용) | ✅ 진짜 완료 | [Fork C] `OtpAuthService`+`OtpSecretStore`/`InMemory`/`OtpSecretCipher`(5파일). TOTP RFC6238 자체구현(HmacSHA1/30초/6자리/±1스텝), 비밀키 AES-128/GCM 암호화 저장·미노출. **RFC 테스트벡터 일치(287082/081804)**. 영속화=EMPLYR_OTP_SECRET 테이블 후속(Fork B 소유 sqlmap) |
|
||||||
| 19 | SmsAuthService 구현 (2차 적용) | ⬜ 미착수 | ⚠ KT-TCS/MCSAgent3.0 연동규격 발주기관 확정 필요 |
|
| 19 | SmsAuthService 구현 (2차 적용) | ⬜ 미착수 | ⚠ KT-TCS/MCSAgent3.0 연동규격 발주기관 확정 필요 |
|
||||||
| 20 | CertAuthService 구현 (3차 적용) | ✅ 완료 | [Fork C] `CertAuthService` 실구현. 기존 `EgovLoginServiceImpl.actionCrtfctLogin`(:108)→`COMTNEMPLYRINFO.CRTFC_DN_VALUE`(EgovLoginUsr_SQL_Mysql.xml:434) 위임, **인증서 본문 미저장·DN 식별값만 대조**. 핸드오프: `CertProcessFilter`(web.xml:46 주석) 재활성 운영협의 |
|
| 20 | CertAuthService 구현 (3차 적용- 공인인증서 ?) | ✅ 완료 | [Fork C] `CertAuthService` 실구현. 기존 `EgovLoginServiceImpl.actionCrtfctLogin`(:108)→`COMTNEMPLYRINFO.CRTFC_DN_VALUE`(EgovLoginUsr_SQL_Mysql.xml:434) 위임, **인증서 본문 미저장·DN 식별값만 대조**. 핸드오프: `CertProcessFilter`(web.xml:46 주석) 재활성 운영협의 |
|
||||||
| 21 | AuthMethodResolver 구현 | ✅ 완료 | [Fork C] `AuthMethodResolver`+`AuthMethodType`(enum)+`AuthMethodPolicyProvider`+`DefaultAuthMethodPolicyProvider` 4종. Map<code,impl> 주입 분기, 폴백 OTP→SMS→CERT, `resolve(String) throws Exception`(1차 계약 정합). javac 통과. 실동작은 #18~20 구현체·빈등록 후 |
|
| 21 | AuthMethodResolver 구현 | ✅ 완료 (?) | [Fork C] `AuthMethodResolver`+`AuthMethodType`(enum)+`AuthMethodPolicyProvider`+`DefaultAuthMethodPolicyProvider` 4종. Map<code,impl> 주입 분기, 폴백 OTP→SMS→CERT, `resolve(String) throws Exception`(1차 계약 정합). javac 통과. 실동작은 #18~20 구현체·빈등록 후 |
|
||||||
|
|
||||||
#### 5) 인증 실패 이력 및 접근통제 정책 (SER-001)
|
#### 5) 인증 실패 이력 및 접근통제 정책 (SER-001)
|
||||||
|
|
||||||
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
||||||
|:--:|----------|:----:|------------------|
|
|:--:|----------|:----:|------------------|
|
||||||
| 22 | 인증 실패 이력 활성 경로 이설 | ✅ 완료 | [Fork A] AS-IS: 실패이력/잠금이 `EgovSpringSecurityLoginFilter:466`(조건부)에만 존재. `EgovLoginController.actionLogin` 재작성(잠금선확인→성공시 이력정리→실패시 조건부 적재/잠금), **기존 LOGIN_HISTORY/LOGIN_LOCK_COUNT·서비스 7메서드 재사용(신규 statement 0)**, 전구간 fail-safe. 서비스/VO 메서드 실재 검증 |
|
| 22 | 인증 실패 이력 활성 경로 이설 | ✅ 진행중 | [ Fork A] AS-IS: 실패이력/잠금이 `EgovSpringSecurityLoginFilter:466`(조건부)에만 존재. `EgovLoginController.actionLogin` 재작성(잠금선확인→성공시 이력정리→실패시 조건부 적재/잠금), **기존 LOGIN_HISTORY/LOGIN_LOCK_COUNT·서비스 7메서드 재사용(신규 statement 0)**, 전구간 fail-safe. 서비스/VO 메서드 실재 검증 |
|
||||||
|
| 22-1 | 인증 실패 이력 IP 기록·보관기간·조회화면 | ✅ 완료 | AS-IS: `LOGIN_HISTORY`가 ID/CREATE_DATE/GUBUN 3컬럼뿐 → IP 미기록, 성공 시 당일 이력 DELETE 로 흔적 소멸. **DDL** `CONECT_IP`(VARCHAR45)+`RESET_YN`+인덱스 2종, `ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY`(기본 730). **적재** `insertFailHistory` 컬럼 명시 전환(★DDL 필수 동반 — 종전 무컬럼 VALUES 는 ERROR 1136)+`ClientIpResolver` IP 세팅, 성공 시 DELETE→`RESET_YN='Y'` 소프트 리셋(`selectFailCount` 가 'N'만 카운트 → 잠금 동작 불변). **화면** `/mnt/sec/loginFailHistory/listView.do`(계정·구분·IP·기간·리셋여부 검색+페이징), 메뉴 9100039. **배치** `purgeExpiredLoginFailHistoryScheduler` 매일 03:30, 1회 5,000건 상한 |
|
||||||
| 23 | 계정 열거 방지 처리 | ✅ 완료 | [Fork C] `EgovLoginController:238` 조건부 "현재 N회" 문구 제거→존재/미존재·실패횟수 무관 단일 `fail.common.login`. 실패이력·잠금은 내부 유지, 응답 노출 제거. 잠금안내만 의도적 예외 |
|
| 23 | 계정 열거 방지 처리 | ✅ 완료 | [Fork C] `EgovLoginController:238` 조건부 "현재 N회" 문구 제거→존재/미존재·실패횟수 무관 단일 `fail.common.login`. 실패이력·잠금은 내부 유지, 응답 노출 제거. 잠금안내만 의도적 예외 |
|
||||||
| 24 | 계정 잠금 정책 정비(10회→5회) | ✅ 완료 | [Fork A] 임계값 `LOGIN_LOCK_COUNT` UPDATE=5 + 소스 fallback `DEFAULT_LOCK_THRESHOLD=5`. 해제화면 기존확인. **신규 `ACCOUNT_UNLOCK_HISTORY`**(DDL+VO+DAO+sqlmap, sql-map-config 등록)에 해제이력 훅(해제자+ClientIpResolver IP, fail-safe). 사유입력은 화면 후속 |
|
| 24 | 계정 잠금 정책 정비(10회→5회) | ✅ 완료 | [Fork A] 임계값 `LOGIN_LOCK_COUNT` UPDATE=5 + 소스 fallback `DEFAULT_LOCK_THRESHOLD=5`. 해제화면 기존확인. **신규 `ACCOUNT_UNLOCK_HISTORY`**(DDL+VO+DAO+sqlmap, sql-map-config 등록)에 해제이력 훅(해제자+ClientIpResolver IP, fail-safe). 사유입력은 화면 후속 |
|
||||||
| 25 | 세션 타임아웃 조정(60→30분) | ✅ 완료 | [Fork C] `web.xml:218` `<session-timeout>30</session-timeout>`(60→30) + 근거주석. 실사용 webapp/web.xml 확정. 관리자 설정화면은 후속 |
|
| 25 | 세션 타임아웃 조정(60→30분) | ✅ 완료 | [Fork C] `web.xml:218` `<session-timeout>30</session-timeout>`(60→30) + 근거주석. 실사용 webapp/web.xml 확정. 관리자 설정화면은 후속 |
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user