테이블 타입 관리 페이지에서 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>
테이블 타입 관리 페이지에서 IDX 토글 / 테이블 라벨 저장이 400 에러로
조용히 실패하던 버그. 백엔드는 body.get("column_name") / get("index_type")
/ get("display_name") 등 snake_case 로 읽는데 프론트가 camelCase 로 보내고
있었음 (CLAUDE.md Map key snake_case 컨벤션 위반).
- POST /table-management/tables/:t/indexes
{ columnName, indexType, action } → { column_name, index_type, action }
- PUT /table-management/tables/:t/label
{ displayName } → { display_name }
PK 는 다이얼로그 확인 흐름, NN/UQ 는 key 가 맞아 영향 없음.
SUPER_ADMIN 으로 테스트 시 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>
새 배치 (REST API → DB) 진입 시 매번 사용자가 "데이터베이스 커넥션 선택" 셀렉트에서
"내부 DB" 를 직접 골라야 했음. 대부분의 배치가 internal 적재라 디폴트 채움이 자연스러움.
1) TO DB 자동 선택
useEffect 로 batchType === "restapi-to-db" + connections 로드 + toConnection 비어있음
조건 만족 시 handleToConnectionChange("internal") 자동 호출. 사용자가 외부 DB 로 변경하면
toConnection != null 이 되어 더 이상 자동 동작 안 함.
2) Select controlled 화
DB 커넥션/테이블 Select 가 value prop 없는 uncontrolled 상태였음.
setToConnection/setToTable state 가 바뀌어도 Select UI 가 placeholder 그대로 →
programmatic 자동 선택이 시각적으로 반영 안 됨.
→ value prop 추가:
- DB 커넥션: toConnection.type === "internal" ? "internal" : String(toConnection.id)
- 테이블: toTable
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
배경:
- frontend build 의 가장 큰 시간 소비 = runner stage 의 COPY node_modules (12분 16초)
- 전체 21분 34초 중 57%
- next.config.mjs 의 output: "standalone" 가 prod 빌드에서 이미 활성 상태였으나, Dockerfile 의 runner stage 가 .next 통째 + node_modules 통째를 COPY 하느라 standalone 결과물 미활용
조치:
- runner stage 재작성:
- .next 전체 → .next/standalone (server.js + 실제 사용 node_modules)
- .next/static 별도 COPY (standalone 가 자동 포함 안 함)
- public 별도 COPY (standalone 가 자동 포함 안 함)
- node_modules 통째 COPY 제거 (standalone 가 알아서 포함)
- package.json COPY 제거 (server.js 직접 실행)
- CMD: npm start → node server.js
검증:
- frontend 에 dynamic require/import 0건 (정적 import 만) → standalone 의존성 추적 정확
- prisma 가 package.json 에 있으나 코드 import 0건 → 자연 제외, 추가 설정 불필요
예상 효과:
- 빌드 시간 21m 34s → 약 9분 (12분 단축, 57% 감소)
- 이미지 크기 약 1GB → 약 300MB (70% 감소)
- pull 시간 단축
- runtime memory footprint 감소
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>
PR #9 deploy 단계 실패의 후속 처리.
배경:
- 운영 cluster 의 backend-spring deployment 에 누군가 직접 sslmode=disable 박은 value override 가 있었음 (env[2] SPRING_DATASOURCE_URL value + valueFrom 동시 존재 → k8s reject)
- PR #9 머지 후 수동으로 cluster 의 ConfigMap 에 sslmode=disable patch + deployment 재생성 + 새 image rollout 완료
- 다만 git 의 k8s/configmap.yaml 은 sslmode 없는 상태 → 다음 머지 시 workflow 가 ConfigMap 을 git 값으로 덮어쓰면 sslmode 다시 빠짐 → backend pod 가 SSL 시도 후 DB 연결 실패 가능
조치:
- k8s/configmap.yaml line 9 의 SPRING_DATASOURCE_URL 에 ?sslmode=disable 영구 추가
- 운영 postgres 가 SSL 강제 안 하는 환경이라 disable 가 안전
- cluster 와 git 의 state 일치
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
main 의 최근 8연속 build 실패 (run 113~120) 원인이 docker build 단계의 OOM Killed.
진단:
- wace 호스트 32GB / available 24GB 충분, 시스템 OOM 기록 없음
- act_runner systemd MemoryMax=infinity, config container.options=null (제한 없음)
- 그러나 Next.js build V8 heap spike (5-8GB+) 가 다른 30개 동시 가동 서비스 (k3s/mailu/nextcloud/mattermost 등) 와 충돌
- Committed_AS 21.6 GB / Limit 24.8 GB 한계 — page touch 시 oom-killer 가 build process kill
- 결과: 28분 빌드 후 Killed → 8연속 main 배포 실패
조치:
- builder stage 에 NODE_OPTIONS=--max-old-space-size=4096 명시 → V8 heap 4GB 로 cap
- build process 가 알아서 절제 → OOM killer 트리거 안 됨
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>
- input-type-mapping.ts: BaseInputType 10개 → UserSelectableInputType 8개 (박창현 image 2). vexplor_rps INPUT_TYPE_DETAIL_TYPES 포팅, select/checkbox/radio variant 를 code base 로 흡수
- input-types.ts: USER_SELECTABLE_INPUT_TYPE_ORDER/LABELS re-export (InputType 12개는 그대로)
- getDetailType.ts (신규): getWidgetVariants / getDefaultWidgetVariant helper
- 드롭다운 호출처 7개 8개 제한: ColumnDetailPanel, AddColumnModal, ColumnDefinitionTable, tableMngList/page.tsx, TableSettingModal, TypeOverviewStrip, types.ts
- ColumnDetailPanel: Legacy row 드롭다운 disabled + v5-glow-sm Alert 배너
- backward shim: BaseInputType / BASE_INPUT_TYPE_OPTIONS / getBaseInputType 등 V2/Properties/DetailSettingsPanel 호환
운영 DB 96.6% 가 이미 8개 안 (V0, 35,316 row). DB zero touch, mapper 5곳 보호.
spec: .omc/specs/deep-dive-table-type-storage-ui-separation.md (v3.2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전: 페이지 진입 시 항상 펼쳐져 큰 공간 차지 → 매핑 영역이 화면 밖으로 밀림
신규: <details> + summary 로 기본 접힘. 클릭 시만 펼침. 토글 아이콘 함께.
특정 사용자/조건으로 API 조회할 때만 쓰는 옵션이라 기본 접힘이 자연스러움.
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>
사용자 보고: 기본 정보 카드가 별도 영역을 차지해서 한 화면에 매핑 영역(중요) 까지
스크롤 없이 안 들어옴. vexplor_rps 스크린샷처럼 상단을 한 줄로 컴팩트하게.
변경:
- 배치 타입 토글 grid + 기본 정보 카드를 단일 grid 로 통합
- xl(1280px+): grid-cols-[minmax(28rem,1.4fr)_1fr_1fr_1.5fr]
→ 모드토글그룹 + 배치명 + 스케줄 + 설명 한 줄
- xl 미만: grid-cols-1 stack
- 모드 토글 카드 컴팩트화: p-3 / h-9 아이콘 / text-[10px] description
- 설명: Textarea → Input (단일 행) 로 축소. 긴 설명은 어차피 메모 용도라 한 줄이면 충분.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0bba1836 에서 batchmngList/page.tsx 와 edit/[id]/page.tsx 두 곳만 max-w 제거했고
batch-management-new 의 root mx-auto max-w-5xl 가 남아있어 가운데 1024px 컬럼으로
본문이 박혀있었음. 좌우 패널이 sparse 하게 보이는 원인.
mx-auto max-w-5xl → w-full
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전: API 서버 URL / 인증 토큰 / 엔드포인트 / 메서드 / 데이터 배열 경로가 각각 단독 행
신규: 의미 단위로 그룹화 + 컴팩트화
- API 서버 URL (3fr) + HTTP 메서드 (1fr) 한 행
- 엔드포인트 + 데이터 배열 경로 50:50 한 행
- 인증 토큰: 라디오 → 세그먼티드 토글 버튼 그룹 + 입력을 한 행에 압축
- Label text-xs / Input h-9 text-sm 로 컴팩트 통일
vexplor_rps batch-management-new (L1417 부근) 의 컴팩트 레이아웃 패턴 참고.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vexplor_rps batch-management-new 의 applyRegisteredRestApi 핵심 흐름을 INVYONE 에
이식. 등록된 REST API 연결 선택 시:
1. 폼(URL/엔드포인트/메서드/Body/인증 토큰) 자동 채움
2. ExternalRestApiConnectionAPI.testConnectionById 로 자동 API 호출
3. 응답 안에서 배열 자동 탐색 (depth ≤ 4)
4. fromApiFields / fromApiData 채움 → 매핑 드롭다운에 필드 즉시 노출
UI: FROM 카드 최상단에 "🔗 등록된 연결" 셀렉터 추가.
로딩 중에는 셀렉터 라벨 옆에 스피너, 에러 시 destructive 톤 메시지.
vexplor_rps 와 다르게 제외한 부분:
- Amaranth/Wehago 회사 전용 프리셋 (AMARANTH_TARGET_PRESETS) — 6종 ERP 동기화
배치 전용 하드코딩이라 INVYONE 일반 사용자에겐 의미 없음. Phase 6 후속에서
검토할 별도 영역.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vexplor_rps 와 비교 시 batch-management-new 에 조건 변환 모드가 빠져있어
batchmngList/edit/[id] 와 같은 패턴으로 정렬.
- MappingItem.sourceType 에 'conditional' 추가 + conditionalConfig 필드
- ConditionalConfig / ConditionalEditor / emptyConditionalConfig import
- Select 옵션에 "조건 변환" 추가
- conditional 일 때 ConditionalEditor 렌더
- validMappings 필터: conditional 은 평가 필드(apiField) + 룰 또는 default 필요
- apiMappings 변환: mapping_type='conditional' + mapping_config 객체 전송
note: 이 페이지는 신규 생성 전용이라 load(편집 복원) 로직은 없음.
편집은 batchmngList/edit/[id]/page.tsx 가 처리.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
문제 1: 배치 수정/생성 탭이 빈 화면
- Tab type 정의는 admin_url (snake_case) 인데 호출자가 adminUrl (camelCase)
로 전달 → TabPageRenderer 매칭 실패 → null 렌더 → 빈 화면
- 콘솔 경고: "[TabPageRenderer] 렌더링 불가 — 매칭 조건 없음" (TabContent.tsx:268)
- 11곳 (4파일) 의 adminUrl → admin_url 정정
문제 2: 리스트/편집 본문이 좁은 가운데 컬럼에 박혀있음
- batchmngList/page.tsx: max-w-[720px] → w-full
- batchmngList/edit/[id]/page.tsx: max-w-[640px] → w-full
해당 파일:
- batchmngList/page.tsx
- batchmngList/edit/[id]/page.tsx
- batchmngList/create/page.tsx
- batch-management-new/page.tsx
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>
- batch.ts: ConditionalRule / ConditionalConfig 타입 추가,
BatchMapping 에 mapping_type ('direct'|'fixed'|'conditional') + mapping_config 필드
- ConditionalEditor.tsx: 평가 필드 선택 + when/then 룰 add/remove + default 입력 컴포넌트.
emptyConditionalConfig / normalizeConditionalConfig 헬퍼 동봉. vexplor_rps 1:1 포팅
- batchmngList/edit/[id]/page.tsx:
· MappingItem.sourceType 에 'conditional' 추가 + conditionalConfig 필드
· 소스타입 Select 에 "조건 변환" 옵션
· Load: mapping_type=conditional 인 매핑은 mapping_config JSON 파싱 후 복원
· Save: sourceType=conditional 매핑은 mapping_config 객체와 함께 전송
저장된 룰: {"rules":[{"when":"1","then":"Y"}],"default":"?"} 형태.
Phase 1 의 BatchService 직렬화 경로로 JSONB 에 저장된다.
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>
기존: scrollContainer 모드의 max-h-[calc(100vh-280px)] 가 viewport 기준 하드코딩이라 페이지 헤더+툴바 합산이 280px 보다 클 때 카드 영역이 viewport 끝까지 차지 → 페이지네이션이 부모 overflow-hidden 에 잘림.
신: outer wrapper 에 flex flex-col min-h-0 flex-1, 내부 테이블/카드에 min-h-0 flex-1 overflow-y-auto. 부모 flex-col 의 남는 공간만 차지 → 페이지네이션 등 형제는 자기 자연 height 유지.
로딩 스켈레톤 동일 패턴 적용.
기존: viewport 기준 (lg:hidden, sm:grid-cols-2) — 사이드바 펼친 상태에서 콘텐츠 영역의 실제 width 와 무관하게 동작 → 좁은 영역에 2열 카드가 들어가 카드가 잘려보이는 문제
신: @container 기반 — 컴포넌트가 자기 부모 컨테이너 width 에 반응
- 컨테이너 < 32rem (512px): 카드 1열
- 32~48rem (512~768px): 카드 2열
- ≥ 48rem (768px): 데스크톱 테이블
Tailwind v4 의 first-class container query 활용 (별도 플러그인 불필요). 데스크톱 테이블의 viewport 기준 max-height 스크롤은 유지.
근거: 2026 베스트프랙티스 — page layout=media query, 컴포넌트=container query (LogRocket / NN-Group / Tailwind v4 가이드).
- ResponsiveDataView: 모바일 카드 뷰 (lg 미만) + 카드 스켈레톤에 scrollContainer 모드의 max-h + overflow-y-auto 적용. 데스크톱 테이블만 적용돼 있던 누락 픽스
- TableHistoryModal: timeline/detail 탭의 ScrollArea 고정 h-[500px] → 모바일 h-[300px] sm:h-[500px] 적응형. DialogContent max-h-[90vh] 와 충돌 방지
증상: 브라우저 width 좁아질 때 카드 그리드/이력 타임라인이 viewport 너머로 잘리고 스크롤 안 됨.
- 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! 로 덮어쓰기.
SubstituteSection 의 loadCandidates 가 res.data.list 를 가정했지만
getUserList(/api/admin/users) 응답은 { data: [...] } 형태 (data 가 list 자체).
결과로 모든 select 가 '지정 가능한 사용자가 없습니다' 로 표시됐음.
Array.isArray(res.data) 와 res.data.list 둘 다 fallback 으로 처리.