개인정보보호 기능개선 10차 — 접속·처리기록을 RFP대로 화면 1개로 정리

제안요청서 과업내용 "다"가 요구하는 화면은 하나다 —
"ㅇ 로그 조회 및 관리 기능 구현 / - 관리자용 로그 조회 화면 개발".
제안요약서 시안도 "접속기록 조회 및 감사" 한 장만 그려져 있다.
8·9차에서 이 영역을 여러 화면으로 벌려 놓은 것을 되돌린다.

· 삭제 — 정기 점검 리포트 화면 / 이상행위 감사 화면 / 개인정보 처리기록 화면
  AbnormalAccessController, abnormal/list.jsp, inspection/list.jsp 제거.
  InspectionResultController 는 화면 엔드포인트(listView/saveResultView)만 걷어내고
  8차 이전의 JSON 계약으로 되돌렸다.
  PersonalDataAccessLogSearchVO.excludeSelect 와 그 sqlmap 조건(Mysql/Oracle 각 3곳)도
  함께 제거 — processLog 진입점이 사라져 죽은 조건이 된다.

· 기능은 남긴다 (RFP 가 '기능'으로 명시한 항목이라 지우면 요건 미충족)
  · "처리기록 검색·조회 기능 제공"       → 처리구분 필터(조회/등록/수정/삭제 토글)
  · "비정상 접근 탐지 및 기록 기능 구현"  → 목록 '감사' 칸(이상/점검) 상시 표시
  · SFR-004 "접속기록 정기점검을 지원"   → 월 1회 배치 + PDAL_INSPECTION_REPORT 적재
  · 파기이력(SER-002-2)                    → 접속기록 화면의 "파기이력" 버튼
  AbnormalAccessDetectionService 6클래스는 감사 칸이 쓰므로 그대로 둔다.

· 메뉴 — 권한관리 하위 "접속기록 조회 및 감사"(9100044, ORDR 10) 하나로 수렴
  9100048/49/50/52/53 제거. install_mysql.sql PART 3 은 [7]+[8] 을 최종형 [7] 하나로
  대체했다(이미 중간형을 적용한 DB 를 위한 정리구문 포함, 신규 설치는 무영향).

검증: mvn compile EXIT=0, JSP 3종 Jasper 오류 0건, 로컬 반영·재기동 정상.
살아있는 화면 accessLog/purgeHistory/mberCrypto 302→로그인,
삭제한 inspection/abnormal/processLog **404**(제거 증명), frt/main.do 200,
통합본 전체 재실행 오류 0건(멱등).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
infraurp 2026-08-27 17:23:21 +09:00
parent 3d359a1a8f
commit 33bd57cae2
13 changed files with 221 additions and 948 deletions

View File

