Files
invyone/notes/hjjeong/2026-05-18-external-db-connection-multi-db.md
hjjeong 8a10edd8e1 fix(배치관리): DB 커넥션 변경 시 테이블 목록이 안 바뀌는 버그
- 외부 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) <noreply@anthropic.com>
2026-05-19 13:23:24 +09:00

7.9 KiB

외부 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:blockflex 를 덮어쓰던 우선순위 충돌 수정 (@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 필요:

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 의 switchoracle 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 동기화 완료