feat(대무자): StartupSchemaMigrator 에 RUN_086 자동 적용 등록

invyone 의 정상 마이그레이션 패턴(StartupSchemaMigrator)에 RUN_086 의 7개 idempotent
DDL/DML 등록. 다음 backend 부팅 시 메타 DB + 모든 활성 테넌트 DB 에 자동 적용됨.

- btree_gist 확장 (CREATE EXTENSION IF NOT EXISTS)
- USER_SUBSTITUTES 테이블 (PK, CHECK 2, EXCLUDE 제약, 인덱스 2)
- SYSTEM_AUDIT_LOG ALTER (PROCESSOR_ID/PROCESSOR_NAME ADD COLUMN IF NOT EXISTS)
- APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES idempotent 데이터 복사

호환성 보정:
- EXCLUDE 의 daterange lower 에 COALESCE(.., CURRENT_DATE) 사용 불가 (IMMUTABLE 만 허용)
  → daterange(START_DATE, END_DATE, '[]') 단순 형식. NULL lower = -infinity 자연 처리.
- APPROVAL_PROXY_SETTINGS 의 START_DATE/END_DATE 가 varchar → DATE cast 추가
- 원본 메타데이터(created/updated) 컬럼명 환경별 차이 회피 → 'migration_086' + NOW() 고정

실측: meta+3 tenants 모두 OK (test01/test02/siflex)
This commit is contained in:
2026-05-12 08:20:59 +09:00
parent 6a9fc06f0e
commit af23fd0316
2 changed files with 80 additions and 6 deletions
@@ -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)
+6 -5
View File
@@ -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