@ -1,146 +0,0 @@
/**
* 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;
}
}

View File

@ -97,26 +97,18 @@ public class PersonalDataAccessLogController {
@RequestMapping(value = "/mnt/sec/accessLog/listView.do")
public String listView(@ModelAttribute("personalDataAccessLogSearchVO") PersonalDataAccessLogSearchVO searchVO,
ModelMap model) throws Exception {
return renderList(searchVO, model, false);
return renderList(searchVO, model);
}
/**
* 개인정보 <b>처리기록</b> 조회 화면 — 제안서 시안 좌측 (2) 메뉴.
* 목록 렌더링.
*
* <p>(1)(접속기록 조회)이 접속·조회를 포함한 전체 기록이라면, 이 화면은 조회(S)를 빼고
* <b>등록·수정·삭제만</b> 남긴다. 정보주체 소명이나 감사 대응에서 "누가 이 사람의 개인정보를
* 바꿨는가"를 먼저 보게 되는 쪽이라 별도 진입점을 둔다. 화면(JSP)과 검색조건은 (1)과 공유하고
* {@code excludeSelect} 조건 하나만 다르다 — 같은 표를 두 벌 관리하지 않기 위해서다.</p>
* <p><b>화면은 이 하나다.</b> 제안요청서 과업내용 "다"가 요구하는 화면은
* "관리자용 로그 조회 화면" 한 개이며, 제안요약서 시안도 이 한 장만 그려져 있다.
* 처리기록(등록·수정·삭제) 조회는 별도 화면이 아니라 <b>처리구분 필터</b>로,
* 비정상 접근 탐지는 목록의 <b>감사 칸</b>으로 이 화면 안에서 제공한다.</p>
*/
@RequestMapping(value = "/mnt/sec/processLog/listView.do")
public String processListView(@ModelAttribute("personalDataAccessLogSearchVO") PersonalDataAccessLogSearchVO searchVO,
ModelMap model) throws Exception {
searchVO.setExcludeSelect(true);
return renderList(searchVO, model, true);
}
/** (1)·(2) 공통 렌더링. {@code processOnly} 는 화면 제목·안내 문구 분기에만 쓴다. */
private String renderList(PersonalDataAccessLogSearchVO searchVO, ModelMap model, boolean processOnly)
private String renderList(PersonalDataAccessLogSearchVO searchVO, ModelMap model)
throws Exception {
if (handlerUser() == null) {
return LOGIN_REDIRECT;
@ -138,7 +130,6 @@ public class PersonalDataAccessLogController {
model.addAttribute("dmlSummary", selectDmlSummary(searchVO));
// 내 다운로드 작업 목록(비동기) — 상태·진행률 확인 및 내려받기
model.addAttribute("dlJobList", selectMyDownloadJobs());
model.addAttribute("processOnly", Boolean.valueOf(processOnly));
model.addAttribute("retentionDays", Integer.valueOf(ACCESS_LOG_RETENTION_DAYS));
// 시안의 "감사" 칸 — 목록 각 행에 이상/점검 표시를 채우고 기간 전체 탐지 요약을 함께 싣는다
markAudit(list, searchVO, model);

View File

@ -58,17 +58,6 @@ public class PersonalDataAccessLogSearchVO implements Serializable {
/** 전체 레코드 수(조회 후 세팅) */
private int totalRecordCount = 0;
/**
* 조회(S)를 제외하고 <b>등록·수정·삭제만</b> 보여줄지 여부.
*
* <p>제안서 시안의 좌측 ② "개인정보 처리기록" 메뉴가 이 조건을 켠 화면이다.
* ① "접속기록 조회"가 접속·조회를 포함한 전체 기록이라면, ②는 개인정보를 실제로
* <u>바꾼</u> 행위만 추린 화면이다 — 정보주체 소명·감사 대응에서 먼저 보게 되는 쪽이다.</p>
*
* <p>{@code dmlGubun}(단일 값 지정)과는 다른 조건이다. 둘 다 지정되면 SQL 에서 AND 로 걸려
* 교집합이 되므로, 화면에서는 한쪽만 쓰게 한다.</p>
*/
private boolean excludeSelect = false;
public String getConectId() { return conectId; }
public void setConectId(String conectId) { this.conectId = conectId; }
@ -106,8 +95,6 @@ public class PersonalDataAccessLogSearchVO implements Serializable {
public int getTotalRecordCount() { return totalRecordCount; }
public void setTotalRecordCount(int totalRecordCount) { this.totalRecordCount = totalRecordCount; }
public boolean isExcludeSelect() { return excludeSelect; }
public void setExcludeSelect(boolean excludeSelect) { this.excludeSelect = excludeSelect; }
// =====================================================================
// SQL 바인딩용 파생값 (2026-08-07) — setter 없음(읽기 전용).

View File

@ -1,6 +1,5 @@
package egovframework.com.cmm.privacy.batch;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -13,20 +12,13 @@ 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) 엔드포인트. 기존 {@code list.do}/{@code detail.do}/{@code saveResult.do}
* 는 jsonView 로 응답하는 백엔드 계약이고, 2026.08.27 추가된 {@code listView.do}/
* {@code saveResultView.do} 가 관리자 화면(#48)이다 — 두 계약을 병존시켜 기존 호출부를 깨지 않는다.
* <p>취급자 영역(/mnt) 백엔드 엔드포인트. 응답은 jsonView(화면 JSP는 #48 대시보드 후속).
* 점검자 식별자는 세션 loginVO 에서 취득한다.</p>
*
* <p><b>화면이 필요한 이유:</b> 월 1회 점검 자체는 배치(#43)가 집계하지만, "누가 언제 보고 무엇을
* 조치했는가"는 사람이 입력해야 남는다. 그 입력 경로가 JSON 엔드포인트뿐이면 점검 사실을
* 증빙으로 제출할 수 없다(SFR-004-7).</p>
*
* @author arko-privacy-auditlog (Fork C - 7차)
* @since 2026-07-21
*/
@ -35,8 +27,6 @@ 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;
@ -45,82 +35,6 @@ 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,

View File

@ -113,9 +113,6 @@
<isNotEmpty prepend="AND" property="conectIp"> CONECT_IP LIKE #conectIpLike# ESCAPE '!' </isNotEmpty>
<isNotEmpty prepend="AND" property="dmlGubun"> DML_GUBUN = #dmlGubun# </isNotEmpty>
<isNotEmpty prepend="AND" property="processResult"> PROCESS_RESULT = #processResult# </isNotEmpty>
<!-- 시안 좌측 ② "개인정보 처리기록" — 조회(S)를 빼고 실제로 개인정보를 바꾼 행위만 본다.
dmlGubun(단일값)과 함께 지정하면 AND 로 걸려 교집합이 되므로 화면에서 한쪽만 쓴다. -->
<isEqual prepend="AND" property="excludeSelect" compareValue="true"> DML_GUBUN &lt;&gt; 'S' </isEqual>
<isNotEmpty prepend="AND" property="fromDt"> CONECT_DT &gt;= #fromDtParam# </isNotEmpty>
<isNotEmpty prepend="AND" property="toDt"> CONECT_DT &lt;= #toDtParam# </isNotEmpty>
</dynamic>
@ -138,9 +135,6 @@
<isNotEmpty prepend="AND" property="conectIp"> CONECT_IP LIKE #conectIpLike# ESCAPE '!' </isNotEmpty>
<isNotEmpty prepend="AND" property="dmlGubun"> DML_GUBUN = #dmlGubun# </isNotEmpty>
<isNotEmpty prepend="AND" property="processResult"> PROCESS_RESULT = #processResult# </isNotEmpty>
<!-- 시안 좌측 ② "개인정보 처리기록" — 조회(S)를 빼고 실제로 개인정보를 바꾼 행위만 본다.
dmlGubun(단일값)과 함께 지정하면 AND 로 걸려 교집합이 되므로 화면에서 한쪽만 쓴다. -->
<isEqual prepend="AND" property="excludeSelect" compareValue="true"> DML_GUBUN &lt;&gt; 'S' </isEqual>
<isNotEmpty prepend="AND" property="fromDt"> CONECT_DT &gt;= #fromDtParam# </isNotEmpty>
<isNotEmpty prepend="AND" property="toDt"> CONECT_DT &lt;= #toDtParam# </isNotEmpty>
</dynamic>
@ -169,9 +163,6 @@
<isNotEmpty prepend="AND" property="conectIp"> CONECT_IP LIKE #conectIpLike# ESCAPE '!' </isNotEmpty>
<isNotEmpty prepend="AND" property="dmlGubun"> DML_GUBUN = #dmlGubun# </isNotEmpty>
<isNotEmpty prepend="AND" property="processResult"> PROCESS_RESULT = #processResult# </isNotEmpty>
<!-- 시안 좌측 ② "개인정보 처리기록" — 조회(S)를 빼고 실제로 개인정보를 바꾼 행위만 본다.
dmlGubun(단일값)과 함께 지정하면 AND 로 걸려 교집합이 되므로 화면에서 한쪽만 쓴다. -->
<isEqual prepend="AND" property="excludeSelect" compareValue="true"> DML_GUBUN &lt;&gt; 'S' </isEqual>
<isNotEmpty prepend="AND" property="fromDt"> CONECT_DT &gt;= #fromDtParam# </isNotEmpty>
<isNotEmpty prepend="AND" property="toDt"> CONECT_DT &lt;= #toDtParam# </isNotEmpty>
</dynamic>

View File

@ -96,9 +96,6 @@
<isNotEmpty prepend="AND" property="conectIp"> CONECT_IP LIKE #conectIpLike# ESCAPE '!' </isNotEmpty>
<isNotEmpty prepend="AND" property="dmlGubun"> DML_GUBUN = #dmlGubun# </isNotEmpty>
<isNotEmpty prepend="AND" property="processResult"> PROCESS_RESULT = #processResult# </isNotEmpty>
<!-- 시안 좌측 ② "개인정보 처리기록" — 조회(S)를 빼고 실제로 개인정보를 바꾼 행위만 본다.
dmlGubun(단일값)과 함께 지정하면 AND 로 걸려 교집합이 되므로 화면에서 한쪽만 쓴다. -->
<isEqual prepend="AND" property="excludeSelect" compareValue="true"> DML_GUBUN &lt;&gt; 'S' </isEqual>
<isNotEmpty prepend="AND" property="fromDt"> CONECT_DT &gt;= #fromDtParam# </isNotEmpty>
<isNotEmpty prepend="AND" property="toDt"> CONECT_DT &lt;= #toDtParam# </isNotEmpty>
</dynamic>
@ -120,9 +117,6 @@
<isNotEmpty prepend="AND" property="conectIp"> CONECT_IP LIKE #conectIpLike# ESCAPE '!' </isNotEmpty>
<isNotEmpty prepend="AND" property="dmlGubun"> DML_GUBUN = #dmlGubun# </isNotEmpty>
<isNotEmpty prepend="AND" property="processResult"> PROCESS_RESULT = #processResult# </isNotEmpty>
<!-- 시안 좌측 ② "개인정보 처리기록" — 조회(S)를 빼고 실제로 개인정보를 바꾼 행위만 본다.
dmlGubun(단일값)과 함께 지정하면 AND 로 걸려 교집합이 되므로 화면에서 한쪽만 쓴다. -->
<isEqual prepend="AND" property="excludeSelect" compareValue="true"> DML_GUBUN &lt;&gt; 'S' </isEqual>
<isNotEmpty prepend="AND" property="fromDt"> CONECT_DT &gt;= #fromDtParam# </isNotEmpty>
<isNotEmpty prepend="AND" property="toDt"> CONECT_DT &lt;= #toDtParam# </isNotEmpty>
</dynamic>
@ -143,9 +137,6 @@
<isNotEmpty prepend="AND" property="conectIp"> CONECT_IP LIKE #conectIpLike# ESCAPE '!' </isNotEmpty>
<isNotEmpty prepend="AND" property="dmlGubun"> DML_GUBUN = #dmlGubun# </isNotEmpty>
<isNotEmpty prepend="AND" property="processResult"> PROCESS_RESULT = #processResult# </isNotEmpty>
<!-- 시안 좌측 ② "개인정보 처리기록" — 조회(S)를 빼고 실제로 개인정보를 바꾼 행위만 본다.
dmlGubun(단일값)과 함께 지정하면 AND 로 걸려 교집합이 되므로 화면에서 한쪽만 쓴다. -->
<isEqual prepend="AND" property="excludeSelect" compareValue="true"> DML_GUBUN &lt;&gt; 'S' </isEqual>
<isNotEmpty prepend="AND" property="fromDt"> CONECT_DT &gt;= #fromDtParam# </isNotEmpty>
<isNotEmpty prepend="AND" property="toDt"> CONECT_DT &lt;= #toDtParam# </isNotEmpty>
</dynamic>

View File

@ -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 '9100047' || param.page eq '9100051'}">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 '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 '9100047' || param.page eq '9100051'}">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 '9100051'}">style="display: block;"</c:if>>
<c:forEach items="${list_menulist}" var="menuUpperVo4">
<c:choose>
<c:when test="${menuUpperVo4.menuNo eq '9100031'}">
@ -122,6 +122,11 @@
<c:when test="${menuUpperVo4.menuNo eq '9100039'}">
<li <c:if test="${param.page eq '9100039'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
</c:when>
<%-- [개인정보/RFP 과업내용 다] 접속기록 조회 및 감사 — 이 영역이 요구하는 화면은 이 하나다.
처리기록은 처리구분 필터, 이상행위는 목록의 감사 칸, 파기이력은 화면 내 버튼으로 제공한다. --%>
<c:when test="${menuUpperVo4.menuNo eq '9100044'}">
<li <c:if test="${param.page eq '9100044'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
</c:when>
<%-- [2026-08-11] 9100045 업무유형 매핑 관리 / 9100046 로그인·로그아웃 접속기록 분기 삭제(화면·메뉴 폐기) --%>
<%-- [개인정보/SER-001 요건4·5] 접근통제 정책 설정(계정잠금·자동로그아웃·세션재발급) --%>
<c:when test="${menuUpperVo4.menuNo eq '9100047'}">
@ -139,40 +144,6 @@
</li>
</c:if>
<%-- ============================================================
[개인정보/제안서 Ⅲ-4 시안] 접속기록 · 감사 (9100052)
① 접속기록 조회(9100044) ② 개인정보 처리기록(9100053)
③ 이상행위 감사(9100049) ④ 정기 점검 리포트(9100048)
제안서 시안이 이 넷을 한 묶음으로 그리고 있어 권한관리 하위에서 이설했다.
접속기록 파기이력은 시안에 메뉴가 없어 정기 점검 리포트 화면 안에서 연다.
============================================================ --%>
<c:if test="${menuUpperVo2.menuNo eq '9100052'}">
<li <c:if test="${param.page eq '9100044' || param.page eq '9100053' || param.page eq '9100049' || param.page eq '9100048'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
<c:forEach items="${list_menulist}" var="menuUpperVo3">
<c:if test="${menuUpperVo3.menuNo eq '9100052'}">
<ul <c:if test="${param.page eq '9100044' || param.page eq '9100053' || param.page eq '9100049' || param.page eq '9100048'}">style="display: block;"</c:if>>
<c:forEach items="${list_menulist}" var="menuUpperVo4">
<c:choose>
<c:when test="${menuUpperVo4.menuNo eq '9100044'}">
<li <c:if test="${param.page eq '9100044'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
</c:when>
<c:when test="${menuUpperVo4.menuNo eq '9100053'}">
<li <c:if test="${param.page eq '9100053'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
</c:when>
<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>
<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>
</c:choose>
</c:forEach>
</ul>
</c:if>
</c:forEach>
</li>
</c:if>
<c:if test="${menuUpperVo2.menuNo eq '9100040'}">
<li <c:if test="${param.page eq '9100041' || param.page eq '9100042' || param.page eq '9100043'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
<c:forEach items="${list_menulist}" var="menuUpperVo3">

View File

@ -1,221 +0,0 @@
<%@ 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> 탐지 요약 &nbsp;:&nbsp; <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>

View File

@ -14,17 +14,13 @@
· 이 테이블은 UPDATE/DELETE statement 미정의(위·변조 방지) — 화면에서도 조회/다운로드만 제공
--%>
<c:set var="PATH" value="/mnt" />
<%-- 제안서 시안 좌측 (1)접속기록 조회 / (2)개인정보 처리기록 — 같은 화면을 조건만 바꿔 쓴다.
목록 재조회·페이징이 자기 화면으로 돌아오도록 진입 URL 을 여기서 정한다. --%>
<c:set var="LIST_URL" value="/mnt/sec/accessLog/listView.do" />
<c:if test="${processOnly}"><c:set var="LIST_URL" value="/mnt/sec/processLog/listView.do" /></c:if>
<script type="text/javascript" src="<c:url value='/js/egovframework/com/cmm/jquery.js'/>"></script>
<script type="text/javascript">
<!--
function fn_list(pageIndex) {
document.frm.pageIndex.value = pageIndex;
document.frm.method = "get";
document.frm.action = "<c:url value='${LIST_URL}' />";
document.frm.action = "<c:url value='${PATH}/sec/accessLog/listView.do' />";
document.frm.submit();
}
@ -44,7 +40,7 @@ function fn_download() {
document.frm.submit();
/* 다음 검색이 다운로드로 가지 않도록 원복 */
document.frm.dlReason.value = "";
document.frm.action = "<c:url value='${LIST_URL}' />";
document.frm.action = "<c:url value='${PATH}/sec/accessLog/listView.do' />";
}
/* 다운로드 '요청'(비동기) : 사유를 받아 작업만 등록하고 목록으로 돌아온다.
@ -273,10 +269,7 @@ tr.audit_w td { background:#fffaf0; }
<!-- //검색창 -->
<div class="guide">
<c:choose>
<c:when test="${processOnly}">· <strong>개인정보를 실제로 바꾼 기록</strong>만 추린 화면입니다(등록·수정·삭제). 조회(S)는 제외되며, 전체 기록은 <strong>접속기록 조회</strong> 메뉴에서 봅니다.<br /></c:when>
<c:otherwise>· 개인정보 <strong>접속·처리 전체 기록</strong>입니다. 바꾼 기록만 보려면 <strong>개인정보 처리기록</strong> 메뉴를 쓰세요.<br /></c:otherwise>
</c:choose>
· 개인정보 <strong>접속·처리 전체 기록</strong>입니다. <strong>등록·수정·삭제만</strong> 보려면 아래 처리행위 요약에서 해당 항목을 누르세요(누르면 그 구분만 검색됩니다).<br />
· <strong>감사</strong> 칸의 <span class="badge audit_a">이상</span>은 처리가 차단·실패한 기록(비인가 접근 시도), <span class="badge audit_w">점검</span>은 처리는 정상이었으나 대량조회·계정공유의심·인증실패반복 대상이거나 업무시간 밖 처리임을 뜻합니다. <strong>탐지되었다는 것이 곧 위반은 아닙니다</strong> — 업무상 정당한 경우가 있으므로 확인 후 판단하세요.<br />
· 개인정보를 처리(조회·등록·수정·삭제)한 기록입니다. 기간은 <strong>yyyy-MM-dd</strong> 또는 <strong>yyyy-MM-dd HH:mm:ss</strong> 형식으로 입력하세요.
날짜만 입력하면 <strong>종료일 당일(23:59:59)까지 포함</strong>해 검색합니다.<br />
@ -307,6 +300,10 @@ tr.audit_w td { background:#fffaf0; }
· <a href="#none" onclick="fn_dml('D'); return false;" title="삭제 기록만 보기">삭제 <strong class="d"><c:out value="${dmlSummary.D}" /></strong></a>]
</span>
<input type="button" class="btn_dl" value="엑셀 다운로드 요청(사유 필수)" onclick="fn_downloadRequest();" />
<%-- 접속기록 파기이력 진입점. 별도 메뉴를 두지 않는다(RFP 가 요구하는 화면은 이 하나).
보관기간 경과분을 규정대로 파기했다는 증빙이라 조회 화면에서 바로 열 수 있게 둔다. --%>
<input type="button" class="btn_search" value="파기이력"
onclick="location.href='<c:url value="${PATH}/sec/purgeHistory/listView.do"/>?page=9100044';" />
</div>
<%-- 비동기 다운로드 작업 목록 — 요청자 본인 것만 보인다.

View File

@ -1,249 +0,0 @@
<%@ 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>건
<%-- 접속기록 파기이력 진입점.
제안서 시안의 좌측메뉴에는 파기이력이 없고, 파기는 "보관·점검"의 일부로 그려져 있다.
그래서 별도 메뉴 대신 점검 화면에서 연다 — 점검할 때 "규정대로 파기됐는가"를
같은 자리에서 확인하는 편이 실제 감사 흐름과도 맞는다. --%>
<input type="button" class="btn_search" value="접속기록 파기이력"
onclick="location.href='<c:url value="${PATH}/sec/purgeHistory/listView.do"/>?page=9100048';" />
</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>

View File

@ -78,10 +78,10 @@ function fn_list(pageIndex) {
<div class="btn_area">
총 <strong><c:out value="${accessLogPurgeHistorySearchVO.totalRecordCount}" /></strong>건
<%-- 이 화면은 좌측메뉴에 없다(제안서 시안에 파기이력 메뉴가 없음).
정기 점검 리포트에서 들어오므로 돌아갈 길을 같이 둔다. --%>
<input type="button" class="btn_search" value="정기 점검 리포트로"
onclick="location.href='<c:url value="${PATH}/sec/inspection/listView.do"/>?page=9100048';" />
<%-- 이 화면은 좌측메뉴에 없다 — RFP 가 요구하는 화면은 "접속기록 조회 및 감사" 하나다.
그 화면의 "파기이력" 버튼으로 들어오므로 돌아갈 길을 같이 둔다. --%>
<input type="button" class="btn_search" value="접속기록 조회로"
onclick="location.href='<c:url value="${PATH}/sec/accessLog/listView.do"/>?page=9100044';" />
</div>
<table summary="개인정보 접속기록 파기·증설 이력 목록" class="list1">

View File

@ -0,0 +1,63 @@
-- =====================================================================
-- 관리자 메뉴 정리 : 접속·처리기록 관리를 화면 1개로 통합
-- 문화품앗이 개인정보보호 기능개선 사업 (RFP 과업내용 다 / SFR-004)
-- 작성일: 2026-08-27 DB: MySQL 8 / MariaDB (스키마 csv)
-- ---------------------------------------------------------------------
-- [근거] 제안요청서 과업내용 "다. 개인정보취급자 접속 및 처리기록 관리 기능 개발" 이
-- 요구하는 화면은 하나다 —
-- "ㅇ 로그 조회 및 관리 기능 구현
-- - 관리자용 로그 조회 화면 개발
-- - 조건별 검색 및 다운로드 기능 제공
-- - 로그 다운로드 이력, 무결성 확보 및 관리 기능 적용"
-- 제안요약서 slide 14~15 의 시안도 이 한 화면("접속기록 조회 및 감사")만 그려져 있다.
--
-- [따라서] 2026-08-27 에 만들었던 "접속기록 · 감사" 그룹(9100052)과 하위 3개 메뉴
-- (개인정보 처리기록 9100053 / 이상행위 감사 9100049 / 정기 점검 리포트 9100048)를
-- 메뉴에서 내리고, 접속기록 화면 하나로 되돌린다.
--
-- ★ 기능은 지우지 않는다 ★
-- RFP 는 이 셋을 '기능'으로는 명시하고 있다 —
-- · "비정상 접근 탐지 및 기록 기능 구현" → 목록의 '감사' 칸(이상/점검)으로 상시 표시
-- · "처리기록 검색·조회 기능 제공" → 처리구분(조회/등록/수정/삭제) 필터로 제공
-- · SFR-004 "접속기록 정기점검을 지원" → 화면 내 버튼으로 진입
-- 화면(컨트롤러·JSP)은 그대로 살아 있고 진입 동선만 접속기록 화면 안으로 옮긴다.
-- 메뉴에서 내렸다고 기능이 없어진 것이 아니다 — 검수 시 이 주석이 근거다.
--
-- [남는 메뉴] 권한관리(9100030) 하위 ORDR 10 : "접속기록 조회 및 감사" (9100044)
--
-- [재실행 안전] UPDATE 는 같은 값으로 수렴, DELETE 는 대상 한정 → 반복 실행 무해.
-- [한글 리터럴 주의] SOURCE 로 실행하고 적용 후 HEX(MENU_NM) 확인.
-- ★★ left.jsp 동반 배포 필수 — 9100052 그룹 블록 제거 + 권한관리 블록에 9100044 분기 복원 ★★
-- =====================================================================
-- ── [1] 접속기록 화면을 권한관리 하위로 되돌리고 시안 표기명으로 정정 ──
UPDATE COMTNMENUINFO
SET UPPER_MENU_NO = 9100030,
MENU_ORDR = 10,
MENU_NM = '접속기록 조회 및 감사',
MENU_DC = '개인정보 접속·처리기록 조회, 이상행위 감사, 다운로드(사유·이력)'
WHERE MENU_NO = 9100044;
UPDATE COMTNPROGRMLIST
SET PROGRM_KOREAN_NM = '접속기록 조회 및 감사',
PROGRM_DC = '개인정보 접속·처리기록 조회 및 감사(RFP 과업내용 다 / SFR-004)'
WHERE PROGRM_FILE_NM = 'AccessLogList';
-- ── [2] 하위 3개 메뉴 내리기 (화면·컨트롤러·프로그램 등록은 유지) ──
-- 9100048 정기 점검 리포트 / 9100049 이상행위 감사 / 9100053 개인정보 처리기록
DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO IN (9100048, 9100049, 9100053);
DELETE FROM COMTNMENUINFO WHERE MENU_NO IN (9100048, 9100049, 9100053);
-- ── [3] "접속기록 · 감사" 그룹(9100052) 제거 ──
DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO = 9100052;
DELETE FROM COMTNMENUINFO WHERE MENU_NO = 9100052;
-- ── [검증] 권한관리 하위에 9100044 하나만 남고 9100048/49/52/53 이 없어야 한다 ──
-- SELECT MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_NM, HEX(MENU_NM)
-- FROM COMTNMENUINFO WHERE MENU_NO IN (9100044,9100048,9100049,9100052,9100053)
-- ORDER BY MENU_ORDR;
-- → 9100044 / 9100030 / 10 / 접속기록 조회 및 감사 (1행만)
-- ── [롤백] 그룹 구조로 되돌리기 ──
-- privacy.menu_privacy_screens4_insert_mysql.sql 재실행(메뉴 재생성)
-- → privacy.menu_audit_group_regroup_mysql.sql 재실행(그룹 재편)

View File

@ -5,10 +5,10 @@
-- → [PART 2] DML(기준/시드 데이터)
-- → [PART 3] 관리자 메뉴 등록 DML(좌측메뉴 노출, COMTNPROGRMLIST/
-- COMTNMENUINFO/COMTNMENUCREATDTLS,
-- 9100035~9100039 + 9100044 + 9100047 + 9100048~9100051
-- + 9100052(접속기록·감사 그룹) + 9100053(개인정보 처리기록))
-- ※ [8]에서 9100044/48/49 를 9100052 하위로 이설하고 9100050(파기이력)
-- 메뉴는 내린다 — 제안서 Ⅲ-4 화면 시안 정합
-- 9100035~9100039 + 9100044 + 9100047 + 9100051)
-- ※ 접속·처리기록 영역의 화면은 9100044 "접속기록 조회 및 감사" 하나다
-- (RFP 과업내용 다 = "관리자용 로그 조회 화면" 단수, 시안도 한 장).
-- 처리기록·이상행위·파기이력은 그 화면 안에서 제공한다.
-- ※ 9100045(업무유형 매핑 관리)·9100046(로그인 접속기록)은 2026-08-11 폐기
-- — 폐기된 번호는 재사용하지 않는다(과거 권한매핑 잔재와 섞인다)
-- → [PART 4] 뷰 GNR 컷오버(comvnusermaster/comvnuserinfo, 회원 이름 암호화
@ -95,7 +95,7 @@ CREATE TABLE IF NOT EXISTS ACCOUNT_MNG_POLICY (
UPD_ID VARCHAR(20) NULL COMMENT '최종 수정자',
UPD_DT DATETIME NULL COMMENT '최종 수정일시',
PRIMARY KEY (POLICY_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='계정 관리 정책(개인정보보호 SFR-002, 단일행)';
-- 기본 정책 시드(없을 때만). 기준 365일·알림 D-30/7/0·보존 365일.
@ -127,7 +127,7 @@ CREATE TABLE IF NOT EXISTS ACCOUNT_UNLOCK_HISTORY (
KEY IX_AUH_TRGET (TRGET_ID, UNLOCK_DT),
KEY IX_AUH_USER (UNLOCK_USER_ID, UNLOCK_DT),
KEY IX_AUH_DT (UNLOCK_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='계정 잠금 해제 이력(개인정보보호 SER-001 #24)';
@ -180,7 +180,7 @@ CREATE TABLE IF NOT EXISTS AUTHOR_CHANGE_HISTORY (
KEY IX_ACH_TRGET (TRGET_UNIQ_ID, CHG_DT),
KEY IX_ACH_CHANGER (CHANGER_UNIQ_ID, CHG_DT),
KEY IX_ACH_CHGDT (CHG_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='권한 변경 이력(개인정보보호 SFR-001, 3년 이상 보관)';
-- ─────────────────────────────────────────────────────────────
@ -217,7 +217,7 @@ CREATE TABLE IF NOT EXISTS AUTHOR_CHANGE_REQUEST (
PRIMARY KEY (REQUEST_ID),
KEY IX_ACR_TRGET (TRGET_UNIQ_ID, REQUEST_DT),
KEY IX_ACR_STTUS (CONFM_STTUS, REQUEST_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='권한 변경 신청·승인 워크플로(개인정보보호 SFR-001 #3)';
CREATE TABLE IF NOT EXISTS AUTHOR_HIST_DOWNLOAD_HISTORY (
@ -231,7 +231,7 @@ CREATE TABLE IF NOT EXISTS AUTHOR_HIST_DOWNLOAD_HISTORY (
DWLD_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '다운로드 일시',
PRIMARY KEY (DWLD_ID),
KEY IX_AHD_DWLD (DOWNLOADER_ID, DWLD_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='권한이력 다운로드 증빙 이력(개인정보보호 SFR-001 #6)';
-- ─────────────────────────────────────────────────────────────
@ -280,7 +280,7 @@ CREATE TABLE IF NOT EXISTS PDAL_DOWNLOAD_HISTORY (
PRIMARY KEY (DL_ID),
KEY IX_PDDH_DT (DOWNLOAD_DT),
KEY IX_PDDH_USER (DOWNLOADER_ID, DOWNLOAD_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='개인정보 접속기록 다운로드 이력(#42, SER-002)';
-- [Oracle] 시퀀스 필요:
@ -324,7 +324,7 @@ CREATE TABLE IF NOT EXISTS PDAL_INSPECTION_REPORT (
PRIMARY KEY (RPT_ID),
UNIQUE KEY UX_PIR_YM (RPT_YM),
KEY IX_PIR_REGDT (REG_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='개인정보 접속기록 정기점검 리포트(#43, 월 1회)';
-- ─────────────────────────────────────────────────────────────
@ -467,7 +467,7 @@ CREATE TABLE IF NOT EXISTS OTP_DEVICE (
DEVICE_LABEL VARCHAR(100) NULL COMMENT '기기/계정 라벨(참고용)',
USE_YN CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '사용여부 Y:사용 N:해지',
PRIMARY KEY (UNIQ_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='OTP 2단계 인증 비밀키 영구 저장(개인정보보호 SFR-003 No.18)';
-- ─────────────────────────────────────────────────────────────
@ -581,7 +581,7 @@ CREATE TABLE IF NOT EXISTS ACCOUNT_DELETE_HISTORY (
PRIMARY KEY (DEL_ID),
KEY IX_ADH_MBER (MBER_ID),
KEY IX_ADH_DELDT (DEL_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='장기 미사용 계정 개인정보 파기 이력(개인정보보호 SFR-002 #12)';
@ -774,7 +774,7 @@ CREATE TABLE IF NOT EXISTS CONECT_IP_LOCATION (
UPDT_DT DATETIME NULL COMMENT '최종 변경일시',
PRIMARY KEY (LOC_ID),
UNIQUE KEY UK_CIL_CIDR (IP_CIDR)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='접속위치 매핑(IP 대역 → 위치명) — 로그인·로그아웃 접속기록 62';
@ -819,7 +819,7 @@ CREATE TABLE IF NOT EXISTS AUTHOR_HIST_DOWNLOAD_JOB (
PRIMARY KEY (JOB_ID),
KEY IX_AHDJ_REQUESTER (REQUESTER_ID, REG_DT),
KEY IX_AHDJ_STATUS (STATUS, REG_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='권한변경 이력 CSV 비동기 다운로드 작업';
@ -847,7 +847,7 @@ CREATE TABLE IF NOT EXISTS ACCOUNT_ACTION_HISTORY (
KEY IX_AAH_TRGET (TRGET_ID, ACTION_DT),
KEY IX_AAH_USER (ACTION_USER_ID, ACTION_DT),
KEY IX_AAH_DT (ACTION_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='장기 미사용 계정 조치(잠금/말소) 이력(개인정보보호 SFR-002 #11)';
@ -870,7 +870,7 @@ CREATE TABLE IF NOT EXISTS LOGIN_LOCK_EXEMPT (
REG_ID VARCHAR(20) NULL COMMENT '등록한 관리자 계정ID',
REG_DT DATETIME NULL COMMENT '등록일시',
PRIMARY KEY (EXEMPT_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='로그인 실패 계정잠금 예외 계정(개인정보보호 SER-001 24, 최고관리자 잠김 방지)';
@ -896,7 +896,7 @@ CREATE TABLE IF NOT EXISTS ACCESS_CONTROL_POLICY (
UPD_ID VARCHAR(20) NULL COMMENT '최종 수정자',
UPD_DT DATETIME NULL COMMENT '최종 수정일시',
PRIMARY KEY (POLICY_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='접근통제 정책(개인정보보호 SER-001 요건4-5, 단일행)';
INSERT INTO ACCESS_CONTROL_POLICY
@ -956,7 +956,7 @@ CREATE TABLE IF NOT EXISTS PDAL_DOWNLOAD_JOB (
PRIMARY KEY (JOB_ID),
KEY IX_PDJ_REQUESTER (REQUESTER_ID, REG_DT),
KEY IX_PDJ_STATUS (STATUS, REG_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='개인정보 접속기록 CSV 비동기 다운로드 작업';
@ -1097,7 +1097,7 @@ CREATE TABLE IF NOT EXISTS MIG_CRYPTO_PROGRESS (
ON UPDATE CURRENT_TIMESTAMP COMMENT '갱신 일시',
RM VARCHAR(500) NULL COMMENT '비고(마지막 오류 메시지 등)',
PRIMARY KEY (JOB_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='개인정보 암호화 이관 배치 진행상태';
@ -1172,6 +1172,71 @@ CALL PRC_PRIVACY_DROP_DEAD_VIEWS();
DROP PROCEDURE IF EXISTS PRC_PRIVACY_DROP_DEAD_VIEWS;
-- ─────────────────────────────
-- [2026-08-27] 신규 utf8mb4 테이블 collation 수렴 (환경간 차이 제거)
-- ─────────────────────────────
-- =====================================================================
-- [문제] `DEFAULT CHARSET=utf8mb4` 만 쓰면 서버 기본값을 따라가서
-- · 운영 MySQL 5.7 → utf8mb4_general_ci
-- · 로컬 MySQL 8.0 → utf8mb4_0900_ai_ci (5.7 에는 존재하지도 않는 값)
-- 로 **같은 스크립트가 환경마다 다른 테이블을 만든다** — 로컬 테스트가
-- 운영을 정확히 재현하지 못하고, 정렬·비교 의미가 미묘하게 달라진다.
--
-- [선택] utf8mb4_unicode_ci — 5.7/8.0 양쪽에 존재하고, 운영의 기존 utf8mb4
-- 테이블(BIZ_VOLUNTEER 등 13개)이 이미 쓰는 값이라 향후 조인이 생겨도
-- 'Illegal mix of collations' 가 나지 않는다.
-- (현재 개인정보보호 SQL 은 utf8mb4 테이블과 조인하지 않음을 실측 확인했으나,
-- 상대가 utf8mb3 일 때는 MySQL 이 더 넓은 utf8mb4 로 자동 변환하므로 무해하다)
--
-- [동작] 위 CREATE TABLE 은 IF NOT EXISTS 라 이미 만들어진 테이블의 collation 을
-- 바꾸지 못한다. 그래서 여기서 다른 값인 것만 골라 ALTER 한다.
-- · 운영(신규 설치) : 이미 unicode_ci 로 만들어져 있으므로 **전부 SKIP**
-- · 로컬(기존 0900_ai_ci) : 해당 테이블만 unicode_ci 로 전환
-- ※ CHARSET=utf8(utf8mb3) 로 만든 PERSONAL_DATA_ACCESS_LOG / PDAL_PURGE_HISTORY 는
-- 대상이 아니다(양쪽 모두 utf8mb3_general_ci 로 이미 동일 — 대용량 파티션
-- 테이블을 불필요하게 재작성하지 않기 위해서도 제외한다).
-- [재실행] 다른 것만 고르므로 반복 실행 무해(두 번째부터는 전부 SKIP).
-- =====================================================================
DROP PROCEDURE IF EXISTS PRC_PRIVACY_ALIGN_COLLATION;
DELIMITER $$
CREATE PROCEDURE PRC_PRIVACY_ALIGN_COLLATION()
BEGIN
DECLARE v_done INT DEFAULT 0;
DECLARE v_name VARCHAR(64);
DECLARE cur CURSOR FOR
SELECT TABLE_NAME FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_COLLATION LIKE 'utf8mb4%'
AND TABLE_COLLATION <> 'utf8mb4_unicode_ci'
AND LOWER(TABLE_NAME) IN (
'access_control_policy','account_action_history','account_delete_history',
'account_mng_policy','account_unlock_history','author_change_history',
'author_change_request','author_hist_download_history','author_hist_download_job',
'conect_ip_location','login_lock_exempt','mber_crypto_access_history',
'mig_crypto_progress','otp_device','pdal_download_history','pdal_download_job',
'pdal_inspection_report','privacy_view_def_backup');
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN cur;
align_loop: LOOP
FETCH cur INTO v_name;
IF v_done = 1 THEN LEAVE align_loop; END IF;
SET @alter_sql = CONCAT('ALTER TABLE `', v_name,
'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
PREPARE stmt_col FROM @alter_sql;
EXECUTE stmt_col;
DEALLOCATE PREPARE stmt_col;
SELECT CONCAT('COLLATION aligned -> utf8mb4_unicode_ci : ', v_name) AS COLLATION_STATUS;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
CALL PRC_PRIVACY_ALIGN_COLLATION();
DROP PROCEDURE IF EXISTS PRC_PRIVACY_ALIGN_COLLATION;
-- ---------------------------------------------------------------------
-- [privacy.mber_crypto_access_history_create_mysql.sql] 2026-08-27 추가
-- 암호화 회원정보 관리자 접근 이력(추가제안 Action4 / 제안서 슬라이드16)
@ -1195,7 +1260,7 @@ CREATE TABLE IF NOT EXISTS MBER_CRYPTO_ACCESS_HISTORY
KEY IDX_MCAH_01 (ACCESS_DT),
KEY IDX_MCAH_02 (HANDLR_ID),
KEY IDX_MCAH_03 (TRGET_MBER_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='암호화 회원정보 관리자 접근 이력(사유·재인증, 추가제안 Action4)';
@ -1252,9 +1317,9 @@ WHERE NOT EXISTS (SELECT 1 FROM CONECT_IP_LOCATION x WHERE x.IP_CIDR = seed.IP_C
-- [1]~[5] 9100035~9100039 권한 신청·승인 / 권한 변경 이력 / 미사용계정 현황 /
-- 계정 관리 정책 / 인증 실패 이력
-- [6] 9100047(ORDR 13) 접근통제 정책
-- [7] 9100048~9100051(ORDR 14~17) 2026-08-27 추가
-- 정기점검 리포트 / 비정상접근 탐지 / 접속기록 파기이력 / 회원정보 암호화 관리
-- ※ 접속기록 조회(9100044, ORDR 10)는 위 [1]~[5] 블록에 함께 들어 있다.
-- [7] 9100044 명칭 정리("접속기록 조회 및 감사") + 9100051(ORDR 17) 회원정보 암호화 관리
-- + 중간 작업에서 만들었다가 내린 메뉴(9100048/49/50/52/53) 정리
-- ※ 접속기록 조회(9100044, ORDR 10)의 등록 자체는 위 [1]~[5] 블록에 들어 있다.
-- 원본: src/script/mysql/dml/privacy.menu_author_request_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_author_history_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_dormant_status_insert_mysql.sql
@ -1269,7 +1334,7 @@ WHERE NOT EXISTS (SELECT 1 FROM CONECT_IP_LOCATION x WHERE x.IP_CIDR = seed.IP_C
-- 권한관리(9100030) 블록에 등록한 메뉴번호마다 <c:when> 분기 +
-- 시스템관리/권한관리 상위 하이라이트 조건(param.page eq '91000xx')이
-- 반드시 함께 배포돼야 렌더된다. (arko_renual 소스에는 9100035~9100039·9100044·
-- 9100047·9100048~9100051 전부 이미 반영 완료 — 운영 배포 시 WAR/배포본에 이 JSP가
-- 9100047·9100051 전부 이미 반영 완료 — 운영 배포 시 WAR/배포본에 이 JSP가
-- 포함됐는지만 확인하면 됨)
--
-- [재실행 안전] 모든 INSERT NOT EXISTS 가드 → 반복 실행 무해.
@ -1404,40 +1469,42 @@ 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);
-- ── [7] 개인정보보호 화면 메뉴 최종형 (2026-08-27) ──
-- [확정 근거] 제안요청서 과업내용 "다. 개인정보취급자 접속 및 처리기록 관리 기능 개발" 이
-- 요구하는 화면은 하나다 — "관리자용 로그 조회 화면 개발".
-- 제안요약서 slide 14~15 의 시안도 "접속기록 조회 및 감사" 한 장만 그려져 있다.
-- 따라서 접속·처리기록 영역은 **메뉴 1개**로 끝난다.
--
-- [그 한 화면 안에서 제공하는 것] — RFP 가 '기능'으로 명시한 항목들이라 지우지 않는다
-- · "처리기록 검색·조회 기능 제공" → 처리구분(조회/등록/수정/삭제) 필터
-- · "비정상 접근 탐지 및 기록 기능 구현" → 목록의 '감사' 칸(이상/점검) 상시 표시
-- · SFR-004 "접속기록 정기점검을 지원" → 월 1회 배치 + PDAL_INSPECTION_REPORT 적재
-- · 파기이력(SER-002-2) → 화면 내 "파기이력" 버튼
--
-- [이 블록이 하는 일]
-- (1) 9100044 를 "접속기록 조회 및 감사" 로 정리(권한관리 9100030 하위, ORDR 10)
-- (2) 9100051 회원정보 암호화 관리(추가제안 Action4) 메뉴 등록
-- (3) 2026-08-27 중간 작업에서 만들었다가 시안 대조 후 내린 메뉴 정리
-- (9100048 정기점검 / 9100049 이상행위 / 9100050 파기이력 /
-- 9100052 그룹 / 9100053 처리기록) — 이미 적용한 DB 를 위한 정리구문이다.
-- 신규 설치라면 해당 행이 없으므로 아무 일도 일어나지 않는다.
--
-- ★ left.jsp 동반 배포 필수 — 권한관리 블록에 9100044·9100051 분기 + 하이라이트 조건
-- [재실행 안전] INSERT 는 NOT EXISTS, UPDATE 는 같은 값으로 수렴, DELETE 는 대상 한정.
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);
-- (1) 접속기록 화면 명칭 정리
UPDATE COMTNMENUINFO
SET UPPER_MENU_NO = 9100030, MENU_ORDR = 10,
MENU_NM = '접속기록 조회 및 감사',
MENU_DC = '개인정보 접속·처리기록 조회, 이상행위 감사, 다운로드(사유·이력)'
WHERE MENU_NO = 9100044;
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);
UPDATE COMTNPROGRMLIST
SET PROGRM_KOREAN_NM = '접속기록 조회 및 감사',
PROGRM_DC = '개인정보 접속·처리기록 조회 및 감사(RFP 과업내용 다 / SFR-004)'
WHERE PROGRM_FILE_NM = 'AccessLogList';
-- (2) 회원정보 암호화 관리 (추가제안 Action4)
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,
@ -1447,110 +1514,27 @@ SELECT * FROM (
) 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,
9100030 AS UPPER_MENU_NO, 17 AS MENU_ORDR, '암호화 회원정보 복호화 CRUD(사유·재인증)' 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
SELECT 9100051, g.ac
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO)
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
WHERE x.MENU_NO = n.mno AND x.AUTHOR_CODE = g.ac);
WHERE x.MENU_NO = 9100051 AND x.AUTHOR_CODE = g.ac);
-- (3) 중간 작업에서 만들었다가 내린 메뉴 정리 (화면·컨트롤러는 유지/삭제되었음)
DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO IN (9100048, 9100049, 9100050, 9100052, 9100053);
DELETE FROM COMTNMENUINFO WHERE MENU_NO IN (9100048, 9100049, 9100050, 9100052, 9100053);
-- ── [8] "접속기록 · 감사" 그룹 재편(9100052) — 2026-08-27, 제안서 Ⅲ-4 시안 정합 ──
-- [왜] 제안요약서 slide 14~15 의 화면 시안이 좌측메뉴를 "접속기록 · 감사" 한 묶음으로 그리고
-- 그 아래 4개(접속기록 조회 / 개인정보 처리기록 / 이상행위 감사 / 정기 점검 리포트)를 둔다.
-- 위 [7]은 이 넷을 권한관리(9100030) 하위에 낱개로 붙여 시안과 어긋났다 — 여기서 되돌린다.
-- [순서] 반드시 [7] 뒤에 온다([7]이 만든 9100048·9100049 를 이설하기 때문).
-- 신규 설치는 [7]→[8]을 이어 실행하면 최종 구조에 도달하고,
-- 이미 [7]만 적용된 DB는 [8]만 실행해도 같은 결과가 된다(전 구간 멱등).
-- [내려가는 것] 접속기록 파기이력(9100050)은 시안에 메뉴가 없다. 메뉴만 제거하고 화면·
-- 컨트롤러·프로그램(URL) 등록은 남긴다 — 정기 점검 리포트 화면의 버튼으로 진입한다.
-- [그룹 메뉴 규약] PROGRM_FILE_NM 은 NOT NULL 이고 기존 그룹이 모두 센티넬 'dir' 을 쓴다
-- (COMTNPROGRMLIST 에 URL='dir' 1행). NULL 을 넣으면 ERROR 1048 로 실패한다.
-- ★ left.jsp 동반 배포 필수 — 9100052 그룹 블록 신설 + 권한관리 블록에서 4개 분기 제거
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, 'dir' AS PROGRM_FILE_NM, 9100052 AS MENU_NO,
9100000 AS UPPER_MENU_NO, 5 AS MENU_ORDR,
'개인정보 접속·처리기록 조회 및 감사(제안서 Ⅲ-4)' AS MENU_DC,
NULL AS RELATE_IMAGE_PATH, NULL AS RELATE_IMAGE_NM
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO)
AND EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = 'dir');
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
SELECT * FROM (
SELECT 'ProcessLogList' AS PROGRM_FILE_NM, '/mnt/sec/processLog/' AS PROGRM_STRE_PATH,
'개인정보 처리기록' AS PROGRM_KOREAN_NM,
'개인정보를 실제로 변경한 기록(등록·수정·삭제)만 조회' AS PROGRM_DC,
'/mnt/sec/processLog/listView.do?page=9100053' 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, 'ProcessLogList' AS PROGRM_FILE_NM, 9100053 AS MENU_NO,
9100052 AS UPPER_MENU_NO, 2 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);
UPDATE COMTNMENUINFO SET UPPER_MENU_NO = 9100052, MENU_ORDR = 1, MENU_NM = '접속기록 조회'
WHERE MENU_NO = 9100044;
UPDATE COMTNMENUINFO SET UPPER_MENU_NO = 9100052, MENU_ORDR = 3, MENU_NM = '이상행위 감사',
MENU_DC = '비정상 접근 5유형 탐지 결과 조회'
WHERE MENU_NO = 9100049;
UPDATE COMTNMENUINFO SET UPPER_MENU_NO = 9100052, MENU_ORDR = 4, MENU_NM = '정기 점검 리포트'
WHERE MENU_NO = 9100048;
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
SELECT n.mno, g.ac
FROM (SELECT 9100052 AS mno UNION ALL SELECT 9100053) 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);
DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO = 9100050;
DELETE FROM COMTNMENUINFO WHERE MENU_NO = 9100050;
-- [검증] SELECT m.MENU_NO,m.UPPER_MENU_NO,m.MENU_ORDR,m.MENU_NM,HEX(m.MENU_NM)
-- FROM COMTNMENUINFO m WHERE m.MENU_NO=9100052 OR m.UPPER_MENU_NO=9100052
-- ORDER BY m.UPPER_MENU_NO, m.MENU_ORDR;
-- [검증] SELECT MENU_NO,UPPER_MENU_NO,MENU_ORDR,MENU_NM,HEX(MENU_NM)
-- FROM COMTNMENUINFO WHERE MENU_NO IN (9100044,9100051) ORDER BY MENU_ORDR;
-- =====================================================================