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:
@@ -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<>();
|
||||
|
||||
+7
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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 결과 역직렬화
|
||||
Reference in New Issue
Block a user