운영 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>
invyone 의 정상 마이그레이션 패턴(StartupSchemaMigrator)에 RUN_086 의 7개 idempotent
DDL/DML 등록. 다음 backend 부팅 시 메타 DB + 모든 활성 테넌트 DB 에 자동 적용됨.
- btree_gist 확장 (CREATE EXTENSION IF NOT EXISTS)
- USER_SUBSTITUTES 테이블 (PK, CHECK 2, EXCLUDE 제약, 인덱스 2)
- SYSTEM_AUDIT_LOG ALTER (PROCESSOR_ID/PROCESSOR_NAME ADD COLUMN IF NOT EXISTS)
- APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES idempotent 데이터 복사
호환성 보정:
- EXCLUDE 의 daterange lower 에 COALESCE(.., CURRENT_DATE) 사용 불가 (IMMUTABLE 만 허용)
→ daterange(START_DATE, END_DATE, '[]') 단순 형식. NULL lower = -infinity 자연 처리.
- APPROVAL_PROXY_SETTINGS 의 START_DATE/END_DATE 가 varchar → DATE cast 추가
- 원본 메타데이터(created/updated) 컬럼명 환경별 차이 회피 → 'migration_086' + NOW() 고정
실측: meta+3 tenants 모두 OK (test01/test02/siflex)
- TenantConsistencyGuard 뒤, ForcePasswordChangeGuard 앞에 등록
- /api/** 요청에 effective_user_ids = [user_id, ...active_original_ids] attribute 세팅
- SUPER_ADMIN (company_code='*') 은 short-circuit
- DB 조회 실패 시 본 요청 차단 안 함 (가용성 우선, warn 로그)
CRITICAL:
- searchUsers 회사/role 격리 가드 추가 (멀티테넌시 침해 차단)
- setPrimaryDept 멤버십 검증 추가 (주부서 데이터 손상 방지)
- parent_dept_code cross-tenant 검증 (validateParent 헬퍼)
HIGH:
- updateDepartment SQL WHERE 에 DELETED_AT IS NULL 추가 (silent corruption 방지)
- update/restore 부서명 중복 검증 추가
- 글로벌 부서 (*) write 작업 SUPER_ADMIN 전용 가드
- 부서코드 자동 생성으로 강제 (사용자 입력 받지 않음)
- 회사 변경 시 상세 패널 초기화
- handleMove 부분 실패 시 화면 동기화
- 검색 시 부모 체인 자동 포함 (broken tree 수정)
- start_date 기본값 today 강제 제거
MEDIUM:
- 멤버 fetch cancellation flag
- 삭제 다이얼로그 dept_code 클로저 캡처
- isDirty 시 X 버튼 폼 초기화 경고
- 변경이력 버튼 disabled (백엔드 API 미구현)
- 일괄등록 실패 상세 모달 (라인 + 사유)
- LIKE 와일드카드 ESCAPE 적용
- nullIfBlank 에 trim 통합
LOW + 새 기능:
- 부서원 추가/제거 UI 신규 구현 (UserSearchModal)
- selectDeptMembers LEFT JOIN 으로 변경
- DepartmentPicker allowRoot 옵션 (최상위로 이동)
- expandAll 전체 departments 사용
- dead code 정리
DB:
- RUN_085 마이그레이션: DEPT_INFO partial UNIQUE + USER_DEPT UNIQUE
- 모든 active 테넌트 DB (siflex/test01/test02_invyone) 적용 완료
Breaking changes:
- 일괄등록 CSV 4컬럼 → 3컬럼 (부서명,상위부서,유형)
- 부서코드 입력란 제거 (자동 부여 DEPT_n)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
충돌 해결 5개 파일:
- .gitignore: .envrc/.direnv (hjjeong direnv 셋업) + .omc/ (gbpark) 양쪽 보존
- docs/MULTI_TENANCY_ARCHITECTURE.md: *.localhost dev 분기 + *.invyone.com/solution.invyone.com 통합
- frontend/lib/api/client.ts: 1-b *.localhost:8081 dev + 1-c DEV_TENANT_HOST(nip.io):8083 + invyone.com 신 도메인
- frontend/lib/tenant/subdomain.ts: IPv4 차단 + *.invyone.com + DEV_TENANT_HOST + *.localhost 모두 처리
- frontend/app/(auth)/login/page.tsx: B안 채택 — buttons 항상 렌더, className 만 mounted 가드 (next-themes 표준 패턴)
검증:
- backend: ./gradlew compileJava 성공 (Java 21)
- frontend: 머지된 4개 파일 관련 타입 에러 0개
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flyway V017 은 메타 DB 만 갱신하지만 MENU_INFO 의 슈퍼관리자 메뉴
(COMPANY_CODE='*') 는 회사 프로비저닝 때 각 테넌트 DB 로 복사되어
박혀있다. StartupSchemaMigrator 에 동일 UPDATE 를 넣어 부팅 시 메타 +
모든 활성 테넌트에 자동 동기화. SEQ 만 갱신하므로 멱등.
Flyway 가 V001 두 개 (기존 V001__create_ai_llm_providers + 새로 들어온 V001__varchar_migration) 를 충돌로 거부 → backend pod CrashLoopBackOff.
운영 schema 와 호환 검토 전까지 db/migration/ 밖으로 빼서 archive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- frontend/components/layout/MenuItemActions.tsx — AppLayout/TopNavBar 가 import 하는데 빠져서 webpack 빌드 fail
- backend-spring db migration V001 (varchar_migration) + V002 (create_missing_tables) 같이 누락분 정리
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 작업자 폰(/mobile)을 SCADA 데모와 ws 로 연결, 알람 발생 시 풀스크린 푸시
· v5 솔리드+글로우 톤, 진동/Web Audio 비프/Wake Lock/auto reconnect
· 시연 안전망: ?test=1 자동 발동, 우상단 hidden 트리거
- backend: com.erp.alarm 신규 패키지 (WebSocketConfig + Handshake + Handler + Controller)
· JWT 토큰 핸드셰이크 검증, userId 기반 채널 매핑 (멀티 디바이스 지원)
· spring-boot-starter-websocket 의존성 추가
· path 를 /api/demo/* 안에 두어 Traefik 라우트 추가 불필요 + 정식 알람과 분리
- SCADA scenario.js 의 emergency 시퀀스(2700ms)에 fetch('/api/demo/alarm/trigger') 배선
· /scada?worker=<user_id> query 로 target user 지정 (iframe src 로 전달)
- 운영 시연 URL: siflex.invyone.com/mobile (siflex_user) ↔ /scada?worker=siflex_user
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
백엔드:
- 6개 AI Service (group/apiKey/provider/conversation/agent/scheduler) 가 응답 메서드에서
`parseJsonField` 헬퍼로 JSONB(::text) 컬럼 (connectors / config / permissions /
metadata / tool_calls / notification / tools) 을 String → Object 자동 변환.
- 모범 패턴 (`AuditLogService.processChanges`, `BusinessRuleService.parseJsonField`,
`DataflowDiagramService.parseJsonbFields`) 동일하게 적용.
- model 의 String getter 는 그대로 유지 — `MultiAgentExecutionEngine` 등
내부 LLM 호출 chain 영향 없음 (`getEntityById` 분리).
- 컨트롤러 시그니처 generic 만 변경 (return type Map).
프론트엔드:
- `safeArray<T>` / `safeObject<T>` 헬퍼 (`lib/utils.ts`) — 백엔드가 미파싱 String 으로
올 때 graceful fallback. 빈 배열/객체 반환.
- `workspace/page.tsx` 멤버 카드:
- `safeArray(member.connectors)` 적용 → `.map()` 폭발 차단.
- 좁은 viewport 에서 한글 텍스트 한 글자씩 세로로 깨지던 문제 해결
(`flex-wrap` + `truncate` + `whitespace-nowrap` + `max-w` + `title`).
그렘린 1000마리 폭격 + architect 자문으로 발견. workspace `Application error`,
`memberConnectors.map is not a function` 모두 해결.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
템플릿 목록 카드의 정적 📋 아이콘을 실제 view 구조 기반의
미니 와이어프레임으로 교체. 사용자가 카드만 보고도 템플릿이
어떤 화면인지(테이블 위주 / 폼 위주 / 단순 버튼 등) 파악 가능.
- backend: getTemplateList SQL 에 VIEWS 컬럼 추가, list 응답 각
row 의 views jsonb 를 객체로 파싱
- frontend: TemplateThumbnail 컴포넌트 신설 — v2(BlockV2.xPct/yPct
/wPct/hPct) 정규화 좌표 우선, v1(order/row) 폴백, 컴포넌트
종류별 색상(table=primary, form=cyan, button=pink)
- TemplateLibraryModal 카드 아이콘 자리 교체
- dashboard.css 에 .dash-lib-card-thumb / -block 스타일 추가
(v5 토큰 준수 — solid + glow, blur 없음)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: SUPER_ADMIN cross-tenant 모드에서 권한 그룹 클릭 시 workspace
500 — NumberFormatException: For input string: "TEST02"
원인: role.xml getRoleNonMemberList 의 if 가
test="company_code != null and company_code != '' and company_code != '*'"
형태로 single-quote 안에 single-quote 가 박혀, OGNL 이 '*' 를 number 로
변환 시도하다 NumberFormatException. 단일 모드 (test01.localhost) 에서는
group.company_code 가 시드 잔여 '*' 라 if 가 false 로 fall-through 해
안 터졌고, cross-tenant 에서 명시적 "TEST02" 가 들어가니 평가 시도
→ 깨짐.
CLAUDE.md "OGNL test: 바깥 작은따옴표" 컨벤션대로 수정:
test='company_code != null and company_code != "" and company_code != "*"'
검증: TEST02 의 '관리자' 그룹 workspace 호출 → HTTP 200, members: 0,
nonMembers: 2 (test02_admin, test02), menus: 0 정상 반환.
22, 334번 라인의 다른 if 문은 != '*' 가 없어 OGNL 평가가 numeric
coercion 까지 안 가므로 무수정 (회귀 위험 0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: 에이전트 삭제 후에도 목록에 그대로 표시.
원인: softDelete 가 status='archived' 로만 변경하는데 list 쿼리가
status 필터 미지정 시 archived 도 모두 반환.
수정: status 명시 X 시 status != 'archived' 기본 적용. status 명시 시
그 값으로 정확 매칭.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: POST /api/ai-agents 가 500 으로 응답.
원인: 커스텀 @Bean ObjectMapper 가 Spring Boot 자동 모듈 등록을 가로
막아 java.time.OffsetDateTime (AiAgent.created_at/updated_at 등) 직렬화
실패. INSERT 자체는 성공했으나 응답 변환에서 깨짐.
- JacksonConfig: registerModule(new JavaTimeModule()) +
WRITE_DATES_AS_TIMESTAMPS=false (ISO-8601 문자열 출력)
- GlobalExceptionHandler: 직전 디버그 메시지 노출 원복
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/ai-agents 가 500 만 반환하고 backend pod logs 직접 접근이
불가하여 진단 불가. 임시로 응답 message 에 [DEBUG] prefix + 예외
클래스명/메시지/cause 를 포함시켜 ULTRAQA 사이클로 root cause 파악.
⚠️ TEMPORARY — 안정화 후 다음 commit 에서 즉시 원복.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V014 가 AI_ASSISTANT_ROOT(부모) + AI_MENU_ASSISTANT(자식) 둘 다 동일
URL(/admin/aiAssistant)로 등록해 사이드바에 같은 이름 메뉴가 두 번
표시되던 증상 fix. V014 자체는 Flyway 체크섬 보호 때문에 수정하지
않고 후행 마이그레이션으로 soft-delete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1(사용자관리) 패턴을 권한관리에 동일 적용. 권한 그룹 CRUD,
멤버 토글, 메뉴 권한 토글 모두 회사 컨텍스트 임시 전환 후 처리.
신규 백엔드
- crosstenant/CrossTenantRoleController.java
/api/admin/cross-tenant/roles/** — 8개 endpoint
· POST — 권한 그룹 생성 (body.company_code 필수)
· PUT /{id} — 권한 그룹 수정 (body.company_code 필수)
· DELETE /{id}?company_code= — 삭제
· GET /{id}/workspace?company_code= — 그룹 + 멤버 + 메뉴 통합 로드
· GET /menus/all?company_code= — 회사 메뉴 트리 (권한 설정용)
· POST /{id}/members/{userId}?company_code= — 멤버 1명 추가
· DELETE /{id}/members/{userId}?company_code= — 멤버 1명 제거
· PATCH /{id}/menu-permissions/{menuObjid} — 토글
CrossTenantExecutor 재사용. 기존 RoleController 무수정 (회귀 0).
중요: @RequestAttribute("user_id") 가 토큰 없을 때 missing 에러로 500
떨어지는 문제 — required=false 로 가드까지 안전하게 도달하도록.
프론트
- lib/api/role.ts — 7개 메서드(create/update/delete/getWorkspace/
getAllMenus/addSingleMember/removeSingleMember/toggleMenuPermission)에
isCrossTenantMode() 분기 + companyCode 인자 추가
- RoleFormModal — update 시 editingRole.company_code 같이 전달
- RoleDeleteModal — delete 시 role.company_code 같이 전달
- rolesList/page.tsx — loadWorkspace / addSingleMember / removeSingleMember /
toggleMenuPermission 호출 시 selectedRole.company_code 전달
검증 (curl, SUPER_ADMIN 토큰):
- 토큰 없음 → 403 super_admin_required
- POST 권한 그룹 (TEST02) → 201, /roles fan-out 에 by={TEST01:1, TEST02:1}
- DELETE → 200, fan-out by={TEST01:1} 로 복귀
미구현 (Phase 2 후속, 별도 작업):
- 일괄 멤버 추가/제거/diff (PUT/POST /members)
- 메뉴 권한 일괄 설정 (PUT /menu-permissions)
- 사용자별 권한 그룹 조회
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
직전 Phase 1 의 후속 폴리시.
신규 백엔드
- crosstenant/CrossTenantDeptController.java
GET /api/admin/cross-tenant/departments?company_code=TEST02
단일 모드 GET /admin/departments 와 응답 형태 동일. company_code query param
으로 명시된 회사 DB 컨텍스트로 임시 전환해서 부서 트리 반환.
버그 수정: 메타 DB DEPT_INFO 시드 (qnc/COMPANY_7 등 다른 회사 부서) 가
TEST02 선택 시에도 dropdown 에 섞여 보이던 문제 해결.
프론트
- lib/api/user.ts — getDepartmentList(companyCode) 가 isCrossTenantMode() 면
/admin/cross-tenant/departments?company_code= 호출.
cross-tenant 모드 + companyCode 미지정 → 빈 배열 반환 (회사 안 골랐는데
메타 부서 보여주는 것 방지).
UserFormModal
- 회사 dropdown 을 폼 가장 위로 이동 — 사용자 ID 중복확인·부서 선택이
모두 회사에 의존하므로 자연스러운 입력 순서
- SUPER_ADMIN 인데 회사 미선택 상태에선 사용자 ID input + 중복확인 버튼
disable + placeholder "회사 먼저 선택"
- checkUserIdDuplicate 가드: 회사 미선택이면 "회사를 먼저 선택해주세요"
(백엔드의 400 "company_code 가 비어있음" 보다 친절)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
지금까지 cross-tenant 는 READ 전용. admin 도메인에서 사용자 등록하면
JWT.company_code='*' 가 그대로 박혀 메타 DB 에 INSERT 되던 버그 해결.
이제 SUPER_ADMIN 이 폼의 "회사" 드롭다운에서 TEST01/TEST02 등 선택하면
그 회사 DB 에 정확히 INSERT.
신규 백엔드
- crosstenant/CrossTenantExecutor.java — 회사 컨텍스트 임시 전환 헬퍼
(company_code → db_name → ensureTenantPool → set → run → restore)
- crosstenant/CrossTenantUserController.java — /api/admin/cross-tenant/users
9개 endpoint (POST/PUT/DELETE/PATCH/with-dept/check-duplicate/단건/이력)
- mapper/provisioning.xml — resolveDbNameByCompanyCode (active 회사만)
기존 단일 회사 모드 (POST /admin/users 등) 무수정 — 회사 도메인
컨텍스트에서 회귀 0.
프론트
- lib/api/user.ts — createUser/updateUser/updateUserStatus/checkDuplicateUserId/
saveUserWithDept 가 isCrossTenantMode() 면 새 endpoint + body.company_code 로 분기
- UserFormModal — checkDuplicateId 호출 시 formData.company_code 같이 전달
- useUserManagement — status toggle 시 row 의 company_code 같이 전달
검증 (curl, SUPER_ADMIN 토큰):
- 토큰 없음 → 403 super_admin_required
- company_code 없음 → 400 "company_code 가 비어있음"
- 잘못된 company_code → 400 "등록되지 않았거나 비활성 회사"
- check-duplicate: TEST01.test02_admin → not_dup, TEST02.test02_admin → dup ✓
- POST 사용자 → TEST02 USER_INFO +1, TEST01·메타 격리 ✓
- /users fan-out: by={'*':8, 'TEST01':1, 'TEST02':2}, hjtest_ct_001 in TEST02만 ✓
- DELETE → status=inactive (soft) ✓
미구현 (Phase 1 후속):
- 부서 dropdown (cross-tenant department endpoint 별도 필요)
- 비밀번호 초기화 모달의 cross-tenant 분기 (UserPasswordResetModal)
- Phase 2 권한관리 (별도 커밋)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frontend 가 /api/ai-knowledge 호출하는데 backend 에 컨트롤러가 없어
"지식 파일 목록 로드 실패" 토스트가 나던 증상 fix.
서비스/매퍼/모델/DTO 는 이미 있고 컨트롤러만 누락이었음. 다른
ai 컨트롤러 (AiAgentController) 와 동일 패턴으로 GET/POST/PUT/DELETE
+ 멀티테넌시(company_code 필터, SUPER_ADMIN 예외) 적용.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존 코드는 addFilterBefore(AiApi, JwtAuthenticationFilter.class) 가
jwt 등록 전에 호출되어 'JwtAuthenticationFilter does not have a
registered order' 예외 발생. Spring Security 6 부터 anchor 는 _이미
등록된_ 필터여야 함.
수정: jwt 를 UsernamePasswordAuthenticationFilter 기준으로 가장 먼저
등록 → JwtAuthenticationFilter.class 가 known map 에 추가됨 →
이후 SubdomainResolver/AiApiKey/TenantConsistency/ForcePw 가 jwt 를
anchor 로 정상 참조.
실행 순서는 동일: SubdomainResolver → AiApiKey → Jwt → TenantConsistency
→ ForcePw → UsernamePasswordAuthenticationFilter
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 DB flyway_schema_history 가 baseline + V015 만 가진 상태에서
V001~V014 가 누락되어 있던 사고를 정리한 후, 향후 비슷한 누락이
있어도 자동 복구 가능하도록 out-of-order 허용.
전제: 운영 DB 의 V015 row 는 별도로 DELETE 처리하여 baseline 만
남겨둔 상태. 다음 부팅에서 V001~V015 가 차례로 적용된다.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SubdomainResolverFilter.extractSubdomain() 가 2파트 {sub}.localhost 호스트도
첫 파트로 파싱 (RFC 6761). 운영 3파트 경로 무변경. 단위 테스트 8건 추가.
frontend/lib/api/client.ts 에 *.localhost (bare 제외) 직접 호출 분기 1-b 추가.
8081 로 직결해 Host 헤더 보존. subdomain.ts 도 동일 규칙 적용.
application.yml CORS 디폴트에 http://*.localhost:[*] 패턴 추가.
docs/MULTI_TENANCY_ARCHITECTURE.md §4.2 (실행 모드 A/B) + §6 (1-b 분기) 갱신.
.gitignore 에 .envrc/.direnv 추가, .java-version=21 명시 (jenv 호환).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SUPER_ADMIN 토큰(company_code=*)이면 등록 회사들 DB 를 순회해 결과를
집계해 돌려주는 CrossTenantAggregator/Controller 추가. 사용자/권한그룹/
배치/다국어 키 4개 도메인의 list API 가 cross-tenant 모드 지원.
UserTable + ResponsiveDataView 에 compact/scrollContainer prop 추가.
페이지 헤더/툴바/페이지네이션은 고정, 테이블만 자체 스크롤.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 도메인이 실제로는 v1.invyone.com / solution.invyone.com / api.invyone.com 인데
코드/문서 곳곳에 v1.invion.com / api.invion.com 등 미존재 도메인이 박혀 있어 정리.
변경 파일 (21):
- frontend lib/api/client.ts, lib/utils/apiUrl.ts: hostname 체크 endsWith(\".invyone.com\") 일반화
- frontend lib/api/dashboard.ts, file.ts, flow.ts, FileViewerModal*2.tsx: 도메인 치환
- frontend invion-layout-v5.html: 시안 내 placeholder 도메인 정리
- backend-spring SecurityConfig.java: CORS 주석 예시 정리
- docker/deploy/docker-compose.yml, k8s/traefik-dynamic.yaml: traefik Host 라벨 정리
- scripts/prod/deploy.sh: 안내 메시지 정리
- .cursor/rules/api-client-usage.mdc, project-conventions.mdc: AI 가이드 정리
- docs/* 4개: 아키텍처/플로우 문서 도메인 정리
- notes/gbpark/* 3개: 과거 메모 정리
신규:
- docs/DOMAIN_MAPPING.md: 운영/개발/폐기 도메인 영구 기록.
AI 에이전트와 신규 개발자가 헷갈리지 않도록 단일 진실 출처.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
화면 폭:
- 4개 페이지(사용자/권한 그룹/권한/부서) max-w 제거 → 화면 전체 사용
- 헤더 텍스트 컴팩트화 (text-xl + 작은 설명문)
부서관리 dept_info 스키마 1:1 매핑:
- 누락 필드 6개 추가: master_sabun, master_user_id, location, location_name, data_type, sales_yn
- frontend types/department.ts: Department / DepartmentFormData 확장
- frontend deptMngList: 폼 추가 + handleSelectDepartment / payload 매핑
- backend mapper: INSERT/UPDATE에 6개 컬럼 추가
- backend DepartmentService: insertParams/updateParams에 6개 필드 추가
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>