개인정보보호 기능개선 8차 — 관리자 화면 4종 신설 제안서·RFP 대비 화면이 없어 증빙을 낼 수 없던 항목을 채운다. · 정기점검 리포트(SFR-004-7 #48) InspectionResultController 가 JSON 엔드포인트만 갖고 있어 점검결과를 화면으로 입력·보관할 수 없었다. listView/saveResultView 를 추가하고 기존 JSON 계약은 그대로 병존시킨다. 배치 집계값은 화면에서 수정하지 않는다 — 감사 증빙이라 사후 변경되면 안 된다. · 비정상접근 탐지(과업내용 다-① #49) 탐지 5유형 로직은 있었으나 호출처가 배치뿐이었다. AbnormalAccessResultVO 에 의심 대상 행(rows)을 실어 "누가 걸렸는지"를 요약 문자열 파싱 없이 보여주고, 각 항목에서 접속기록 화면으로 조건을 넘겨 원본을 확인한다. 탐지 결과를 적재하지 않는 이유는 컨트롤러 Javadoc 참조(임계치 변경 시 과거 기준이 굳는 문제). · 접속기록 파기이력(SER-002-2 / DBR-001-2) PDAL_PURGE_HISTORY 는 쌓이고 있었으나 볼 수 있는 곳이 없었다. "보관기간 경과분을 규정대로 파기했고 그 외 경로로는 손대지 않았다"를 함께 입증하는 증빙이라 조회 전용으로만 연다. · 회원정보 암호화 관리 CRUD(추가제안 Action4 / 제안서 슬라이드16) 복호화 조회·수정·파기·신규등록 + 사유 입력 + 재인증(10분 TTL). 목록은 항상 마스킹하고 평문은 사유를 남긴 상세 한 건에서만 보인다 — 목록에서 평문이 뜨면 사유를 받는 의미가 없다. 파기는 행을 지우지 않고 식별 항목만 비운다(신청·활동 기록이 참조 중). MBER_CRYPTO_ACCESS_HISTORY 에 처리자·일시·IP·사유·변경항목명을 남기되 개인정보 본문은 담지 않는다. 재인증에 로그인 2차인증을 재사용하지 않은 이유는 MberCryptoReauth Javadoc 참조(세션 내내 유지돼 재확인이 안 되고, OTP 미등록자가 잠긴다). 검증: JDK8 mvn compile EXIT=0, JSP 9종 Jasper 사전컴파일 오류 0건, 로컬 ROOT 반영 후 Tomcat 재기동 정상(신규 sqlMap 4건 기동시 파싱 통과), 신규 엔드포인트 6개 302→로그인 / 대조군 404 / frt/main.do 200. DB: MBER_CRYPTO_ACCESS_HISTORY DDL + 메뉴 9100048~9100051 (privacy.install_mysql.sql PART1·PART3 에도 병합, 재실행 안전) 후속 발견: 기존 탈퇴 구문 mberManageDAO.updateDeleteMber_S 는 개인정보를 비우면서도 이름(MBER_NM/ENC/HASH)은 지우지 않는다 — 파기 누락 소지. 신설 화면의 파기는 이름까지 파기한다. 기존 경로 보정은 별도 판단 필요. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> @
This commit is contained in:
parent
db1999175a
commit
5c7d064fbd
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Class Name : AbnormalAccessController.java
|
||||
* Description : 비정상 접근 탐지 알림함 화면 — 개인정보보호 기능개선 사업(SFR-004 / SER-002 / #49)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성 — 탐지 5유형(#35)의 화면 출력
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.auditlog;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import egovframework.com.cmm.LoginVO;
|
||||
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
||||
|
||||
/**
|
||||
* 비정상 접근 탐지 알림함 (과업내용 다-① "비정상 접근 탐지 및 기록", SER-002).
|
||||
*
|
||||
* <p>{@link AbnormalAccessDetectionService}(#35)의 5유형 탐지 결과를 기간 단위로 보여준다.
|
||||
* 화면: {@code /WEB-INF/jsp/site/mnt/sec/abnormal/list.jsp}</p>
|
||||
*
|
||||
* <p><b>왜 저장 테이블이 아니라 즉시 집계인가:</b> 탐지 임계치는 정책값이라 언제든 바뀐다
|
||||
* ({@link AbnormalAccessPolicy}). 탐지 결과를 별도 테이블에 적재해 두면 임계치를 조정한 뒤에도
|
||||
* 과거 기준으로 굳은 행이 남아 "지금 기준으로 몇 건인가"에 답할 수 없다. 원천인 접속기록은
|
||||
* 730일 보존되므로, 조회 시점 기준으로 다시 세는 편이 정확하다.
|
||||
* 월 단위 스냅샷이 필요한 경우는 정기점검 리포트(#43)가 {@code ABNORMAL_SUMMARY} 로 남긴다.</p>
|
||||
*
|
||||
* <p><b>조회 부하 주의:</b> 5유형 각각이 접속기록 전체를 기간으로 훑는 집계라 기간이 넓으면
|
||||
* 무거워진다. 그래서 기본 조회기간을 최근 30일로 두고, 최대 {@link #MAX_RANGE_DAYS}일로 제한한다.</p>
|
||||
*
|
||||
* <p><b>접근통제:</b> 인터셉터 ①인증판정이 비활성이라 화면 단에서 다시 판정한다
|
||||
* ({@link PersonalDataAccessLogController} 와 동일 방식). 롤 판정은 {@code urlRoleMapping}
|
||||
* 의 {@code /mnt/sec/*} = ROLE_ADMIN 이 담당한다.</p>
|
||||
*/
|
||||
@Controller
|
||||
public class AbnormalAccessController {
|
||||
|
||||
private static final String LOGIN_REDIRECT = "redirect:/uat/uia/egovLoginUsr.do";
|
||||
|
||||
/** 기본 조회기간(일) — 최근 30일. */
|
||||
private static final int DEFAULT_RANGE_DAYS = 30;
|
||||
|
||||
/** 최대 조회기간(일). 5유형 집계가 접속기록 전체를 훑으므로 상한을 둔다. */
|
||||
private static final int MAX_RANGE_DAYS = 366;
|
||||
|
||||
private static final String DATE_PATTERN = "yyyy-MM-dd";
|
||||
|
||||
@Resource(name = "abnormalAccessDetectionService")
|
||||
private AbnormalAccessDetectionService abnormalAccessDetectionService;
|
||||
|
||||
/**
|
||||
* 비정상 접근 탐지 결과 화면.
|
||||
*
|
||||
* @param fromDt 시작일(yyyy-MM-dd). 미지정 시 오늘 - 30일
|
||||
* @param toDt 종료일(yyyy-MM-dd). 미지정 시 오늘
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/abnormal/listView.do")
|
||||
public String listView(@RequestParam(value = "fromDt", required = false) String fromDt,
|
||||
@RequestParam(value = "toDt", required = false) String toDt,
|
||||
ModelMap model) throws Exception {
|
||||
if (handlerUser() == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
|
||||
SimpleDateFormat dateFmt = new SimpleDateFormat(DATE_PATTERN);
|
||||
Calendar cal = Calendar.getInstance();
|
||||
|
||||
String toDate = isBlank(toDt) ? dateFmt.format(cal.getTime()) : toDt.trim();
|
||||
String fromDate;
|
||||
if (isBlank(fromDt)) {
|
||||
cal.add(Calendar.DAY_OF_MONTH, -DEFAULT_RANGE_DAYS);
|
||||
fromDate = dateFmt.format(cal.getTime());
|
||||
} else {
|
||||
fromDate = fromDt.trim();
|
||||
}
|
||||
|
||||
String rangeMsg = null;
|
||||
long days = diffDays(dateFmt, fromDate, toDate);
|
||||
if (days < 0) {
|
||||
// 시작 > 종료 — 조회하지 않고 되돌린다(뒤집힌 조건으로 "이상징후 0건"이 나오면 오독한다)
|
||||
rangeMsg = "시작일이 종료일보다 뒤입니다. 기간을 다시 지정하세요.";
|
||||
} else if (days > MAX_RANGE_DAYS) {
|
||||
rangeMsg = "조회기간은 최대 " + MAX_RANGE_DAYS + "일까지입니다. 기간을 좁혀 주세요.";
|
||||
}
|
||||
|
||||
if (rangeMsg == null) {
|
||||
// 종료일 당일(23:59:59)까지 포함 — 접속기록 조회화면과 같은 규칙
|
||||
List<AbnormalAccessResultVO> results =
|
||||
abnormalAccessDetectionService.detect(fromDate + " 00:00:00", toDate + " 23:59:59");
|
||||
model.addAttribute("resultList", results);
|
||||
model.addAttribute("summary",
|
||||
abnormalAccessDetectionService.summarize(fromDate + " 00:00:00", toDate + " 23:59:59"));
|
||||
}
|
||||
|
||||
model.addAttribute("fromDt", fromDate);
|
||||
model.addAttribute("toDt", toDate);
|
||||
model.addAttribute("resultMsg", rangeMsg);
|
||||
return "site/mnt/sec/abnormal/list";
|
||||
}
|
||||
|
||||
/** 두 날짜(yyyy-MM-dd) 간 일수 차. 파싱 실패 시 0(=검증 통과)로 두고 조회를 진행한다. */
|
||||
private long diffDays(SimpleDateFormat fmt, String from, String to) {
|
||||
try {
|
||||
Date f = fmt.parse(from);
|
||||
Date t = fmt.parse(to);
|
||||
return (t.getTime() - f.getTime()) / (24L * 60L * 60L * 1000L);
|
||||
} catch (Exception e) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(String v) {
|
||||
return v == null || v.trim().length() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증된 업무사용자(USR)만 통과. 그 외에는 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;
|
||||
}
|
||||
}
|
||||
@ -51,8 +51,8 @@ public class AbnormalAccessDetectionServiceImpl implements AbnormalAccessDetecti
|
||||
|
||||
// ② 대량조회(의심 접속자 목록)
|
||||
List<Map<String, Object>> mass = abnormalAccessDetectionDAO.selectMassQuery(p);
|
||||
results.add(new AbnormalAccessResultVO(AbnormalAccessType.MASS_QUERY, size(mass),
|
||||
offenders(mass, "cnt", "건")));
|
||||
results.add(withRows(new AbnormalAccessResultVO(AbnormalAccessType.MASS_QUERY, size(mass),
|
||||
offenders(mass, "cnt", "건")), mass));
|
||||
|
||||
// ③ 비정상시간대접속
|
||||
int off = abnormalAccessDetectionDAO.countOffHours(p);
|
||||
@ -60,13 +60,13 @@ public class AbnormalAccessDetectionServiceImpl implements AbnormalAccessDetecti
|
||||
|
||||
// ④ 계정공유의심(의심 접속자 목록)
|
||||
List<Map<String, Object>> sharing = abnormalAccessDetectionDAO.selectAccountSharing(p);
|
||||
results.add(new AbnormalAccessResultVO(AbnormalAccessType.ACCOUNT_SHARING, size(sharing),
|
||||
offenders(sharing, "ipCnt", "IP")));
|
||||
results.add(withRows(new AbnormalAccessResultVO(AbnormalAccessType.ACCOUNT_SHARING, size(sharing),
|
||||
offenders(sharing, "ipCnt", "IP")), sharing));
|
||||
|
||||
// ⑤ 인증실패반복(의심 접속자 목록)
|
||||
List<Map<String, Object>> authFail = abnormalAccessDetectionDAO.selectRepeatedAuthFailure(p);
|
||||
results.add(new AbnormalAccessResultVO(AbnormalAccessType.REPEATED_AUTH_FAILURE, size(authFail),
|
||||
offenders(authFail, "cnt", "회")));
|
||||
results.add(withRows(new AbnormalAccessResultVO(AbnormalAccessType.REPEATED_AUTH_FAILURE, size(authFail),
|
||||
offenders(authFail, "cnt", "회")), authFail));
|
||||
|
||||
return results;
|
||||
}
|
||||
@ -91,6 +91,15 @@ public class AbnormalAccessDetectionServiceImpl implements AbnormalAccessDetecti
|
||||
return sb.length() == 0 ? "(이상징후 없음)" : sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상자형 유형에 의심 대상 상세 행을 실어 준다(알림함 화면 #49 용).
|
||||
* 요약 문자열({@code detail})은 종전 그대로라 리포트(#43) 동작에는 영향이 없다.
|
||||
*/
|
||||
private static AbnormalAccessResultVO withRows(AbnormalAccessResultVO vo, List<Map<String, Object>> rows) {
|
||||
vo.setRows(rows);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static int size(List<Map<String, Object>> list) {
|
||||
return list == null ? 0 : list.size();
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package egovframework.com.cmm.privacy.auditlog;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 비정상 접근 탐지 결과 1유형 (#35).
|
||||
@ -38,4 +40,16 @@ public class AbnormalAccessResultVO implements Serializable {
|
||||
|
||||
public String getDetail() { return detail; }
|
||||
public void setDetail(String detail) { this.detail = detail; }
|
||||
|
||||
/**
|
||||
* 의심 대상 상세 행(접속자ID + 건수/IP수). 대상자형 유형(대량조회·계정공유·인증실패반복)에만 있고,
|
||||
* 건수형(비인가접근·비정상시간대)은 비어 있다 — 이 둘은 특정 대상자가 아니라 기간 전체의 건수다.
|
||||
*
|
||||
* <p>알림함 화면(#49)이 "누가 걸렸는지"를 요약 문자열에서 파싱하지 않도록 원본 행을 그대로 싣는다.
|
||||
* 개인정보 본문은 담지 않는다(접속자ID·집계값뿐).</p>
|
||||
*/
|
||||
private List<Map<String, Object>> rows;
|
||||
|
||||
public List<Map<String, Object>> getRows() { return rows; }
|
||||
public void setRows(List<Map<String, Object>> rows) { this.rows = rows; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Class Name : AccessLogPurgeHistoryController.java
|
||||
* Description : 개인정보 접속기록 파기이력 조회 화면 — 개인정보보호 기능개선 사업(SFR-004 / DBR-001 / SER-002)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성 — 파기 배치(#37) 이력의 화면 출력
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.auditlog;
|
||||
|
||||
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.privacy.batch.AccessLogPartitionDAO;
|
||||
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
||||
import egovframework.rte.ptl.mvc.tags.ui.pagination.PaginationInfo;
|
||||
|
||||
/**
|
||||
* 접속기록 파기·증설 이력 조회 컨트롤러 (SER-002-2 / DBR-001-2).
|
||||
*
|
||||
* <p>보관기간(730일) 경과 파티션을 파기한 내역과, 미래 월 파티션을 미리 확보한 내역을 보여준다.
|
||||
* 화면: {@code /WEB-INF/jsp/site/mnt/sec/purgeHistory/list.jsp}</p>
|
||||
*
|
||||
* <p><b>조회 전용이다.</b> 파기 이력 자체가 "접속기록을 규정대로 지웠고 그 외 경로로는 손대지
|
||||
* 않았다"는 증빙이므로, 화면에서 이력을 고치거나 지울 수 있으면 증빙 가치가 사라진다.
|
||||
* 등록·수정·삭제 엔드포인트를 두지 않는다.</p>
|
||||
*
|
||||
* <p><b>접근통제:</b> 인터셉터 3단 중 ①인증판정이 비활성이라 /mnt 하위라도 무인증 진입이
|
||||
* 가능하므로 {@link #handlerUser()} 로 화면 단에서 다시 판정한다
|
||||
* ({@link PersonalDataAccessLogController} 와 동일 방식). 롤 판정은 {@code urlRoleMapping}
|
||||
* 의 {@code /mnt/sec/*} = ROLE_ADMIN 이 담당한다.</p>
|
||||
*/
|
||||
@Controller
|
||||
public class AccessLogPurgeHistoryController {
|
||||
|
||||
private static final String LOGIN_REDIRECT = "redirect:/uat/uia/egovLoginUsr.do";
|
||||
|
||||
@Resource(name = "accessLogPartitionDAO")
|
||||
private AccessLogPartitionDAO accessLogPartitionDAO;
|
||||
|
||||
/**
|
||||
* 파기이력 목록 화면.
|
||||
* 뷰: /WEB-INF/jsp/site/mnt/sec/purgeHistory/list.jsp
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/purgeHistory/listView.do")
|
||||
public String listView(@ModelAttribute("accessLogPurgeHistorySearchVO") AccessLogPurgeHistorySearchVO searchVO,
|
||||
ModelMap model) throws Exception {
|
||||
if (handlerUser() == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
|
||||
if (searchVO.getPageIndex() < 1) {
|
||||
searchVO.setPageIndex(1);
|
||||
}
|
||||
if (searchVO.getRecordCountPerPage() < 1) {
|
||||
searchVO.setRecordCountPerPage(10);
|
||||
}
|
||||
searchVO.setFirstIndex((searchVO.getPageIndex() - 1) * searchVO.getRecordCountPerPage());
|
||||
|
||||
int totalCount = accessLogPartitionDAO.selectPurgeHistoryListCount(searchVO);
|
||||
searchVO.setTotalRecordCount(totalCount);
|
||||
List<AccessLogPurgeHistoryVO> list = accessLogPartitionDAO.selectPurgeHistoryList(searchVO);
|
||||
|
||||
PaginationInfo paginationInfo = new PaginationInfo();
|
||||
paginationInfo.setCurrentPageNo(searchVO.getPageIndex());
|
||||
paginationInfo.setRecordCountPerPage(searchVO.getRecordCountPerPage());
|
||||
paginationInfo.setPageSize(10);
|
||||
paginationInfo.setTotalRecordCount(totalCount);
|
||||
|
||||
model.addAttribute("resultList", list);
|
||||
model.addAttribute("paginationInfo", paginationInfo);
|
||||
return "site/mnt/sec/purgeHistory/list";
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증된 업무사용자(USR)만 통과. 그 외에는 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,77 @@
|
||||
/**
|
||||
* Class Name : AccessLogPurgeHistorySearchVO.java
|
||||
* Description : 접속기록 파기이력 검색조건 VO — 개인정보보호 기능개선 사업(SFR-004 / SER-002)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.auditlog;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 파기이력 조회 검색조건 + 페이징.
|
||||
*
|
||||
* <p><b>종료일 보정:</b> 사용자가 {@code 2026-08-27} 처럼 날짜만 넣으면 그날 00:00:00 이 되어
|
||||
* 당일 파기분이 통째로 빠진다. {@link #getToDtParam()} 가 날짜만 들어온 경우
|
||||
* {@code 23:59:59} 를 붙여 당일을 포함시킨다 — 접속기록 조회화면
|
||||
* ({@link PersonalDataAccessLogSearchVO})과 같은 규칙이라 두 화면의 기간 검색이 어긋나지 않는다.</p>
|
||||
*/
|
||||
public class AccessLogPurgeHistorySearchVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 유형 — DROP / ADD / 전체(빈값). */
|
||||
private String purgeType;
|
||||
/** 수행일시 시작(yyyy-MM-dd 또는 yyyy-MM-dd HH:mm:ss). */
|
||||
private String fromDt;
|
||||
/** 수행일시 종료. */
|
||||
private String toDt;
|
||||
|
||||
private int pageIndex = 1;
|
||||
private int recordCountPerPage = 10;
|
||||
/** 조회 시작 offset(0-base) = (pageIndex-1) * recordCountPerPage */
|
||||
private int firstIndex = 0;
|
||||
private int totalRecordCount = 0;
|
||||
|
||||
public String getPurgeType() { return purgeType; }
|
||||
public void setPurgeType(String purgeType) { this.purgeType = purgeType; }
|
||||
|
||||
public String getFromDt() { return fromDt; }
|
||||
public void setFromDt(String fromDt) { this.fromDt = fromDt; }
|
||||
|
||||
public String getToDt() { return toDt; }
|
||||
public void setToDt(String toDt) { this.toDt = toDt; }
|
||||
|
||||
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; }
|
||||
|
||||
/** 시작일시 바인딩값 — 날짜만 들어오면 00:00:00 을 붙인다. */
|
||||
public String getFromDtParam() {
|
||||
return isDateOnly(fromDt) ? fromDt + " 00:00:00" : fromDt;
|
||||
}
|
||||
|
||||
/** 종료일시 바인딩값 — 날짜만 들어오면 당일 23:59:59 까지 포함시킨다. */
|
||||
public String getToDtParam() {
|
||||
return isDateOnly(toDt) ? toDt + " 23:59:59" : toDt;
|
||||
}
|
||||
|
||||
private boolean isDateOnly(String v) {
|
||||
return v != null && v.trim().length() == 10;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Class Name : AccessLogPurgeHistoryVO.java
|
||||
* Description : 개인정보 접속기록 파기·증설 이력 VO — 개인정보보호 기능개선 사업(SFR-004 / DBR-001 / SER-002)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성 — 파기이력 조회 화면(#37 후속)
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.auditlog;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* {@code PDAL_PURGE_HISTORY} 한 행 — 파기 배치(#37)가 수행한 파티션 DROP(파기)·ADD(선행증설) 내역.
|
||||
*
|
||||
* <p><b>이 이력이 증빙인 이유:</b> 접속기록 본문 테이블은 UPDATE/DELETE statement 자체를 두지 않고,
|
||||
* 삭제는 오직 파티션 DROP 으로만 일어난다. 그래서 "언제 무엇이 지워졌는가"를 답할 수 있는 유일한
|
||||
* 근거가 이 표다 — 보관기간 경과분을 실제로 파기했다는 사실(SER-002-2)과, 그 외 경로로는 지운 적이
|
||||
* 없다는 사실을 함께 보인다.</p>
|
||||
*/
|
||||
public class AccessLogPurgeHistoryVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 이력 일련번호. */
|
||||
private long purgeId;
|
||||
/** 대상 파티션명(예: p202607). */
|
||||
private String partitionName;
|
||||
/** 파티션 상한 경계값(LESS THAN). */
|
||||
private String boundaryDt;
|
||||
/** 유형 — DROP:파기 / ADD:선행증설. */
|
||||
private String purgeType;
|
||||
/** 수행 일시. */
|
||||
private String purgeDt;
|
||||
/** 비고(보관기간·사유). */
|
||||
private String note;
|
||||
|
||||
public long getPurgeId() { return purgeId; }
|
||||
public void setPurgeId(long purgeId) { this.purgeId = purgeId; }
|
||||
|
||||
public String getPartitionName() { return partitionName; }
|
||||
public void setPartitionName(String partitionName) { this.partitionName = partitionName; }
|
||||
|
||||
public String getBoundaryDt() { return boundaryDt; }
|
||||
public void setBoundaryDt(String boundaryDt) { this.boundaryDt = boundaryDt; }
|
||||
|
||||
public String getPurgeType() { return purgeType; }
|
||||
public void setPurgeType(String purgeType) { this.purgeType = purgeType; }
|
||||
|
||||
public String getPurgeDt() { return purgeDt; }
|
||||
public void setPurgeDt(String purgeDt) { this.purgeDt = purgeDt; }
|
||||
|
||||
public String getNote() { return note; }
|
||||
public void setNote(String note) { this.note = note; }
|
||||
}
|
||||
@ -5,6 +5,8 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistorySearchVO;
|
||||
import egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistoryVO;
|
||||
import egovframework.com.cmm.service.impl.EgovComAbstractDAO;
|
||||
|
||||
/**
|
||||
@ -54,4 +56,24 @@ public class AccessLogPartitionDAO extends EgovComAbstractDAO {
|
||||
public void insertPurgeHistory(Map<String, String> param) throws Exception {
|
||||
insert("PersonalDataAccessLogDAO.insertPurgeHistory", param);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파기·증설 이력 목록(화면 조회용, 페이징).
|
||||
*
|
||||
* <p>배치가 쓰는 DAO 에 조회를 함께 두는 이유는 {@code PDAL_PURGE_HISTORY} 의 소유자가
|
||||
* 이 클래스이기 때문이다. 이력을 읽는 statement 를 다른 sqlmap 으로 흩어 놓으면
|
||||
* 적재 컬럼이 바뀔 때 조회 쪽이 조용히 어긋난다.</p>
|
||||
*
|
||||
* @param searchVO 유형·기간·페이징
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<AccessLogPurgeHistoryVO> selectPurgeHistoryList(AccessLogPurgeHistorySearchVO searchVO) {
|
||||
return list("PersonalDataAccessLogDAO.selectPurgeHistoryList", searchVO);
|
||||
}
|
||||
|
||||
/** 파기·증설 이력 총건수(페이징용). 목록과 <b>같은 검색조건</b>을 유지할 것. */
|
||||
public int selectPurgeHistoryListCount(AccessLogPurgeHistorySearchVO searchVO) {
|
||||
Object cnt = selectByPk("PersonalDataAccessLogDAO.selectPurgeHistoryListCount", searchVO);
|
||||
return cnt == null ? 0 : ((Number) cnt).intValue();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package egovframework.com.cmm.privacy.batch;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -12,13 +13,20 @@ import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import egovframework.com.cmm.LoginVO;
|
||||
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
||||
|
||||
/**
|
||||
* 정기점검 결과 입력·보관 컨트롤러 (#44, SFR-004/SER-002).
|
||||
*
|
||||
* <p>취급자 영역(/mnt) 백엔드 엔드포인트. 응답은 jsonView(화면 JSP는 #48 대시보드 후속).
|
||||
* <p>취급자 영역(/mnt) 엔드포인트. 기존 {@code list.do}/{@code detail.do}/{@code saveResult.do}
|
||||
* 는 jsonView 로 응답하는 백엔드 계약이고, 2026.08.27 추가된 {@code listView.do}/
|
||||
* {@code saveResultView.do} 가 관리자 화면(#48)이다 — 두 계약을 병존시켜 기존 호출부를 깨지 않는다.
|
||||
* 점검자 식별자는 세션 loginVO 에서 취득한다.</p>
|
||||
*
|
||||
* <p><b>화면이 필요한 이유:</b> 월 1회 점검 자체는 배치(#43)가 집계하지만, "누가 언제 보고 무엇을
|
||||
* 조치했는가"는 사람이 입력해야 남는다. 그 입력 경로가 JSON 엔드포인트뿐이면 점검 사실을
|
||||
* 증빙으로 제출할 수 없다(SFR-004-7).</p>
|
||||
*
|
||||
* @author arko-privacy-auditlog (Fork C - 7차)
|
||||
* @since 2026-07-21
|
||||
*/
|
||||
@ -27,6 +35,8 @@ public class InspectionResultController {
|
||||
|
||||
protected static final String JSON = "jsonView";
|
||||
|
||||
private static final String LOGIN_REDIRECT = "redirect:/uat/uia/egovLoginUsr.do";
|
||||
|
||||
@Resource(name = "inspectionResultService")
|
||||
private InspectionResultService inspectionResultService;
|
||||
|
||||
@ -35,6 +45,82 @@ public class InspectionResultController {
|
||||
return (o instanceof LoginVO) ? (LoginVO) o : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 정기점검 리포트 목록·상세 화면(서버 렌더링, #48).
|
||||
* 뷰: /WEB-INF/jsp/site/mnt/sec/inspection/list.jsp
|
||||
*
|
||||
* <p>월 1행짜리 표라 페이징을 두지 않는다(연 12행). 연도 필터와 점검여부 필터만 제공한다.
|
||||
* {@code rptId} 가 있으면 그 리포트 상세와 점검결과 입력 폼을 함께 편다 —
|
||||
* 목록에서 상세로 화면을 옮기면 점검 중 다른 달과 대조하기가 불편하다.</p>
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/inspection/listView.do")
|
||||
public String listView(@org.springframework.web.bind.annotation.RequestParam(value = "rptYmPrefix", required = false) String rptYmPrefix,
|
||||
@org.springframework.web.bind.annotation.RequestParam(value = "inspectedYn", required = false) String inspectedYn,
|
||||
@org.springframework.web.bind.annotation.RequestParam(value = "rptId", required = false) Long rptId,
|
||||
@org.springframework.web.bind.annotation.RequestParam(value = "resultMsg", required = false) String resultMsg,
|
||||
ModelMap model) throws Exception {
|
||||
if (handlerUser() == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
Map<String, Object> param = new HashMap<String, Object>();
|
||||
param.put("rptYmPrefix", rptYmPrefix);
|
||||
param.put("inspectedYn", inspectedYn);
|
||||
List<InspectionReportVO> list = inspectionResultService.selectReportList(param);
|
||||
|
||||
model.addAttribute("resultList", list);
|
||||
model.addAttribute("totalCount", list == null ? 0 : list.size());
|
||||
model.addAttribute("rptYmPrefix", rptYmPrefix);
|
||||
model.addAttribute("inspectedYn", inspectedYn);
|
||||
model.addAttribute("resultMsg", resultMsg);
|
||||
if (rptId != null) {
|
||||
model.addAttribute("report", inspectionResultService.selectReport(rptId.longValue()));
|
||||
}
|
||||
return "site/mnt/sec/inspection/list";
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검결과 입력·보관(화면용, #48). 저장 후 같은 리포트를 펼친 목록으로 되돌린다.
|
||||
*
|
||||
* <p>배치가 적재한 집계값은 손대지 않고 점검자 입력 필드만 갱신한다
|
||||
* (감사 증빙이므로 집계값이 사후에 바뀌면 안 된다).</p>
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/inspection/saveResultView.do")
|
||||
public String saveResultView(InspectionReportVO vo, HttpServletRequest request) throws Exception {
|
||||
LoginVO user = handlerUser();
|
||||
if (user == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
vo.setInspectorId(user.getId());
|
||||
String message;
|
||||
try {
|
||||
int cnt = inspectionResultService.saveInspectionResult(vo);
|
||||
message = (cnt > 0) ? "점검결과를 저장했습니다." : "저장 대상 리포트가 없습니다.";
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 점검결과 미입력 등 유효성 위반 — 화면에 사유를 그대로 보여준다
|
||||
message = e.getMessage();
|
||||
}
|
||||
// Spring 3.0.5 에는 RedirectAttributes 가 없다(3.1 도입). 이 코드베이스의 관용대로
|
||||
// 결과 메시지를 URL 인코딩해 쿼리스트링으로 넘긴다(AccountMngController 와 동일 방식).
|
||||
return "redirect:/mnt/sec/inspection/listView.do?rptId=" + vo.getRptId()
|
||||
+ "&resultMsg=" + URLEncoder.encode(message == null ? "" : message, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증된 업무사용자(USR)만 통과. 그 외에는 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;
|
||||
}
|
||||
|
||||
/** 정기점검 리포트 월별 목록 (검색: rptYmPrefix 연도, inspectedYn Y/N) */
|
||||
@RequestMapping(value = "/mnt/sec/inspection/list.do")
|
||||
public String list(@org.springframework.web.bind.annotation.RequestParam(value = "rptYmPrefix", required = false) String rptYmPrefix,
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Class Name : MberCryptoAccessHistoryVO.java
|
||||
* Description : 암호화 회원정보 관리자 접근 이력 VO — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성 — 사유·재인증 이력
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* {@code MBER_CRYPTO_ACCESS_HISTORY} 한 행 — 관리자가 평문 개인정보를 열람·변경한 행위의 소명 근거.
|
||||
*
|
||||
* <p><b>개인정보 본문은 담지 않는다.</b> 무엇을 바꿨는지는 {@link #changedItems}(항목명 목록)로만
|
||||
* 남기고 값은 적재하지 않는다 — 이력이 또 하나의 평문 개인정보 저장소가 되면 안 된다.</p>
|
||||
*/
|
||||
public class MberCryptoAccessHistoryVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 행위 — 복호화 조회. */
|
||||
public static final String ACTION_SELECT = "S";
|
||||
/** 행위 — 신규 등록. */
|
||||
public static final String ACTION_INSERT = "I";
|
||||
/** 행위 — 수정. */
|
||||
public static final String ACTION_UPDATE = "U";
|
||||
/** 행위 — 개인정보 파기. */
|
||||
public static final String ACTION_DELETE = "D";
|
||||
|
||||
private long histId;
|
||||
/** 처리자(취급자) 계정ID. */
|
||||
private String handlrId;
|
||||
/** 행위 S/I/U/D. */
|
||||
private String actionTy;
|
||||
/** 대상 회원ID. */
|
||||
private String trgetMberId;
|
||||
/** 사유(필수). */
|
||||
private String accessReason;
|
||||
/** 변경 항목명 목록(값 미적재). */
|
||||
private String changedItems;
|
||||
/** 처리자 IP. */
|
||||
private String handlrIp;
|
||||
/** 재인증 통과 여부 Y/N. */
|
||||
private String reauthYn;
|
||||
/** 처리 일시. */
|
||||
private String accessDt;
|
||||
|
||||
public long getHistId() { return histId; }
|
||||
public void setHistId(long histId) { this.histId = histId; }
|
||||
|
||||
public String getHandlrId() { return handlrId; }
|
||||
public void setHandlrId(String handlrId) { this.handlrId = handlrId; }
|
||||
|
||||
public String getActionTy() { return actionTy; }
|
||||
public void setActionTy(String actionTy) { this.actionTy = actionTy; }
|
||||
|
||||
public String getTrgetMberId() { return trgetMberId; }
|
||||
public void setTrgetMberId(String trgetMberId) { this.trgetMberId = trgetMberId; }
|
||||
|
||||
public String getAccessReason() { return accessReason; }
|
||||
public void setAccessReason(String accessReason) { this.accessReason = accessReason; }
|
||||
|
||||
public String getChangedItems() { return changedItems; }
|
||||
public void setChangedItems(String changedItems) { this.changedItems = changedItems; }
|
||||
|
||||
public String getHandlrIp() { return handlrIp; }
|
||||
public void setHandlrIp(String handlrIp) { this.handlrIp = handlrIp; }
|
||||
|
||||
public String getReauthYn() { return reauthYn; }
|
||||
public void setReauthYn(String reauthYn) { this.reauthYn = reauthYn; }
|
||||
|
||||
public String getAccessDt() { return accessDt; }
|
||||
public void setAccessDt(String accessDt) { this.accessDt = accessDt; }
|
||||
|
||||
/** 행위코드의 한글 표기(화면·리포트용). */
|
||||
public String getActionNm() {
|
||||
if (ACTION_SELECT.equals(actionTy)) { return "복호화조회"; }
|
||||
if (ACTION_INSERT.equals(actionTy)) { return "등록"; }
|
||||
if (ACTION_UPDATE.equals(actionTy)) { return "수정"; }
|
||||
if (ACTION_DELETE.equals(actionTy)) { return "개인정보파기"; }
|
||||
return actionTy;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Class Name : MberCryptoAdminController.java
|
||||
* Description : 암호화 회원정보 관리자 전용 CRUD 화면 — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성 — 복호화 조회·수정·삭제·등록 + 사유·재인증
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
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 org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import egovframework.com.cmm.LoginVO;
|
||||
import egovframework.com.cmm.privacy.auditlog.ClientIpResolver;
|
||||
import egovframework.com.cmm.privacy.util.MaskingUtil;
|
||||
import egovframework.com.cmm.util.EgovUserDetailsHelper;
|
||||
import egovframework.rte.ptl.mvc.tags.ui.pagination.PaginationInfo;
|
||||
|
||||
/**
|
||||
* 관리자 전용 회원정보(암호화 항목) 화면 — 제안서 추가제안 Action4(슬라이드16).
|
||||
*
|
||||
* <p><b>화면 구성</b>
|
||||
* <ul>
|
||||
* <li>{@code listView.do} 목록 — <b>항상 마스킹</b>해서 보여준다(이름 홍*동, 휴대폰 010-****-1234).</li>
|
||||
* <li>{@code view.do} 상세 — <b>사유 입력 + 재인증</b>을 통과해야 평문이 보인다.</li>
|
||||
* <li>{@code registView.do}/{@code regist.do} 신규 등록, {@code update.do} 수정,
|
||||
* {@code deletePii.do} 개인정보 파기 — 모두 사유·재인증 필수.</li>
|
||||
* <li>{@code historyView.do} 이 화면에서 일어난 접근 이력(사유 포함) 조회.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>목록을 마스킹으로 고정한 이유:</b> 관리자가 목록만 열어도 평문 이름·연락처가 화면 한 장에
|
||||
* 수십 건씩 뜨면, 사유를 받는 의미가 사라진다(정작 대량 노출은 목록에서 일어난다).
|
||||
* 평문은 대상을 특정해 사유를 남긴 <u>상세 한 건</u>에서만 보이게 한다.</p>
|
||||
*
|
||||
* <p><b>접근통제 3중:</b> ① {@code urlRoleMapping} 의 {@code /mnt/sec/*} = ROLE_ADMIN(전체관리자
|
||||
* 전용) ② {@link #handlerUser()} 로 업무사용자(USR) 세션 재확인 ③ 평문에 닿는 모든 경로에서
|
||||
* {@link MberCryptoReauth} 재인증(10분). 여기에 접속기록 인터셉터가 {@code /mnt/sec/**} 를
|
||||
* 자동 수집하므로 '언제 누가 열었는가'는 별도로도 남는다.</p>
|
||||
*
|
||||
* <p><b>상태 변경은 POST 만 받는다.</b> 등록·수정·파기가 GET 으로도 열리면 링크 한 번, 이미지
|
||||
* 태그 하나로 실행될 수 있다.</p>
|
||||
*/
|
||||
@Controller
|
||||
public class MberCryptoAdminController {
|
||||
|
||||
private static final String LOGIN_REDIRECT = "redirect:/uat/uia/egovLoginUsr.do";
|
||||
private static final String LIST_REDIRECT = "redirect:/mnt/sec/mberCrypto/listView.do?page=9100051";
|
||||
|
||||
/** 재인증 화면으로 되돌아올 수 있는 경로의 접두사. 이 밖의 값은 받지 않는다(오픈 리다이렉트 차단). */
|
||||
private static final String RETURN_URL_PREFIX = "/mnt/sec/mberCrypto/";
|
||||
|
||||
@Resource(name = "mberCryptoAdminService")
|
||||
private MberCryptoAdminService mberCryptoAdminService;
|
||||
|
||||
/* ================================================================
|
||||
* 목록 — 항상 마스킹
|
||||
* ================================================================ */
|
||||
|
||||
/**
|
||||
* 회원정보 목록(마스킹). 뷰: /WEB-INF/jsp/site/mnt/sec/mberCrypto/list.jsp
|
||||
*
|
||||
* <p>이름 검색은 완전일치만 된다(암호문은 부분검색이 원리적으로 불가). 그 사실을 화면 안내에
|
||||
* 명시하고, 검색 결과가 0건일 때 "부분일치로는 찾을 수 없다"는 점을 함께 보여준다.</p>
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/listView.do")
|
||||
public String listView(@ModelAttribute("mberCryptoSearchVO") MberCryptoSearchVO searchVO,
|
||||
@RequestParam(value = "resultMsg", required = false) String resultMsg,
|
||||
HttpServletRequest request, ModelMap model) throws Exception {
|
||||
if (handlerUser() == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
|
||||
if (searchVO.getPageIndex() < 1) {
|
||||
searchVO.setPageIndex(1);
|
||||
}
|
||||
if (searchVO.getRecordCountPerPage() < 1) {
|
||||
searchVO.setRecordCountPerPage(10);
|
||||
}
|
||||
searchVO.setFirstIndex((searchVO.getPageIndex() - 1) * searchVO.getRecordCountPerPage());
|
||||
|
||||
List<MberCryptoVO> list = mberCryptoAdminService.selectMberList(searchVO);
|
||||
maskAll(list);
|
||||
|
||||
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);
|
||||
model.addAttribute("resultMsg", resultMsg);
|
||||
model.addAttribute("reauthRemain", Long.valueOf(MberCryptoReauth.remainSeconds(request)));
|
||||
return "site/mnt/sec/mberCrypto/list";
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* 재인증
|
||||
* ================================================================ */
|
||||
|
||||
/** 재인증 입력 화면. 뷰: /WEB-INF/jsp/site/mnt/sec/mberCrypto/reauth.jsp */
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/reauthView.do")
|
||||
public String reauthView(@RequestParam(value = "returnUrl", required = false) String returnUrl,
|
||||
@RequestParam(value = "resultMsg", required = false) String resultMsg,
|
||||
ModelMap model) throws Exception {
|
||||
if (handlerUser() == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
model.addAttribute("returnUrl", safeReturnUrl(returnUrl));
|
||||
model.addAttribute("resultMsg", resultMsg);
|
||||
return "site/mnt/sec/mberCrypto/reauth";
|
||||
}
|
||||
|
||||
/**
|
||||
* 재인증 처리(본인 비밀번호 재확인). 성공하면 원래 가려던 화면으로 돌려보낸다.
|
||||
*
|
||||
* <p>실패 문구는 사유를 구분하지 않는다(계정 없음/비밀번호 불일치 동일) — 계정 열거를 막는
|
||||
* 이 사업의 기존 처리(#23)와 같은 방침이다.</p>
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/reauth.do", method = org.springframework.web.bind.annotation.RequestMethod.POST)
|
||||
public String reauth(@RequestParam(value = "password", required = false) String password,
|
||||
@RequestParam(value = "returnUrl", required = false) String returnUrl,
|
||||
HttpServletRequest request) throws Exception {
|
||||
LoginVO user = handlerUser();
|
||||
if (user == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
String target = safeReturnUrl(returnUrl);
|
||||
|
||||
if (mberCryptoAdminService.verifyHandlerPassword(user.getId(), password)) {
|
||||
MberCryptoReauth.markVerified(request);
|
||||
return "redirect:" + target;
|
||||
}
|
||||
MberCryptoReauth.clear(request);
|
||||
return "redirect:/mnt/sec/mberCrypto/reauthView.do?returnUrl=" + URLEncoder.encode(target, "UTF-8")
|
||||
+ "&resultMsg=" + URLEncoder.encode("본인 확인에 실패했습니다. 비밀번호를 다시 입력하세요.", "UTF-8");
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* 상세(복호화) — 사유 + 재인증
|
||||
* ================================================================ */
|
||||
|
||||
/**
|
||||
* 회원 상세(평문). 사유 미입력이면 열지 않고, 재인증이 만료됐으면 재인증 화면으로 보낸다.
|
||||
* 뷰: /WEB-INF/jsp/site/mnt/sec/mberCrypto/view.jsp
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/view.do")
|
||||
public String view(@RequestParam(value = "mberId", required = false) String mberId,
|
||||
@RequestParam(value = "reason", required = false) String reason,
|
||||
@RequestParam(value = "resultMsg", required = false) String resultMsg,
|
||||
HttpServletRequest request, ModelMap model) throws Exception {
|
||||
LoginVO user = handlerUser();
|
||||
if (user == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
if (isBlank(mberId)) {
|
||||
return listRedirect("대상 회원이 지정되지 않았습니다.");
|
||||
}
|
||||
if (isBlank(reason)) {
|
||||
return listRedirect("개인정보 조회 사유는 필수입니다. 목록에서 사유를 입력한 뒤 조회하세요.");
|
||||
}
|
||||
if (!MberCryptoReauth.isVerified(request)) {
|
||||
return reauthRedirect(viewUrl(mberId, reason));
|
||||
}
|
||||
|
||||
MberCryptoVO vo = mberCryptoAdminService.selectMber(mberId);
|
||||
if (vo == null) {
|
||||
return listRedirect("대상 회원을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 평문을 실제로 화면에 띄우는 시점에 사유와 함께 이력을 남긴다.
|
||||
mberCryptoAdminService.recordDecryptView(mberId, user.getId(),
|
||||
ClientIpResolver.getClientIp(request), reason);
|
||||
|
||||
model.addAttribute("mber", vo);
|
||||
model.addAttribute("reason", reason);
|
||||
model.addAttribute("resultMsg", resultMsg);
|
||||
model.addAttribute("reauthRemain", Long.valueOf(MberCryptoReauth.remainSeconds(request)));
|
||||
return "site/mnt/sec/mberCrypto/view";
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* 등록 / 수정 / 파기 — 모두 POST + 사유 + 재인증
|
||||
* ================================================================ */
|
||||
|
||||
/** 신규 등록 화면. 뷰: /WEB-INF/jsp/site/mnt/sec/mberCrypto/regist.jsp */
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/registView.do")
|
||||
public String registView(@ModelAttribute("mberCryptoVO") MberCryptoVO vo,
|
||||
@RequestParam(value = "resultMsg", required = false) String resultMsg,
|
||||
HttpServletRequest request, ModelMap model) throws Exception {
|
||||
if (handlerUser() == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
if (!MberCryptoReauth.isVerified(request)) {
|
||||
return reauthRedirect(RETURN_URL_PREFIX + "registView.do?page=9100051");
|
||||
}
|
||||
model.addAttribute("resultMsg", resultMsg);
|
||||
model.addAttribute("reauthRemain", Long.valueOf(MberCryptoReauth.remainSeconds(request)));
|
||||
return "site/mnt/sec/mberCrypto/regist";
|
||||
}
|
||||
|
||||
/** 신규 등록 처리. */
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/regist.do", method = org.springframework.web.bind.annotation.RequestMethod.POST)
|
||||
public String regist(@ModelAttribute("mberCryptoVO") MberCryptoVO vo,
|
||||
@RequestParam(value = "reason", required = false) String reason,
|
||||
HttpServletRequest request) throws Exception {
|
||||
LoginVO user = handlerUser();
|
||||
if (user == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
if (!MberCryptoReauth.isVerified(request)) {
|
||||
return reauthRedirect(RETURN_URL_PREFIX + "registView.do?page=9100051");
|
||||
}
|
||||
try {
|
||||
String mberId = mberCryptoAdminService.registMber(vo, user.getId(),
|
||||
ClientIpResolver.getClientIp(request), reason);
|
||||
return listRedirect("회원을 등록했습니다: " + mberId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return registRedirect(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 수정 처리. */
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/update.do", method = org.springframework.web.bind.annotation.RequestMethod.POST)
|
||||
public String update(@ModelAttribute("mberCryptoVO") MberCryptoVO vo,
|
||||
@RequestParam(value = "reason", required = false) String reason,
|
||||
HttpServletRequest request) throws Exception {
|
||||
LoginVO user = handlerUser();
|
||||
if (user == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
if (!MberCryptoReauth.isVerified(request)) {
|
||||
return reauthRedirect(viewUrl(vo.getMberId(), reason));
|
||||
}
|
||||
try {
|
||||
int cnt = mberCryptoAdminService.updateMber(vo, user.getId(),
|
||||
ClientIpResolver.getClientIp(request), reason);
|
||||
return listRedirect(cnt > 0 ? "회원정보를 수정했습니다." : "수정 대상이 없습니다.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
return listRedirect(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개인정보 파기 처리.
|
||||
*
|
||||
* <p>되돌릴 수 없는 작업이라 화면에서 회원ID를 직접 입력해 확인시키고
|
||||
* ({@code confirmId}), 서버에서도 대상과 일치하는지 다시 본다 — 확인 절차가 화면에만 있으면
|
||||
* 요청을 직접 만드는 경로에서는 없는 것과 같다.</p>
|
||||
*/
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/deletePii.do", method = org.springframework.web.bind.annotation.RequestMethod.POST)
|
||||
public String deletePii(@RequestParam(value = "mberId", required = false) String mberId,
|
||||
@RequestParam(value = "confirmId", required = false) String confirmId,
|
||||
@RequestParam(value = "reason", required = false) String reason,
|
||||
HttpServletRequest request) throws Exception {
|
||||
LoginVO user = handlerUser();
|
||||
if (user == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
if (!MberCryptoReauth.isVerified(request)) {
|
||||
return reauthRedirect(viewUrl(mberId, reason));
|
||||
}
|
||||
if (isBlank(mberId) || confirmId == null || !mberId.trim().equals(confirmId.trim())) {
|
||||
return listRedirect("파기 확인을 위해 대상 회원ID를 정확히 입력해야 합니다.");
|
||||
}
|
||||
try {
|
||||
int cnt = mberCryptoAdminService.deleteMberPii(mberId, user.getId(),
|
||||
ClientIpResolver.getClientIp(request), reason);
|
||||
return listRedirect(cnt > 0
|
||||
? "개인정보를 파기하고 회원상태를 탈퇴로 전환했습니다: " + mberId.trim()
|
||||
: "파기 대상이 없습니다.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
return listRedirect(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* 접근 이력
|
||||
* ================================================================ */
|
||||
|
||||
/** 이 화면에서 일어난 접근 이력(사유 포함) 조회. 뷰: .../mberCrypto/history.jsp */
|
||||
@RequestMapping(value = "/mnt/sec/mberCrypto/historyView.do")
|
||||
public String historyView(@RequestParam(value = "trgetMberId", required = false) String trgetMberId,
|
||||
@RequestParam(value = "handlrId", required = false) String handlrId,
|
||||
@RequestParam(value = "actionTy", required = false) String actionTy,
|
||||
@RequestParam(value = "pageIndex", required = false, defaultValue = "1") int pageIndex,
|
||||
ModelMap model) throws Exception {
|
||||
if (handlerUser() == null) {
|
||||
return LOGIN_REDIRECT;
|
||||
}
|
||||
int page = pageIndex < 1 ? 1 : pageIndex;
|
||||
int perPage = 10;
|
||||
|
||||
Map<String, Object> param = new HashMap<String, Object>();
|
||||
param.put("trgetMberId", trim(trgetMberId));
|
||||
param.put("handlrId", trim(handlrId));
|
||||
param.put("actionTy", trim(actionTy));
|
||||
param.put("firstIndex", Integer.valueOf((page - 1) * perPage));
|
||||
param.put("recordCountPerPage", Integer.valueOf(perPage));
|
||||
|
||||
int total = mberCryptoAdminService.selectAccessHistoryListCount(param);
|
||||
List<MberCryptoAccessHistoryVO> list = mberCryptoAdminService.selectAccessHistoryList(param);
|
||||
|
||||
PaginationInfo paginationInfo = new PaginationInfo();
|
||||
paginationInfo.setCurrentPageNo(page);
|
||||
paginationInfo.setRecordCountPerPage(perPage);
|
||||
paginationInfo.setPageSize(10);
|
||||
paginationInfo.setTotalRecordCount(total);
|
||||
|
||||
model.addAttribute("resultList", list);
|
||||
model.addAttribute("paginationInfo", paginationInfo);
|
||||
model.addAttribute("totalCount", Integer.valueOf(total));
|
||||
model.addAttribute("trgetMberId", trgetMberId);
|
||||
model.addAttribute("handlrId", handlrId);
|
||||
model.addAttribute("actionTy", actionTy);
|
||||
return "site/mnt/sec/mberCrypto/history";
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* 내부 헬퍼
|
||||
* ================================================================ */
|
||||
|
||||
/** 목록에 실릴 값을 전부 마스킹한다(평문은 상세에서만 보여준다). */
|
||||
private void maskAll(List<MberCryptoVO> list) {
|
||||
if (list == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
MberCryptoVO vo = list.get(i);
|
||||
vo.setMberNm(MaskingUtil.maskName(vo.getMberNm()));
|
||||
vo.setMoblphonNo(MaskingUtil.maskPhone(vo.getMoblphonNo()));
|
||||
vo.setMberEmailAdres(MaskingUtil.maskEmail(vo.getMberEmailAdres()));
|
||||
// 암호문·해시는 화면으로 나갈 이유가 없다. 목록 모델에서 지운다.
|
||||
vo.setMberNmEnc(null);
|
||||
vo.setMberNmHash(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 돌아갈 URL 을 이 화면 대역으로 제한한다.
|
||||
* 외부 주소나 다른 화면이 들어오면 목록으로 되돌린다(오픈 리다이렉트 차단).
|
||||
*/
|
||||
private String safeReturnUrl(String returnUrl) {
|
||||
if (returnUrl == null) {
|
||||
return RETURN_URL_PREFIX + "listView.do?page=9100051";
|
||||
}
|
||||
String u = returnUrl.trim();
|
||||
// "//host" 형태는 프로토콜 상대 URL 이라 외부로 나간다 — 접두사 검사만으로는 막히지 않는다.
|
||||
if (u.startsWith(RETURN_URL_PREFIX) && !u.startsWith("//")) {
|
||||
return u;
|
||||
}
|
||||
return RETURN_URL_PREFIX + "listView.do?page=9100051";
|
||||
}
|
||||
|
||||
private String viewUrl(String mberId, String reason) throws Exception {
|
||||
StringBuilder sb = new StringBuilder(RETURN_URL_PREFIX).append("view.do?page=9100051");
|
||||
if (!isBlank(mberId)) {
|
||||
sb.append("&mberId=").append(URLEncoder.encode(mberId.trim(), "UTF-8"));
|
||||
}
|
||||
if (!isBlank(reason)) {
|
||||
sb.append("&reason=").append(URLEncoder.encode(reason.trim(), "UTF-8"));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String reauthRedirect(String returnUrl) throws Exception {
|
||||
return "redirect:/mnt/sec/mberCrypto/reauthView.do?returnUrl="
|
||||
+ URLEncoder.encode(returnUrl, "UTF-8");
|
||||
}
|
||||
|
||||
private String listRedirect(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return LIST_REDIRECT;
|
||||
}
|
||||
return LIST_REDIRECT + "&resultMsg=" + URLEncoder.encode(message, "UTF-8");
|
||||
}
|
||||
|
||||
private String registRedirect(String message) throws Exception {
|
||||
String base = "redirect:/mnt/sec/mberCrypto/registView.do?page=9100051";
|
||||
if (message == null) {
|
||||
return base;
|
||||
}
|
||||
return base + "&resultMsg=" + URLEncoder.encode(message, "UTF-8");
|
||||
}
|
||||
|
||||
private String trim(String v) {
|
||||
return (v == null || v.trim().length() == 0) ? null : v.trim();
|
||||
}
|
||||
|
||||
private boolean isBlank(String v) {
|
||||
return v == null || v.trim().length() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증된 업무사용자(USR)만 통과. 그 외에는 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,107 @@
|
||||
/**
|
||||
* Class Name : MberCryptoAdminDAO.java
|
||||
* Description : 암호화 회원정보 관리자 CRUD DAO — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import egovframework.com.cmm.service.impl.EgovComAbstractDAO;
|
||||
|
||||
/**
|
||||
* 관리자 전용 회원정보 화면의 데이터 접근.
|
||||
*
|
||||
* <p><b>복호화 지점을 여기 한 곳으로 모은다.</b> 조회 statement 는 {@code MBER_NM_ENC} 를
|
||||
* {@code mberNm} 별칭으로 가져오고, 이 DAO 가 반환 직전
|
||||
* {@link MemberNmDecryptSupport} 로 같은 자리에 평문을 되돌려 쓴다. 서비스·화면은 복호화를
|
||||
* 의식하지 않는다 — 읽기 전환 시 정착된 이 코드베이스의 관용을 그대로 따른다.</p>
|
||||
*
|
||||
* <p><b>기존 회원관리(mberManageDAO)와 왜 분리했나:</b> 표준 회원관리 VO 는 활동지역·재능 등
|
||||
* 수십 개 필드를 함께 다뤄 이 화면이 필요로 하지 않는 값까지 오간다. 개인정보 항목만 좁게
|
||||
* 취급하는 별도 계층을 두는 편이 "무엇을 읽고 무엇을 바꿨는가"를 이력에 정확히 남기기 쉽다.</p>
|
||||
*/
|
||||
@Repository("mberCryptoAdminDAO")
|
||||
public class MberCryptoAdminDAO extends EgovComAbstractDAO {
|
||||
|
||||
/** 회원 목록(검색·페이징). 반환 직전 이름을 복호화해 {@code mberNm} 에 되돌려 쓴다. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<MberCryptoVO> selectMberList(MberCryptoSearchVO searchVO) {
|
||||
List<MberCryptoVO> rows = list("MberCryptoAdminDAO.selectMberList", searchVO);
|
||||
MemberNmDecryptSupport.decryptListInPlace(rows, "mberNm");
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 회원 목록 총건수(페이징용). 목록과 <b>같은 검색조건</b>을 유지할 것. */
|
||||
public int selectMberListCount(MberCryptoSearchVO searchVO) {
|
||||
Object cnt = selectByPk("MberCryptoAdminDAO.selectMberListCount", searchVO);
|
||||
return cnt == null ? 0 : ((Number) cnt).intValue();
|
||||
}
|
||||
|
||||
/** 회원 단건. 없으면 null. 반환 직전 이름을 복호화한다. */
|
||||
public MberCryptoVO selectMber(String mberId) {
|
||||
MberCryptoVO row = (MberCryptoVO) selectByPk("MberCryptoAdminDAO.selectMber", mberId);
|
||||
MemberNmDecryptSupport.decryptInPlace(row, "mberNm");
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 회원ID 중복 여부(신규 등록 전 확인). */
|
||||
public int selectMberIdCount(String mberId) {
|
||||
Object cnt = selectByPk("MberCryptoAdminDAO.selectMberIdCount", mberId);
|
||||
return cnt == null ? 0 : ((Number) cnt).intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 신규 회원 등록. 이름은 암호문·해시로만 저장하며, 평문 컬럼에는 표식만 들어간다
|
||||
* (컬럼이 NOT NULL 이라 비울 수 없다 — sqlmap 주석 참조).
|
||||
*/
|
||||
public void insertMber(MberCryptoVO vo) {
|
||||
insert("MberCryptoAdminDAO.insertMber", vo);
|
||||
}
|
||||
|
||||
/** 회원 개인정보 수정(이름은 암호문·해시 동시 갱신). */
|
||||
public int updateMber(MberCryptoVO vo) {
|
||||
return update("MberCryptoAdminDAO.updateMber", vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 개인정보 파기 — 행을 지우지 않고 개인정보 항목만 비우고 상태를 탈퇴('D')로 바꾼다.
|
||||
* 물리 삭제를 하지 않는 이유는 서비스 Javadoc 참조(참조 무결성·통계 정합).
|
||||
*/
|
||||
public int deleteMberPii(MberCryptoVO vo) {
|
||||
return update("MberCryptoAdminDAO.deleteMberPii", vo);
|
||||
}
|
||||
|
||||
/** 취급자 비밀번호(재인증 대조용). 계정이 없으면 null. */
|
||||
public String selectHandlerPassword(String emplyrId) {
|
||||
return (String) selectByPk("MberCryptoAdminDAO.selectHandlerPassword", emplyrId);
|
||||
}
|
||||
|
||||
/** 관리자 접근 이력 적재(사유·재인증 여부 포함). */
|
||||
public void insertAccessHistory(MberCryptoAccessHistoryVO vo) {
|
||||
insert("MberCryptoAdminDAO.insertAccessHistory", vo);
|
||||
}
|
||||
|
||||
/** 관리자 접근 이력 목록. param: trgetMberId/handlrId/actionTy/fromDt/toDt/firstIndex/recordCountPerPage */
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<MberCryptoAccessHistoryVO> selectAccessHistoryList(Map<String, Object> param) {
|
||||
return list("MberCryptoAdminDAO.selectAccessHistoryList", param);
|
||||
}
|
||||
|
||||
/** 관리자 접근 이력 총건수. */
|
||||
public int selectAccessHistoryListCount(Map<String, Object> param) {
|
||||
Object cnt = selectByPk("MberCryptoAdminDAO.selectAccessHistoryListCount", param);
|
||||
return cnt == null ? 0 : ((Number) cnt).intValue();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Class Name : MberCryptoAdminService.java
|
||||
* Description : 암호화 회원정보 관리자 CRUD 서비스 — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 관리자 전용 회원정보(암호화 항목) CRUD.
|
||||
*
|
||||
* <p>제안서 추가제안 Action4(슬라이드16)의 요건을 구현한다 —
|
||||
* <b>관리자 전용 화면에서 복호화 조회·수정·삭제·신규 등록, 조회·수정·삭제 시 사유 입력 및 재인증 연계.</b></p>
|
||||
*
|
||||
* <p><b>사유는 서비스가 강제한다.</b> 화면에서만 막으면 URL 을 직접 부르는 경로가 뚫린다.
|
||||
* 사유가 비어 있으면 {@link IllegalArgumentException} 으로 작업 자체를 거절한다
|
||||
* (접속기록 다운로드 사유 필수 처리와 같은 방식).</p>
|
||||
*
|
||||
* <p><b>모든 변경은 이력과 한 트랜잭션이다.</b> {@code egovframework.com..*Impl.*} 이
|
||||
* 트랜잭션 어드바이스 대상이라, 회원정보 변경만 커밋되고 이력이 빠지는 상태가 생기지 않는다.</p>
|
||||
*/
|
||||
public interface MberCryptoAdminService {
|
||||
|
||||
/** 회원 목록(검색·페이징). 이름 검색은 완전일치(해시)로 처리된다. */
|
||||
List<MberCryptoVO> selectMberList(MberCryptoSearchVO searchVO) throws Exception;
|
||||
|
||||
/** 회원 단건 조회(복호화). 없으면 null. */
|
||||
MberCryptoVO selectMber(String mberId) throws Exception;
|
||||
|
||||
/**
|
||||
* 복호화 상세 조회 행위를 이력에 남긴다(SFR: 조회 시 사유 입력).
|
||||
*
|
||||
* @throws IllegalArgumentException 사유 미입력
|
||||
*/
|
||||
void recordDecryptView(String mberId, String handlerId, String handlerIp, String reason) throws Exception;
|
||||
|
||||
/**
|
||||
* 신규 회원 등록. 이름은 암호문·해시로 저장한다.
|
||||
*
|
||||
* @return 등록된 회원ID
|
||||
* @throws IllegalArgumentException 필수값 누락·사유 미입력·회원ID 중복
|
||||
*/
|
||||
String registMber(MberCryptoVO vo, String handlerId, String handlerIp, String reason) throws Exception;
|
||||
|
||||
/**
|
||||
* 회원 개인정보 수정. 바뀐 항목명만 이력에 남긴다(값은 남기지 않는다).
|
||||
*
|
||||
* @return 갱신 건수(대상 없음 0)
|
||||
* @throws IllegalArgumentException 사유 미입력·대상 회원 없음
|
||||
*/
|
||||
int updateMber(MberCryptoVO vo, String handlerId, String handlerIp, String reason) throws Exception;
|
||||
|
||||
/**
|
||||
* 개인정보 파기 — 행은 남기고 개인정보 항목만 비운 뒤 상태를 탈퇴('D')로 바꾼다.
|
||||
*
|
||||
* @return 처리 건수
|
||||
* @throws IllegalArgumentException 사유 미입력·대상 회원 없음
|
||||
*/
|
||||
int deleteMberPii(String mberId, String handlerId, String handlerIp, String reason) throws Exception;
|
||||
|
||||
/**
|
||||
* 취급자 비밀번호 재확인(재인증).
|
||||
*
|
||||
* @return 일치하면 true
|
||||
*/
|
||||
boolean verifyHandlerPassword(String handlerId, String rawPassword) throws Exception;
|
||||
|
||||
/** 관리자 접근 이력 목록. */
|
||||
List<MberCryptoAccessHistoryVO> selectAccessHistoryList(Map<String, Object> param) throws Exception;
|
||||
|
||||
/** 관리자 접근 이력 총건수. */
|
||||
int selectAccessHistoryListCount(Map<String, Object> param) throws Exception;
|
||||
}
|
||||
@ -0,0 +1,317 @@
|
||||
/**
|
||||
* Class Name : MberCryptoAdminServiceImpl.java
|
||||
* Description : 암호화 회원정보 관리자 CRUD 서비스 구현 — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import egovframework.com.utl.sim.service.EgovFileScrty;
|
||||
import egovframework.rte.fdl.idgnr.EgovIdGnrService;
|
||||
|
||||
/**
|
||||
* {@link MberCryptoAdminService} 구현.
|
||||
*
|
||||
* <p><b>암호화 헬퍼를 필드로 들고 있는 이유:</b> {@link MemberDataCipher}·{@link MemberDataHasher}
|
||||
* 는 생성 시점에 키·pepper 를 읽고 없으면 예외를 던진다. 매 호출마다 새로 만들면 키 미설정을
|
||||
* 요청 시점에야 알게 되므로, 최초 사용 시 한 번 만들어 재사용한다(둘 다 불변·스레드 안전).</p>
|
||||
*
|
||||
* <p><b>왜 지연 생성인가:</b> 필드 초기화로 생성하면 키가 없는 개발 환경에서 이 빈 하나 때문에
|
||||
* 컨텍스트 기동이 통째로 실패한다. 회원정보 암호화가 적용되지 않은 환경에서도 나머지 관리자
|
||||
* 기능은 떠야 하므로, 이 화면을 실제로 쓸 때 실패하게 미룬다.</p>
|
||||
*/
|
||||
@Service("mberCryptoAdminService")
|
||||
public class MberCryptoAdminServiceImpl implements MberCryptoAdminService {
|
||||
|
||||
/** 파기 후 평문 이름 컬럼(NOT NULL)에 남기는 표식. */
|
||||
private static final String PII_PURGED_MARK = "(파기)";
|
||||
|
||||
/** 신규 등록 시 평문 이름 컬럼(NOT NULL)에 남기는 표식 — 평문은 저장하지 않는다. */
|
||||
private static final String ENCRYPTED_MARK = "(암호화)";
|
||||
|
||||
@Resource(name = "mberCryptoAdminDAO")
|
||||
private MberCryptoAdminDAO mberCryptoAdminDAO;
|
||||
|
||||
/** ESNTL_ID 채번 — 표준 회원관리(EgovMberManageServiceImpl)와 같은 채번기를 쓴다. */
|
||||
@Resource(name = "egovUsrCnfrmIdGnrService")
|
||||
private EgovIdGnrService idgenService;
|
||||
|
||||
private volatile MemberDataCipher cipher;
|
||||
private volatile MemberDataHasher hasher;
|
||||
|
||||
private MemberDataCipher cipher() {
|
||||
MemberDataCipher c = cipher;
|
||||
if (c == null) {
|
||||
synchronized (this) {
|
||||
c = cipher;
|
||||
if (c == null) {
|
||||
c = new MemberDataCipher();
|
||||
cipher = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private MemberDataHasher hasher() {
|
||||
MemberDataHasher h = hasher;
|
||||
if (h == null) {
|
||||
synchronized (this) {
|
||||
h = hasher;
|
||||
if (h == null) {
|
||||
h = new MemberDataHasher();
|
||||
hasher = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MberCryptoVO> selectMberList(MberCryptoSearchVO searchVO) throws Exception {
|
||||
// 이름은 암호문이라 LIKE 가 불가능하다. 입력 이름을 해시로 바꿔 완전일치로만 찾는다.
|
||||
// 평문 이름은 SQL 에 실리지 않는다(바인딩되는 값은 해시뿐).
|
||||
if (isNotBlank(searchVO.getMberNm())) {
|
||||
searchVO.setMberNmHash(hasher().hashName(searchVO.getMberNm()));
|
||||
} else {
|
||||
searchVO.setMberNmHash(null);
|
||||
}
|
||||
|
||||
int total = mberCryptoAdminDAO.selectMberListCount(searchVO);
|
||||
searchVO.setTotalRecordCount(total);
|
||||
if (total == 0) {
|
||||
return new java.util.ArrayList<MberCryptoVO>();
|
||||
}
|
||||
return mberCryptoAdminDAO.selectMberList(searchVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MberCryptoVO selectMber(String mberId) throws Exception {
|
||||
if (!isNotBlank(mberId)) {
|
||||
return null;
|
||||
}
|
||||
return mberCryptoAdminDAO.selectMber(mberId.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordDecryptView(String mberId, String handlerId, String handlerIp, String reason)
|
||||
throws Exception {
|
||||
String r = requireReason(reason);
|
||||
writeHistory(MberCryptoAccessHistoryVO.ACTION_SELECT, handlerId, handlerIp, mberId, r, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String registMber(MberCryptoVO vo, String handlerId, String handlerIp, String reason)
|
||||
throws Exception {
|
||||
String r = requireReason(reason);
|
||||
if (vo == null || !isNotBlank(vo.getMberId())) {
|
||||
throw new IllegalArgumentException("회원ID는 필수입니다.");
|
||||
}
|
||||
if (!isNotBlank(vo.getMberNm())) {
|
||||
throw new IllegalArgumentException("이름은 필수입니다.");
|
||||
}
|
||||
if (!isNotBlank(vo.getPassword())) {
|
||||
throw new IllegalArgumentException("초기 비밀번호는 필수입니다.");
|
||||
}
|
||||
String mberId = vo.getMberId().trim();
|
||||
if (mberCryptoAdminDAO.selectMberIdCount(mberId) > 0) {
|
||||
throw new IllegalArgumentException("이미 사용 중인 회원ID입니다: " + mberId);
|
||||
}
|
||||
|
||||
vo.setMberId(mberId);
|
||||
vo.setUniqId(idgenService.getNextStringId());
|
||||
// 이름은 암호문·해시로만 들어간다. 평문 컬럼(NOT NULL)에는 표식만 남긴다.
|
||||
vo.setMberNmEnc(cipher().encrypt(vo.getMberNm().trim()));
|
||||
vo.setMberNmHash(hasher().hashName(vo.getMberNm().trim()));
|
||||
vo.setCryptoKeyVer(Integer.valueOf(MemberDataCipher.CURRENT_KEY_VER));
|
||||
vo.setMberNm(ENCRYPTED_MARK);
|
||||
// 초기 비밀번호는 솔트 해시로 저장한다(평문·무솔트 저장 금지 — SER-003).
|
||||
vo.setPassword(EgovFileScrty.encryptPasswordWithSalt(vo.getPassword()));
|
||||
if (!isNotBlank(vo.getMberSttus())) {
|
||||
vo.setMberSttus("P");
|
||||
}
|
||||
normalizeBlanks(vo);
|
||||
|
||||
mberCryptoAdminDAO.insertMber(vo);
|
||||
writeHistory(MberCryptoAccessHistoryVO.ACTION_INSERT, handlerId, handlerIp, mberId, r,
|
||||
"이름,이메일,휴대폰,주소");
|
||||
return mberId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateMber(MberCryptoVO vo, String handlerId, String handlerIp, String reason)
|
||||
throws Exception {
|
||||
String r = requireReason(reason);
|
||||
if (vo == null || !isNotBlank(vo.getMberId())) {
|
||||
throw new IllegalArgumentException("회원ID는 필수입니다.");
|
||||
}
|
||||
if (!isNotBlank(vo.getMberNm())) {
|
||||
throw new IllegalArgumentException("이름은 필수입니다.");
|
||||
}
|
||||
String mberId = vo.getMberId().trim();
|
||||
MberCryptoVO before = mberCryptoAdminDAO.selectMber(mberId);
|
||||
if (before == null) {
|
||||
throw new IllegalArgumentException("대상 회원을 찾을 수 없습니다: " + mberId);
|
||||
}
|
||||
|
||||
// 바뀐 항목명만 뽑는다 — 값은 이력에 남기지 않는다(이력이 평문 저장소가 되면 안 된다).
|
||||
String changed = diffItems(before, vo);
|
||||
|
||||
vo.setMberId(mberId);
|
||||
vo.setMberNmEnc(cipher().encrypt(vo.getMberNm().trim()));
|
||||
vo.setMberNmHash(hasher().hashName(vo.getMberNm().trim()));
|
||||
vo.setCryptoKeyVer(Integer.valueOf(MemberDataCipher.CURRENT_KEY_VER));
|
||||
if (!isNotBlank(vo.getMberSttus())) {
|
||||
vo.setMberSttus(before.getMberSttus());
|
||||
}
|
||||
normalizeBlanks(vo);
|
||||
|
||||
int cnt = mberCryptoAdminDAO.updateMber(vo);
|
||||
if (cnt > 0) {
|
||||
writeHistory(MberCryptoAccessHistoryVO.ACTION_UPDATE, handlerId, handlerIp, mberId, r,
|
||||
changed.length() == 0 ? "(변경 항목 없음)" : changed);
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteMberPii(String mberId, String handlerId, String handlerIp, String reason)
|
||||
throws Exception {
|
||||
String r = requireReason(reason);
|
||||
if (!isNotBlank(mberId)) {
|
||||
throw new IllegalArgumentException("회원ID는 필수입니다.");
|
||||
}
|
||||
String id = mberId.trim();
|
||||
MberCryptoVO before = mberCryptoAdminDAO.selectMber(id);
|
||||
if (before == null) {
|
||||
throw new IllegalArgumentException("대상 회원을 찾을 수 없습니다: " + id);
|
||||
}
|
||||
|
||||
MberCryptoVO vo = new MberCryptoVO();
|
||||
vo.setMberId(id);
|
||||
vo.setMberNm(PII_PURGED_MARK);
|
||||
|
||||
int cnt = mberCryptoAdminDAO.deleteMberPii(vo);
|
||||
if (cnt > 0) {
|
||||
writeHistory(MberCryptoAccessHistoryVO.ACTION_DELETE, handlerId, handlerIp, id, r,
|
||||
"이름,이메일,휴대폰,주소,생년월일,성별");
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyHandlerPassword(String handlerId, String rawPassword) throws Exception {
|
||||
if (!isNotBlank(handlerId) || !isNotBlank(rawPassword)) {
|
||||
return false;
|
||||
}
|
||||
String stored = mberCryptoAdminDAO.selectHandlerPassword(handlerId.trim());
|
||||
if (stored == null || stored.length() == 0) {
|
||||
// 계정이 없거나 비밀번호가 비어 있으면 통과시키지 않는다.
|
||||
// (존재 여부를 응답으로 구분해 주지 않기 위해 호출부는 실패 문구를 한 가지로 쓴다)
|
||||
return false;
|
||||
}
|
||||
// 솔트 유무를 자동 판별해 대조한다(무솔트 레거시 해시도 그대로 검증된다).
|
||||
return EgovFileScrty.matches(rawPassword, stored);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MberCryptoAccessHistoryVO> selectAccessHistoryList(Map<String, Object> param) throws Exception {
|
||||
return mberCryptoAdminDAO.selectAccessHistoryList(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int selectAccessHistoryListCount(Map<String, Object> param) throws Exception {
|
||||
return mberCryptoAdminDAO.selectAccessHistoryListCount(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이력 1행 적재. 재인증은 컨트롤러가 이미 통과시킨 뒤에만 이 서비스에 닿으므로 'Y'로 기록한다.
|
||||
* (재인증 없이 도달할 수 있는 경로가 생기면 이 값도 인자로 받도록 바꿀 것)
|
||||
*/
|
||||
private void writeHistory(String actionTy, String handlerId, String handlerIp,
|
||||
String trgetMberId, String reason, String changedItems) {
|
||||
MberCryptoAccessHistoryVO h = new MberCryptoAccessHistoryVO();
|
||||
h.setActionTy(actionTy);
|
||||
h.setHandlrId(handlerId);
|
||||
h.setHandlrIp(handlerIp);
|
||||
h.setTrgetMberId(trgetMberId);
|
||||
h.setAccessReason(reason);
|
||||
h.setChangedItems(changedItems);
|
||||
h.setReauthYn("Y");
|
||||
mberCryptoAdminDAO.insertAccessHistory(h);
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT NULL 컬럼에 대응하는 필드의 null 을 빈 문자열로 바꾼다.
|
||||
*
|
||||
* <p>{@code COMTNGNRLMBER} 는 ZIP·ADRES·MBTLNUM 등이 NOT NULL 이라, 화면에서 비워 둔 값이
|
||||
* 그대로 null 로 내려가면 INSERT/UPDATE 가 제약 위반으로 실패한다. SQL 쪽에서 {@code IFNULL}
|
||||
* 로 감싸는 방법도 있으나, 그러면 "이 컬럼은 비어도 되는가"의 답이 SQL 에 흩어진다.
|
||||
* 값의 형태를 맞추는 일은 서비스가 한 곳에서 한다.</p>
|
||||
*/
|
||||
private void normalizeBlanks(MberCryptoVO vo) {
|
||||
vo.setZip(blankIfNull(vo.getZip()));
|
||||
vo.setAdres(blankIfNull(vo.getAdres()));
|
||||
vo.setDetailAdres(blankIfNull(vo.getDetailAdres()));
|
||||
vo.setSexdstnCode(blankIfNull(vo.getSexdstnCode()));
|
||||
vo.setMoblphonNo(blankIfNull(vo.getMoblphonNo()));
|
||||
// MBER_EMAIL_ADRES 는 NULL 허용 컬럼이라 그대로 둔다(빈 문자열과 미입력을 구분).
|
||||
}
|
||||
|
||||
private String blankIfNull(String v) {
|
||||
return v == null ? "" : v.trim();
|
||||
}
|
||||
|
||||
/** 사유 필수 검증. 통과하면 앞뒤 공백을 다듬은 값을 돌려준다. */
|
||||
private String requireReason(String reason) {
|
||||
if (!isNotBlank(reason)) {
|
||||
throw new IllegalArgumentException("개인정보 처리 사유는 필수 입력입니다. (접근 이력에 기록됩니다)");
|
||||
}
|
||||
String r = reason.trim();
|
||||
return r.length() > 500 ? r.substring(0, 500) : r;
|
||||
}
|
||||
|
||||
/** 변경된 항목명만 콤마로 잇는다(값은 담지 않는다). */
|
||||
private String diffItems(MberCryptoVO before, MberCryptoVO after) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendIfChanged(sb, "이름", before.getMberNm(), after.getMberNm());
|
||||
appendIfChanged(sb, "이메일", before.getMberEmailAdres(), after.getMberEmailAdres());
|
||||
appendIfChanged(sb, "휴대폰", before.getMoblphonNo(), after.getMoblphonNo());
|
||||
appendIfChanged(sb, "우편번호", before.getZip(), after.getZip());
|
||||
appendIfChanged(sb, "주소", before.getAdres(), after.getAdres());
|
||||
appendIfChanged(sb, "상세주소", before.getDetailAdres(), after.getDetailAdres());
|
||||
appendIfChanged(sb, "성별", before.getSexdstnCode(), after.getSexdstnCode());
|
||||
appendIfChanged(sb, "회원상태", before.getMberSttus(), after.getMberSttus());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void appendIfChanged(StringBuilder sb, String label, String before, String after) {
|
||||
String b = before == null ? "" : before;
|
||||
String a = after == null ? "" : after;
|
||||
if (b.equals(a)) {
|
||||
return;
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append(label);
|
||||
}
|
||||
|
||||
private boolean isNotBlank(String v) {
|
||||
return v != null && v.trim().length() > 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Class Name : MberCryptoReauth.java
|
||||
* Description : 암호화 회원정보 화면 재인증 세션 표식 — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
/**
|
||||
* 관리자 회원정보 화면의 <b>재인증</b> 상태를 세션에 표시하고 유효시간을 판정한다.
|
||||
*
|
||||
* <p>제안서 슬라이드16의 "조회·수정·삭제 시 사유 입력 및 <b>재인증</b> 연계" 요건이다.
|
||||
* 로그인 자체가 아니라 <u>이 화면에 들어올 때 한 번 더</u> 본인을 확인한다 — 자리를 비운 사이
|
||||
* 열려 있던 세션으로 평문 개인정보를 훑는 경로를 끊는 것이 목적이다.</p>
|
||||
*
|
||||
* <p><b>왜 로그인 2차 인증(SECOND_FACTOR_DONE)을 재사용하지 않는가:</b> 2차 인증은 로그인 시점에
|
||||
* 한 번 통과하면 세션 내내 유지된다(30분 자동 로그아웃까지). 그 표식을 그대로 쓰면 "다시 확인"이
|
||||
* 되지 못한다. 또 OTP 를 등록하지 않은 취급자는 2차 인증을 통과한 적이 없어 이 화면에 영영
|
||||
* 들어오지 못한다. 그래서 <b>본인 비밀번호 재확인</b>이라는 독립 표식을 둔다.</p>
|
||||
*
|
||||
* <p><b>유효시간을 짧게 두는 이유:</b> 한 번 통과하면 세션이 끝날 때까지 열어 두는 방식은
|
||||
* 사실상 재인증이 없는 것과 같다. {@link #TTL_MILLIS}(10분)마다 다시 묻는다. 작업 중 만료되면
|
||||
* 재인증 화면을 거쳐 원래 위치로 돌아온다.</p>
|
||||
*
|
||||
* <p><b>연장하지 않는다.</b> 통과 시각을 갱신하지 않고 최초 통과 시각을 그대로 두므로, 화면을
|
||||
* 계속 쓰더라도 10분마다 반드시 다시 확인한다(슬라이딩 만료가 아니다).</p>
|
||||
*/
|
||||
public final class MberCryptoReauth {
|
||||
|
||||
/** 재인증 통과 시각(밀리초)을 담는 세션 속성명. */
|
||||
public static final String SESSION_KEY = "MBER_CRYPTO_REAUTH_AT";
|
||||
|
||||
/** 재인증 유효시간 — 10분. */
|
||||
public static final long TTL_MILLIS = 10L * 60L * 1000L;
|
||||
|
||||
private MberCryptoReauth() {
|
||||
}
|
||||
|
||||
/** 재인증이 유효한 상태인지 판정한다. 세션이 없거나 만료면 false. */
|
||||
public static boolean isVerified(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return false;
|
||||
}
|
||||
Object v = session.getAttribute(SESSION_KEY);
|
||||
if (!(v instanceof Long)) {
|
||||
return false;
|
||||
}
|
||||
long at = ((Long) v).longValue();
|
||||
return (System.currentTimeMillis() - at) < TTL_MILLIS;
|
||||
}
|
||||
|
||||
/** 재인증 통과를 표시한다(현재 시각 기록). */
|
||||
public static void markVerified(HttpServletRequest request) {
|
||||
request.getSession(true).setAttribute(SESSION_KEY, Long.valueOf(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
/** 재인증 표식을 지운다(화면을 벗어나거나 실패했을 때). */
|
||||
public static void clear(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
session.removeAttribute(SESSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/** 남은 유효시간(초). 만료·미인증이면 0. 화면에 남은 시간을 알려 주기 위한 값이다. */
|
||||
public static long remainSeconds(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return 0L;
|
||||
}
|
||||
Object v = session.getAttribute(SESSION_KEY);
|
||||
if (!(v instanceof Long)) {
|
||||
return 0L;
|
||||
}
|
||||
long elapsed = System.currentTimeMillis() - ((Long) v).longValue();
|
||||
long remain = TTL_MILLIS - elapsed;
|
||||
return remain <= 0 ? 0L : remain / 1000L;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Class Name : MberCryptoSearchVO.java
|
||||
* Description : 암호화 회원정보 관리자 화면 검색조건 VO — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 관리자 회원정보 검색조건 + 페이징.
|
||||
*
|
||||
* <p><b>이름은 완전일치만 된다.</b> 이름은 랜덤 IV AES-GCM 으로 저장돼 같은 이름도 매번 다른
|
||||
* 암호문이라 {@code LIKE '%홍%'} 이 원리적으로 불가능하다. 대신 검색 전용 HMAC 해시 컬럼
|
||||
* ({@code MBER_NM_HASH})에 인덱스가 걸려 있어 <u>정확히 같은 이름</u>은 빠르게 찾는다.
|
||||
* 서비스가 입력 이름을 {@link MemberDataHasher#hashName(String)} 로 바꿔 {@link #setMberNmHash}
|
||||
* 에 실어 주면 SQL 은 해시만 비교한다 — 평문 이름은 SQL 에 등장하지 않는다.</p>
|
||||
*
|
||||
* <p>이메일·휴대폰·회원ID 는 아직 평문 컬럼이라 전방일치 검색이 가능하다
|
||||
* (암호화 대상 확대는 후속 단계 — 확대되면 이 필드들도 해시 검색으로 옮겨야 한다).</p>
|
||||
*/
|
||||
public class MberCryptoSearchVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 회원ID(전방일치). */
|
||||
private String mberId;
|
||||
/** 이름(완전일치 — 해시 검색). 화면 입력값이며 SQL 에는 해시만 나간다. */
|
||||
private String mberNm;
|
||||
/** 이메일(전방일치). */
|
||||
private String mberEmailAdres;
|
||||
/** 휴대폰(전방일치). */
|
||||
private String moblphonNo;
|
||||
/** 회원상태 P/D. 빈값이면 전체. */
|
||||
private String mberSttus;
|
||||
|
||||
/** 서비스가 채우는 이름 해시(파생값). 화면에서 직접 입력받지 않는다. */
|
||||
private String mberNmHash;
|
||||
|
||||
private int pageIndex = 1;
|
||||
private int recordCountPerPage = 10;
|
||||
/** 조회 시작 offset(0-base) = (pageIndex-1) * recordCountPerPage */
|
||||
private int firstIndex = 0;
|
||||
private int totalRecordCount = 0;
|
||||
|
||||
private static final String LIKE_ESCAPE = "!";
|
||||
|
||||
public String getMberId() { return mberId; }
|
||||
public void setMberId(String mberId) { this.mberId = mberId; }
|
||||
|
||||
public String getMberNm() { return mberNm; }
|
||||
public void setMberNm(String mberNm) { this.mberNm = mberNm; }
|
||||
|
||||
public String getMberEmailAdres() { return mberEmailAdres; }
|
||||
public void setMberEmailAdres(String mberEmailAdres) { this.mberEmailAdres = mberEmailAdres; }
|
||||
|
||||
public String getMoblphonNo() { return moblphonNo; }
|
||||
public void setMoblphonNo(String moblphonNo) { this.moblphonNo = moblphonNo; }
|
||||
|
||||
public String getMberSttus() { return mberSttus; }
|
||||
public void setMberSttus(String mberSttus) { this.mberSttus = mberSttus; }
|
||||
|
||||
public String getMberNmHash() { return mberNmHash; }
|
||||
public void setMberNmHash(String mberNmHash) { this.mberNmHash = mberNmHash; }
|
||||
|
||||
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; }
|
||||
|
||||
/** 회원ID 전방일치 바인딩값. */
|
||||
public String getMberIdLike() { return toPrefixLike(mberId); }
|
||||
|
||||
/** 이메일 전방일치 바인딩값. */
|
||||
public String getMberEmailAdresLike() { return toPrefixLike(mberEmailAdres); }
|
||||
|
||||
/** 휴대폰 전방일치 바인딩값. */
|
||||
public String getMoblphonNoLike() { return toPrefixLike(moblphonNo); }
|
||||
|
||||
/**
|
||||
* 전방일치 LIKE 값으로 변환한다. 사용자가 넣은 {@code %} {@code _} 는 와일드카드가 아니라
|
||||
* 글자 그대로 찾게 이스케이프한다(SQL 은 {@code ESCAPE '!'} 로 받는다).
|
||||
*/
|
||||
private String toPrefixLike(String v) {
|
||||
if (v == null) {
|
||||
return null;
|
||||
}
|
||||
String t = v.trim();
|
||||
if (t.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(t.length() + 8);
|
||||
for (int i = 0; i < t.length(); i++) {
|
||||
char c = t.charAt(i);
|
||||
if (c == '%' || c == '_' || c == '!') {
|
||||
sb.append(LIKE_ESCAPE);
|
||||
}
|
||||
sb.append(c);
|
||||
}
|
||||
return sb.append('%').toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Class Name : MberCryptoVO.java
|
||||
* Description : 암호화 회원정보 관리자 화면 VO — 개인정보보호 기능개선 사업(추가제안 Action4)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성 — 관리자 전용 복호화 CRUD 화면
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.crypto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 관리자 전용 회원정보 화면이 다루는 {@code COMTNGNRLMBER} 한 행.
|
||||
*
|
||||
* <p><b>{@code mberNm} 에 무엇이 들어있는가는 단계에 따라 다르다.</b> DAO 가 조회하는 컬럼은
|
||||
* {@code MBER_NM_ENC}(암호문)이고, DAO 반환 직전 {@link MemberNmDecryptSupport} 가 같은 자리에
|
||||
* 평문을 되돌려 쓴다 — 서비스·화면은 항상 평문으로 본다. 저장(등록·수정) 방향은 서비스가
|
||||
* 평문을 받아 {@link MemberDataCipher#encrypt(String)}·{@link MemberDataHasher#hashName(String)}
|
||||
* 로 {@code mberNmEnc}/{@code mberNmHash} 를 채운 뒤 DAO 에 넘긴다.</p>
|
||||
*
|
||||
* <p><b>평문 컬럼 {@code MBER_NM} 은 이 화면이 건드리지 않는다.</b> 암호화 이관 당시 롤백 창구로
|
||||
* 남겨둔 컬럼이라, 관리자 화면이 여기에 새 평문을 쓰기 시작하면 "이관이 끝나면 평문 컬럼을 지운다"는
|
||||
* 계획이 조용히 무너진다. 신규 등록 시에도 평문 자리에는 마스킹 표식만 넣는다
|
||||
* ({@code MBER_NM} 이 NOT NULL 이라 빈 값을 넣을 수 없기 때문이다).</p>
|
||||
*/
|
||||
public class MberCryptoVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 회원ID(PK). */
|
||||
private String mberId;
|
||||
/** 내부 식별자 ESNTL_ID. */
|
||||
private String uniqId;
|
||||
|
||||
/**
|
||||
* 이름 — 조회 결과에서는 <b>복호화된 평문</b>, 등록·수정 입력에서도 평문.
|
||||
* (DB 컬럼 MBER_NM_ENC 와 이 필드를 잇는 변환은 서비스·DAO 가 담당한다)
|
||||
*/
|
||||
private String mberNm;
|
||||
/** 이름 암호문(내부 전달용). 화면에 노출하지 않는다. */
|
||||
private String mberNmEnc;
|
||||
/** 이름 검색용 HMAC 해시(내부 전달용). */
|
||||
private String mberNmHash;
|
||||
/** 암호화 키 버전. 미암호화 행은 null. */
|
||||
private Integer cryptoKeyVer;
|
||||
|
||||
private String mberEmailAdres;
|
||||
private String moblphonNo;
|
||||
private String zip;
|
||||
private String adres;
|
||||
private String detailAdres;
|
||||
private String sexdstnCode;
|
||||
/** 회원상태 — P:정상 D:탈퇴(기존 코드계 그대로). */
|
||||
private String mberSttus;
|
||||
private String sbscrbDe;
|
||||
|
||||
/** 신규 등록 시 초기 비밀번호(평문 입력). 저장 시 솔트 해시로 변환되며 조회에는 실리지 않는다. */
|
||||
private String password;
|
||||
|
||||
public String getMberId() { return mberId; }
|
||||
public void setMberId(String mberId) { this.mberId = mberId; }
|
||||
|
||||
public String getUniqId() { return uniqId; }
|
||||
public void setUniqId(String uniqId) { this.uniqId = uniqId; }
|
||||
|
||||
public String getMberNm() { return mberNm; }
|
||||
public void setMberNm(String mberNm) { this.mberNm = mberNm; }
|
||||
|
||||
public String getMberNmEnc() { return mberNmEnc; }
|
||||
public void setMberNmEnc(String mberNmEnc) { this.mberNmEnc = mberNmEnc; }
|
||||
|
||||
public String getMberNmHash() { return mberNmHash; }
|
||||
public void setMberNmHash(String mberNmHash) { this.mberNmHash = mberNmHash; }
|
||||
|
||||
public Integer getCryptoKeyVer() { return cryptoKeyVer; }
|
||||
public void setCryptoKeyVer(Integer cryptoKeyVer) { this.cryptoKeyVer = cryptoKeyVer; }
|
||||
|
||||
public String getMberEmailAdres() { return mberEmailAdres; }
|
||||
public void setMberEmailAdres(String mberEmailAdres) { this.mberEmailAdres = mberEmailAdres; }
|
||||
|
||||
public String getMoblphonNo() { return moblphonNo; }
|
||||
public void setMoblphonNo(String moblphonNo) { this.moblphonNo = moblphonNo; }
|
||||
|
||||
public String getZip() { return zip; }
|
||||
public void setZip(String zip) { this.zip = zip; }
|
||||
|
||||
public String getAdres() { return adres; }
|
||||
public void setAdres(String adres) { this.adres = adres; }
|
||||
|
||||
public String getDetailAdres() { return detailAdres; }
|
||||
public void setDetailAdres(String detailAdres) { this.detailAdres = detailAdres; }
|
||||
|
||||
public String getSexdstnCode() { return sexdstnCode; }
|
||||
public void setSexdstnCode(String sexdstnCode) { this.sexdstnCode = sexdstnCode; }
|
||||
|
||||
public String getMberSttus() { return mberSttus; }
|
||||
public void setMberSttus(String mberSttus) { this.mberSttus = mberSttus; }
|
||||
|
||||
public String getSbscrbDe() { return sbscrbDe; }
|
||||
public void setSbscrbDe(String sbscrbDe) { this.sbscrbDe = sbscrbDe; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
}
|
||||
@ -152,4 +152,7 @@
|
||||
|
||||
<!-- 개인정보보호(SER-001 #24) - 로그인 실패 계정잠금 예외 화이트리스트(LOGIN_LOCK_EXEMPT) -->
|
||||
<sqlMap resource="egovframework/sqlmap/site/mnt/biz/privacy/PrivacyLoginLockExempt_SQL_Mysql.xml"/>
|
||||
|
||||
<!-- 개인정보보호(추가제안 Action4) - 관리자 전용 회원정보 CRUD + 접근이력(MBER_CRYPTO_ACCESS_HISTORY) -->
|
||||
<sqlMap resource="egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMberCryptoAdmin_SQL_Mysql.xml"/>
|
||||
</sqlMapConfig>
|
||||
|
||||
@ -409,4 +409,45 @@
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<!-- ================================================================
|
||||
[SFR-004 / DBR-001 / SER-002-2] 접속기록 파기·증설 이력 조회 (화면 /mnt/sec/purgeHistory).
|
||||
파기 배치(#37)가 남긴 PDAL_PURGE_HISTORY 를 유형·기간으로 조회한다.
|
||||
|
||||
이 표가 증빙인 이유: 접속기록 본문은 UPDATE/DELETE statement 자체가 없고 삭제는 오직
|
||||
파티션 DROP 으로만 일어난다. 따라서 "보관기간 경과분을 실제로 파기했다"와 "그 외 경로로는
|
||||
지운 적이 없다"를 동시에 보이는 유일한 근거가 이 이력이다.
|
||||
조회 전용 — 화면에서도 수정·삭제를 제공하지 않는다.
|
||||
================================================================ -->
|
||||
<select id="PersonalDataAccessLogDAO.selectPurgeHistoryList"
|
||||
parameterClass="egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistorySearchVO"
|
||||
resultClass="egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistoryVO">
|
||||
SELECT PURGE_ID AS purgeId,
|
||||
PARTITION_NAME AS partitionName,
|
||||
BOUNDARY_DT AS boundaryDt,
|
||||
PURGE_TYPE AS purgeType,
|
||||
PURGE_DT AS purgeDt,
|
||||
NOTE AS note
|
||||
FROM PDAL_PURGE_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<!-- 판정(isNotEmpty)은 원본 필드로, 바인딩은 종료일 보정된 파생 getter 로 하는 짝을 지킬 것. -->
|
||||
<isNotEmpty prepend="AND" property="purgeType"> PURGE_TYPE = #purgeType# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="fromDt"> PURGE_DT >= #fromDtParam# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="toDt"> PURGE_DT <= #toDtParam# </isNotEmpty>
|
||||
</dynamic>
|
||||
ORDER BY PURGE_ID DESC
|
||||
LIMIT #firstIndex#, #recordCountPerPage#
|
||||
</select>
|
||||
|
||||
<select id="PersonalDataAccessLogDAO.selectPurgeHistoryListCount"
|
||||
parameterClass="egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistorySearchVO"
|
||||
resultClass="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM PDAL_PURGE_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="purgeType"> PURGE_TYPE = #purgeType# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="fromDt"> PURGE_DT >= #fromDtParam# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="toDt"> PURGE_DT <= #toDtParam# </isNotEmpty>
|
||||
</dynamic>
|
||||
</select>
|
||||
|
||||
</sqlMap>
|
||||
|
||||
@ -332,4 +332,47 @@
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<!-- ================================================================
|
||||
[SFR-004 / DBR-001 / SER-002-2] 접속기록 파기·증설 이력 조회 (화면 /mnt/sec/purgeHistory).
|
||||
MySQL 쌍(PrivacyAccessLog_SQL_Mysql.xml)과 동일 계약. 페이징만 방언이 다르다
|
||||
(MySQL LIMIT offset,count ↔ Oracle ROWNUM 이중 인라인뷰).
|
||||
================================================================ -->
|
||||
<select id="PersonalDataAccessLogDAO.selectPurgeHistoryList"
|
||||
parameterClass="egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistorySearchVO"
|
||||
resultClass="egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistoryVO">
|
||||
SELECT purgeId, partitionName, boundaryDt, purgeType, purgeDt, note
|
||||
FROM (
|
||||
SELECT ROWNUM AS rnum, a.*
|
||||
FROM (
|
||||
SELECT PURGE_ID AS purgeId,
|
||||
PARTITION_NAME AS partitionName,
|
||||
BOUNDARY_DT AS boundaryDt,
|
||||
PURGE_TYPE AS purgeType,
|
||||
TO_CHAR(PURGE_DT, 'YYYY-MM-DD HH24:MI:SS') AS purgeDt,
|
||||
NOTE AS note
|
||||
FROM PDAL_PURGE_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="purgeType"> PURGE_TYPE = #purgeType# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="fromDt"> PURGE_DT >= TO_DATE(#fromDtParam#, 'YYYY-MM-DD HH24:MI:SS') </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="toDt"> PURGE_DT <= TO_DATE(#toDtParam#, 'YYYY-MM-DD HH24:MI:SS') </isNotEmpty>
|
||||
</dynamic>
|
||||
ORDER BY PURGE_ID DESC
|
||||
) a
|
||||
WHERE ROWNUM <= #firstIndex# + #recordCountPerPage#
|
||||
)
|
||||
WHERE rnum > #firstIndex#
|
||||
</select>
|
||||
|
||||
<select id="PersonalDataAccessLogDAO.selectPurgeHistoryListCount"
|
||||
parameterClass="egovframework.com.cmm.privacy.auditlog.AccessLogPurgeHistorySearchVO"
|
||||
resultClass="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM PDAL_PURGE_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="purgeType"> PURGE_TYPE = #purgeType# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="fromDt"> PURGE_DT >= TO_DATE(#fromDtParam#, 'YYYY-MM-DD HH24:MI:SS') </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="toDt"> PURGE_DT <= TO_DATE(#toDtParam#, 'YYYY-MM-DD HH24:MI:SS') </isNotEmpty>
|
||||
</dynamic>
|
||||
</select>
|
||||
|
||||
</sqlMap>
|
||||
|
||||
@ -0,0 +1,256 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
개인정보보호 기능개선 사업 (추가제안 Action4 / 제안서 슬라이드16)
|
||||
관리자 전용 회원정보 CRUD + 접근 이력.
|
||||
|
||||
[이름 컬럼 취급 규칙 — 이 파일 전체에 적용]
|
||||
· 조회: MBER_NM_ENC 를 mberNm 별칭으로 가져온다. 복호화는 DAO 가 반환 직전에 한다
|
||||
(MberCryptoAdminDAO → MemberNmDecryptSupport). SQL 에는 평문이 없다.
|
||||
· 저장: mberNmEnc(암호문) + mberNmHash(HMAC) 만 쓴다.
|
||||
· 평문 컬럼 MBER_NM 은 이 화면이 새 평문을 쓰지 않는다. NOT NULL 이라 비울 수 없어
|
||||
표식 문자열('(암호화)' / '(파기)')만 넣는다. 암호화 이관의 롤백 창구로 남겨둔
|
||||
컬럼이라, 여기에 평문을 다시 쓰기 시작하면 평문 컬럼 제거 계획이 무너진다.
|
||||
|
||||
[검색]
|
||||
· 이름: 랜덤 IV AES-GCM 이라 LIKE 불가 → MBER_NM_HASH 완전일치(IX_GNRLMBER_NM_HASH 인덱스).
|
||||
· 회원ID/이메일/휴대폰: 아직 평문 컬럼이라 전방일치(LIKE ... ESCAPE '!').
|
||||
판정(isNotEmpty)은 원본 필드로, 바인딩은 파생 getter 로 하는 짝을 지킬 것.
|
||||
|
||||
[Oracle 쌍] PrivacyMberCryptoAdmin_SQL_Oracle.xml 과 statement id·파라미터 계약이 같아야 한다.
|
||||
-->
|
||||
<sqlMap namespace="MberCryptoAdminDAO">
|
||||
|
||||
<!-- ================================================================
|
||||
목록 조회 (마스킹은 화면 계층 담당 — SQL 은 원본을 그대로 준다)
|
||||
================================================================ -->
|
||||
<select id="MberCryptoAdminDAO.selectMberList"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoSearchVO"
|
||||
resultClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
SELECT MBER_ID AS mberId,
|
||||
ESNTL_ID AS uniqId,
|
||||
MBER_NM_ENC AS mberNm,
|
||||
CRYPTO_KEY_VER AS cryptoKeyVer,
|
||||
MBER_EMAIL_ADRES AS mberEmailAdres,
|
||||
MBTLNUM AS moblphonNo,
|
||||
ZIP AS zip,
|
||||
ADRES AS adres,
|
||||
DETAIL_ADRES AS detailAdres,
|
||||
SEXDSTN_CODE AS sexdstnCode,
|
||||
MBER_STTUS AS mberSttus,
|
||||
SBSCRB_DE AS sbscrbDe
|
||||
FROM COMTNGNRLMBER
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="mberId"> MBER_ID LIKE #mberIdLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberNmHash"> MBER_NM_HASH = #mberNmHash# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberEmailAdres"> MBER_EMAIL_ADRES LIKE #mberEmailAdresLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="moblphonNo"> MBTLNUM LIKE #moblphonNoLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberSttus"> MBER_STTUS = #mberSttus# </isNotEmpty>
|
||||
</dynamic>
|
||||
ORDER BY SBSCRB_DE DESC, MBER_ID DESC
|
||||
LIMIT #firstIndex#, #recordCountPerPage#
|
||||
</select>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectMberListCount"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoSearchVO"
|
||||
resultClass="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM COMTNGNRLMBER
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="mberId"> MBER_ID LIKE #mberIdLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberNmHash"> MBER_NM_HASH = #mberNmHash# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberEmailAdres"> MBER_EMAIL_ADRES LIKE #mberEmailAdresLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="moblphonNo"> MBTLNUM LIKE #moblphonNoLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberSttus"> MBER_STTUS = #mberSttus# </isNotEmpty>
|
||||
</dynamic>
|
||||
</select>
|
||||
|
||||
<!-- ================================================================
|
||||
단건 조회 / 중복 확인
|
||||
================================================================ -->
|
||||
<select id="MberCryptoAdminDAO.selectMber"
|
||||
parameterClass="java.lang.String"
|
||||
resultClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
SELECT MBER_ID AS mberId,
|
||||
ESNTL_ID AS uniqId,
|
||||
MBER_NM_ENC AS mberNm,
|
||||
CRYPTO_KEY_VER AS cryptoKeyVer,
|
||||
MBER_EMAIL_ADRES AS mberEmailAdres,
|
||||
MBTLNUM AS moblphonNo,
|
||||
ZIP AS zip,
|
||||
ADRES AS adres,
|
||||
DETAIL_ADRES AS detailAdres,
|
||||
SEXDSTN_CODE AS sexdstnCode,
|
||||
MBER_STTUS AS mberSttus,
|
||||
SBSCRB_DE AS sbscrbDe
|
||||
FROM COMTNGNRLMBER
|
||||
WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectMberIdCount"
|
||||
parameterClass="java.lang.String" resultClass="java.lang.Integer">
|
||||
<![CDATA[
|
||||
SELECT COUNT(1) FROM COMTNGNRLMBER WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<!-- ================================================================
|
||||
신규 등록
|
||||
NOT NULL 제약(ZIP/ADRES/AREA_NO/END_TELNO/MBTLNUM/MIDDLE_TELNO)이 있어
|
||||
입력하지 않은 항목은 빈 문자열로 채운다. 이 화면은 개인정보 항목만 다루므로
|
||||
활동지역·재능 등 부가 컬럼은 건드리지 않는다(기본값/빈값 유지).
|
||||
================================================================ -->
|
||||
<insert id="MberCryptoAdminDAO.insertMber"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
INSERT INTO COMTNGNRLMBER (
|
||||
ESNTL_ID, MBER_ID, MBER_NM, MBER_NM_ENC, MBER_NM_HASH, CRYPTO_KEY_VER,
|
||||
PASSWORD, SEXDSTN_CODE, ZIP, ADRES, AREA_NO, MBER_STTUS, DETAIL_ADRES,
|
||||
END_TELNO, MBTLNUM, MBER_EMAIL_ADRES, MIDDLE_TELNO, SBSCRB_DE
|
||||
) VALUES (
|
||||
#uniqId#,
|
||||
#mberId#,
|
||||
#mberNm#,
|
||||
#mberNmEnc#,
|
||||
#mberNmHash#,
|
||||
#cryptoKeyVer:TINYINT#,
|
||||
#password#,
|
||||
#sexdstnCode:VARCHAR#,
|
||||
#zip:VARCHAR#,
|
||||
#adres:VARCHAR#,
|
||||
'',
|
||||
#mberSttus#,
|
||||
#detailAdres:VARCHAR#,
|
||||
'',
|
||||
#moblphonNo:VARCHAR#,
|
||||
#mberEmailAdres:VARCHAR#,
|
||||
'',
|
||||
NOW()
|
||||
)
|
||||
]]>
|
||||
</insert>
|
||||
|
||||
<!-- ================================================================
|
||||
수정 — 개인정보 항목만. 비밀번호·가입일·식별자는 이 화면에서 바꾸지 않는다.
|
||||
================================================================ -->
|
||||
<update id="MberCryptoAdminDAO.updateMber"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
UPDATE COMTNGNRLMBER
|
||||
SET MBER_NM_ENC = #mberNmEnc#,
|
||||
MBER_NM_HASH = #mberNmHash#,
|
||||
CRYPTO_KEY_VER = #cryptoKeyVer:TINYINT#,
|
||||
MBER_EMAIL_ADRES = #mberEmailAdres:VARCHAR#,
|
||||
MBTLNUM = #moblphonNo:VARCHAR#,
|
||||
ZIP = #zip:VARCHAR#,
|
||||
ADRES = #adres:VARCHAR#,
|
||||
DETAIL_ADRES = #detailAdres:VARCHAR#,
|
||||
SEXDSTN_CODE = #sexdstnCode:VARCHAR#,
|
||||
MBER_STTUS = #mberSttus#
|
||||
WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</update>
|
||||
|
||||
<!-- ================================================================
|
||||
개인정보 파기 — 행을 지우지 않고 개인정보 항목만 비운다.
|
||||
|
||||
[왜 DELETE 가 아닌가] MBER_ID·ESNTL_ID 는 신청·활동확인·마일리지 등 여러 표가
|
||||
참조한다. 행을 지우면 그 기록들이 주인 없는 값을 가리키게 되고, 통계·정산이 어긋난다.
|
||||
개인정보 보호의 목적은 "식별할 수 있는 정보를 없애는 것"이지 "행을 지우는 것"이 아니므로,
|
||||
식별 항목만 비우고 상태를 탈퇴로 바꾼다.
|
||||
|
||||
[기존 탈퇴(mberManageDAO.updateDeleteMber_S)와의 차이] 기존 탈퇴 구문은 이름
|
||||
(MBER_NM/MBER_NM_ENC/MBER_NM_HASH)을 지우지 않아 탈퇴 후에도 이름이 남는다.
|
||||
이 구문은 이름과 암호문·해시까지 함께 파기한다.
|
||||
================================================================ -->
|
||||
<update id="MberCryptoAdminDAO.deleteMberPii"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
UPDATE COMTNGNRLMBER
|
||||
SET MBER_NM = #mberNm#,
|
||||
MBER_NM_ENC = NULL,
|
||||
MBER_NM_HASH = NULL,
|
||||
CRYPTO_KEY_VER = NULL,
|
||||
IHIDNUM = NULL,
|
||||
MBER_EMAIL_ADRES = NULL,
|
||||
MBTLNUM = '',
|
||||
MBER_FXNUM = '',
|
||||
ZIP = '',
|
||||
ADRES = '',
|
||||
DETAIL_ADRES = '',
|
||||
AREA_NO = '',
|
||||
END_TELNO = '',
|
||||
MIDDLE_TELNO = '',
|
||||
SEXDSTN_CODE = '',
|
||||
MBER_STTUS = 'D'
|
||||
WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</update>
|
||||
|
||||
<!-- ================================================================
|
||||
재인증 — 취급자 본인 비밀번호 대조용 저장값 조회.
|
||||
반환값은 해시 문자열이며 화면으로 나가지 않는다(서비스에서 대조만 한다).
|
||||
================================================================ -->
|
||||
<select id="MberCryptoAdminDAO.selectHandlerPassword"
|
||||
parameterClass="java.lang.String" resultClass="java.lang.String">
|
||||
<![CDATA[
|
||||
SELECT PASSWORD
|
||||
FROM COMTNEMPLYRINFO
|
||||
WHERE EMPLYR_ID = #emplyrId#
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<!-- ================================================================
|
||||
관리자 접근 이력 (사유·재인증 여부). 개인정보 본문은 적재하지 않는다.
|
||||
조회·적재만 두고 UPDATE/DELETE 는 정의하지 않는다 — 소명 근거라 사후 변경 경로를 만들지 않는다.
|
||||
================================================================ -->
|
||||
<insert id="MberCryptoAdminDAO.insertAccessHistory"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoAccessHistoryVO">
|
||||
<![CDATA[
|
||||
INSERT INTO MBER_CRYPTO_ACCESS_HISTORY (
|
||||
HANDLR_ID, ACTION_TY, TRGET_MBER_ID, ACCESS_REASON,
|
||||
CHANGED_ITEMS, HANDLR_IP, REAUTH_YN, ACCESS_DT
|
||||
) VALUES (
|
||||
#handlrId#, #actionTy#, #trgetMberId:VARCHAR#, #accessReason#,
|
||||
#changedItems:VARCHAR#, #handlrIp:VARCHAR#, #reauthYn#, NOW()
|
||||
)
|
||||
]]>
|
||||
</insert>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectAccessHistoryList"
|
||||
parameterClass="java.util.HashMap"
|
||||
resultClass="egovframework.com.cmm.privacy.crypto.MberCryptoAccessHistoryVO">
|
||||
SELECT HIST_ID AS histId,
|
||||
HANDLR_ID AS handlrId,
|
||||
ACTION_TY AS actionTy,
|
||||
TRGET_MBER_ID AS trgetMberId,
|
||||
ACCESS_REASON AS accessReason,
|
||||
CHANGED_ITEMS AS changedItems,
|
||||
HANDLR_IP AS handlrIp,
|
||||
REAUTH_YN AS reauthYn,
|
||||
ACCESS_DT AS accessDt
|
||||
FROM MBER_CRYPTO_ACCESS_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="trgetMberId"> TRGET_MBER_ID = #trgetMberId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="handlrId"> HANDLR_ID = #handlrId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="actionTy"> ACTION_TY = #actionTy# </isNotEmpty>
|
||||
</dynamic>
|
||||
ORDER BY HIST_ID DESC
|
||||
LIMIT #firstIndex#, #recordCountPerPage#
|
||||
</select>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectAccessHistoryListCount"
|
||||
parameterClass="java.util.HashMap" resultClass="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM MBER_CRYPTO_ACCESS_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="trgetMberId"> TRGET_MBER_ID = #trgetMberId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="handlrId"> HANDLR_ID = #handlrId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="actionTy"> ACTION_TY = #actionTy# </isNotEmpty>
|
||||
</dynamic>
|
||||
</select>
|
||||
|
||||
</sqlMap>
|
||||
@ -0,0 +1,232 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
개인정보보호 기능개선 사업 (추가제안 Action4 / 제안서 슬라이드16)
|
||||
관리자 전용 회원정보 CRUD + 접근 이력 — Oracle 쌍.
|
||||
|
||||
MySQL 쌍(PrivacyMberCryptoAdmin_SQL_Mysql.xml)과 statement id·파라미터 계약이 동일하다.
|
||||
방언 차이만 다르다:
|
||||
· 페이징 : LIMIT offset,count → ROWNUM 이중 인라인뷰
|
||||
· 현재시각: NOW() → SYSDATE
|
||||
· 날짜포맷: 그대로 반환 → TO_CHAR(..., 'YYYY-MM-DD HH24:MI:SS')
|
||||
|
||||
운영 DB 는 MySQL(csv)이며 이 파일은 방언 쌍 유지 관례에 따른 것이다.
|
||||
이름 컬럼 취급 규칙·검색 제약은 MySQL 쌍 주석 참조.
|
||||
-->
|
||||
<sqlMap namespace="MberCryptoAdminDAO">
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectMberList"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoSearchVO"
|
||||
resultClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
SELECT mberId, uniqId, mberNm, cryptoKeyVer, mberEmailAdres, moblphonNo,
|
||||
zip, adres, detailAdres, sexdstnCode, mberSttus, sbscrbDe
|
||||
FROM (
|
||||
SELECT ROWNUM AS rnum, a.*
|
||||
FROM (
|
||||
SELECT MBER_ID AS mberId,
|
||||
ESNTL_ID AS uniqId,
|
||||
MBER_NM_ENC AS mberNm,
|
||||
CRYPTO_KEY_VER AS cryptoKeyVer,
|
||||
MBER_EMAIL_ADRES AS mberEmailAdres,
|
||||
MBTLNUM AS moblphonNo,
|
||||
ZIP AS zip,
|
||||
ADRES AS adres,
|
||||
DETAIL_ADRES AS detailAdres,
|
||||
SEXDSTN_CODE AS sexdstnCode,
|
||||
MBER_STTUS AS mberSttus,
|
||||
TO_CHAR(SBSCRB_DE, 'YYYY-MM-DD HH24:MI:SS') AS sbscrbDe
|
||||
FROM COMTNGNRLMBER
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="mberId"> MBER_ID LIKE #mberIdLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberNmHash"> MBER_NM_HASH = #mberNmHash# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberEmailAdres"> MBER_EMAIL_ADRES LIKE #mberEmailAdresLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="moblphonNo"> MBTLNUM LIKE #moblphonNoLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberSttus"> MBER_STTUS = #mberSttus# </isNotEmpty>
|
||||
</dynamic>
|
||||
ORDER BY SBSCRB_DE DESC, MBER_ID DESC
|
||||
) a
|
||||
WHERE ROWNUM <= #firstIndex# + #recordCountPerPage#
|
||||
)
|
||||
WHERE rnum > #firstIndex#
|
||||
</select>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectMberListCount"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoSearchVO"
|
||||
resultClass="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM COMTNGNRLMBER
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="mberId"> MBER_ID LIKE #mberIdLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberNmHash"> MBER_NM_HASH = #mberNmHash# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberEmailAdres"> MBER_EMAIL_ADRES LIKE #mberEmailAdresLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="moblphonNo"> MBTLNUM LIKE #moblphonNoLike# ESCAPE '!' </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="mberSttus"> MBER_STTUS = #mberSttus# </isNotEmpty>
|
||||
</dynamic>
|
||||
</select>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectMber"
|
||||
parameterClass="java.lang.String"
|
||||
resultClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
SELECT MBER_ID AS mberId,
|
||||
ESNTL_ID AS uniqId,
|
||||
MBER_NM_ENC AS mberNm,
|
||||
CRYPTO_KEY_VER AS cryptoKeyVer,
|
||||
MBER_EMAIL_ADRES AS mberEmailAdres,
|
||||
MBTLNUM AS moblphonNo,
|
||||
ZIP AS zip,
|
||||
ADRES AS adres,
|
||||
DETAIL_ADRES AS detailAdres,
|
||||
SEXDSTN_CODE AS sexdstnCode,
|
||||
MBER_STTUS AS mberSttus,
|
||||
TO_CHAR(SBSCRB_DE, 'YYYY-MM-DD HH24:MI:SS') AS sbscrbDe
|
||||
FROM COMTNGNRLMBER
|
||||
WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectMberIdCount"
|
||||
parameterClass="java.lang.String" resultClass="java.lang.Integer">
|
||||
<![CDATA[
|
||||
SELECT COUNT(1) FROM COMTNGNRLMBER WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<insert id="MberCryptoAdminDAO.insertMber"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
INSERT INTO COMTNGNRLMBER (
|
||||
ESNTL_ID, MBER_ID, MBER_NM, MBER_NM_ENC, MBER_NM_HASH, CRYPTO_KEY_VER,
|
||||
PASSWORD, SEXDSTN_CODE, ZIP, ADRES, AREA_NO, MBER_STTUS, DETAIL_ADRES,
|
||||
END_TELNO, MBTLNUM, MBER_EMAIL_ADRES, MIDDLE_TELNO, SBSCRB_DE
|
||||
) VALUES (
|
||||
#uniqId#,
|
||||
#mberId#,
|
||||
#mberNm#,
|
||||
#mberNmEnc#,
|
||||
#mberNmHash#,
|
||||
#cryptoKeyVer:NUMERIC#,
|
||||
#password#,
|
||||
#sexdstnCode:VARCHAR#,
|
||||
#zip:VARCHAR#,
|
||||
#adres:VARCHAR#,
|
||||
'',
|
||||
#mberSttus#,
|
||||
#detailAdres:VARCHAR#,
|
||||
'',
|
||||
#moblphonNo:VARCHAR#,
|
||||
#mberEmailAdres:VARCHAR#,
|
||||
'',
|
||||
SYSDATE
|
||||
)
|
||||
]]>
|
||||
</insert>
|
||||
|
||||
<update id="MberCryptoAdminDAO.updateMber"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
UPDATE COMTNGNRLMBER
|
||||
SET MBER_NM_ENC = #mberNmEnc#,
|
||||
MBER_NM_HASH = #mberNmHash#,
|
||||
CRYPTO_KEY_VER = #cryptoKeyVer:NUMERIC#,
|
||||
MBER_EMAIL_ADRES = #mberEmailAdres:VARCHAR#,
|
||||
MBTLNUM = #moblphonNo:VARCHAR#,
|
||||
ZIP = #zip:VARCHAR#,
|
||||
ADRES = #adres:VARCHAR#,
|
||||
DETAIL_ADRES = #detailAdres:VARCHAR#,
|
||||
SEXDSTN_CODE = #sexdstnCode:VARCHAR#,
|
||||
MBER_STTUS = #mberSttus#
|
||||
WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</update>
|
||||
|
||||
<update id="MberCryptoAdminDAO.deleteMberPii"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoVO">
|
||||
<![CDATA[
|
||||
UPDATE COMTNGNRLMBER
|
||||
SET MBER_NM = #mberNm#,
|
||||
MBER_NM_ENC = NULL,
|
||||
MBER_NM_HASH = NULL,
|
||||
CRYPTO_KEY_VER = NULL,
|
||||
IHIDNUM = NULL,
|
||||
MBER_EMAIL_ADRES = NULL,
|
||||
MBTLNUM = '',
|
||||
MBER_FXNUM = '',
|
||||
ZIP = '',
|
||||
ADRES = '',
|
||||
DETAIL_ADRES = '',
|
||||
AREA_NO = '',
|
||||
END_TELNO = '',
|
||||
MIDDLE_TELNO = '',
|
||||
SEXDSTN_CODE = '',
|
||||
MBER_STTUS = 'D'
|
||||
WHERE MBER_ID = #mberId#
|
||||
]]>
|
||||
</update>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectHandlerPassword"
|
||||
parameterClass="java.lang.String" resultClass="java.lang.String">
|
||||
<![CDATA[
|
||||
SELECT PASSWORD
|
||||
FROM COMTNEMPLYRINFO
|
||||
WHERE EMPLYR_ID = #emplyrId#
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<insert id="MberCryptoAdminDAO.insertAccessHistory"
|
||||
parameterClass="egovframework.com.cmm.privacy.crypto.MberCryptoAccessHistoryVO">
|
||||
<![CDATA[
|
||||
INSERT INTO MBER_CRYPTO_ACCESS_HISTORY (
|
||||
HIST_ID, HANDLR_ID, ACTION_TY, TRGET_MBER_ID, ACCESS_REASON,
|
||||
CHANGED_ITEMS, HANDLR_IP, REAUTH_YN, ACCESS_DT
|
||||
) VALUES (
|
||||
MBER_CRYPTO_ACCESS_HIST_SEQ.NEXTVAL,
|
||||
#handlrId#, #actionTy#, #trgetMberId:VARCHAR#, #accessReason#,
|
||||
#changedItems:VARCHAR#, #handlrIp:VARCHAR#, #reauthYn#, SYSDATE
|
||||
)
|
||||
]]>
|
||||
</insert>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectAccessHistoryList"
|
||||
parameterClass="java.util.HashMap"
|
||||
resultClass="egovframework.com.cmm.privacy.crypto.MberCryptoAccessHistoryVO">
|
||||
SELECT histId, handlrId, actionTy, trgetMberId, accessReason,
|
||||
changedItems, handlrIp, reauthYn, accessDt
|
||||
FROM (
|
||||
SELECT ROWNUM AS rnum, a.*
|
||||
FROM (
|
||||
SELECT HIST_ID AS histId,
|
||||
HANDLR_ID AS handlrId,
|
||||
ACTION_TY AS actionTy,
|
||||
TRGET_MBER_ID AS trgetMberId,
|
||||
ACCESS_REASON AS accessReason,
|
||||
CHANGED_ITEMS AS changedItems,
|
||||
HANDLR_IP AS handlrIp,
|
||||
REAUTH_YN AS reauthYn,
|
||||
TO_CHAR(ACCESS_DT, 'YYYY-MM-DD HH24:MI:SS') AS accessDt
|
||||
FROM MBER_CRYPTO_ACCESS_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="trgetMberId"> TRGET_MBER_ID = #trgetMberId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="handlrId"> HANDLR_ID = #handlrId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="actionTy"> ACTION_TY = #actionTy# </isNotEmpty>
|
||||
</dynamic>
|
||||
ORDER BY HIST_ID DESC
|
||||
) a
|
||||
WHERE ROWNUM <= #firstIndex# + #recordCountPerPage#
|
||||
)
|
||||
WHERE rnum > #firstIndex#
|
||||
</select>
|
||||
|
||||
<select id="MberCryptoAdminDAO.selectAccessHistoryListCount"
|
||||
parameterClass="java.util.HashMap" resultClass="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM MBER_CRYPTO_ACCESS_HISTORY
|
||||
<dynamic prepend="WHERE">
|
||||
<isNotEmpty prepend="AND" property="trgetMberId"> TRGET_MBER_ID = #trgetMberId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="handlrId"> HANDLR_ID = #handlrId# </isNotEmpty>
|
||||
<isNotEmpty prepend="AND" property="actionTy"> ACTION_TY = #actionTy# </isNotEmpty>
|
||||
</dynamic>
|
||||
</select>
|
||||
|
||||
</sqlMap>
|
||||
@ -94,10 +94,10 @@
|
||||
</li>
|
||||
</c:if>
|
||||
<c:if test="${menuUpperVo2.menuNo eq '9100030'}">
|
||||
<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' || param.page eq '9100044' || param.page eq '9100047'}">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' || param.page eq '9100044' || param.page eq '9100047' || param.page eq '9100048' || param.page eq '9100049' || param.page eq '9100050' || param.page eq '9100051'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
||||
<c:forEach items="${list_menulist}" var="menuUpperVo3">
|
||||
<c:if test="${menuUpperVo3.menuNo eq '9100030'}">
|
||||
<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' || param.page eq '9100044' || param.page eq '9100047'}">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' || param.page eq '9100044' || param.page eq '9100047' || param.page eq '9100048' || param.page eq '9100049' || param.page eq '9100050' || param.page eq '9100051'}">style="display: block;"</c:if>>
|
||||
<c:forEach items="${list_menulist}" var="menuUpperVo4">
|
||||
<c:choose>
|
||||
<c:when test="${menuUpperVo4.menuNo eq '9100031'}">
|
||||
@ -131,6 +131,22 @@
|
||||
<c:when test="${menuUpperVo4.menuNo eq '9100047'}">
|
||||
<li <c:if test="${param.page eq '9100047'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||
</c:when>
|
||||
<%-- [개인정보/SFR-004-7 #48] 접속기록 정기점검 리포트(점검결과 입력·보관) --%>
|
||||
<c:when test="${menuUpperVo4.menuNo eq '9100048'}">
|
||||
<li <c:if test="${param.page eq '9100048'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||
</c:when>
|
||||
<%-- [개인정보/과업내용 다-① #49] 비정상접근 탐지 5유형 --%>
|
||||
<c:when test="${menuUpperVo4.menuNo eq '9100049'}">
|
||||
<li <c:if test="${param.page eq '9100049'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||
</c:when>
|
||||
<%-- [개인정보/SER-002-2] 접속기록 파기이력(파티션 DROP/ADD 증빙) --%>
|
||||
<c:when test="${menuUpperVo4.menuNo eq '9100050'}">
|
||||
<li <c:if test="${param.page eq '9100050'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||
</c:when>
|
||||
<%-- [개인정보/추가제안 Action4] 회원정보 암호화 관리(복호화 CRUD·사유·재인증) --%>
|
||||
<c:when test="${menuUpperVo4.menuNo eq '9100051'}">
|
||||
<li <c:if test="${param.page eq '9100051'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||
</c:when>
|
||||
</c:choose>
|
||||
</c:forEach>
|
||||
</ul>
|
||||
|
||||
221
src/main/webapp/WEB-INF/jsp/site/mnt/sec/abnormal/list.jsp
Normal file
221
src/main/webapp/WEB-INF/jsp/site/mnt/sec/abnormal/list.jsp
Normal file
@ -0,0 +1,221 @@
|
||||
<%@ 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="spring" uri="http://www.springframework.org/tags"%>
|
||||
|
||||
<%--
|
||||
비정상 접근 탐지 알림함 (개인정보보호 과업내용 다-① / SER-002 / #49)
|
||||
· AbnormalAccessDetectionService(#35)의 5유형을 기간 단위로 즉시 집계해 보여준다
|
||||
· 탐지 결과를 별도 테이블에 적재하지 않는 이유는 컨트롤러 Javadoc 참조(임계치 변경 시 과거 기준이 굳는 문제)
|
||||
· 각 항목은 접속기록 조회화면(#47)으로 조건을 넘겨 바로 원본 기록을 확인할 수 있게 연결한다
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
function fn_search() {
|
||||
document.frm.method = "get";
|
||||
document.frm.action = "<c:url value='${PATH}/sec/abnormal/listView.do' />";
|
||||
document.frm.submit();
|
||||
}
|
||||
|
||||
/* 최근 N일로 기간을 다시 잡는다(자주 쓰는 구간을 손으로 입력하지 않게) */
|
||||
function fn_range(days) {
|
||||
var to = new Date();
|
||||
var from = new Date();
|
||||
from.setDate(from.getDate() - days);
|
||||
document.getElementById("fromDt").value = fn_fmt(from);
|
||||
document.getElementById("toDt").value = fn_fmt(to);
|
||||
fn_search();
|
||||
}
|
||||
|
||||
function fn_fmt(d) {
|
||||
var m = d.getMonth() + 1;
|
||||
var day = d.getDate();
|
||||
return d.getFullYear() + "-" + (m < 10 ? "0" + m : m) + "-" + (day < 10 ? "0" + day : day);
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.msg { margin:6px 0 10px; padding:8px 10px; background:#fff8e6; border:1px solid #e6d5a8; font-size:13px; color:#7a5b00; }
|
||||
.mono { font-family:Consolas,monospace; }
|
||||
.nodata { color:#aaa; }
|
||||
.btn_area { margin:8px 0; text-align:right; }
|
||||
.sum_line { margin:10px 0 14px; padding:10px 12px; background:#eef2f7; border:1px solid #dde3ea; font-size:13px; color:#334; }
|
||||
.card_wrap { overflow:hidden; margin:0 0 6px; }
|
||||
.card { float:left; width:18.4%; margin:0 1.9% 12px 0; box-sizing:border-box; border:1px solid #dde3ea; background:#fff; }
|
||||
.card.last { margin-right:0; }
|
||||
.card .hd { padding:7px 9px; background:#f7f9fb; border-bottom:1px solid #dde3ea; font-size:12px; color:#445; font-weight:bold; }
|
||||
.card .bd { padding:12px 9px; text-align:center; }
|
||||
.card .cnt { font-size:26px; font-weight:bold; color:#4a7ebb; line-height:1.1; }
|
||||
.card .cnt.hit { color:#c25b5b; }
|
||||
.card .unit { font-size:12px; color:#778; }
|
||||
.card .dt { padding:0 9px 10px; font-size:11px; color:#889; text-align:center; word-break:break-all; }
|
||||
.detail_box { margin:6px 0 18px; }
|
||||
.detail_box h3 { margin:0 0 6px; font-size:14px; color:#334; }
|
||||
.detail_box h3 .hint { font-size:12px; font-weight:normal; color:#889; margin-left:8px; }
|
||||
.clear { clear:both; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<c:if test="${not empty resultMsg}">
|
||||
<div class="msg"><c:out value="${resultMsg}" /></div>
|
||||
</c:if>
|
||||
|
||||
<form id="frm" name="frm" method="get" onsubmit="fn_search(); return false;">
|
||||
<!-- 검색창 -->
|
||||
<div id="search_wrap">
|
||||
<fieldset class="date_search">
|
||||
<span>탐지기간 :</span>
|
||||
<input type="text" name="fromDt" id="fromDt" class="tbox" title="시작일"
|
||||
value="<c:out value='${fromDt}'/>" placeholder="yyyy-MM-dd" maxlength="10" />
|
||||
~
|
||||
<input type="text" name="toDt" id="toDt" class="tbox" title="종료일"
|
||||
value="<c:out value='${toDt}'/>" placeholder="yyyy-MM-dd" maxlength="10" />
|
||||
<input type="submit" class="btn_search" value="탐지" />
|
||||
<span>
|
||||
<a href="#none" onclick="fn_range(7); return false;">최근 7일</a> ·
|
||||
<a href="#none" onclick="fn_range(30); return false;">30일</a> ·
|
||||
<a href="#none" onclick="fn_range(90); return false;">90일</a>
|
||||
</span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<!-- //검색창 -->
|
||||
</form>
|
||||
|
||||
<div class="guide">
|
||||
· 개인정보 접속기록(730일 보존)을 <strong>조회 시점의 정책 임계치</strong>로 다시 집계한 결과입니다. 탐지 결과를 따로 저장하지 않으므로 임계치를 조정하면 과거 기간도 새 기준으로 다시 계산됩니다.<br />
|
||||
· <strong>탐지되었다는 것이 곧 위반은 아닙니다.</strong> 업무상 정당한 대량조회·야간작업이 있을 수 있으므로, 각 항목의 <strong>접속기록 보기</strong>로 원본 기록을 확인한 뒤 판단하세요.<br />
|
||||
· 월 단위 점검 기록으로 남겨야 하는 경우에는 <strong>정기점검 리포트</strong> 화면에서 점검결과·조치사항을 입력하세요(이 화면은 조회 전용입니다).<br />
|
||||
· 5유형 각각이 기간 전체를 훑는 집계라 기간이 넓으면 조회가 느려집니다(최대 366일).
|
||||
</div>
|
||||
|
||||
<c:if test="${not empty summary}">
|
||||
<div class="sum_line">
|
||||
<strong><c:out value="${fromDt}" /> ~ <c:out value="${toDt}" /></strong> 탐지 요약 : <c:out value="${summary}" />
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<%-- 유형별 요약 카드 5장 --%>
|
||||
<div class="card_wrap">
|
||||
<c:forEach items="${resultList}" var="r" varStatus="st">
|
||||
<div class="card <c:if test='${st.last}'>last</c:if>">
|
||||
<div class="hd"><c:out value="${r.type.label}" /></div>
|
||||
<div class="bd">
|
||||
<span class="cnt <c:if test='${r.count > 0}'>hit</c:if>"><c:out value="${r.count}" /></span>
|
||||
<span class="unit">
|
||||
<c:choose>
|
||||
<c:when test="${r.type eq 'UNAUTHORIZED_ACCESS' or r.type eq 'OFF_HOURS_ACCESS'}">건</c:when>
|
||||
<c:otherwise>명</c:otherwise>
|
||||
</c:choose>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dt"><c:out value="${r.detail}" /></div>
|
||||
</div>
|
||||
</c:forEach>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
|
||||
<%-- 유형별 상세 --%>
|
||||
<c:forEach items="${resultList}" var="r">
|
||||
<div class="detail_box">
|
||||
<h3>
|
||||
<c:out value="${r.type.label}" />
|
||||
<span class="hint">
|
||||
<c:choose>
|
||||
<c:when test="${r.type eq 'UNAUTHORIZED_ACCESS'}">처리결과가 실패·차단(F)으로 남은 기록입니다 — 권한 없는 화면 접근 시도가 여기에 잡힙니다.</c:when>
|
||||
<c:when test="${r.type eq 'MASS_QUERY'}">단위기간 조회(S) 건수가 임계를 넘은 취급자입니다.</c:when>
|
||||
<c:when test="${r.type eq 'OFF_HOURS_ACCESS'}">업무시간 밖(기본 09시 이전·18시 이후)에 처리된 기록입니다.</c:when>
|
||||
<c:when test="${r.type eq 'ACCOUNT_SHARING'}">같은 계정이 서로 다른 IP에서 사용된 경우입니다 — 계정 공유·탈취 의심 신호입니다.</c:when>
|
||||
<c:otherwise>같은 취급자에게서 실패(F) 기록이 반복된 경우입니다.</c:otherwise>
|
||||
</c:choose>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<c:choose>
|
||||
<%-- 대상자형(대량조회·계정공유·인증실패반복) : 누가 걸렸는지 목록으로 --%>
|
||||
<c:when test="${not empty r.rows}">
|
||||
<table summary="<c:out value='${r.type.label}'/> 의심 대상 목록" class="list1">
|
||||
<caption><c:out value="${r.type.label}" /> 의심 대상 목록</caption>
|
||||
<colgroup><col width="8%" /><col width="30%" /><col width="20%" /><col width="*" /></colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>NO.</th>
|
||||
<th>접속자ID</th>
|
||||
<th>
|
||||
<c:choose>
|
||||
<c:when test="${r.type eq 'ACCOUNT_SHARING'}">사용 IP 수</c:when>
|
||||
<c:when test="${r.type eq 'REPEATED_AUTH_FAILURE'}">실패 횟수</c:when>
|
||||
<c:otherwise>조회 건수</c:otherwise>
|
||||
</c:choose>
|
||||
</th>
|
||||
<th>원본 확인</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach items="${r.rows}" var="row" varStatus="st">
|
||||
<tr>
|
||||
<td><c:out value="${st.index + 1}" /></td>
|
||||
<td class="mono"><c:out value="${row.conectId}" /></td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${r.type eq 'ACCOUNT_SHARING'}"><c:out value="${row.ipCnt}" /></c:when>
|
||||
<c:otherwise><c:out value="${row.cnt}" /></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td>
|
||||
<%-- 접속기록 조회화면(#47)으로 조건을 넘겨 원본을 바로 확인한다.
|
||||
인증실패반복은 처리결과 F, 대량조회는 처리구분 S 로 좁혀 준다. --%>
|
||||
<a href="<c:url value='${PATH}/sec/accessLog/listView.do'/>?conectId=<c:out value='${row.conectId}'/>&fromDt=<c:out value='${fromDt}'/>&toDt=<c:out value='${toDt}'/><c:if test="${r.type eq 'REPEATED_AUTH_FAILURE'}">&processResult=F</c:if><c:if test="${r.type eq 'MASS_QUERY'}">&dmlGubun=S</c:if>">접속기록 보기</a>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</c:when>
|
||||
|
||||
<%-- 건수형(비인가접근·비정상시간대) : 대상자가 아니라 기간 전체 건수라 목록이 없다 --%>
|
||||
<c:when test="${r.type eq 'UNAUTHORIZED_ACCESS' and r.count > 0}">
|
||||
<div class="sum_line">
|
||||
기간 내 <strong><c:out value="${r.count}" /></strong>건 —
|
||||
<a href="<c:url value='${PATH}/sec/accessLog/listView.do'/>?processResult=F&fromDt=<c:out value='${fromDt}'/>&toDt=<c:out value='${toDt}'/>">접속기록에서 실패·차단 기록 보기</a>
|
||||
</div>
|
||||
</c:when>
|
||||
<c:when test="${r.type eq 'OFF_HOURS_ACCESS' and r.count > 0}">
|
||||
<div class="sum_line">
|
||||
기간 내 <strong><c:out value="${r.count}" /></strong>건 —
|
||||
<a href="<c:url value='${PATH}/sec/accessLog/listView.do'/>?fromDt=<c:out value='${fromDt}'/>&toDt=<c:out value='${toDt}'/>">접속기록에서 해당 기간 기록 보기</a>
|
||||
<span class="nodata">(시간대 조건은 접속기록 화면의 접속일시로 확인하세요)</span>
|
||||
</div>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="sum_line"><span class="nodata">해당 기간에 탐지된 항목이 없습니다.</span></div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</div>
|
||||
</c:forEach>
|
||||
|
||||
<c:if test="${empty resultList}">
|
||||
<table summary="비정상 접근 탐지 결과" class="list1">
|
||||
<caption>비정상 접근 탐지 결과</caption>
|
||||
<tbody>
|
||||
<tr><td><spring:message code="common.nodata.msg" /></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</c:if>
|
||||
|
||||
</div>
|
||||
<!--// 컨텐츠 영역 -->
|
||||
</div>
|
||||
<!--// 오른쪽 영역 -->
|
||||
</div>
|
||||
<!--// 전체 둘러싸기 -->
|
||||
</body>
|
||||
</html>
|
||||
243
src/main/webapp/WEB-INF/jsp/site/mnt/sec/inspection/list.jsp
Normal file
243
src/main/webapp/WEB-INF/jsp/site/mnt/sec/inspection/list.jsp
Normal file
@ -0,0 +1,243 @@
|
||||
<%@ 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="spring" uri="http://www.springframework.org/tags"%>
|
||||
|
||||
<%--
|
||||
개인정보 접속기록 정기점검 리포트 (개인정보보호 SFR-004-7 / SER-002 / #48)
|
||||
· 월 1회 배치(#43)가 전월분 접속기록을 집계해 1행씩 적재한 PDAL_INSPECTION_REPORT
|
||||
· 점검자가 점검결과·조치사항을 입력하면 INSPECTOR_ID/INSPECT_RESULT/ACTN_MATTER/INSPECT_DT 가 채워진다(#44)
|
||||
· 배치 집계값(총건수·무결성·이상징후 등)은 화면에서 수정하지 않는다 — 감사 증빙이라 사후 변경되면 안 된다
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
function fn_list() {
|
||||
document.frm.method = "get";
|
||||
document.frm.action = "<c:url value='${PATH}/sec/inspection/listView.do' />";
|
||||
document.frm.submit();
|
||||
}
|
||||
|
||||
/* 상세 펼치기 — 목록을 떠나지 않고 같은 화면에 상세·입력폼을 연다(다른 달과 대조하며 점검) */
|
||||
function fn_detail(rptId) {
|
||||
location.href = "<c:url value='${PATH}/sec/inspection/listView.do' />"
|
||||
+ "?rptId=" + rptId
|
||||
+ "&rptYmPrefix=" + encodeURIComponent(document.getElementById("rptYmPrefix").value)
|
||||
+ "&inspectedYn=" + encodeURIComponent(document.getElementById("inspectedYn").value);
|
||||
}
|
||||
|
||||
/* 점검결과 저장 — 점검결과는 필수. 서버도 다시 막지만(IllegalArgumentException)
|
||||
빈 값으로 왕복하는 낭비를 줄인다. */
|
||||
function fn_saveResult() {
|
||||
var r = document.getElementById("inspectResult");
|
||||
if (r.value.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("점검결과는 필수 입력입니다.");
|
||||
r.focus();
|
||||
return;
|
||||
}
|
||||
var f = document.saveFrm;
|
||||
f.method = "post";
|
||||
f.action = "<c:url value='${PATH}/sec/inspection/saveResultView.do' />";
|
||||
f.submit();
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:12px; color:#fff; }
|
||||
.badge.ok { background:#4a7ebb; }
|
||||
.badge.ng { background:#c25b5b; }
|
||||
.badge.wait { background:#999; }
|
||||
.mono { font-family:Consolas,monospace; }
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.msg { margin:6px 0 10px; padding:8px 10px; background:#fff8e6; border:1px solid #e6d5a8; font-size:13px; color:#7a5b00; }
|
||||
.btn_area { margin:8px 0; text-align:right; }
|
||||
.nodata { color:#aaa; }
|
||||
.rpt_detail { margin:14px 0 20px; padding:0; border:1px solid #dde3ea; }
|
||||
.rpt_detail h3 { margin:0; padding:8px 12px; background:#eef2f7; font-size:14px; color:#334; border-bottom:1px solid #dde3ea; }
|
||||
.rpt_detail .inner { padding:12px; }
|
||||
.rpt_detail dl { margin:0 0 10px; font-size:13px; }
|
||||
.rpt_detail dt { float:left; width:120px; clear:both; color:#556; padding:3px 0; }
|
||||
.rpt_detail dd { margin-left:130px; padding:3px 0; word-break:break-all; }
|
||||
.rpt_detail textarea { width:98%; }
|
||||
.summary_box { background:#fbfcfd; border:1px solid #e5eaf0; padding:8px 10px; font-size:12px; color:#445; white-space:pre-wrap; word-break:break-all; }
|
||||
tr.on td { background:#f2f6fb; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<c:if test="${not empty resultMsg}">
|
||||
<div class="msg"><c:out value="${resultMsg}" /></div>
|
||||
</c:if>
|
||||
|
||||
<form id="frm" name="frm" method="get" onsubmit="fn_list(); return false;">
|
||||
<!-- 검색창 -->
|
||||
<div id="search_wrap">
|
||||
<fieldset class="date_search">
|
||||
<span>대상 연도 :</span>
|
||||
<input type="text" name="rptYmPrefix" id="rptYmPrefix" class="tbox" title="대상 연도(yyyy)"
|
||||
value="<c:out value='${rptYmPrefix}'/>" placeholder="yyyy" maxlength="4" />
|
||||
<span>점검여부 :</span>
|
||||
<select name="inspectedYn" id="inspectedYn" title="점검여부">
|
||||
<option value="" <c:if test="${empty inspectedYn}">selected="selected"</c:if>>전체</option>
|
||||
<option value="Y" <c:if test="${inspectedYn eq 'Y'}">selected="selected"</c:if>>점검완료</option>
|
||||
<option value="N" <c:if test="${inspectedYn eq 'N'}">selected="selected"</c:if>>미점검</option>
|
||||
</select>
|
||||
<input type="submit" class="btn_search" value="검색" />
|
||||
</fieldset>
|
||||
</div>
|
||||
<!-- //검색창 -->
|
||||
</form>
|
||||
|
||||
<div class="guide">
|
||||
· 매월 1일 배치가 <strong>전월분 접속기록</strong>을 집계해 리포트 1건을 생성합니다(개인정보의 안전성 확보조치 기준 제8조 — 월 1회 이상 점검).<br />
|
||||
· <strong>집계값(총 처리건수·실패건수·무결성 검증결과·이상징후·다운로드 건수)은 배치가 적재한 값이며 화면에서 수정할 수 없습니다</strong> — 사후 변경이 가능하면 점검 증빙이 되지 못합니다.<br />
|
||||
· 점검자가 입력하는 항목은 <strong>점검결과·조치사항</strong>이며, 저장 시 <strong>점검자 계정과 점검일시가 자동 기록</strong>됩니다.<br />
|
||||
· <strong>무결성 위반(VIOLATION)</strong>은 접속기록 해시체인이 끊긴 상태로, 위·변조 가능성을 뜻합니다. 최초 불일치 기록번호를 확인해 즉시 조치사항에 남기세요.
|
||||
</div>
|
||||
|
||||
<div class="btn_area">
|
||||
총 <strong><c:out value="${totalCount}" /></strong>건
|
||||
</div>
|
||||
|
||||
<table summary="정기점검 리포트 목록" class="list1">
|
||||
<caption>정기점검 리포트 목록</caption>
|
||||
<colgroup>
|
||||
<col width="9%" /><col width="15%" /><col width="9%" /><col width="9%" />
|
||||
<col width="11%" /><col width="*" /><col width="10%" /><col width="12%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>대상연월</th>
|
||||
<th>집계기간</th>
|
||||
<th>총 처리</th>
|
||||
<th>실패/차단</th>
|
||||
<th>무결성</th>
|
||||
<th>이상징후 요약</th>
|
||||
<th>점검여부</th>
|
||||
<th>점검자/일시</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach items="${resultList}" var="r">
|
||||
<tr <c:if test="${not empty report and report.rptId eq r.rptId}">class="on"</c:if>>
|
||||
<td><a href="#none" onclick="fn_detail('<c:out value="${r.rptId}"/>'); return false;"><strong><c:out value="${r.rptYm}" /></strong></a></td>
|
||||
<td class="mono"><c:out value="${fn:substring(r.periodFrom, 0, 10)}" /> ~ <c:out value="${fn:substring(r.periodTo, 0, 10)}" /></td>
|
||||
<td><c:out value="${r.totalLogCnt}" /></td>
|
||||
<td><c:out value="${r.failCnt}" /></td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${r.integrityResult eq 'OK'}"><span class="badge ok">정상</span></c:when>
|
||||
<c:otherwise><span class="badge ng">위반</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td><c:out value="${r.abnormalSummary}" /></td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${not empty r.inspectDt}"><span class="badge ok">점검완료</span></c:when>
|
||||
<c:otherwise><span class="badge wait">미점검</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${not empty r.inspectDt}">
|
||||
<c:out value="${r.inspectorId}" /><br /><span class="mono"><c:out value="${r.inspectDt}" /></span>
|
||||
</c:when>
|
||||
<c:otherwise><span class="nodata">-</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
<c:if test="${fn:length(resultList) == 0}">
|
||||
<tr>
|
||||
<td colspan="8"><spring:message code="common.nodata.msg" /></td>
|
||||
</tr>
|
||||
</c:if>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%-- 상세 + 점검결과 입력 --%>
|
||||
<c:if test="${not empty report}">
|
||||
<div class="rpt_detail">
|
||||
<h3><c:out value="${report.rptYm}" /> 정기점검 리포트 상세</h3>
|
||||
<div class="inner">
|
||||
<dl>
|
||||
<dt>집계기간</dt>
|
||||
<dd class="mono"><c:out value="${report.periodFrom}" /> ~ <c:out value="${report.periodTo}" /></dd>
|
||||
<dt>총 처리건수</dt>
|
||||
<dd><c:out value="${report.totalLogCnt}" />건 (실패/차단 <c:out value="${report.failCnt}" />건)</dd>
|
||||
<dt>다운로드</dt>
|
||||
<dd><c:out value="${report.downloadCnt}" />건</dd>
|
||||
<dt>무결성 검증</dt>
|
||||
<dd>
|
||||
<c:choose>
|
||||
<c:when test="${report.integrityResult eq 'OK'}">
|
||||
<span class="badge ok">정상</span> 검증대상 <c:out value="${report.integrityTotalCnt}" />건 — 해시체인 연결 이상 없음
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<span class="badge ng">위반</span> 검증대상 <c:out value="${report.integrityTotalCnt}" />건 —
|
||||
최초 불일치 기록번호 <strong class="mono"><c:out value="${report.integrityMismatchId}" /></strong>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</dd>
|
||||
<dt>이상징후</dt>
|
||||
<dd><div class="summary_box"><c:out value="${report.abnormalSummary}" /></div></dd>
|
||||
<dt>취급자별 현황</dt>
|
||||
<dd><div class="summary_box"><c:out value="${report.handlerSummary}" /></div></dd>
|
||||
<dt>리포트 생성</dt>
|
||||
<dd class="mono"><c:out value="${report.regDt}" /></dd>
|
||||
</dl>
|
||||
|
||||
<form name="saveFrm" id="saveFrm" method="post">
|
||||
<input type="hidden" name="rptId" value="<c:out value='${report.rptId}'/>" />
|
||||
<table summary="점검결과 입력" class="list1">
|
||||
<caption>점검결과 입력</caption>
|
||||
<colgroup><col width="15%" /><col width="*" /></colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">점검결과 <span style="color:#c25b5b;">*</span></th>
|
||||
<td>
|
||||
<textarea name="inspectResult" id="inspectResult" rows="3" title="점검결과"><c:out value="${report.inspectResult}" /></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">조치사항</th>
|
||||
<td>
|
||||
<textarea name="actnMatter" id="actnMatter" rows="4" title="조치사항"><c:out value="${report.actnMatter}" /></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">점검자 / 점검일시</th>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${not empty report.inspectDt}">
|
||||
<c:out value="${report.inspectorId}" /> / <span class="mono"><c:out value="${report.inspectDt}" /></span>
|
||||
<span class="nodata">(저장 시 현재 로그인 계정·시각으로 갱신됩니다)</span>
|
||||
</c:when>
|
||||
<c:otherwise><span class="nodata">저장 시 현재 로그인 계정·시각이 자동 기록됩니다.</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="btn_area">
|
||||
<input type="button" class="btn_search" value="점검결과 저장" onclick="fn_saveResult();" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
</div>
|
||||
<!--// 컨텐츠 영역 -->
|
||||
</div>
|
||||
<!--// 오른쪽 영역 -->
|
||||
</div>
|
||||
<!--// 전체 둘러싸기 -->
|
||||
</body>
|
||||
</html>
|
||||
165
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/history.jsp
Normal file
165
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/history.jsp
Normal file
@ -0,0 +1,165 @@
|
||||
<%@ 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="spring" uri="http://www.springframework.org/tags"%>
|
||||
|
||||
<%--
|
||||
암호화 회원정보 관리 — 접근 이력 (사유·재인증 기록)
|
||||
· 개인정보 접속기록(PDAL)이 '언제 어느 화면을 열었나'라면, 이 표는 '왜 열어봤나'를 담는다
|
||||
· 개인정보 본문은 담기지 않는다 — 변경 항목명만 남고 값은 남지 않는다
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<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/mberCrypto/historyView.do' />";
|
||||
document.frm.submit();
|
||||
}
|
||||
|
||||
function fn_back() {
|
||||
location.href = "<c:url value='${PATH}/sec/mberCrypto/listView.do' />?page=9100051";
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:12px; color:#fff; }
|
||||
.badge.s { background:#4a7ebb; }
|
||||
.badge.i { background:#5a9367; }
|
||||
.badge.u { background:#b08a3e; }
|
||||
.badge.d { background:#c25b5b; }
|
||||
.mono { font-family:Consolas,monospace; }
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.btn_area { margin:8px 0; text-align:right; }
|
||||
.nodata { color:#aaa; }
|
||||
.reason { word-break:break-all; text-align:left; }
|
||||
#search_wrap .srow span { padding-left:12px; }
|
||||
#search_wrap .srow > span:first-child { padding-left:0; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<form id="frm" name="frm" method="get" onsubmit="fn_list('1'); return false;">
|
||||
<input type="hidden" name="pageIndex" value="1" />
|
||||
<input type="hidden" name="page" value="9100051" />
|
||||
|
||||
<!-- 검색창 -->
|
||||
<div id="search_wrap">
|
||||
<fieldset class="date_search">
|
||||
<div class="srow">
|
||||
<span>대상 회원ID :</span>
|
||||
<input type="text" name="trgetMberId" class="tbox" title="대상 회원ID"
|
||||
value="<c:out value='${trgetMberId}'/>" />
|
||||
<span>처리자 :</span>
|
||||
<input type="text" name="handlrId" class="tbox" title="처리자 계정ID"
|
||||
value="<c:out value='${handlrId}'/>" />
|
||||
<span>행위 :</span>
|
||||
<select name="actionTy" title="행위">
|
||||
<option value="" <c:if test="${empty actionTy}">selected="selected"</c:if>>전체</option>
|
||||
<option value="S" <c:if test="${actionTy eq 'S'}">selected="selected"</c:if>>복호화조회</option>
|
||||
<option value="I" <c:if test="${actionTy eq 'I'}">selected="selected"</c:if>>등록</option>
|
||||
<option value="U" <c:if test="${actionTy eq 'U'}">selected="selected"</c:if>>수정</option>
|
||||
<option value="D" <c:if test="${actionTy eq 'D'}">selected="selected"</c:if>>개인정보파기</option>
|
||||
</select>
|
||||
<input type="submit" class="btn_search" value="검색" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<!-- //검색창 -->
|
||||
|
||||
<div class="guide">
|
||||
· 관리자 전용 회원정보 화면에서 <strong>평문 개인정보를 열람·변경한 행위</strong>의 소명 근거입니다.<br />
|
||||
· 접속기록(개인정보 접속기록 화면)이 <u>언제 어느 화면을 열었는가</u>를 남긴다면, 이 표는 <u>왜 열어봤는가(사유)</u>를 남깁니다. 둘은 서로를 대조하는 이중 기록입니다.<br />
|
||||
· <strong>개인정보 본문은 기록되지 않습니다</strong> — 무엇을 바꿨는지는 항목명만 남고 값은 남기지 않습니다(이력이 또 하나의 평문 저장소가 되면 안 됩니다).<br />
|
||||
· 이 이력은 <strong>수정·삭제할 수 없습니다</strong>(해당 SQL 구문 자체를 두지 않았습니다).
|
||||
</div>
|
||||
|
||||
<div class="btn_area">
|
||||
총 <strong><c:out value="${totalCount}" /></strong>건
|
||||
<input type="button" class="btn_search" value="회원정보 목록" onclick="fn_back();" />
|
||||
</div>
|
||||
|
||||
<table summary="암호화 회원정보 접근 이력" class="list1">
|
||||
<caption>암호화 회원정보 접근 이력</caption>
|
||||
<colgroup>
|
||||
<col width="15%" /><col width="11%" /><col width="10%" /><col width="11%" />
|
||||
<col width="*" /><col width="14%" /><col width="11%" /><col width="6%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>처리일시</th>
|
||||
<th>처리자</th>
|
||||
<th>행위</th>
|
||||
<th>대상 회원</th>
|
||||
<th>사유</th>
|
||||
<th>변경 항목</th>
|
||||
<th>처리자 IP</th>
|
||||
<th>재인증</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach items="${resultList}" var="h">
|
||||
<tr>
|
||||
<td class="mono"><c:out value="${h.accessDt}" /></td>
|
||||
<td class="mono"><c:out value="${h.handlrId}" /></td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${h.actionTy eq 'S'}"><span class="badge s">복호화조회</span></c:when>
|
||||
<c:when test="${h.actionTy eq 'I'}"><span class="badge i">등록</span></c:when>
|
||||
<c:when test="${h.actionTy eq 'U'}"><span class="badge u">수정</span></c:when>
|
||||
<c:when test="${h.actionTy eq 'D'}"><span class="badge d">파기</span></c:when>
|
||||
<c:otherwise><c:out value="${h.actionTy}" /></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td class="mono">
|
||||
<c:choose>
|
||||
<c:when test="${not empty h.trgetMberId}"><c:out value="${h.trgetMberId}" /></c:when>
|
||||
<c:otherwise><span class="nodata">-</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td class="reason"><c:out value="${h.accessReason}" /></td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${not empty h.changedItems}"><c:out value="${h.changedItems}" /></c:when>
|
||||
<c:otherwise><span class="nodata">-</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td class="mono"><c:out value="${h.handlrIp}" /></td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${h.reauthYn eq 'Y'}">통과</c:when>
|
||||
<c:otherwise><span class="nodata">-</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
<c:if test="${fn:length(resultList) == 0}">
|
||||
<tr>
|
||||
<td colspan="8"><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>
|
||||
|
||||
</div>
|
||||
<!--// 컨텐츠 영역 -->
|
||||
</div>
|
||||
<!--// 오른쪽 영역 -->
|
||||
</div>
|
||||
<!--// 전체 둘러싸기 -->
|
||||
</body>
|
||||
</html>
|
||||
209
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/list.jsp
Normal file
209
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/list.jsp
Normal file
@ -0,0 +1,209 @@
|
||||
<%@ 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="form" uri="http://www.springframework.org/tags/form"%>
|
||||
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
|
||||
|
||||
<%--
|
||||
암호화 회원정보 관리 — 목록 (개인정보보호 추가제안 Action4 / 제안서 슬라이드16)
|
||||
· 목록은 항상 마스킹이다. 평문은 대상을 특정해 사유를 남긴 '상세 한 건'에서만 보인다
|
||||
(목록에서 평문이 뜨면 사유를 받는 의미가 없다 — 대량 노출은 목록에서 일어난다)
|
||||
· 이름 검색은 완전일치만 된다. 랜덤 IV AES-GCM 이라 부분검색이 원리적으로 불가능하다
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<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/mberCrypto/listView.do' />";
|
||||
document.frm.submit();
|
||||
}
|
||||
|
||||
/* 상세(평문) 보기 — 사유 입력 필수. 사유는 접근 이력에 그대로 기록된다.
|
||||
재인증이 만료돼 있으면 서버가 재인증 화면으로 보냈다가 이 위치로 되돌린다. */
|
||||
function fn_view(mberId) {
|
||||
var reason = prompt("개인정보 조회 사유를 입력하세요.\n(필수 — 접근 이력에 기록됩니다)", "");
|
||||
if (reason == null) {
|
||||
return; /* 취소 */
|
||||
}
|
||||
if (reason.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("조회 사유는 필수입니다.");
|
||||
return;
|
||||
}
|
||||
location.href = "<c:url value='${PATH}/sec/mberCrypto/view.do' />?page=9100051"
|
||||
+ "&mberId=" + encodeURIComponent(mberId)
|
||||
+ "&reason=" + encodeURIComponent(reason);
|
||||
}
|
||||
|
||||
function fn_regist() {
|
||||
location.href = "<c:url value='${PATH}/sec/mberCrypto/registView.do' />?page=9100051";
|
||||
}
|
||||
|
||||
function fn_history() {
|
||||
location.href = "<c:url value='${PATH}/sec/mberCrypto/historyView.do' />?page=9100051";
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:12px; color:#fff; }
|
||||
.badge.p { background:#4a7ebb; }
|
||||
.badge.d { background:#999; }
|
||||
.badge.enc { background:#5a9367; }
|
||||
.badge.plain { background:#c25b5b; }
|
||||
.mono { font-family:Consolas,monospace; }
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.msg { margin:6px 0 10px; padding:8px 10px; background:#fff8e6; border:1px solid #e6d5a8; font-size:13px; color:#7a5b00; }
|
||||
.btn_area { margin:8px 0; text-align:right; }
|
||||
.nodata { color:#aaa; }
|
||||
.reauth { float:left; font-size:12px; color:#556; padding-top:4px; }
|
||||
.reauth strong { color:#4a7ebb; }
|
||||
#search_wrap .srow { white-space:nowrap; }
|
||||
#search_wrap .srow + .srow { margin-top:4px; }
|
||||
#search_wrap .srow input.tbox { width:120px; }
|
||||
#search_wrap .srow span { padding-left:12px; }
|
||||
#search_wrap .srow > span:first-child { padding-left:0; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<c:if test="${not empty resultMsg}">
|
||||
<div class="msg"><c:out value="${resultMsg}" /></div>
|
||||
</c:if>
|
||||
|
||||
<form:form commandName="mberCryptoSearchVO" onsubmit="fn_list('1'); return false;" id="frm" name="frm" method="get" >
|
||||
<form:hidden path="pageIndex" />
|
||||
<input type="hidden" name="page" value="9100051" />
|
||||
|
||||
<!-- 검색창 -->
|
||||
<div id="search_wrap">
|
||||
<fieldset class="date_search">
|
||||
<div class="srow">
|
||||
<span>회원ID :</span>
|
||||
<form:input path="mberId" title="회원ID" cssClass="tbox" />
|
||||
<span>이름(완전일치) :</span>
|
||||
<form:input path="mberNm" title="이름 완전일치" cssClass="tbox" />
|
||||
<span>상태 :</span>
|
||||
<select name="mberSttus" id="mberSttus" title="회원상태">
|
||||
<option value="" <c:if test="${empty mberCryptoSearchVO.mberSttus}">selected="selected"</c:if>>전체</option>
|
||||
<option value="P" <c:if test="${mberCryptoSearchVO.mberSttus eq 'P'}">selected="selected"</c:if>>정상</option>
|
||||
<option value="D" <c:if test="${mberCryptoSearchVO.mberSttus eq 'D'}">selected="selected"</c:if>>탈퇴</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="srow">
|
||||
<span>이메일 :</span>
|
||||
<form:input path="mberEmailAdres" title="이메일" cssClass="tbox" />
|
||||
<span>휴대폰 :</span>
|
||||
<form:input path="moblphonNo" title="휴대폰" cssClass="tbox" />
|
||||
<input type="submit" class="btn_search" value="검색" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<!-- //검색창 -->
|
||||
|
||||
<div class="guide">
|
||||
· 이 목록의 이름·휴대폰·이메일은 <strong>항상 마스킹</strong>되어 표시됩니다. 평문은 <strong>사유를 입력하고 상세를 연 한 건</strong>에서만 보입니다.<br />
|
||||
· <strong>이름 검색은 완전일치만 가능합니다.</strong> 이름은 암호화(AES-GCM, 랜덤 IV) 저장되어 같은 이름도 저장된 값이 매번 달라 부분검색(예: '홍')이 원리적으로 불가능합니다 — 검색 전용 해시로 <u>정확히 같은 이름</u>만 찾습니다.<br />
|
||||
· 조회·수정·파기에는 <strong>사유 입력과 재인증(본인 비밀번호 재확인, 10분 유효)</strong>이 필요하며, 처리자·일시·IP·사유가 접근 이력에 기록됩니다.<br />
|
||||
· 이 화면의 모든 요청은 <strong>개인정보 접속기록</strong>에도 자동으로 남습니다.
|
||||
</div>
|
||||
|
||||
<div class="btn_area">
|
||||
<span class="reauth">
|
||||
<c:choose>
|
||||
<c:when test="${reauthRemain > 0}">재인증 유효 — 남은 시간 <strong><c:out value="${reauthRemain}" />초</strong></c:when>
|
||||
<c:otherwise>재인증 필요 <span class="nodata">(상세·등록·수정·파기 시 본인 확인을 요청합니다)</span></c:otherwise>
|
||||
</c:choose>
|
||||
</span>
|
||||
총 <strong><c:out value="${mberCryptoSearchVO.totalRecordCount}" /></strong>건
|
||||
<input type="button" class="btn_search" value="신규 등록" onclick="fn_regist();" />
|
||||
<input type="button" class="btn_search" value="접근 이력" onclick="fn_history();" />
|
||||
</div>
|
||||
|
||||
<table summary="암호화 회원정보 목록" class="list1">
|
||||
<caption>암호화 회원정보 목록</caption>
|
||||
<colgroup>
|
||||
<col width="5%" /><col width="14%" /><col width="10%" /><col width="18%" />
|
||||
<col width="13%" /><col width="*" /><col width="8%" /><col width="10%" /><col width="8%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>NO.</th>
|
||||
<th>회원ID</th>
|
||||
<th>이름</th>
|
||||
<th>이메일</th>
|
||||
<th>휴대폰</th>
|
||||
<th>주소</th>
|
||||
<th>상태</th>
|
||||
<th>가입일</th>
|
||||
<th>상세</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach items="${resultList}" var="result" varStatus="status">
|
||||
<tr>
|
||||
<td><c:out value="${mberCryptoSearchVO.totalRecordCount - (status.index + (mberCryptoSearchVO.pageIndex-1)*mberCryptoSearchVO.recordCountPerPage)}" /></td>
|
||||
<td class="mono"><c:out value="${result.mberId}" /></td>
|
||||
<td>
|
||||
<c:out value="${result.mberNm}" />
|
||||
<%-- 암호화 여부 표시 — 이관 후 남은 미암호화 행을 눈으로 찾을 수 있게 한다 --%>
|
||||
<c:choose>
|
||||
<c:when test="${not empty result.cryptoKeyVer}"><span class="badge enc" title="암호화 저장(키 버전 ${result.cryptoKeyVer})">암호화</span></c:when>
|
||||
<c:otherwise><span class="badge plain" title="암호문이 없는 행 — 백필 대상이거나 파기된 회원입니다">미암호화</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td><c:out value="${result.mberEmailAdres}" /></td>
|
||||
<td class="mono"><c:out value="${result.moblphonNo}" /></td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${not empty result.adres}"><c:out value="${result.adres}" /></c:when>
|
||||
<c:otherwise><span class="nodata">-</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${result.mberSttus eq 'P'}"><span class="badge p">정상</span></c:when>
|
||||
<c:when test="${result.mberSttus eq 'D'}"><span class="badge d">탈퇴</span></c:when>
|
||||
<c:otherwise><c:out value="${result.mberSttus}" /></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td><c:out value="${fn:substring(result.sbscrbDe, 0, 10)}" /></td>
|
||||
<td>
|
||||
<a href="#none" onclick="fn_view('<c:out value="${result.mberId}"/>'); return false;">사유입력·조회</a>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
<c:if test="${fn:length(resultList) == 0}">
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<spring:message code="common.nodata.msg" />
|
||||
<c:if test="${not empty mberCryptoSearchVO.mberNm}">
|
||||
<br /><span class="nodata">이름은 <strong>완전일치</strong>로만 검색됩니다. 성만 입력하거나 일부만 입력하면 찾을 수 없습니다.</span>
|
||||
</c:if>
|
||||
</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>
|
||||
@ -0,0 +1,82 @@
|
||||
<%@ 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 --%>
|
||||
|
||||
<%--
|
||||
암호화 회원정보 관리 — 재인증(본인 비밀번호 재확인)
|
||||
· 제안서 슬라이드16 "조회·수정·삭제 시 사유 입력 및 재인증 연계" 요건
|
||||
· 로그인 2차 인증을 재사용하지 않는 이유는 MberCryptoReauth Javadoc 참조
|
||||
(2차 인증은 로그인 시 한 번이라 '다시 확인'이 되지 못하고, 미등록 취급자는 영영 못 들어온다)
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
function fn_list() {
|
||||
location.href = "<c:url value='${PATH}/sec/mberCrypto/listView.do' />?page=9100051";
|
||||
}
|
||||
|
||||
function fn_confirm() {
|
||||
var f = document.reauthFrm;
|
||||
if (f.password.value == "") {
|
||||
alert("비밀번호를 입력하세요.");
|
||||
f.password.focus();
|
||||
return;
|
||||
}
|
||||
f.method = "post";
|
||||
f.action = "<c:url value='${PATH}/sec/mberCrypto/reauth.do' />";
|
||||
f.submit();
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.msg { margin:6px 0 10px; padding:8px 10px; background:#fdf3f3; border:1px solid #e6bcbc; font-size:13px; color:#8a3b3b; }
|
||||
.btn_area { margin:10px 0; text-align:center; }
|
||||
.reauth_box { max-width:520px; margin:20px auto; border:1px solid #dde3ea; }
|
||||
.reauth_box h3 { margin:0; padding:10px 12px; background:#eef2f7; font-size:14px; color:#334; border-bottom:1px solid #dde3ea; }
|
||||
.reauth_box .inner { padding:16px 18px; }
|
||||
.reauth_box label { display:block; margin-bottom:6px; font-size:13px; color:#445; }
|
||||
.reauth_box input.tbox { width:100%; box-sizing:border-box; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<div class="reauth_box">
|
||||
<h3>본인 확인</h3>
|
||||
<div class="inner">
|
||||
<c:if test="${not empty resultMsg}">
|
||||
<div class="msg"><c:out value="${resultMsg}" /></div>
|
||||
</c:if>
|
||||
|
||||
<div class="guide">
|
||||
개인정보 평문을 다루는 화면입니다. <strong>본인 비밀번호</strong>를 한 번 더 입력해 주세요.<br />
|
||||
확인 후 <strong>10분 동안</strong> 유효하며, 시간이 지나면 다시 확인합니다.
|
||||
</div>
|
||||
|
||||
<form name="reauthFrm" id="reauthFrm" method="post" onsubmit="fn_confirm(); return false;">
|
||||
<input type="hidden" name="page" value="9100051" />
|
||||
<input type="hidden" name="returnUrl" value="<c:out value='${returnUrl}'/>" />
|
||||
<label for="password">비밀번호</label>
|
||||
<input type="password" name="password" id="password" class="tbox" maxlength="30"
|
||||
title="비밀번호" autocomplete="current-password" />
|
||||
<div class="btn_area">
|
||||
<input type="button" class="btn_search" value="목록으로" onclick="fn_list();" />
|
||||
<input type="button" class="btn_search" value="확인" onclick="fn_confirm();" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--// 컨텐츠 영역 -->
|
||||
</div>
|
||||
<!--// 오른쪽 영역 -->
|
||||
</div>
|
||||
<!--// 전체 둘러싸기 -->
|
||||
</body>
|
||||
</html>
|
||||
141
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/regist.jsp
Normal file
141
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/regist.jsp
Normal file
@ -0,0 +1,141 @@
|
||||
<%@ 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 --%>
|
||||
|
||||
<%--
|
||||
암호화 회원정보 관리 — 신규 등록
|
||||
· 이름은 입력 즉시 암호화되어 저장된다(평문 컬럼에는 '(암호화)' 표식만 들어간다)
|
||||
· 초기 비밀번호는 솔트 해시로 저장된다
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
function fn_list() {
|
||||
location.href = "<c:url value='${PATH}/sec/mberCrypto/listView.do' />?page=9100051";
|
||||
}
|
||||
|
||||
function fn_regist() {
|
||||
var f = document.regFrm;
|
||||
if (f.mberId.value.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("회원ID는 필수입니다.");
|
||||
f.mberId.focus();
|
||||
return;
|
||||
}
|
||||
if (f.mberNm.value.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("이름은 필수입니다.");
|
||||
f.mberNm.focus();
|
||||
return;
|
||||
}
|
||||
if (f.password.value.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("초기 비밀번호는 필수입니다.");
|
||||
f.password.focus();
|
||||
return;
|
||||
}
|
||||
var reason = prompt("등록 사유를 입력하세요.\n(필수 — 접근 이력에 기록됩니다)", "");
|
||||
if (reason == null) {
|
||||
return;
|
||||
}
|
||||
if (reason.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("등록 사유는 필수입니다.");
|
||||
return;
|
||||
}
|
||||
f.reason.value = reason;
|
||||
f.method = "post";
|
||||
f.action = "<c:url value='${PATH}/sec/mberCrypto/regist.do' />";
|
||||
f.submit();
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.msg { margin:6px 0 10px; padding:8px 10px; background:#fff8e6; border:1px solid #e6d5a8; font-size:13px; color:#7a5b00; }
|
||||
.btn_area { margin:10px 0; text-align:right; }
|
||||
.req { color:#c25b5b; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<c:if test="${not empty resultMsg}">
|
||||
<div class="msg"><c:out value="${resultMsg}" /></div>
|
||||
</c:if>
|
||||
|
||||
<div class="guide">
|
||||
· 이름은 <strong>저장 시점에 암호화</strong>되며 평문으로는 보관되지 않습니다(검색용 해시가 함께 만들어집니다).<br />
|
||||
· <strong>초기 비밀번호</strong>는 솔트를 붙인 해시로 저장됩니다. 등록 후 회원에게 변경을 안내하세요.<br />
|
||||
· 이 화면은 개인정보 항목만 다룹니다. 활동지역·재능 등 부가 정보는 기존 회원관리 화면에서 입력합니다.<br />
|
||||
· 등록에도 <strong>사유 입력</strong>이 필요하며 처리자·일시·IP·사유가 접근 이력에 기록됩니다.
|
||||
<c:if test="${reauthRemain > 0}"><br />· 재인증 남은 시간 <strong><c:out value="${reauthRemain}" />초</strong></c:if>
|
||||
</div>
|
||||
|
||||
<form name="regFrm" id="regFrm" method="post">
|
||||
<input type="hidden" name="page" value="9100051" />
|
||||
<input type="hidden" name="reason" value="" />
|
||||
|
||||
<table summary="회원 신규 등록" class="list1">
|
||||
<caption>회원 신규 등록</caption>
|
||||
<colgroup><col width="16%" /><col width="34%" /><col width="16%" /><col width="*" /></colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">회원ID <span class="req">*</span></th>
|
||||
<td><input type="text" name="mberId" id="mberId" class="tbox" maxlength="20" title="회원ID" /></td>
|
||||
<th scope="row">초기 비밀번호 <span class="req">*</span></th>
|
||||
<td><input type="password" name="password" id="password" class="tbox" maxlength="30" title="초기 비밀번호" autocomplete="new-password" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">이름 <span class="req">*</span></th>
|
||||
<td><input type="text" name="mberNm" id="mberNm" class="tbox" maxlength="50" title="이름" /></td>
|
||||
<th scope="row">성별</th>
|
||||
<td>
|
||||
<select name="sexdstnCode" title="성별">
|
||||
<option value="">선택안함</option>
|
||||
<option value="M">남</option>
|
||||
<option value="F">여</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">이메일</th>
|
||||
<td><input type="text" name="mberEmailAdres" class="tbox" maxlength="50" title="이메일" /></td>
|
||||
<th scope="row">휴대폰</th>
|
||||
<td><input type="text" name="moblphonNo" class="tbox" maxlength="20" title="휴대폰" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">우편번호</th>
|
||||
<td><input type="text" name="zip" class="tbox" maxlength="6" title="우편번호" /></td>
|
||||
<th scope="row">회원상태</th>
|
||||
<td>
|
||||
<select name="mberSttus" title="회원상태">
|
||||
<option value="P" selected="selected">정상</option>
|
||||
<option value="D">탈퇴</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">주소</th>
|
||||
<td colspan="3">
|
||||
<input type="text" name="adres" class="tbox" style="width:45%;" maxlength="100" title="주소" />
|
||||
<input type="text" name="detailAdres" class="tbox" style="width:45%;" maxlength="100" title="상세주소" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="btn_area">
|
||||
<input type="button" class="btn_search" value="목록" onclick="fn_list();" />
|
||||
<input type="button" class="btn_search" value="등록(사유 입력)" onclick="fn_regist();" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<!--// 컨텐츠 영역 -->
|
||||
</div>
|
||||
<!--// 오른쪽 영역 -->
|
||||
</div>
|
||||
<!--// 전체 둘러싸기 -->
|
||||
</body>
|
||||
</html>
|
||||
222
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/view.jsp
Normal file
222
src/main/webapp/WEB-INF/jsp/site/mnt/sec/mberCrypto/view.jsp
Normal file
@ -0,0 +1,222 @@
|
||||
<%@ 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"%>
|
||||
|
||||
<%--
|
||||
암호화 회원정보 관리 — 상세(평문) / 수정 / 파기
|
||||
· 이 화면에 도달했다는 것은 사유 입력과 재인증을 모두 통과했다는 뜻이다(컨트롤러가 판정)
|
||||
· 화면을 연 사실 자체가 이미 접근 이력에 기록되어 있다(복호화조회 S)
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
function fn_list() {
|
||||
location.href = "<c:url value='${PATH}/sec/mberCrypto/listView.do' />?page=9100051";
|
||||
}
|
||||
|
||||
/* 수정 — 사유를 다시 받는다. 조회 사유와 변경 사유는 다를 수 있고, 이력에도 각각 남는다. */
|
||||
function fn_update() {
|
||||
var f = document.updFrm;
|
||||
if (f.mberNm.value.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("이름은 필수입니다.");
|
||||
f.mberNm.focus();
|
||||
return;
|
||||
}
|
||||
var reason = prompt("수정 사유를 입력하세요.\n(필수 — 접근 이력에 기록됩니다)", "");
|
||||
if (reason == null) {
|
||||
return;
|
||||
}
|
||||
if (reason.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("수정 사유는 필수입니다.");
|
||||
return;
|
||||
}
|
||||
f.reason.value = reason;
|
||||
f.method = "post";
|
||||
f.action = "<c:url value='${PATH}/sec/mberCrypto/update.do' />";
|
||||
f.submit();
|
||||
}
|
||||
|
||||
/* 개인정보 파기 — 되돌릴 수 없다.
|
||||
회원ID를 직접 입력하게 해 오조작을 막고, 서버에서도 같은 값인지 다시 확인한다. */
|
||||
function fn_deletePii() {
|
||||
var target = document.getElementById("mberIdRef").value;
|
||||
var typed = prompt("이 회원의 개인정보를 파기합니다.\n\n"
|
||||
+ "· 이름·이메일·휴대폰·주소·성별이 삭제되고 회원상태가 '탈퇴'로 바뀝니다.\n"
|
||||
+ "· 되돌릴 수 없습니다(암호문·해시까지 함께 지워집니다).\n"
|
||||
+ "· 활동·신청 기록은 남습니다(식별정보만 지웁니다).\n\n"
|
||||
+ "확인을 위해 회원ID를 그대로 입력하세요: " + target, "");
|
||||
if (typed == null) {
|
||||
return;
|
||||
}
|
||||
if (typed.replace(/^\s+|\s+$/g, "") != target) {
|
||||
alert("회원ID가 일치하지 않습니다. 파기를 취소합니다.");
|
||||
return;
|
||||
}
|
||||
var reason = prompt("파기 사유를 입력하세요.\n(필수 — 접근 이력에 기록됩니다)", "");
|
||||
if (reason == null) {
|
||||
return;
|
||||
}
|
||||
if (reason.replace(/^\s+|\s+$/g, "") == "") {
|
||||
alert("파기 사유는 필수입니다.");
|
||||
return;
|
||||
}
|
||||
var f = document.delFrm;
|
||||
f.confirmId.value = typed.replace(/^\s+|\s+$/g, "");
|
||||
f.reason.value = reason;
|
||||
f.method = "post";
|
||||
f.action = "<c:url value='${PATH}/sec/mberCrypto/deletePii.do' />";
|
||||
f.submit();
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:12px; color:#fff; }
|
||||
.badge.p { background:#4a7ebb; }
|
||||
.badge.d { background:#999; }
|
||||
.badge.enc { background:#5a9367; }
|
||||
.mono { font-family:Consolas,monospace; }
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.warn { margin:6px 0 10px; padding:8px 10px; background:#fdf3f3; border:1px solid #e6bcbc; font-size:12px; color:#8a3b3b; }
|
||||
.msg { margin:6px 0 10px; padding:8px 10px; background:#fff8e6; border:1px solid #e6d5a8; font-size:13px; color:#7a5b00; }
|
||||
.btn_area { margin:10px 0; text-align:right; }
|
||||
.btn_del { padding:5px 14px; border:1px solid #c25b5b; background:#c25b5b; color:#fff; cursor:pointer; font-size:12px; }
|
||||
.reason_box { margin:6px 0 12px; padding:8px 10px; background:#eef2f7; border:1px solid #dde3ea; font-size:12px; color:#334; }
|
||||
.nodata { color:#aaa; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<c:if test="${not empty resultMsg}">
|
||||
<div class="msg"><c:out value="${resultMsg}" /></div>
|
||||
</c:if>
|
||||
|
||||
<div class="reason_box">
|
||||
<strong>조회 사유</strong> : <c:out value="${reason}" />
|
||||
| 이 조회는 접근 이력에 기록되었습니다.
|
||||
<c:if test="${reauthRemain > 0}"> | 재인증 남은 시간 <strong><c:out value="${reauthRemain}" />초</strong></c:if>
|
||||
</div>
|
||||
|
||||
<div class="guide">
|
||||
· 아래 값은 <strong>복호화된 평문</strong>입니다. 화면을 열어 둔 채 자리를 비우지 마세요.<br />
|
||||
· 수정 시 이름은 <strong>다시 암호화</strong>되고 검색용 해시도 함께 갱신됩니다(평문 이름은 저장되지 않습니다).<br />
|
||||
· 비밀번호·가입일·내부 식별자는 이 화면에서 변경하지 않습니다.
|
||||
</div>
|
||||
|
||||
<form name="updFrm" id="updFrm" method="post">
|
||||
<input type="hidden" name="page" value="9100051" />
|
||||
<input type="hidden" name="reason" value="" />
|
||||
<input type="hidden" name="mberId" value="<c:out value='${mber.mberId}'/>" />
|
||||
<input type="hidden" id="mberIdRef" value="<c:out value='${mber.mberId}'/>" />
|
||||
|
||||
<table summary="회원 개인정보 상세" class="list1">
|
||||
<caption>회원 개인정보 상세</caption>
|
||||
<colgroup><col width="16%" /><col width="34%" /><col width="16%" /><col width="*" /></colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">회원ID</th>
|
||||
<td class="mono">
|
||||
<c:out value="${mber.mberId}" />
|
||||
<c:choose>
|
||||
<c:when test="${mber.mberSttus eq 'P'}"><span class="badge p">정상</span></c:when>
|
||||
<c:when test="${mber.mberSttus eq 'D'}"><span class="badge d">탈퇴</span></c:when>
|
||||
</c:choose>
|
||||
</td>
|
||||
<th scope="row">내부 식별자</th>
|
||||
<td class="mono"><c:out value="${mber.uniqId}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">이름 <span style="color:#c25b5b;">*</span></th>
|
||||
<td>
|
||||
<input type="text" name="mberNm" id="mberNm" class="tbox" maxlength="50"
|
||||
value="<c:out value='${mber.mberNm}'/>" title="이름" />
|
||||
<c:if test="${not empty mber.cryptoKeyVer}">
|
||||
<span class="badge enc" title="암호화 저장">암호화 v<c:out value="${mber.cryptoKeyVer}" /></span>
|
||||
</c:if>
|
||||
</td>
|
||||
<th scope="row">성별</th>
|
||||
<td>
|
||||
<select name="sexdstnCode" title="성별">
|
||||
<option value="" <c:if test="${empty mber.sexdstnCode}">selected="selected"</c:if>>선택안함</option>
|
||||
<option value="M" <c:if test="${mber.sexdstnCode eq 'M'}">selected="selected"</c:if>>남</option>
|
||||
<option value="F" <c:if test="${mber.sexdstnCode eq 'F'}">selected="selected"</c:if>>여</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">이메일</th>
|
||||
<td>
|
||||
<input type="text" name="mberEmailAdres" class="tbox" maxlength="50"
|
||||
value="<c:out value='${mber.mberEmailAdres}'/>" title="이메일" />
|
||||
</td>
|
||||
<th scope="row">휴대폰</th>
|
||||
<td>
|
||||
<input type="text" name="moblphonNo" class="tbox" maxlength="20"
|
||||
value="<c:out value='${mber.moblphonNo}'/>" title="휴대폰" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">우편번호</th>
|
||||
<td>
|
||||
<input type="text" name="zip" class="tbox" maxlength="6"
|
||||
value="<c:out value='${mber.zip}'/>" title="우편번호" />
|
||||
</td>
|
||||
<th scope="row">회원상태</th>
|
||||
<td>
|
||||
<select name="mberSttus" title="회원상태">
|
||||
<option value="P" <c:if test="${mber.mberSttus eq 'P'}">selected="selected"</c:if>>정상</option>
|
||||
<option value="D" <c:if test="${mber.mberSttus eq 'D'}">selected="selected"</c:if>>탈퇴</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">주소</th>
|
||||
<td colspan="3">
|
||||
<input type="text" name="adres" class="tbox" style="width:45%;" maxlength="100"
|
||||
value="<c:out value='${mber.adres}'/>" title="주소" />
|
||||
<input type="text" name="detailAdres" class="tbox" style="width:45%;" maxlength="100"
|
||||
value="<c:out value='${mber.detailAdres}'/>" title="상세주소" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">가입일</th>
|
||||
<td colspan="3"><c:out value="${mber.sbscrbDe}" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="btn_area">
|
||||
<input type="button" class="btn_search" value="목록" onclick="fn_list();" />
|
||||
<input type="button" class="btn_search" value="수정(사유 입력)" onclick="fn_update();" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="warn">
|
||||
<strong>개인정보 파기</strong> — 이름·이메일·휴대폰·주소·성별을 삭제하고 회원상태를 '탈퇴'로 바꿉니다.
|
||||
암호문과 검색용 해시도 함께 지워지며 <strong>되돌릴 수 없습니다</strong>.
|
||||
회원 행 자체는 남깁니다(신청·활동 기록이 참조하고 있어 행을 지우면 그 기록들이 주인 없는 값을 가리키게 됩니다).
|
||||
</div>
|
||||
|
||||
<form name="delFrm" id="delFrm" method="post">
|
||||
<input type="hidden" name="page" value="9100051" />
|
||||
<input type="hidden" name="mberId" value="<c:out value='${mber.mberId}'/>" />
|
||||
<input type="hidden" name="confirmId" value="" />
|
||||
<input type="hidden" name="reason" value="" />
|
||||
<div class="btn_area">
|
||||
<input type="button" class="btn_del" value="개인정보 파기" onclick="fn_deletePii();" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<!--// 컨텐츠 영역 -->
|
||||
</div>
|
||||
<!--// 오른쪽 영역 -->
|
||||
</div>
|
||||
<!--// 전체 둘러싸기 -->
|
||||
</body>
|
||||
</html>
|
||||
151
src/main/webapp/WEB-INF/jsp/site/mnt/sec/purgeHistory/list.jsp
Normal file
151
src/main/webapp/WEB-INF/jsp/site/mnt/sec/purgeHistory/list.jsp
Normal file
@ -0,0 +1,151 @@
|
||||
<%@ 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="form" uri="http://www.springframework.org/tags/form"%>
|
||||
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
|
||||
|
||||
<%--
|
||||
개인정보 접속기록 파기이력 (개인정보보호 SFR-004 / DBR-001 / SER-002-2)
|
||||
· 보관기간(730일) 경과 파티션 DROP(파기) + 미래 월 파티션 ADD(선행증설) 내역
|
||||
· 조회 전용 — 이력을 화면에서 고치거나 지울 수 있으면 증빙 가치가 사라지므로
|
||||
등록·수정·삭제 엔드포인트를 두지 않는다(컨트롤러에도 없다)
|
||||
--%>
|
||||
<c:set var="PATH" value="/mnt" />
|
||||
<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/purgeHistory/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.drop { background:#c25b5b; }
|
||||
.badge.add { background:#4a7ebb; }
|
||||
.mono { font-family:Consolas,monospace; }
|
||||
.guide { margin:6px 0 10px; padding:8px 10px; background:#f5f7fa; border:1px solid #dde3ea; font-size:12px; color:#555; }
|
||||
.btn_area { margin:8px 0; text-align:right; }
|
||||
.nodata { color:#aaa; }
|
||||
#search_wrap .srow { white-space:nowrap; }
|
||||
#search_wrap .srow input.tbox.dt { width:140px; }
|
||||
#search_wrap .srow select { max-width:170px; }
|
||||
#search_wrap .srow span { padding-left:12px; }
|
||||
#search_wrap .srow > span:first-child { padding-left:0; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 타이틀 -->
|
||||
<c:import url = "/mnt/include/navigation.do" />
|
||||
<!-- //타이틀 -->
|
||||
<!-- 컨텐츠 영역 -->
|
||||
|
||||
<form:form commandName="accessLogPurgeHistorySearchVO" onsubmit="fn_list('1'); return false;" id="frm" name="frm" method="get" >
|
||||
<form:hidden path="pageIndex" />
|
||||
|
||||
<!-- 검색창 -->
|
||||
<div id="search_wrap">
|
||||
<fieldset class="date_search">
|
||||
<div class="srow">
|
||||
<span>유형 :</span>
|
||||
<select name="purgeType" id="purgeType" title="유형">
|
||||
<option value="" <c:if test="${empty accessLogPurgeHistorySearchVO.purgeType}">selected="selected"</c:if>>전체</option>
|
||||
<option value="DROP" <c:if test="${accessLogPurgeHistorySearchVO.purgeType eq 'DROP'}">selected="selected"</c:if>>파기(DROP)</option>
|
||||
<option value="ADD" <c:if test="${accessLogPurgeHistorySearchVO.purgeType eq 'ADD'}">selected="selected"</c:if>>선행증설(ADD)</option>
|
||||
</select>
|
||||
<span>수행일시 :</span>
|
||||
<form:input path="fromDt" title="시작일시" cssClass="tbox dt" placeholder="yyyy-MM-dd" />
|
||||
~
|
||||
<form:input path="toDt" title="종료일시" cssClass="tbox dt" placeholder="yyyy-MM-dd" />
|
||||
<input type="submit" class="btn_search" value="검색" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<!-- //검색창 -->
|
||||
|
||||
<div class="guide">
|
||||
· 개인정보 접속기록(<strong>PERSONAL_DATA_ACCESS_LOG</strong>)의 <strong>파기·증설 이력</strong>입니다. 기간은 <strong>yyyy-MM-dd</strong> 또는 <strong>yyyy-MM-dd HH:mm:ss</strong> 형식으로 입력하세요(날짜만 입력 시 종료일 당일 포함).<br />
|
||||
· 접속기록의 삭제 경로는 <strong>월 단위 파티션 삭제(DROP)</strong> 하나뿐입니다. 개별 레코드 DELETE 는 사용하지 않으며, 접속기록 테이블에는 UPDATE·DELETE 구문 자체가 정의되어 있지 않습니다.<br />
|
||||
· 따라서 이 목록은 <strong>“보관기간(730일) 경과분을 규정대로 파기했고, 그 외 경로로는 손대지 않았다”</strong>는 사실을 함께 입증하는 증빙 자료입니다(안전성 확보조치 기준 제8조).<br />
|
||||
· <strong>선행증설(ADD)</strong>은 앞으로 쌓일 기록을 담을 월 파티션을 미리 만들어 두는 작업입니다 — 파기가 아니라 <u>수집이 끊기지 않게 하는</u> 조치입니다.<br />
|
||||
· 이 화면은 <strong>조회 전용</strong>입니다. 이력의 수정·삭제 기능은 제공하지 않습니다.
|
||||
</div>
|
||||
|
||||
<div class="btn_area">
|
||||
총 <strong><c:out value="${accessLogPurgeHistorySearchVO.totalRecordCount}" /></strong>건
|
||||
</div>
|
||||
|
||||
<table summary="개인정보 접속기록 파기·증설 이력 목록" class="list1">
|
||||
<caption>개인정보 접속기록 파기·증설 이력 목록</caption>
|
||||
<colgroup>
|
||||
<col width="6%" />
|
||||
<col width="17%" />
|
||||
<col width="14%" />
|
||||
<col width="14%" />
|
||||
<col width="12%" />
|
||||
<col width="*" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>NO.</th>
|
||||
<th>수행일시</th>
|
||||
<th>대상 파티션</th>
|
||||
<th>파티션 경계</th>
|
||||
<th>유형</th>
|
||||
<th>비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach items="${resultList}" var="result" varStatus="status">
|
||||
<tr>
|
||||
<td><c:out value="${accessLogPurgeHistorySearchVO.totalRecordCount - (status.index + (accessLogPurgeHistorySearchVO.pageIndex-1)*accessLogPurgeHistorySearchVO.recordCountPerPage)}" /></td>
|
||||
<td><c:out value="${result.purgeDt}" /></td>
|
||||
<td class="mono"><c:out value="${result.partitionName}" /></td>
|
||||
<td class="mono">
|
||||
<c:choose>
|
||||
<c:when test="${not empty result.boundaryDt}"><c:out value="${result.boundaryDt}" /></c:when>
|
||||
<c:otherwise><span class="nodata">-</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${result.purgeType eq 'DROP'}"><span class="badge drop">파기</span></c:when>
|
||||
<c:when test="${result.purgeType eq 'ADD'}"><span class="badge add">선행증설</span></c:when>
|
||||
<c:otherwise><c:out value="${result.purgeType}" /></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${not empty result.note}"><c:out value="${result.note}" /></c:when>
|
||||
<c:otherwise><span class="nodata">-</span></c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
<c:if test="${fn:length(resultList) == 0}">
|
||||
<tr>
|
||||
<td colspan="6"><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>
|
||||
@ -0,0 +1,55 @@
|
||||
-- =====================================================================
|
||||
-- 암호화 회원정보 관리자 접근 이력 : MBER_CRYPTO_ACCESS_HISTORY
|
||||
-- 문화품앗이 개인정보보호 기능개선 사업 (추가제안 Action4 / 제안서 Ⅲ 슬라이드16)
|
||||
-- 작성일: 2026-08-27 DB: MySQL 8 / MariaDB (스키마 csv)
|
||||
-- ---------------------------------------------------------------------
|
||||
-- [목적] 관리자 전용 회원정보 화면에서 일어난 '복호화 조회·수정·삭제·등록' 행위를
|
||||
-- 사유와 함께 남긴다. 제안서 슬라이드16의 접근통제 요건:
|
||||
-- "관리자 페이지 별도 권한 부여, 조회·수정·삭제 시 사유 입력 및 재인증 연계"
|
||||
--
|
||||
-- [PERSONAL_DATA_ACCESS_LOG(#30)와 무엇이 다른가]
|
||||
-- · 접속기록(PDAL)은 URL 기준으로 '언제 어느 화면을 열었는가'를 자동 수집한다.
|
||||
-- 이 화면도 /mnt/sec/** 라 자동 수집 대상이지만, 거기에는 사유가 없다.
|
||||
-- · 이 표는 '왜 열어봤는가(사유)'와 '재인증을 거쳤는가'를 담는다. 평문 개인정보를
|
||||
-- 실제로 눈으로 본 행위의 소명 근거라 접속기록과 목적이 다르다.
|
||||
-- · 둘 다 남기는 것은 중복이 아니라 이중 기록이다(하나가 비면 다른 하나로 대조).
|
||||
--
|
||||
-- [담지 않는 것] 개인정보 본문은 적재하지 않는다. 대상 회원ID와 '어떤 항목을 바꿨는가'의
|
||||
-- 항목명만 남긴다 — 이력 테이블이 또 하나의 평문 개인정보 저장소가 되면 안 된다.
|
||||
--
|
||||
-- [보관] 접속기록과 같은 2년(730일) 이상 보관을 전제로 한다. 별도 파기 배치는 두지 않고
|
||||
-- 행 수가 적어(관리자 수동 조회 기록) 운영 중 증가량을 관찰한 뒤 결정한다.
|
||||
--
|
||||
-- [재실행 안전] CREATE TABLE IF NOT EXISTS — 반복 실행 무해.
|
||||
-- =====================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS MBER_CRYPTO_ACCESS_HISTORY
|
||||
(
|
||||
HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호',
|
||||
HANDLR_ID VARCHAR(20) NOT NULL COMMENT '처리자(취급자) 계정ID',
|
||||
ACTION_TY CHAR(1) NOT NULL COMMENT '행위 S:복호화조회 I:등록 U:수정 D:개인정보파기',
|
||||
TRGET_MBER_ID VARCHAR(20) NULL COMMENT '대상 회원ID(목록 검색은 NULL)',
|
||||
ACCESS_REASON VARCHAR(500) NOT NULL COMMENT '사유(필수 입력)',
|
||||
CHANGED_ITEMS VARCHAR(500) NULL COMMENT '변경 항목명 목록(값은 미적재). 예: 이름,휴대폰',
|
||||
HANDLR_IP VARCHAR(45) NULL COMMENT '처리자 IP(IPv6 대비 45)',
|
||||
REAUTH_YN CHAR(1) NOT NULL DEFAULT 'N' COMMENT '재인증 통과 여부',
|
||||
ACCESS_DT DATETIME NOT NULL COMMENT '처리 일시',
|
||||
PRIMARY KEY (HIST_ID),
|
||||
KEY IDX_MCAH_01 (ACCESS_DT),
|
||||
KEY IDX_MCAH_02 (HANDLR_ID),
|
||||
KEY IDX_MCAH_03 (TRGET_MBER_ID)
|
||||
)
|
||||
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='암호화 회원정보 관리자 접근 이력(사유·재인증, 추가제안 Action4)';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- [적용 확인]
|
||||
-- ---------------------------------------------------------------------
|
||||
-- SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE FROM information_schema.COLUMNS
|
||||
-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'MBER_CRYPTO_ACCESS_HISTORY'
|
||||
-- ORDER BY ORDINAL_POSITION;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- [롤백]
|
||||
-- ---------------------------------------------------------------------
|
||||
-- DROP TABLE IF EXISTS MBER_CRYPTO_ACCESS_HISTORY;
|
||||
@ -0,0 +1,127 @@
|
||||
-- =====================================================================
|
||||
-- 관리자 메뉴 등록 : 개인정보보호 화면 4종 (MENU_NO 9100048 ~ 9100051)
|
||||
-- 문화품앗이 개인정보보호 기능개선 사업
|
||||
-- 작성일: 2026-08-27 DB: MySQL 8 / MariaDB (스키마 csv)
|
||||
-- ---------------------------------------------------------------------
|
||||
-- [등록 대상]
|
||||
-- 9100048 / ORDR 14 정기점검 리포트 /mnt/sec/inspection/listView.do (SFR-004-7, #48)
|
||||
-- 9100049 / ORDR 15 비정상접근 탐지 /mnt/sec/abnormal/listView.do (과업내용 다-①, #49)
|
||||
-- 9100050 / ORDR 16 접속기록 파기이력 /mnt/sec/purgeHistory/listView.do (SER-002-2 / DBR-001-2)
|
||||
-- 9100051 / ORDR 17 회원정보 암호화관리 /mnt/sec/mberCrypto/listView.do (추가제안 Action4)
|
||||
--
|
||||
-- [배치] 시스템관리(9100000) > 권한관리(9100030) 하위. 직전 항목은
|
||||
-- 접근통제 정책(9100047, ORDR 13)이라 그 뒤 순번을 이어 붙인다.
|
||||
--
|
||||
-- [메뉴번호 근거] 9100044(접속기록)·9100047(접근통제 정책)까지 사용 중이고,
|
||||
-- 9100045·9100046 은 2026-08-11 화면 폐기와 함께 삭제됐다(재사용하지 않는다 —
|
||||
-- 폐기 이력이 있는 번호를 되쓰면 과거 권한매핑 잔재와 섞인다).
|
||||
-- 9100048~9100051 은 레포 전체 미사용을 확인했다(9100044~9100070 구간에 9100070 만 존재).
|
||||
-- 노출 순서는 MENU_NO 가 아니라 MENU_ORDR 을 따른다.
|
||||
--
|
||||
-- ★★ 이 DML만으로는 좌측메뉴에 안 뜬다 — left.jsp 수정 동반 배포 필수 ★★
|
||||
-- src/main/webapp/WEB-INF/jsp/site/mnt/left.jsp 는 메뉴번호 하드코딩 JSP라
|
||||
-- ① 권한관리(9100030) 블록에 9100048~9100051 <c:when> 분기 4개
|
||||
-- ② 시스템관리/권한관리 상위 하이라이트 조건(param.page eq '910004X') 2곳
|
||||
-- 이 함께 배포돼야 렌더된다. (소스에는 이미 반영 완료)
|
||||
--
|
||||
-- [재실행 안전] 전 구간 NOT EXISTS 가드 → 반복 실행 무해.
|
||||
-- [노출권한] 기존 권한관리 메뉴와 동일하게 IS_AUTHENTICATED_FULLY, ROLE_ADMIN 2종.
|
||||
-- 실제 접근 차단은 urlRoleMapping(/mnt/sec/* = ROLE_ADMIN)이 담당한다.
|
||||
-- [한글 리터럴 주의] git-bash + mysql.exe 로 이 파일을 stdin 파이프 실행하면 한글이
|
||||
-- 이중 인코딩으로 손상된다(기존 실측). 적용 후 반드시 HEX(MENU_NM) 로 검증하고,
|
||||
-- 손상 시 CONVERT(UNHEX('<UTF-8 hex>') USING utf8mb4) UPDATE 로 교정할 것.
|
||||
-- [Oracle] 대상 운영 DB가 MySQL(csv)이라 Oracle 쌍 미작성(기존 privacy 메뉴 DML 과 동일).
|
||||
-- =====================================================================
|
||||
|
||||
-- ── [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 'InspectionReportList' AS PROGRM_FILE_NM, '/mnt/sec/inspection/' AS PROGRM_STRE_PATH,
|
||||
'정기점검 리포트' AS PROGRM_KOREAN_NM,
|
||||
'접속기록 정기점검 리포트 조회·점검결과 입력(SFR-004-7)' AS PROGRM_DC,
|
||||
'/mnt/sec/inspection/listView.do?page=9100048' AS URL
|
||||
) seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||
|
||||
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||
SELECT * FROM (
|
||||
SELECT 'AbnormalAccessList' AS PROGRM_FILE_NM, '/mnt/sec/abnormal/' AS PROGRM_STRE_PATH,
|
||||
'비정상접근 탐지' AS PROGRM_KOREAN_NM,
|
||||
'비정상 접근 탐지 5유형 조회(과업내용 다-①)' AS PROGRM_DC,
|
||||
'/mnt/sec/abnormal/listView.do?page=9100049' AS URL
|
||||
) seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||
|
||||
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||
SELECT * FROM (
|
||||
SELECT 'AccessLogPurgeHistory' AS PROGRM_FILE_NM, '/mnt/sec/purgeHistory/' AS PROGRM_STRE_PATH,
|
||||
'접속기록 파기이력' AS PROGRM_KOREAN_NM,
|
||||
'접속기록 파티션 파기·증설 이력 조회(SER-002-2)' AS PROGRM_DC,
|
||||
'/mnt/sec/purgeHistory/listView.do?page=9100050' AS URL
|
||||
) seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||
|
||||
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||
SELECT * FROM (
|
||||
SELECT 'MberCryptoAdmin' AS PROGRM_FILE_NM, '/mnt/sec/mberCrypto/' AS PROGRM_STRE_PATH,
|
||||
'회원정보 암호화 관리' AS PROGRM_KOREAN_NM,
|
||||
'암호화 회원정보 복호화 조회·수정·파기·등록(추가제안 Action4)' AS PROGRM_DC,
|
||||
'/mnt/sec/mberCrypto/listView.do?page=9100051' AS URL
|
||||
) seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||
|
||||
-- ── [2] 메뉴 등록 : 권한관리(9100030) 하위 ──
|
||||
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, 'InspectionReportList' AS PROGRM_FILE_NM, 9100048 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 14 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 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, 'AbnormalAccessList' AS PROGRM_FILE_NM, 9100049 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 15 AS MENU_ORDR, '비정상 접근 탐지 5유형 조회' 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 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, 'AccessLogPurgeHistory' AS PROGRM_FILE_NM, 9100050 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 16 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 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, 'MberCryptoAdmin' AS PROGRM_FILE_NM, 9100051 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 17 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] 권한↔메뉴 노출 매핑 (4메뉴 × 2권한, FK 안전) ──
|
||||
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
|
||||
SELECT n.mno, g.ac
|
||||
FROM (SELECT 9100048 AS mno UNION ALL SELECT 9100049 UNION ALL
|
||||
SELECT 9100050 UNION ALL SELECT 9100051) n
|
||||
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 = n.mno AND x.AUTHOR_CODE = g.ac);
|
||||
|
||||
-- ── [검증] 등록결과 + 한글 손상 여부(HEX 가 EA/EB/EC 대로 시작해야 정상) ──
|
||||
-- SELECT m.MENU_NO, m.UPPER_MENU_NO, m.MENU_ORDR, m.MENU_NM, HEX(m.MENU_NM), 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 9100048 AND 9100051
|
||||
-- ORDER BY m.MENU_ORDR;
|
||||
|
||||
-- ── [롤백] (역순) ──
|
||||
-- DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO BETWEEN 9100048 AND 9100051;
|
||||
-- DELETE FROM COMTNMENUINFO WHERE MENU_NO BETWEEN 9100048 AND 9100051;
|
||||
-- DELETE FROM COMTNPROGRMLIST WHERE PROGRM_FILE_NM IN
|
||||
-- ('InspectionReportList','AbnormalAccessList','AccessLogPurgeHistory','MberCryptoAdmin');
|
||||
@ -1054,6 +1054,33 @@ DROP VIEW IF EXISTS selectboardarticlelist;
|
||||
DROP VIEW IF EXISTS selectmiddleappconfirmlist;
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- [privacy.mber_crypto_access_history_create_mysql.sql] 2026-08-27 추가
|
||||
-- 암호화 회원정보 관리자 접근 이력(추가제안 Action4 / 제안서 슬라이드16)
|
||||
-- · 관리자 전용 회원정보 화면의 복호화 조회·등록·수정·파기 행위를 '사유'와 함께 남긴다.
|
||||
-- · 접속기록(PERSONAL_DATA_ACCESS_LOG)이 '언제 어느 화면을 열었나'라면 이 표는 '왜 열어봤나'다.
|
||||
-- 둘 다 남기는 것은 중복이 아니라 이중 기록이다(하나가 비면 다른 하나로 대조).
|
||||
-- · 개인정보 본문은 적재하지 않는다 — 변경 항목명만 남기고 값은 남기지 않는다.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS MBER_CRYPTO_ACCESS_HISTORY
|
||||
(
|
||||
HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호',
|
||||
HANDLR_ID VARCHAR(20) NOT NULL COMMENT '처리자(취급자) 계정ID',
|
||||
ACTION_TY CHAR(1) NOT NULL COMMENT '행위 S:복호화조회 I:등록 U:수정 D:개인정보파기',
|
||||
TRGET_MBER_ID VARCHAR(20) NULL COMMENT '대상 회원ID(목록 검색은 NULL)',
|
||||
ACCESS_REASON VARCHAR(500) NOT NULL COMMENT '사유(필수 입력)',
|
||||
CHANGED_ITEMS VARCHAR(500) NULL COMMENT '변경 항목명 목록(값은 미적재). 예: 이름,휴대폰',
|
||||
HANDLR_IP VARCHAR(45) NULL COMMENT '처리자 IP(IPv6 대비 45)',
|
||||
REAUTH_YN CHAR(1) NOT NULL DEFAULT 'N' COMMENT '재인증 통과 여부',
|
||||
ACCESS_DT DATETIME NOT NULL COMMENT '처리 일시',
|
||||
PRIMARY KEY (HIST_ID),
|
||||
KEY IDX_MCAH_01 (ACCESS_DT),
|
||||
KEY IDX_MCAH_02 (HANDLR_ID),
|
||||
KEY IDX_MCAH_03 (TRGET_MBER_ID)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='암호화 회원정보 관리자 접근 이력(사유·재인증, 추가제안 Action4)';
|
||||
|
||||
|
||||
-- =====================================================================
|
||||
-- PART 2. DML (기준/시드 데이터) ※ 멱등: NOT EXISTS 가드로 중복 적재 방지
|
||||
-- 수록: ACCOUNT_MNG_POLICY(기본정책 1행)
|
||||
@ -1249,6 +1276,91 @@ SELECT 9100047, g.ac
|
||||
-- ('AuthorRequestList','AuthorHistoryList','DormantStatusList','AccountPolicySet');
|
||||
|
||||
|
||||
-- ── [7] 개인정보보호 화면 4종 메뉴(9100048~9100051) — 2026-08-27 추가 ──
|
||||
-- 9100048 / ORDR 14 정기점검 리포트 /mnt/sec/inspection/listView.do (SFR-004-7)
|
||||
-- 9100049 / ORDR 15 비정상접근 탐지 /mnt/sec/abnormal/listView.do (과업내용 다-①)
|
||||
-- 9100050 / ORDR 16 접속기록 파기이력 /mnt/sec/purgeHistory/listView.do (SER-002-2)
|
||||
-- 9100051 / ORDR 17 회원정보 암호화관리 /mnt/sec/mberCrypto/listView.do (추가제안 Action4)
|
||||
-- ★ left.jsp 9100048~9100051 <c:when> 분기 4개 + 상위 하이라이트 조건 2곳 동반 배포 필수
|
||||
-- ※ 9100045·9100046 은 2026-08-11 화면 폐기와 함께 삭제된 번호라 재사용하지 않는다.
|
||||
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||
SELECT * FROM (
|
||||
SELECT 'InspectionReportList' AS PROGRM_FILE_NM, '/mnt/sec/inspection/' AS PROGRM_STRE_PATH,
|
||||
'정기점검 리포트' AS PROGRM_KOREAN_NM,
|
||||
'접속기록 정기점검 리포트 조회·점검결과 입력(SFR-004-7)' AS PROGRM_DC,
|
||||
'/mnt/sec/inspection/listView.do?page=9100048' AS URL
|
||||
) seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||
|
||||
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||
SELECT * FROM (
|
||||
SELECT 'AbnormalAccessList' AS PROGRM_FILE_NM, '/mnt/sec/abnormal/' AS PROGRM_STRE_PATH,
|
||||
'비정상접근 탐지' AS PROGRM_KOREAN_NM,
|
||||
'비정상 접근 탐지 5유형 조회(과업내용 다-①)' AS PROGRM_DC,
|
||||
'/mnt/sec/abnormal/listView.do?page=9100049' AS URL
|
||||
) seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||
|
||||
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||
SELECT * FROM (
|
||||
SELECT 'AccessLogPurgeHistory' AS PROGRM_FILE_NM, '/mnt/sec/purgeHistory/' AS PROGRM_STRE_PATH,
|
||||
'접속기록 파기이력' AS PROGRM_KOREAN_NM,
|
||||
'접속기록 파티션 파기·증설 이력 조회(SER-002-2)' AS PROGRM_DC,
|
||||
'/mnt/sec/purgeHistory/listView.do?page=9100050' AS URL
|
||||
) seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
|
||||
|
||||
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||
SELECT * FROM (
|
||||
SELECT 'MberCryptoAdmin' AS PROGRM_FILE_NM, '/mnt/sec/mberCrypto/' AS PROGRM_STRE_PATH,
|
||||
'회원정보 암호화 관리' AS PROGRM_KOREAN_NM,
|
||||
'암호화 회원정보 복호화 조회·수정·파기·등록(추가제안 Action4)' AS PROGRM_DC,
|
||||
'/mnt/sec/mberCrypto/listView.do?page=9100051' 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, 'InspectionReportList' AS PROGRM_FILE_NM, 9100048 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 14 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 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, 'AbnormalAccessList' AS PROGRM_FILE_NM, 9100049 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 15 AS MENU_ORDR, '비정상 접근 탐지 5유형 조회' 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 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, 'AccessLogPurgeHistory' AS PROGRM_FILE_NM, 9100050 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 16 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 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, 'MberCryptoAdmin' AS PROGRM_FILE_NM, 9100051 AS MENU_NO,
|
||||
9100030 AS UPPER_MENU_NO, 17 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 n.mno, g.ac
|
||||
FROM (SELECT 9100048 AS mno UNION ALL SELECT 9100049 UNION ALL
|
||||
SELECT 9100050 UNION ALL SELECT 9100051) n
|
||||
CROSS JOIN (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 = n.mno AND x.AUTHOR_CODE = g.ac);
|
||||
|
||||
|
||||
-- =====================================================================
|
||||
-- PART 4. 뷰 GNR 컷오버 — comvnusermaster / comvnuserinfo (2026-08-11 통합)
|
||||
-- 원본: src/script/mysql/ddl/privacy.mber_nm_view_hash_cutover_mysql.sql
|
||||
|
||||
@ -25,6 +25,29 @@
|
||||
|
||||
---
|
||||
|
||||
## 1-0. 2026-08-27 작업 — 관리자 화면 4종 신설
|
||||
|
||||
> ⚠ 아래 §1-1 이후의 기록은 **2026-07-22 기준**이라 현재 소스와 어긋난다(그 뒤 커밋에서
|
||||
> 인터셉터 `enforce=true`·`urlRoleMapping` 활성, 관리자 화면 8종, OTP DB영속, 회원정보 암호화
|
||||
> 컷오버가 모두 반영됨). 갱신은 별도 작업으로 남긴다.
|
||||
|
||||
| 대상 | 요구사항 | 산출물 | 상태 |
|
||||
|------|----------|--------|:----:|
|
||||
| #48 정기점검 리포트 화면 | SFR-004-7 | `InspectionResultController.listView/saveResultView` + `site/mnt/sec/inspection/list.jsp` + 메뉴 9100048 | ✅ 완료 |
|
||||
| #49 비정상접근 탐지 화면 | 과업내용 다-①, SER-002 | `AbnormalAccessController`(신규) + `AbnormalAccessResultVO.rows` 확장 + `abnormal/list.jsp` + 메뉴 9100049 | ✅ 완료 |
|
||||
| 접속기록 파기이력 화면(신규 항목) | SER-002-2 / DBR-001-2 | `AccessLogPurgeHistoryController`+VO 2종, `AccessLogPartitionDAO` 조회 2메서드, sqlmap 2 statement, `purgeHistory/list.jsp`, 메뉴 9100050 | ✅ 완료 |
|
||||
| 회원정보 암호화 관리 CRUD(추가제안 Action4) | 제안서 슬라이드16 | `MberCryptoAdmin*` 8클래스 + `PrivacyMberCryptoAdmin_SQL_Mysql/Oracle` + `MBER_CRYPTO_ACCESS_HISTORY` DDL + JSP 5종 + 메뉴 9100051 | ✅ 완료 |
|
||||
|
||||
- 검증: JDK8 `mvn -o compile` **EXIT=0**, JSP 9종 Jasper 사전컴파일 **오류 0건**.
|
||||
- 배포 델타: `_workspace_privacy/deploy/20260827_1410/DELTA.md` (SQL 2건 적용순서 포함).
|
||||
- **미완**: Tomcat 기동 상태 실화면 검증(관리자 계정 필요), 개발/운영 서버 반영.
|
||||
- **후속 발견**: 기존 탈퇴 구문 `mberManageDAO.updateDeleteMber_S` 는 개인정보를 비우면서도
|
||||
**이름(MBER_NM/MBER_NM_ENC/MBER_NM_HASH)은 지우지 않는다** — 탈퇴 후에도 이름이 남는 파기 누락
|
||||
소지. 신설 화면의 파기(`MberCryptoAdminDAO.deleteMberPii`)는 이름까지 파기한다. 기존 탈퇴 경로
|
||||
보정은 별도 판단 필요(영향 범위: 프론트 탈퇴 전체).
|
||||
|
||||
---
|
||||
|
||||
## 1-1. 오늘의 작업 (2026-07-21, ~17:00 목표)
|
||||
|
||||
fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반 항목 6개**를 당일 완료 목표로 착수한다.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user