diff --git a/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java b/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java index 44ec7040..195f1b4e 100644 --- a/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java +++ b/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java @@ -89,7 +89,80 @@ public class StartupSchemaMigrator { "ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS SALES_YN", "ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS SHOW_IN_CHART", "ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS ERP_MANAGED", - "ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS DATA_TYPE" + "ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS DATA_TYPE", + + // RUN_086 (1) btree_gist 확장 — USER_SUBSTITUTES 의 EXCLUDE 제약 의존성 + "CREATE EXTENSION IF NOT EXISTS btree_gist", + + // RUN_086 (2) 대무자(代務者) 관리 테이블 + // self-위임 차단 (CHECK), 같은 쌍 활성 기간 겹침 차단 (EXCLUDE). + // 재실행 시 IF NOT EXISTS 로 안전. EXCLUDE/CHECK 제약은 첫 생성 때만 적용. + """ + CREATE TABLE IF NOT EXISTS USER_SUBSTITUTES ( + SUBSTITUTE_ID BIGSERIAL PRIMARY KEY, + COMPANY_CODE VARCHAR(50) NOT NULL, + ORIGINAL_USER_ID VARCHAR(50) NOT NULL, + PROXY_USER_ID VARCHAR(50) NOT NULL, + START_DATE DATE NULL, + END_DATE DATE NOT NULL, + REASON VARCHAR(500), + IS_ACTIVE BOOLEAN NOT NULL DEFAULT TRUE, + CREATED_BY VARCHAR(50), + CREATED_DATE TIMESTAMP NOT NULL DEFAULT NOW(), + UPDATED_BY VARCHAR(50), + UPDATED_DATE TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT chk_user_substitutes_self + CHECK (ORIGINAL_USER_ID <> PROXY_USER_ID), + CONSTRAINT chk_user_substitutes_date + CHECK (START_DATE IS NULL OR START_DATE <= END_DATE), + CONSTRAINT excl_user_substitutes_overlap + EXCLUDE USING gist ( + COMPANY_CODE WITH =, + ORIGINAL_USER_ID WITH =, + PROXY_USER_ID WITH =, + daterange(START_DATE, END_DATE, '[]') WITH && + ) WHERE (IS_ACTIVE = TRUE) + ) + """, + + // RUN_086 (3) USER_SUBSTITUTES 인덱스 — Filter 핫패스 + 조회 가속 + "CREATE INDEX IF NOT EXISTS idx_user_substitutes_original ON USER_SUBSTITUTES (COMPANY_CODE, ORIGINAL_USER_ID, IS_ACTIVE)", + "CREATE INDEX IF NOT EXISTS idx_user_substitutes_proxy ON USER_SUBSTITUTES (COMPANY_CODE, PROXY_USER_ID, IS_ACTIVE)", + + // RUN_086 (4) SYSTEM_AUDIT_LOG — 처리자(actual processor) 분리 기록 컬럼 + "ALTER TABLE SYSTEM_AUDIT_LOG ADD COLUMN IF NOT EXISTS PROCESSOR_ID VARCHAR(50)", + "ALTER TABLE SYSTEM_AUDIT_LOG ADD COLUMN IF NOT EXISTS PROCESSOR_NAME VARCHAR(100)", + + // RUN_086 (5) APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES 1회 데이터 복사 (idempotent) + // 기존 운영 데이터 보존 + 어댑터 read 경로가 즉시 동작하도록. + // IS_ACTIVE 매핑: APPROVAL_PROXY_SETTINGS 의 CHAR('Y'/'N') → BOOLEAN. + // 메타데이터(created/updated) 는 원본 컬럼 의존 없이 'migration_086' + NOW() 로 고정 + // (APPROVAL_PROXY_SETTINGS 의 timestamp 컬럼명이 환경별로 다를 수 있어 안전한 default 채택). + """ + INSERT INTO USER_SUBSTITUTES ( + COMPANY_CODE, ORIGINAL_USER_ID, PROXY_USER_ID, + START_DATE, END_DATE, REASON, IS_ACTIVE, + CREATED_BY, CREATED_DATE, UPDATED_BY, UPDATED_DATE + ) + SELECT + p.COMPANY_CODE, p.ORIGINAL_USER_ID, p.PROXY_USER_ID, + CAST(NULLIF(p.START_DATE, '') AS DATE), + CAST(NULLIF(p.END_DATE, '') AS DATE), + p.REASON, + CASE WHEN p.IS_ACTIVE = 'Y' THEN TRUE ELSE FALSE END, + 'migration_086', NOW(), + 'migration_086', NOW() + FROM APPROVAL_PROXY_SETTINGS p + WHERE NULLIF(p.END_DATE, '') IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM USER_SUBSTITUTES s + WHERE s.COMPANY_CODE = p.COMPANY_CODE + AND s.ORIGINAL_USER_ID = p.ORIGINAL_USER_ID + AND s.PROXY_USER_ID = p.PROXY_USER_ID + AND s.START_DATE IS NOT DISTINCT FROM CAST(NULLIF(p.START_DATE, '') AS DATE) + AND s.END_DATE = CAST(NULLIF(p.END_DATE, '') AS DATE) + ) + """ ); @EventListener(ApplicationReadyEvent.class) diff --git a/db/migrations/RUN_086_MIGRATION.md b/db/migrations/RUN_086_MIGRATION.md index 0d509abb..82ec4545 100644 --- a/db/migrations/RUN_086_MIGRATION.md +++ b/db/migrations/RUN_086_MIGRATION.md @@ -92,8 +92,11 @@ CREATE TABLE IF NOT EXISTS USER_SUBSTITUTES ( COMPANY_CODE WITH =, ORIGINAL_USER_ID WITH =, PROXY_USER_ID WITH =, - daterange(COALESCE(START_DATE, CURRENT_DATE), END_DATE, '[]') WITH && + daterange(START_DATE, END_DATE, '[]') WITH && ) WHERE (IS_ACTIVE = TRUE) + -- daterange 의 lower bound 가 NULL 이면 -infinity. EXCLUDE 인덱스는 + -- IMMUTABLE 함수만 허용하므로 COALESCE(.., CURRENT_DATE) 같은 STABLE 함수 사용 불가. + -- START_DATE NULL 인 같은 쌍 활성 row 들은 어쨌든 겹침으로 자연 차단됨. ); -- 2. 인덱스 @@ -119,10 +122,8 @@ SELECT p.COMPANY_CODE, p.ORIGINAL_USER_ID, p.PROXY_USER_ID, p.START_DATE, p.END_DATE, p.REASON, CASE WHEN p.IS_ACTIVE = 'Y' THEN TRUE ELSE FALSE END, - COALESCE(p.CREATED_BY, 'migration_086'), - COALESCE(p.CREATED_DATE, NOW()), - COALESCE(p.UPDATED_BY, 'migration_086'), - COALESCE(p.UPDATED_DATE, NOW()) + 'migration_086', NOW(), + 'migration_086', NOW() FROM APPROVAL_PROXY_SETTINGS p WHERE NOT EXISTS ( SELECT 1 FROM USER_SUBSTITUTES s