style(rolesList): 다른 메뉴 톤에 맞춰 사이즈/글씨 축소 #12
@@ -29,7 +29,11 @@ public class BatchService extends BaseService {
|
||||
|
||||
public Map<String, Object> getBatchInfo(Map<String, Object> params) {
|
||||
commonService.applyCompanyCodeFilter(params);
|
||||
return sqlSession.selectOne(NS + "getBatchInfo", params);
|
||||
Map<String, Object> batch = sqlSession.selectOne(NS + "getBatchInfo", params);
|
||||
if (batch != null) {
|
||||
attachMappings(batch);
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -37,9 +41,18 @@ public class BatchService extends BaseService {
|
||||
sqlSession.insert(NS + "insertBatch", params);
|
||||
Long id = params.get("id") != null ? Long.parseLong(params.get("id").toString()) : null;
|
||||
if (id != null) {
|
||||
// batch_configs INSERT 직후 mappings 동기화 (params 에 mappings 키가 있을 때만)
|
||||
if (params.containsKey("mappings")) {
|
||||
syncMappings(id,
|
||||
toStr(params.get("company_code")),
|
||||
toMappingList(params.get("mappings")),
|
||||
toStr(params.get("created_by")));
|
||||
}
|
||||
Map<String, Object> infoParams = new HashMap<>();
|
||||
infoParams.put("id", id);
|
||||
return sqlSession.selectOne(NS + "getBatchInfo", infoParams);
|
||||
Map<String, Object> result = sqlSession.selectOne(NS + "getBatchInfo", infoParams);
|
||||
if (result != null) attachMappings(result);
|
||||
return result;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
@@ -48,9 +61,61 @@ public class BatchService extends BaseService {
|
||||
public Map<String, Object> updateBatch(Map<String, Object> params) {
|
||||
commonService.applyCompanyCodeFilter(params);
|
||||
sqlSession.update(NS + "updateBatch", params);
|
||||
Long id = params.get("id") != null ? Long.parseLong(params.get("id").toString()) : null;
|
||||
// replace-all: body 에 mappings 키가 들어왔으면 (빈 배열 포함) 매핑 전체 교체
|
||||
if (id != null && params.containsKey("mappings")) {
|
||||
syncMappings(id,
|
||||
toStr(params.get("company_code")),
|
||||
toMappingList(params.get("mappings")),
|
||||
toStr(params.get("updated_by") != null ? params.get("updated_by") : params.get("created_by")));
|
||||
}
|
||||
Map<String, Object> infoParams = new HashMap<>();
|
||||
infoParams.put("id", params.get("id"));
|
||||
return sqlSession.selectOne(NS + "getBatchInfo", infoParams);
|
||||
Map<String, Object> result = sqlSession.selectOne(NS + "getBatchInfo", infoParams);
|
||||
if (result != null) attachMappings(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── batch_mappings replace-all 동기화 ─────────────────────────────────────
|
||||
|
||||
/** batch_config_id 의 매핑을 전부 지우고 mappings 리스트로 다시 채운다. */
|
||||
private void syncMappings(Long batchConfigId, String companyCode,
|
||||
List<Map<String, Object>> mappings, String userId) {
|
||||
Map<String, Object> delParams = new HashMap<>();
|
||||
delParams.put("batch_config_id", batchConfigId);
|
||||
sqlSession.delete(NS + "deleteBatchMappingsByConfigId", delParams);
|
||||
|
||||
if (mappings == null || mappings.isEmpty()) return;
|
||||
|
||||
for (int i = 0; i < mappings.size(); i++) {
|
||||
Map<String, Object> row = new HashMap<>(mappings.get(i));
|
||||
row.put("batch_config_id", batchConfigId);
|
||||
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);
|
||||
sqlSession.insert(NS + "insertBatchMapping", row);
|
||||
}
|
||||
}
|
||||
|
||||
/** getBatchInfo 결과에 batch_mappings 리스트 attach. */
|
||||
private void attachMappings(Map<String, Object> batch) {
|
||||
Object idObj = batch.get("id");
|
||||
if (idObj == null) return;
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("batch_config_id", idObj);
|
||||
List<Map<String, Object>> mappings = sqlSession.selectList(NS + "getBatchMappingsByConfigId", params);
|
||||
batch.put("batch_mappings", mappings != null ? mappings : new ArrayList<>());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> toMappingList(Object raw) {
|
||||
if (raw == null) return new ArrayList<>();
|
||||
if (raw instanceof List) return (List<Map<String, Object>>) raw;
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private String toStr(Object v) {
|
||||
return v != null ? v.toString() : null;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -102,6 +102,114 @@
|
||||
<include refid="common.companyCodeFilter"/>
|
||||
</delete>
|
||||
|
||||
<!-- batch_mappings: 특정 batch_config_id 의 매핑 행들 조회 -->
|
||||
<select id="getBatchMappingsByConfigId" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
ID
|
||||
, BATCH_CONFIG_ID
|
||||
, COMPANY_CODE
|
||||
, FROM_CONNECTION_TYPE
|
||||
, FROM_CONNECTION_ID
|
||||
, FROM_TABLE_NAME
|
||||
, FROM_COLUMN_NAME
|
||||
, FROM_COLUMN_TYPE
|
||||
, FROM_API_URL
|
||||
, FROM_API_KEY
|
||||
, FROM_API_METHOD
|
||||
, FROM_API_PARAM_TYPE
|
||||
, FROM_API_PARAM_NAME
|
||||
, FROM_API_PARAM_VALUE
|
||||
, FROM_API_PARAM_SOURCE
|
||||
, FROM_API_BODY
|
||||
, TO_CONNECTION_TYPE
|
||||
, TO_CONNECTION_ID
|
||||
, TO_TABLE_NAME
|
||||
, TO_COLUMN_NAME
|
||||
, TO_COLUMN_TYPE
|
||||
, TO_API_URL
|
||||
, TO_API_KEY
|
||||
, TO_API_METHOD
|
||||
, TO_API_BODY
|
||||
, MAPPING_ORDER
|
||||
, MAPPING_TYPE
|
||||
, CREATED_BY
|
||||
, CREATED_DATE
|
||||
FROM BATCH_MAPPINGS
|
||||
WHERE BATCH_CONFIG_ID = #{batch_config_id}::varchar
|
||||
ORDER BY MAPPING_ORDER, ID
|
||||
</select>
|
||||
|
||||
<!-- batch_mappings: 단건 INSERT (replace-all 패턴이라 INSERT/DELETE 만 사용) -->
|
||||
<insert id="insertBatchMapping" parameterType="map" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
|
||||
INSERT INTO BATCH_MAPPINGS (
|
||||
BATCH_CONFIG_ID
|
||||
, COMPANY_CODE
|
||||
, FROM_CONNECTION_TYPE
|
||||
, FROM_CONNECTION_ID
|
||||
, FROM_TABLE_NAME
|
||||
, FROM_COLUMN_NAME
|
||||
, FROM_COLUMN_TYPE
|
||||
, FROM_API_URL
|
||||
, FROM_API_KEY
|
||||
, FROM_API_METHOD
|
||||
, FROM_API_PARAM_TYPE
|
||||
, FROM_API_PARAM_NAME
|
||||
, FROM_API_PARAM_VALUE
|
||||
, FROM_API_PARAM_SOURCE
|
||||
, FROM_API_BODY
|
||||
, TO_CONNECTION_TYPE
|
||||
, TO_CONNECTION_ID
|
||||
, TO_TABLE_NAME
|
||||
, TO_COLUMN_NAME
|
||||
, TO_COLUMN_TYPE
|
||||
, TO_API_URL
|
||||
, TO_API_KEY
|
||||
, TO_API_METHOD
|
||||
, TO_API_BODY
|
||||
, MAPPING_ORDER
|
||||
, MAPPING_TYPE
|
||||
, CREATED_BY
|
||||
, CREATED_DATE
|
||||
) VALUES (
|
||||
#{batch_config_id}::varchar
|
||||
, #{company_code}
|
||||
, #{from_connection_type}
|
||||
, #{from_connection_id}
|
||||
, #{from_table_name}
|
||||
, #{from_column_name}
|
||||
, #{from_column_type}
|
||||
, #{from_api_url}
|
||||
, #{from_api_key}
|
||||
, #{from_api_method}
|
||||
, #{from_api_param_type}
|
||||
, #{from_api_param_name}
|
||||
, #{from_api_param_value}
|
||||
, #{from_api_param_source}
|
||||
, #{from_api_body}
|
||||
, #{to_connection_type}
|
||||
, #{to_connection_id}
|
||||
, #{to_table_name}
|
||||
, #{to_column_name}
|
||||
, #{to_column_type}
|
||||
, #{to_api_url}
|
||||
, #{to_api_key}
|
||||
, #{to_api_method}
|
||||
, #{to_api_body}
|
||||
, #{mapping_order}
|
||||
, <choose>
|
||||
<when test="mapping_type != null and mapping_type != ''">#{mapping_type}</when>
|
||||
<otherwise>'direct'</otherwise>
|
||||
</choose>
|
||||
, #{created_by}
|
||||
, NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- batch_mappings: 특정 batch_config_id 의 매핑 전부 삭제 (replace-all 의 앞단계) -->
|
||||
<delete id="deleteBatchMappingsByConfigId" parameterType="map">
|
||||
DELETE FROM BATCH_MAPPINGS WHERE BATCH_CONFIG_ID = #{batch_config_id}::varchar
|
||||
</delete>
|
||||
|
||||
<!-- 내부 DB 테이블 목록 조회 -->
|
||||
<select id="getInternalTables" resultType="map">
|
||||
SELECT
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<sql id="batchExecutionLogSearchCondition">
|
||||
<if test="batch_config_id != null">
|
||||
AND bel.batch_config_id = #{batch_config_id}
|
||||
AND bel.batch_config_id = #{batch_config_id}::varchar
|
||||
</if>
|
||||
<if test="execution_status != null and execution_status != ''">
|
||||
AND bel.execution_status = #{execution_status}
|
||||
@@ -84,7 +84,7 @@
|
||||
<select id="getBatchExecutionLogLatest" parameterType="map" resultType="map">
|
||||
SELECT * FROM batch_execution_logs
|
||||
|
||||
WHERE batch_config_id = #{batch_config_id}
|
||||
WHERE batch_config_id = #{batch_config_id}::varchar
|
||||
|
||||
ORDER BY start_time DESC
|
||||
LIMIT 1
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
WHERE 1=1
|
||||
<if test="batch_config_id != null">
|
||||
AND batch_config_id = #{batch_config_id}
|
||||
AND batch_config_id = #{batch_config_id}::varchar
|
||||
</if>
|
||||
<if test="start_date != null and start_date != ''">
|
||||
AND start_time >= #{start_date}::timestamp
|
||||
@@ -123,7 +123,7 @@
|
||||
total_records, success_records, failed_records,
|
||||
error_message, error_details, server_name, process_id
|
||||
) VALUES (
|
||||
#{batch_config_id}, #{company_code}, #{execution_status},
|
||||
#{batch_config_id}::varchar, #{company_code}, #{execution_status},
|
||||
COALESCE(#{start_time}::timestamp, NOW()),
|
||||
#{end_time}::timestamp,
|
||||
#{duration_ms},
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
execution_today AS (
|
||||
SELECT COUNT(*) AS today_count,
|
||||
SUM(CASE WHEN execution_status = 'FAILED' THEN 1 ELSE 0 END) AS today_failed
|
||||
FROM batch_execution_log
|
||||
FROM batch_execution_logs
|
||||
WHERE DATE(start_time) = CURRENT_DATE
|
||||
<include refid="common.companyCodeFilter"/>
|
||||
),
|
||||
execution_yesterday AS (
|
||||
SELECT COUNT(*) AS yesterday_count,
|
||||
SUM(CASE WHEN execution_status = 'FAILED' THEN 1 ELSE 0 END) AS yesterday_failed
|
||||
FROM batch_execution_log
|
||||
FROM batch_execution_logs
|
||||
WHERE DATE(start_time) = CURRENT_DATE - INTERVAL '1 day'
|
||||
<include refid="common.companyCodeFilter"/>
|
||||
)
|
||||
@@ -77,9 +77,9 @@
|
||||
SUM(CASE WHEN execution_status = 'SUCCESS' THEN 1 ELSE 0 END) AS success_count,
|
||||
SUM(CASE WHEN execution_status = 'FAILED' THEN 1 ELSE 0 END) AS failed_count
|
||||
|
||||
FROM batch_execution_log
|
||||
FROM batch_execution_logs
|
||||
|
||||
WHERE batch_config_id = #{batch_config_id}
|
||||
WHERE batch_config_id = #{batch_config_id}::varchar
|
||||
AND start_time >= NOW() - INTERVAL '24 hours'
|
||||
|
||||
GROUP BY DATE_TRUNC('hour', start_time)
|
||||
@@ -100,9 +100,9 @@
|
||||
failed_records,
|
||||
error_message
|
||||
|
||||
FROM batch_execution_log
|
||||
FROM batch_execution_logs
|
||||
|
||||
WHERE batch_config_id = #{batch_config_id}
|
||||
WHERE batch_config_id = #{batch_config_id}::varchar
|
||||
|
||||
ORDER BY start_time DESC
|
||||
LIMIT 20
|
||||
|
||||
@@ -564,7 +564,7 @@ export default function BatchManagementPage() {
|
||||
const isSuccess = lastStatus === "SUCCESS";
|
||||
|
||||
return (
|
||||
<div key={batchId} className={`overflow-hidden rounded-lg border transition-all ${isExpanded ? "ring-1 ring-primary/20" : "hover:border-muted-foreground/20"} ${!isActive ? "opacity-55" : ""}`}>
|
||||
<div key={`${batch.company_code ?? "x"}-${batchId}`} className={`overflow-hidden rounded-lg border transition-all ${isExpanded ? "ring-1 ring-primary/20" : "hover:border-muted-foreground/20"} ${!isActive ? "opacity-55" : ""}`}>
|
||||
{/* 행 */}
|
||||
<div className="flex cursor-pointer items-center gap-3 px-4 py-3.5 sm:gap-4" onClick={() => handleRowClick(batchId)}>
|
||||
{/* 토글 */}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
# 배치/파이프라인 이식 작업 분석 (2026-05-12)
|
||||
|
||||
작성자: hjjeong
|
||||
배경: 박창현 팀장님(`chpark`) 카톡 (2026-05-11)
|
||||
|
||||
> *"그 수집관리 쪽은 파이프라인 쪽 가져와야 하는데. 빠이프. 내가 파이프 커밋해둠."*
|
||||
> *"DB 라던지 restapi 라던지 소스 데이터 가져온뒤에 우리 db에 적재할 때, 원본 소스의 값이 1 인데 우리 시스템은 Y 일경우 → 조건변환을 통해서 변경된 값으로 우리 db에 적재."*
|
||||
|
||||
---
|
||||
|
||||
## 한 줄 요약
|
||||
|
||||
**팀장님이 만들어두신 "파이프" 는 `/Users/jhj/vexplor_rps/` (별도 저장소) 에 있는 완성된 ETL 코드**. INVYONE 으로 옮기는 작업은 **Node.js → Spring 재작성** + 일부 Frontend 보강 + DB 스키마 한 컬럼 추가가 필요하다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 도메인 매트릭스 — INVYONE 의 현재 상태
|
||||
|
||||
INVYONE 안에 비슷한 이름의 도메인이 셋이고, 셋 다 **데이터 이동 실행 로직이 비어있다**.
|
||||
|
||||
| 도메인 | 메뉴 | 페이지 | 상태 |
|
||||
|---|---|---|---|
|
||||
| 배치관리 (구) | `/admin/batch-management` | [page.tsx](../../frontend/app/(main)/admin/batch-management/page.tsx) 206줄 | 모달 기반 db-to-db 만 |
|
||||
| 배치관리 (신) | `/admin/automaticMng/batchmngList` | [edit/[id]/page.tsx](../../frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx) 1850줄 | 매핑 UI 풍부 — **저장도 실행도 안 됨** |
|
||||
| 수집관리 | `/admin/systemMng/collection-managementList` | [page.tsx](../../frontend/app/(main)/admin/systemMng/collection-managementList/page.tsx) | `executeCollection` 이 jobs 테이블에 빈 행 INSERT 후 `records_processed=0` 박고 종료 |
|
||||
| 제어관리 (파이프라인) | `/admin/systemMng/dataflow/...` | [DataFlowDesigner.tsx](../../frontend/components/dataflow/DataFlowDesigner.tsx) | 노드 그래프 UI. flow_data JSONB 저장만. 실행 엔진 없음 |
|
||||
|
||||
→ 팀장님 카톡 의미: **수집관리 + 배치관리 양쪽에 파이프라인(변환/실행) 기능을 가져와야 한다**.
|
||||
|
||||
---
|
||||
|
||||
## 2. 진짜 파이프라인 코드 위치 — `/Users/jhj/vexplor_rps/`
|
||||
|
||||
별도 git 저장소. INVYONE 의 [`_pipeline_backup/`](../../_pipeline_backup/) 폴더와는 무관(그건 mcp-agent-orchestrator 실행 기록).
|
||||
|
||||
### Backend — `vexplor_rps/backend-node/` (TypeScript/Node.js)
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `src/services/batchSchedulerService.ts` | **ETL 본체**. `executeMapping()` 함수가 FROM 읽기 → 변환 → TO 저장 3단계 실행 |
|
||||
| `src/services/batchManagementService.ts` | 외부 DB 커넥터 (PG/MySQL/Oracle/MSSQL 등) |
|
||||
| `src/services/erpApiClient.ts` (226줄) | Wehago/Amaranth ERP REST API 호출, HMAC-SHA256 서명 |
|
||||
| `src/services/erpBatchSeedService.ts` (429줄) | 6종 매칭 배치 자동 시드 |
|
||||
| `src/services/erpPresetSeedService.ts` (158줄) | REST API 연결 사전 설정 |
|
||||
| `src/services/erpSyncService.ts` (539줄) | 동기화 로직 |
|
||||
| `src/services/erpTableMigration.ts` (172줄) | Idempotent 마이그레이션 |
|
||||
|
||||
#### `executeMapping()` 핵심 흐름 (`batchSchedulerService.ts`)
|
||||
|
||||
```
|
||||
L291~500 FROM 읽기 — 연결별 테이블 그룹화 후 batch fetch
|
||||
├ internal: PostgreSQL 직접 쿼리
|
||||
├ external_db: DatabaseConnectorFactory 동적 커넥터
|
||||
└ restapi: GET/POST 응답 + dataArrayPath 추출
|
||||
|
||||
L550~596 매핑 변환 — row.map() 안에서 mapping_type 분기
|
||||
├ "direct": row[from] → to (그대로 복사)
|
||||
├ "fixed": from_column_name 자체가 고정값
|
||||
└ "conditional": when/then 매칭 + default ← 1→Y 변환
|
||||
|
||||
L619~ TO 저장
|
||||
├ DB: INSERT 또는 UPSERT (save_mode 기반)
|
||||
└ REST: POST/PUT/DELETE + Request Body 템플릿
|
||||
```
|
||||
|
||||
### Frontend — `vexplor_rps/frontend/`
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `app/(main)/admin/batch-management-new/page.tsx` (1865줄) | 배치 에디터 UI 본체. 좌우 2패널 + 매핑 그리드 + ConditionalEditor |
|
||||
| `lib/api/batch.ts`, `batchManagement.ts` | API 클라이언트 + 타입 정의 |
|
||||
|
||||
INVYONE 의 [`batchmngList/edit/[id]/page.tsx`](../../frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx) (1850줄) 가 vexplor_rps 의 그것과 **거의 동일 구조** — 팀장님이 INVYONE 으로 한 번 옮긴 흔적. 다만 `mapping_type='conditional'` 분기와 ConditionalEditor 가 빠져있다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 조건 변환 자료구조
|
||||
|
||||
### 매핑 row 데이터 모델
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string,
|
||||
batch_config_id: string,
|
||||
from_connection_type: "internal" | "external_db" | "restapi",
|
||||
from_table_name: string,
|
||||
from_column_name: string,
|
||||
to_connection_type: "internal" | "external_db" | "restapi",
|
||||
to_table_name: string,
|
||||
to_column_name: string,
|
||||
mapping_order: number,
|
||||
mapping_type: "direct" | "fixed" | "conditional", // ← 핵심
|
||||
mapping_config: ConditionalConfig | null, // ← 신규 컬럼 필요
|
||||
// ... API 매핑용 (from_api_url, to_api_body 등)
|
||||
}
|
||||
```
|
||||
|
||||
### `ConditionalConfig` (mapping_type='conditional' 일 때)
|
||||
|
||||
```typescript
|
||||
interface ConditionalConfig {
|
||||
rules: { when: string; then: string }[];
|
||||
default: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 평가 알고리즘 (단순 문자열 매칭)
|
||||
|
||||
```ts
|
||||
function evaluateConditional(sourceVal: string, cfg: ConditionalConfig): string {
|
||||
for (const rule of cfg.rules) {
|
||||
if (rule.when === sourceVal) return rule.then;
|
||||
}
|
||||
return cfg.default;
|
||||
}
|
||||
```
|
||||
|
||||
표현식 평가(SpEL/JEXL) 안 씀. Java 로 옮길 때 `Map<String,String>` + `getOrDefault` 한 줄이면 끝.
|
||||
|
||||
### 예시 (팀장님 카톡: 1 → Y)
|
||||
|
||||
```json
|
||||
{
|
||||
"mapping_type": "conditional",
|
||||
"mapping_config": {
|
||||
"rules": [
|
||||
{ "when": "1", "then": "Y" },
|
||||
{ "when": "0", "then": "N" }
|
||||
],
|
||||
"default": "?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. chpark 결정적 커밋 목록 (vexplor_rps)
|
||||
|
||||
```
|
||||
2026-05-12 945b65b8 시퀀스 관리 메뉴 + 테이블 타입관리 코멘트/검증 + 설계 문서
|
||||
2026-05-11 a61643c2 Merge origin/main
|
||||
2026-05-08 57509869 배치 편집 conditional 매핑 평가 필드 UX 개선 ⭐
|
||||
2026-05-08 40070423 ECR · 고객 CS · 결재 + Amaranth + wace_plm 데이터 import
|
||||
2026-05-07 638543b3 Merge feature/rps-rebrand-pipeline-design ⭐
|
||||
2026-05-07 97b333dd Amaranth(Wehago) ERP REST API 연계 + 배치 시스템 강화 ⭐
|
||||
2026-04-30 9a8196a3 RPS 브랜딩 · COMPANY_16 단독 운영 · Pipeline 디자인 채용
|
||||
```
|
||||
|
||||
### 결정적 커밋 분석
|
||||
|
||||
| 해시 | 일자 | 내용 |
|
||||
|---|---|---|
|
||||
| **97b333dd** | 5/7 | 배치 시스템 본체 강화 (conditional mapping, row_filter, UPSERT) + ERP 연계 |
|
||||
| **638543b3** | 5/7 | `feature/rps-rebrand-pipeline-design` 브랜치 머지 (Pipeline 디자인 반영) |
|
||||
| **57509869** | 5/8 | conditional 매핑의 "평가 필드" 필수 표기 UX 개선 (1파일 +27/−13) |
|
||||
|
||||
→ **conditional 매핑 본체 구현은 97b333dd (5/7)**, 57509869 는 그 위에 UX 보완.
|
||||
|
||||
---
|
||||
|
||||
## 5. INVYONE 이식 작업
|
||||
|
||||
### 5-1. Backend (Spring 재작성)
|
||||
|
||||
| vexplor_rps (Node.js) | INVYONE (Spring) 대응 | 분량 |
|
||||
|---|---|---|
|
||||
| `batchSchedulerService.executeMapping()` | 신규 `BatchExecutor.java` (`@Service`, BaseService 상속) | ~2주 |
|
||||
| 외부 DB 커넥터 | 이미 일부 있음 ([ExternalDbConnectionService](../../backend-spring/src/main/java/com/erp/service/ExternalDbConnectionService.java)) | ~3일 |
|
||||
| REST API 호출 | 이미 있음 ([ExternalRestApiConnectionService](../../backend-spring/src/main/java/com/erp/service/ExternalRestApiConnectionService.java)) | 통합만 |
|
||||
| `evaluateConditional()` | `MappingTransformer.java` (Map 기반 lookup) | ~반나절 |
|
||||
| Quartz 스케줄링 | INVYONE 에 이미 Quartz 도입됨 (`V012__create_quartz_tables.sql`) | 통합만 |
|
||||
| ERP 사전 시드 / HMAC | 선택적 (Phase 4) | 1주+ |
|
||||
|
||||
### 5-2. Frontend
|
||||
|
||||
| 작업 | 위치 | 분량 |
|
||||
|---|---|---|
|
||||
| `ConditionalEditor` 컴포넌트 신규 | `components/admin/batch/ConditionalEditor.tsx` (신규) | 1일 |
|
||||
| 매핑 row 에 `sourceType='conditional'` 옵션 + `mapping_config` 필드 | [`batchmngList/edit/[id]/page.tsx`](../../frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx) | 1일 |
|
||||
| API 타입 확장 | [`lib/api/batch.ts`](../../frontend/lib/api/batch.ts) | 0.5일 |
|
||||
|
||||
### 5-3. DB 스키마 변경
|
||||
|
||||
| 컬럼 | 필요 여부 |
|
||||
|---|---|
|
||||
| `batch_mappings.mapping_type` | **이미 있음** (현재 'direct' 값만 사용) |
|
||||
| `batch_mappings.mapping_config JSONB` | **추가 필요** (conditional_config 저장) |
|
||||
| `batch_mappings.row_filter JSONB` | (선택) row 단위 필터링 룰. vexplor_rps 에 있음 |
|
||||
|
||||
마이그레이션: Flyway V021 + StartupSchemaMigrator 양쪽에.
|
||||
|
||||
### 5-4. INVYONE 측 선결과제 (Phase 0)
|
||||
|
||||
vexplor_rps 의 이식 외에도 INVYONE 자체의 **매핑 path 가 비어있는 문제** 가 별도. 이 Phase 0 가 없으면 conditional 룰을 만들어도 저장 안 됨.
|
||||
|
||||
| 작업 | 비고 |
|
||||
|---|---|
|
||||
| `batch_mappings` INSERT/UPDATE/DELETE 매퍼 작성 | 현재 0건 |
|
||||
| `getBatchInfo` 응답에 batch_mappings 포함 (LEFT JOIN 또는 별도 query) | 현재 BATCH_CONFIGS 만 SELECT |
|
||||
| `BatchManagementService.updateBatchConfig` 가 body 의 mappings 받아 batch_mappings 동기화 | 현재 silently drop |
|
||||
| `executeBatchConfig` 가 `BatchExecutor` 호출 | 현재 stub, 0건 응답 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 분해 (재추정)
|
||||
|
||||
| Phase | 작업 | 분량 |
|
||||
|---|---|---|
|
||||
| **0** | INVYONE 의 `batch_mappings` CRUD path 구현 + GET 응답 포함 + PUT 동기화 | 3일 |
|
||||
| **1** | `mapping_config JSONB` 컬럼 추가 (Flyway V021 + StartupSchemaMigrator) | 0.5일 |
|
||||
| **2** | Frontend `ConditionalEditor` + sourceType='conditional' 분기 + API 타입 확장 | 2일 |
|
||||
| **3** | Backend `MappingTransformer` (lookup 엔진) | 0.5일 |
|
||||
| **4** | Backend `BatchExecutor` 작성 — vexplor_rps `executeMapping` 알고리즘 1:1 이식 (FROM→Transform→TO 3단계, 내부/외부 DB/REST 모두 지원) | 2주 |
|
||||
| **5** | `executeBatchConfig` 가 `BatchExecutor` 호출, 실행 로그 (`batch_execution_logs`) 기록 | 3일 |
|
||||
| **6** | (선택) ERP 사전 시드, HMAC 서명, 6종 배치 자동 부팅 | 1주+ |
|
||||
|
||||
**합계** (Phase 0~5): 약 **3~4주** (1인 기준, 테스트·QA 제외). Phase 6 는 별도.
|
||||
|
||||
---
|
||||
|
||||
## 7. 팀장님께 확인하면 좋을 항목
|
||||
|
||||
1. **vexplor_rps 의 `batchSchedulerService.ts` 알고리즘을 1:1 이식하는 게 맞나** — 아니면 더 단순화/확장된 버전을 원하시는지
|
||||
2. **conditional 외에 다른 mapping_type 도 필요한가** — row_filter, expression eval 등
|
||||
3. **ERP 시드 (Phase 6) 도 이번 스프린트 범위인가** — 아니면 Phase 0~5 만
|
||||
4. **수집관리(`collection-managementList`) 와 배치관리(`batchmngList`) 가 같은 코드 공유해도 되는지** — UI 두 군데에 둘 다 노출인지
|
||||
5. **`mapping_config` 컬럼명 선호** — 다른 컨벤션이 있다면
|
||||
|
||||
---
|
||||
|
||||
## 부록 A. 분석 시 사용한 주요 파일
|
||||
|
||||
### INVYONE 측
|
||||
|
||||
- [`BatchController.java`](../../backend-spring/src/main/java/com/erp/controller/BatchController.java)
|
||||
- [`BatchManagementController.java`](../../backend-spring/src/main/java/com/erp/controller/BatchManagementController.java)
|
||||
- [`BatchService.java`](../../backend-spring/src/main/java/com/erp/service/BatchService.java)
|
||||
- [`BatchManagementService.java`](../../backend-spring/src/main/java/com/erp/service/BatchManagementService.java)
|
||||
- [`CollectionService.java`](../../backend-spring/src/main/java/com/erp/service/CollectionService.java)
|
||||
- [`NodeFlowService.java`](../../backend-spring/src/main/java/com/erp/service/NodeFlowService.java)
|
||||
- [`mapper/batch.xml`](../../backend-spring/src/main/resources/mapper/batch.xml)
|
||||
- [`mapper/collection.xml`](../../backend-spring/src/main/resources/mapper/collection.xml)
|
||||
- [`mapper/batchManagement.xml`](../../backend-spring/src/main/resources/mapper/batchManagement.xml)
|
||||
- [`mapper/nodeFlow.xml`](../../backend-spring/src/main/resources/mapper/nodeFlow.xml)
|
||||
- [`frontend/app/(main)/admin/automaticMng/batchmngList/`](../../frontend/app/(main)/admin/automaticMng/batchmngList/)
|
||||
- [`frontend/app/(main)/admin/systemMng/collection-managementList/`](../../frontend/app/(main)/admin/systemMng/collection-managementList/)
|
||||
- [`frontend/app/(main)/admin/systemMng/dataflow/`](../../frontend/app/(main)/admin/systemMng/dataflow/)
|
||||
- [`frontend/components/dataflow/DataFlowDesigner.tsx`](../../frontend/components/dataflow/DataFlowDesigner.tsx)
|
||||
|
||||
### vexplor_rps 측 (별도 저장소)
|
||||
|
||||
- `/Users/jhj/vexplor_rps/backend-node/src/services/batchSchedulerService.ts`
|
||||
- `/Users/jhj/vexplor_rps/backend-node/src/services/batchManagementService.ts`
|
||||
- `/Users/jhj/vexplor_rps/backend-node/src/services/erp*.ts`
|
||||
- `/Users/jhj/vexplor_rps/frontend/app/(main)/admin/batch-management-new/page.tsx`
|
||||
|
||||
### 운영 DB
|
||||
|
||||
- `batch_configs`, `batch_mappings` (스키마 ok / 운영 데이터 ok / backend 처리 없음), `batch_execution_logs`, `node_flow`, `data_collection_configs`
|
||||
|
||||
---
|
||||
|
||||
## 부록 B. INVYONE batch_mappings 운영 데이터 현황
|
||||
|
||||
```text
|
||||
batch_config_id | count
|
||||
----------------+------
|
||||
5 | 5
|
||||
10 | 4
|
||||
18 | 4
|
||||
20 | 2
|
||||
21 | 4
|
||||
28 | 1
|
||||
30 | 1
|
||||
31 | 2
|
||||
32 | 2
|
||||
37 | 4
|
||||
```
|
||||
|
||||
`created_date` 가 2025-09 등으로 오래된 행 다수. 현 backend 코드는 이 데이터를 읽지도 쓰지도 못함 — 사실상 dead data. vexplor_rps 시절 또는 옛 Node.js 시절의 흔적으로 추정.
|
||||
Reference in New Issue
Block a user