# 배치 파이프라인 작업 핸드오프 (2026-05-13, 2026-05-14 업데이트)

작성자: hjjeong
관련 시작 노트: `notes/hjjeong/2026-05-12-batch-pipeline-current-state.md`

다음 새 세션에서 이 노트만 읽으면 즉시 컨텍스트 잡고 이어갈 수 있게 정리.

## 한 줄 상태

vexplor_rps → INVYONE 배치 파이프라인 이식 **Phase 0~5 코드 완료 + 푸시 완료 + 정적 검증 완료 + PR #12 머지 완료 (2026-05-13)**. **Phase 3 conditional 룰 종단 런타임 검증은 첫 사용자 시점으로 deferred** (사용자 결정 2026-05-14). JUnit @SpringBootTest 종단 시도 (2026-05-14) → SELECT path OK, INSERT path 는 transaction context 차이로 검증 불가 → 옵션 B (보류) 결정.

## 2026-05-14 세션 업데이트 (정적 점검 + 운영 DB read-only 확인)

이번 세션에서 한 일:
1. **코드 ↔ XML namespace.id 매칭 정합성 확인** — `batchExecutionLog.{insert,update}BatchExecutionLog` 둘 다 존재. 매핑 OK
2. **`batch_execution_logs` 실 컬럼 타입 vs Service INSERT 값 정합성** — 운영 4개 DB (`invyone`, `siflex_invyone`, `test01_invyone`, `test02_invyone`) read-only 조회. `id`/`batch_config_id`/`duration_ms`/`*_records` 전부 VARCHAR. Service 가 `String.valueOf` 명시 송출 → 정합 OK
3. **`useGeneratedKeys="true" keyProperty="id"` 동작 가능성** — `id` 가 VARCHAR + `nextval('batch_execution_logs_id_seq')` 디폴트. PG JDBC 가 String 으로 회수 → mapper UPDATE `WHERE id = #{id}` 그대로 사용 가능. OK
4. **tenant routing 정합성 (정적)** — `executeBatchConfig` 가 `@Transactional` 아님 → 매 `sqlSession.getConnection()` 마다 `TenantRoutingDataSource.determineCurrentLookupKey()` 호출 → `TenantHolder` 통과. OK
5. **과거 dev 실행 흔적 발견** — 메타 DB `invyone.batch_execution_logs` 에서:
   - `id=1027 (2026-02-08, total=1, success=1, server_name='unknown', process_id=54848)` — 신 코드 `safeHostName()` "unknown" fallback 지문 + 높은 PID = 로컬 dev JVM. **Phase 4 ETL 1행 처리 성공 흔적**
   - `id=14 (2026-04-03, total=0, server_name='unknown', process_id=54454)` — **Phase 5 INSERT/UPDATE 성공 흔적** (전체 코드 경로 통과 후 SUCCESS 마무리)
6. **Phase 3 conditional 룰은 0건 사용 중** — 4개 DB 통틀어 `batch_mappings.mapping_config` 채워진 행 0건. UI 는 살아있지만 사용자가 입력한 적 없음 → 운영 첫 사용 시점에 종단 검증 필요

결론: **정적 점검 + dev 흔적 기준으로 Phase 4/5 골격은 동작 확인.** PR 올리고 종결, Phase 3 conditional 종단은 첫 운영 사용자 시점에 함께 검증.

## 2026-05-14 후반 세션 — JUnit @SpringBootTest 종단 시도 → 옵션 B 보류

PR #12 머지 후, 사용자가 종단 검증을 자동화로 시도하길 요청. `@SpringBootTest` 통합 테스트를 만들어 운영 invyone 메타 DB 에 임시 `__phase3_*` 테이블 + `BatchExecutor.execute` 직접 호출로 검증 시도.

### 시도한 것

