-- ##################################################################### -- 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 + 9100048~9100051 -- + 9100052(접속기록·감사 그룹) + 9100053(개인정보 처리기록)) -- ※ [8]에서 9100044/48/49 를 9100052 하위로 이설하고 9100050(파기이력) -- 메뉴는 내린다 — 제안서 Ⅲ-4 화면 시안 정합 -- ※ 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 하드코딩 분기가 -- 반드시 함께 배포돼야 한다(소스에는 이미 반영됨). 상세는 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; -- --------------------------------------------------------------------- -- [privacy.mber_crypto_access_history_create_mysql.sql] 2026-08-27 추가 -- 암호화 회원정보 관리자 접근 이력(추가제안 Action4 / 제안서 슬라이드16) -- · 관리자 전용 회원정보 화면의 복호화 조회·등록·수정·파기 행위를 '사유'와 함께 남긴다. -- · 접속기록(PERSONAL_DATA_ACCESS_LOG)이 '언제 어느 화면을 열었나'라면 이 표는 '왜 열어봤나'다. -- 둘 다 남기는 것은 중복이 아니라 이중 기록이다(하나가 비면 다른 하나로 대조). -- · 개인정보 본문은 적재하지 않는다 — 변경 항목명만 남기고 값은 남기지 않는다. -- --------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS MBER_CRYPTO_ACCESS_HISTORY ( HIST_ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '이력 일련번호', HANDLR_ID VARCHAR(20) NOT NULL COMMENT '처리자(취급자) 계정ID', ACTION_TY CHAR(1) NOT NULL COMMENT '행위 S:복호화조회 I:등록 U:수정 D:개인정보파기', TRGET_MBER_ID VARCHAR(20) NULL COMMENT '대상 회원ID(목록 검색은 NULL)', ACCESS_REASON VARCHAR(500) NOT NULL COMMENT '사유(필수 입력)', CHANGED_ITEMS VARCHAR(500) NULL COMMENT '변경 항목명 목록(값은 미적재). 예: 이름,휴대폰', HANDLR_IP VARCHAR(45) NULL COMMENT '처리자 IP(IPv6 대비 45)', REAUTH_YN CHAR(1) NOT NULL DEFAULT 'N' COMMENT '재인증 통과 여부', ACCESS_DT DATETIME NOT NULL COMMENT '처리 일시', PRIMARY KEY (HIST_ID), KEY IDX_MCAH_01 (ACCESS_DT), KEY IDX_MCAH_02 (HANDLR_ID), KEY IDX_MCAH_03 (TRGET_MBER_ID) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='암호화 회원정보 관리자 접근 이력(사유·재인증, 추가제안 Action4)'; -- ===================================================================== -- PART 2. DML (기준/시드 데이터) ※ 멱등: NOT EXISTS 가드로 중복 적재 방지 -- 수록: ACCOUNT_MNG_POLICY(기본정책 1행) -- 미수록: 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] 9100048~9100051(ORDR 14~17) 2026-08-27 추가 -- 정기점검 리포트 / 비정상접근 탐지 / 접속기록 파기이력 / 회원정보 암호화 관리 -- ※ 접속기록 조회(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) 블록에 등록한 메뉴번호마다 분기 + -- 시스템관리/권한관리 상위 하이라이트 조건(param.page eq '91000xx')이 -- 반드시 함께 배포돼야 렌더된다. (arko_renual 소스에는 9100035~9100039·9100044· -- 9100047·9100048~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 분기 동반 배포 필수(위와 동일 제약) 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 분기 + 하이라이트 조건 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 분기 + 하이라이트 조건 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] 개인정보보호 화면 4종 메뉴(9100048~9100051) — 2026-08-27 추가 ── -- 9100048 / ORDR 14 정기점검 리포트 /mnt/sec/inspection/listView.do (SFR-004-7) -- 9100049 / ORDR 15 비정상접근 탐지 /mnt/sec/abnormal/listView.do (과업내용 다-①) -- 9100050 / ORDR 16 접속기록 파기이력 /mnt/sec/purgeHistory/listView.do (SER-002-2) -- 9100051 / ORDR 17 회원정보 암호화관리 /mnt/sec/mberCrypto/listView.do (추가제안 Action4) -- ★ left.jsp 9100048~9100051 분기 4개 + 상위 하이라이트 조건 2곳 동반 배포 필수 -- ※ 9100045·9100046 은 2026-08-11 화면 폐기와 함께 삭제된 번호라 재사용하지 않는다. INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL) SELECT * FROM ( SELECT 'InspectionReportList' AS PROGRM_FILE_NM, '/mnt/sec/inspection/' AS PROGRM_STRE_PATH, '정기점검 리포트' AS PROGRM_KOREAN_NM, '접속기록 정기점검 리포트 조회·점검결과 입력(SFR-004-7)' AS PROGRM_DC, '/mnt/sec/inspection/listView.do?page=9100048' AS URL ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM); INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL) SELECT * FROM ( SELECT 'AbnormalAccessList' AS PROGRM_FILE_NM, '/mnt/sec/abnormal/' AS PROGRM_STRE_PATH, '비정상접근 탐지' AS PROGRM_KOREAN_NM, '비정상 접근 탐지 5유형 조회(과업내용 다-①)' AS PROGRM_DC, '/mnt/sec/abnormal/listView.do?page=9100049' AS URL ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM); INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL) SELECT * FROM ( SELECT 'AccessLogPurgeHistory' AS PROGRM_FILE_NM, '/mnt/sec/purgeHistory/' AS PROGRM_STRE_PATH, '접속기록 파기이력' AS PROGRM_KOREAN_NM, '접속기록 파티션 파기·증설 이력 조회(SER-002-2)' AS PROGRM_DC, '/mnt/sec/purgeHistory/listView.do?page=9100050' AS URL ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM); INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL) SELECT * FROM ( SELECT 'MberCryptoAdmin' AS PROGRM_FILE_NM, '/mnt/sec/mberCrypto/' AS PROGRM_STRE_PATH, '회원정보 암호화 관리' AS PROGRM_KOREAN_NM, '암호화 회원정보 복호화 조회·수정·파기·등록(추가제안 Action4)' AS PROGRM_DC, '/mnt/sec/mberCrypto/listView.do?page=9100051' AS URL ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM); INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM) SELECT * FROM ( SELECT '정기점검 리포트' AS MENU_NM, 'InspectionReportList' AS PROGRM_FILE_NM, 9100048 AS MENU_NO, 9100030 AS UPPER_MENU_NO, 14 AS MENU_ORDR, '접속기록 정기점검 리포트 조회·점검결과 입력' AS MENU_DC, '/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO); INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM) SELECT * FROM ( SELECT '비정상접근 탐지' AS MENU_NM, 'AbnormalAccessList' AS PROGRM_FILE_NM, 9100049 AS MENU_NO, 9100030 AS UPPER_MENU_NO, 15 AS MENU_ORDR, '비정상 접근 탐지 5유형 조회' AS MENU_DC, '/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO); INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM) SELECT * FROM ( SELECT '접속기록 파기이력' AS MENU_NM, 'AccessLogPurgeHistory' AS PROGRM_FILE_NM, 9100050 AS MENU_NO, 9100030 AS UPPER_MENU_NO, 16 AS MENU_ORDR, '접속기록 파티션 파기·증설 이력' AS MENU_DC, '/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO); INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM) SELECT * FROM ( SELECT '회원정보 암호화 관리' AS MENU_NM, 'MberCryptoAdmin' AS PROGRM_FILE_NM, 9100051 AS MENU_NO, 9100030 AS UPPER_MENU_NO, 17 AS MENU_ORDR, '암호화 회원정보 복호화 조회·수정·파기·등록' AS MENU_DC, '/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO); INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE) SELECT n.mno, g.ac FROM (SELECT 9100048 AS mno UNION ALL SELECT 9100049 UNION ALL SELECT 9100050 UNION ALL SELECT 9100051) n CROSS JOIN (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO) AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x WHERE x.MENU_NO = n.mno AND x.AUTHOR_CODE = g.ac); -- ── [8] "접속기록 · 감사" 그룹 재편(9100052) — 2026-08-27, 제안서 Ⅲ-4 시안 정합 ── -- [왜] 제안요약서 slide 14~15 의 화면 시안이 좌측메뉴를 "접속기록 · 감사" 한 묶음으로 그리고 -- 그 아래 4개(접속기록 조회 / 개인정보 처리기록 / 이상행위 감사 / 정기 점검 리포트)를 둔다. -- 위 [7]은 이 넷을 권한관리(9100030) 하위에 낱개로 붙여 시안과 어긋났다 — 여기서 되돌린다. -- [순서] 반드시 [7] 뒤에 온다([7]이 만든 9100048·9100049 를 이설하기 때문). -- 신규 설치는 [7]→[8]을 이어 실행하면 최종 구조에 도달하고, -- 이미 [7]만 적용된 DB는 [8]만 실행해도 같은 결과가 된다(전 구간 멱등). -- [내려가는 것] 접속기록 파기이력(9100050)은 시안에 메뉴가 없다. 메뉴만 제거하고 화면· -- 컨트롤러·프로그램(URL) 등록은 남긴다 — 정기 점검 리포트 화면의 버튼으로 진입한다. -- [그룹 메뉴 규약] PROGRM_FILE_NM 은 NOT NULL 이고 기존 그룹이 모두 센티넬 'dir' 을 쓴다 -- (COMTNPROGRMLIST 에 URL='dir' 1행). NULL 을 넣으면 ERROR 1048 로 실패한다. -- ★ left.jsp 동반 배포 필수 — 9100052 그룹 블록 신설 + 권한관리 블록에서 4개 분기 제거 INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM) SELECT * FROM ( SELECT '접속기록 · 감사' AS MENU_NM, 'dir' AS PROGRM_FILE_NM, 9100052 AS MENU_NO, 9100000 AS UPPER_MENU_NO, 5 AS MENU_ORDR, '개인정보 접속·처리기록 조회 및 감사(제안서 Ⅲ-4)' AS MENU_DC, NULL AS RELATE_IMAGE_PATH, NULL AS RELATE_IMAGE_NM ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO) AND EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = 'dir'); INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL) SELECT * FROM ( SELECT 'ProcessLogList' AS PROGRM_FILE_NM, '/mnt/sec/processLog/' AS PROGRM_STRE_PATH, '개인정보 처리기록' AS PROGRM_KOREAN_NM, '개인정보를 실제로 변경한 기록(등록·수정·삭제)만 조회' AS PROGRM_DC, '/mnt/sec/processLog/listView.do?page=9100053' AS URL ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST p WHERE p.PROGRM_FILE_NM = seed.PROGRM_FILE_NM); INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM) SELECT * FROM ( SELECT '개인정보 처리기록' AS MENU_NM, 'ProcessLogList' AS PROGRM_FILE_NM, 9100053 AS MENU_NO, 9100052 AS UPPER_MENU_NO, 2 AS MENU_ORDR, '등록·수정·삭제 기록만 조회' AS MENU_DC, '/' AS RELATE_IMAGE_PATH, '/' AS RELATE_IMAGE_NM ) seed WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO m WHERE m.MENU_NO = seed.MENU_NO); UPDATE COMTNMENUINFO SET UPPER_MENU_NO = 9100052, MENU_ORDR = 1, MENU_NM = '접속기록 조회' WHERE MENU_NO = 9100044; UPDATE COMTNMENUINFO SET UPPER_MENU_NO = 9100052, MENU_ORDR = 3, MENU_NM = '이상행위 감사', MENU_DC = '비정상 접근 5유형 탐지 결과 조회' WHERE MENU_NO = 9100049; UPDATE COMTNMENUINFO SET UPPER_MENU_NO = 9100052, MENU_ORDR = 4, MENU_NM = '정기 점검 리포트' WHERE MENU_NO = 9100048; INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE) SELECT n.mno, g.ac FROM (SELECT 9100052 AS mno UNION ALL SELECT 9100053) n CROSS JOIN (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO) AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x WHERE x.MENU_NO = n.mno AND x.AUTHOR_CODE = g.ac); DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO = 9100050; DELETE FROM COMTNMENUINFO WHERE MENU_NO = 9100050; -- [검증] SELECT m.MENU_NO,m.UPPER_MENU_NO,m.MENU_ORDR,m.MENU_NM,HEX(m.MENU_NM) -- FROM COMTNMENUINFO m WHERE m.MENU_NO=9100052 OR m.UPPER_MENU_NO=9100052 -- ORDER BY m.UPPER_MENU_NO, m.MENU_ORDR; -- ===================================================================== -- 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 ##############################