# 외부 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` 컨테이너, `<Table divClassName="flex-1 overflow-auto">`, `<TableHeader className="sticky top-0 z-10 bg-muted">`
- 테이블 헤더/행 컴팩트화

#### `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:<database_name 을 파일경로로>` (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 동기화 완료