- `backend-spring/src/test/java/com/erp/batch/BatchExecutorIntegrationTest.java` (이 파일은 검증 후 삭제 — 운영 DB 가리키는 위험한 테스트라 PR 에 남기지 않음)
- 3개 테스트: `conditional_endToEnd`, `upsert_secondRunUpdates`, `rowFailureIsolation`
- `@Transactional @Commit` 으로 transaction context 부여, `DataSource` 직접 주입으로 setup/teardown
- Cleanup: `@BeforeAll` setupSchema (DROP/CREATE), `@AfterAll` teardown (DROP), `@BeforeEach` TRUNCATE

### 발견한 것

| 단계 | 결과 |
|---|---|
| Spring 컨텍스트 로딩 | ✅ OK (`JWT_SECRET` 더미 env 만 필요) |
| 운영 invyone 메타 connection | ✅ OK (`TenantRoutingDataSource` 가 META fallback) |
| 임시 테이블 setup/teardown | ✅ OK (잔여 0건) |
| `BatchExecutor.readFromInternal` (SELECT) | ✅ `totalRecords=4` 정확히 통과 |
| `BatchExecutor.writeToInternal` (INSERT) | ❌ "Connection is closed" — 전 row INSERT fail |

### 진단

운영 HTTP 요청 흐름 (Tomcat thread + Spring transaction-aware connection 관리) 에서는 `try (Connection c = sqlSession.getConnection()) { ... c.prepareStatement(...) }` 패턴이 잘 동작 (dev 흔적 `id=1027` 가 증거).

JUnit `@SpringBootTest` 는 `@Transactional` 붙여도 `SqlSessionTemplate` 의 transaction binding 이 완벽히 작동 안 함 → 외부 `try (Connection c = ...)` 의 c 가 body 진입 시점에 release 되어 다음 `c.prepareStatement(...)` 가 "Connection is closed" 던짐.

흥미로운 점: SELECT path 는 `try (Connection c = ...; PreparedStatement ps = c.prepareStatement(...); ResultSet rs = ...)` 처럼 한 줄에서 resource 전부 init → 시간차 없어 OK. INSERT path 는 외부 try body 에서 별도 prepareStatement 호출이라 fail.

### 결정 (사용자, 2026-05-14)

옵션 B (보류) 선택. 이유:

- BatchExecutor 의 핵심 변환 로직 (MappingTransformer) 은 이미 단위 테스트 18건으로 검증됨
- INSERT 흐름의 connection 관리는 production HTTP 컨텍스트에서 dev 흔적 (`id=14, 1027`) 으로 검증됨
- JUnit 으로 종단 검증하려면 ① backend 띄우고 HTTP curl (셋업 ~30분) 또는 ② BatchExecutor 코드 수정 (머지된 코드라 위험) 둘 다 비용 vs 가치 불균형
- 원래 2026-05-14 초반 결정: 종단은 운영 첫 사용자 시점에 6개 검증 항목 (A~F) 으로 흡수

### 다음 세션 참고

만약 종단 검증을 꼭 자동화하려면 **backend 띄우고 HTTP curl** 이 정공법:

1. `docker compose -f docker/dev/docker-compose.backend.mac.yml up -d` 또는 `./gradlew bootRun` (운영 DB 가리킴)
2. 인증 토큰 발급 (`POST /api/auth/login` 같은 endpoint — admin 계정 필요)
3. 메타 DB 에 batch_config + batch_mappings INSERT (SQL)
4. `POST /api/batch-management/batch-configs/{id}/execute` 호출
5. `batch_execution_logs` 확인 + cleanup

BatchExecutor 코드 자체 수정은 PR #12 머지된 코드라 의도적 회피.

## Git 상태 (세션 끝 기준)

- 현재 브랜치: `hjjeong`
- 푸시: `origin/hjjeong` 에 22 커밋 (`7315603f..f53307a7`)
- `origin/main` 과 무충돌 머지 완료 (`f53307a7`)
- PR 미생성 — 사용자가 직접 https://git.junggomoa.com/gbpark/invyone/pulls/new/hjjeong 에서 생성 예정

