feat(batch): Phase 1 — BATCH_MAPPINGS.MAPPING_CONFIG JSONB 컬럼 + JSON 직렬화

- V021 Flyway: ALTER TABLE BATCH_MAPPINGS ADD COLUMN MAPPING_CONFIG JSONB
- StartupSchemaMigrator: 같은 ALTER 를 idempotent 항목으로 추가 (모든 활성 테넌트 DB 부팅 시 동기화)
- batch.xml: getBatchMappingsByConfigId SELECT 에 MAPPING_CONFIG::TEXT cast,
  insertBatchMapping VALUES 에 #{mapping_config,jdbcType=OTHER}::jsonb
- BatchService: ObjectMapper 주입, parseJsonField/stringifyJsonField 유틸,
  syncMappings 는 INSERT 전 직렬화, attachMappings 는 SELECT 후 Map 으로 역직렬화
- RUN_087_MIGRATION.md: 운영용 마이그레이션 runbook (사전 점검/사후 검증/롤백)

conditional 매핑(when/then/default) 룰을 행 단위 저장하는 컬럼.
direct/fixed 는 NULL. Phase 2~3 에서 프런트/엔진이 이 컬럼을 읽고 쓴다.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-13 10:24:56 +09:00
parent dce665caea
commit 2675c82904
5 changed files with 174 additions and 1 deletions
@@ -177,7 +177,13 @@ public class StartupSchemaMigrator {
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)
)
"""
""",
// V021 / RUN_087: BATCH_MAPPINGS 에 MAPPING_CONFIG JSONB 컬럼 추가.
// conditional 매핑(when/then/default) 규칙 저장용.
// direct/fixed 매핑은 NULL. 메타 DB 는 Flyway V021 로도 적용되지만
// 프로비저닝된 테넌트 DB 는 부팅 때 동기화.
"ALTER TABLE BATCH_MAPPINGS ADD COLUMN IF NOT EXISTS MAPPING_CONFIG JSONB"
);
@EventListener(ApplicationReadyEvent.class)
@@ -1,6 +1,7 @@
package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -15,6 +16,9 @@ public class BatchService extends BaseService {
@Autowired
private CommonService commonService;
@Autowired
private ObjectMapper objectMapper;
private static final String NS = "batch.";
private static final String EXT_NS = "externalDbConnection.";
@@ -93,6 +97,7 @@ public class BatchService extends BaseService {
if (row.get("company_code") == null) row.put("company_code", companyCode);
if (row.get("created_by") == null) row.put("created_by", userId);
if (row.get("mapping_order") == null) row.put("mapping_order", i + 1);
stringifyJsonField(row, "mapping_config");
sqlSession.insert(NS + "insertBatchMapping", row);
}
}
@@ -104,9 +109,36 @@ public class BatchService extends BaseService {
Map<String, Object> params = new HashMap<>();
params.put("batch_config_id", idObj);
List<Map<String, Object>> mappings = sqlSession.selectList(NS + "getBatchMappingsByConfigId", params);
if (mappings != null) {
for (Map<String, Object> row : mappings) parseJsonField(row, "mapping_config");
}
batch.put("batch_mappings", mappings != null ? mappings : new ArrayList<>());
}
/** JSONB → 객체. SELECT 결과의 TEXT cast 값을 파싱해 Map/List 로 되돌린다. */
private void parseJsonField(Map<String, Object> row, String key) {
Object val = row.get(key);
if (val instanceof String && !((String) val).isEmpty()) {
try {
row.put(key, objectMapper.readValue((String) val, Object.class));
} catch (Exception e) {
log.warn("Failed to parse JSONB field '{}': {}", key, e.getMessage());
}
}
}
/** 객체 → JSON 문자열. INSERT 전 ::jsonb 캐스팅을 위해 직렬화한다. null 은 그대로 둠. */
private void stringifyJsonField(Map<String, Object> params, String key) {
Object val = params.get(key);
if (val == null || val instanceof String) return;
try {
params.put(key, objectMapper.writeValueAsString(val));
} catch (Exception e) {
log.warn("Failed to stringify field '{}': {}", key, e.getMessage());
params.put(key, null);
}
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> toMappingList(Object raw) {
if (raw == null) return new ArrayList<>();
@@ -0,0 +1,7 @@
-- V021: BATCH_MAPPINGS.MAPPING_CONFIG JSONB 컬럼 추가
-- conditional 매핑(when/then/default) 규칙을 행 단위로 저장한다.
-- direct/fixed 매핑은 NULL. 메타 DB 뿐 아니라 모든 활성 테넌트 DB 에도
-- StartupSchemaMigrator 로 idempotent 하게 동일 ALTER 가 부팅 시 적용된다.
ALTER TABLE BATCH_MAPPINGS
ADD COLUMN IF NOT EXISTS MAPPING_CONFIG JSONB;
@@ -132,6 +132,7 @@
, TO_API_BODY
, MAPPING_ORDER
, MAPPING_TYPE
, MAPPING_CONFIG::TEXT AS MAPPING_CONFIG
, CREATED_BY
, CREATED_DATE
FROM BATCH_MAPPINGS
@@ -168,6 +169,7 @@
, TO_API_BODY
, MAPPING_ORDER
, MAPPING_TYPE
, MAPPING_CONFIG
, CREATED_BY
, CREATED_DATE
) VALUES (
@@ -200,6 +202,7 @@
<when test="mapping_type != null and mapping_type != ''">#{mapping_type}</when>
<otherwise>'direct'</otherwise>
</choose>
, #{mapping_config,jdbcType=OTHER}::jsonb
, #{created_by}
, NOW()
)
+125
View File
@@ -0,0 +1,125 @@
# 087 마이그레이션 — BATCH_MAPPINGS.MAPPING_CONFIG JSONB 추가
작성일: 2026-05-13
작성자: hjjeong
관련: `notes/hjjeong/2026-05-12-batch-pipeline-current-state.md` (Phase 1)
## 목적
vexplor_rps 의 conditional 매핑(파이프라인) 기능을 INVYONE 으로 이식하기 위한 첫 단계.
`BATCH_MAPPINGS` 행마다 매핑 규칙(when/then/default) 을 JSONB 로 저장할 컬럼 추가.
- `mapping_type='direct'` / `'fixed'``MAPPING_CONFIG` 는 NULL
- `mapping_type='conditional'``MAPPING_CONFIG``{"rules":[{"when":"1","then":"Y"}],"default":"?"}` 형태 저장
Phase 2 (frontend ConditionalEditor + API 확장) 와 Phase 3 (Backend MappingTransformer) 가
이 컬럼을 읽고 쓰는 전제로 동작한다.
## 스키마
### BATCH_MAPPINGS ALTER
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
| `MAPPING_CONFIG` | JSONB | NULL 허용 | conditional 평가 규칙. direct/fixed 면 NULL |
저장 포맷(`mapping_type='conditional'`):
```json
{
"rules": [
{ "when": "1", "then": "Y" },
{ "when": "0", "then": "N" }
],
"default": "?"
}
```
## SQL
```sql
-- =================================================================
-- 087: BATCH_MAPPINGS.MAPPING_CONFIG JSONB 추가 (idempotent)
-- =================================================================
ALTER TABLE BATCH_MAPPINGS
ADD COLUMN IF NOT EXISTS MAPPING_CONFIG JSONB;
```
부팅 시 `StartupSchemaMigrator` 가 메타 DB + 모든 활성 테넌트 DB 에 동일 ALTER 를
`IF NOT EXISTS` 로 적용하므로 일반적으로는 별도 수동 실행이 필요 없음.
별도 환경(콜드 백업 복원 등)에서 수동 실행이 필요할 때 위 SQL 한 줄을 그대로 사용.
## 사전 점검
```sql
-- A. 컬럼 사전 상태
SELECT column_name, data_type FROM information_schema.columns
WHERE table_name = 'batch_mappings' AND column_name = 'mapping_config';
-- 빈 결과여야 정상. 이미 있으면 ALTER 의 IF NOT EXISTS 가 안전.
-- B. 기존 데이터 행수 (마이그레이션 영향 범위 확인)
SELECT COUNT(*) FROM BATCH_MAPPINGS;
-- 컬럼만 추가하므로 기존 행은 MAPPING_CONFIG = NULL 로 유지됨.
```
## 사후 검증
```sql
-- C. 컬럼 추가 확인
SELECT column_name, data_type FROM information_schema.columns
WHERE table_name = 'batch_mappings' AND column_name = 'mapping_config';
-- 기대: data_type = 'jsonb'
-- D. JSONB 동작 확인 (테스트)
BEGIN;
UPDATE BATCH_MAPPINGS
SET MAPPING_CONFIG = '{"rules":[{"when":"1","then":"Y"}],"default":"?"}'::jsonb
WHERE ID = (SELECT ID FROM BATCH_MAPPINGS LIMIT 1);
SELECT MAPPING_CONFIG->'rules'->0->>'when' AS sample
FROM BATCH_MAPPINGS
WHERE MAPPING_CONFIG IS NOT NULL
LIMIT 1;
-- 기대: sample = '1'
ROLLBACK;
```
## 실행
```bash
# 1) 메타 DB
psql -h <host> -U postgres -d invyone -f RUN_087.sql
# 2) 각 테넌트 DB (StartupSchemaMigrator 가 부팅 시 자동 적용하므로 통상 생략 가능)
for db in $(psql -tA -d invyone -c "SELECT db_name FROM company_mng WHERE db_status='active'"); do
echo "=== $db ==="
psql -h <host> -U postgres -d "$db" -f RUN_087.sql
done
```
`RUN_087.sql` 은 위 "SQL" 섹션의 ALTER 한 줄을 그대로 담은 파일입니다.
## 롤백
```sql
-- MAPPING_CONFIG 컬럼 제거 (저장된 conditional 규칙은 함께 삭제됨)
ALTER TABLE BATCH_MAPPINGS DROP COLUMN IF EXISTS MAPPING_CONFIG;
```
## 적용 환경 체크리스트
- [ ] 로컬 docker `naengangi-pg` (메타 + 활성 테넌트 전부)
- [ ] wace 개발서버 PostgreSQL
- [ ] 운영 메타 DB (`invyone`)
- [ ] 운영 각 테넌트 DB (loop or 부팅 시 자동)
## 관련 코드
- Flyway: `backend-spring/src/main/resources/db/migration/V021__add_batch_mappings_mapping_config.sql`
- StartupSchemaMigrator: `backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java` (마지막 항목)
- Mapper: `backend-spring/src/main/resources/mapper/batch.xml`
- `getBatchMappingsByConfigId` 의 SELECT 절: `MAPPING_CONFIG::TEXT AS MAPPING_CONFIG`
- `insertBatchMapping` 의 VALUES 절: `#{mapping_config,jdbcType=OTHER}::jsonb`
- Service: `backend-spring/src/main/java/com/erp/service/BatchService.java`
- `syncMappings()``stringifyJsonField(row, "mapping_config")` 로 직렬화 후 INSERT
- `attachMappings()``parseJsonField(row, "mapping_config")` 로 SELECT 결과 역직렬화