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()
)