arko_renual/src/script/mysql/privacy.install_mysql.sql
infraurp 33bd57cae2 개인정보보호 기능개선 10차 — 접속·처리기록을 RFP대로 화면 1개로 정리
제안요청서 과업내용 "다"가 요구하는 화면은 하나다 —
"ㅇ 로그 조회 및 관리 기능 구현 / - 관리자용 로그 조회 화면 개발".
제안요약서 시안도 "접속기록 조회 및 감사" 한 장만 그려져 있다.
8·9차에서 이 영역을 여러 화면으로 벌려 놓은 것을 되돌린다.

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:23:21 +09:00

1771 lines
122 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- #####################################################################
-- arko_renual 설치용 통합 SQL (개인정보보호 기능개선 사업 · arko 대비 변동분)
-- 대상 DB : MySQL 5.7 / MariaDB (스키마 csv)
-- 구성 : [PART 1] DDL(테이블 생성·변경, 25개 테이블/인덱스)
-- → [PART 2] DML(기준/시드 데이터)
-- → [PART 3] 관리자 메뉴 등록 DML(좌측메뉴 노출, COMTNPROGRMLIST/
-- COMTNMENUINFO/COMTNMENUCREATDTLS,
-- 9100035~9100039 + 9100044 + 9100047 + 9100051)
-- ※ 접속·처리기록 영역의 화면은 9100044 "접속기록 조회 및 감사" 하나다
-- (RFP 과업내용 다 = "관리자용 로그 조회 화면" 단수, 시안도 한 장).
-- 처리기록·이상행위·파기이력은 그 화면 안에서 제공한다.
-- ※ 9100045(업무유형 매핑 관리)·9100046(로그인 접속기록)은 2026-08-11 폐기
-- — 폐기된 번호는 재사용하지 않는다(과거 권한매핑 잔재와 섞인다)
-- → [PART 4] 뷰 GNR 컷오버(comvnusermaster/comvnuserinfo, 회원 이름 암호화
-- 최종 단계) — **자체 게이트 내장**, 백필 미완료 시 자동 SKIP(PART 4 헤더 참조)
-- 실행순서: 반드시 PART 1 → PART 2 → PART 3 → PART 4 순서로 실행
-- (테이블 생성 → 시드 적재 → 메뉴 등록 → 뷰 컷오버. 메뉴는 FK로 프로그램
-- 등록을 먼저 요구하고, PART 4는 PART 1의 COMTNGNRLMBER 컬럼 추가를 전제한다)
-- 재실행 : **전 구간 재실행 안전(멱등)** — PART 1 은 CREATE TABLE IF NOT EXISTS 와
-- information_schema 존재 확인 후 실행되는 ALTER(컬럼/인덱스), PART 2·3 은
-- NOT EXISTS 가드, PART 4 는 자체 게이트(이미 적용됨/백필 미완료 시 SKIP)를 쓴다.
-- 이미 적용된 항목은 'SKIP: ...' 메시지만 출력하고 넘어가므로, 기존 환경에도
-- 이 파일 하나만 그대로 실행하면 된다(PART 4는 백필 완료 후 재실행 1회 필요할 수 있음).
-- (2026-08-05 이전 버전은 재실행 시 1050/1060/1061 오류가 났다)
-- ★★ PART 3 적용만으로는 좌측메뉴에 안 뜬다 — left.jsp 하드코딩 <c:when> 분기가
-- 반드시 함께 배포돼야 한다(소스에는 이미 반영됨). 상세는 PART 3 헤더 참조. ★★
-- ★★ PART 4(뷰 컷오버) 전에 회원 이름 백필 배치가 끝나 있어야 한다 —
-- /mnt/sec/memberNmCrypto/start.do?confirm=BACKFILL(POST, ROLE_ADMIN, 2026-08-11 신설
-- MemberNmCryptoController). 안 끝났으면 PART 4는 조용히 SKIP만 하고 넘어간다(에러 아님). ★★
-- 2026-08-11 개정: DDL 6건(access_control_policy/download_job/emplyr_scrty_index/
-- mber_nm_encrypt_create/mber_nm_dead_views_drop 5건→PART 1, mber_nm_view_hash_cutover
-- 1건→신설 PART 4) + 메뉴 1건(access_policy 9100047→PART 3)을 통합해 이 파일 하나가
-- 다시 완전한 정본이 되도록 했다(그 전까지 DDL 27건 중 19건만 커버하던 drift 해소).
-- `backup/privacy.mber_nm_backup_mysql.sql`(평문 백업)·
-- `backup/privacy.mber_nm_dead_views_backup_20260810.sql`(죽은 뷰 정의 백업)은
-- 되돌리기용 스냅샷 성격이라 이 통합 파일 실행 **전**에 별도로 실행하는 것을 권장하며
-- 통합하지 않았다(설치 로직이 아니라 사고 대비 아카이브라 실행 시점이 다름).
-- 2026-08-27 개정(운영 실측 대조 반영):
-- [운영 실측 2026-08-27 15:31] @@version=5.7.31-log (**MySQL 5.7, MariaDB 아님**),
-- @@lower_case_table_names=**0**(대소문자 구분),
-- @@sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,
-- ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
-- (로컬 8.0.46 은 lctn=1, sql_mode 는 사실상 동일 → sql_mode 는 위험요소 아님)
-- 운영 DDL 덤프(csv_structure_Dump20260520) ↔ 로컬 csv 전수 대조 결과를 반영했다.
-- ① [치명] 운영은 객체명이 **대문자**(`COMTNGNRLMBER`), 로컬은 소문자다. PART 4 의
-- REPLACE() 는 lower_case_table_names 와 무관하게 항상 대소문자를 구분하므로,
-- 종전의 소문자 하드코딩은 운영에서 5개 치환이 전부 불발되고도
-- CREATE OR REPLACE VIEW 가 **에러 없이 성공**해 USER_NM_HASH 없는 뷰를 남겼다.
-- → information_schema 에서 실제 저장된 케이스를 읽어 검색문자열을 조립하고,
-- 치환 결과가 기대(USER_NM_HASH 4회 + MBER_NM_ENC + 평문참조 0)와 다르면
-- SIGNAL SQLSTATE '45000' 으로 **중단**하는 하드 게이트를 추가했다.
-- ② PART 1 의 죽은 뷰 8개 DROP 도 같은 이유로 운영에서 조용히 실패했다(운영 실제명은
-- `excelAppconfirmList` 등 혼합 케이스). → information_schema 기반 동적 DROP 으로 교체.
-- ③ 로컬 DB 에만 있고 저장소 어느 SQL 에도 없던 인덱스 2건
-- (COMTNEMPLYRINFO.IX_EMPLYR_ESNTL, BIZ_GROUP.IX_BIZ_GROUP_ESNTL) 을 PART 1 에 추가.
-- ④ PART 4 끝단 '반영 후 검증' SELECT 들도 뷰명을 소문자로 직접 참조하고 있었다.
-- 운영에서는 뷰 교체가 **성공한 직후** 이 SELECT 가 1146 으로 터져, 성공한 컷오버가
-- 실패처럼 보인다. → @v_master/@v_info 를 쓰는 동적 SQL 로 교체.
-- ⑤ PART 1 죽은 뷰 8개는 DROP 전에 정의를 privacy_view_def_backup 에 백업하도록 했다.
-- (backup/privacy.mber_nm_dead_views_backup_20260810.sql 은 로컬 소문자 캡처라
-- 운영 롤백 자산으로 쓸 수 없다 — 해당 파일에도 경고를 달아 두었다)
-- #####################################################################
-- =====================================================================
-- 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 COLLATE=utf8mb4_unicode_ci
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 IF NOT EXISTS 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 COLLATE=utf8mb4_unicode_ci
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 IF NOT EXISTS 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 COLLATE=utf8mb4_unicode_ci
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 IF NOT EXISTS 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 COLLATE=utf8mb4_unicode_ci
COMMENT='권한 변경 신청·승인 워크플로(개인정보보호 SFR-001 #3)';
CREATE TABLE IF NOT EXISTS 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 COLLATE=utf8mb4_unicode_ci
COMMENT='권한이력 다운로드 증빙 이력(개인정보보호 SFR-001 #6)';
-- ─────────────────────────────────────────────────────────────
-- [PDAL_JOB_MAPPING — 2026-08-11 제거]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 업무유형 매핑 테이블(PDAL_JOB_MAPPING) 생성 DDL 과 변경자 컬럼 확장 DDL 이
-- 여기 있었다(구 privacy.job_mapping_create_mysql.sql / job_mapping_audit_cols_mysql.sql).
--
-- [제거 사유] 이 테이블은 폐기된 서비스 AOP 수집 방식(PersonalDataLogAspect)의
-- 분류 기준이었다. 2026-08-07 수집이 MenuAccessLogInterceptor(메뉴명 기반)로 바뀌면서
-- 소비자가 없어졌고, 2026-08-11 관리 화면(메뉴 9100045)까지 삭제하면서 DDL 을 뺐다.
-- 접속기록의 수행업무(JOB_SE_CODE/JOB_SE_NM) 자체는 그대로 유지된다 — 값의 출처가
-- 매핑 테이블에서 COMTNMENUINFO 메뉴명으로 바뀐 것뿐이다(SFR-004 요건 회귀 아님).
--
-- ★ 기존 운영 DB 의 PDAL_JOB_MAPPING 테이블은 DROP 하지 않는다(별도 판단 사항).
-- FK 로 참조하는 테이블이 없고 조회하는 sqlMap 도 남아 있지 않아 방치해도 무해하다.
-- =====================================================================
-- ─────────────────────────────────────────────────────────────
-- [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 IF NOT EXISTS 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 COLLATE=utf8mb4_unicode_ci
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 IF NOT EXISTS 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 COLLATE=utf8mb4_unicode_ci
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 IF NOT EXISTS 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 IF NOT EXISTS 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 COLLATE=utf8mb4_unicode_ci
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 경로로 처리한다.
--
-- [멱등] MySQL 8 은 ADD COLUMN/INDEX IF NOT EXISTS 미지원 → information_schema 로
-- 존재를 확인해 없을 때만 실행한다(재실행 안전, MySQL 8 / MariaDB 공통).
-- =====================================================================
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNDIAPAUSEMBER' AND COLUMN_NAME = 'MBER_TY_CODE') > 0,
'SELECT ''SKIP: COMTNDIAPAUSEMBER.MBER_TY_CODE already exists'' AS msg',
'ALTER TABLE COMTNDIAPAUSEMBER
ADD COLUMN MBER_TY_CODE CHAR(3) NOT NULL DEFAULT ''GNR''
COMMENT ''회원유형 GNR:일반회원 EMP:개인정보취급자'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 조회 성능(취급자/일반 분리 조회) 보조 인덱스
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNDIAPAUSEMBER' AND INDEX_NAME = 'IX_DIAPAUSE_MBERTY') > 0,
'SELECT ''SKIP: IX_DIAPAUSE_MBERTY already exists'' AS msg',
'ALTER TABLE COMTNDIAPAUSEMBER
ADD INDEX IX_DIAPAUSE_MBERTY (MBER_TY_CODE, DIAPAUSE_STAT)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ─────────────────────────────────────────────────────────────
-- [privacy.account_mng_policy_purge_useyn_mysql.sql]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 계정 관리 정책 확장 : ACCOUNT_MNG_POLICY 에 파기 배치 사용여부 컬럼 추가
-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 개발항목 #12)
-- 작성일: 2026-07-28
-- ---------------------------------------------------------------------
-- 파기(#12)는 되돌릴 수 없는 처리라 배치를 항상 등록해 두되 실제 수행 여부를
-- 이 플래그로 제어한다('N'이면 서비스가 즉시 0건 반환). 기본 'N'이므로 설치
-- 직후에는 아무것도 파기되지 않으며, 관리자가 계정 관리 정책 화면(#13)에서
-- '사용'으로 바꿔야 시작된다. 파기 기준일은 기존 DELETE_PRESERVE_DAY 를 사용.
--
-- ※ 신규 설치(위 PART 1 에서 ACCOUNT_MNG_POLICY 를 막 생성한 경우)에는 컬럼이
-- 없으므로 추가되고, 기존 환경 재실행 시에는 존재 확인 후 건너뛴다(재실행 안전).
-- =====================================================================
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ACCOUNT_MNG_POLICY' AND COLUMN_NAME = 'PURGE_USE_YN') > 0,
'SELECT ''SKIP: ACCOUNT_MNG_POLICY.PURGE_USE_YN already exists'' AS msg',
'ALTER TABLE ACCOUNT_MNG_POLICY
ADD COLUMN PURGE_USE_YN CHAR(1) NOT NULL DEFAULT ''N''
COMMENT ''장기 미사용 계정 파기 배치 사용여부 Y:사용 N:미사용(#12)'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ─────────────────────────────────────────────────────────────
-- [privacy.account_delete_history_create_mysql.sql]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 장기 미사용 계정 개인정보 파기 이력 : ACCOUNT_DELETE_HISTORY
-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-002, 개발항목 #12 계정 삭제 기능)
-- 작성일: 2026-07-28 DB: MySQL 8 / MariaDB
-- ---------------------------------------------------------------------
-- [목적] 휴면 확정(COMTNDIAPAUSEMBER.DIAPAUSE_STAT='P') 후 정책상 보존기간
-- (ACCOUNT_MNG_POLICY.DELETE_PRESERVE_DAY)이 경과한 계정의 개인정보를
-- 파기(마스킹)한 이력을 남긴다. 실제 파기 방식은 행 DELETE가 아니라
-- PII 컬럼 마스킹(기존 회원탈퇴 MBER_STTUS='D' 컨벤션과 동일) — 다른
-- 테이블(게시글·마일리지·활동확인 등)이 MBER_ID/EMPLYR_ID를 참조하는
-- 소프트 조인이 많아 행 자체를 지우면 그쪽이 깨지기 때문.
--
-- [보존기간] ACCOUNT_MNG_POLICY.DELETE_PRESERVE_DAY 값을 실행 시점에 그대로
-- 조회해 사용(하드코딩 없음). 이 테이블의 PRESERVE_DAY 컬럼은
-- 그 시점에 적용된 값의 스냅샷(정책이 나중에 바뀌어도 이력은 불변).
--
-- [처리량 제한] 배치가 오래 정지돼 있다가 재가동되는 날 백로그가 한꺼번에
-- 몰리는 것을 막기 위해 1회 처리건수를 상한(LIMIT 200)으로 제한.
--
-- ★ 배치(BatchScheduler.purgeExpiredAccountScheduler)는 파기가 되돌릴 수 없는
-- 처리라 기본 비활성(@Scheduled 주석) 상태다. 보존기간 확정·운영협의 후 활성화.
-- =====================================================================
CREATE TABLE IF NOT EXISTS ACCOUNT_DELETE_HISTORY (
DEL_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '파기 일련번호(PK)',
MBER_ID VARCHAR(32) NOT NULL COMMENT '파기 대상 계정ID(MBER_ID/EMPLYR_ID)',
MBER_TY_CODE CHAR(3) NOT NULL COMMENT '회원유형 GNR:일반회원 EMP:개인정보취급자',
ESNTL_ID VARCHAR(20) NULL COMMENT '고유식별자(파기 시점 스냅샷, 참고용)',
DIAPAUSE_DT DATETIME NULL COMMENT '휴면 확정 기준일시(파기 판단 기준)',
PRESERVE_DAY INT NOT NULL COMMENT '파기 시점 적용된 보존기간(정책값 스냅샷, 일)',
DEL_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '파기 처리 일시',
PRIMARY KEY (DEL_ID),
KEY IX_ADH_MBER (MBER_ID),
KEY IX_ADH_DELDT (DEL_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='장기 미사용 계정 개인정보 파기 이력(개인정보보호 SFR-002 #12)';
-- ─────────────────────────────────────────────────────────────
-- [privacy.login_history_extend_mysql.sql]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 인증 실패 이력 확장 : LOGIN_HISTORY (IP 기록 · 보관기간)
-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001, 개발항목 #22-1)
-- 작성일: 2026-08-05
-- ---------------------------------------------------------------------
-- [목적] #22 로 살아난 실패이력 적재에 ① 접속 IP ② 보존(성공 시 DELETE→소프트
-- 리셋) ③ 보관기간 정책을 추가한다. 잠금 카운트는 RESET_YN='N' 인
-- 당일 행만 세므로 계정잠금 동작은 종전과 동일하다.
--
-- ★ 이 ALTER 는 sqlmap 동반 배포 필수 ★
-- loginDAO.insertFailHistory 가 컬럼목록 없는 `VALUES(...)` 3값 INSERT 라
-- 컬럼 추가 후 ERROR 1136(Column count doesn't match)로 실패한다.
-- 컬럼 명시 INSERT 로 교체된 EgovLoginUsr_SQL_Mysql.xml 과 함께 배포할 것.
--
-- [재실행] MySQL 8 은 ADD COLUMN/KEY IF NOT EXISTS 미지원 → 항목마다 information_schema
-- 로 존재를 확인해 없을 때만 실행한다(일부만 적용된 환경도 나머지가 채워짐).
-- =====================================================================
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'LOGIN_HISTORY' AND COLUMN_NAME = 'CONECT_IP') > 0,
'SELECT ''SKIP: LOGIN_HISTORY.CONECT_IP already exists'' AS msg',
'ALTER TABLE LOGIN_HISTORY
ADD COLUMN CONECT_IP VARCHAR(45) NULL COMMENT ''접속 IP(#22-1, ClientIpResolver 취득)'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'LOGIN_HISTORY' AND COLUMN_NAME = 'RESET_YN') > 0,
'SELECT ''SKIP: LOGIN_HISTORY.RESET_YN already exists'' AS msg',
'ALTER TABLE LOGIN_HISTORY
ADD COLUMN RESET_YN CHAR(1) NOT NULL DEFAULT ''N''
COMMENT ''잠금카운터 리셋여부 Y:성공으로 리셋됨 N:유효(#22-1)'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'LOGIN_HISTORY' AND INDEX_NAME = 'IX_LOGIN_HISTORY_01') > 0,
'SELECT ''SKIP: IX_LOGIN_HISTORY_01 already exists'' AS msg',
'ALTER TABLE LOGIN_HISTORY ADD KEY IX_LOGIN_HISTORY_01 (CREATE_DATE)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'LOGIN_HISTORY' AND INDEX_NAME = 'IX_LOGIN_HISTORY_02') > 0,
'SELECT ''SKIP: IX_LOGIN_HISTORY_02 already exists'' AS msg',
'ALTER TABLE LOGIN_HISTORY ADD KEY IX_LOGIN_HISTORY_02 (ID, GUBUN, CREATE_DATE)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ACCOUNT_MNG_POLICY' AND COLUMN_NAME = 'LOGIN_HIST_KEEP_DAY') > 0,
'SELECT ''SKIP: ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY already exists'' AS msg',
'ALTER TABLE ACCOUNT_MNG_POLICY
ADD COLUMN LOGIN_HIST_KEEP_DAY INT NOT NULL DEFAULT 730
COMMENT ''인증 실패 이력 보관기간(일, 기본 730=2년, #22-1)'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ─────────────────────────────────────────────────────────────
-- [privacy.conect_log_extend_mysql.sql]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 로그인·로그아웃 접속기록 확장 : COMTNLOGINLOG + CONECT_IP_LOCATION
-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001 / SER-002)
-- 개발항목 62 - 관리자 로그인·로그아웃 접속기록 조회·다운로드
-- 작성일: 2026-08-07 DB: MySQL 8 / MariaDB
-- ---------------------------------------------------------------------
-- [AS-IS 실측 2026-08-07] COMTNLOGINLOG 는 이미 살아 있으나(373,959행)
-- CONECT_IP 가 전 건 NULL 이고 접속위치 컬럼이 없다. 인덱스도 PK 하나뿐.
-- 또 CONECT_ID(고유식별자) 적재값이 16자리 hex 라 회원 테이블 4종 어디와도 0건 매칭된다(실측).
-- → 접속 계정ID·사용자구분을 별도 컬럼(CONECT_ACNT_ID / CONECT_USER_SE)에 직접 적재한다.
-- ★★ sqlmap(EgovLoginLog_SQL_Mysql.xml) 동반 배포 필수 ★★
-- logInsertLoginLog 가 CONECT_LC / CONECT_ACNT_ID / CONECT_USER_SE 를 컬럼 목록에 넣도록 바뀐다.
-- DDL 없이 sqlmap 만 올리면 Unknown column 으로 접속기록 적재가 깨진다.
-- =====================================================================
SET @ddl := IF(
(SELECT CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND COLUMN_NAME = 'CONECT_IP') >= 45,
'SELECT ''SKIP: COMTNLOGINLOG.CONECT_IP already >= 45'' AS msg',
'ALTER TABLE COMTNLOGINLOG
MODIFY COLUMN CONECT_IP VARCHAR(45) NULL COMMENT ''접속 IP(IPv6 대응)'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND COLUMN_NAME = 'CONECT_LC') > 0,
'SELECT ''SKIP: COMTNLOGINLOG.CONECT_LC already exists'' AS msg',
'ALTER TABLE COMTNLOGINLOG
ADD COLUMN CONECT_LC VARCHAR(100) NULL COMMENT ''접속위치명(적재시점 스냅샷)'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND COLUMN_NAME = 'CONECT_ACNT_ID') > 0,
'SELECT ''SKIP: COMTNLOGINLOG.CONECT_ACNT_ID already exists'' AS msg',
'ALTER TABLE COMTNLOGINLOG
ADD COLUMN CONECT_ACNT_ID VARCHAR(20) NULL COMMENT ''접속 계정ID(로그인 ID)'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND COLUMN_NAME = 'CONECT_USER_SE') > 0,
'SELECT ''SKIP: COMTNLOGINLOG.CONECT_USER_SE already exists'' AS msg',
'ALTER TABLE COMTNLOGINLOG
ADD COLUMN CONECT_USER_SE VARCHAR(3) NULL COMMENT ''사용자구분 USR/GNR/GRO/ENT'''
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND INDEX_NAME = 'IX_CLG_ACNT_DT') > 0,
'SELECT ''SKIP: IX_CLG_ACNT_DT already exists'' AS msg',
'CREATE INDEX IX_CLG_ACNT_DT ON COMTNLOGINLOG (CONECT_ACNT_ID, CREAT_DT)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND INDEX_NAME = 'IX_CLG_DT') > 0,
'SELECT ''SKIP: IX_CLG_DT already exists'' AS msg',
'CREATE INDEX IX_CLG_DT ON COMTNLOGINLOG (CREAT_DT)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND INDEX_NAME = 'IX_CLG_ID_DT') > 0,
'SELECT ''SKIP: IX_CLG_ID_DT already exists'' AS msg',
'CREATE INDEX IX_CLG_ID_DT ON COMTNLOGINLOG (CONECT_ID, CREAT_DT)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND INDEX_NAME = 'IX_CLG_MTHD_DT') > 0,
'SELECT ''SKIP: IX_CLG_MTHD_DT already exists'' AS msg',
'CREATE INDEX IX_CLG_MTHD_DT ON COMTNLOGINLOG (CONECT_MTHD, CREAT_DT)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNLOGINLOG' AND INDEX_NAME = 'IX_CLG_LC') > 0,
'SELECT ''SKIP: IX_CLG_LC already exists'' AS msg',
'CREATE INDEX IX_CLG_LC ON COMTNLOGINLOG (CONECT_LC)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS CONECT_IP_LOCATION (
LOC_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '일련번호(PK)',
IP_CIDR VARCHAR(45) NOT NULL COMMENT 'IP 대역(CIDR 표기)',
LOC_NM VARCHAR(100) NOT NULL COMMENT '접속위치명',
LOC_SE CHAR(1) NOT NULL DEFAULT 'E' COMMENT '내외부 구분 I:내부망 E:외부',
SORT_ORDR INT NOT NULL DEFAULT 0 COMMENT '정렬순서',
USE_AT CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '사용여부 Y/N',
REMARK VARCHAR(200) NULL COMMENT '비고',
REGISTER_ID VARCHAR(20) NULL COMMENT '등록자 계정ID',
REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '등록일시',
UPDUSR_ID VARCHAR(20) NULL COMMENT '최종 변경자 계정ID',
UPDT_DT DATETIME NULL COMMENT '최종 변경일시',
PRIMARY KEY (LOC_ID),
UNIQUE KEY UK_CIL_CIDR (IP_CIDR)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='접속위치 매핑(IP 대역 → 위치명) — 로그인·로그아웃 접속기록 62';
-- ─────────────────────────────────────────────────────────────
-- [privacy.author_hist_download_job_create_mysql.sql]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 권한변경 이력 CSV 비동기 다운로드 작업 : AUTHOR_HIST_DOWNLOAD_JOB
-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-001 / 개발항목 6)
-- ---------------------------------------------------------------------
-- 종전 동기 다운로드는 조건 전체를 메모리에 올려 응답에 실었다. 이 테이블은
-- '다운로드 요청'을 작업으로 등록해 두고 백그라운드가 파일을 만든 뒤, 완료된 것만
-- 내려받게 하기 위한 작업 대장이다. 상태 전이: W(대기) -> R(생성중) -> S(완료) -> X(파기), 실패는 F.
-- FILE_PATH 는 반드시 웹 루트 밖(Globals.privacyDownloadPath). 파일은 암호화 저장하고
-- EXPIRE_DT 경과분은 파기 배치가 삭제한다. 내려받기 증빙은 AUTHOR_HIST_DOWNLOAD_HISTORY 가 담당.
-- =====================================================================
CREATE TABLE IF NOT EXISTS AUTHOR_HIST_DOWNLOAD_JOB (
JOB_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '작업 일련번호(PK)',
REQUESTER_ID VARCHAR(20) NOT NULL COMMENT '요청자 계정ID(취급자)',
REQUESTER_IP VARCHAR(45) NULL COMMENT '요청 IP(IPv6 대응 45)',
DL_REASON VARCHAR(500) NOT NULL COMMENT '다운로드 사유(필수)',
SEARCH_COND VARCHAR(1000) NULL COMMENT '검색조건 요약(표시용)',
F_TRGET_ID VARCHAR(50) NULL COMMENT '조건: 대상자ID(계정ID 또는 고유식별자)',
F_CHANGER_ID VARCHAR(50) NULL COMMENT '조건: 변경자 계정ID',
F_CHG_SE_CODE CHAR(1) NULL COMMENT '조건: 변경구분 G/M/R',
F_FROM_DT VARCHAR(19) NULL COMMENT '조건: 기간 시작',
F_TO_DT VARCHAR(19) NULL COMMENT '조건: 기간 종료',
STATUS CHAR(1) NOT NULL DEFAULT 'W' COMMENT '상태 W:대기 R:생성중 S:완료 F:실패 X:파기',
TOTAL_CNT INT NOT NULL DEFAULT 0 COMMENT '대상 총건수',
DONE_CNT INT NOT NULL DEFAULT 0 COMMENT '기록 완료 건수(진행률)',
FILE_NM VARCHAR(200) NULL COMMENT '내려받을 때 보여줄 파일명',
FILE_PATH VARCHAR(300) NULL COMMENT '서버 저장 절대경로(웹 루트 밖, 암호화 저장)',
FILE_SIZE BIGINT NOT NULL DEFAULT 0 COMMENT '암호화 파일 크기(byte)',
ERR_MSG VARCHAR(500) NULL COMMENT '실패 사유',
DOWNLOAD_CNT INT NOT NULL DEFAULT 0 COMMENT '내려받은 횟수',
LAST_DL_DT DATETIME NULL COMMENT '마지막 내려받은 일시',
REG_DT DATETIME NOT NULL COMMENT '요청 일시',
START_DT DATETIME NULL COMMENT '생성 시작 일시',
END_DT DATETIME NULL COMMENT '생성 종료 일시',
EXPIRE_DT DATETIME NULL COMMENT '파기 예정 일시(경과 시 파일 삭제)',
PRIMARY KEY (JOB_ID),
KEY IX_AHDJ_REQUESTER (REQUESTER_ID, REG_DT),
KEY IX_AHDJ_STATUS (STATUS, REG_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='권한변경 이력 CSV 비동기 다운로드 작업';
-- ─────────────────────────────────────────────────────────────
-- [privacy.account_action_history_create_mysql.sql]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 장기 미사용 계정 조치 이력 : ACCOUNT_ACTION_HISTORY
-- 개발항목 #11 — 미사용계정 현황 화면의 '선택 잠금 / 선택 말소' 조치 기록.
-- 잠금 '해제' 이력은 ACCOUNT_UNLOCK_HISTORY(SER-001 #24)가 담당하며 역할이 다르다.
-- 애플리케이션 계층은 INSERT 만 정의(UPDATE/DELETE statement 없음).
-- =====================================================================
CREATE TABLE IF NOT EXISTS ACCOUNT_ACTION_HISTORY (
ACTION_HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '조치 이력 일련번호(PK)',
TRGET_ID VARCHAR(50) NOT NULL COMMENT '조치 대상 계정 ID(EMPLYR_ID)',
TRGET_SE VARCHAR(10) NULL DEFAULT 'EMP' COMMENT '대상 회원구분(취급자 EMP)',
ACTION_TY CHAR(1) NOT NULL COMMENT '조치 유형 L:잠금(휴면확정) D:말소',
ACTION_USER_ID VARCHAR(50) NULL COMMENT '조치 처리자(관리자) ID',
ACTION_RESN VARCHAR(500) NULL COMMENT '조치 사유(화면 입력 필수)',
ACTION_IP VARCHAR(45) NULL COMMENT '조치자 접속 IP(IPv6 대응, X-Forwarded-For 보정값)',
NO_CONECT_DAY INT NULL DEFAULT 0 COMMENT '조치 시점의 미접속 일수(사후 검증용 스냅샷)',
ACTION_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '조치 일시',
PRIMARY KEY (ACTION_HIST_ID),
KEY IX_AAH_TRGET (TRGET_ID, ACTION_DT),
KEY IX_AAH_USER (ACTION_USER_ID, ACTION_DT),
KEY IX_AAH_DT (ACTION_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='장기 미사용 계정 조치(잠금/말소) 이력(개인정보보호 SFR-002 #11)';
-- ─────────────────────────────────────────────────────────────
-- [privacy.login_lock_exempt_create_mysql.sql]
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 로그인 실패 계정잠금 예외 : LOGIN_LOCK_EXEMPT (화이트리스트, SER-001 24)
-- ---------------------------------------------------------------------
-- 인증 실패 5회 잠금은 userSe 구분 없이 전 계정에 적용되고, 잠금 해제 화면도
-- 관리자 로그인이 있어야 들어갈 수 있어 최고관리자가 잠기면 아무도 해제할 수 없다.
-- 이 테이블은 그 break-glass 통로다. 잠금 '판정'만 면제하며, 실패 이력(LOGIN_HISTORY)
-- 적재와 2차 인증(OTP)은 예외 계정에도 그대로 적용된다.
-- 시드 없음 — 예외 대상 ID는 관리자 화면(접근통제 정책)에서 운영팀이 직접 등록한다.
-- =====================================================================
CREATE TABLE IF NOT EXISTS LOGIN_LOCK_EXEMPT (
EXEMPT_ID VARCHAR(20) NOT NULL COMMENT '잠금 예외 대상 계정ID(COMTNEMPLYRINFO.EMPLYR_ID)',
EXEMPT_RESN VARCHAR(500) NOT NULL COMMENT '예외 등록 사유(필수 - 감사 대응)',
REG_ID VARCHAR(20) NULL COMMENT '등록한 관리자 계정ID',
REG_DT DATETIME NULL COMMENT '등록일시',
PRIMARY KEY (EXEMPT_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='로그인 실패 계정잠금 예외 계정(개인정보보호 SER-001 24, 최고관리자 잠김 방지)';
-- ─────────────────────────────────────────────────────────────
-- [privacy.access_control_policy_create_mysql.sql] (2026-08-11 통합)
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 접근통제 정책 : ACCESS_CONTROL_POLICY (단일행 정책 테이블)
-- 문화품앗이 개인정보보호 기능개선 사업 (SER-001 요건 4·5 / 접근통제 정책 적용)
-- [용도] 계정 잠금 임계값·자동 로그아웃 시간·세션 재발급 여부를 관리자 설정값으로 운영.
-- [반영 경로] LOCK_THRESHOLD→LOGIN_LOCK_COUNT 동기화 / AUTO_LOGOUT_MIN→SessionPolicyInterceptor
-- / SESSION_REGEN_YN→SecondFactorController 세션 재발급
-- [재실행 안전] CREATE TABLE IF NOT EXISTS + 시드 NOT EXISTS 가드
-- =====================================================================
CREATE TABLE IF NOT EXISTS ACCESS_CONTROL_POLICY (
POLICY_ID INT NOT NULL DEFAULT 1 COMMENT '정책 식별자(단일행=1)',
LOCK_THRESHOLD INT NOT NULL DEFAULT 5 COMMENT '인증 실패 계정 잠금 임계값(회). 현행 10 -> 5',
UNLOCK_MODE VARCHAR(20) NOT NULL DEFAULT 'ADMIN_HIST' COMMENT '잠금 해제 방식 ADMIN_HIST:관리자화면+이력 / ADMIN_HIST_RESN:사유 필수',
AUTO_LOGOUT_MIN INT NOT NULL DEFAULT 30 COMMENT '미사용 자동 로그아웃(분). 현행 web.xml 60 -> 30',
EXPIRE_NOTICE_YN CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '세션 만료 임박 안내 사용여부 Y/N',
EXPIRE_NOTICE_MIN INT NOT NULL DEFAULT 5 COMMENT '만료 임박 안내 시점(만료 N분 전)',
SESSION_REGEN_YN CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '2차 인증 성공 시 세션 재발급(세션고정 차단) 사용여부 Y/N',
UPD_ID VARCHAR(20) NULL COMMENT '최종 수정자',
UPD_DT DATETIME NULL COMMENT '최종 수정일시',
PRIMARY KEY (POLICY_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='접근통제 정책(개인정보보호 SER-001 요건4-5, 단일행)';
INSERT INTO ACCESS_CONTROL_POLICY
(POLICY_ID, LOCK_THRESHOLD, UNLOCK_MODE, AUTO_LOGOUT_MIN,
EXPIRE_NOTICE_YN, EXPIRE_NOTICE_MIN, SESSION_REGEN_YN, UPD_ID, UPD_DT)
SELECT 1, 5, 'ADMIN_HIST', 30, 'Y', 5, 'Y', 'SYSTEM', NOW()
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM ACCESS_CONTROL_POLICY WHERE POLICY_ID = 1);
-- 인증 경로(EgovLoginController/EgovSpringSecurityLoginFilter)가 읽는 LOGIN_LOCK_COUNT 를
-- 정책값과 일치시킨다(화면 저장 시에도 AccessControlPolicyServiceImpl 이 동일하게 동기화).
UPDATE LOGIN_LOCK_COUNT
SET LOCK_COUNT = (SELECT LOCK_THRESHOLD FROM ACCESS_CONTROL_POLICY WHERE POLICY_ID = 1);
INSERT INTO LOGIN_LOCK_COUNT (LOCK_COUNT)
SELECT (SELECT LOCK_THRESHOLD FROM ACCESS_CONTROL_POLICY WHERE POLICY_ID = 1)
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM LOGIN_LOCK_COUNT);
-- ─────────────────────────────────────────────────────────────
-- [privacy.download_job_create_mysql.sql] (2026-08-11 통합)
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 개인정보 접속기록 CSV 비동기 다운로드 작업 : PDAL_DOWNLOAD_JOB (SFR-004-6 / SER-002)
-- [보안 전제] FILE_PATH 는 반드시 웹 루트 밖(Globals.privacyDownloadPath), 저장 파일은
-- 암호화(평문 CSV 미보관). EXPIRE_DT 경과분은 파기 배치가 파일 삭제+STATUS='X'.
-- [상태 전이] W(대기) -> R(생성중) -> S(완료) -> X(파기) / 실패는 F
-- =====================================================================
CREATE TABLE IF NOT EXISTS PDAL_DOWNLOAD_JOB (
JOB_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '작업 일련번호(PK)',
REQUESTER_ID VARCHAR(20) NOT NULL COMMENT '요청자 계정ID(취급자)',
REQUESTER_IP VARCHAR(45) NULL COMMENT '요청 IP(IPv6 대응 45)',
DL_REASON VARCHAR(500) NOT NULL COMMENT '다운로드 사유(필수)',
SEARCH_COND VARCHAR(1000) NULL COMMENT '검색조건 요약(개인정보 본문 미포함)',
F_CONECT_ID VARCHAR(50) NULL COMMENT '조건: 접속자ID',
F_JOB_SE_CODE VARCHAR(30) NULL COMMENT '조건: 수행업무 코드',
F_TRGET_ID VARCHAR(50) NULL COMMENT '조건: 처리대상 식별자',
F_CONECT_IP VARCHAR(45) NULL COMMENT '조건: 접속IP',
F_DML_GUBUN CHAR(1) NULL COMMENT '조건: 처리구분 S/I/U/D',
F_PROCESS_RESULT CHAR(1) NULL COMMENT '조건: 처리결과 S/F',
F_FROM_DT VARCHAR(19) NULL COMMENT '조건: 기간 시작',
F_TO_DT VARCHAR(19) NULL COMMENT '조건: 기간 종료',
STATUS CHAR(1) NOT NULL DEFAULT 'W' COMMENT '상태 W:대기 R:생성중 S:완료 F:실패 X:파기',
TOTAL_CNT INT NOT NULL DEFAULT 0 COMMENT '대상 총건수',
DONE_CNT INT NOT NULL DEFAULT 0 COMMENT '기록 완료 건수(진행률)',
FILE_NM VARCHAR(200) NULL COMMENT '내려받을 때 보여줄 파일명',
FILE_PATH VARCHAR(300) NULL COMMENT '서버 저장 절대경로(웹 루트 밖, 암호화 저장)',
FILE_SIZE BIGINT NOT NULL DEFAULT 0 COMMENT '암호화 파일 크기(byte)',
ERR_MSG VARCHAR(500) NULL COMMENT '실패 사유',
DOWNLOAD_CNT INT NOT NULL DEFAULT 0 COMMENT '내려받은 횟수',
LAST_DL_DT DATETIME NULL COMMENT '마지막 내려받은 일시',
REG_DT DATETIME NOT NULL COMMENT '요청 일시',
START_DT DATETIME NULL COMMENT '생성 시작 일시',
END_DT DATETIME NULL COMMENT '생성 종료 일시',
EXPIRE_DT DATETIME NULL COMMENT '파기 예정 일시(경과 시 파일 삭제)',
PRIMARY KEY (JOB_ID),
KEY IX_PDJ_REQUESTER (REQUESTER_ID, REG_DT),
KEY IX_PDJ_STATUS (STATUS, REG_DT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='개인정보 접속기록 CSV 비동기 다운로드 작업';
-- ─────────────────────────────────────────────────────────────
-- [privacy.emplyr_scrty_index_mysql.sql] (2026-08-11 통합)
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 권한 매핑 조회 인덱스 : COMTNEMPLYRSCRTYESTBS(SCRTY_DTRMN_TRGET_ID) (SFR-001/SFR-002)
-- [문제] 인덱스 0개(운영 데이터 기준 약 8만행) — 로그인마다 권한조회 풀스캔, 계정당 약 200ms.
-- [재실행 안전] information_schema 로 존재 확인 후 생성(MySQL 은 CREATE INDEX IF NOT EXISTS 미지원)
-- =====================================================================
SET @idx := (SELECT COUNT(1) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNEMPLYRSCRTYESTBS'
AND INDEX_NAME = 'IX_EMPLYRSCRTY_TRGET');
SET @ddl := IF(@idx > 0,
'SELECT ''SKIP: IX_EMPLYRSCRTY_TRGET already exists'' AS msg',
'CREATE INDEX IX_EMPLYRSCRTY_TRGET ON COMTNEMPLYRSCRTYESTBS (SCRTY_DTRMN_TRGET_ID)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ─────────────────────────────
-- [2026-08-27 드리프트 보완] 스크립트에 빠져 있던 ESNTL_ID 조회 인덱스 2건
-- ─────────────────────────────
-- =====================================================================
-- [발견 경위] 운영 스키마 덤프(csv_structure_Dump20260520) ↔ 로컬 csv 스키마
-- 전수 대조 결과, 로컬에만 존재하고 저장소 어느 SQL 파일에도 없는
-- 인덱스 2개를 발견했다(개발 중 DB에 직접 수실행하고 스크립트화를 누락).
-- 이 파일만 운영에 돌리면 둘 다 누락돼 로컬에서만 빠른 조회가 된다.
--
-- · COMTNEMPLYRINFO(ESNTL_ID) : comvnusermaster/comvnuserinfo 뉴의 USR 분기와
-- 권한·미사용계정 화면이 ESNTL_ID 로 조인한다.
-- · BIZ_GROUP(ESNTL_ID) : 동일 뉴의 GRO 분기.
--
-- [안전성] 추가(additive) 인덱스라 기존 동작에 영향 없음.
-- [재실행] information_schema 존재 확인 후 생성 — 반복 실행 무해.
-- [롤백] DROP INDEX IX_EMPLYR_ESNTL ON COMTNEMPLYRINFO;
-- DROP INDEX IX_BIZ_GROUP_ESNTL ON BIZ_GROUP;
-- =====================================================================
SET @idx := (SELECT COUNT(1) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'COMTNEMPLYRINFO'
AND INDEX_NAME = 'IX_EMPLYR_ESNTL');
SET @ddl := IF(@idx > 0,
'SELECT ''SKIP: IX_EMPLYR_ESNTL already exists'' AS msg',
'CREATE INDEX IX_EMPLYR_ESNTL ON COMTNEMPLYRINFO (ESNTL_ID)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @idx := (SELECT COUNT(1) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'BIZ_GROUP'
AND INDEX_NAME = 'IX_BIZ_GROUP_ESNTL');
SET @ddl := IF(@idx > 0,
'SELECT ''SKIP: IX_BIZ_GROUP_ESNTL already exists'' AS msg',
'CREATE INDEX IX_BIZ_GROUP_ESNTL ON BIZ_GROUP (ESNTL_ID)'
);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ─────────────────────────────────────────────────────────────
-- [privacy.mber_nm_encrypt_create_mysql.sql] (2026-08-11 통합)
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 회원 이름(MBER_NM) 암호화 컬럼 추가 : COMTNGNRLMBER (추가제안 Action4)
-- [대상] COMTNGNRLMBER 단독(실측 77,053건). biz_mber_ssn/biz_join_event/biz_stat_summary_* 는
-- 5,000건 기준 미달로 미대상.
-- [설계] 평문 컬럼 유지 + 별도 컬럼 추가(MBER_NM_ENC/MBER_NM_HASH/CRYPTO_KEY_VER).
-- MBER_NM_ENC IS NULL 로 미암호화 여부를 판정하므로 이중 암호화 사고가 원천 차단된다.
-- [검색] AES-GCM 랜덤IV라 부분일치 불가 — 완전일치는 HMAC-SHA256 해시(MBER_NM_HASH)로 별도 지원.
-- [무중단] ALGORITHM=INPLACE, LOCK=NONE.
-- ★★ 이 DDL 적용 후, PART 4(뷰 GNR 컷오버) 실행 전에 회원 이름 백필 배치가 반드시
-- 완료돼야 한다 — /mnt/sec/memberNmCrypto/start.do?confirm=BACKFILL (POST, ROLE_ADMIN)
-- 로 트리거. PART 4 자체에 미완료 시 자동 SKIP 가드가 있다. ★★
-- =====================================================================
DROP PROCEDURE IF EXISTS PRC_PRIVACY_ADD_NM_CRYPTO_COLS;
DELIMITER $$
CREATE PROCEDURE PRC_PRIVACY_ADD_NM_CRYPTO_COLS()
BEGIN
DECLARE v_cnt INT DEFAULT 0;
SELECT COUNT(*) INTO v_cnt FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'COMTNGNRLMBER'
AND COLUMN_NAME = 'MBER_NM_ENC';
IF v_cnt = 0 THEN
ALTER TABLE COMTNGNRLMBER
ADD COLUMN MBER_NM_ENC VARCHAR(255) NULL
COMMENT '이름 암호문 Base64(IV12||CT||TAG16), AES-128/GCM',
ALGORITHM=INPLACE, LOCK=NONE;
END IF;
SELECT COUNT(*) INTO v_cnt FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'COMTNGNRLMBER'
AND COLUMN_NAME = 'MBER_NM_HASH';
IF v_cnt = 0 THEN
ALTER TABLE COMTNGNRLMBER
ADD COLUMN MBER_NM_HASH CHAR(64) NULL
COMMENT '이름 완전일치 검색용 HMAC-SHA256(pepper) 소문자 hex',
ALGORITHM=INPLACE, LOCK=NONE;
END IF;
SELECT COUNT(*) INTO v_cnt FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'COMTNGNRLMBER'
AND COLUMN_NAME = 'CRYPTO_KEY_VER';
IF v_cnt = 0 THEN
ALTER TABLE COMTNGNRLMBER
ADD COLUMN CRYPTO_KEY_VER TINYINT NULL
COMMENT '암호화 키 버전(키 교체 대비). 미암호화 행은 NULL',
ALGORITHM=INPLACE, LOCK=NONE;
END IF;
SELECT COUNT(*) INTO v_cnt FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'COMTNGNRLMBER'
AND INDEX_NAME = 'IX_GNRLMBER_NM_HASH';
IF v_cnt = 0 THEN
CREATE INDEX IX_GNRLMBER_NM_HASH ON COMTNGNRLMBER (MBER_NM_HASH);
END IF;
END$$
DELIMITER ;
CALL PRC_PRIVACY_ADD_NM_CRYPTO_COLS();
DROP PROCEDURE IF EXISTS PRC_PRIVACY_ADD_NM_CRYPTO_COLS;
-- 백필 진행상태 테이블. 커서를 잃어도 MBER_NM_ENC IS NULL 조건이 있어 재실행하면 복구된다.
-- LAST_KEY 가 VARBINARY 인 이유: COMTNGNRLMBER.MBER_ID 가 VARBINARY(32)이고 비인쇄 바이트 포함행 존재.
CREATE TABLE IF NOT EXISTS MIG_CRYPTO_PROGRESS (
JOB_ID VARCHAR(50) NOT NULL COMMENT '작업 식별자(예: COMTNGNRLMBER.MBER_NM)',
LAST_KEY VARBINARY(32) NULL COMMENT '마지막 처리 PK(커서, 원형 바이트). NULL이면 처음부터',
DONE_CNT BIGINT NOT NULL DEFAULT 0 COMMENT '누적 처리 건수',
ERR_CNT BIGINT NOT NULL DEFAULT 0 COMMENT '누적 실패 건수',
JOB_STTUS CHAR(1) NOT NULL DEFAULT 'R' COMMENT 'R:진행 S:중지요청 C:완료 E:오류',
STR_DT DATETIME NULL COMMENT '시작 일시',
END_DT DATETIME NULL COMMENT '종료 일시',
UPDT_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP COMMENT '갱신 일시',
RM VARCHAR(500) NULL COMMENT '비고(마지막 오류 메시지 등)',
PRIMARY KEY (JOB_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='개인정보 암호화 이관 배치 진행상태';
-- ─────────────────────────────────────────────────────────────
-- [privacy.mber_nm_dead_views_drop_mysql.sql] (2026-08-11 통합)
-- ─────────────────────────────────────────────────────────────
-- =====================================================================
-- 회원 이름 암호화 — 죽은 뷰 8개 정리
-- [배경] COMTNGNRLMBER.MBER_NM 참조 뷰 10개 중 comvnuserinfo/comvnusermaster(PART 4 대상)만
-- 실사용. 나머지 8개는 FROM절 참조 0건인 화석(원래 뷰를 쓰던 기능이 인라인 UNION으로
-- 대체되어 남은 것) — 그대로 두면 뷰도 GNR 분기 평문을 노출한다.
-- [백업] 운영 반영 전 `src/script/mysql/backup/privacy.mber_nm_dead_views_backup_20260810.sql`
-- 로 정의를 먼저 백업해 둘 것(복원 시 그대로 재실행).
-- [재실행 안전] DROP VIEW IF EXISTS.
-- ★ [2026-08-27 대소문자 보정] 운영 DB(csv_structure_Dump20260520 실측)는 객체명을
-- 대문자/혼합 케이스로 저장한다(COMTNBBSUSERINFO, excelAppconfirmList ...). 로컬
-- Windows MySQL 은 전부 소문자라 종전의 소문자 DROP 문이 로컬에서만 동작하고
-- 운영에서는 IF EXISTS 때문에 **오류 없이 조용히 아무것도 지우지 않는다**.
-- → lower_case_table_names 설정에 의존하지 않도록 information_schema 에서
-- 실제 저장된 이름을 읽어 동적으로 DROP 한다(양쪽 환경 모두 동작).
-- =====================================================================
DROP PROCEDURE IF EXISTS PRC_PRIVACY_DROP_DEAD_VIEWS;
DELIMITER $$
CREATE PROCEDURE PRC_PRIVACY_DROP_DEAD_VIEWS()
BEGIN
DECLARE v_done INT DEFAULT 0;
DECLARE v_name VARCHAR(64);
DECLARE cur CURSOR FOR
SELECT TABLE_NAME FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE()
AND LOWER(TABLE_NAME) IN ('comtnbbsuserinfo','comvnusermaster2','comvnusermaster_1',
'excelappconfirmlist','excelappconfirmlist_auth',
'selectaddedoptionsinf','selectboardarticlelist',
'selectmiddleappconfirmlist');
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
-- [2026-08-27] DROP 전에 **운영본 정의를 그 DB 안에** 백업한다.
-- backup/privacy.mber_nm_dead_views_backup_20260810.sql 은 로컬(소문자) 캡처라
-- 운영 복원에 쓸 수 없다. 롤백 자산은 반드시 운영에서 떠 둬야 한다.
CREATE TABLE IF NOT EXISTS privacy_view_def_backup (
view_name VARCHAR(64) NOT NULL,
backup_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
view_definition MEDIUMTEXT NOT NULL,
PRIMARY KEY (view_name, backup_at)
);
INSERT INTO privacy_view_def_backup (view_name, view_definition)
SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE()
AND LOWER(TABLE_NAME) IN ('comtnbbsuserinfo','comvnusermaster2','comvnusermaster_1',
'excelappconfirmlist','excelappconfirmlist_auth',
'selectaddedoptionsinf','selectboardarticlelist',
'selectmiddleappconfirmlist')
AND NOT EXISTS (SELECT 1 FROM privacy_view_def_backup b
WHERE b.view_name = information_schema.views.TABLE_NAME);
OPEN cur;
drop_loop: LOOP
FETCH cur INTO v_name;
IF v_done = 1 THEN LEAVE drop_loop; END IF;
SET @drop_sql = CONCAT('DROP VIEW IF EXISTS `', v_name, '`');
PREPARE stmt_dv FROM @drop_sql;
EXECUTE stmt_dv;
DEALLOCATE PREPARE stmt_dv;
SELECT CONCAT('DROPPED dead view (definition backed up): ', v_name) AS DEAD_VIEW_STATUS;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
CALL PRC_PRIVACY_DROP_DEAD_VIEWS();
DROP PROCEDURE IF EXISTS PRC_PRIVACY_DROP_DEAD_VIEWS;
-- ─────────────────────────────
-- [2026-08-27] 신규 utf8mb4 테이블 collation 수렴 (환경간 차이 제거)
-- ─────────────────────────────
-- =====================================================================
-- [문제] `DEFAULT CHARSET=utf8mb4` 만 쓰면 서버 기본값을 따라가서
-- · 운영 MySQL 5.7 → utf8mb4_general_ci
-- · 로컬 MySQL 8.0 → utf8mb4_0900_ai_ci (5.7 에는 존재하지도 않는 값)
-- 로 **같은 스크립트가 환경마다 다른 테이블을 만든다** — 로컬 테스트가
-- 운영을 정확히 재현하지 못하고, 정렬·비교 의미가 미묘하게 달라진다.
--
-- [선택] utf8mb4_unicode_ci — 5.7/8.0 양쪽에 존재하고, 운영의 기존 utf8mb4
-- 테이블(BIZ_VOLUNTEER 등 13개)이 이미 쓰는 값이라 향후 조인이 생겨도
-- 'Illegal mix of collations' 가 나지 않는다.
-- (현재 개인정보보호 SQL 은 utf8mb4 테이블과 조인하지 않음을 실측 확인했으나,
-- 상대가 utf8mb3 일 때는 MySQL 이 더 넓은 utf8mb4 로 자동 변환하므로 무해하다)
--
-- [동작] 위 CREATE TABLE 은 IF NOT EXISTS 라 이미 만들어진 테이블의 collation 을
-- 바꾸지 못한다. 그래서 여기서 다른 값인 것만 골라 ALTER 한다.
-- · 운영(신규 설치) : 이미 unicode_ci 로 만들어져 있으므로 **전부 SKIP**
-- · 로컬(기존 0900_ai_ci) : 해당 테이블만 unicode_ci 로 전환
-- ※ CHARSET=utf8(utf8mb3) 로 만든 PERSONAL_DATA_ACCESS_LOG / PDAL_PURGE_HISTORY 는
-- 대상이 아니다(양쪽 모두 utf8mb3_general_ci 로 이미 동일 — 대용량 파티션
-- 테이블을 불필요하게 재작성하지 않기 위해서도 제외한다).
-- [재실행] 다른 것만 고르므로 반복 실행 무해(두 번째부터는 전부 SKIP).
-- =====================================================================
DROP PROCEDURE IF EXISTS PRC_PRIVACY_ALIGN_COLLATION;
DELIMITER $$
CREATE PROCEDURE PRC_PRIVACY_ALIGN_COLLATION()
BEGIN
DECLARE v_done INT DEFAULT 0;
DECLARE v_name VARCHAR(64);
DECLARE cur CURSOR FOR
SELECT TABLE_NAME FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_COLLATION LIKE 'utf8mb4%'
AND TABLE_COLLATION <> 'utf8mb4_unicode_ci'
AND LOWER(TABLE_NAME) IN (
'access_control_policy','account_action_history','account_delete_history',
'account_mng_policy','account_unlock_history','author_change_history',
'author_change_request','author_hist_download_history','author_hist_download_job',
'conect_ip_location','login_lock_exempt','mber_crypto_access_history',
'mig_crypto_progress','otp_device','pdal_download_history','pdal_download_job',
'pdal_inspection_report','privacy_view_def_backup');
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN cur;
align_loop: LOOP
FETCH cur INTO v_name;
IF v_done = 1 THEN LEAVE align_loop; END IF;
SET @alter_sql = CONCAT('ALTER TABLE `', v_name,
'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
PREPARE stmt_col FROM @alter_sql;
EXECUTE stmt_col;
DEALLOCATE PREPARE stmt_col;
SELECT CONCAT('COLLATION aligned -> utf8mb4_unicode_ci : ', v_name) AS COLLATION_STATUS;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
CALL PRC_PRIVACY_ALIGN_COLLATION();
DROP PROCEDURE IF EXISTS PRC_PRIVACY_ALIGN_COLLATION;
-- ---------------------------------------------------------------------
-- [privacy.mber_crypto_access_history_create_mysql.sql] 2026-08-27 추가
-- 암호화 회원정보 관리자 접근 이력(추가제안 Action4 / 제안서 슬라이드16)
-- · 관리자 전용 회원정보 화면의 복호화 조회·등록·수정·파기 행위를 '사유'와 함께 남긴다.
-- · 접속기록(PERSONAL_DATA_ACCESS_LOG)이 '언제 어느 화면을 열었나'라면 이 표는 '왜 열어봤나'다.
-- 둘 다 남기는 것은 중복이 아니라 이중 기록이다(하나가 비면 다른 하나로 대조).
-- · 개인정보 본문은 적재하지 않는다 — 변경 항목명만 남기고 값은 남기지 않는다.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS MBER_CRYPTO_ACCESS_HISTORY
(
HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호',
HANDLR_ID VARCHAR(20) NOT NULL COMMENT '처리자(취급자) 계정ID',
ACTION_TY CHAR(1) NOT NULL COMMENT '행위 S:복호화조회 I:등록 U:수정 D:개인정보파기',
TRGET_MBER_ID VARCHAR(20) NULL COMMENT '대상 회원ID(목록 검색은 NULL)',
ACCESS_REASON VARCHAR(500) NOT NULL COMMENT '사유(필수 입력)',
CHANGED_ITEMS VARCHAR(500) NULL COMMENT '변경 항목명 목록(값은 미적재). 예: 이름,휴대폰',
HANDLR_IP VARCHAR(45) NULL COMMENT '처리자 IP(IPv6 대비 45)',
REAUTH_YN CHAR(1) NOT NULL DEFAULT 'N' COMMENT '재인증 통과 여부',
ACCESS_DT DATETIME NOT NULL COMMENT '처리 일시',
PRIMARY KEY (HIST_ID),
KEY IDX_MCAH_01 (ACCESS_DT),
KEY IDX_MCAH_02 (HANDLR_ID),
KEY IDX_MCAH_03 (TRGET_MBER_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='암호화 회원정보 관리자 접근 이력(사유·재인증, 추가제안 Action4)';
-- =====================================================================
-- PART 2. DML (기준/시드 데이터) ※ 멱등: NOT EXISTS 가드로 중복 적재 방지
-- 수록: ACCOUNT_MNG_POLICY(기본정책 1행)
-- 미수록: OTP_DEVICE / PERSONAL_DATA_ACCESS_LOG / *_HISTORY = 런타임 적재(시드 없음)
-- [2026-08-11] PDAL_JOB_MAPPING(업무유형 매핑 13행) 시드 제거 — 업무유형 매핑 관리 화면
-- (메뉴 9100045) 폐기. 접속기록의 수행업무는 MenuAccessLogInterceptor 가
-- COMTNMENUINFO 메뉴명에서 직접 채우므로 이 시드는 더 이상 소비되지 않는다.
-- =====================================================================
-- 1) 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);
-- 3) CONECT_IP_LOCATION — 접속위치 매핑 기본 시드(개발항목 62)
-- 운영 대역(본부·전산실·지역문화원 등)은 확정 후 같은 형식으로 INSERT 만 추가한다.
-- 매칭이 없으면 소스 fallback(루프백/사설/외부)이 적용되므로 비어 있어도 동작한다.
INSERT INTO CONECT_IP_LOCATION (IP_CIDR, LOC_NM, LOC_SE, SORT_ORDR, USE_AT, REMARK, REGISTER_ID)
SELECT * FROM (
SELECT '127.0.0.0/8' AS IP_CIDR, '서버 로컬' AS LOC_NM, 'I' AS LOC_SE, 1 AS SORT_ORDR,
'Y' AS USE_AT, '루프백 — WAS 자체 호출' AS REMARK, 'SYSTEM' AS REGISTER_ID
) seed
WHERE NOT EXISTS (SELECT 1 FROM CONECT_IP_LOCATION x WHERE x.IP_CIDR = seed.IP_CIDR);
INSERT INTO CONECT_IP_LOCATION (IP_CIDR, LOC_NM, LOC_SE, SORT_ORDR, USE_AT, REMARK, REGISTER_ID)
SELECT * FROM (
SELECT '10.0.0.0/8' AS IP_CIDR, '내부망' AS LOC_NM, 'I' AS LOC_SE, 2 AS SORT_ORDR, 'Y' AS USE_AT, '사설 대역 — 세부 위치는 더 긴 CIDR 로 덮어쓰기' AS REMARK, 'SYSTEM' AS REGISTER_ID
) seed
WHERE NOT EXISTS (SELECT 1 FROM CONECT_IP_LOCATION x WHERE x.IP_CIDR = seed.IP_CIDR);
INSERT INTO CONECT_IP_LOCATION (IP_CIDR, LOC_NM, LOC_SE, SORT_ORDR, USE_AT, REMARK, REGISTER_ID)
SELECT * FROM (
SELECT '172.16.0.0/12' AS IP_CIDR, '내부망' AS LOC_NM, 'I' AS LOC_SE, 3 AS SORT_ORDR, 'Y' AS USE_AT, '사설 대역' AS REMARK, 'SYSTEM' AS REGISTER_ID
) seed
WHERE NOT EXISTS (SELECT 1 FROM CONECT_IP_LOCATION x WHERE x.IP_CIDR = seed.IP_CIDR);
INSERT INTO CONECT_IP_LOCATION (IP_CIDR, LOC_NM, LOC_SE, SORT_ORDR, USE_AT, REMARK, REGISTER_ID)
SELECT * FROM (
SELECT '192.168.0.0/16' AS IP_CIDR, '내부망' AS LOC_NM, 'I' AS LOC_SE, 4 AS SORT_ORDR, 'Y' AS USE_AT, '사설 대역' AS REMARK, 'SYSTEM' AS REGISTER_ID
) seed
WHERE NOT EXISTS (SELECT 1 FROM CONECT_IP_LOCATION x WHERE x.IP_CIDR = seed.IP_CIDR);
-- =====================================================================
-- PART 3. 관리자 메뉴 등록 DML (좌측/상단 메뉴 노출)
-- 구성: 권한관리(9100030) 하위에 MENU_ORDR 순으로 등록
-- [1]~[5] 9100035~9100039 권한 신청·승인 / 권한 변경 이력 / 미사용계정 현황 /
-- 계정 관리 정책 / 인증 실패 이력
-- [6] 9100047(ORDR 13) 접근통제 정책
-- [7] 9100044 명칭 정리("접속기록 조회 및 감사") + 9100051(ORDR 17) 회원정보 암호화 관리
-- + 중간 작업에서 만들었다가 내린 메뉴(9100048/49/50/52/53) 정리
-- ※ 접속기록 조회(9100044, ORDR 10)의 등록 자체는 위 [1]~[5] 블록에 들어 있다.
-- 원본: src/script/mysql/dml/privacy.menu_author_request_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_author_history_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_dormant_status_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_account_policy_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_access_log_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_access_policy_insert_mysql.sql
-- src/script/mysql/dml/privacy.menu_privacy_screens4_insert_mysql.sql
-- (개별 파일에 화면별 배치 근거·롤백 SQL이 더 상세히 있음 — 여기서는 통합 실행용)
--
-- ★★ 이 DML만으로는 좌측메뉴에 안 뜬다 — left.jsp 수정 배포 필수 ★★
-- src/main/webapp/WEB-INF/jsp/site/mnt/left.jsp 는 메뉴번호 하드코딩 JSP라
-- 권한관리(9100030) 블록에 등록한 메뉴번호마다 <c:when> 분기 +
-- 시스템관리/권한관리 상위 하이라이트 조건(param.page eq '91000xx')이
-- 반드시 함께 배포돼야 렌더된다. (arko_renual 소스에는 9100035~9100039·9100044·
-- 9100047·9100051 전부 이미 반영 완료 — 운영 배포 시 WAR/배포본에 이 JSP가
-- 포함됐는지만 확인하면 됨)
--
-- [재실행 안전] 모든 INSERT NOT EXISTS 가드 → 반복 실행 무해.
-- [노출권한] 전 건 IS_AUTHENTICATED_FULLY, ROLE_ADMIN 동일
-- (실제 접근 차단은 urlRoleMapping 의 /mnt/sec/* = ROLE_ADMIN 이 담당)
-- [Oracle] 대상 운영 DB가 MySQL(csv)이라 Oracle 쌍 미작성
-- =====================================================================
-- ── [1] 프로그램(URL) 등록 (COMTNMENUINFO.PROGRM_FILE_NM → COMTNPROGRMLIST FK, 메뉴보다 먼저) ──
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
SELECT * FROM (
SELECT 'AuthorRequestList' AS PROGRM_FILE_NM, '/mnt/sec/authorRequest/' AS PROGRM_STRE_PATH, '개인정보취급자 권한 관리' AS PROGRM_KOREAN_NM, '개인정보취급자 권한 관리(SFR-001 #46)' AS PROGRM_DC, '/mnt/sec/authorRequest/listView.do?page=9100035' AS URL UNION ALL
SELECT 'AuthorHistoryList', '/mnt/sec/authorHistory/', '권한변경 이력조회', '권한변경 이력조회·검색·다운로드(SFR-001 #6)', '/mnt/sec/authorHistory/listView.do?page=9100036' UNION ALL
SELECT 'DormantStatusList', '/mnt/sec/dormantStatus/', '장기 미사용 관리자 계정 현황', '장기 미사용 관리자 계정 현황 조회(SFR-002 #11)', '/mnt/sec/dormantStatus/listView.do?page=9100037' UNION ALL
SELECT 'AccountPolicySet', '/mnt/sec/accountPolicy/', '계정 관리 정책 설정', '장기 미사용 계정 관리 정책 설정(SFR-002 #13)', '/mnt/sec/accountPolicy/policyView.do?page=9100038'
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
-- ── [2] 메뉴 등록 : 권한관리(9100030) 하위, MENU_ORDR 5~8 순서 ──
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
SELECT * FROM (
SELECT '개인정보취급자 권한 관리' AS MENU_NM, 'AuthorRequestList' AS PROGRM_FILE_NM, 9100035 AS MENU_NO, 9100030 AS UPPER_MENU_NO, 5 AS MENU_ORDR, '개인정보취급자 권한 부여·변경·말소 현황 및 승인 처리' AS MENU_DC, '/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM UNION ALL
SELECT '권한변경 이력조회', 'AuthorHistoryList', 9100036, 9100030, 6, '권한변경 이력조회·검색·다운로드', '/', '/' UNION ALL
SELECT '미사용 관리자 계정 현황', 'DormantStatusList', 9100037, 9100030, 7, '장기 미사용 관리자 계정 현황 조회', '/', '/' UNION ALL
SELECT '계정 관리 정책', 'AccountPolicySet', 9100038, 9100030, 8, '장기 미사용 계정 관리 정책 설정', '/', '/'
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO);
-- ── [2-1] 명칭 정정(2026-08-10, 재실행 안전/멱등) — 9100037 이 관리자(EMP) 전용 화면임을 명시.
-- 이미 9100037 이 위 INSERT 로 적재된 환경은 NOT EXISTS 가드 때문에 INSERT 값이
-- 반영되지 않으므로 이 UPDATE 가 실제 적용 지점이다. 한글 리터럴은 UNHEX 우회 권장
-- (python3 -c "print('미사용 관리자 계정 현황'.encode('utf-8').hex().upper())").
UPDATE COMTNMENUINFO SET MENU_NM = '미사용 관리자 계정 현황' WHERE MENU_NO = 9100037;
UPDATE COMTNPROGRMLIST SET PROGRM_KOREAN_NM = '장기 미사용 관리자 계정 현황',
PROGRM_DC = '장기 미사용 관리자 계정 현황 조회(SFR-002 #11)'
WHERE PROGRM_FILE_NM = 'DormantStatusList';
-- ── [3] 권한↔메뉴 노출 매핑 (4건 × 2권한, FK 안전) ──
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
SELECT seed.menu_no, g.ac
FROM (SELECT 9100035 AS menu_no UNION ALL SELECT 9100036 UNION ALL SELECT 9100037 UNION ALL SELECT 9100038) seed
CROSS JOIN (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO) -- FK 안전
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
WHERE x.MENU_NO = seed.menu_no AND x.AUTHOR_CODE = g.ac);
-- ── [4] 인증 실패 이력 조회 메뉴(9100039) — SER-001 #22-1, 2026-08-05 추가 ──
-- 원본: src/script/mysql/dml/privacy.menu_login_fail_history_insert_mysql.sql
-- ★ left.jsp 9100039 <c:when> 분기 동반 배포 필수(위와 동일 제약)
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
SELECT * FROM (
SELECT 'LoginFailHistoryList' AS PROGRM_FILE_NM, '/mnt/sec/loginFailHistory/' AS PROGRM_STRE_PATH,
'인증 실패 이력' AS PROGRM_KOREAN_NM, '로그인 인증 실패 이력 조회(SER-001 #22-1)' AS PROGRM_DC,
'/mnt/sec/loginFailHistory/listView.do?page=9100039' AS URL
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
SELECT * FROM (
SELECT '인증 실패 이력' AS MENU_NM, 'LoginFailHistoryList' AS PROGRM_FILE_NM, 9100039 AS MENU_NO,
9100030 AS UPPER_MENU_NO, 9 AS MENU_ORDR, '로그인 인증 실패 이력 조회' AS MENU_DC,
'/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO);
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
SELECT 9100039, g.ac
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO)
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
WHERE x.MENU_NO = 9100039 AND x.AUTHOR_CODE = g.ac);
-- ── [5] 개인정보 접속기록 조회·다운로드 메뉴(9100044) — SFR-004 #47, 2026-08-06 추가 ──
-- 원본: src/script/mysql/dml/privacy.menu_access_log_insert_mysql.sql
-- ★ left.jsp 9100044 <c:when> 분기 + 하이라이트 조건 4곳 동반 배포 필수(위와 동일 제약)
-- ※ 9100040~9100043 은 '게시판관리' 그룹이 선점 → 미사용 번호 9100044 사용(MENU_ORDR 10)
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
SELECT * FROM (
SELECT 'AccessLogList' AS PROGRM_FILE_NM, '/mnt/sec/accessLog/' AS PROGRM_STRE_PATH,
'개인정보 접속기록' AS PROGRM_KOREAN_NM, '개인정보 접속기록 조회·다운로드(SFR-004 #47)' AS PROGRM_DC,
'/mnt/sec/accessLog/listView.do?page=9100044' AS URL
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
SELECT * FROM (
SELECT '개인정보 접속기록' AS MENU_NM, 'AccessLogList' AS PROGRM_FILE_NM, 9100044 AS MENU_NO,
9100030 AS UPPER_MENU_NO, 10 AS MENU_ORDR, '개인정보 접속기록 조회·다운로드' AS MENU_DC,
'/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO);
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
SELECT 9100044, g.ac
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO)
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
WHERE x.MENU_NO = 9100044 AND x.AUTHOR_CODE = g.ac);
-- ── [6] 접근통제 정책 설정 메뉴(9100047) — SER-001 요건 4·5, 2026-08-10 추가(2026-08-11 통합) ──
-- 원본: src/script/mysql/dml/privacy.menu_access_policy_insert_mysql.sql
-- ★ left.jsp 9100047 <c:when> 분기 + 하이라이트 조건 4곳 동반 배포 필수(위와 동일 제약)
-- [선행] 위 PART 1의 ACCESS_CONTROL_POLICY 가 먼저 생성돼 있어야 화면 진입 시 기본값 표시
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
SELECT 'AccessPolicySet', '/mnt/sec/accessPolicy/', '접근통제 정책 설정',
'계정 잠금·자동 로그아웃·세션 재발급 정책 설정(SER-001 요건 4·5)', '/mnt/sec/accessPolicy/policyView.do?page=9100047'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST WHERE PROGRM_FILE_NM = 'AccessPolicySet');
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
SELECT '접근통제 정책', 'AccessPolicySet', 9100047, 9100030, 13, '계정 잠금·자동 로그아웃·세션 재발급 정책 설정', '/', '/'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO WHERE MENU_NO = 9100047);
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
SELECT 9100047, g.ac
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO)
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
WHERE x.MENU_NO = 9100047 AND x.AUTHOR_CODE = g.ac);
-- ── [검증] ──
-- SELECT m.MENU_NO,m.UPPER_MENU_NO,m.MENU_NM,m.MENU_ORDR,p.URL,
-- (SELECT COUNT(*) FROM COMTNMENUCREATDTLS d WHERE d.MENU_NO=m.MENU_NO) grants
-- FROM COMTNMENUINFO m JOIN COMTNPROGRMLIST p ON m.PROGRM_FILE_NM=p.PROGRM_FILE_NM
-- WHERE m.MENU_NO IN (9100035,9100036,9100037,9100038,9100039,9100044,9100047) ORDER BY m.MENU_ORDR;
-- ── [롤백] (역순 권장, 4건 일괄) ──
-- DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO BETWEEN 9100035 AND 9100038;
-- DELETE FROM COMTNMENUINFO WHERE MENU_NO BETWEEN 9100035 AND 9100038;
-- DELETE FROM COMTNPROGRMLIST WHERE PROGRM_FILE_NM IN
-- ('AuthorRequestList','AuthorHistoryList','DormantStatusList','AccountPolicySet');
-- ── [7] 개인정보보호 화면 메뉴 최종형 (2026-08-27) ──
-- [확정 근거] 제안요청서 과업내용 "다. 개인정보취급자 접속 및 처리기록 관리 기능 개발" 이
-- 요구하는 화면은 하나다 — "관리자용 로그 조회 화면 개발".
-- 제안요약서 slide 14~15 의 시안도 "접속기록 조회 및 감사" 한 장만 그려져 있다.
-- 따라서 접속·처리기록 영역은 **메뉴 1개**로 끝난다.
--
-- [그 한 화면 안에서 제공하는 것] — RFP 가 '기능'으로 명시한 항목들이라 지우지 않는다
-- · "처리기록 검색·조회 기능 제공" → 처리구분(조회/등록/수정/삭제) 필터
-- · "비정상 접근 탐지 및 기록 기능 구현" → 목록의 '감사' 칸(이상/점검) 상시 표시
-- · SFR-004 "접속기록 정기점검을 지원" → 월 1회 배치 + PDAL_INSPECTION_REPORT 적재
-- · 파기이력(SER-002-2) → 화면 내 "파기이력" 버튼
--
-- [이 블록이 하는 일]
-- (1) 9100044 를 "접속기록 조회 및 감사" 로 정리(권한관리 9100030 하위, ORDR 10)
-- (2) 9100051 회원정보 암호화 관리(추가제안 Action4) 메뉴 등록
-- (3) 2026-08-27 중간 작업에서 만들었다가 시안 대조 후 내린 메뉴 정리
-- (9100048 정기점검 / 9100049 이상행위 / 9100050 파기이력 /
-- 9100052 그룹 / 9100053 처리기록) — 이미 적용한 DB 를 위한 정리구문이다.
-- 신규 설치라면 해당 행이 없으므로 아무 일도 일어나지 않는다.
--
-- ★ left.jsp 동반 배포 필수 — 권한관리 블록에 9100044·9100051 분기 + 하이라이트 조건
-- [재실행 안전] INSERT 는 NOT EXISTS, UPDATE 는 같은 값으로 수렴, DELETE 는 대상 한정.
-- (1) 접속기록 화면 명칭 정리
UPDATE COMTNMENUINFO
SET UPPER_MENU_NO = 9100030, MENU_ORDR = 10,
MENU_NM = '접속기록 조회 및 감사',
MENU_DC = '개인정보 접속·처리기록 조회, 이상행위 감사, 다운로드(사유·이력)'
WHERE MENU_NO = 9100044;
UPDATE COMTNPROGRMLIST
SET PROGRM_KOREAN_NM = '접속기록 조회 및 감사',
PROGRM_DC = '개인정보 접속·처리기록 조회 및 감사(RFP 과업내용 다 / SFR-004)'
WHERE PROGRM_FILE_NM = 'AccessLogList';
-- (2) 회원정보 암호화 관리 (추가제안 Action4)
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
SELECT * FROM (
SELECT 'MberCryptoAdmin' AS PROGRM_FILE_NM, '/mnt/sec/mberCrypto/' AS PROGRM_STRE_PATH,
'회원정보 암호화 관리' AS PROGRM_KOREAN_NM,
'암호화 회원정보 복호화 조회·수정·파기·등록(추가제안 Action4)' AS PROGRM_DC,
'/mnt/sec/mberCrypto/listView.do?page=9100051' AS URL
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM);
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
SELECT * FROM (
SELECT '회원정보 암호화 관리' AS MENU_NM, 'MberCryptoAdmin' AS PROGRM_FILE_NM, 9100051 AS MENU_NO,
9100030 AS UPPER_MENU_NO, 17 AS MENU_ORDR, '암호화 회원정보 복호화 CRUD(사유·재인증)' AS MENU_DC,
'/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM
) seed
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO);
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
SELECT 9100051, g.ac
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO)
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
WHERE x.MENU_NO = 9100051 AND x.AUTHOR_CODE = g.ac);
-- (3) 중간 작업에서 만들었다가 내린 메뉴 정리 (화면·컨트롤러는 유지/삭제되었음)
DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO IN (9100048, 9100049, 9100050, 9100052, 9100053);
DELETE FROM COMTNMENUINFO WHERE MENU_NO IN (9100048, 9100049, 9100050, 9100052, 9100053);
-- [검증] SELECT MENU_NO,UPPER_MENU_NO,MENU_ORDR,MENU_NM,HEX(MENU_NM)
-- FROM COMTNMENUINFO WHERE MENU_NO IN (9100044,9100051) ORDER BY MENU_ORDR;
-- =====================================================================
-- PART 4. 뷰 GNR 컷오버 — comvnusermaster / comvnuserinfo (2026-08-11 통합)
-- 원본: src/script/mysql/ddl/privacy.mber_nm_view_hash_cutover_mysql.sql
-- 추가제안 Action4(회원정보 암호화)의 마지막 단계. GNR 분기를 평문(MBER_NM)에서
-- 암호문(MBER_NM_ENC)으로 바꾸고 검색용 USER_NM_HASH 컬럼을 추가한다.
--
-- ★★ all-or-nothing 게이트 — 이 PART는 자동으로 스스로를 지킨다 ★★
-- 아래에서 실제 뷰 재정의를 실행하기 '전에' 두 가지를 확인한다:
-- ① comvnusermaster 에 USER_NM_HASH 가 이미 있으면(=이미 컷오버됨) SKIP
-- ② COMTNGNRLMBER 에 MBER_NM_ENC IS NULL 인 행이 하나라도 있으면(=백필 미완료) SKIP
-- → 이 경우 /mnt/sec/memberNmCrypto/start.do?confirm=BACKFILL(POST, ROLE_ADMIN)로
-- 백필을 먼저 끝내고 이 install_mysql.sql 을 다시 실행하면 된다(재실행 안전).
-- 두 조건을 모두 통과했을 때만 실제 CREATE OR REPLACE VIEW 를 수행한다 — 이 파일 하나만
-- "필요할 때마다" 반복 실행하는 것이 정상 사용법이다(설치 1회 + 백필 후 재실행 1회, 2번 실행).
--
-- [기법] information_schema.views.VIEW_DEFINITION 을 서버사이드 REPLACE()로 최소 치환
-- (PREPARE/EXECUTE 동적 SQL) — comvnuserinfo 의 한글 리터럴(USER_GUBUN_NM 등)을 클라이언트
-- 에서 절대 재입력하지 않아 셸→mysql.exe stdin 이중 인코딩 문제를 원천 회피한다.
-- [백업] 실행 전 원본 정의를 privacy_view_def_backup 테이블에 저장(롤백은 그 값으로
-- CREATE OR REPLACE VIEW 재실행).
-- =====================================================================
DROP PROCEDURE IF EXISTS PRC_PRIVACY_MBER_NM_VIEW_CUTOVER;
DELIMITER $$
CREATE PROCEDURE PRC_PRIVACY_MBER_NM_VIEW_CUTOVER()
BEGIN
DECLARE v_pending INT DEFAULT 0;
DECLARE v_done INT DEFAULT 0;
SELECT COUNT(*) INTO v_pending FROM COMTNGNRLMBER WHERE MBER_NM_ENC IS NULL;
SELECT COUNT(*) INTO v_done FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'comvnusermaster' AND COLUMN_NAME = 'USER_NM_HASH';
IF v_done > 0 THEN
SELECT 'SKIP: comvnusermaster.USER_NM_HASH 이미 존재 — 뷰 컷오버 기완료' AS PART4_GATE_STATUS;
ELSEIF v_pending > 0 THEN
SELECT CONCAT('SKIP: 백필 미완료 — COMTNGNRLMBER 미암호화 ', v_pending,
'건 남음. /mnt/sec/memberNmCrypto/start.do?confirm=BACKFILL 로 먼저 완료 후',
' 이 파일을 재실행할 것') AS PART4_GATE_STATUS;
ELSE
SELECT 'PROCEED: 백필 완료 확인(미암호화 0건) — 뷰 컷오버 진행' AS PART4_GATE_STATUS;
-- 0. 백업: 현재 뷰 정의를 별도 테이블에 저장(롤백용)
CREATE TABLE IF NOT EXISTS privacy_view_def_backup (
view_name VARCHAR(64) NOT NULL,
backup_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
view_definition MEDIUMTEXT NOT NULL,
PRIMARY KEY (view_name, backup_at)
);
INSERT INTO privacy_view_def_backup (view_name, view_definition)
SELECT 'comvnusermaster', VIEW_DEFINITION
FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'comvnusermaster';
INSERT INTO privacy_view_def_backup (view_name, view_definition)
SELECT 'comvnuserinfo', VIEW_DEFINITION
FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'comvnuserinfo';
-- **★★ [2026-08-27 대소문자 보정] ★★**
-- REPLACE() 는 lower_case_table_names 설정과 무관하게 **항상 대소문자를 구분**한다.
-- 운영 DB(csv_structure_Dump20260520 실측)의 VIEW_DEFINITION 은 테이블명이 대문자
-- (`COMTNGNRLMBER`)인데, 종전 코드는 소문자(`comtngnrlmber`)를 찾고 있었다.
-- -> 운영에서 5개 REPLACE 가 전부 불발되고, 바뀌지 않은 정의로
-- CREATE OR REPLACE VIEW 가 **에러 없이 성공**해 USER_NM_HASH 없는 뉴가 남는다.
-- 아래처럼 실제 저장된 객체명을 information_schema 에서 읽어 검색문자열을 조립한다.
SET @n_gnr = NULL; SET @n_emp = NULL; SET @n_ent = NULL; SET @n_grp = NULL;
SET @v_master = NULL; SET @v_info = NULL;
SELECT TABLE_NAME INTO @n_gnr FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'comtngnrlmber' LIMIT 1;
SELECT TABLE_NAME INTO @n_emp FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'comtnemplyrinfo' LIMIT 1;
SELECT TABLE_NAME INTO @n_ent FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'comtnentrprsmber' LIMIT 1;
SELECT TABLE_NAME INTO @n_grp FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'biz_group' LIMIT 1;
SELECT TABLE_NAME INTO @v_master FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'comvnusermaster' LIMIT 1;
SELECT TABLE_NAME INTO @v_info FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'comvnuserinfo' LIMIT 1;
IF @n_gnr IS NULL OR @n_emp IS NULL OR @n_ent IS NULL OR @n_grp IS NULL
OR @v_master IS NULL OR @v_info IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PART4 ABORT: base table or view not found (COMTNGNRLMBER/COMTNEMPLYRINFO/COMTNENTRPRSMBER/BIZ_GROUP/COMVNUSERMASTER/COMVNUSERINFO)';
END IF;
-- 1. comvnusermaster 재정의
-- GNR 분기: MBER_NM(평문) -> MBER_NM_ENC(암호문), USER_NM_HASH 컴럼 추가(MBER_NM_HASH)
-- USR/ENT/GRO 분기: USER_NM_HASH 컴럼 추가(NULL - 암호화 대상 아님)
SET @def_master = (
SELECT VIEW_DEFINITION FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'comvnusermaster'
);
SET @def_master = REPLACE(@def_master,
CONCAT('`', @n_gnr, '`.`MBER_NM` AS `USER_NM`'),
CONCAT('`', @n_gnr, '`.`MBER_NM_ENC` AS `USER_NM`'));
SET @def_master = REPLACE(@def_master,
CONCAT(' end) AS `ADULT` from `', @n_gnr, '` where'),
CONCAT(' end) AS `ADULT`,`', @n_gnr, '`.`MBER_NM_HASH` AS `USER_NM_HASH` from `', @n_gnr, '` where'));
SET @def_master = REPLACE(@def_master,
CONCAT('\'', 'N', '\'', ' AS `ADULT` from `', @n_emp, '` union all'),
CONCAT('\'', 'N', '\'', ' AS `ADULT`,NULL AS `USER_NM_HASH` from `', @n_emp, '` union all'));
SET @def_master = REPLACE(@def_master,
CONCAT('\'', 'N', '\'', ' AS `ADULT` from `', @n_ent, '` where'),
CONCAT('\'', 'N', '\'', ' AS `ADULT`,NULL AS `USER_NM_HASH` from `', @n_ent, '` where'));
SET @def_master = REPLACE(@def_master,
CONCAT('\'', 'N', '\'', ' AS `ADULT` from `', @n_grp, '` where'),
CONCAT('\'', 'N', '\'', ' AS `ADULT`,NULL AS `USER_NM_HASH` from `', @n_grp, '` where'));
SELECT
(LENGTH(@def_master) - LENGTH(REPLACE(@def_master, 'USER_NM_HASH', ''))) / LENGTH('USER_NM_HASH') AS hash_column_occurrences,
(@def_master LIKE '%MBER_NM_ENC%') AS gnr_uses_enc_column,
(@def_master NOT LIKE CONCAT('%`', @n_gnr, '`.`MBER_NM` AS `USER_NM`%')) AS old_plaintext_reference_removed;
-- ★ 하드 게이트: 치환이 하나라도 불발되면 여기서 중단한다.
-- (종전에는 위 SELECT 가 '보고용'이라 불발돼도 그대로 진행 -> 깨진 뉴가 남았다)
IF (LENGTH(@def_master) - LENGTH(REPLACE(@def_master, 'USER_NM_HASH', ''))) / LENGTH('USER_NM_HASH') <> 4
OR @def_master NOT LIKE '%MBER_NM_ENC%'
OR @def_master LIKE CONCAT('%`', @n_gnr, '`.`MBER_NM` AS `USER_NM`%') THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PART4 ABORT: comvnusermaster rewrite incomplete (expected 4 USER_NM_HASH + MBER_NM_ENC + no plaintext MBER_NM). View NOT replaced.';
END IF;
SET @sql_master = CONCAT('CREATE OR REPLACE VIEW `', @v_master, '` AS ', @def_master);
PREPARE stmt_master FROM @sql_master;
EXECUTE stmt_master;
DEALLOCATE PREPARE stmt_master;
-- 2. comvnuserinfo 재정의 (USER_GUBUN_NM 한글 리터럴은 REPLACE 대상에 포함하지 않는다 - 그대로 보존)
SET @def_info = (
SELECT VIEW_DEFINITION FROM information_schema.views
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) = 'comvnuserinfo'
);
SET @def_info = REPLACE(@def_info,
CONCAT('`', @n_gnr, '`.`MBER_NM` AS `USER_NM`'),
CONCAT('`', @n_gnr, '`.`MBER_NM_ENC` AS `USER_NM`'));
SET @def_info = REPLACE(@def_info,
CONCAT('`', @n_gnr, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT` from `', @n_gnr, '` union all'),
CONCAT('`', @n_gnr, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT`,`', @n_gnr, '`.`MBER_NM_HASH` AS `USER_NM_HASH` from `', @n_gnr, '` union all'));
SET @def_info = REPLACE(@def_info,
CONCAT('`', @n_grp, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT` from `', @n_grp, '` union all'),
CONCAT('`', @n_grp, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT`,NULL AS `USER_NM_HASH` from `', @n_grp, '` union all'));
SET @def_info = REPLACE(@def_info,
CONCAT('`', @n_ent, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT` from `', @n_ent, '` union all'),
CONCAT('`', @n_ent, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT`,NULL AS `USER_NM_HASH` from `', @n_ent, '` union all'));
SET @def_info = REPLACE(@def_info,
CONCAT('`', @n_emp, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT` from `', @n_emp, '`'),
CONCAT('`', @n_emp, '`.`ADMIN_UNLOCK_DT` AS `ADMIN_UNLOCK_DT`,NULL AS `USER_NM_HASH` from `', @n_emp, '`'));
SELECT
(LENGTH(@def_info) - LENGTH(REPLACE(@def_info, 'USER_NM_HASH', ''))) / LENGTH('USER_NM_HASH') AS hash_column_occurrences,
(@def_info LIKE '%MBER_NM_ENC%') AS gnr_uses_enc_column,
(@def_info NOT LIKE CONCAT('%`', @n_gnr, '`.`MBER_NM` AS `USER_NM`%')) AS old_plaintext_reference_removed;
-- ★ 하드 게이트 (comvnuserinfo)
IF (LENGTH(@def_info) - LENGTH(REPLACE(@def_info, 'USER_NM_HASH', ''))) / LENGTH('USER_NM_HASH') <> 4
OR @def_info NOT LIKE '%MBER_NM_ENC%'
OR @def_info LIKE CONCAT('%`', @n_gnr, '`.`MBER_NM` AS `USER_NM`%') THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PART4 ABORT: comvnuserinfo rewrite incomplete (expected 4 USER_NM_HASH + MBER_NM_ENC + no plaintext MBER_NM). View NOT replaced.';
END IF;
SET @sql_info = CONCAT('CREATE OR REPLACE VIEW `', @v_info, '` AS ', @def_info);
PREPARE stmt_info FROM @sql_info;
EXECUTE stmt_info;
DEALLOCATE PREPARE stmt_info;
-- 3. 반영 후 검증
-- ★ [2026-08-27] 이 검증 SELECT 들도 뉴 이름을 직접 참조하므로 동적 SQL 로 바꿈.
-- 운영(lower_case_table_names=0, 2026-08-27 실측)에서는 소문자 comvnusermaster 가
-- 존재하지 않아 1146(Table doesn't exist) 로 프로시저가 중단된다.
-- — 뉴 교체는 이미 성공한 뒤라, 성공한 컷오버가 실패처럼 보이게 된다.
-- (information_schema 비교는 콜레이션상 대소문자 무관이라 LOWER() 로 충분하다)
SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION
FROM information_schema.columns
WHERE TABLE_SCHEMA = DATABASE()
AND LOWER(TABLE_NAME) IN ('comvnusermaster', 'comvnuserinfo')
AND COLUMN_NAME IN ('USER_NM', 'USER_SE', 'USER_GUBUN', 'USER_NM_HASH')
ORDER BY TABLE_NAME, ORDINAL_POSITION;
SET @v_chk = CONCAT(
'SELECT USER_ID, USER_SE, LEFT(USER_NM, 20) AS user_nm_sample,',
' LEFT(USER_NM_HASH, 20) AS hash_sample FROM `', @v_master, '`',
' WHERE USER_SE = ''GNR'' LIMIT 1');
PREPARE stmt_chk FROM @v_chk; EXECUTE stmt_chk; DEALLOCATE PREPARE stmt_chk;
SET @v_chk = CONCAT(
'SELECT USER_ID, USER_SE, LEFT(USER_NM, 20) AS user_nm_sample, USER_NM_HASH',
' FROM `', @v_master, '` WHERE USER_SE = ''USR'' LIMIT 1');
PREPARE stmt_chk FROM @v_chk; EXECUTE stmt_chk; DEALLOCATE PREPARE stmt_chk;
SET @v_chk = CONCAT('SELECT USER_SE, COUNT(*) AS cnt FROM `', @v_master, '` GROUP BY USER_SE');
PREPARE stmt_chk FROM @v_chk; EXECUTE stmt_chk; DEALLOCATE PREPARE stmt_chk;
SET @v_chk = CONCAT(
'SELECT USER_GUBUN, LEFT(USER_NM, 20) AS user_nm_sample,',
' LEFT(USER_NM_HASH, 20) AS hash_sample FROM `', @v_info, '`',
' WHERE USER_GUBUN = ''GNR'' LIMIT 1');
PREPARE stmt_chk FROM @v_chk; EXECUTE stmt_chk; DEALLOCATE PREPARE stmt_chk;
SET @v_chk = CONCAT('SELECT USER_GUBUN, COUNT(*) AS cnt FROM `', @v_info, '` GROUP BY USER_GUBUN');
PREPARE stmt_chk FROM @v_chk; EXECUTE stmt_chk; DEALLOCATE PREPARE stmt_chk;
END IF;
END$$
DELIMITER ;
CALL PRC_PRIVACY_MBER_NM_VIEW_CUTOVER();
DROP PROCEDURE IF EXISTS PRC_PRIVACY_MBER_NM_VIEW_CUTOVER;
-- ── [롤백] ──
-- ★ 운영은 뷰 이름이 대문자(COMVNUSERMASTER)다. 소문자로 CREATE OR REPLACE 하면
-- lower_case_table_names=0 인 환경에서 **원본을 되돌리는 게 아니라 소문자 뷰가 새로
-- 하나 더 생긴다**. 아래처럼 실제 저장된 이름을 조회해서 그 이름으로 재실행할 것.
--
-- -- ① 실제 저장된 뷰 이름 확인
-- SELECT TABLE_NAME FROM information_schema.views
-- WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) IN ('comvnusermaster','comvnuserinfo');
--
-- -- ② 백업된 원본 정의 조회
-- SELECT view_definition FROM privacy_view_def_backup
-- WHERE view_name = 'comvnusermaster' ORDER BY backup_at DESC LIMIT 1;
--
-- -- ③ ①의 실제 이름 + ②의 값으로 재실행 (comvnuserinfo 동일)
-- -- CREATE OR REPLACE VIEW `COMVNUSERMASTER` AS <②의 value>
-- ############################## END ##############################