From 8a10edd8e16ce0dd25cc8a7ac14bcbfe6c77b08e Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 19 May 2026 13:23:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EB=B0=B0=EC=B9=98=EA=B4=80=EB=A6=AC):=20DB?= =?UTF-8?q?=20=EC=BB=A4=EB=84=A5=EC=85=98=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=AA=A9=EB=A1=9D=EC=9D=B4=20?= =?UTF-8?q?=EC=95=88=20=EB=B0=94=EB=80=8C=EB=8A=94=20=EB=B2=84=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 외부 DB id 비교를 strict === 에서 toString() 기반 string 비교로 변경 — number/string 어느 쪽으로 오든 매칭. find 실패로 toConnection=null 되면 auto-select useEffect 가 "내부 DB" 로 강제 복귀시키던 문제 해소 - 연결 변경 시 toTables/fromTables 즉시 초기화 — fetch 실패해도 직전 DB 의 테이블이 잔존하지 않도록 - 배치 파이프라인 / 외부커넥션 멀티 DB 작업 핸드오프 노트 함께 추가 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../admin/batch-management-new/page.tsx | 10 +- .../2026-05-13-batch-pipeline-handoff.md | 223 ++++++++++++++++++ ...6-05-18-external-db-connection-multi-db.md | 132 +++++++++++ 3 files changed, 360 insertions(+), 5 deletions(-) create mode 100644 notes/hjjeong/2026-05-13-batch-pipeline-handoff.md create mode 100644 notes/hjjeong/2026-05-18-external-db-connection-multi-db.md diff --git a/frontend/app/(main)/admin/batch-management-new/page.tsx b/frontend/app/(main)/admin/batch-management-new/page.tsx index dd86b1b2..811ba62f 100644 --- a/frontend/app/(main)/admin/batch-management-new/page.tsx +++ b/frontend/app/(main)/admin/batch-management-new/page.tsx @@ -331,14 +331,14 @@ export default function BatchManagementNewPage() { // 내부 데이터베이스 선택 connection = connections.find((conn) => conn.type === "internal") || null; } else { - // 외부 데이터베이스 선택 - const connectionId = parseInt(connectionValue); - connection = connections.find((conn) => conn.id === connectionId) || null; + // 외부 데이터베이스 선택 — id 가 number/string 어느 쪽이든 안전하게 비교 + connection = connections.find((conn) => conn.id?.toString() === connectionValue) || null; } setToConnection(connection); setToTable(""); setToColumns([]); + setToTables([]); if (connection) { try { @@ -383,12 +383,12 @@ export default function BatchManagementNewPage() { if (connectionValue === "internal") { connection = connections.find((conn) => conn.type === "internal") || null; } else { - const connectionId = parseInt(connectionValue); - connection = connections.find((conn) => conn.id === connectionId) || null; + connection = connections.find((conn) => conn.id?.toString() === connectionValue) || null; } setFromConnection(connection); setFromTable(""); setFromColumns([]); + setFromTables([]); if (connection) { try { diff --git a/notes/hjjeong/2026-05-13-batch-pipeline-handoff.md b/notes/hjjeong/2026-05-13-batch-pipeline-handoff.md new file mode 100644 index 00000000..0512e3f2 --- /dev/null +++ b/notes/hjjeong/2026-05-13-batch-pipeline-handoff.md @@ -0,0 +1,223 @@ +# 배치 파이프라인 작업 핸드오프 (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 만**. diff --git a/notes/hjjeong/2026-05-18-external-db-connection-multi-db.md b/notes/hjjeong/2026-05-18-external-db-connection-multi-db.md new file mode 100644 index 00000000..0269089f --- /dev/null +++ b/notes/hjjeong/2026-05-18-external-db-connection-multi-db.md @@ -0,0 +1,132 @@ +# 외부 DB 커넥션 멀티 DB 지원 + UI 정돈 핸드오프 + +**작업자**: hjjeong +**날짜**: 2026-05-18 +**관련 PR**: #21 (머지 완료, main 반영) +**관련 커밋**: +- `d61777ab` fix(admin): 외부커넥션 mapper varchar 캐스팅 + 외부커넥션/배치관리 UI 정돈 +- `46707bd1` feat(admin): 외부 DB 커넥션 멀티 DB 테스트 + 프로비저닝 시퀀스 reset 보강 + +--- + +## 1. 배경 + +`/admin/automaticMng/exconList` (외부커넥션관리) 페이지가 작동은 했지만 다음 문제가 누적: +- 페이지 자체 스크롤이 안 생겨 잘림, 컬럼/탭 폰트가 다른 admin 페이지보다 큼 +- 연결 테스트 시 500 (mapper SQL 의 VARCHAR vs bigint 비교 오류) +- 비-PostgreSQL DB 등록은 되는데 테스트 단계에서 "PostgreSQL 만 지원" 가드로 막힘 +- 모달이 길어지면 저장/취소 버튼이 화면 밖으로 밀려나감 +- 회사 프로비저닝 시 외부커넥션 INSERT 가 `duplicate key (id)=(5)` 로 실패 + +배치관리 (`/admin/automaticMng/batchmngList`) 도 같은 컨테이너 잘림 + 페이지네이션 부재. + +--- + +## 2. 변경 사항 + +### 2.1 UI / 레이아웃 + +#### `frontend/app/(main)/admin/automaticMng/exconList/page.tsx` +- 컨테이너: `flex h-full min-h-0 overflow-hidden` + Tabs/TabsContent 가 flex 컬럼 → 페이지는 viewport 에 고정, 테이블만 자체 스크롤 +- 폰트 컴팩트: `text-3xl → text-lg`, `text-sm → text-xs`, `h-10 → h-8` +- ResponsiveDataView 에 `scrollContainer` + `compact` 활성화 + +#### `frontend/components/admin/RestApiConnectionList.tsx` +- 동일 패턴: `flex flex-1 min-h-0 overflow-hidden` 컨테이너, ``, `` +- 테이블 헤더/행 컴팩트화 + +#### `frontend/components/common/ResponsiveDataView.tsx` +- `compact` 모드일 때 폰트/셀패딩/카드 폰트도 함께 축소 +- `scrollContainer` 모드일 때 `@3xl:block` 이 `flex` 를 덮어쓰던 우선순위 충돌 수정 (`@3xl:flex` + `flex-col` 분기) +- sticky header bg 알파 50% → 100% (bg-muted) — 본문이 헤더 뒤로 비치던 문제 + +#### `frontend/components/admin/ExternalDbConnectionModal.tsx` +- DialogContent 를 flex 컬럼으로, 본문 div 자체 스크롤, DialogFooter `shrink-0` → 폼이 길어도 저장/취소 항상 보임 + +#### `frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx` +- 컨테이너 잘림 해결 (동일 패턴) +- **페이지네이션 추가**: RPS `vexplor_rps` 배치관리 페이지 참고. `Pagination` 컴포넌트(`frontend/components/common/Pagination.tsx`) 활용. 페이지당 10/20/50/100 선택, 필터 변경 시 1페이지 리셋 + +### 2.2 mapper SQL — VARCHAR 캐스팅 + +V001 legacy 마이그레이션(`notes/gbpark/2026-05-03-legacy-sql-archive/V001__varchar_migration.sql`)이 `EXTERNAL_DB_CONNECTIONS` 의 다음 컬럼들을 VARCHAR 로 바꿔놨는데 mapper 가 안 따라가서 500 발생: + +| 컬럼 | 원래 타입 | V001 후 | 영향 SQL | +|---|---|---|---| +| `ID` | bigint | varchar | WHERE ID = #{id} 비교 | +| `port` | int | varchar | INSERT/UPDATE 바인딩 | +| `connection_timeout` | int | varchar | 동일 | +| `query_timeout` | int | varchar | 동일 | +| `max_connections` | int | varchar | 동일 | + +**`backend-spring/src/main/resources/mapper/externalDbConnection.xml`** 수정 — 모든 위치에 `::varchar` 캐스팅 추가: +- 단건 조회 / 비밀번호 조회 / 이름 중복 체크(exclude) / UPDATE / DELETE 의 ID 비교 6곳 +- INSERT/UPDATE 의 숫자→VARCHAR 컬럼 4종 + +이미 `externalRestApiConnection.xml` 은 캐스팅 처리돼 있어서(REST API 테스트는 정상이었음) 정작 누락된 건 DB mapper 만이었음. 동일 패턴이 다른 mapper 에도 잠재. 의심되는 mapper 가 있으면 `WHERE *_id = #{*_id}` 패턴 검색해서 `::varchar` 캐스트 보강 필요. + +### 2.3 백엔드 — 멀티 DB 테스트 지원 + +**`backend-spring/build.gradle`** — 드라이버 4종 `runtimeOnly` 추가: +``` +runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:3.4.1' +runtimeOnly 'com.mysql:mysql-connector-j:8.4.0' +runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11' +runtimeOnly 'org.xerial:sqlite-jdbc:3.46.1.0' +``` + +Oracle 은 라이선스로 미포함 (UI 에 옵션 있지만 백엔드 default 분기로 "지원하지 않는 DB 타입" 응답). + +**`backend-spring/src/main/java/com/erp/service/ExternalDbConnectionService.java`** +- `executeConnectionTest` 의 PostgreSQL-only 가드 제거 +- `switch (type)` 으로 dbType 별 JDBC URL/props 분기: + - **postgresql**: `jdbc:postgresql://h:p/d` + `connect_timeout` / `sslmode=require` + - **mysql**: `jdbc:mysql://h:p/d` + `connectTimeout`(ms) / `useSSL` / `allowPublicKeyRetrieval` + - **mariadb**: `jdbc:mariadb://h:p/d` + `connectTimeout`(ms) / `useSsl` + - **mssql/sqlserver**: `jdbc:sqlserver://h:p;databaseName=d;loginTimeout=...;encrypt=...` + - **sqlite**: `jdbc:sqlite:` (host/port 무시) +- `defaultPort(String dbType)` 헬퍼 추가 (mysql/mariadb=3306, mssql=1433 등) + +### 2.4 프로비저닝 — VARCHAR PK 시퀀스 reset + +**문제**: 회사 신규 생성 시 `DataCopier.resetSequences()` 가 integer 컬럼만 setval 하고 VARCHAR PK 는 건너뛰어, V001 으로 INT→VARCHAR 변환됐지만 `DEFAULT nextval(...)` 의존성이 남은 컬럼(external_db_connections.id 등) 이 매번 새 회사에서 충돌. + +**원래 코드의 의도** (DataCopier.java 의 주석): +> 레거시 DB 에선 SERIAL 이었다가 나중에 TEXT 로 타입 변경된 컬럼이 있을 수 있음. 이런 컬럼에 setval 을 호출하면 "COALESCE types text and integer cannot be matched" 예외 발생. +> invyone 은 대다수 PK 가 VARCHAR (문자열 PK). 시퀀스가 연결되어 있어도 실제 INSERT 때 nextval 을 사용하지 않으므로 setval 은 no-op. + +→ 두 번째 가정이 V001 영향 테이블에는 안 맞았음. + +**`backend-spring/src/main/java/com/erp/provisioning/DataCopier.java`** `resetSequences()` 확장: +- `isIntegerLike()` 외에 `isVarcharLike()` 분기 추가 +- VARCHAR 인 경우 `setval(seq, GREATEST(COALESCE((SELECT MAX(col::bigint) FROM tbl WHERE col ~ '^[0-9]+$'), 0), 1))` 사용 +- `::bigint` 명시 캐스트로 COALESCE 타입 충돌 회피 +- `^[0-9]+$` 정규식으로 UUID 같은 비숫자 PK 거름 → 0 으로 떨어져 무해 + +→ 새 회사 생성 시 자동 처리. 기존 회사 DB 들은 1회성 SQL 필요: +```sql +SELECT setval( + pg_get_serial_sequence('external_db_connections', 'id'), + COALESCE((SELECT MAX(id::bigint) FROM external_db_connections), 0) +); +``` + +--- + +## 3. 미해결 / 후속 작업 + +1. **암호화 키 mismatch 데이터** — `external_db_connections.password` 컬럼이 다른 인스턴스에서 가져온 경우(예: VEX or 다른 INVYONE) AES 키가 달라 `Given final block not properly padded` 로 복호화 실패. 코드 차원 해결 불가. 각 행 편집해서 비밀번호 재입력하거나, 이전 인스턴스의 `encryption.secret-key` 값을 알면 SQL 일괄 재암호화 가능. + +2. **SQL 실행 / 테이블 메타 조회 가드** — `ExternalDbConnectionService.executeQuery` (L369), `getTables` (L406 부근), `getColumns` (L446 부근) 에 동일한 PostgreSQL-only 가드가 남아있음. 멀티 DB 테스트는 풀었지만 이 기능들은 아직 PG 전용. 사용자 요청 시 같은 패턴으로 풀 수 있음. + +3. **Oracle 드라이버 미포함** — `com.oracle.database.jdbc:ojdbc11` 추가 + service 의 `switch` 에 `oracle` case 추가하면 됨. 라이선스 검토 필요. + +4. **다른 mapper 의 VARCHAR 캐스팅 누락 잠재** — V001 로 INT→VARCHAR 변환된 컬럼이 다른 mapper 에도 있을 수 있음. 신규 500 보이면 mapper 가 `WHERE *_id = #{*_id}` 패턴인지 먼저 의심. + +--- + +## 4. 검증 + +- `./gradlew build` (backend) — BUILD SUCCESSFUL, unit test 통과 +- `npm run build` (frontend) — 전체 페이지 빌드 통과 (115+ 라우트) +- main 머지 후 fast-forward 동기화 완료