1. 운영 console.log/warn 제거 (G15) — page.tsx 의 이모지 prefix 디버그 로그
(🔍🔄✅🗑️📥📊📋) 일괄 제거. catch 블록의 console.error 는 추적용으로
유지. CreateTableModal 의 컬럼 조회 디버그 로그도 정리.
2. useLogTable dead code 정리 (G16) — CreateTableModal 의 useLogTable state,
handleCreateTable 분기, 주석 처리된 체크박스 UI 모두 제거. 시그니처 안 맞는
createLogTable 호출 페이로드까지 같이 사라짐. Activity / Checkbox import 도
필요 없어졌으므로 제거.
3. 에러 메시지 일관화 (G17) — DdlController 와 TableManagementController 의
"최고 관리자 권한이 필요합니다." 메시지 모두 "최고 관리자(SUPER_ADMIN) 권한이
필요합니다." 로 통일. 일반 ADMIN 권한 메시지는 그대로 유지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. getColumnGroup metaCols 확장 — 시스템 자동 생성 컬럼이 5개만 인식되어
사용자 컬럼에 섞여 보이던 문제. objid / tenant_id / creator / modifier /
created_at / updated_at 추가 (총 11개).
2. updateColumnWebType 회사코드 격리 — 모든 호출이 company_code='*' 로 저장돼
회사 관리자가 자기 회사 전용 web_type 변경 시 모든 회사 공통 설정을 건드림.
컨트롤러에서 @RequestAttribute("company_code") 받아 service 에 전달.
3. validateUniqueConstraints N+1 해소 — hasColumn 이 루프 안에서 매번 호출되어
UNIQUE 컬럼 N 개일 때 N 번의 information_schema 조회. 루프 밖으로 빼서 1 번.
4. sanitize 강화 — 빈 문자열 / 숫자 시작 / 63자 초과 / SQL 예약어 모두
IllegalArgumentException 으로 거부. 빈 식별자가 동적 SQL 에 끼어들어 500
에러 노출되던 패턴 방지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
종합 감사 결과 발견된 블로커 5건:
1. AddColumnModal / CreateTableModal prop 미스매치 — 호출부 tableName/sourceTableName(camelCase)
vs 인터페이스 table_name/source_table_name(snake_case). 결과적으로 컬럼 추가 기능이 실제로
동작하지 않았음 (/api/ddl/tables/undefined/columns). 호출부를 인터페이스에 맞춰 통일.
2. IS_NULLABLE 강제 'Y' 덮어쓰기 (backend mapper) — upsertColumnSettings 의 VALUES 절이
모든 케이스에서 'Y' 리터럴을 박음. NN 토글 후 "컬럼 설정 저장" 누르면 필수 입력 제약이
조용히 사라짐. COALESCE(#{is_nullable}, 'Y') 로 변경 + ON CONFLICT 절에 IS_NULLABLE
COALESCE 추가. Service 도 settings 에서 is_nullable 을 읽어 'Y'/'N' 으로 정규화 후 전달.
3. 저장 안 한 편집 침묵 손실 — 우측 패널에서 편집 후 저장 안 누르고 다른 테이블로 이동 시
변경 사항 소실. hasUnsavedChanges memo 추가 (columns vs originalColumns JSON 비교).
handleTableSelect 에서 dirty 면 confirm 다이얼로그.
4. PK / NN / IDX / UQ 위치별 비대칭 저장 — 그리드 행 칩은 즉시 저장하는데 우측 상세 패널
토글은 메모리만 변경하고 저장 버튼 필요. 두 위치 모두 즉시 저장으로 통일 (상세 패널의
onColumnChange 가 is_nullable / is_unique 필드를 받으면 handleNullableToggle /
handleUniqueToggle 호출하도록).
5. SUPER_ADMIN role 검증 누락 (backend 보안) — DdlController 의 isSuperAdmin 이 company_code
== "*" 만 보고 role 클레임 무시. 토큰 변조 또는 "*" 회사 소속만으로 운영 DB DROP / ALTER
가능. company_code "*" AND role "SUPER_ADMIN" 둘 다 충족 시에만 통과로 변경. 11개 엔드포인트
에 @RequestAttribute("role") 추가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 백엔드: getBatchManagementGlobalSparklineData 쿼리 추가 (generate_series 로
24개 슬롯 고정, 회사 전체 배치 LEFT JOIN 집계)
- 백엔드: GET /api/batch-management/sparkline 엔드포인트 추가
- 프론트: BatchStats/SparklineData 타입을 백엔드 mapper 의 snake_case 응답키와
일치시킴 (today_count, today_failed_count, hour_slot, success_count, ...).
키 미스매치로 stats 카드가 NaN 으로 표시되던 버그 해소
- 프론트: GlobalSparkline 컴포넌트의 Math.random() 더미 막대를 실데이터 prop 으로
교체. row-level Sparkline 도 동일 키 정렬로 정상 렌더되도록 수정
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DdlService.dropColumn: ALTER TABLE ... DROP COLUMN (CASCADE 미사용 → FK 참조 시 Postgres 거부, DBeaver 동일)
- 시스템 테이블 / 예약 컬럼(id/created_date/updated_date/company_code/writer) 보호
- 같은 트랜잭션에서 table_type_columns / column_labels 메타 청소 + ddl_execution_log 기록
- DdlController: DELETE /api/ddl/tables/{table}/columns/{column} (SUPER_ADMIN 전용)
- ddlApi.dropColumn 헬퍼
- ColumnGrid: ... 버튼을 DropdownMenu 로 교체, "컬럼 삭제" destructive 메뉴 아이템
- page.tsx: 컬럼 삭제 확인 다이얼로그 + 핸들러, FK 거부 시 토스트로 안내
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존엔 PostgreSQL 만 테스트 가능했고, V001 (SERIAL→VARCHAR) 마이그레이션 이후 회사 프로비저닝 시 시퀀스가 max(id) 보다 작은 상태로 남아있어서 새 외부 커넥션 등록 시 duplicate key 가 재발하던 문제 해결.
backend
- build.gradle: MariaDB/MySQL/MSSQL/SQLite JDBC 드라이버 4종 runtimeOnly 추가
- ExternalDbConnectionService.executeConnectionTest: PostgreSQL-only 가드 제거, dbType 별 JDBC URL/props 분기 구현 (postgresql/mysql/mariadb/mssql/sqlite). defaultPort helper 추가
- mapper/externalDbConnection.xml: INSERT/UPDATE 의 port/connection_timeout/query_timeout/max_connections 에 ::varchar 캐스팅 추가 (V001 으로 VARCHAR 가 됐는데 클라가 숫자로 보내서 character varying = bigint 비교 불가로 500 나던 것)
- DataCopier.resetSequences: VARCHAR PK + 시퀀스 의존성이 남은 컬럼도 setval 대상에 포함. MAX(col::bigint) + col ~ '^[0-9]+$' 정규식으로 type mismatch 회피하면서 숫자형 VARCHAR PK 만 안전하게 reset
frontend
- ExternalDbConnectionModal: DialogContent 를 flex 컬럼으로, 본문에 자체 스크롤 + Footer shrink-0 → 길어진 폼에서도 취소/생성 버튼이 항상 보이도록
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존: 중복 그룹 코드 등록 시 PK violation 으로 500 + "서버 내부 오류"
→ 사용자가 왜 안 되는지 알 수 없음.
변경: insertCodeInfo 진입 시 getCodeInfoInfo 로 사전 체크. 이미 존재하면
IllegalArgumentException 으로 던져 GlobalExceptionHandler 가 자동으로
400 + "이미 존재하는 그룹 코드입니다: {코드}" 메시지로 응답.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5/15 common-code 재설계가 화이트리스트를 8종으로 좁히면서 빠뜨린
운영 DB 데이터 정리. 90787d83 의 화이트리스트 확장 fix 는 회복용
보호막이었고, 본 PR 은 데이터를 표준으로 통합하는 후속 정리.
매핑:
category/select/radio/checkbox/boolean → code
textarea → text
datetime → date
영향: 메타 DB 1,207 row 갱신. 테넌트 DB 들은 비어있어 0 row.
WHERE input_type IN (...) 으로 멱등 (재실행 시 0 row).
화이트리스트 축소는 운영 안정 확인 후 별도 PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5/15 common-code 재설계가 화이트리스트를 8종으로 좁히면서 운영 DB 의
옛 input_type 값들을 매핑/정리하는 마이그레이션을 빠뜨려, 컬럼 설정
저장 batch POST 가 한 row 라도 legacy 값(category/select/textarea/
checkbox/radio/datetime/boolean)을 포함하면 400 거부.
운영 메타 DB 실측: 화이트리스트 밖 row 1,207건 (category 886,
select 149, textarea 102, checkbox 55, radio 12, datetime 2, boolean 1).
운영 데이터/UI 의미를 보존하기 위해 매핑이 아닌 화이트리스트 확장
(legacy 7종 추가)으로 회복. legacy 정리는 별도 PR 에서 점진적으로.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 DB 의 SCREEN_LAYOUTS.PROPERTIES 컬럼이 character varying 인데 mapper SQL 은
PROPERTIES->>'...' 와 JSONB_SET(PROPERTIES, ...) 를 그대로 사용해
PG 가 'operator does not exist: character varying ->> unknown' 으로 거부.
이로 인해 syncScreenLayouts 가 던지는 SQLException 이 try-catch 로
무시되긴 하지만 외부 @Transactional 이 이미 aborted 상태가 되어
후속 ensureTableInLabels (insertTableLabelIfNotExists) 가
'current transaction is aborted' 로 연쇄 실패 → 컬럼 설정 저장 500.
- SL.PROPERTIES::JSONB 캐스팅 (WHERE / SET 양쪽)
- JSONB_SET 결과를 ::TEXT 로 캐스팅해 varchar 컬럼에 안전 저장
- 운영 4 DB (invyone, siflex/test01/test02 _invyone) 전수 검증:
invalid JSON row 0건 → 캐스팅 안전
mapper SQL 만 변경. DB 마이그레이션 불필요.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
테이블 타입관리의 모든 쓰기 API (UNIQUE/NOT NULL 토글, 컬럼 설정 저장,
input-type upsert) 가 500 반환. 원인은 mapper SQL 의
ON CONFLICT (TABLE_NAME, COLUMN_NAME, COMPANY_CODE) 가 매칭할 unique
제약/인덱스가 운영 DB 에 존재하지 않아 PG 가
"there is no unique or exclusion constraint matching the ON CONFLICT
specification" 으로 거부.
- StartupSchemaMigrator MIGRATIONS 에 V025 / RUN_090 (1) (2) 추가:
(1) ROW_NUMBER 로 (table, column, company) 중복 행 정리
(운영 메타 DB 실측 2 그룹 / 4 row — 동일 데이터의 NULL updated_date
옛 row 제거. 테넌트 DB 들은 중복 0건).
(2) UX_TABLE_TYPE_COLUMNS_TCC UNIQUE INDEX 생성 (IF NOT EXISTS — 멱등).
- RUN_090_MIGRATION.md 신설.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5/15 common-code 재설계(commit 2348800e) 가 mapper SQL 6 군데 컬럼
참조를 CODE_INFO 로 바꾸면서 DB 컬럼 rename 마이그레이션을 빠뜨려,
모든 테넌트 사이트에서 테이블타입관리 > 테이블 클릭 시
GET /api/table-management/tables/{name}/columns 가 500
(column "code_info" does not exist) 을 반환.
- StartupSchemaMigrator MIGRATIONS 에 V024 항목 추가
DO 블록으로 information_schema 확인 후 rename — 멱등.
- RUN_089_MIGRATION.md 신설 (V023 IS_SOLUTION_ONLY 운영 가이드도 합본).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AdminController.getAdminMenus 에 Host 헤더 기반 is_management_host 추가
(기존엔 user-menus 만 필터, /admin/menus 는 미적용이라 관리자 모드 사이드바에서 노출됨)
- admin.xml selectAdminMenuList anchor + recursive 양쪽에 IS_SOLUTION_ONLY 필터 추가
- StartupSchemaMigrator: ALTER 외에 UPDATE 추가, 프로비저닝된 테넌트 DB 의
회사관리/서브도메인관리/감사로그 메뉴 행을 부팅 시 IS_SOLUTION_ONLY=TRUE 로 마킹
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- mapper/externalDbConnection.xml: WHERE ID = #{id} 5곳 + ID != #{exclude_id} 1곳에 ::varchar 캐스팅 추가
(EXTERNAL_DB_CONNECTIONS.ID 가 V001 마이그레이션으로 VARCHAR 인데 long 바인딩되어 character varying = bigint 비교 불가로 500 발생하던 것을 해결)
- exconList: 페이지 overflow-hidden + Tabs/TabsContent 가 flex 컨테이너, ResponsiveDataView scrollContainer 활성화로 테이블 안에서만 sticky header + 자체 스크롤
- exconList/RestApiConnectionList: text-3xl→text-lg/text-sm→text-xs/h-10→h-8 등 컴팩트 폰트로 통일 (배치관리/플로우관리와 톤 매칭)
- RestApiConnectionList: Table divClassName 으로 wrapper 자체에 스크롤 위임 + sticky TableHeader 적용
- ResponsiveDataView: compact 모드일 때 폰트/셀패딩/카드 폰트도 함께 축소, scrollContainer 모드에서 @3xl:block 이 flex 를 덮어쓰던 우선순위 충돌 해결, sticky header 알파 제거
- batchmngList: Pagination 컴포넌트 적용 (RPS batchmngList 참고, 페이지당 10/20/50/100 선택), 컨테이너를 h-full min-h-0 overflow-hidden + 리스트만 자체 스크롤로 변경
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이번 PR 은 invyone 멀티테넌시 SaaS 의 "관리 plane vs 테넌트 plane" 격리를
4 영역(PR #A~D) 에서 강화하고, 별도로 진행 중이던 부서관리 후속 작업을 포함한다.
# 보안 (plane 격리)
PR #A — controller/CompanyManagementController 인증 누락 패치
/api/company-management/* 가 JWT/role/host 체크 없이 외부에서 누구나 회사 삭제
+ 디스크 통계 호출 가능했던 critical 누수 막음. SuperAdminGuard.enforce() 적용.
PR #C — cross-tenant 컨트롤러 호스트 격리 + 감사 로그
CrossTenantContext.requireManagementHost() 헬퍼 추가, 5 컨트롤러
(CrossTenantContext/Controller/UserController/RoleController/DeptController) 모두
테넌트 호스트에서 호출 시 403. CompanyAuditLogService 에 cross-tenant write 4종
(USER_CREATE/DELETE, PW_RESET, ROLE_UPDATE) audit action 추가.
SuperAdminGuard.isTenantHost 가시성 public static 으로 승격.
PR #B — 프론트 솔루션 전용 admin 페이지 가드
admin/* 페이지 전수 분류 결과 솔루션 전용 3건 식별:
subdomainList / companyList / audit-log. 각 페이지에 isManagementHost
useEffect 가드 + redirect 추가. 사이드바도 같이 숨김.
PR #D — MENU_INFO.IS_SOLUTION_ONLY 컬럼 + DB-driven 메뉴 필터
V023 마이그레이션으로 컬럼 추가 + 솔루션 메뉴 3개 마킹.
admin.xml selectUserMenuList 에 호스트 기반 필터 추가, AdminController.getUserMenus
가 Host 헤더로 is_management_host 결정. 프론트 MANAGEMENT_ONLY_MENU_URLS
하드코딩 set 폐기 (DB 가 대신함). 페이지 자체 가드는 defense in depth 로 유지.
StartupSchemaMigrator 에 V023 등록되어 모든 테넌트 DB 부팅 시 자동 적용.
# 부서관리 후속 (이전 PR #18/#19 follow-up)
DepartmentController/Service + frontend deptMngList/department.ts 의 추가 작업분.
이번 격리 작업과 무관하지만 같이 정리.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- security: syncManagers 가 user_id 의 회사 격리·실존 검증 (cross-tenant injection 차단)
- bug: base_date 필터에 START_DATE IS NULL 조건 추가 — 옛날 데이터가 기준일 켜자마자 사라지는 문제 해결
- bug: PUT partial update — 매니저 키 없으면 sync skip (단일 필드 수정 시 매니저 보존)
- bug: 페이지 로드 시 단일 컬럼 backfill — PR #19 이전 데이터가 chip UI 에서 사라지는 문제 해결
- validation: Controller base_date YYYY-MM-DD regex (잘못된 형식 시 400)
- validation: 프론트 handleSave start_date > end_date 체크
- robustness: parseManagersJson 64KB max + log.warn (catch silent swallow 제거)
- ops: StartupSchemaMigrator 실패 테넌트 DB 명단 부팅 종료 시 log.error 집계
- 백엔드: selectDepartments 에 base_date <if> 블록 추가 (start_date <= base_date AND (end_date IS NULL OR end_date >= base_date))
- 서비스/컨트롤러에 3-arg overload 와 @RequestParam("base_date") 추가
- 프론트: 사용기간 RadioGroup + 시작일/종료일 Row 의 {false &&} hide 제거
- loadDepartments 가 periodMode === "date" 일 때 baseDate 를 API 에 전달
이전 PR (#15) 의 Java parseColumnArray 분기 추가가 실제 운영 환경에서
빈 배열을 그대로 반환 — MyBatis ↔ PostgreSQL JDBC 의 array 타입 변환이
java.sql.Array 가 아닌 다른 경로로 도착하는 듯.
방식 변경: SQL 단에서 ARRAY_AGG(...)::text 캐스트 → PostgreSQL 가
"{email,phone}" String 으로 반환. parseColumnArray 의 기존 String 분기
(중괄호 제거 + 쉼표 split) 가 자연스럽게 처리.
장점:
- JDBC 드라이버 / MyBatis 변환 동작에 의존하지 않음
- parseColumnArray 코드 단순 복원 (List/String 2분기)
- 한 줄 SQL 변경으로 PK/IDX 두 쿼리 모두 해결
검증:
- gradle compileJava BUILD SUCCESSFUL
- solution.invyone.com 에서 customer_mng PK columns / email IDX columns
비어있지 않음 확인 예정
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
테이블 타입 관리 페이지에서 PK/IDX 토글 후 UI 상태가 갱신되지 않던 버그.
1. Backend: TableManagementService.parseColumnArray
- PostgreSQL ARRAY_AGG 가 java.sql.Array (PgArray) 로 오는데
List/String 만 처리해서 항상 List.of() 반환 → columns 빈 배열
- 조치: java.sql.Array 분기 추가, arr.getArray() → Object[] → List<String>
2. Frontend: loadConstraints
- 백엔드는 result.put("primary_key", ...) 로 snake_case 반환
- 프론트가 data.primaryKey 로 camelCase 로 읽어 undefined → 항상 빈 PK
- 조치: data.primary_key 로 통일
이전 PR (#14) 의 IDX payload 수정과 합쳐, PK/IDX 토글이 API 호출/DB 적용/
UI 반영까지 한 사이클로 동작.
검증:
- gradle compileJava BUILD SUCCESSFUL
- solution.invyone.com 에서 IDX 토글 양방향 확인 예정
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bug hunt security-reviewer 발견 2건 보안 fix:
1. TableManagementController 인가 누락 (OWASP A01 Broken Access Control)
- 15개 write/DDL endpoint 가 admin role 검증 없이 JWT 만 있으면 호출 가능
- 일반 사용자가 PK 재설정/index 변경/컬럼 수정 가능했음
- 조치:
- DepartmentController 의 isAdmin/isSuperAdmin helper 패턴 복사
- SUPER_ADMIN 전용 (DDL 5건): primary-key, indexes, nullable, unique, log
- admin (COMPANY_ADMIN+) (10건): updateColumnSettings, addTableData, editTableData, deleteTableData, multi-save 등
- read 19건은 그대로 (일반 사용자 접근 유지, company_code 격리만)
2. createLogTable SQL injection (OWASP A03 Injection)
- information_schema.data_type 을 raw concat 으로 DDL 생성
- 조치:
- ALLOWED_LOG_COLUMN_TYPES Set 으로 화이트리스트 (varchar/text/integer/numeric/boolean/date/timestamp/jsonb 등 21개)
- sanitize 빈 식별자 차단 + 원본 테이블에 없는 컬럼 skip
- colDefs empty 시 IllegalArgumentException
- 알 수 없는 type 은 text 로 안전 대체
검증:
- gradle compileJava BUILD SUCCESSFUL
- mapper XML 0건 변경 (READ 경로 보호)
- 별도 미해결 보안 이슈 (k8s secrets git 노출, CORS 와일드카드 등) 는 본 PR scope 외
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
배치 생성 흐름 검증 중 발견된 4가지 이슈 일괄 정정.
1) BatchManagementService.previewRestApiData — camelCase 키 명시 remap
직전 커밋(b752de23)에서 convertCamelToSnake() 호출 추가했지만 그 함수의 실제 구현이
batch_configs 전용 snake→snake remap 이라 사실상 no-op. 프론트의 apiUrl 등 camelCase
가 변환되지 않아 isBlank(api_url)=true → 400.
→ previewRestApiData 진입부에 직접 remap (apiUrl/apiKey/requestBody/dataArrayPath/
paramType/paramName/paramValue/paramSource/authServiceName 9개 키).
2) batchManagement.ts.previewRestApiData — 응답 totalCount 정규화
백엔드는 total_count (snake_case) 로 응답하는데 프론트는 result.totalCount 로 읽음.
토스트가 "2개 필드, undefined개 레코드" 로 표시됨.
→ 응답 normalize: total_count ?? totalCount ?? 0.
3) batch-management-new/page.tsx — root h-full overflow-y-auto
페이지 root 가 overflow 처리가 없어 FROM/TO 카드 아래의 매핑 카드가 탭 컨테이너
밖으로 잘려 사용자가 못 봄.
→ root div 에 h-full overflow-y-auto 추가.
4) RestApiToDbMappingCard — v5 컨벤션에 맞춘 컴팩트화
다른 메뉴들과 톤 통일. CardHeader 패딩 축소, 폰트 size 일괄 다운,
행 padding p-3 → p-2, Select/Input h-9 → h-7 text-xs, 순서 원형 h-6 → h-5,
카드 내부 height 360 → 300px, 매핑 추가 버튼/삭제 버튼 컴팩트.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
데이터 불러오고 매핑하기 클릭 시 400 발생.
원인: 프론트는 camelCase (apiUrl/endpoint/method/apiKey/dataArrayPath/paramType/...)
로 body 를 보내는데 백엔드는 snake_case (api_url/endpoint/method/api_key/...) 키로 읽음.
다른 service 진입점 (updateBatchConfig / executeBatchConfig 등) 은 convertCamelToSnake
를 호출해서 자동 변환하는데 previewRestApiData 만 빠져있어 isBlank(apiUrl)=true 가
되며 IllegalArgumentException → 400.
수정: previewRestApiData 진입부에 convertCamelToSnake(body) 한 줄 추가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TableManagementService.normalizeInputType(value, context) 오버로드 — user-insert/user-update-type 만 8개 검증, user-update-other/system-normalize 는 skip
- TableManagementService.updateColumnSettings: payload 의 input_type 키 존재 여부로 context 분기 (input_type 자체 변경 vs 다른 속성 변경)
- DdlService.addColumn / saveColumnMetadata: convertToInputType 결과를 USER_SELECTABLE_INPUT_TYPES (8개) 와 대조, 외이면 IllegalArgumentException
mapper XML 5곳 (categoryTree / entityJoin / tableCategoryValue / screenManagement / tableManagement / entityReference) 무변경 — READ 경로 12개 그대로.
spec: .omc/specs/deep-dive-table-type-storage-ui-separation.md (v3.2 §6.3)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
외부 커넥션 관리에서 연결 테스트 시 500 발생.
원인: 운영 DB 의 external_rest_api_connections.id 가 V001 legacy 마이그레이션으로
character varying 인데 mapper 의 WHERE ID = #{id} 가 controller 의 int id 를
그대로 받아 PgJDBC 가 보내는 BIND 가 정수.
PostgreSQL 이 "operator does not exist: character varying = integer" 거부 →
SQLException → ApiResponse 500 ("서버 내부 오류").
수정: 4곳 모두 #{id}::varchar 캐스팅 추가.
- getExternalRestApiConnectionInfo (SELECT)
- updateExternalRestApiConnection (UPDATE)
- deleteExternalRestApiConnection (DELETE)
- updateExternalRestApiConnectionTestResult (UPDATE)
배치 작업의 batch_config_id 패턴과 동일. 같은 V001 영향을 받은 다른 mapper
(externalDbConnection.xml / externalCallConfig.xml / booking.xml / delivery.xml /
multiConnection.xml / taxInvoice.xml 등) 도 같은 수정이 필요할 가능성 — 별도 작업으로 분리.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 DB 검증 결과 batch_execution_logs.duration_ms / total_records /
success_records / failed_records 가 모두 character varying 으로 정의됨
(V001 legacy 마이그레이션 흔적). PgJDBC 가 Long/Integer 를 VARCHAR 컬럼에
자동 변환하지 못할 위험이 있어 명시적으로 String.valueOf 로 변환 후 전달.
mapper 의 COALESCE default 가 '0' (문자열) 인 점과도 일관.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
검증 전 선제 보강 2건.
1) BatchExecutor.writeTo 의 @Transactional 제거
- 같은 클래스 내부 호출(execute → writeTo)이라 Spring AOP 가 우회되어 어차피 안 걸림
- batch 의 정상 동작은 row 단위 독립 commit (일부 실패해도 다른 row 는 살아야 함).
vexplor_rps 도 동일 패턴
2) BatchManagementService.executeBatchConfig 의 end_time 을 Timestamp 객체로 직접 전달
- 기존: Timestamp.toString() 으로 변환 후 #{end_time}::timestamp 캐스트
- 신규: Timestamp 객체 그대로 → PostgreSQL JDBC 가 timestamp 자동 변환,
mapper 의 ::timestamp 캐스트는 noop
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존 stub(0 만 리턴) 였던 BatchManagementService.executeBatchConfig 를 실제 ETL 실행 + 로그 기록 흐름으로 교체.
흐름:
1. RUNNING 상태로 batch_execution_logs INSERT (도중 비정상 종료 추적용)
2. BatchExecutor.execute(batchConfig) 호출
- 정상: failedRecords > 0 면 PARTIAL/FAILED, 아니면 SUCCESS
- 예외: FAILED 로 마킹, error_message 기록
3. 로그 UPDATE — execution_status, end_time, duration_ms, total/success/failed_records, error_message
4. controller 응답에 execution_status + error_message 동봉
server_name 은 InetAddress.getLocalHost(), process_id 는 ProcessHandle.current().pid().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vexplor_rps batchSchedulerService.executeBatchMappings 의 1:1 이식.
흐름:
1. mappings 를 fixed/non-fixed 로 partition
2. non-fixed 를 (from_connection_type, from_connection_id, from_table_name) 으로 그룹화
3. 그룹별 FROM 읽기 → MappingTransformer.transformRow → TO 저장
4. (totalRecords, successRecords, failedRecords) 집계
FROM 소스:
- internal : sqlSession.getConnection() 의 동적 SELECT (식별자 화이트리스트 escape)
- external_db : ExternalDbConnectionService.executeQuery (SELECT-only)
- restapi : ExternalRestApiConnectionService.fetchData (등록된 연결 + dataArrayPath)
TO 대상:
- internal : INSERT / UPSERT(save_mode + conflict_key, ON CONFLICT DO UPDATE/NOTHING)
updated_date 컬럼 있으면 자동으로 NOW() 갱신
- restapi : 행 단위 POST/PUT/DELETE — testConnection 으로 호출
- external_db : 미지원 (보안 정책: ExternalDbConnectionService 가 SELECT-only)
vexplor_rps 대비 단순화 항목 (필요 시 후속 Phase 로 분리):
- to_api_body 템플릿 기반 일괄 전송
- URL_PATH_PARAM 컬럼 처리
- auth_tokens 자동 조회 (inline-mode REST API: from_connection_id 없이 from_api_url 직접 호출)
- row_filter_config
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vexplor_rps batchSchedulerService L540~617 의 변환 로직 1:1 이식.
의존성 없는 정적 유틸 — BatchExecutor 가 FROM 읽기 결과를 TO 형태로 변환할 때 사용.
- transformRow: mapping_type 별 분기 (direct/conditional/fixed),
멀티테넌시용 company_code 자동 주입, 점 표기법 path 평가
- evaluateConditional: ConditionalConfig.rules 의 when/then lookup + default 폴백.
단순 문자열 동등 비교 (SpEL/JEXL 표현식 평가 안 함)
- getValueByPath: "response.access_token" 같은 중첩 키 지원
- parseConditionalConfig: JSONB 가 Map/String/null 셋 다 가능 — 안전 normalize
- partitionFixed: non-fixed / fixed 매핑 분리 헬퍼
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- V021 Flyway: ALTER TABLE BATCH_MAPPINGS ADD COLUMN MAPPING_CONFIG JSONB
- StartupSchemaMigrator: 같은 ALTER 를 idempotent 항목으로 추가 (모든 활성 테넌트 DB 부팅 시 동기화)
- batch.xml: getBatchMappingsByConfigId SELECT 에 MAPPING_CONFIG::TEXT cast,
insertBatchMapping VALUES 에 #{mapping_config,jdbcType=OTHER}::jsonb
- BatchService: ObjectMapper 주입, parseJsonField/stringifyJsonField 유틸,
syncMappings 는 INSERT 전 직렬화, attachMappings 는 SELECT 후 Map 으로 역직렬화
- RUN_087_MIGRATION.md: 운영용 마이그레이션 runbook (사전 점검/사후 검증/롤백)
conditional 매핑(when/then/default) 룰을 행 단위 저장하는 컬럼.
direct/fixed 는 NULL. Phase 2~3 에서 프런트/엔진이 이 컬럼을 읽고 쓴다.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- BatchService: insertBatch/updateBatch 가 body.mappings 받아 replace-all 동기화,
getBatchInfo 가 batch_mappings 리스트 attach (지금까지는 silently drop)
- batch.xml: getBatchMappingsByConfigId / insertBatchMapping / deleteBatchMappingsByConfigId 신규
- batchExecutionLog.xml / batchManagement.xml: batch_config_id 에 ::varchar 캐스팅,
오타 'batch_execution_log' → 'batch_execution_logs' 정정
- batchmngList/page.tsx: 같은 batch ID 가 회사 간 중복될 때 React key 충돌 방지
- notes: vexplor_rps → INVYONE 배치 파이프라인 이식 분석 노트
vexplor_rps → INVYONE 파이프라인 이식의 Phase 0.
구체 분해는 notes/hjjeong/2026-05-12-batch-pipeline-current-state.md 참조.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Frontend: body 키를 snake_case (user_id/new_password) 로 변환
- Controller: new_password 도 추출해서 service 에 전달
- Service: 2-arg 오버로드 추가, newPassword 입력값 사용 (blank 일 때만 Welcome1! fallback), userId null/blank 시 IllegalArgumentException
증상: 사용자관리에서 비밀번호 초기화 modal 입력 → backend 가 user_id=null 로 SQL 실행 (0행) + newPassword 무시 후 항상 Welcome1! 로 덮어쓰기.