#13/#9 정책 조회·수정, #11 장기 미사용 계정 현황 조회, #10 휴면 사전알림 대상 조회.
+ * statement는 {@code PrivacyAccountMng_SQL_Mysql.xml}(namespace {@code AccountMngDAO}),
+ * sql-map-config 등록.
+ */
+@Repository("accountMngDAO")
+public class AccountMngDAO extends EgovComAbstractDAO {
+
+ /** #13/#9 정책 단일행 조회. 없으면 null(호출부 기본값 fallback). */
+ public AccountMngPolicyVO selectPolicy() {
+ return (AccountMngPolicyVO) selectByPk("AccountMngDAO.selectPolicy", null);
+ }
+
+ /** #13 정책 수정. */
+ public int updatePolicy(AccountMngPolicyVO vo) {
+ return update("AccountMngDAO.updatePolicy", vo);
+ }
+
+ /** #11 장기 미사용 계정 현황 목록. */
+ @SuppressWarnings("unchecked")
+ public List selectDormantStatusList(DormantAccountStatusVO searchVO) {
+ return list("AccountMngDAO.selectDormantStatusList", searchVO);
+ }
+
+ /** #11 현황 전체 건수(페이징). */
+ public int selectDormantStatusListCount(DormantAccountStatusVO searchVO) {
+ Object cnt = selectByPk("AccountMngDAO.selectDormantStatusListCount", searchVO);
+ return cnt == null ? 0 : ((Integer) cnt).intValue();
+ }
+
+ /** #10 휴면 D-N일 전 사전알림 대상 조회. param: alertBefore(Integer). */
+ @SuppressWarnings("unchecked")
+ public List selectDormantAlertTargets(int alertBefore) {
+ HashMap param = new HashMap();
+ param.put("alertBefore", Integer.valueOf(alertBefore));
+ return list("AccountMngDAO.selectDormantAlertTargets", param);
+ }
+}
diff --git a/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyService.java b/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyService.java
new file mode 100644
index 0000000..d482fd3
--- /dev/null
+++ b/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyService.java
@@ -0,0 +1,13 @@
+package egovframework.com.cmm.privacy.account;
+
+/**
+ * 계정 관리 정책 서비스 (#13 정책 설정 / #9 휴면 기준일수 정책값화).
+ */
+public interface AccountMngPolicyService {
+
+ /** 현재 정책 조회(미설정 시 기본값 VO 반환). */
+ AccountMngPolicyVO selectPolicy() throws Exception;
+
+ /** 정책 수정. */
+ void updatePolicy(AccountMngPolicyVO vo) throws Exception;
+}
diff --git a/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyServiceImpl.java b/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyServiceImpl.java
new file mode 100644
index 0000000..da45b16
--- /dev/null
+++ b/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyServiceImpl.java
@@ -0,0 +1,31 @@
+package egovframework.com.cmm.privacy.account;
+
+import javax.annotation.Resource;
+
+import org.springframework.stereotype.Service;
+
+/**
+ * 계정 관리 정책 서비스 구현 (#13/#9).
+ *
+ *
#9의 휴면 기준일수는 sqlmap 서브쿼리(ACCOUNT_MNG_POLICY.DORMANT_DAY, 미설정 시 365)로
+ * 직접 적용되며, 이 서비스는 관리자 설정 화면(#13)의 조회/수정을 담당한다.
+ * 정책 미설정(테이블 empty) 시 기본값 VO를 반환해 화면·배치가 안전하게 동작한다.
+ */
+@Service("accountMngPolicyService")
+public class AccountMngPolicyServiceImpl implements AccountMngPolicyService {
+
+ @Resource(name = "accountMngDAO")
+ private AccountMngDAO accountMngDAO;
+
+ @Override
+ public AccountMngPolicyVO selectPolicy() throws Exception {
+ AccountMngPolicyVO vo = accountMngDAO.selectPolicy();
+ // 정책 미설정 시 기본값(휴면 365일 등) — DDL 시드가 없어도 화면/배치 안전.
+ return vo != null ? vo : new AccountMngPolicyVO();
+ }
+
+ @Override
+ public void updatePolicy(AccountMngPolicyVO vo) throws Exception {
+ accountMngDAO.updatePolicy(vo);
+ }
+}
diff --git a/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyVO.java b/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyVO.java
new file mode 100644
index 0000000..cbda2d9
--- /dev/null
+++ b/src/main/java/egovframework/com/cmm/privacy/account/AccountMngPolicyVO.java
@@ -0,0 +1,62 @@
+package egovframework.com.cmm.privacy.account;
+
+import java.io.Serializable;
+
+/**
+ * 계정 관리 정책 VO — ACCOUNT_MNG_POLICY(단일행) 매핑.
+ *
+ *
+ */
+public interface DormantAlertService {
+
+ /**
+ * 정책(ALERT_D1/D2/D3)에 해당하는 휴면 사전알림 대상을 조회해 본인·관리자에게 발송한다.
+ * (배치에서 호출; 개별 발송 실패는 로깅 후 계속 — fail-safe)
+ *
+ * @return 발송 건수
+ */
+ int sendDormantPreAlerts() throws Exception;
+}
diff --git a/src/main/java/egovframework/com/cmm/privacy/account/DormantAlertServiceImpl.java b/src/main/java/egovframework/com/cmm/privacy/account/DormantAlertServiceImpl.java
new file mode 100644
index 0000000..b32a9ad
--- /dev/null
+++ b/src/main/java/egovframework/com/cmm/privacy/account/DormantAlertServiceImpl.java
@@ -0,0 +1,112 @@
+package egovframework.com.cmm.privacy.account;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Resource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import egovframework.com.cop.ems.service.EgovSndngMailRegistService;
+import egovframework.com.cop.ems.service.SndngMailVO;
+
+/**
+ * 휴면 전 사전 알림 서비스 구현 (#10, D-30/D-7/D-Day + 관리자 알림).
+ *
+ *
정책(ACCOUNT_MNG_POLICY)의 ALERT_D1/D2/D3(기본 30/7/0)일 전 대상을 조회해
+ * 본인과 관리자(ADMIN_EMAIL)에게 사전 알림 메일을 발송한다. 기존 휴면 배치의
+ * 유예/확정 본인 메일(EgovLoginServiceImpl.processDiapauseList)과 독립적으로 동작하며,
+ * 발송 실패가 배치 본류를 막지 않도록 전 구간 fail-safe.
PERSONAL_DATA_ACCESS_LOG 표준 컬럼만 SELECT(별도 탐지 테이블 없음, 위·변조 방지).
+ * statement는 이미 등록된 {@code PrivacyAccessLog_SQL_Mysql.xml}
+ * (namespace {@code PersonalDataAccessLogDAO})에 정의 — 신규 sqlmap 등록 불요.
+ *
+ * @author arko-privacy-auditlog (Fork B - 6차)
+ * @since 2026-07-21
+ */
+@Repository("abnormalAccessDetectionDAO")
+public class AbnormalAccessDetectionDAO extends EgovComAbstractDAO {
+
+ /** ① 비인가접근시도(처리결과 F) 총건수. */
+ public int countUnauthorized(Map param) {
+ Object o = selectByPk("PersonalDataAccessLogDAO.detectUnauthorized", param);
+ return o == null ? 0 : ((Integer) o).intValue();
+ }
+
+ /** ② 대량조회 의심 접속자 목록({conectId, cnt}). */
+ @SuppressWarnings("unchecked")
+ public List
+ *
+ *
운영 안전(관리자 잠금 방지): 이 컨트롤러는 취급자를 잠그지 않는다.
+ * 잠금 회피 판정은 {@link SecondFactorInterceptor}가 수행한다(수단 미설정 사용자는 통과).
+ * 본 컨트롤러는 인증수단이 준비된 사용자에게 검증 UI만 제공한다.
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ * 수정일 수정자 수정내용
+ * ---------- ------ ---------------------------
+ * 2026.07.22 URP 최초 생성 (개인정보보호 SFR-003 / #52 — 2차 인증 화면·검증·OTP 등록)
+ *
+ */
+@Controller
+public class SecondFactorController {
+
+ protected static final Log LOG = LogFactory.getLog(SecondFactorController.class);
+
+ /** 2차 인증 실패 누적 세션 키 — 과도한 시도 억제(계정 잠금과 별개, 이 세션 한정). */
+ private static final String ATTR_SF_ATTEMPT = "SF_ATTEMPT";
+ /** 세션당 2차 인증 최대 시도 횟수. */
+ private static final int MAX_ATTEMPT = 5;
+ /** 검증 성공 후 기본 이동 목적지. */
+ private static final String DEFAULT_TARGET = "/mnt/main.do";
+ /** 미인증 시 이동할 로그인 화면. */
+ private static final String LOGIN_URL = "/uat/uia/egovLoginUsr.do";
+
+ /** 취급자별 인증수단 분기 리졸버(빈: authMethodResolver). */
+ @Resource(name = "authMethodResolver")
+ private AuthMethodResolver authMethodResolver;
+
+ /** OTP 인증 서비스(등록 경로에서 비밀키 발급/검증에 직접 사용). */
+ @Resource(name = "otpAuthService")
+ private OtpAuthService otpAuthService;
+
+ // ────────────────────────────────────────────────────────── 2차 인증 화면
+
+ /**
+ * 2차 인증 입력 화면(GET). 로그인 사용자에게 배정된 인증수단을 판정해 안내한다.
+ */
+ @RequestMapping(value = "/uat/uia/secondFactor.do")
+ public String secondFactorView(HttpServletRequest request, ModelMap model,
+ @RequestParam(value = "returnUrl", required = false) String returnUrl) throws Exception {
+
+ LoginVO user = loginUser(request);
+ if (user == null) {
+ return "redirect:" + LOGIN_URL;
+ }
+
+ model.addAttribute("returnUrl", sanitizeTarget(returnUrl));
+
+ AuthMethodService svc = null;
+ try {
+ svc = authMethodResolver.resolve(user.getUniqId());
+ } catch (Exception e) {
+ // 사용 가능한 수단이 하나도 없음 → 등록 유도(잠그지 않음).
+ LOG.info("[개인정보/#52] 2차 인증수단 미가용 — 등록 유도: user=" + user.getId() + ", cause=" + e.toString());
+ }
+
+ if (svc == null || !safeAvailable(svc, user.getUniqId())) {
+ model.addAttribute("needEnroll", Boolean.TRUE);
+ model.addAttribute("methodType", "NONE");
+ return "egovframework/com/uat/uia/EgovSecondFactor";
+ }
+
+ String methodType = methodTypeOf(svc);
+ model.addAttribute("methodType", methodType);
+
+ // SMS 수단이면 화면 진입 시 인증번호 발송(구현체 준비 후 동작). 실패해도 화면은 표시.
+ if ("SMS".equals(methodType)) {
+ try {
+ svc.issue(user.getUniqId());
+ model.addAttribute("maskedPhone", MaskingUtil.maskPhone(user.getMbtlnum()));
+ } catch (Exception e) {
+ LOG.warn("[개인정보/#52] SMS 인증번호 발송 미가용(구현 대기): " + e.toString());
+ model.addAttribute("issueFail", Boolean.TRUE);
+ }
+ }
+
+ return "egovframework/com/uat/uia/EgovSecondFactor";
+ }
+
+ /**
+ * 2차 인증 코드 검증(POST). 성공 시 세션 재발급 + 완료표식 세팅 후 목적지 이동.
+ */
+ @RequestMapping(value = "/uat/uia/secondFactorConfirm.do")
+ public String secondFactorConfirm(HttpServletRequest request, ModelMap model,
+ @RequestParam(value = "authCode", required = false) String authCode,
+ @RequestParam(value = "returnUrl", required = false) String returnUrl) throws Exception {
+
+ LoginVO user = loginUser(request);
+ if (user == null) {
+ return "redirect:" + LOGIN_URL;
+ }
+
+ String target = sanitizeTarget(returnUrl);
+ model.addAttribute("returnUrl", target);
+
+ // 세션당 과도한 시도 억제
+ HttpSession session = request.getSession();
+ int attempt = attemptCount(session);
+ if (attempt >= MAX_ATTEMPT) {
+ model.addAttribute("methodType", "LOCKED");
+ model.addAttribute("errorMsg", "인증 시도 횟수를 초과했습니다. 다시 로그인해 주세요.");
+ return "egovframework/com/uat/uia/EgovSecondFactor";
+ }
+
+ AuthMethodService svc = null;
+ try {
+ svc = authMethodResolver.resolve(user.getUniqId());
+ } catch (Exception e) {
+ LOG.info("[개인정보/#52] 검증 단계 수단 미가용 — 등록 유도: user=" + user.getId());
+ }
+ if (svc == null || !safeAvailable(svc, user.getUniqId())) {
+ model.addAttribute("needEnroll", Boolean.TRUE);
+ model.addAttribute("methodType", "NONE");
+ return "egovframework/com/uat/uia/EgovSecondFactor";
+ }
+
+ String methodType = methodTypeOf(svc);
+ model.addAttribute("methodType", methodType);
+
+ boolean ok = false;
+ try {
+ ok = svc.verify(user.getUniqId(), authCode);
+ } catch (Exception e) {
+ LOG.warn("[개인정보/#52] 2차 인증 검증 오류: " + e.toString());
+ ok = false;
+ }
+
+ if (ok) {
+ // 성공 — 세션 재발급(세션 고정 차단, #16) + 완료표식 세팅. attempt 카운터는 세션과 함께 폐기됨.
+ SessionRegenerator.onSecondFactorSuccess(request);
+ LOG.info("[개인정보/#14] 2차 인증 성공 — 세션 재발급, 목적지 이동: user=" + user.getId() + ", target=" + target);
+ return "redirect:" + target;
+ }
+
+ // 실패 — 시도횟수 증가 후 화면 재표시(계정열거/수단 노출 없이 일관 메시지)
+ session.setAttribute(ATTR_SF_ATTEMPT, Integer.valueOf(attempt + 1));
+ model.addAttribute("errorMsg", "인증코드가 올바르지 않습니다. 다시 시도해 주세요.");
+ return "egovframework/com/uat/uia/EgovSecondFactor";
+ }
+
+ // ────────────────────────────────────────────────────────── OTP 최소 등록(enrollment)
+
+ /**
+ * OTP 등록 화면(GET). 신규 비밀키를 발급·암호화 저장하고 프로비저닝 정보(secret/otpauth URI)를 표시한다.
+ *
+ *
주의: 매 진입 시 새 비밀키가 발급되므로, 표시된 secret으로 인증앱을 설정한 뒤
+ * 반드시 하단 최초검증까지 완료해야 한다. (기본 저장소 {@code InMemoryOtpSecretStore}는
+ * 애플리케이션 재기동 시 유실되므로 운영에서는 DB 저장소 구현체로 교체한다.)
+ */
+ @RequestMapping(value = "/uat/uia/secondFactorEnroll.do")
+ public String enrollView(HttpServletRequest request, ModelMap model) throws Exception {
+ LoginVO user = loginUser(request);
+ if (user == null) {
+ return "redirect:" + LOGIN_URL;
+ }
+
+ String otpauthUri = otpAuthService.provision(user.getUniqId());
+ model.addAttribute("otpauthUri", otpauthUri);
+ model.addAttribute("otpSecret", extractSecret(otpauthUri)); // 수동 입력용(QR 보조) Base32 secret
+ model.addAttribute("accountLabel", user.getId());
+ // otpauth URI 를 스캔 가능한 QR(자립형 data URI PNG)로 렌더 — 외부 리소스/네트워크 없음(ZXing 서버측 생성).
+ try {
+ model.addAttribute("otpQrDataUri", buildQrDataUri(otpauthUri, 220));
+ } catch (Exception e) {
+ // QR 생성 실패해도 수동 입력(secret)로 등록 가능하도록 화면은 계속 표시.
+ LOG.warn("[개인정보/#18] OTP QR 생성 실패 — 수동 입력으로 대체: " + e.toString());
+ }
+ return "egovframework/com/uat/uia/EgovSecondFactorEnroll";
+ }
+
+ /**
+ * OTP 등록 최초검증(POST). 발급한 비밀키로 첫 코드를 검증한다.
+ * 성공 시 곧바로 2차 인증 완료로 간주하고 세션을 재발급한 뒤 취급자 영역으로 이동한다.
+ */
+ @RequestMapping(value = "/uat/uia/secondFactorEnrollConfirm.do")
+ public String enrollConfirm(HttpServletRequest request, ModelMap model,
+ @RequestParam(value = "authCode", required = false) String authCode) throws Exception {
+
+ LoginVO user = loginUser(request);
+ if (user == null) {
+ return "redirect:" + LOGIN_URL;
+ }
+
+ boolean ok = false;
+ try {
+ ok = otpAuthService.verify(user.getUniqId(), authCode);
+ } catch (Exception e) {
+ LOG.warn("[개인정보/#18] OTP 등록 검증 오류: " + e.toString());
+ ok = false;
+ }
+
+ if (ok) {
+ SessionRegenerator.onSecondFactorSuccess(request);
+ LOG.info("[개인정보/#18] OTP 등록·최초검증 완료 — 세션 재발급: user=" + user.getId());
+ return "redirect:" + DEFAULT_TARGET;
+ }
+
+ model.addAttribute("enrollError", "인증코드가 올바르지 않습니다. 인증앱의 현재 코드로 다시 시도하거나 재발급하세요.");
+ return "egovframework/com/uat/uia/EgovSecondFactorEnroll";
+ }
+
+ // ────────────────────────────────────────────────────────── 내부 유틸
+
+ private LoginVO loginUser(HttpServletRequest request) {
+ HttpSession session = request.getSession(false);
+ if (session == null) {
+ return null;
+ }
+ Object o = session.getAttribute("loginVO");
+ return (o instanceof LoginVO) ? (LoginVO) o : null;
+ }
+
+ private int attemptCount(HttpSession session) {
+ Object o = session.getAttribute(ATTR_SF_ATTEMPT);
+ return (o instanceof Integer) ? ((Integer) o).intValue() : 0;
+ }
+
+ /** {@code isAvailable} 호출 시 구현체(스텁)가 던지는 예외를 흡수해 false로 취급. */
+ private boolean safeAvailable(AuthMethodService svc, String uniqId) {
+ try {
+ return svc.isAvailable(uniqId);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private String methodTypeOf(AuthMethodService svc) {
+ if (svc instanceof OtpAuthService) {
+ return "OTP";
+ }
+ if (svc instanceof SmsAuthService) {
+ return "SMS";
+ }
+ if (svc instanceof CertAuthService) {
+ return "CERT";
+ }
+ return "OTP";
+ }
+
+ /**
+ * 목적지 URL 화이트리스트 검증 — open-redirect 방지.
+ * 취급자 영역({@code /mnt/}) 내부 경로만 허용하고, 그 외/이상값은 기본 목적지로 대체한다.
+ */
+ private String sanitizeTarget(String returnUrl) {
+ if (returnUrl == null) {
+ return DEFAULT_TARGET;
+ }
+ String t = returnUrl.trim();
+ if (t.length() == 0) {
+ return DEFAULT_TARGET;
+ }
+ // 절대 URL/프로토콜 상대(//host)/역참조 차단 — 반드시 "/mnt/"로 시작하는 앱 내부 경로만 허용
+ if (!t.startsWith("/mnt/") || t.startsWith("//") || t.indexOf('\\') >= 0
+ || t.indexOf("://") >= 0 || t.indexOf("..") >= 0) {
+ return DEFAULT_TARGET;
+ }
+ return t;
+ }
+
+ /**
+ * otpauth URI를 스캔 가능한 QR 코드로 인코딩해 자립형 data URI(PNG, Base64)로 반환한다.
+ *
외부 리소스/네트워크 없이 서버측(ZXing, 기존 의존성)에서 생성한다 — 폐쇄망 전제.
+ * 반환값은 {@code data:image/png;base64,...} 형식으로 {@code }에 그대로 바인딩된다.
+ */
+ private String buildQrDataUri(String content, int size) throws Exception {
+ QRCodeWriter writer = new QRCodeWriter();
+ java.util.Map hints = new java.util.HashMap();
+ hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
+ hints.put(EncodeHintType.MARGIN, Integer.valueOf(1));
+ hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
+ BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size, hints);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ MatrixToImageWriter.writeToStream(matrix, "png", baos);
+ String b64 = new String(Base64.encodeBase64(baos.toByteArray()), "US-ASCII");
+ return "data:image/png;base64," + b64;
+ }
+
+ /** otpauth URI에서 secret 파라미터(Base32)만 추출한다(수동 입력 안내용). */
+ private String extractSecret(String otpauthUri) {
+ if (otpauthUri == null) {
+ return "";
+ }
+ int i = otpauthUri.indexOf("secret=");
+ if (i < 0) {
+ return "";
+ }
+ int start = i + "secret=".length();
+ int amp = otpauthUri.indexOf('&', start);
+ return (amp < 0) ? otpauthUri.substring(start) : otpauthUri.substring(start, amp);
+ }
+}
diff --git a/src/main/java/egovframework/com/uss/umt/service/impl/EgovAccountUnlockManageServiceImpl.java b/src/main/java/egovframework/com/uss/umt/service/impl/EgovAccountUnlockManageServiceImpl.java
index 79965a4..051d209 100644
--- a/src/main/java/egovframework/com/uss/umt/service/impl/EgovAccountUnlockManageServiceImpl.java
+++ b/src/main/java/egovframework/com/uss/umt/service/impl/EgovAccountUnlockManageServiceImpl.java
@@ -3,10 +3,17 @@ package egovframework.com.uss.umt.service.impl;
import java.util.List;
import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Service;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
import egovframework.com.cmm.LoginVO;
+import egovframework.com.cmm.privacy.auditlog.ClientIpResolver;
+import egovframework.com.cmm.privacy.lock.AccountUnlockHistoryDAO;
+import egovframework.com.cmm.privacy.lock.AccountUnlockHistoryVO;
+import egovframework.com.cmm.util.EgovUserDetailsHelper;
import egovframework.com.uat.uia.service.impl.LoginDAO;
import egovframework.com.uss.umt.service.DiapauseManageVO;
import egovframework.com.uss.umt.service.EgovAccountUnlockManageService;
@@ -15,17 +22,21 @@ import egovframework.rte.fdl.cmmn.AbstractServiceImpl;
@Service("egovAccountUnlockManageService")
public class EgovAccountUnlockManageServiceImpl extends AbstractServiceImpl implements EgovAccountUnlockManageService{
-
+
/** diapauseManageDAO */
@Resource(name="diapauseManageDAO")
- private DiapauseManageDAO diapauseManageDAO;
-
+ private DiapauseManageDAO diapauseManageDAO;
+
//@Resource(name = "loginService")
- //protected EgovLoginService egovLoginService;
-
+ //protected EgovLoginService egovLoginService;
+
@Resource(name="loginDAO")
private LoginDAO loginDAO;
+ /** 개인정보보호(SER-001 / #24) 계정 잠금 해제 이력 DAO */
+ @Resource(name="accountUnlockHistoryDAO")
+ private AccountUnlockHistoryDAO accountUnlockHistoryDAO;
+
@Override
public List selectUserLockCheckList(UserDefaultVO userSearchVO) {
return diapauseManageDAO.selectUserLockCheckList(userSearchVO);
@@ -40,41 +51,88 @@ public class EgovAccountUnlockManageServiceImpl extends AbstractServiceImpl imp
public void updateUnlock(String user_id, String user_gubun) {
String [] sUser_id = user_id.split(",");
String [] sUser_gubun = user_gubun.split(",");
-
+
DiapauseManageVO vo = new DiapauseManageVO();
LoginVO loginVo = new LoginVO();
- for (int i=0; i※ 스케줄 활성화 주의: 본 프로젝트는 {@code }이
+ * context-common.xml/egov-com-servlet.xml 양쪽 모두 주석 처리되어 있어 현재
+ * {@code @Scheduled}가 동작하지 않는다(배치는 OS crontab 운영). 활성화 시 기존 휴면/메일 등
+ * 휴면 배치가 함께 기동되므로, 활성화 여부·경합은 운영과 협의해 결정한다.
+ *
+ * @throws Exception 파기 오류
+ */
+ @Scheduled(cron = "0 0 4 1 * ?")
+ public void purgeAuthorChangeHistoryScheduler() throws Exception {
+ int purged = privacyBatchService.purgeExpiredAuthorChangeHistory();
+ log.info("[개인정보/#5] 권한변경이력 파기 배치 완료 — " + purged + "건");
+ }
+
+ /**
+ * [개인정보보호 #40] 개인정보 접속기록(PERSONAL_DATA_ACCESS_LOG) 해시체인 무결성 검증 배치.
+ * 매월 1일 03:00, 적재 순서대로 재계산해 위·변조를 탐지하고 불일치 시 즉시 관리자 통보.
+ * (스케줄 활성화 주의는 {@link #purgeAuthorChangeHistoryScheduler()} 참조.)
+ *
+ * @throws Exception 검증 오류
+ */
+ @Scheduled(cron = "0 0 3 1 * ?")
+ public void verifyAccessLogHashChainScheduler() throws Exception {
+ HashChainVerifyResultVO result = privacyBatchService.verifyAccessLogHashChain();
+ log.info("[개인정보/#40] 접속기록 무결성 검증 배치 완료 — 총 " + result.getTotalCount()
+ + "건, 결과=" + (result.isOk() ? "정상" : "위반(LOG_ID=" + result.getMismatchLogId() + ")"));
+ }
+
+ /**
+ * [개인정보보호 #37] 개인정보 접속기록(PERSONAL_DATA_ACCESS_LOG) 보관정책 파기 배치.
+ * 매월 1일 02:00(저부하 시간대), 보관기간(730일) 경과 파티션을 DROP(개별 DELETE 미사용)하고
+ * 미래 월 파티션을 선행 증설한다. COMTNSYSLOG 7일 삭제정책은 이 테이블에 적용하지 않는다(#38).
+ * (스케줄 활성화 주의는 {@link #purgeAuthorChangeHistoryScheduler()} 참조 — 현재 전역 비활성.)
+ *
+ * @throws Exception 파기/증설 오류
+ */
+ @Scheduled(cron = "0 0 2 1 * ?")
+ public void purgeAccessLogPartitionScheduler() throws Exception {
+ int dropped = privacyBatchService.purgeExpiredAccessLogPartitions();
+ log.info("[개인정보/#37] 접속기록 파티션 파기/증설 배치 완료 — 파기 " + dropped + "개 파티션");
+ }
+
+ /**
+ * [개인정보보호 #43] 개인정보 접속기록 정기점검 리포트 생성 배치.
+ * 매월 1일 05:00(기존 파기 02:00/무결성 03:00/이력파기 04:00 배치와 시각 분리).
+ * 전월분 접속기록을 집계(총건수·실패·무결성#40·취급자별 처리현황)해 PDAL_INSPECTION_REPORT 적재.
+ *
+ *
※ 스케줄 활성화 주의: 전역 {@code } 비활성 상태라
+ * 현재 {@code @Scheduled}는 동작하지 않는다(OS crontab 운영). 활성화·경합은 운영과 협의.
+ *
+ * @throws Exception 집계/적재 오류
+ */
+ @Scheduled(cron = "0 0 5 1 * ?")
+ public void generateInspectionReportScheduler() throws Exception {
+ InspectionReportVO report = privacyBatchService.generateMonthlyInspectionReport();
+ log.info("[개인정보/#43] 정기점검 리포트 생성 배치 완료 — " + report.getRptYm()
+ + " 총 " + report.getTotalLogCnt() + "건, 무결성=" + report.getIntegrityResult());
+ }
+
+ /**
+ * [개인정보보호 #10] 휴면 전 사전 알림 배치 — D-30/D-7/D-Day 3단계 + 관리자 알림.
+ * 매일 06:00, 정책(ACCOUNT_MNG_POLICY)의 ALERT_D1/D2/D3 시점에 해당하는 휴면 예정
+ * 계정을 본인·관리자에게 사전 통지한다. 발송 실패는 fail-safe(로깅 후 계속).
+ *
+ *
※ 전역 {@code } 비활성 상태라 현재 @Scheduled 는 동작하지
+ * 않는다(OS crontab 운영). 활성화·경합은 운영과 협의(핸드오프).
+ *
+ * @throws Exception 알림 처리 오류
+ */
+ @Scheduled(cron = "0 0 6 * * ?")
+ public void dormantPreAlertScheduler() throws Exception {
+ int sent = dormantAlertService.sendDormantPreAlerts();
+ log.info("[개인정보/#10] 휴면 사전알림 배치 완료 — " + sent + "건 발송");
+ }
+
}
\ No newline at end of file
diff --git a/src/main/resources/egovframework/spring/com/context-egovuserdetailshelper.xml b/src/main/resources/egovframework/spring/com/context-egovuserdetailshelper.xml
index 9072897..e187388 100644
--- a/src/main/resources/egovframework/spring/com/context-egovuserdetailshelper.xml
+++ b/src/main/resources/egovframework/spring/com/context-egovuserdetailshelper.xml
@@ -32,8 +32,11 @@
-
-
+
+
+
+
+
diff --git a/src/main/resources/egovframework/spring/com/context-syslogaop.xml b/src/main/resources/egovframework/spring/com/context-syslogaop.xml
index 651340f..2b09386 100644
--- a/src/main/resources/egovframework/spring/com/context-syslogaop.xml
+++ b/src/main/resources/egovframework/spring/com/context-syslogaop.xml
@@ -24,4 +24,39 @@
-->
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/egovframework/sqlmap/com/uat/uia/EgovLoginUsr_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/com/uat/uia/EgovLoginUsr_SQL_Mysql.xml
index 4bc609a..0c2aab6 100644
--- a/src/main/resources/egovframework/sqlmap/com/uat/uia/EgovLoginUsr_SQL_Mysql.xml
+++ b/src/main/resources/egovframework/sqlmap/com/uat/uia/EgovLoginUsr_SQL_Mysql.xml
@@ -185,6 +185,119 @@
]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화(미설정 시 365) */
+ AND a.MBER_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
+ AND a.MBER_STTUS = 'P'
+ ]]>
+
+
+
+
+ (SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화(미설정 시 365) */
+ AND e.EMPLYR_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
+ AND e.EMPLYR_STTUS_CODE = 'P'
+ ]]>
+
+
+
+
+
+
+
+
+
+
+ UPDATE COMTNDIAPAUSEMBER SET DIAPAUSE_STAT = #diapause_stat# WHERE MBER_ID = #id#
+
+
+
+
+
+
+
+ UPDATE COMTNDIAPAUSEMBER
+ SET TOKEN = #token#
+ , TOKEN_EXPDT = DATE_ADD(NOW(), INTERVAL 10 MINUTE)
+ WHERE MBER_ID = #mber_id#
+ AND ESNTL_ID = #esntl_id#
+
+
365
+ WHERE NVL(SYSDATE - b.CREAT_DT,SYSDATE - a.SBSCRB_DE) > (SELECT NVL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화 */
AND a.MBER_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
AND a.MBER_STTUS = 'P'
]]>
+
+
+
+ (SELECT NVL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY) /* No.9 정책값화 */
+ AND e.EMPLYR_ID NOT IN (SELECT MBER_ID FROM COMTNDIAPAUSEMBER)
+ AND e.EMPLYR_STTUS_CODE = 'P'
+ ]]>
+
+
+
+
= DIAPAUSE_DT)
+ WHERE A.MBER_TY_CODE = 'GNR'
+ AND (DIAPAUSE_STAT IS NULL OR (DIAPAUSE_STAT = 'S' AND SYSDATE >= DIAPAUSE_DT))
diff --git a/src/main/resources/egovframework/sqlmap/config/mysql/sql-map-config-mysql-0-site.xml b/src/main/resources/egovframework/sqlmap/config/mysql/sql-map-config-mysql-0-site.xml
index a77b364..a2f6df5 100644
--- a/src/main/resources/egovframework/sqlmap/config/mysql/sql-map-config-mysql-0-site.xml
+++ b/src/main/resources/egovframework/sqlmap/config/mysql/sql-map-config-mysql-0-site.xml
@@ -99,4 +99,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLogIntegrity_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLogIntegrity_SQL_Mysql.xml
new file mode 100644
index 0000000..1c49341
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLogIntegrity_SQL_Mysql.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLogIntegrity_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLogIntegrity_SQL_Oracle.xml
new file mode 100644
index 0000000..4a8131c
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLogIntegrity_SQL_Oracle.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLog_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLog_SQL_Mysql.xml
new file mode 100644
index 0000000..919b0ae
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLog_SQL_Mysql.xml
@@ -0,0 +1,358 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ALTER TABLE PERSONAL_DATA_ACCESS_LOG DROP PARTITION $value$
+
+
+
+
+ ALTER TABLE PERSONAL_DATA_ACCESS_LOG
+ ADD PARTITION (PARTITION $partitionName$ VALUES LESS THAN ('$boundaryDt$'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLog_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLog_SQL_Oracle.xml
new file mode 100644
index 0000000..94842e7
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccessLog_SQL_Oracle.xml
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ALTER TABLE PERSONAL_DATA_ACCESS_LOG DROP PARTITION $value$
+
+
+
+ ALTER TABLE PERSONAL_DATA_ACCESS_LOG
+ ADD PARTITION $partitionName$ VALUES LESS THAN (TO_DATE('$boundaryDt$', 'YYYY-MM-DD'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountMng_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountMng_SQL_Mysql.xml
new file mode 100644
index 0000000..e40bd44
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountMng_SQL_Mysql.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountMng_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountMng_SQL_Oracle.xml
new file mode 100644
index 0000000..832df1a
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountMng_SQL_Oracle.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountUnlockHistory_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountUnlockHistory_SQL_Mysql.xml
new file mode 100644
index 0000000..177d80a
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountUnlockHistory_SQL_Mysql.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountUnlockHistory_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountUnlockHistory_SQL_Oracle.xml
new file mode 100644
index 0000000..cf467a6
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAccountUnlockHistory_SQL_Oracle.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorHistoryPurge_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorHistoryPurge_SQL_Mysql.xml
new file mode 100644
index 0000000..ba425e9
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorHistoryPurge_SQL_Mysql.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorHistoryPurge_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorHistoryPurge_SQL_Oracle.xml
new file mode 100644
index 0000000..497a74f
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorHistoryPurge_SQL_Oracle.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorWorkflow_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorWorkflow_SQL_Mysql.xml
new file mode 100644
index 0000000..a97303f
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorWorkflow_SQL_Mysql.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorWorkflow_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorWorkflow_SQL_Oracle.xml
new file mode 100644
index 0000000..4017a81
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthorWorkflow_SQL_Oracle.xml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthority_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthority_SQL_Mysql.xml
new file mode 100644
index 0000000..e526198
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthority_SQL_Mysql.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthority_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthority_SQL_Oracle.xml
new file mode 100644
index 0000000..50876ab
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyAuthority_SQL_Oracle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyJobMapping_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyJobMapping_SQL_Mysql.xml
new file mode 100644
index 0000000..ea3185c
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyJobMapping_SQL_Mysql.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyJobMapping_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyJobMapping_SQL_Oracle.xml
new file mode 100644
index 0000000..e771490
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyJobMapping_SQL_Oracle.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Mysql.xml
new file mode 100644
index 0000000..5e7d89c
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Mysql.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Oracle.xml
new file mode 100644
index 0000000..91edfcd
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyMenuAuth_SQL_Oracle.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyOtpDevice_SQL_Mysql.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyOtpDevice_SQL_Mysql.xml
new file mode 100644
index 0000000..a5c682b
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyOtpDevice_SQL_Mysql.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyOtpDevice_SQL_Oracle.xml b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyOtpDevice_SQL_Oracle.xml
new file mode 100644
index 0000000..8c68204
--- /dev/null
+++ b/src/main/resources/egovframework/sqlmap/site/mnt/biz/privacy/PrivacyOtpDevice_SQL_Oracle.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/config/egovframework/springmvc/egov-com-servlet.xml b/src/main/webapp/WEB-INF/config/egovframework/springmvc/egov-com-servlet.xml
index b036377..0cf783e 100644
--- a/src/main/webapp/WEB-INF/config/egovframework/springmvc/egov-com-servlet.xml
+++ b/src/main/webapp/WEB-INF/config/egovframework/springmvc/egov-com-servlet.xml
@@ -64,11 +64,22 @@
-
+
+
+
+
+
+
@@ -94,6 +105,107 @@
+
+
+
+
+
+
+
+
+
+ \A/mnt/(main|left|top|right|footer)\.do.*\Z
+ \A/mnt/(include)/.*\.do.*\Z
+ \A/uat/uia/.*\Z
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \A/mnt/(main|left|top|right|footer)\.do.*\Z
+ \A/mnt/(include)/.*\.do.*\Z
+ \A/uat/uia/.*\Z
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ROLE_ADMIN
+ ROLE_ADMINS
+
+
+
+
+ 30
diff --git a/src/script/mysql/ddl/privacy.account_mng_policy_create_mysql.sql b/src/script/mysql/ddl/privacy.account_mng_policy_create_mysql.sql
new file mode 100644
index 0000000..8aa95ec
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.account_mng_policy_create_mysql.sql
@@ -0,0 +1,34 @@
+-- =====================================================================
+-- 계정 관리 정책 : ACCOUNT_MNG_POLICY (단일행 정책 테이블)
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 장기 미사용 계정 관리)
+-- 개발항목 #13(정책 설정) + #9(휴면 기준일수 정책값화) - 담당: arko-privacy-authctrl (Fork A)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [용도] 휴면 기준일수·사전알림 시점·삭제 보존기간·배치주기 등 계정 관리 정책을
+-- 하드코딩이 아닌 관리자 설정값으로 운영하기 위한 단일행 정책 테이블.
+-- [#9] 휴면 select(EgovLoginUsr_SQL_Mysql.xml)의 365 하드코딩을
+-- (SELECT DORMANT_DAY FROM ACCOUNT_MNG_POLICY) 로 대체(미설정 시 365 fallback).
+-- [#10] ALERT_D1/D2/D3(기본 30/7/0) = 휴면예정 D-30/D-7/D-Day 사전알림 시점.
+-- ADMIN_EMAIL = 사전알림 관리자 수신 주소.
+-- =====================================================================
+
+CREATE TABLE IF NOT EXISTS ACCOUNT_MNG_POLICY (
+ POLICY_ID INT NOT NULL DEFAULT 1 COMMENT '정책 식별자(단일행=1)',
+ DORMANT_DAY INT NOT NULL DEFAULT 365 COMMENT '휴면 전환 기준 미접속 일수(#9)',
+ ALERT_D1 INT NOT NULL DEFAULT 30 COMMENT '사전알림 1차(휴면 D-일수)',
+ ALERT_D2 INT NOT NULL DEFAULT 7 COMMENT '사전알림 2차(휴면 D-일수)',
+ ALERT_D3 INT NOT NULL DEFAULT 0 COMMENT '사전알림 3차(D-Day, 0=당일)',
+ DELETE_PRESERVE_DAY INT NOT NULL DEFAULT 365 COMMENT '휴면/탈퇴 후 삭제 보존기간(일)',
+ BATCH_CYCLE VARCHAR(20) NOT NULL DEFAULT 'DAILY' COMMENT '배치 주기(DAILY/WEEKLY/MONTHLY)',
+ ADMIN_EMAIL VARCHAR(200) NULL COMMENT '사전알림 관리자 수신 이메일',
+ UPD_ID VARCHAR(20) NULL COMMENT '최종 수정자',
+ UPD_DT DATETIME NULL COMMENT '최종 수정일시',
+ PRIMARY KEY (POLICY_ID)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='계정 관리 정책(개인정보보호 SFR-002, 단일행)';
+
+-- 기본 정책 시드(없을 때만). 기준 365일·알림 D-30/7/0·보존 365일.
+INSERT INTO ACCOUNT_MNG_POLICY (POLICY_ID, DORMANT_DAY, ALERT_D1, ALERT_D2, ALERT_D3, DELETE_PRESERVE_DAY, BATCH_CYCLE, UPD_ID, UPD_DT)
+SELECT 1, 365, 30, 7, 0, 365, 'DAILY', 'SYSTEM', NOW()
+ FROM DUAL
+ WHERE NOT EXISTS (SELECT 1 FROM ACCOUNT_MNG_POLICY WHERE POLICY_ID = 1);
diff --git a/src/script/mysql/ddl/privacy.account_unlock_history_create_mysql.sql b/src/script/mysql/ddl/privacy.account_unlock_history_create_mysql.sql
new file mode 100644
index 0000000..b5ada4b
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.account_unlock_history_create_mysql.sql
@@ -0,0 +1,38 @@
+-- =====================================================================
+-- 계정 잠금 해제 이력 : ACCOUNT_UNLOCK_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001, 계정 잠금 정책 정비)
+-- 개발항목 #24 - 담당: arko-privacy-authctrl (Fork A)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 잠긴 계정 해제를 DB 직접조작이 아니라 관리자 화면(EgovAccountUnlockManage)
+-- 에서 처리하고, 해제자·해제일시·대상·사유를 이력으로 남긴다.
+-- [무결성] 애플리케이션 계층에 UPDATE/DELETE statement 미정의(INSERT/SELECT 한정).
+-- =====================================================================
+
+CREATE TABLE ACCOUNT_UNLOCK_HISTORY (
+ UNLOCK_HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '해제 이력 일련번호(PK)',
+ TRGET_ID VARCHAR(50) NULL COMMENT '해제 대상 계정 ID(전체해제 시 NULL 가능)',
+ TRGET_SE VARCHAR(10) NULL COMMENT '대상 회원구분 GNR/GRO/ENT/USR',
+ UNLOCK_TY CHAR(1) NOT NULL DEFAULT 'I' COMMENT '해제 유형 I:개별 A:전체',
+ UNLOCK_USER_ID VARCHAR(50) NULL COMMENT '해제 처리자(관리자) ID',
+ UNLOCK_RESN VARCHAR(500) NULL COMMENT '해제 사유',
+ UNLOCK_IP VARCHAR(45) NULL COMMENT '해제자 접속 IP(IPv6 대응)',
+ UNLOCK_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '해제 일시',
+ PRIMARY KEY (UNLOCK_HIST_ID),
+ 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
+ COMMENT='계정 잠금 해제 이력(개인정보보호 SER-001 #24)';
+
+
+-- =====================================================================
+-- 계정 잠금 임계값 10회 → 5회 (#24)
+-- 임계값은 하드코딩이 아니라 LOGIN_LOCK_COUNT.LOCK_COUNT(단일행) 데이터에서 조회됨
+-- (loginDAO.selectFailTotalCount). 아래 UPDATE 로 운영 정책값을 5회로 조정한다.
+-- ※ 소스(EgovLoginController)에는 값 부재 시 기본 5회 fallback 이 반영되어 있음.
+-- =====================================================================
+UPDATE LOGIN_LOCK_COUNT SET LOCK_COUNT = 5;
+-- 행이 없을 경우 대비(최초 세팅):
+-- INSERT INTO LOGIN_LOCK_COUNT (LOCK_COUNT) SELECT 5 FROM DUAL
+-- WHERE NOT EXISTS (SELECT 1 FROM LOGIN_LOCK_COUNT);
diff --git a/src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql b/src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql
new file mode 100644
index 0000000..3c49e77
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql
@@ -0,0 +1,37 @@
+-- =====================================================================
+-- 권한 변경 이력 테이블 : AUTHOR_CHANGE_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-001, 역할별 차등 접근/권한 변경 추적)
+-- 개발항목 #4 - 담당: arko-privacy-authctrl (Fork A → 오케스트레이터 완료)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 권한 신설·변경·회수(#1 getAuthorities 대상 COMTNEMPLYRSCRTYESTBS 등)의
+-- 모든 변경을 변경자·변경일시·변경전/후 권한과 함께 이력으로 적재한다.
+-- 권한 신청·승인 워크플로(#3)의 승인 반영 시점에 권한반영과 단일 트랜잭션으로 적재.
+--
+-- [보관·파기] 3년(1,095일) 이상 보관 전제. 월 1회 @Scheduled 파기 배치(#5)가
+-- 보관기간(1,095일) 경과 이력만 선별 파기한다. 개별 삭제 API 미제공.
+-- 다운로드 시 사유 입력 필수 + 다운로드 이력 적재(#6)는 별도 처리.
+--
+-- [무결성] 이력 위·변조 억제를 위해 변경전/후 권한을 함께 보관(사후 대사 가능).
+-- 애플리케이션 계층에 UPDATE statement 미정의(INSERT/SELECT/파기 DELETE 한정).
+-- =====================================================================
+
+CREATE TABLE AUTHOR_CHANGE_HISTORY (
+ HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호(PK)',
+ TRGET_UNIQ_ID VARCHAR(20) NOT NULL COMMENT '변경 대상자 고유식별자(ESNTL_ID)',
+ TRGET_ID VARCHAR(20) NULL COMMENT '변경 대상자 계정ID(참고용)',
+ CHG_SE_CODE CHAR(1) NOT NULL COMMENT '변경구분 G:부여(grant) R:회수(revoke) M:변경(modify)',
+ BEFORE_AUTHOR VARCHAR(1000) NULL COMMENT '변경 전 권한(코드 CSV 또는 스냅샷)',
+ AFTER_AUTHOR VARCHAR(1000) NULL COMMENT '변경 후 권한(코드 CSV 또는 스냅샷)',
+ CHG_RESN VARCHAR(500) NULL COMMENT '변경 사유(신청/승인 워크플로 연계 #3)',
+ APROVL_ID VARCHAR(20) NULL COMMENT '승인자 계정ID(승인 워크플로 #3)',
+ CHANGER_UNIQ_ID VARCHAR(20) NOT NULL COMMENT '변경자(처리자) 고유식별자',
+ CHANGER_ID VARCHAR(20) NULL COMMENT '변경자 계정ID(참고용)',
+ CHANGER_IP VARCHAR(45) NULL COMMENT '변경자 접속 IP(IPv6 대응)',
+ CHG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '변경 일시',
+ PRIMARY KEY (HIST_ID),
+ 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
+ COMMENT='권한 변경 이력(개인정보보호 SFR-001, 3년 이상 보관)';
diff --git a/src/script/mysql/ddl/privacy.author_workflow_create_mysql.sql b/src/script/mysql/ddl/privacy.author_workflow_create_mysql.sql
new file mode 100644
index 0000000..486e44b
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.author_workflow_create_mysql.sql
@@ -0,0 +1,47 @@
+-- =====================================================================
+-- 권한 신청·승인 워크플로 + 권한이력 다운로드 이력
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-001, 권한 신설·변경·회수 승인통제)
+-- 개발항목 #3(신청·승인 워크플로) / #6(권한이력 조회·다운로드) - 담당: arko-privacy-authctrl (Fork A 6차)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [#3] AUTHOR_CHANGE_REQUEST : 권한 변경 신청→승인/반려 워크플로.
+-- 승인(A) 시 권한 반영(COMTNEMPLYRSCRTYESTBS)과 이력 적재(AUTHOR_CHANGE_HISTORY)를
+-- 단일 트랜잭션으로 처리한다(egovframework.com..*Impl AOP tx advisor).
+-- [#6] AUTHOR_HIST_DOWNLOAD_HISTORY : 권한이력 다운로드 증빙(사유 필수).
+-- =====================================================================
+
+CREATE TABLE AUTHOR_CHANGE_REQUEST (
+ REQUEST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '신청 일련번호(PK)',
+ TRGET_UNIQ_ID VARCHAR(20) NOT NULL COMMENT '권한변경 대상자 고유식별자(ESNTL_ID)',
+ TRGET_ID VARCHAR(20) NULL COMMENT '권한변경 대상자 계정ID(참고용)',
+ REQUEST_SE CHAR(1) NOT NULL COMMENT '신청구분 N:신설(부여) C:변경 R:회수',
+ BEFORE_AUTHOR VARCHAR(1000) NULL COMMENT '신청 시점 현재 권한(스냅샷)',
+ AFTER_AUTHOR VARCHAR(1000) NULL COMMENT '신청 목표 권한(코드/CSV)',
+ REQUEST_RESN VARCHAR(500) NOT NULL COMMENT '신청 사유(필수)',
+ REQSTER_ID VARCHAR(20) NOT NULL COMMENT '신청자 계정ID',
+ REQSTER_UNIQ_ID VARCHAR(20) NULL COMMENT '신청자 고유식별자',
+ REQUEST_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '신청 일시',
+ CONFM_STTUS CHAR(1) NOT NULL DEFAULT 'R' COMMENT '처리상태 R:신청 A:승인 J:반려',
+ CONFM_ID VARCHAR(20) NULL COMMENT '승인/반려 처리자 계정ID',
+ CONFM_UNIQ_ID VARCHAR(20) NULL COMMENT '승인/반려 처리자 고유식별자',
+ CONFM_DT DATETIME NULL COMMENT '승인/반려 일시',
+ CONFM_RESN VARCHAR(500) NULL COMMENT '반려 사유',
+ 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
+ COMMENT='권한 변경 신청·승인 워크플로(개인정보보호 SFR-001 #3)';
+
+CREATE TABLE AUTHOR_HIST_DOWNLOAD_HISTORY (
+ DWLD_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '다운로드 일련번호(PK)',
+ DOWNLOADER_ID VARCHAR(20) NOT NULL COMMENT '다운로드자 계정ID',
+ DOWNLOADER_UNIQ_ID VARCHAR(20) NULL COMMENT '다운로드자 고유식별자',
+ DOWNLOADER_IP VARCHAR(45) NULL COMMENT '다운로드자 접속 IP(IPv6 대응)',
+ DWLD_RESN VARCHAR(500) NOT NULL COMMENT '다운로드 사유(필수·미입력 차단)',
+ SEARCH_COND VARCHAR(1000) NULL COMMENT '다운로드 시 검색조건 스냅샷',
+ DWLD_CNT INT NOT NULL DEFAULT 0 COMMENT '다운로드 건수',
+ 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
+ COMMENT='권한이력 다운로드 증빙 이력(개인정보보호 SFR-001 #6)';
diff --git a/src/script/mysql/ddl/privacy.diapause_emplyr_extend_mysql.sql b/src/script/mysql/ddl/privacy.diapause_emplyr_extend_mysql.sql
new file mode 100644
index 0000000..be142c2
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.diapause_emplyr_extend_mysql.sql
@@ -0,0 +1,25 @@
+-- =====================================================================
+-- 휴면(장기 미사용) 대상 확장 : COMTNDIAPAUSEMBER 회원유형 구분 컬럼 추가
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 장기 미사용 계정 관리)
+-- 개발항목 #7 - 담당: arko-privacy-authctrl (Fork A)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [배경] 기존 휴면 대상은 일반회원(COMTNGNRLMBER)뿐이며, 휴면 테이블
+-- COMTNDIAPAUSEMBER 에는 회원유형 구분 컬럼이 없다. 개인정보취급자
+-- (COMTNEMPLYRINFO)를 휴면 대상에 포함(#7)하면서 일반회원 휴면/해제
+-- 흐름과 혼선되지 않도록 회원유형 구분 컬럼을 신설한다.
+--
+-- [안전] DEFAULT 'GNR'(일반회원)로 신설 → 기존 적재 행은 자동 'GNR' 처리되어
+-- 기존 selectDiapauseList(일반회원 join COMTNGNRLMBER) 흐름 무영향.
+-- 취급자 휴면 행은 'EMP'로 적재하여 별도 select/notify 경로로 처리한다.
+--
+-- [멱등] 재실행 안전을 위해 컬럼 존재 시 무시(수동 확인 후 실행).
+-- =====================================================================
+
+ALTER TABLE COMTNDIAPAUSEMBER
+ ADD COLUMN MBER_TY_CODE CHAR(3) NOT NULL DEFAULT 'GNR'
+ COMMENT '회원유형 GNR:일반회원 EMP:개인정보취급자';
+
+-- 조회 성능(취급자/일반 분리 조회) 보조 인덱스
+ALTER TABLE COMTNDIAPAUSEMBER
+ ADD INDEX IX_DIAPAUSE_MBERTY (MBER_TY_CODE, DIAPAUSE_STAT);
diff --git a/src/script/mysql/ddl/privacy.download_history_create_mysql.sql b/src/script/mysql/ddl/privacy.download_history_create_mysql.sql
new file mode 100644
index 0000000..6ce6aa8
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.download_history_create_mysql.sql
@@ -0,0 +1,30 @@
+-- =====================================================================
+-- 개인정보 접속기록 다운로드 이력 : PDAL_DOWNLOAD_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / SER-002)
+-- 개발항목 #42 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 개인정보 접속기록(PERSONAL_DATA_ACCESS_LOG) 조회결과 다운로드 시,
+-- 다운로드 사유(필수)·다운로드자·일시·IP·건수를 이력으로 남긴다.
+-- 사유 미입력 시 다운로드는 서비스 계층에서 차단(본 테이블 미적재).
+-- [원칙] 개인정보 본문 미적재 — 다운로드 검색조건 요약(SEARCH_COND)만 보관.
+-- 이 테이블은 INSERT/SELECT 만 수행(위·변조 방지, UPDATE/DELETE 미정의).
+-- =====================================================================
+
+CREATE TABLE PDAL_DOWNLOAD_HISTORY (
+ DL_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '다운로드 이력 일련번호(PK)',
+ DOWNLOADER_ID VARCHAR(20) NOT NULL COMMENT '다운로드자 계정ID',
+ DOWNLOAD_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '다운로드 일시',
+ DOWNLOADER_IP VARCHAR(45) NULL COMMENT '다운로드자 접속 IP(IPv6 대응)',
+ DL_REASON VARCHAR(500) NOT NULL COMMENT '다운로드 사유(필수 — 미입력 시 차단)',
+ SEARCH_COND VARCHAR(1000) NULL COMMENT '다운로드 검색조건 요약(개인정보 본문 미포함)',
+ RESULT_CNT INT NOT NULL DEFAULT 0 COMMENT '다운로드 건수',
+ REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '등록 일시',
+ PRIMARY KEY (DL_ID),
+ KEY IX_PDDH_DT (DOWNLOAD_DT),
+ KEY IX_PDDH_USER (DOWNLOADER_ID, DOWNLOAD_DT)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='개인정보 접속기록 다운로드 이력(#42, SER-002)';
+
+-- [Oracle] 시퀀스 필요:
+-- CREATE SEQUENCE PDAL_DOWNLOAD_HISTORY_SEQ START WITH 1 INCREMENT BY 1 NOCACHE;
diff --git a/src/script/mysql/ddl/privacy.inspection_report_create_mysql.sql b/src/script/mysql/ddl/privacy.inspection_report_create_mysql.sql
new file mode 100644
index 0000000..2ae17c1
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.inspection_report_create_mysql.sql
@@ -0,0 +1,37 @@
+-- =====================================================================
+-- 개인정보 접속기록 정기점검 리포트 : PDAL_INSPECTION_REPORT
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / SER-002)
+-- 개발항목 #43 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 월 1회 정기점검 배치가 전월분 접속기록을 집계해 1행으로 적재한다.
+-- "월 1회 이상 점검 내역"을 화면 출력으로 제출(개인정보보호위 점검 대응).
+--
+-- [집계원] PERSONAL_DATA_ACCESS_LOG(#30) + 해시체인 무결성 검증(#40) 재사용.
+-- [자리표시] 이상징후 5유형(#35)·다운로드 이력(#42)은 미구현 → 컬럼만 두고 배치가 0/요약만 적재.
+-- [화면입력] INSPECTOR_ID/INSPECT_RESULT/ACTN_MATTER 는 배치가 아닌 점검자가 화면(#48)에서 입력.
+-- =====================================================================
+
+CREATE TABLE PDAL_INSPECTION_REPORT (
+ RPT_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '리포트 일련번호(PK)',
+ RPT_YM CHAR(6) NOT NULL COMMENT '점검 대상 연월(전월, yyyyMM)',
+ PERIOD_FROM DATETIME NOT NULL COMMENT '집계 시작일시',
+ PERIOD_TO DATETIME NOT NULL COMMENT '집계 종료일시',
+ TOTAL_LOG_CNT INT NOT NULL DEFAULT 0 COMMENT '총 접속기록 건수',
+ FAIL_CNT INT NOT NULL DEFAULT 0 COMMENT '처리결과 F(실패/차단) 건수 = 이상징후 일부',
+ ABNORMAL_SUMMARY VARCHAR(2000) NULL COMMENT '이상징후 5유형 요약(#35 연계 전 자리표시)',
+ INTEGRITY_RESULT VARCHAR(20) NOT NULL DEFAULT 'OK' COMMENT '해시체인 무결성: OK / VIOLATION',
+ INTEGRITY_MISMATCH_ID BIGINT NULL COMMENT '최초 불일치 LOG_ID(위반 시)',
+ INTEGRITY_TOTAL_CNT INT NOT NULL DEFAULT 0 COMMENT '무결성 검증 대상 건수',
+ HANDLER_SUMMARY VARCHAR(4000) NULL COMMENT '취급자별 처리현황 요약',
+ DOWNLOAD_CNT INT NOT NULL DEFAULT 0 COMMENT '다운로드 이력 건수(#42 연계 전 0)',
+ INSPECTOR_ID VARCHAR(20) NULL COMMENT '점검자 계정ID(화면 입력, #44)',
+ INSPECT_RESULT VARCHAR(500) NULL COMMENT '점검결과(화면 입력, #44)',
+ ACTN_MATTER VARCHAR(1000) NULL COMMENT '조치사항(화면 입력, #44)',
+ INSPECT_DT DATETIME NULL COMMENT '점검일시(점검결과 입력 시각, #44 감사증빙)',
+ REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '리포트 생성일시',
+ PRIMARY KEY (RPT_ID),
+ UNIQUE KEY UX_PIR_YM (RPT_YM),
+ KEY IX_PIR_REGDT (REG_DT)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='개인정보 접속기록 정기점검 리포트(#43, 월 1회)';
diff --git a/src/script/mysql/ddl/privacy.job_mapping_create_mysql.sql b/src/script/mysql/ddl/privacy.job_mapping_create_mysql.sql
new file mode 100644
index 0000000..5a0b6f0
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.job_mapping_create_mysql.sql
@@ -0,0 +1,53 @@
+-- =====================================================================
+-- 개인정보 처리 업무유형 매핑 테이블 : PDAL_JOB_MAPPING
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / SER-002)
+-- 개발항목 #32 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 접속기록 수집(#31 PersonalDataLogAspect)이 가로챈 서비스 메서드를
+-- 업무코드(JOB_SE_CODE)·업무명(JOB_SE_NM)으로 매핑한다.
+-- AOP pointcut 범위(개인정보 처리 서비스 패키지) 안에서 신규 화면이 추가되면
+-- 이 테이블에 행만 등록하면 수집되며, Aspect/pointcut 소스 수정이 불요하다.
+--
+-- [매칭 규칙] Aspect가 (대상클래스 단순명, 메서드명)으로 조회.
+-- - TARGET_CLASS : 서비스 impl 단순 클래스명(예: EgovMberManageServiceImpl) 또는 FQN
+-- - METHOD_PTTRN : 메서드명 접두 매칭(예: 'select','insertMber') 또는 '*'(전체)
+-- - DML_GUBUN : 지정 시 우선, NULL이면 메서드명 접두로 유도(insert*→I/update*→U/delete*→D/그외 S)
+-- - SORT_ORDR : 다중 매칭 시 낮은 값 우선(구체 규칙을 위로)
+--
+-- ※ URL 패턴 기반 수집이 필요하면(서블릿 필터형) 동일 테이블에 URL_PTTRN 컬럼을 추가해 확장.
+-- =====================================================================
+
+CREATE TABLE PDAL_JOB_MAPPING (
+ MAPPING_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '매핑 일련번호(PK)',
+ TARGET_CLASS VARCHAR(200) NOT NULL COMMENT '대상 서비스 impl 단순 클래스명 또는 FQN',
+ METHOD_PTTRN VARCHAR(100) NOT NULL DEFAULT '*' COMMENT '메서드명 접두 매칭(또는 * 전체)',
+ JOB_SE_CODE VARCHAR(30) NOT NULL COMMENT '수행업무 유형 코드',
+ JOB_SE_NM VARCHAR(100) NOT NULL COMMENT '수행업무 유형(화면·기능명)',
+ DML_GUBUN CHAR(1) NULL COMMENT '처리구분 강제값 S/I/U/D (NULL=메서드명 유도)',
+ SORT_ORDR INT NOT NULL DEFAULT 100 COMMENT '매칭 우선순위(낮을수록 우선)',
+ USE_AT CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '사용여부 Y/N',
+ RM VARCHAR(300) NULL COMMENT '비고',
+ PRIMARY KEY (MAPPING_ID),
+ KEY IX_PJM_LOOKUP (USE_AT, TARGET_CLASS, SORT_ORDR)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='개인정보 처리 업무유형 매핑(접속기록 #31/#32)';
+
+-- ---------------------------------------------------------------------
+-- 초기 매핑 시드 : 현행 개인정보 처리 서비스(#31 pointcut 대상과 일치)
+-- 신규 개인정보 화면 추가 시 아래 형식으로 행만 추가하면 수집된다.
+-- ---------------------------------------------------------------------
+INSERT INTO PDAL_JOB_MAPPING (TARGET_CLASS, METHOD_PTTRN, JOB_SE_CODE, JOB_SE_NM, DML_GUBUN, SORT_ORDR, RM) VALUES
+ ('EgovMberManageServiceImpl', 'select', 'MBER_VIEW', '회원관리-조회', 'S', 10, '회원 개인정보 조회'),
+ ('EgovMberManageServiceImpl', 'insert', 'MBER_REG', '회원관리-등록', 'I', 10, NULL),
+ ('EgovMberManageServiceImpl', 'update', 'MBER_MOD', '회원관리-수정', 'U', 10, NULL),
+ ('EgovMberManageServiceImpl', 'delete', 'MBER_DEL', '회원관리-삭제', 'D', 10, NULL),
+ ('EgovMberManageServiceImpl', '*', 'MBER_ETC', '회원관리-기타', NULL, 90, '미분류 폴백'),
+ ('EgovEntrprsManageServiceImpl', 'select', 'ENTRPRS_VIEW', '업체관리-조회', 'S', 10, '업체 담당자 개인정보 조회'),
+ ('EgovEntrprsManageServiceImpl', 'insert', 'ENTRPRS_REG', '업체관리-등록', 'I', 10, NULL),
+ ('EgovEntrprsManageServiceImpl', 'update', 'ENTRPRS_MOD', '업체관리-수정', 'U', 10, NULL),
+ ('EgovEntrprsManageServiceImpl', 'delete', 'ENTRPRS_DEL', '업체관리-삭제', 'D', 10, NULL),
+ ('EgovEntrprsManageServiceImpl', '*', 'ENTRPRS_ETC', '업체관리-기타', NULL, 90, '미분류 폴백'),
+ ('MberSsnServiceImpl', '*', 'MBER_SSN', '회원 고유식별정보 처리', NULL, 20, '주민등록번호 등 고유식별정보'),
+ ('BizMberMergeServiceImpl', '*', 'MBER_MERGE', '회원 통합/이관', NULL, 30, NULL),
+ ('BizTalentMberManageServiceImpl', '*', 'TALENT_MBER', '재능나눔 회원관리', NULL, 30, NULL);
diff --git a/src/script/mysql/ddl/privacy.otp_device_create_mysql.sql b/src/script/mysql/ddl/privacy.otp_device_create_mysql.sql
new file mode 100644
index 0000000..51142f6
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.otp_device_create_mysql.sql
@@ -0,0 +1,24 @@
+-- =====================================================================
+-- OTP(2단계 인증) 비밀키 영구 저장 테이블
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-003 / IAR-001, 추가 인증수단 1차 OTP)
+-- 개발항목 No.18 후속 - 담당: arko-privacy-authctrl
+-- 작성일: 2026-07-22 DB: MySQL 5.7/8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [배경] OtpSecretStore 기본 구현이 InMemory(재기동 시 유실)라 이를 대체하는
+-- DB 영속 저장소(DaoOtpSecretStore)가 사용하는 테이블.
+-- [보안] SECRET_ENC 는 OtpSecretCipher(AES-GCM) 암호문(Base64)만 저장한다.
+-- 평문 비밀키/OTP 코드는 저장하지 않는다(SER-003).
+-- [키] UNIQ_ID = 개인정보취급자 고유식별자(ESNTL_ID), 취급자당 1건(PK).
+-- =====================================================================
+
+CREATE TABLE IF NOT EXISTS OTP_DEVICE (
+ UNIQ_ID VARCHAR(20) NOT NULL COMMENT '개인정보취급자 고유식별자(ESNTL_ID, PK)',
+ SECRET_ENC VARCHAR(255) NOT NULL COMMENT 'OTP 비밀키 AES-GCM 암호문(Base64) - 평문 금지',
+ REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '등록(발급) 일시',
+ LAST_VERIFY_DT DATETIME NULL COMMENT '최종 검증 성공 일시(참고용)',
+ USE_CNT INT NOT NULL DEFAULT 0 COMMENT '검증 성공 누적 횟수(참고용)',
+ 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
+ COMMENT='OTP 2단계 인증 비밀키 영구 저장(개인정보보호 SFR-003 No.18)';
diff --git a/src/script/mysql/ddl/privacy.pdal_create_mysql.sql b/src/script/mysql/ddl/privacy.pdal_create_mysql.sql
new file mode 100644
index 0000000..bf487d7
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.pdal_create_mysql.sql
@@ -0,0 +1,79 @@
+-- =====================================================================
+-- 개인정보 접속기록 표준 테이블 : PERSONAL_DATA_ACCESS_LOG
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / DBR-001 / SER-002)
+-- 개발항목 #30 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [설계 원칙] 신규 스키마 설계가 아니라 기존 표준 PERSONAL_DATA_DML_HISTORY 를
+-- 표준 접속기록 테이블로 "승격" + 보관/무결성 컬럼 확장이다.
+--
+-- [기존 PERSONAL_DATA_DML_HISTORY → PERSONAL_DATA_ACCESS_LOG 컬럼 매핑]
+-- CHG_USER_ID → CONECT_ID (① 사용자 식별자)
+-- CREATE_DATE → CONECT_DT (② 접속일시, SYSDATE())
+-- CHG_CONECT_IP → CONECT_IP (③ 접속지 IP) * VARCHAR(45) IPv6 대응
+-- UI_GUBUN → JOB_SE_CODE/NM (④ 수행업무 유형) * 업무유형 매핑 테이블로 코드/명 분리
+-- ESNTL_ID → TRGET_ID (⑤ 처리대상 식별자)
+-- DML_GUBUN → DML_GUBUN (처리구분 S/I/U/D 그대로 계승)
+-- CHG_USER_SE → USER_SE (처리자 구분)
+-- [신규 확장 컬럼]
+-- TRGET_CN - 처리대상 건수/범위 (⑤ 보강)
+-- PROCESS_RESULT - 처리결과 S:성공 F:실패/차단 (비정상접근 탐지 5유형 적재 겸용)
+-- HASH_VAL - 위·변조 방지 해시 (SHA-256, 개발항목 #39)
+-- PREV_HASH_VAL - 직전 레코드 해시 (해시체인)
+--
+-- [보관·파기] 730일 보관 전제. 월 단위 RANGE COLUMNS 파티셔닝으로
+-- 파기 배치가 만료 파티션을 ALTER TABLE ... DROP PARTITION 으로 제거
+-- (개별 DELETE 미사용). 애플리케이션 계층에 UPDATE/DELETE statement 미정의.
+-- [분리 원칙] COMTNSYSLOG 의 7일 삭제정책은 본 테이블에 절대 적용하지 않는다.
+-- =====================================================================
+
+CREATE TABLE PERSONAL_DATA_ACCESS_LOG
+(
+ LOG_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '로그 일련번호',
+ CONECT_ID VARCHAR(20) NOT NULL COMMENT '① 사용자 식별자(ID)',
+ CONECT_DT DATETIME NOT NULL COMMENT '② 접속일시(파티션 키)',
+ CONECT_IP VARCHAR(45) NOT NULL COMMENT '③ 접속지 정보(IP, IPv6 대응)',
+ JOB_SE_CODE VARCHAR(30) NOT NULL COMMENT '④ 수행업무 유형 코드',
+ JOB_SE_NM VARCHAR(100) NOT NULL COMMENT '④ 수행업무 유형 명(화면/기능명)',
+ TRGET_ID VARCHAR(50) NULL COMMENT '⑤ 처리대상 식별자',
+ TRGET_CN VARCHAR(200) NULL COMMENT '⑤ 처리대상 건수/범위',
+ DML_GUBUN CHAR(1) NOT NULL COMMENT '처리구분 S:조회 I:등록 U:수정 D:삭제',
+ PROCESS_RESULT CHAR(1) NOT NULL DEFAULT 'S' COMMENT '처리결과 S:성공 F:실패/차단',
+ USER_SE VARCHAR(10) NULL COMMENT '처리자 구분',
+ HASH_VAL VARCHAR(64) NOT NULL COMMENT '위·변조 방지 해시(SHA-256 Hex)',
+ PREV_HASH_VAL VARCHAR(64) NULL COMMENT '직전 레코드 해시(해시체인)',
+ PRIMARY KEY (LOG_ID, CONECT_DT),
+ KEY IDX_PDAL_01 (CONECT_ID, CONECT_DT), -- 접속자 ID 검색 (현행 CONECT_MTHD LIKE 결함 해소)
+ KEY IDX_PDAL_02 (CONECT_DT), -- 기간 검색 / 파티션 프루닝
+ KEY IDX_PDAL_03 (TRGET_ID, CONECT_DT), -- 처리대상 추적
+ KEY IDX_PDAL_04 (JOB_SE_CODE, CONECT_DT) -- 업무유형/이상탐지유형 집계
+)
+ENGINE=InnoDB DEFAULT CHARSET=utf8
+COMMENT='개인정보 접속기록 표준 테이블(PERSONAL_DATA_DML_HISTORY 승격, 730일 보관)'
+PARTITION BY RANGE COLUMNS(CONECT_DT)
+(
+ -- 파티션 키(CONECT_DT)를 PK에 포함해야 RANGE COLUMNS 파티셔닝 성립 (MySQL/MariaDB 제약)
+ -- 월 단위 사전 생성. 유지보수 배치가 매월 다음 달 파티션을 ADD PARTITION 으로 선행 생성하고,
+ -- 파기 배치(월 1회)가 보관기간(기본 730일) 경과 파티션을 DROP PARTITION 으로 제거한다.
+ -- ※ MAXVALUE 파티션은 두지 않는다 — 만료 데이터가 catch-all 에 갇혀 DROP 불가해지는 것을 방지.
+ PARTITION p202607 VALUES LESS THAN ('2026-08-01'),
+ PARTITION p202608 VALUES LESS THAN ('2026-09-01'),
+ PARTITION p202609 VALUES LESS THAN ('2026-10-01'),
+ PARTITION p202610 VALUES LESS THAN ('2026-11-01'),
+ PARTITION p202611 VALUES LESS THAN ('2026-12-01'),
+ PARTITION p202612 VALUES LESS THAN ('2027-01-01'),
+ PARTITION p202701 VALUES LESS THAN ('2027-02-01'),
+ PARTITION p202702 VALUES LESS THAN ('2027-03-01')
+)
+;
+
+-- ---------------------------------------------------------------------
+-- [유지보수 참고] 월별 파티션 추가/삭제 예시 (배치가 동적으로 수행)
+--
+-- -- 다음 달 파티션 선행 생성
+-- ALTER TABLE PERSONAL_DATA_ACCESS_LOG
+-- ADD PARTITION (PARTITION p202703 VALUES LESS THAN ('2027-04-01'));
+--
+-- -- 보관기간(730일) 경과 파티션 파기
+-- ALTER TABLE PERSONAL_DATA_ACCESS_LOG DROP PARTITION p202607;
+-- ---------------------------------------------------------------------
diff --git a/src/script/mysql/ddl/privacy.pdal_purge_history_create_mysql.sql b/src/script/mysql/ddl/privacy.pdal_purge_history_create_mysql.sql
new file mode 100644
index 0000000..cb430ef
--- /dev/null
+++ b/src/script/mysql/ddl/privacy.pdal_purge_history_create_mysql.sql
@@ -0,0 +1,28 @@
+-- =====================================================================
+-- 개인정보 접속기록 파기·증설 이력 : PDAL_PURGE_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / DBR-001)
+-- 개발항목 #37 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] PERSONAL_DATA_ACCESS_LOG 의 보관정책(730일) 파기 배치가 수행한
+-- 파티션 DROP(파기)·ADD(선행 증설) 내역을 증빙으로 기록한다.
+-- 개인정보 접속기록의 유일한 삭제 경로가 "파티션 DROP"임을 감사에서 입증하는 근거.
+--
+-- [원칙] 접속기록 본문(PERSONAL_DATA_ACCESS_LOG)에는 UPDATE/DELETE 를 정의하지 않으며,
+-- 파기는 개별 DELETE 가 아니라 파티션 단위 DROP 으로만 수행한다(개발항목 #37).
+-- =====================================================================
+
+CREATE TABLE PDAL_PURGE_HISTORY
+(
+ PURGE_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호',
+ PARTITION_NAME VARCHAR(64) NOT NULL COMMENT '대상 파티션명(예: p202607)',
+ BOUNDARY_DT VARCHAR(30) NULL COMMENT '파티션 상한 경계값(LESS THAN, 예: 2026-08-01)',
+ PURGE_TYPE VARCHAR(10) NOT NULL COMMENT '유형 DROP:파기 ADD:선행증설',
+ PURGE_DT DATETIME NOT NULL COMMENT '수행 일시',
+ NOTE VARCHAR(300) NULL COMMENT '비고(보관기간·사유)',
+ PRIMARY KEY (PURGE_ID),
+ KEY IDX_PDAL_PURGE_01 (PURGE_DT),
+ KEY IDX_PDAL_PURGE_02 (PARTITION_NAME)
+)
+ENGINE=InnoDB DEFAULT CHARSET=utf8
+COMMENT='개인정보 접속기록 파티션 파기·증설 이력(개발항목 #37, 파기 증빙)';
diff --git a/src/script/mysql/privacy.install_mysql.sql b/src/script/mysql/privacy.install_mysql.sql
new file mode 100644
index 0000000..ff0fe4a
--- /dev/null
+++ b/src/script/mysql/privacy.install_mysql.sql
@@ -0,0 +1,509 @@
+-- #####################################################################
+-- arko_renual 설치용 통합 SQL (개인정보보호 기능개선 사업 · arko 대비 변동분)
+-- 대상 DB : MySQL 5.7 / MariaDB (스키마 csv)
+-- 구성 : [PART 1] DDL(테이블 생성·변경) → [PART 2] DML(기준/시드 데이터)
+-- 실행순서: 반드시 PART 1 → PART 2 순서로 실행(테이블 생성 후 시드 적재)
+-- 주의 : 신규 설치용. 기존 테이블이 있는 환경 재실행 시 CREATE 중복 오류 가능
+-- (IF NOT EXISTS 미지정 테이블). 그런 환경은 PART 2(DML)만 실행하세요.
+-- #####################################################################
+
+
+-- =====================================================================
+-- PART 1. DDL (테이블 생성 / 컬럼 확장) ※ 시드 INSERT는 PART 2로 분리
+-- =====================================================================
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.account_mng_policy_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 계정 관리 정책 : ACCOUNT_MNG_POLICY (단일행 정책 테이블)
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 장기 미사용 계정 관리)
+-- 개발항목 #13(정책 설정) + #9(휴면 기준일수 정책값화) - 담당: arko-privacy-authctrl (Fork A)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [용도] 휴면 기준일수·사전알림 시점·삭제 보존기간·배치주기 등 계정 관리 정책을
+-- 하드코딩이 아닌 관리자 설정값으로 운영하기 위한 단일행 정책 테이블.
+-- [#9] 휴면 select(EgovLoginUsr_SQL_Mysql.xml)의 365 하드코딩을
+-- (SELECT DORMANT_DAY FROM ACCOUNT_MNG_POLICY) 로 대체(미설정 시 365 fallback).
+-- [#10] ALERT_D1/D2/D3(기본 30/7/0) = 휴면예정 D-30/D-7/D-Day 사전알림 시점.
+-- ADMIN_EMAIL = 사전알림 관리자 수신 주소.
+-- =====================================================================
+
+CREATE TABLE IF NOT EXISTS ACCOUNT_MNG_POLICY (
+ POLICY_ID INT NOT NULL DEFAULT 1 COMMENT '정책 식별자(단일행=1)',
+ DORMANT_DAY INT NOT NULL DEFAULT 365 COMMENT '휴면 전환 기준 미접속 일수(#9)',
+ ALERT_D1 INT NOT NULL DEFAULT 30 COMMENT '사전알림 1차(휴면 D-일수)',
+ ALERT_D2 INT NOT NULL DEFAULT 7 COMMENT '사전알림 2차(휴면 D-일수)',
+ ALERT_D3 INT NOT NULL DEFAULT 0 COMMENT '사전알림 3차(D-Day, 0=당일)',
+ DELETE_PRESERVE_DAY INT NOT NULL DEFAULT 365 COMMENT '휴면/탈퇴 후 삭제 보존기간(일)',
+ BATCH_CYCLE VARCHAR(20) NOT NULL DEFAULT 'DAILY' COMMENT '배치 주기(DAILY/WEEKLY/MONTHLY)',
+ ADMIN_EMAIL VARCHAR(200) NULL COMMENT '사전알림 관리자 수신 이메일',
+ UPD_ID VARCHAR(20) NULL COMMENT '최종 수정자',
+ UPD_DT DATETIME NULL COMMENT '최종 수정일시',
+ PRIMARY KEY (POLICY_ID)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='계정 관리 정책(개인정보보호 SFR-002, 단일행)';
+
+-- 기본 정책 시드(없을 때만). 기준 365일·알림 D-30/7/0·보존 365일.
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.account_unlock_history_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 계정 잠금 해제 이력 : ACCOUNT_UNLOCK_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001, 계정 잠금 정책 정비)
+-- 개발항목 #24 - 담당: arko-privacy-authctrl (Fork A)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 잠긴 계정 해제를 DB 직접조작이 아니라 관리자 화면(EgovAccountUnlockManage)
+-- 에서 처리하고, 해제자·해제일시·대상·사유를 이력으로 남긴다.
+-- [무결성] 애플리케이션 계층에 UPDATE/DELETE statement 미정의(INSERT/SELECT 한정).
+-- =====================================================================
+
+CREATE TABLE ACCOUNT_UNLOCK_HISTORY (
+ UNLOCK_HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '해제 이력 일련번호(PK)',
+ TRGET_ID VARCHAR(50) NULL COMMENT '해제 대상 계정 ID(전체해제 시 NULL 가능)',
+ TRGET_SE VARCHAR(10) NULL COMMENT '대상 회원구분 GNR/GRO/ENT/USR',
+ UNLOCK_TY CHAR(1) NOT NULL DEFAULT 'I' COMMENT '해제 유형 I:개별 A:전체',
+ UNLOCK_USER_ID VARCHAR(50) NULL COMMENT '해제 처리자(관리자) ID',
+ UNLOCK_RESN VARCHAR(500) NULL COMMENT '해제 사유',
+ UNLOCK_IP VARCHAR(45) NULL COMMENT '해제자 접속 IP(IPv6 대응)',
+ UNLOCK_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '해제 일시',
+ PRIMARY KEY (UNLOCK_HIST_ID),
+ 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
+ COMMENT='계정 잠금 해제 이력(개인정보보호 SER-001 #24)';
+
+
+-- =====================================================================
+-- 계정 잠금 임계값 10회 → 5회 (#24)
+-- 임계값은 하드코딩이 아니라 LOGIN_LOCK_COUNT.LOCK_COUNT(단일행) 데이터에서 조회됨
+-- (loginDAO.selectFailTotalCount). 아래 UPDATE 로 운영 정책값을 5회로 조정한다.
+-- ※ 소스(EgovLoginController)에는 값 부재 시 기본 5회 fallback 이 반영되어 있음.
+-- =====================================================================
+UPDATE LOGIN_LOCK_COUNT SET LOCK_COUNT = 5;
+-- 행이 없을 경우 대비(최초 세팅):
+-- INSERT INTO LOGIN_LOCK_COUNT (LOCK_COUNT) SELECT 5 FROM DUAL
+-- WHERE NOT EXISTS (SELECT 1 FROM LOGIN_LOCK_COUNT);
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.author_change_history_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 권한 변경 이력 테이블 : AUTHOR_CHANGE_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-001, 역할별 차등 접근/권한 변경 추적)
+-- 개발항목 #4 - 담당: arko-privacy-authctrl (Fork A → 오케스트레이터 완료)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 권한 신설·변경·회수(#1 getAuthorities 대상 COMTNEMPLYRSCRTYESTBS 등)의
+-- 모든 변경을 변경자·변경일시·변경전/후 권한과 함께 이력으로 적재한다.
+-- 권한 신청·승인 워크플로(#3)의 승인 반영 시점에 권한반영과 단일 트랜잭션으로 적재.
+--
+-- [보관·파기] 3년(1,095일) 이상 보관 전제. 월 1회 @Scheduled 파기 배치(#5)가
+-- 보관기간(1,095일) 경과 이력만 선별 파기한다. 개별 삭제 API 미제공.
+-- 다운로드 시 사유 입력 필수 + 다운로드 이력 적재(#6)는 별도 처리.
+--
+-- [무결성] 이력 위·변조 억제를 위해 변경전/후 권한을 함께 보관(사후 대사 가능).
+-- 애플리케이션 계층에 UPDATE statement 미정의(INSERT/SELECT/파기 DELETE 한정).
+-- =====================================================================
+
+CREATE TABLE AUTHOR_CHANGE_HISTORY (
+ HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호(PK)',
+ TRGET_UNIQ_ID VARCHAR(20) NOT NULL COMMENT '변경 대상자 고유식별자(ESNTL_ID)',
+ TRGET_ID VARCHAR(20) NULL COMMENT '변경 대상자 계정ID(참고용)',
+ CHG_SE_CODE CHAR(1) NOT NULL COMMENT '변경구분 G:부여(grant) R:회수(revoke) M:변경(modify)',
+ BEFORE_AUTHOR VARCHAR(1000) NULL COMMENT '변경 전 권한(코드 CSV 또는 스냅샷)',
+ AFTER_AUTHOR VARCHAR(1000) NULL COMMENT '변경 후 권한(코드 CSV 또는 스냅샷)',
+ CHG_RESN VARCHAR(500) NULL COMMENT '변경 사유(신청/승인 워크플로 연계 #3)',
+ APROVL_ID VARCHAR(20) NULL COMMENT '승인자 계정ID(승인 워크플로 #3)',
+ CHANGER_UNIQ_ID VARCHAR(20) NOT NULL COMMENT '변경자(처리자) 고유식별자',
+ CHANGER_ID VARCHAR(20) NULL COMMENT '변경자 계정ID(참고용)',
+ CHANGER_IP VARCHAR(45) NULL COMMENT '변경자 접속 IP(IPv6 대응)',
+ CHG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '변경 일시',
+ PRIMARY KEY (HIST_ID),
+ 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
+ COMMENT='권한 변경 이력(개인정보보호 SFR-001, 3년 이상 보관)';
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.author_workflow_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 권한 신청·승인 워크플로 + 권한이력 다운로드 이력
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-001, 권한 신설·변경·회수 승인통제)
+-- 개발항목 #3(신청·승인 워크플로) / #6(권한이력 조회·다운로드) - 담당: arko-privacy-authctrl (Fork A 6차)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [#3] AUTHOR_CHANGE_REQUEST : 권한 변경 신청→승인/반려 워크플로.
+-- 승인(A) 시 권한 반영(COMTNEMPLYRSCRTYESTBS)과 이력 적재(AUTHOR_CHANGE_HISTORY)를
+-- 단일 트랜잭션으로 처리한다(egovframework.com..*Impl AOP tx advisor).
+-- [#6] AUTHOR_HIST_DOWNLOAD_HISTORY : 권한이력 다운로드 증빙(사유 필수).
+-- =====================================================================
+
+CREATE TABLE AUTHOR_CHANGE_REQUEST (
+ REQUEST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '신청 일련번호(PK)',
+ TRGET_UNIQ_ID VARCHAR(20) NOT NULL COMMENT '권한변경 대상자 고유식별자(ESNTL_ID)',
+ TRGET_ID VARCHAR(20) NULL COMMENT '권한변경 대상자 계정ID(참고용)',
+ REQUEST_SE CHAR(1) NOT NULL COMMENT '신청구분 N:신설(부여) C:변경 R:회수',
+ BEFORE_AUTHOR VARCHAR(1000) NULL COMMENT '신청 시점 현재 권한(스냅샷)',
+ AFTER_AUTHOR VARCHAR(1000) NULL COMMENT '신청 목표 권한(코드/CSV)',
+ REQUEST_RESN VARCHAR(500) NOT NULL COMMENT '신청 사유(필수)',
+ REQSTER_ID VARCHAR(20) NOT NULL COMMENT '신청자 계정ID',
+ REQSTER_UNIQ_ID VARCHAR(20) NULL COMMENT '신청자 고유식별자',
+ REQUEST_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '신청 일시',
+ CONFM_STTUS CHAR(1) NOT NULL DEFAULT 'R' COMMENT '처리상태 R:신청 A:승인 J:반려',
+ CONFM_ID VARCHAR(20) NULL COMMENT '승인/반려 처리자 계정ID',
+ CONFM_UNIQ_ID VARCHAR(20) NULL COMMENT '승인/반려 처리자 고유식별자',
+ CONFM_DT DATETIME NULL COMMENT '승인/반려 일시',
+ CONFM_RESN VARCHAR(500) NULL COMMENT '반려 사유',
+ 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
+ COMMENT='권한 변경 신청·승인 워크플로(개인정보보호 SFR-001 #3)';
+
+CREATE TABLE AUTHOR_HIST_DOWNLOAD_HISTORY (
+ DWLD_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '다운로드 일련번호(PK)',
+ DOWNLOADER_ID VARCHAR(20) NOT NULL COMMENT '다운로드자 계정ID',
+ DOWNLOADER_UNIQ_ID VARCHAR(20) NULL COMMENT '다운로드자 고유식별자',
+ DOWNLOADER_IP VARCHAR(45) NULL COMMENT '다운로드자 접속 IP(IPv6 대응)',
+ DWLD_RESN VARCHAR(500) NOT NULL COMMENT '다운로드 사유(필수·미입력 차단)',
+ SEARCH_COND VARCHAR(1000) NULL COMMENT '다운로드 시 검색조건 스냅샷',
+ DWLD_CNT INT NOT NULL DEFAULT 0 COMMENT '다운로드 건수',
+ 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
+ COMMENT='권한이력 다운로드 증빙 이력(개인정보보호 SFR-001 #6)';
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.job_mapping_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 개인정보 처리 업무유형 매핑 테이블 : PDAL_JOB_MAPPING
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / SER-002)
+-- 개발항목 #32 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 접속기록 수집(#31 PersonalDataLogAspect)이 가로챈 서비스 메서드를
+-- 업무코드(JOB_SE_CODE)·업무명(JOB_SE_NM)으로 매핑한다.
+-- AOP pointcut 범위(개인정보 처리 서비스 패키지) 안에서 신규 화면이 추가되면
+-- 이 테이블에 행만 등록하면 수집되며, Aspect/pointcut 소스 수정이 불요하다.
+--
+-- [매칭 규칙] Aspect가 (대상클래스 단순명, 메서드명)으로 조회.
+-- - TARGET_CLASS : 서비스 impl 단순 클래스명(예: EgovMberManageServiceImpl) 또는 FQN
+-- - METHOD_PTTRN : 메서드명 접두 매칭(예: 'select','insertMber') 또는 '*'(전체)
+-- - DML_GUBUN : 지정 시 우선, NULL이면 메서드명 접두로 유도(insert*→I/update*→U/delete*→D/그외 S)
+-- - SORT_ORDR : 다중 매칭 시 낮은 값 우선(구체 규칙을 위로)
+--
+-- ※ URL 패턴 기반 수집이 필요하면(서블릿 필터형) 동일 테이블에 URL_PTTRN 컬럼을 추가해 확장.
+-- =====================================================================
+
+CREATE TABLE PDAL_JOB_MAPPING (
+ MAPPING_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '매핑 일련번호(PK)',
+ TARGET_CLASS VARCHAR(200) NOT NULL COMMENT '대상 서비스 impl 단순 클래스명 또는 FQN',
+ METHOD_PTTRN VARCHAR(100) NOT NULL DEFAULT '*' COMMENT '메서드명 접두 매칭(또는 * 전체)',
+ JOB_SE_CODE VARCHAR(30) NOT NULL COMMENT '수행업무 유형 코드',
+ JOB_SE_NM VARCHAR(100) NOT NULL COMMENT '수행업무 유형(화면·기능명)',
+ DML_GUBUN CHAR(1) NULL COMMENT '처리구분 강제값 S/I/U/D (NULL=메서드명 유도)',
+ SORT_ORDR INT NOT NULL DEFAULT 100 COMMENT '매칭 우선순위(낮을수록 우선)',
+ USE_AT CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '사용여부 Y/N',
+ RM VARCHAR(300) NULL COMMENT '비고',
+ PRIMARY KEY (MAPPING_ID),
+ KEY IX_PJM_LOOKUP (USE_AT, TARGET_CLASS, SORT_ORDR)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='개인정보 처리 업무유형 매핑(접속기록 #31/#32)';
+
+-- ---------------------------------------------------------------------
+-- 초기 매핑 시드 : 현행 개인정보 처리 서비스(#31 pointcut 대상과 일치)
+-- 신규 개인정보 화면 추가 시 아래 형식으로 행만 추가하면 수집된다.
+-- ---------------------------------------------------------------------
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.download_history_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 개인정보 접속기록 다운로드 이력 : PDAL_DOWNLOAD_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / SER-002)
+-- 개발항목 #42 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 개인정보 접속기록(PERSONAL_DATA_ACCESS_LOG) 조회결과 다운로드 시,
+-- 다운로드 사유(필수)·다운로드자·일시·IP·건수를 이력으로 남긴다.
+-- 사유 미입력 시 다운로드는 서비스 계층에서 차단(본 테이블 미적재).
+-- [원칙] 개인정보 본문 미적재 — 다운로드 검색조건 요약(SEARCH_COND)만 보관.
+-- 이 테이블은 INSERT/SELECT 만 수행(위·변조 방지, UPDATE/DELETE 미정의).
+-- =====================================================================
+
+CREATE TABLE PDAL_DOWNLOAD_HISTORY (
+ DL_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '다운로드 이력 일련번호(PK)',
+ DOWNLOADER_ID VARCHAR(20) NOT NULL COMMENT '다운로드자 계정ID',
+ DOWNLOAD_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '다운로드 일시',
+ DOWNLOADER_IP VARCHAR(45) NULL COMMENT '다운로드자 접속 IP(IPv6 대응)',
+ DL_REASON VARCHAR(500) NOT NULL COMMENT '다운로드 사유(필수 — 미입력 시 차단)',
+ SEARCH_COND VARCHAR(1000) NULL COMMENT '다운로드 검색조건 요약(개인정보 본문 미포함)',
+ RESULT_CNT INT NOT NULL DEFAULT 0 COMMENT '다운로드 건수',
+ REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '등록 일시',
+ PRIMARY KEY (DL_ID),
+ KEY IX_PDDH_DT (DOWNLOAD_DT),
+ KEY IX_PDDH_USER (DOWNLOADER_ID, DOWNLOAD_DT)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='개인정보 접속기록 다운로드 이력(#42, SER-002)';
+
+-- [Oracle] 시퀀스 필요:
+-- CREATE SEQUENCE PDAL_DOWNLOAD_HISTORY_SEQ START WITH 1 INCREMENT BY 1 NOCACHE;
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.inspection_report_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 개인정보 접속기록 정기점검 리포트 : PDAL_INSPECTION_REPORT
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / SER-002)
+-- 개발항목 #43 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] 월 1회 정기점검 배치가 전월분 접속기록을 집계해 1행으로 적재한다.
+-- "월 1회 이상 점검 내역"을 화면 출력으로 제출(개인정보보호위 점검 대응).
+--
+-- [집계원] PERSONAL_DATA_ACCESS_LOG(#30) + 해시체인 무결성 검증(#40) 재사용.
+-- [자리표시] 이상징후 5유형(#35)·다운로드 이력(#42)은 미구현 → 컬럼만 두고 배치가 0/요약만 적재.
+-- [화면입력] INSPECTOR_ID/INSPECT_RESULT/ACTN_MATTER 는 배치가 아닌 점검자가 화면(#48)에서 입력.
+-- =====================================================================
+
+CREATE TABLE PDAL_INSPECTION_REPORT (
+ RPT_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '리포트 일련번호(PK)',
+ RPT_YM CHAR(6) NOT NULL COMMENT '점검 대상 연월(전월, yyyyMM)',
+ PERIOD_FROM DATETIME NOT NULL COMMENT '집계 시작일시',
+ PERIOD_TO DATETIME NOT NULL COMMENT '집계 종료일시',
+ TOTAL_LOG_CNT INT NOT NULL DEFAULT 0 COMMENT '총 접속기록 건수',
+ FAIL_CNT INT NOT NULL DEFAULT 0 COMMENT '처리결과 F(실패/차단) 건수 = 이상징후 일부',
+ ABNORMAL_SUMMARY VARCHAR(2000) NULL COMMENT '이상징후 5유형 요약(#35 연계 전 자리표시)',
+ INTEGRITY_RESULT VARCHAR(20) NOT NULL DEFAULT 'OK' COMMENT '해시체인 무결성: OK / VIOLATION',
+ INTEGRITY_MISMATCH_ID BIGINT NULL COMMENT '최초 불일치 LOG_ID(위반 시)',
+ INTEGRITY_TOTAL_CNT INT NOT NULL DEFAULT 0 COMMENT '무결성 검증 대상 건수',
+ HANDLER_SUMMARY VARCHAR(4000) NULL COMMENT '취급자별 처리현황 요약',
+ DOWNLOAD_CNT INT NOT NULL DEFAULT 0 COMMENT '다운로드 이력 건수(#42 연계 전 0)',
+ INSPECTOR_ID VARCHAR(20) NULL COMMENT '점검자 계정ID(화면 입력, #44)',
+ INSPECT_RESULT VARCHAR(500) NULL COMMENT '점검결과(화면 입력, #44)',
+ ACTN_MATTER VARCHAR(1000) NULL COMMENT '조치사항(화면 입력, #44)',
+ INSPECT_DT DATETIME NULL COMMENT '점검일시(점검결과 입력 시각, #44 감사증빙)',
+ REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '리포트 생성일시',
+ PRIMARY KEY (RPT_ID),
+ UNIQUE KEY UX_PIR_YM (RPT_YM),
+ KEY IX_PIR_REGDT (REG_DT)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+ COMMENT='개인정보 접속기록 정기점검 리포트(#43, 월 1회)';
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.pdal_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 개인정보 접속기록 표준 테이블 : PERSONAL_DATA_ACCESS_LOG
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / DBR-001 / SER-002)
+-- 개발항목 #30 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [설계 원칙] 신규 스키마 설계가 아니라 기존 표준 PERSONAL_DATA_DML_HISTORY 를
+-- 표준 접속기록 테이블로 "승격" + 보관/무결성 컬럼 확장이다.
+--
+-- [기존 PERSONAL_DATA_DML_HISTORY → PERSONAL_DATA_ACCESS_LOG 컬럼 매핑]
+-- CHG_USER_ID → CONECT_ID (① 사용자 식별자)
+-- CREATE_DATE → CONECT_DT (② 접속일시, SYSDATE())
+-- CHG_CONECT_IP → CONECT_IP (③ 접속지 IP) * VARCHAR(45) IPv6 대응
+-- UI_GUBUN → JOB_SE_CODE/NM (④ 수행업무 유형) * 업무유형 매핑 테이블로 코드/명 분리
+-- ESNTL_ID → TRGET_ID (⑤ 처리대상 식별자)
+-- DML_GUBUN → DML_GUBUN (처리구분 S/I/U/D 그대로 계승)
+-- CHG_USER_SE → USER_SE (처리자 구분)
+-- [신규 확장 컬럼]
+-- TRGET_CN - 처리대상 건수/범위 (⑤ 보강)
+-- PROCESS_RESULT - 처리결과 S:성공 F:실패/차단 (비정상접근 탐지 5유형 적재 겸용)
+-- HASH_VAL - 위·변조 방지 해시 (SHA-256, 개발항목 #39)
+-- PREV_HASH_VAL - 직전 레코드 해시 (해시체인)
+--
+-- [보관·파기] 730일 보관 전제. 월 단위 RANGE COLUMNS 파티셔닝으로
+-- 파기 배치가 만료 파티션을 ALTER TABLE ... DROP PARTITION 으로 제거
+-- (개별 DELETE 미사용). 애플리케이션 계층에 UPDATE/DELETE statement 미정의.
+-- [분리 원칙] COMTNSYSLOG 의 7일 삭제정책은 본 테이블에 절대 적용하지 않는다.
+-- =====================================================================
+
+CREATE TABLE PERSONAL_DATA_ACCESS_LOG
+(
+ LOG_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '로그 일련번호',
+ CONECT_ID VARCHAR(20) NOT NULL COMMENT '① 사용자 식별자(ID)',
+ CONECT_DT DATETIME NOT NULL COMMENT '② 접속일시(파티션 키)',
+ CONECT_IP VARCHAR(45) NOT NULL COMMENT '③ 접속지 정보(IP, IPv6 대응)',
+ JOB_SE_CODE VARCHAR(30) NOT NULL COMMENT '④ 수행업무 유형 코드',
+ JOB_SE_NM VARCHAR(100) NOT NULL COMMENT '④ 수행업무 유형 명(화면/기능명)',
+ TRGET_ID VARCHAR(50) NULL COMMENT '⑤ 처리대상 식별자',
+ TRGET_CN VARCHAR(200) NULL COMMENT '⑤ 처리대상 건수/범위',
+ DML_GUBUN CHAR(1) NOT NULL COMMENT '처리구분 S:조회 I:등록 U:수정 D:삭제',
+ PROCESS_RESULT CHAR(1) NOT NULL DEFAULT 'S' COMMENT '처리결과 S:성공 F:실패/차단',
+ USER_SE VARCHAR(10) NULL COMMENT '처리자 구분',
+ HASH_VAL VARCHAR(64) NOT NULL COMMENT '위·변조 방지 해시(SHA-256 Hex)',
+ PREV_HASH_VAL VARCHAR(64) NULL COMMENT '직전 레코드 해시(해시체인)',
+ PRIMARY KEY (LOG_ID, CONECT_DT),
+ KEY IDX_PDAL_01 (CONECT_ID, CONECT_DT), -- 접속자 ID 검색 (현행 CONECT_MTHD LIKE 결함 해소)
+ KEY IDX_PDAL_02 (CONECT_DT), -- 기간 검색 / 파티션 프루닝
+ KEY IDX_PDAL_03 (TRGET_ID, CONECT_DT), -- 처리대상 추적
+ KEY IDX_PDAL_04 (JOB_SE_CODE, CONECT_DT) -- 업무유형/이상탐지유형 집계
+)
+ENGINE=InnoDB DEFAULT CHARSET=utf8
+COMMENT='개인정보 접속기록 표준 테이블(PERSONAL_DATA_DML_HISTORY 승격, 730일 보관)'
+PARTITION BY RANGE COLUMNS(CONECT_DT)
+(
+ -- 파티션 키(CONECT_DT)를 PK에 포함해야 RANGE COLUMNS 파티셔닝 성립 (MySQL/MariaDB 제약)
+ -- 월 단위 사전 생성. 유지보수 배치가 매월 다음 달 파티션을 ADD PARTITION 으로 선행 생성하고,
+ -- 파기 배치(월 1회)가 보관기간(기본 730일) 경과 파티션을 DROP PARTITION 으로 제거한다.
+ -- ※ MAXVALUE 파티션은 두지 않는다 — 만료 데이터가 catch-all 에 갇혀 DROP 불가해지는 것을 방지.
+ PARTITION p202607 VALUES LESS THAN ('2026-08-01'),
+ PARTITION p202608 VALUES LESS THAN ('2026-09-01'),
+ PARTITION p202609 VALUES LESS THAN ('2026-10-01'),
+ PARTITION p202610 VALUES LESS THAN ('2026-11-01'),
+ PARTITION p202611 VALUES LESS THAN ('2026-12-01'),
+ PARTITION p202612 VALUES LESS THAN ('2027-01-01'),
+ PARTITION p202701 VALUES LESS THAN ('2027-02-01'),
+ PARTITION p202702 VALUES LESS THAN ('2027-03-01')
+)
+;
+
+-- ---------------------------------------------------------------------
+-- [유지보수 참고] 월별 파티션 추가/삭제 예시 (배치가 동적으로 수행)
+--
+-- -- 다음 달 파티션 선행 생성
+-- ALTER TABLE PERSONAL_DATA_ACCESS_LOG
+-- ADD PARTITION (PARTITION p202703 VALUES LESS THAN ('2027-04-01'));
+--
+-- -- 보관기간(730일) 경과 파티션 파기
+-- ALTER TABLE PERSONAL_DATA_ACCESS_LOG DROP PARTITION p202607;
+-- ---------------------------------------------------------------------
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.pdal_purge_history_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 개인정보 접속기록 파기·증설 이력 : PDAL_PURGE_HISTORY
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-004 / DBR-001)
+-- 개발항목 #37 - 담당: arko-privacy-auditlog (Fork B)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [목적] PERSONAL_DATA_ACCESS_LOG 의 보관정책(730일) 파기 배치가 수행한
+-- 파티션 DROP(파기)·ADD(선행 증설) 내역을 증빙으로 기록한다.
+-- 개인정보 접속기록의 유일한 삭제 경로가 "파티션 DROP"임을 감사에서 입증하는 근거.
+--
+-- [원칙] 접속기록 본문(PERSONAL_DATA_ACCESS_LOG)에는 UPDATE/DELETE 를 정의하지 않으며,
+-- 파기는 개별 DELETE 가 아니라 파티션 단위 DROP 으로만 수행한다(개발항목 #37).
+-- =====================================================================
+
+CREATE TABLE PDAL_PURGE_HISTORY
+(
+ PURGE_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호',
+ PARTITION_NAME VARCHAR(64) NOT NULL COMMENT '대상 파티션명(예: p202607)',
+ BOUNDARY_DT VARCHAR(30) NULL COMMENT '파티션 상한 경계값(LESS THAN, 예: 2026-08-01)',
+ PURGE_TYPE VARCHAR(10) NOT NULL COMMENT '유형 DROP:파기 ADD:선행증설',
+ PURGE_DT DATETIME NOT NULL COMMENT '수행 일시',
+ NOTE VARCHAR(300) NULL COMMENT '비고(보관기간·사유)',
+ PRIMARY KEY (PURGE_ID),
+ KEY IDX_PDAL_PURGE_01 (PURGE_DT),
+ KEY IDX_PDAL_PURGE_02 (PARTITION_NAME)
+)
+ENGINE=InnoDB DEFAULT CHARSET=utf8
+COMMENT='개인정보 접속기록 파티션 파기·증설 이력(개발항목 #37, 파기 증빙)';
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.otp_device_create_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- OTP(2단계 인증) 비밀키 영구 저장 테이블
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-003 / IAR-001, 추가 인증수단 1차 OTP)
+-- 개발항목 No.18 후속 - 담당: arko-privacy-authctrl
+-- 작성일: 2026-07-22 DB: MySQL 5.7/8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [배경] OtpSecretStore 기본 구현이 InMemory(재기동 시 유실)라 이를 대체하는
+-- DB 영속 저장소(DaoOtpSecretStore)가 사용하는 테이블.
+-- [보안] SECRET_ENC 는 OtpSecretCipher(AES-GCM) 암호문(Base64)만 저장한다.
+-- 평문 비밀키/OTP 코드는 저장하지 않는다(SER-003).
+-- [키] UNIQ_ID = 개인정보취급자 고유식별자(ESNTL_ID), 취급자당 1건(PK).
+-- =====================================================================
+
+CREATE TABLE IF NOT EXISTS OTP_DEVICE (
+ UNIQ_ID VARCHAR(20) NOT NULL COMMENT '개인정보취급자 고유식별자(ESNTL_ID, PK)',
+ SECRET_ENC VARCHAR(255) NOT NULL COMMENT 'OTP 비밀키 AES-GCM 암호문(Base64) - 평문 금지',
+ REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '등록(발급) 일시',
+ LAST_VERIFY_DT DATETIME NULL COMMENT '최종 검증 성공 일시(참고용)',
+ USE_CNT INT NOT NULL DEFAULT 0 COMMENT '검증 성공 누적 횟수(참고용)',
+ 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
+ COMMENT='OTP 2단계 인증 비밀키 영구 저장(개인정보보호 SFR-003 No.18)';
+
+-- ─────────────────────────────────────────────────────────────
+-- [privacy.diapause_emplyr_extend_mysql.sql]
+-- ─────────────────────────────────────────────────────────────
+-- =====================================================================
+-- 휴면(장기 미사용) 대상 확장 : COMTNDIAPAUSEMBER 회원유형 구분 컬럼 추가
+-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 장기 미사용 계정 관리)
+-- 개발항목 #7 - 담당: arko-privacy-authctrl (Fork A)
+-- 작성일: 2026-07-21 DB: MySQL 8 / MariaDB
+-- ---------------------------------------------------------------------
+-- [배경] 기존 휴면 대상은 일반회원(COMTNGNRLMBER)뿐이며, 휴면 테이블
+-- COMTNDIAPAUSEMBER 에는 회원유형 구분 컬럼이 없다. 개인정보취급자
+-- (COMTNEMPLYRINFO)를 휴면 대상에 포함(#7)하면서 일반회원 휴면/해제
+-- 흐름과 혼선되지 않도록 회원유형 구분 컬럼을 신설한다.
+--
+-- [안전] DEFAULT 'GNR'(일반회원)로 신설 → 기존 적재 행은 자동 'GNR' 처리되어
+-- 기존 selectDiapauseList(일반회원 join COMTNGNRLMBER) 흐름 무영향.
+-- 취급자 휴면 행은 'EMP'로 적재하여 별도 select/notify 경로로 처리한다.
+--
+-- [멱등] 재실행 안전을 위해 컬럼 존재 시 무시(수동 확인 후 실행).
+-- =====================================================================
+
+ALTER TABLE COMTNDIAPAUSEMBER
+ ADD COLUMN MBER_TY_CODE CHAR(3) NOT NULL DEFAULT 'GNR'
+ COMMENT '회원유형 GNR:일반회원 EMP:개인정보취급자';
+
+-- 조회 성능(취급자/일반 분리 조회) 보조 인덱스
+ALTER TABLE COMTNDIAPAUSEMBER
+ ADD INDEX IX_DIAPAUSE_MBERTY (MBER_TY_CODE, DIAPAUSE_STAT);
+
+
+-- =====================================================================
+-- PART 2. DML (기준/시드 데이터) ※ 멱등: NOT EXISTS 가드로 중복 적재 방지
+-- 수록: PDAL_JOB_MAPPING(업무유형 매핑 13행), ACCOUNT_MNG_POLICY(기본정책 1행)
+-- 미수록: OTP_DEVICE / PERSONAL_DATA_ACCESS_LOG / *_HISTORY = 런타임 적재(시드 없음)
+-- =====================================================================
+
+-- 1) PDAL_JOB_MAPPING — 개인정보 처리 업무유형 매핑(접속기록 AOP 분류 기준, 필수)
+INSERT INTO PDAL_JOB_MAPPING (TARGET_CLASS, METHOD_PTTRN, JOB_SE_CODE, JOB_SE_NM, DML_GUBUN, SORT_ORDR, RM)
+SELECT * FROM (
+ SELECT 'EgovMberManageServiceImpl' AS TARGET_CLASS, 'select' AS METHOD_PTTRN, 'MBER_VIEW' AS JOB_SE_CODE, '회원관리-조회' AS JOB_SE_NM, 'S' AS DML_GUBUN, 10 AS SORT_ORDR, '회원 개인정보 조회' AS RM UNION ALL
+ SELECT 'EgovMberManageServiceImpl', 'insert', 'MBER_REG', '회원관리-등록', 'I', 10, NULL UNION ALL
+ SELECT 'EgovMberManageServiceImpl', 'update', 'MBER_MOD', '회원관리-수정', 'U', 10, NULL UNION ALL
+ SELECT 'EgovMberManageServiceImpl', 'delete', 'MBER_DEL', '회원관리-삭제', 'D', 10, NULL UNION ALL
+ SELECT 'EgovMberManageServiceImpl', '*', 'MBER_ETC', '회원관리-기타', NULL, 90, '미분류 폴백' UNION ALL
+ SELECT 'EgovEntrprsManageServiceImpl', 'select', 'ENTRPRS_VIEW', '업체관리-조회', 'S', 10, '업체 담당자 개인정보 조회' UNION ALL
+ SELECT 'EgovEntrprsManageServiceImpl', 'insert', 'ENTRPRS_REG', '업체관리-등록', 'I', 10, NULL UNION ALL
+ SELECT 'EgovEntrprsManageServiceImpl', 'update', 'ENTRPRS_MOD', '업체관리-수정', 'U', 10, NULL UNION ALL
+ SELECT 'EgovEntrprsManageServiceImpl', 'delete', 'ENTRPRS_DEL', '업체관리-삭제', 'D', 10, NULL UNION ALL
+ SELECT 'EgovEntrprsManageServiceImpl', '*', 'ENTRPRS_ETC', '업체관리-기타', NULL, 90, '미분류 폴백' UNION ALL
+ SELECT 'MberSsnServiceImpl', '*', 'MBER_SSN', '회원 고유식별정보 처리', NULL, 20, '주민등록번호 등 고유식별정보' UNION ALL
+ SELECT 'BizMberMergeServiceImpl', '*', 'MBER_MERGE', '회원 통합/이관', NULL, 30, NULL UNION ALL
+ SELECT 'BizTalentMberManageServiceImpl', '*', 'TALENT_MBER', '재능나눔 회원관리', NULL, 30, NULL
+) seed
+WHERE NOT EXISTS (SELECT 1 FROM PDAL_JOB_MAPPING pjm WHERE pjm.JOB_SE_CODE = seed.JOB_SE_CODE);
+
+-- 2) ACCOUNT_MNG_POLICY — 장기 미사용 계정 관리 기본정책(단일행 POLICY_ID=1)
+INSERT INTO ACCOUNT_MNG_POLICY (POLICY_ID, DORMANT_DAY, ALERT_D1, ALERT_D2, ALERT_D3, DELETE_PRESERVE_DAY, BATCH_CYCLE, UPD_ID, UPD_DT)
+SELECT 1, 365, 30, 7, 0, 365, 'DAILY', 'SYSTEM', NOW()
+ FROM DUAL
+ WHERE NOT EXISTS (SELECT 1 FROM ACCOUNT_MNG_POLICY WHERE POLICY_ID = 1);
+
+-- (부록) LOGIN_LOCK_COUNT — 기존 arko 테이블(변동분 아님). 미존재 신규 환경에서만 사용(기본 5):
+-- INSERT INTO LOGIN_LOCK_COUNT (LOCK_COUNT)
+-- SELECT 5 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM LOGIN_LOCK_COUNT);
+
+-- ############################## END ##############################
diff --git a/개인정보보호_개발진행현황.md b/개인정보보호_개발진행현황.md
new file mode 100644
index 0000000..6412069
--- /dev/null
+++ b/개인정보보호_개발진행현황.md
@@ -0,0 +1,348 @@
+# 문화품앗이 개인정보보호 기능개선 사업 — 개발 진행현황
+
+> 발주기관: 한국문화원연합회 | 사업기간: 계약일~2026.12.18 | 대상: 문화품앗이(csv.culture.go.kr)
+> 근거: 개인정보보호위원회 「고유식별정보 안전조치 관리실태 점검」 결과통보(2025.11.21.)
+> 작성일: **2026-07-21** | 기준: `개인정보보호_개발항목.docx`(61개 개발항목) 대비 소스 실측
+
+---
+
+## 1. 종합 진행률 대시보드
+
+| 단계 | 상태 | 비고 |
+|------|:----:|------|
+| 요구사항 분석·설계 | ✅ 완료 | 제안서 Ⅲ장 + 개발항목 61개 정리 + 디자인 브리프(10개 화면) |
+| 자동화 하네스 구축 | ✅ 완료 | `arko-privacy-*` 에이전트 3종 + 스킬 4종 + 리뷰어 확장 |
+| **코드 구현** | 🟡 **진행 (42 / 61, 69%)** | 2026-07-21 7파 각 6개 — 1~6차 36개 + 7차(#9·#10·#11·#13·#36·#44) — 아래 §1-1 |
+| 시험·검증(Ⅲ-5) | ⬜ 대기 | 구현 후 진행 |
+| 적용·안정화(Ⅲ-6) | ⬜ 대기 | 구현 후 진행 |
+
+**개발항목 61개 코드 구현 기준 진행률: 42개 완료 (2026-07-21 당일, 7파 각 6개 — 69%)**
+
+- 분석·설계 및 하네스는 완료되어 **바로 구현 단계 진입 가능**한 상태.
+- 현재 워킹트리의 소스 변경(`EgovSpringSecurityLoginFilter.java`, `pom.xml`)은 **로컬 개발 IP 허용(127.0.0.1) 추가**로, 본 사업 산출물이 아님 — 진행률에서 제외.
+
+---
+
+## 1-1. 오늘의 작업 (2026-07-21, ~17:00 목표)
+
+fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반 항목 6개**를 당일 완료 목표로 착수한다.
+(발주기관 확정 대기 #12·#19, 운영환경 실측 선행 #55~58, 대규모 화면 #45~54는 오늘 범위에서 제외.)
+
+| Fork | 영역 | 대상 # | 산출물 | 상태 |
+|:----:|------|:------:|--------|:----:|
+| **A** | 권한관리 | #1 `getAuthorities()` 구현 | `EgovUserDetailsSessionServiceImpl` + 신규 sqlmap/DDL | ✅ 완료 |
+| **A** | 권한관리 | #4 AUTHOR_CHANGE_HISTORY DDL 신설 | 신규 `.sql` DDL(3년 보관) | ✅ 완료 |
+| **B** | 접속기록 | #30 PERSONAL_DATA_ACCESS_LOG DDL | 신규 `.sql`(무결성 컬럼+월 파티셔닝) | ✅ 완료 |
+| **B** | 접속기록 | #39 SHA-256 해시체인 유틸 | 신규 util `.java`(HASH_VAL/PREV_HASH_VAL) | ✅ 완료 |
+| **C** | 인증체계 | #17 AuthMethodService 인터페이스 설계 | 신규 인터페이스 + 3종 구현 스텁 | ✅ 완료 |
+| **C** | 인증체계 | #27 비밀번호 해시 알고리즘 점검 | `EgovFileScrty.encryptPassword()` 점검노트 | ✅ 완료 |
+
+> 파일 충돌 방지: Fork A만 `sql-map-config`/기존 ServiceImpl 수정, Fork B·C는 신규 파일만 생성. 진행현황 MD 기록은 오케스트레이터 단독 수행.
+
+**착수 로그**
+- 2026-07-21 착수: Fork A/B/C 병렬 투입, 대상 6개 항목 🟡 진행중 전환.
+- 2026-07-21 **Fork C 완료** (#17, #27): 인증수단 인터페이스 4파일 + 비밀번호 해시 점검노트 생성. 실측 확인 후 ✅ 전환. 후속 발견: 무솔트·무반복 해시, 해시값 로그출력(#27 보완 권고), 인증수단 빈 등록(#21 후속).
+- 2026-07-21 **Fork B 완료** (#30, #39): 접속기록 DDL(월 파티셔닝) + 해시체인 유틸(javac+실행 검증) 생성. 실측 확인 후 ✅ 전환. 후속 연계: 파티션 자동증설·730일 파기 배치(#37), 단일스레드 순차적재 전제(#33).
+- 2026-07-21 **Fork A 완료** (#1, #4): #1 getAuthorities() 5계층 배선(ServiceImpl→DAO→sqlmap→config→빈주입) 완료·실측 정합 확인. Fork A가 #4 DDL 착수 직후 API 오류로 중단되어, #4 AUTHOR_CHANGE_HISTORY DDL은 **오케스트레이터가 직접 완료**(컬럼명 오타 CHG_RESN 정정 포함). → **1차 목표 6개 항목 전부 ✅.**
+
+### 2차 작업 (2026-07-21, 추가 6개 목표)
+
+1차 기반(#1·#4·#17·#30·#39) 위에 쌓이는 항목. 공용 config 수정은 Fork A에 단독 집중해 충돌 차단.
+
+| Fork | 영역 | 대상 # | 산출물 | 상태 |
+|:----:|------|:------:|--------|:----:|
+| **A** | 배치(config 단독소유) | #5 권한 이력 파기 배치 | AUTHOR_CHANGE_HISTORY DAO/sqlmap + @Scheduled(1,095일) | ✅ 완료 |
+| **A** | 배치(config 단독소유) | #40 해시체인 무결성 검증 배치 | HashChainService.verifyChain 재계산 배치 + 관리자 통보 | ✅ 완료 |
+| **B** | util(신규파일만) | #34 IP 취득 로직 보정 | ClientIpResolver(XFF→Proxy-Client-IP→getRemoteAddr) | ✅ 완료 |
+| **B** | util(신규파일만) | #16 세션 재발급(세션고정 차단) | SessionRegenerator util | ✅ 완료 |
+| **C** | resolver+분석 | #21 AuthMethodResolver 구현 | #17 인터페이스 기반 인증수단 분기 | ✅ 완료 |
+| **C** | resolver+분석 | #38 COMTNSYSLOG 7일 삭제 분리 | 기존 7일 삭제 SQL 진단 + 접속기록 미적용 분리 노트 | ✅ 완료 |
+
+- 2026-07-21 2차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
+
+### 3차 작업 (2026-07-21, 추가 6개 목표 — 통합 단계)
+
+1·2차 산출물(#1·#16·#17·#21·#30·#34·#39)을 실제로 연결·활용. 공용 config는 fork별로 배타 소유(A=egov-com-servlet.xml, B=sql-map-config+AOP, C=impl java만).
+
+| Fork | 영역 | 대상 # | 산출물 | 상태 |
+|:----:|------|:------:|--------|:----:|
+| **A** | 인터셉터(servlet.xml 단독) | #2 AuthorityInterceptor 등록 | URL·메뉴 권한 대조 차단(#1 getAuthorities 활용) | ✅ 완료 |
+| **A** | 인터셉터(servlet.xml 단독) | #14 SecondFactorInterceptor 등록 | 2차 인증 판정(#16 SessionRegenerator+#21 Resolver) | ✅ 완료 |
+| **B** | 접속기록수집(sqlmap+AOP 단독) | #31 PersonalDataLogAspect | 개인정보 서비스 한정 AOP 수집(#34 IP+#39 해시) | ✅ 완료 |
+| **B** | 접속기록수집(sqlmap+AOP 단독) | #32 업무유형 매핑 테이블 | URL패턴→업무코드 DDL+로더 | ✅ 완료 |
+| **C** | 인증수단(impl java만) | #18 OtpAuthService(TOTP) | #17 스텁 채움, TOTP 자체구현·비밀키 암호화 | ✅ 완료 |
+| **C** | 인증수단(impl java만) | #20 CertAuthService | #17 스텁 채움, GPKI 기존자산 위임 | ✅ 완료 |
+
+- 2026-07-21 3차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
+
+### 4차 작업 (2026-07-21, 추가 6개 목표)
+
+접근통제·계정잠금 + 접속기록 배치/비동기 + 세션/마스킹. 로그인 흐름은 Fork A 단독 소유로 충돌 차단.
+
+| Fork | 영역 | 대상 # | 산출물 | 상태 |
+|:----:|------|:------:|--------|:----:|
+| **A** | 로그인흐름(sql-map-config 단독) | #22 인증실패이력 활성경로 이설 | LOGIN_HISTORY/LOGIN_LOCK_COUNT 재사용, actionLogin 활성 | ✅ 완료 |
+| **A** | 로그인흐름(sql-map-config 단독) | #24 계정 잠금 10→5회 | 임계값 5 + 해제 관리자화면 전환·해제이력 적재 | ✅ 완료 |
+| **B** | 접속기록배치(자기sqlmap·스케줄러 단독) | #33 비동기 적재 큐 | 로그적재↔업무txn 분리(단일컨슈머 순번보장) | ✅ 완료 |
+| **B** | 접속기록배치(자기sqlmap·스케줄러 단독) | #37 보관 730일 파기 배치 | 월1회 파티션 DROP(개별 DELETE 미사용) | ✅ 완료 |
+| **C** | 세션·마스킹(web.xml+util 단독) | #25 세션 타임아웃 60→30분 | web.xml session-config + 정책 훅 | ✅ 완료 |
+| **C** | 세션·마스킹(web.xml+util 단독) | #29 2차인증 화면 마스킹 | 휴대폰번호 마스킹 util | ✅ 완료 |
+
+- 2026-07-21 4차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
+
+### 5차 작업 (2026-07-21, 추가 6개 목표)
+
+미사용계정 휴면(첫 착수) + 접속기록 조회·점검 + 접근통제 마무리(#23 핸드오프 해소). 로그인/인터셉터는 Fork C 단독.
+
+| Fork | 영역 | 대상 # | 산출물 | 상태 |
+|:----:|------|:------:|--------|:----:|
+| **A** | 휴면계정(umt·휴면 sqlmap 단독) | #7 휴면 대상 취급자 확장 | 휴면 체인 S→P를 COMTNEMPLYRINFO까지 | ✅ 완료 |
+| **A** | 휴면계정(umt·휴면 sqlmap 단독) | #8 MariaDB 휴면배치 statement 정비 | Oracle에만 있던 배치 statement MariaDB 추가 | ✅ 완료 |
+| **B** | 접속기록조회·점검(자기sqlmap·스케줄러 단독) | #41 접속기록 조회·검색 개선 | 접속자ID 등 신규 검색조건(현행 LIKE+날짜뿐) | ✅ 완료 |
+| **B** | 접속기록조회·점검(자기sqlmap·스케줄러 단독) | #43 정기점검 리포트 생성 배치 | 월1회 이상징후·무결성·처리현황·다운로드 집계 | ✅ 완료 |
+| **C** | 접근통제마무리(로그인·servlet.xml 단독) | #23 계정 열거 방지 | 실패 메시지 존재여부 무관 일관화(#22 핸드오프 해소) | ✅ 완료 |
+| **C** | 접근통제마무리(로그인·servlet.xml 단독) | #26 인터셉터 3단 순서 확정 | 인증→SecondFactor→Authority 순서 확정·검증 | ✅ 완료 |
+
+- 2026-07-21 5차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
+
+### 6차 작업 (2026-07-21, 추가 6개 목표 — 2개 영역 완결)
+
+권한관리(1~6)·접근통제(22~29) 완결 목표. 공용 config는 fork별 배타 소유(A=sql-map-config, B=BatchScheduler+등록된sqlmap, C=servlet.xml).
+
+| Fork | 영역 | 대상 # | 산출물 | 상태 |
+|:----:|------|:------:|--------|:----:|
+| **A** | 권한관리완결(sql-map-config 단독) | #3 권한 신청·승인 워크플로 | 신청R→승인A/반려J 테이블+서비스, 승인시 권한반영+#4이력 단일TX | ✅ 완료 |
+| **A** | 권한관리완결(sql-map-config 단독) | #6 권한 이력 조회·검색·다운로드 | #4 이력 양방향검색 + 다운로드 사유필수·다운로드이력 | ✅ 완료 |
+| **B** | 접속기록마무리(스케줄러·등록sqlmap 단독) | #35 비정상 접근 탐지 5유형 | 비인가/대량조회/비정상시간대/계정공유의심/인증실패반복 | ✅ 완료 |
+| **B** | 접속기록마무리(스케줄러·등록sqlmap 단독) | #42 다운로드 사유·이력 관리 | 접속기록 다운로드 사유필수+다운로드자·일시·IP·건수 이력 | ✅ 완료 |
+| **C** | 인증판정·정책(servlet.xml·인터셉터 단독) | #15 2차인증 적용대상·경로 판정 | 취급자 한정, 외부 필수/내부 정책(#14 확장) | ✅ 완료 |
+| **C** | 인증판정·정책(servlet.xml·인터셉터 단독) | #28 신규 인증정보 저장정책 적용 | OTP암호화/인증번호미저장/인증서 식별값만 점검·확정 | ✅ 완료 |
+
+- 2026-07-21 6차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
+
+### 7차 작업 (2026-07-21, 추가 6개 목표 — 2개 영역 완결)
+
+미사용계정(SFR-002) 완결 + 접속기록(SFR-004) 15/15 완결. 미사용계정 4개는 공유파일 충돌로 Fork A 단독, 접속기록 2개는 파일 분리(B=aspect, C=report).
+
+| Fork | 영역 | 대상 # | 산출물 | 상태 |
+|:----:|------|:------:|--------|:----:|
+| **A** | 미사용계정완결(휴면/정책·sql-map-config 단독) | #9 휴면 기준일수 정책값화 | 365 하드코딩→정책테이블 | ✅ 완료 |
+| **A** | 미사용계정완결 | #10 휴면 전 관리자 알림 D-30/D-7/D-Day | 현재 본인메일 2단계→3단계+관리자 | ✅ 완료 |
+| **A** | 미사용계정완결 | #11 장기 미사용 계정 현황 조회(백엔드) | ID·성명·소속·최종로그인·미접속일수·상태·휴면예정일 | ✅ 완료 |
+| **A** | 미사용계정완결 | #13 계정 관리 정책 설정(백엔드) | 기준일수·알림시점·삭제보존기간·배치주기 정책 CRUD | ✅ 완료 |
+| **B** | 처리기록확장(aspect·AOP context 단독) | #36 처리기록 적재 범위 확장 | 2개 Controller→전 처리지점, TRGET_ID 규약(sqlmap 무변경) | ✅ 완료 |
+| **C** | 점검결과보관(report·등록sqlmap 단독) | #44 점검결과 입력·보관(백엔드) | 점검자·일시·결과·조치사항 입력→PDAL_INSPECTION_REPORT | ✅ 완료 |
+
+- 2026-07-21 7차 착수: Fork A/B/C 병렬 재투입, 대상 6개 🟡 진행중 전환.
+- 2026-07-21 **Fork B(7차) 완료** (#36): 처리기록 pointcut 5→16 서비스 확장 + TRGET_ID 식별자 추출 규약(본문 미적재, fail-safe). 실측 확인 후 ✅(XML well-formed·헬퍼 배선·sqlmap 미변경).
+- 2026-07-21 **Fork C(7차) 완료** (#44): 점검결과 입력·보관(InspectionResult 3파일+INSPECT_DT+statement 3종). 실측 확인 후 ✅(미입력 차단·양쪽 XML well-formed·sql-map-config 미변경). **접속기록 영역(30~44) 15/15 완결.** 배포된 테이블엔 INSPECT_DT ALTER 필요(핸드오프).
+- 2026-07-21 **Fork A(7차) 완료** (#9,#10,#11,#13): 정책테이블(ACCOUNT_MNG_POLICY)+정책값화+D-30/7/Day 알림+현황조회, 10파일·javac EXIT=0. 실측 확인 후 ✅(365→정책 서브쿼리·sql-map-config 등록·알림 스케줄러 확인). → **미사용계정 영역 6/7 완결(#12 발주대기만). 7차 6개 전부 ✅ (누적 42/61, 69%).**
+- 2026-07-21 **Fork C(6차) 중단→완결** (#15, #28): Fork C가 #15(SecondFactorInterceptor 확장) 완료 후 #28 점검노트 착수 직전 API 오류 중단. #15는 실측 확인(외부필수/내부정책 판정·enforce=false·xml well-formed·중괄호 75/75)→✅. **#28 점검노트는 오케스트레이터가 직접 완결**(3종 저장정책 실측, 위반 0건, OTP URI 주의)→✅. → **접근통제 영역(22~29) 8/8 완결.**
+- 2026-07-21 **Fork B(6차) 완료** (#35, #42): 비정상 탐지 5유형(6파일, 표준컬럼 집계) + 다운로드 사유필수·이력(DDL+4파일). 실측 확인 후 ✅(#43 리포트 자리표시→실집계 `summarize()`/`countByPeriod()` 교체, 양쪽 XML well-formed).
+- 2026-07-21 **Fork A(6차) 완료** (#3, #6): 권한 신청·승인 워크플로(단일TX) + 권한이력 조회·다운로드(사유필수). 실측 확인 후 ✅(10파일·DDL·sql-map-config 등록·approveRequest 단일TX·다운로드 차단 확인). → **권한관리 영역(1~6) 6/6 완결. 6차 6개 전부 ✅ (누적 36/61, 59%).**
+- 2026-07-21 **Fork C(5차) 완료** (#23, #26): 계정 열거 방지(조건부 문구 제거→단일 메시지) + 인터셉터 3단 순서 확정·문서화. 실측 확인 후 ✅(문구 제거·순서·xml well-formed 확인). **4차 #22 핸드오프(계정열거) 해소 완료.** ①authenticInterceptor 재활성은 운영 협의 후속.
+- 2026-07-21 **Fork B(5차) 중단→분할 완결**: Fork B가 #41 Java 4파일까지 만들고 API 오류로 중단(누락 sqlmap·#43 미착수). #41은 **오케스트레이터가 누락 sqlmap statement(Mysql/Oracle) 완결**(DAO statement id 정합·XML well-formed 검증)→✅. #43은 전용 fork 재투입(진행중).
+- 2026-07-21 **Fork A(5차) 완료** (#7, #8): 휴면 취급자 확장(MBER_TY_CODE 분리) + MariaDB 휴면 statement 5종 이설. 실측 확인 후 ✅ — **핵심 발견: MariaDB sqlmap에 휴면 statement 전무해 실운영 휴면배치가 불능이던 것을 복구**(#56 착수전 확인사항과 직결). DAO↔sqlmap id·javac EXIT=0 확인.
+- 2026-07-21 **#43 재투입 fork 완료**: 정기점검 리포트 배치(PDAL_INSPECTION_REPORT+집계 3종+스케줄러). 실측 확인 후 ✅(파일·서비스·XML well-formed). #35 이상징후·#42 다운로드는 자리표시(후속 실집계 교체). → **5차 목표 6개 전부 ✅ (누적 30/61 — 절반 근접).**
+- 2026-07-21 **Fork C(4차) 완료** (#25, #29): web.xml 세션타임아웃 30분 + MaskingUtil(휴대폰/이메일/이름). 실측 확인 후 ✅(web.xml:218=30, javac 통과). 후속: 세션타임아웃 관리자 설정화면.
+- 2026-07-21 **Fork A(4차) 완료** (#22, #24): 운영 로그인 경로(`EgovLoginController.actionLogin`) 실패이력·잠금 이설 + ACCOUNT_UNLOCK_HISTORY 해제이력. 실측 확인 후 ✅ — **참조 서비스 7메서드·VO 3게터 인터페이스 실재 검증, fail-safe로 로그인 무손상.** 핸드오프: 필터 활성 시 이중카운트 정리, #23 실패메시지 완전통일(현재 존재계정에만 "현재 N회" 부가).
+- 2026-07-21 **Fork B(4차) 완료** (#33, #37): 비동기 적재 큐(단일컨슈머 순번보장) + 730일 파티션 파기·선행증설 배치. 실측 확인 후 ✅(Aspect enqueue 전환·파티션 DROP/ADD statement·BatchScheduler 등록 확인). → **4차 목표 6개 전부 ✅ (누적 24/61).**
+- 2026-07-21 **Fork A(3차) 완료** (#2, #14): 인터셉터 2종 신규+`egov-com-servlet.xml` 등록(순서 #26 반영), javac 통과·xml well-formed. **운영 안전 확인: SecondFactor enforce=false·Authority urlRoleMapping 공백=둘 다 fail-open이라 현재 사이트 무손상.** 실측 확인 후 ✅. 핵심 실측: 이 프로젝트는 `selectAnnotaionMapper`가 `/**/*.do` 전체 적용(mvc:interceptors 미사용)·기존 `authenticInterceptor` 비활성 → 스코프/잠금방지를 인터셉터 내부에서 처리. 후속: #52·#18~20 후 enforce=true, urlRoleMapping 운영 등록.
+- 2026-07-21 **Fork C(3차) 완료** (#18, #20): OtpAuthService(TOTP RFC6238 자체구현, RFC 테스트벡터 일치) 5파일 + CertAuthService(기존 GPKI actionCrtfctLogin 위임). 스텁→실구현, javac+실행 검증. 실측 확인 후 ✅(UnsupportedOperationException 0건, SmsAuth#19 미변경 유지). 후속: OTP 비밀키 영속화 테이블, CertProcessFilter 재활성.
+- 2026-07-21 **Fork B(3차) 완료** (#31, #32): PersonalDataLogAspect 수집 파이프라인 10클래스+DDL+sqlmap 6+AOP context, javac EXIT=0. 실측 확인 후 ✅(pointcut 개인정보 서비스 5개 한정 확인=과수집 방지, 기존 COMTNSYSLOG 광역 블록 주석 유지). 후속: #33 비동기 큐, #36 TRGET_ID 규약. → **3차 목표 6개 전부 ✅ (누적 18/61).**
+- 2026-07-21 **Fork B(2차) 완료** (#34, #16): ClientIpResolver(신뢰CIDR 검증) + SessionRegenerator 생성, javac+단위검증 통과. 실측 확인 후 ✅. 후속: 신뢰대역 운영값(#57), #14 인터셉터 연동 배선.
+- 2026-07-21 **Fork C(2차) 완료** (#21, #38): AuthMethodResolver 4종(javac 통과) + 시스템로그 삭제정책 분리노트. 실측 확인 후 ✅(7일 삭제 SQL `EgovSysLog_SQL_Mysql.xml:159` 실제와 정합, 스케줄러 비활성 확인). 후속: #18~20 구현체·빈등록.
+- 2026-07-21 **Fork A(2차) 완료** (#5, #40): 파기·무결성검증 배치 2종(배치 클래스+DAO+sqlmap 6파일) + `BatchScheduler` @Scheduled 2메서드 + sql-map-config 등록. 실측 확인 후 ✅(파기 SQL이 실제 DDL 컬럼 CHG_DT 사용 정합). **핵심 핸드오프: 전역 `task:annotation-driven`이 양쪽 context에 주석 처리→@Scheduled 현재 무동작(기존 배치도 동일). 활성화·crontab 경합은 운영 협의 필요.** → **2차 목표 6개 전부 ✅ (누적 12/61).**
+
+---
+
+## 2. 영역별 진행 요약
+
+| 영역 | 요구사항 ID | 항목(구간) | 담당 | 완료 | 진행중 | 미착수 |
+|------|-------------|:----------:|------|:----:|:------:|:------:|
+| Ⅰ. 권한관리 ✅ | SFR-001 | 1~6 | arko-privacy-authctrl | 6 | 0 | 0 |
+| Ⅰ. 미사용계정관리 | SFR-002 | 7~13 | arko-privacy-authctrl | 6 | 0 | 1 |
+| Ⅱ. 인증체계 | SFR-003 / IAR-001 | 14~21 | arko-privacy-authctrl | 7 | 0 | 1 |
+| Ⅱ. 접근통제·인증정보 ✅ | SER-001 / SER-003 | 22~29 | arko-privacy-authctrl | 8 | 0 | 0 |
+| Ⅲ. 접속기록 ✅ | SFR-004 / DBR-001 / SER-002 | 30~44 | arko-privacy-auditlog | 15 | 0 | 0 |
+| Ⅳ. 신규 화면(UI) | - | 45~54 | arko-frontend | 0 | 0 | 10 |
+| Ⅴ. 착수전 확인 | - | 55~58 | arko-privacy-analyst | 0 | 0 | 4 |
+| Ⅵ. 보안·품질 | SER-004 | 59~61 | arko-reviewer | 0 | 0 | 3 |
+| **합계** | | **61** | | **42** | **0** | **19** |
+
+범례: ✅ 완료 · 🟡 진행중 · ⬜ 미착수
+
+---
+
+## 3. 항목별 상세 진행현황
+
+### Ⅰ. 권한관리 (Ⅲ-2, SFR-001·SFR-002) — arko-privacy-authctrl
+
+#### 1) 권한 신설·변경·회수 및 역할별 차등 적용 (SFR-001)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 1 | `getAuthorities()` 구현 | ✅ 완료 | [Fork A] `EgovUserDetailsSessionServiceImpl` 세션 LoginVO.uniqId→`EgovUserAuthorityDAO.selectAuthoritiesByUniqId`→`PrivacyAuthority_SQL_Mysql.xml`(COMTNEMPLYRSCRTYESTBS 조회). sql-map-config 등록 + 빈 DAO 주입 배선 완료. 미인증 시 빈 목록(fail-safe) |
+| 2 | AuthorityInterceptor 신규 등록 | ✅ 완료 | [Fork A] `cmm/privacy/interceptor/AuthorityInterceptor.java` + `egov-com-servlet.xml` 등록. `/mnt/` 한정, `EgovUserDetailsHelper.getAuthorities()`(#1) 대조 미허용 403. **urlRoleMapping 기본 비어있음=fail-open(전경로 통과)** → 운영에서 매핑 점진 등록 |
+| 3 | 권한 신청·승인 워크플로 구현 | ✅ 완료 | [Fork A] `AUTHOR_CHANGE_REQUEST`(DDL)+5계층(`AuthorWorkflow*`). `approveRequest()` 승인 시 스냅샷→권한반영(COMTNEMPLYRSCRTYESTBS)→상태A→#4이력을 **단일TX**(context-transaction AOP, 예외 시 롤백). 화면 #46 후속 |
+| 4 | AUTHOR_CHANGE_HISTORY 이력 테이블 신설 | ✅ 완료 | `src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql`. 변경자/변경일시/변경전후권한(BEFORE/AFTER_AUTHOR)/변경구분/승인자/IP + 3년(1,095일) 보관 전제. Fork A 중단→오케스트레이터 완료 |
+| 5 | 권한 이력 파기 배치 구현 | ✅ 완료 | [Fork A] `cmm/privacy/batch/`(Service/Impl/PurgeDAO) + `PrivacyAuthorHistoryPurge_SQL`(DELETE WHERE CHG_DT365`→`>(SELECT IFNULL(MAX(DORMANT_DAY),365) FROM ACCOUNT_MNG_POLICY)`(Mysql/Oracle). **미설정 시 365 fallback**, Java 시그니처 무변경 |
+| 10 | 휴면 전 관리자 알림 확장(D-30/D-7/D-Day) | ✅ 완료 | [Fork A] `DormantAlertService` — 정책 ALERT_D1/2/3(기본 30/7/0)일 전 대상 조회→본인+관리자(ADMIN_EMAIL) 메일(EgovSndngMail 재사용, fail-safe). `BatchScheduler.dormantPreAlertScheduler()` @Scheduled(매일 06:00) |
+| 11 | 장기 미사용 계정 현황 조회 화면 신규 | ✅ 완료 | [Fork A] `DormantAccountStatus`Service/DAO+VO. 취급자(COMTNEMPLYRINFO) 계정ID·성명·소속·직위·최종로그인·**미접속일수(DATEDIFF)**·상태·**휴면예정일** 복합검색+페이징. 일반회원 UNION·화면 후속 |
+| 12 | 계정 삭제 기능 구현 | ⬜ 미착수 | ⚠ 보존기간 발주기관 확정 필요 |
+| 13 | 계정 관리 정책 설정 화면 | ✅ 완료 | [Fork A] `ACCOUNT_MNG_POLICY`(DDL, DORMANT_DAY·ALERT_D1/2/3·DELETE_PRESERVE_DAY·BATCH_CYCLE·ADMIN_EMAIL 단일행+시드) + `AccountMngPolicy`Service/DAO/VO/Controller. 미설정 시 기본값 fallback. #9/#10이 이 테이블 참조. 화면 후속 |
+
+### Ⅱ. 인증체계 (Ⅲ-3, SFR-003·IAR-001·SER-001·SER-003) — arko-privacy-authctrl
+
+#### 3) 2단계 인증 삽입 (SFR-003)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 14 | SecondFactorInterceptor 신규 등록 | ✅ 완료 | [Fork A] `cmm/privacy/interceptor/SecondFactorInterceptor.java`, authority보다 먼저 등록(#26 순서 반영). `/mnt/` 한정 SECOND_FACTOR_DONE 검사. **enforce=false(미강제)** → #52 화면·#18~20 수단 완료 후 true+resolver 주입 활성화. javac 통과 |
+| 15 | 2차 인증 적용대상·경로 판정 로직 | ✅ 완료 | [Fork C] `SecondFactorInterceptor` 확장 — `/mnt/` 취급자 한정, **외부접속 필수(설정불가)/내부접속 정책(applyToInternal, internalIpCidrs CIDR 판정)**. enforce=false 유지(잠금방지). 중괄호 75/75·xml well-formed |
+| 16 | 세션 재발급 처리(세션고정 차단) | ✅ 완료 | [Fork B] `cmm/privacy/security/SessionRegenerator.java`. `regenerate()`=속성캡처→invalidate→getSession(true)→재적재, `onSecondFactorSuccess()` 표식. javac 통과. #14 SecondFactorInterceptor 연동 시 호출 배선 필요 |
+
+#### 4) 추가 인증수단 3종 (IAR-001)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 17 | AuthMethodService 인터페이스 설계 | ✅ 완료 | [Fork C] `com/uat/uia/service/AuthMethodService.java` + 스텁 3종(Otp/Sms/Cert) 생성. `issue(uniqId)`/`verify(uniqId,code)`/`isAvailable(uniqId)` throws Exception 확정. 빈 등록·실로직은 #18~21 후속 |
+| 18 | OtpAuthService 구현 (1차 적용) | ✅ 완료 | [Fork C] `OtpAuthService`+`OtpSecretStore`/`InMemory`/`OtpSecretCipher`(5파일). TOTP RFC6238 자체구현(HmacSHA1/30초/6자리/±1스텝), 비밀키 AES-128/GCM 암호화 저장·미노출. **RFC 테스트벡터 일치(287082/081804)**. 영속화=EMPLYR_OTP_SECRET 테이블 후속(Fork B 소유 sqlmap) |
+| 19 | SmsAuthService 구현 (2차 적용) | ⬜ 미착수 | ⚠ KT-TCS/MCSAgent3.0 연동규격 발주기관 확정 필요 |
+| 20 | CertAuthService 구현 (3차 적용) | ✅ 완료 | [Fork C] `CertAuthService` 실구현. 기존 `EgovLoginServiceImpl.actionCrtfctLogin`(:108)→`COMTNEMPLYRINFO.CRTFC_DN_VALUE`(EgovLoginUsr_SQL_Mysql.xml:434) 위임, **인증서 본문 미저장·DN 식별값만 대조**. 핸드오프: `CertProcessFilter`(web.xml:46 주석) 재활성 운영협의 |
+| 21 | AuthMethodResolver 구현 | ✅ 완료 | [Fork C] `AuthMethodResolver`+`AuthMethodType`(enum)+`AuthMethodPolicyProvider`+`DefaultAuthMethodPolicyProvider` 4종. Map 주입 분기, 폴백 OTP→SMS→CERT, `resolve(String) throws Exception`(1차 계약 정합). javac 통과. 실동작은 #18~20 구현체·빈등록 후 |
+
+#### 5) 인증 실패 이력 및 접근통제 정책 (SER-001)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 22 | 인증 실패 이력 활성 경로 이설 | ✅ 완료 | [Fork A] AS-IS: 실패이력/잠금이 `EgovSpringSecurityLoginFilter:466`(조건부)에만 존재. `EgovLoginController.actionLogin` 재작성(잠금선확인→성공시 이력정리→실패시 조건부 적재/잠금), **기존 LOGIN_HISTORY/LOGIN_LOCK_COUNT·서비스 7메서드 재사용(신규 statement 0)**, 전구간 fail-safe. 서비스/VO 메서드 실재 검증 |
+| 23 | 계정 열거 방지 처리 | ✅ 완료 | [Fork C] `EgovLoginController:238` 조건부 "현재 N회" 문구 제거→존재/미존재·실패횟수 무관 단일 `fail.common.login`. 실패이력·잠금은 내부 유지, 응답 노출 제거. 잠금안내만 의도적 예외 |
+| 24 | 계정 잠금 정책 정비(10회→5회) | ✅ 완료 | [Fork A] 임계값 `LOGIN_LOCK_COUNT` UPDATE=5 + 소스 fallback `DEFAULT_LOCK_THRESHOLD=5`. 해제화면 기존확인. **신규 `ACCOUNT_UNLOCK_HISTORY`**(DDL+VO+DAO+sqlmap, sql-map-config 등록)에 해제이력 훅(해제자+ClientIpResolver IP, fail-safe). 사유입력은 화면 후속 |
+| 25 | 세션 타임아웃 조정(60→30분) | ✅ 완료 | [Fork C] `web.xml:218` `30`(60→30) + 근거주석. 실사용 webapp/web.xml 확정. 관리자 설정화면은 후속 |
+| 26 | 인터셉터 3단 판정 순서 확정 | ✅ 완료 | [Fork C] `egov-com-servlet.xml:66` 리스트 순서 확정 ①authenticInterceptor(비활성)→②secondFactor→③authority + 근거주석. ①재활성 시 위치 유지 핸드오프. xml well-formed |
+
+#### 6) 인증정보 보호 (SER-003)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 27 | 비밀번호 해시 알고리즘 점검 | ✅ 완료 | [Fork C] 점검노트 `개인정보보호_비밀번호해시_점검노트.md`. 알고리즘 **SHA-256(기준 충족)**, 단 운영경로 **무솔트·무반복** + `BizMberFrtManageController.java:553` 해시값 로그출력 3건 보완 권고 |
+| 28 | 신규 인증정보 저장정책 적용 | ✅ 완료 | [Fork C→오케스트레이터] 점검노트 `개인정보보호_인증정보저장정책_점검노트.md`. OTP AES암호화저장(로그노출0)·SMS 미저장설계·CERT DN만 → **3종 준수, 위반 0건**. OTP 프로비저닝 URI 화면(#53) 취급 주의 1건 |
+| 29 | 2차 인증 화면 개인정보 마스킹 | ✅ 완료 | [Fork C] `cmm/privacy/util/MaskingUtil.java`. maskPhone(010-****-1234)/maskEmail(ho**@)/maskName(홍*동)/범용 mask. null·형식 방어, javac+self-check 통과. #52 화면에서 사용 |
+
+### Ⅲ. 접속 및 처리기록 관리 (Ⅲ-4, SFR-004·DBR-001·SER-002) — arko-privacy-auditlog
+
+#### 7) 접속기록 수집·저장 (SFR-004)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 30 | PERSONAL_DATA_ACCESS_LOG 신규 테이블 | ✅ 완료 | [Fork B] `src/script/mysql/ddl/privacy.pdal_create_mysql.sql`. 기존 PERSONAL_DATA_DML_HISTORY 실측 승격 + HASH_VAL/PREV_HASH_VAL, PK(LOG_ID,CONECT_DT), 월 RANGE COLUMNS 파티셔닝(접속자ID 검색 인덱스 포함). 파티션 자동증설/파기는 #37 연계 |
+| 31 | AOP pointcut 한정 수집 설계 | ✅ 완료 | [Fork B] `PersonalDataLogAspect`+Service/Impl+DAO(10파일)+`context-syslogaop.xml`. pointcut **개인정보 서비스 impl 5개 한정**(과수집 방지), `ClientIpResolver`(#34)+`HashChainService.linkAndSeal`(#39) 적재, synchronized 순번직렬화. 본문 미적재. 비동기(#33)·TRGET_ID(#36) 후속 |
+| 32 | 업무유형 매핑 테이블 구현 | ✅ 완료 | [Fork B] `privacy.job_mapping_create_mysql.sql`(`PDAL_JOB_MAPPING`+시드 13행) + `JobMappingResolver`(@Component 캐시)+DAO+sqlmap. (클래스,메서드)→업무코드/명, 신규화면은 행 등록만으로 수집(소스 무수정) |
+| 33 | 비동기 적재 큐 구현 | ✅ 완료 | [Fork B] `AccessLogQueue`(LinkedBlockingQueue cap1만+단일 데몬컨슈머). Aspect가 enqueue만(업무 응답 무영향), 컨슈머가 persistNow 순차→**단일 라이터로 해시체인 순번 보장**. 백프레셔(offer 5s), @PreDestroy flush 유실방지 |
+| 34 | IP 취득 로직 보정 | ✅ 완료 | [Fork B] `cmm/privacy/auditlog/ClientIpResolver.java`. XFF→Proxy/WL-Proxy→HTTP_CLIENT_IP→getRemoteAddr 순 + **신뢰프록시 CIDR 검증(헤더위조 방지)** + IPv6 루프백 정규화. javac+단위검증 통과. AS-IS `IpObtainInterceptor`(getRemoteAddr+인증시만) 격차 확인. 신뢰대역 운영값=#57 후 확정 |
+
+#### 8) 비정상 접근 탐지 및 처리기록 (SER-002)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 35 | 비정상 접근 탐지 5유형 구현 | ✅ 완료 | [Fork B] `AbnormalAccessType`(enum)+`Policy`+`Detection`Service/Impl/DAO+VO(6파일). 표준컬럼 집계로 5유형(비인가F/대량조회/업무외시간/동일ID다중IP/실패반복≥5). 임계 정책추상화. sqlmap 5종(Mysql/Oracle). **#43 리포트 실집계 교체** |
+| 36 | 처리기록 적재 범위 확장 | ✅ 완료 | [Fork B] pointcut 5→16개 개인정보 취급 서비스(회원/취급자/휴면/신청/활동/그룹/마일리지/프로필/쪽지/SMS 등 명시열거, 조직·통계·게시성 제외). **TRGET_ID 식별자만 베스트에포트 추출**(getter/Map키 우선순위, String인자 스킵, 본문 미적재, fail-safe). sqlmap 무변경(컬럼 기보유). javac EXIT=0 |
+
+#### 9) 보관정책 및 위·변조 방지 (DBR-001)
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 37 | 보관정책 730일 + 파기 배치 구현 | ✅ 완료 | [Fork B] `AccessLogPartitionDAO`+`PDAL_PURGE_HISTORY`(DDL). INFORMATION_SCHEMA.PARTITIONS 경계≤오늘-730일 파티션 DROP + 당월~+2M ADD 선행증설. `BatchScheduler.purgeAccessLogPartitionScheduler()`(매월1일 02:00). ⚠파티션명 `$` 치환=Java계산값(안전, 리뷰포인트) |
+| 38 | COMTNSYSLOG 7일 삭제정책 재설계 | ✅ 완료 | [Fork C] 노트 `개인정보보호_시스템로그삭제정책_분리노트.md`. 7일 삭제 SQL 위치 `EgovSysLog_SQL_Mysql.xml:159`(COMTNSYSLOG 단일, Quartz 1H이나 **현재 스케줄러 비활성**) 실측 확인. 오적용 위험 낮음(테이블 하드코딩+namespace 분리), 분리 보강 5개 권고 |
+| 39 | SHA-256 해시체인 구현 | ✅ 완료 | [Fork B] `cmm/privacy/auditlog/HashChainService.java`(+VO). MessageDigest 자체구현, 결합규칙 CONECT_ID\|CONECT_DT\|CONECT_IP\|JOB_SE_CODE\|TRGET_ID\|DML_GUBUN\|PREV_HASH_VAL→SHA-256. computeHash/linkAndSeal/verifyChain, javac+실행 변조탐지 검증. 순번보장=단일스레드 순차INSERT 전제(#33) |
+| 40 | 해시체인 무결성 검증 배치 | ✅ 완료 | [Fork A] `AccessLogIntegrityDAO`+`PrivacyAccessLogIntegrity_SQL`(LOG_ID ASC, CONECT_DT 형식일치) + `verifyAccessLogHashChain()`→verifyChain. `BatchScheduler.verifyAccessLogHashChainScheduler()` @Scheduled(매월1일 03:00). 불일치 시 log.error+notifyAdmin(#49 TODO)·리포트(#43) 반환 |
+
+#### 10) 조회·다운로드 및 정기점검
+
+| # | 개발항목 | 상태 | 소스 실측 / 비고 |
+|:--:|----------|:----:|------------------|
+| 41 | 접속기록 조회·검색 화면 개선 | ✅ 완료 | [Fork B→오케스트레이터] `PersonalDataAccessLogSearch`DAO/Service/Impl/VO + `selectAccessLogList`/`Count` sqlmap(Mysql/Oracle). 접속자ID/업무유형/대상/IP/처리구분/결과/기간 복합+페이징. Fork B 중단→오케스트레이터가 누락 sqlmap 완결(codebase 관용 맞춰 동적WHERE 인라인, XML well-formed). 화면 #47 후속 |
+| 42 | 다운로드 사유·이력 관리 | ✅ 완료 | [Fork B] `PDAL_DOWNLOAD_HISTORY`(DDL)+VO/DAO+`AccessLogDownloadService`. **사유 공백 시 IllegalArgumentException 차단** + 다운로드자·일시·IP·사유·건수 이력(본문 미보관, 10만건 상한). sqlmap 3종. **#43 리포트 실집계 교체** |
+| 43 | 정기점검 리포트 생성 배치 | ✅ 완료 | [Fork B] `PDAL_INSPECTION_REPORT`(DDL)+`InspectionReportVO`/`DAO`+`generateMonthlyInspectionReport()`+`BatchScheduler`(매월1일 05:00). 집계: 총/실패건수·#40무결성(재사용)·취급자별 top20 [구현], 이상징후#35·다운로드#42 [자리표시]. sql-map-config 무수정(기존 sqlmap 확장) |
+| 44 | 점검결과 입력·보관 화면 | ✅ 완료 | [Fork C] `InspectionResult`Service/Impl/Controller + `INSPECT_DT` 컬럼 추가 + statement 3종(목록/상세/UPDATE). `saveInspectionResult()` 결과 미입력 차단·리포트 존재확인·INSPECTOR_ID/INSPECT_RESULT/ACTN_MATTER/INSPECT_DT 갱신(감사증빙, 배치집계값 불변). 화면 #48 후속 |
+
+### Ⅳ. 신규 화면 (디자인.md 연계) — arko-frontend
+
+| # | 개발항목 | 구분 | 상태 |
+|:--:|----------|:----:|:----:|
+| 45 | 권한부여 신청/승인 관리 화면 | mnt | ⬜ 미착수 |
+| 46 | 권한변경 이력 조회 화면 | mnt | ⬜ 미착수 |
+| 47 | 개인정보 접속기록 조회/다운로드 화면 | mnt | ⬜ 미착수 |
+| 48 | 접속기록 정기점검 리포트 대시보드 | mnt | ⬜ 미착수 |
+| 49 | 비정상접근 탐지 알림함 화면 | mnt | ⬜ 미착수 |
+| 50 | 인증수단 관리 설정 화면 | mnt | ⬜ 미착수 |
+| 51 | 계정 잠금/휴면계정 관리 확장 화면 | mnt | ⬜ 미착수 |
+| 52 | 2차 인증(추가인증) 입력 화면 | frt | ⬜ 미착수 |
+| 53 | 마이페이지 인증수단 등록/관리 화면 | frt | ⬜ 미착수 |
+| 54 | 계정 잠금 안내 화면 | frt | ⬜ 미착수 |
+
+> UI 목업 브리프(스티치 AI 프롬프트 포함)는 `디자인.md`에 문서화 완료 — **설계 산출물은 준비됨**, 코드 구현은 미착수.
+
+### Ⅴ. 착수 즉시 확인 필요 사항 — arko-privacy-analyst
+
+| # | 확인항목 | 상태 | 리스크 |
+|:--:|----------|:----:|--------|
+| 55 | 운영 web.xml 필터 매핑 상태 확인 | ⬜ 미확인 | R1 — 소스/운영 배포본 상이 가능성 |
+| 56 | 휴면 배치 운영 로그 확인 | ⬜ 미확인 | MariaDB statement 부재 실운영 재확인 |
+| 57 | CONECT_IP 적재값 분포 확인 | ⬜ 미확인 | Apache+L4 이중구성 L4 IP 고정 여부 |
+| 58 | 인터셉터 등록부 현재 상태 확인 | ⬜ 미확인 | 등록 순서 재검증 |
+
+### Ⅵ. 보안·품질 점검 (SER-004) — arko-reviewer
+
+| # | 개발항목 | 상태 | 비고 |
+|:--:|----------|:----:|------|
+| 59 | 시큐어코딩 적용 및 점검 | ⬜ 미착수 | 행안부 기준 · 구현 후 |
+| 60 | 웹취약점 점검 | ⬜ 미착수 | 국정원 기준 · 개발 완료 후 |
+| 61 | 중점 점검 5종 확인 | ⬜ 미착수 | 인증우회/권한상승/접속기록조작/계정열거/인증정보노출 |
+
+---
+
+## 4. 완료된 선행 작업 (구현 착수 기반)
+
+| 산출물 | 위치 | 상태 |
+|--------|------|:----:|
+| 요구사항·기술설계 원문 | `E:\지역문화원\제안서\mds\Ⅲ_사업수행계획.md` (Ⅲ-2/3/4) | ✅ |
+| 개발항목 정리(61개) | `개인정보보호_개발항목.docx` | ✅ |
+| 신규 화면 디자인 브리프(10종) | `디자인.md` | ✅ |
+| 권한/인증 구현 에이전트 | `.claude/agents/arko-privacy-authctrl.md` | ✅ |
+| 접속기록 구현 에이전트 | `.claude/agents/arko-privacy-auditlog.md` | ✅ |
+| 요구사항 분석 에이전트 | `.claude/agents/arko-privacy-analyst.md` | ✅ |
+| 구현 패턴 스킬(권한/접속/추적/오케스트레이터) | `.claude/skills/arko-privacy-*` | ✅ |
+| 리뷰어 개인정보보호 점검항목 확장 | `.claude/agents/arko-reviewer.md` | ✅ |
+
+---
+
+## 5. 권장 착수 순서
+
+1. **Ⅴ. 착수전 확인(55~58)** — 운영 환경 실측으로 리스크 R1 등 조기 해소 (구현 전 필수)
+2. **Ⅰ-1) 권한관리(1~6)** — `getAuthorities()` 구현이 전체 권한통제의 뿌리 → 최우선
+3. **Ⅱ. 인증체계(14~29)** — 인터셉터 3단 순서 확정 후 2단계 인증·인증수단 3종
+4. **Ⅲ. 접속기록(30~44)** — 테이블·AOP·해시체인·배치
+5. **Ⅳ. 화면(45~54)** — 백엔드 진척에 맞춰 frt/mnt 병행
+6. **Ⅵ. 보안·품질(59~61)** — 각 기능 완료분 대상 상시 + 최종 웹취약점 점검
+
+## 6. 발주기관 확정 대기 항목 (블로커)
+
+| 항목 | 관련 # | 내용 |
+|------|:------:|------|
+| 계정 삭제 보존기간 | 12 | 취급자/회원 각 보존기간 정책 |
+| 문자발송모듈 연동 규격 | 19 | KT-TCS / MCSAgent3.0 연동 스펙 |
+
+---
+
+_본 문서는 `개인정보보호_개발항목.docx`의 61개 개발항목을 소스 실측(2026-07-21)과 대조해 작성한 진행현황표이다. 코드 구현 진척 시 각 항목의 상태(⬜→🟡→✅)와 소스 실측 열을 갱신한다._
diff --git a/개인정보보호_개발항목.docx b/개인정보보호_개발항목.docx
new file mode 100644
index 0000000000000000000000000000000000000000..941c81bee1819726ed71a5f28a933e4d3783148e
GIT binary patch
literal 41659
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