## 완료된 커밋 (12개, 가독성 순)

```
54a8f97f  fix(batch): 미리보기 → 매핑 카드 표시 흐름 정상화 + 매핑 카드 컴팩트화
cbf94dc9  feat(batch): TO DB 자동 선택 (internal) + Select 컴포넌트 controlled 화
b752de23  fix(batch): previewRestApiData convertCamelToSnake (실은 no-op, 54a8f97f 에서 정정)
6fcb101f  style(batch): API 파라미터 설정을 collapsible 로 변경 — 기본 접힘
47eed680  fix(external-rest-api): WHERE ID = #{id} 에 ::varchar 캐스팅 추가
d8f606ab  style(batch): 기본 정보를 모드 토글과 한 행으로 통합
e8f517ed  fix(batch): batch-management-new 도 풀폭 적용
d02bc38f  style(batch): FROM 카드 행 그룹화 + 컴팩트
0c9e22a6  feat(batch): 등록 REST API 연결 자동 호출 + 응답 필드 추출
570b3267  feat(batch): batch-management-new 에 conditional 매핑 추가
0bba1836  fix(batch): 빈 화면 원인 openTab 키명 정정 + 풀폭
f70719ae  fix(batch): batch_execution_logs VARCHAR 컬럼에 String.valueOf
3ab7deb1  test(batch): Phase 3 — MappingTransformerTest (18 cases)
d5925472  fix(batch): writeTo @Transactional 제거 + end_time Timestamp 객체
6f8461a5  feat(batch): Phase 5 — executeBatchConfig + batch_execution_logs 기록
17172cf9  feat(batch): Phase 4 — BatchExecutor ETL 본체
f9a9c678  feat(batch): Phase 3 — MappingTransformer lookup 엔진
f31a7f85  feat(batch): Phase 2 — 프런트 ConditionalEditor + 조건 변환 매핑 UI
2675c829  feat(batch): Phase 1 — MAPPING_CONFIG JSONB 컬럼 + JSON 직렬화
dce665ca  feat(batch): Phase 0 — batch_mappings CRUD path
f53307a7  Merge remote-tracking branch 'origin/main' into hjjeong
```

## 검증 상태 (2026-05-14 갱신)

| Phase | 검증 방식 | 결과 |
|---|---|---|
| Phase 0 (CRUD path) | DB 스키마 + GET 응답 — 부분 검증 | ✅ 스키마 OK / 런타임 미검증 |
| Phase 1 (MAPPING_CONFIG) | 운영 DB 컬럼 존재 확인 | ✅ 이미 컬럼 있음 (멱등 안전) |
| Phase 2 (ConditionalEditor) | 사용자 브라우저 UI 검증 | ✅ 화면 표시/조작 확인 |
| Phase 3 (MappingTransformer) | JUnit 18 tests | ✅ 전부 통과 |
| Phase 4 (BatchExecutor) | 정적 점검 + dev 실행 흔적 | 🟡 골격 OK (`id=1027 (2026-02-08, total=1)` 1행 처리 성공 흔적). 실 운영 데이터로 종단 미검증 |
| Phase 5 (executeBatchConfig + 로그) | 정적 점검 + dev 실행 흔적 | 🟡 INSERT/UPDATE 동작 확인 (`id=14 (2026-04-03, server_name='unknown')`). 다중 회사 동시 실행은 미검증 |

## 🚨 아직 테스트 안 한 것 / 운영 첫 사용 시 검증할 것 (★ 필독)

PR 머지 후 첫 운영 사용자 또는 QA 가 반드시 직접 확인할 항목.

### A. Phase 3 conditional 룰 종단 (가장 큰 갭)

