충돌 해결 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 가 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>
- 백엔드 TenantController: GET /api/tenant/check?subdomain=xxx
(메타 DB 강제 라우팅 + CompanyResolver 로 존재 여부 반환)
- frontend/lib/tenant/subdomain.ts: 호스트 파싱 + 예약어(solution/www/admin 등) 제외
- TenantGuard 클라이언트 컴포넌트: layout.tsx 에서 wrap,
sessionStorage 로 같은 서브도메인 재체크 방지
- /tenant-not-found 페이지: v5 solid+glow 스타일
등록되지 않은 서브도메인 접속 시 즉시 /tenant-not-found 로 리다이렉트.
- application.yml, k8s configmap, docker-compose 4종: SPRING_DATASOURCE_URL path
- provisioning 코드: {prefix}_vexplor → {prefix}_invyone 테넌트 DB 네이밍 규칙
- 프론트 마법사: Step1Basic, Step4Run 미리보기 라벨
- CompanyAccordionRow: 기본 DB 이름 포맷
- 마이그레이션/멀티테넌시 문서 동기화
- Traefik 와일드카드 설정 산출물 보관 (notes/)
비밀번호(vexplor0909!!) 및 역사 기록 문서(INVYONE_CONCEPT,
DDD1542, test-output, dashboard-runtime-fixes) 는 의도적으로 미변경.