- 현재 4개 DB 통틀어 `batch_mappings.mapping_config` 채워진 행 **0건**
- 첫 사용자가 ConditionalEditor 로 룰 입력 → save → execute 까지의 전체 흐름이 종단으로 동작하는 적이 한 번도 없었음
- **확인 절차**:
  1. `batchmngList/edit/[id]` 또는 `batch-management-new` 진입
  2. ConditionalEditor 로 conditional 룰 1건 추가 (예: `from_value='A' → to_value='가나'`)
  3. 저장 → DB 에서 `batch_mappings.mapping_config` 가 JSONB 로 잘 들어갔는지 확인 (`SELECT id, mapping_type, mapping_config FROM batch_mappings WHERE batch_config_id=...`)
  4. 수동 실행 버튼 → `batch_execution_logs` 새 행 추가 확인 + TO 테이블에 `to_value` 가 `'가나'` 로 변환되어 들어갔는지 확인
- 잠재 이슈: `MappingTransformer.applyConditional` 의 default fallback / 매칭 우선순위 / NULL 처리 — 단위 테스트 통과지만 실 데이터 형태가 다를 수 있음

### B. Phase 4 외부 소스/대상 종단

- **FROM = `external_db`** (외부 DB SELECT) — `ExternalDbConnectionService.executeQuery` 경유. dev 흔적 없음
- **FROM = `restapi`** (등록 REST API 호출) — `ExternalRestApiConnectionService.fetchData` + `dataArrayPath` 추출. dev 흔적 없음
- **TO = `restapi`** (행 단위 POST/PUT/DELETE) — `ExternalRestApiConnectionService.testConnection` 경유. dev 흔적 없음
- 확인: 각 타입별로 1건씩 실 호출 성공/실패 카운트가 `batch_execution_logs.success_records`/`failed_records` 에 정확히 반영되는지

### C. Phase 5 다중 회사 동시 실행 (tenant routing 충돌 가능성)

- 정적으로는 `executeBatchConfig` 가 `@Transactional` 아님 + 매번 `sqlSession.getConnection()` 새로 borrow → 안전하게 보임
- 하지만 같은 시각에 회사 A 의 cron 과 회사 B 의 수동 실행이 겹칠 때 `TenantHolder` (ThreadLocal) 가 정확히 매번 set/clear 되는지 미검증
- 확인: 회사 2개 (`siflex`, `test01`) 에서 동시에 같은 batch 실행 → 각자의 `batch_execution_logs` 에만 행 추가되고 cross-DB 오염 없는지

### D. UPSERT (`save_mode=UPSERT` + `conflict_key`) 종단

- `BatchExecutor.buildInsertSql` 의 `ON CONFLICT (...) DO UPDATE SET ... = EXCLUDED. ...` 분기 — dev 흔적 없음
- 확인: UPSERT 모드 batch 1건 만들어 실행 → 같은 키로 두 번째 실행 시 INSERT 가 아니라 UPDATE 로 동작하는지

### E. 행 단위 실패 격리

- `writeToInternal` 가 try-catch 로 row-level 실패만 카운트 — 전체 트랜잭션 롤백 없음 (vexplor_rps 와 동일 패턴, 의도적)
- 확인: 일부러 NOT NULL 위반 행 1건 + 정상 행 9건 섞어 실행 → `success=9, failed=1` 정확히 집계되는지

### F. 회사 코드 필터 (`companyCode`) 누수

- `BatchExecutor.execute` 가 `config.get("company_code")` 로만 회사 식별 → MappingTransformer 의 fixed/conditional 룰에 회사 정보 주입은 정확한지
- 확인: 회사 A 의 batch 가 회사 B 의 데이터를 fetch/write 하지 않는지 (multi-tenant 핵심)

## 알려진 잠재 리스크 (정적 점검에서 OK 였지만 운영 시 마주칠 가능성)

1. ~~**`sqlSession.getConnection()` + tenant routing**~~ — ✅ 2026-05-14 정적 OK 확인 (`@Transactional` 미사용 → 매번 routing). 다중 회사 동시 실행 종단 검증은 위 "🚨 C" 항목
2. ~~**MyBatis namespace.id 매칭**~~ — ✅ 2026-05-14 XML 대조로 `batchExecutionLog.{insert,update}BatchExecutionLog` 둘 다 존재 확인
3. **`@Transactional writeTo` self-call** — `d5925472` 에서 제거함. 행 단위 독립 commit (vexplor_rps 와 동일 패턴)
4. ~~**`duration_ms / *_records` VARCHAR**~~ — ✅ 2026-05-14 운영 DB read-only 로 컬럼 타입 일치 확인 + `String.valueOf` 송출 정합 OK
5. **외부 호출 인증 안내** — Wehago HMAC 자동 안내 / `auth_tokens` 자동 조회는 의도적 미구현 (Amaranth 회사 전용이라 INVYONE 일반에는 부적합)

## Phase 4 의 의도적 단순화 (vexplor_rps 대비)

- `to_api_body` 템플릿 기반 일괄 전송 — 미지원 (행 단위 POST/PUT/DELETE 만)
- `URL_PATH_PARAM` 컬럼 처리 — 미지원
- inline-mode REST(`from_connection_id` 없이 직접 URL/Key) — 미지원
- `auth_tokens` 자동 조회 — 미지원
- `row_filter_config` — 미지원
- `external_db` TO 쓰기 — 미지원 (INVYONE `ExternalDbConnectionService` 가 보안상 SELECT-only)

위 항목이 운영 배치에 필요해지면 Phase 4.x 로 incremental 추가.

## 다음 후보 작업 (우선순위 무관)

### A. 편집 화면 일관성
`batchmngList/edit/[id]/page.tsx` 에 신규 생성 화면 (batch-management-new) 과 동일한 흐름 적용:
- TO DB 자동 선택
- Select controlled (value prop)
- ConditionalEditor 동작 검증 (Phase 2 에서 이미 추가했지만 사용자 직접 확인 필요)

### B. 같은 패턴 일괄 점검
- Select `value` prop 누락 다른 페이지에 있을 가능성
- ::varchar 캐스팅 누락 매퍼들 (`externalDbConnection.xml`, `externalCallConfig.xml`, `booking.xml`, `delivery.xml`, `multiConnection.xml`, `taxInvoice.xml` 등)
- camelCase / snake_case mismatch 다른 진입점에 있을 가능성

### ~~C. Phase 4+5 통합 검증 ★~~ — 2026-05-14 deferred

PR 머지 + 운영 첫 사용 시점에 위 "🚨 아직 테스트 안 한 것" 의 A~F 항목으로 흡수. 별도 사전 검증 라운드는 안 함 (사용자 결정).

### D. vexplor_rps 차이 보강
- 응답 미리보기 Quick Test 버튼 (`runQuickResponseTest`) — 등록 연결 외 inline 모드에서도 즉시 응답 확인
- 인증 토큰 자동 안내 UI

### E. 사이드 이슈
- 사이드바 메뉴 로딩 timeout (사용자 사용 중 단발성 발생, `/admin/user-menus` 30s timeout) — 단발성이라 패스했지만 재현되면 별도 진단 필요
- Frontend tsc 타입 에러 2871건 — pre-existing 누적, dev 동작은 OK 라 무시 가능하지만 정리 가치 있음

## 참고 메모리

- `feedback_no_db_no_settings.md` — 명시 요청 없으면 DB/env/build 변경 금지
- `feedback_commit_after_solved.md` — 중간 단계마다 커밋 X, 해결 후 묶어서
- `project_batch_varchar.md` — `batch_*` 의 `_id` 컬럼은 VARCHAR. ::varchar 캐스팅 필수

## 운영 DB 접속 (검증용 read-only 만)

```
host: 183.99.177.40
port: 5432
user: postgres
password: invyone0909!!
db: invyone (메타) / 테넌트 별 invyone_* DB
```

사용자 허락한 범위: **read-only SELECT 만**.
