도메인 정리: invion.com → invyone.com 전체 일괄 치환 + 매핑 문서화
Build & Deploy to K8s / build-and-deploy (push) Has been cancelled

운영 도메인이 실제로는 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>
This commit is contained in:
Johngreen
2026-04-28 07:39:04 +09:00
parent eed70014c2
commit 52386efb83
48 changed files with 3533 additions and 49 deletions
+5 -5
View File
@@ -11,7 +11,7 @@ description: API 요청 시 항상 전용 API 클라이언트를 사용하도록
## 이유 ## 이유
1. **환경별 URL 자동 처리**: 프로덕션(`v1.invion.com`)과 개발(`localhost`) 환경에서 올바른 백엔드 서버로 요청 1. **환경별 URL 자동 처리**: 프로덕션(`v1.invyone.com`)과 개발(`localhost`) 환경에서 올바른 백엔드 서버로 요청
2. **일관된 에러 처리**: 모든 API 호출에서 동일한 에러 핸들링 2. **일관된 에러 처리**: 모든 API 호출에서 동일한 에러 핸들링
3. **인증 토큰 자동 포함**: Authorization 헤더 자동 추가 3. **인증 토큰 자동 포함**: Authorization 헤더 자동 추가
4. **유지보수성**: API 변경 시 한 곳에서만 수정 4. **유지보수성**: API 변경 시 한 곳에서만 수정
@@ -116,9 +116,9 @@ const getApiBaseUrl = (): string => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const currentHost = window.location.hostname; const currentHost = window.location.hostname;
// 프로덕션: v1.invion.com → api.invion.com // 프로덕션: v1.invyone.com → api.invyone.com
if (currentHost === "v1.invion.com") { if (currentHost === "v1.invyone.com") {
return "https://api.invion.com/api"; return "https://api.invyone.com/api";
} }
// 로컬 개발 // 로컬 개발
@@ -155,7 +155,7 @@ API 클라이언트는 자동으로 환경을 감지합니다:
| 현재 호스트 | 백엔드 API URL | | 현재 호스트 | 백엔드 API URL |
| ---------------- | ----------------------------- | | ---------------- | ----------------------------- |
| `v1.invion.com` | `https://api.invion.com/api` | | `v1.invyone.com` | `https://api.invyone.com/api` |
| `localhost:9771` | `http://localhost:8080/api` | | `localhost:9771` | `http://localhost:8080/api` |
| `localhost:3000` | `http://localhost:8080/api` | | `localhost:3000` | `http://localhost:8080/api` |
+1 -1
View File
@@ -661,7 +661,7 @@ if (req.user && req.user.companyCode !== "*") {
| 환경 | 프론트엔드 | 백엔드 API | | 환경 | 프론트엔드 | 백엔드 API |
|------|-----------|-----------| |------|-----------|-----------|
| 프로덕션 | `v1.invion.com` | `https://api.invion.com/api` | | 프로덕션 | `v1.invyone.com` | `https://api.invyone.com/api` |
| 개발 (로컬) | `localhost:9771` 또는 `localhost:3000` | `http://localhost:8080/api` | | 개발 (로컬) | `localhost:9771` 또는 `localhost:3000` | `http://localhost:8080/api` |
- 프론트엔드 `apiClient`가 `window.location.hostname` 기반으로 자동 판별 - 프론트엔드 `apiClient`가 `window.location.hostname` 기반으로 자동 판별
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,604 @@
# AI 오케스트레이션 교체 PRD (invyone × vexplor_pipeline)
> 작성: planner (BMAD 1단계 / Analyst·PM)
> 일자: 2026-04-27
> 상태: Architect 검토 대기
> 다음 단계: architect (기술 아키텍처 결정) → executor → verifier
---
## 0. TL;DR
invyone 프로젝트의 기존 **AI 어시스턴트(Node Express, port 3100)** 를 전면 제거하고, vexplor_pipeline 프로젝트에서 운영 중인 **멀티 에이전트 오케스트레이션 시스템**(에이전트 CRUD, 그룹 조립, 커넥터, OpenClaw, 스케줄러, 사용량 추적)을 invyone에 이식한다. 백엔드는 **TypeScript/Node → Java 21/Spring Boot 3.3.5/MyBatis** 로 1:1 포팅, 프론트엔드는 vexplor의 7개 화면을 invyone Next.js로 그대로 옮기고, OpenClaw은 컨테이너로 함께 운영한다.
**총량**: 8 Epic / 39 Story 추정 / 백엔드 포팅 비중 가장 큼
---
## 1. 목표 (Goals & Non-Goals)
### 1.1 Goals (P0)
- (G1) invyone에 멀티 에이전트 운영(에이전트 정의 → 그룹 조립 → 실행 모드 = 순차/병렬/혼합) 기능 도입
- (G2) **API 키 기반 외부 호출**(OpenAI 호환 `/v1/chat/completions`) 엔드포인트 유지 — 외부 시스템이 invyone을 LLM 게이트웨이로 사용 가능
- (G3) 멀티테넌시 강제 — 모든 AI 리소스(에이전트/그룹/키/대화/사용량)에 `company_code` 필터 일관 적용 (`*` = 전체 공유)
- (G4) **기존 AI 어시스턴트(ai-assistant 컨테이너 + 프록시 + DB 테이블) 완전 제거** — 운영 데이터 보존 불필요(사용자 확인 완료)
- (G5) Spring Security + JJWT 기반 인증 통합 (관리 API는 JWT, 외부 API는 sk-pipe-* 키)
- (G6) 부서/권한 그룹 시스템(직전 커밋 `2c57dc8c`)에 새 AI 메뉴들 등록 가능
### 1.2 Goals (P1)
- (G7) 다중 LLM 프로바이더(Anthropic, OpenAI, Google, DeepSeek, Ollama) 동시 운영 + 우선순위 라우팅
- (G8) 에이전트별 커넥터(DB/REST/PLC/크롤러/파일) 매핑으로 운영 데이터에 기반한 분석 수행
- (G9) cron 기반 스케줄 실행 + 알림(시스템 공지/슬랙 웹훅/이메일)
- (G10) 사용량 대시보드(일별/월별 토큰·비용·요청 수)
### 1.3 Goals (P2)
- (G11) 대화 모니터링(에이전트 간 메시지 트레이스)
- (G12) 지식 파일 라이브러리(에이전트별 RAG 컨텍스트 주입)
- (G13) AI 분석 이력 기반 정확도 추적(`ai_analysis_logs`)
### 1.4 Non-Goals
- (NG1) **vexplor_pipeline 자체의 화면관리/테이블관리/AI 화면 자동생성**(`workflow_patterns`, `keyword_mapping` 등)은 이번 작업 범위 아님
- (NG2) OpenClaw 자체 코드 포팅/수정. invyone은 OpenClaw을 **블랙박스 외부 엔진**으로만 사용
- (NG3) Spring AI / Spring AI MCP 도입 결정 — 이번에는 직접 HTTP 호출(LlmClient 직역) 유지 (architect 단계에서 재검토)
- (NG4) 사용량 청구/결제 기능
- (NG5) 다중 회사 간 에이전트 권한 위임(P3 이상)
- (NG6) 기존 vexplor `_pipeline_backup` 폴더 처리
---
## 2. 이해관계자 / 사용자 시나리오
| 페르소나 | 권한 | 핵심 시나리오 |
|---|---|---|
| **슈퍼관리자(SUPER_ADMIN)** | 전사 | 글로벌 LLM 프로바이더 등록/키 관리, `*` 회사용 공유 에이전트 정의, 전체 사용량 모니터링 |
| **회사관리자(COMPANY_ADMIN)** | 자사 `company_code` | 자사 에이전트/그룹 CRUD, 자사 API 키 발급, 자사 사용량 조회 |
| **일반사용자** | 자사 + 메뉴 권한 | AI 채팅 사용, 자기 API 키 발급(역할별로), 대화 이력 조회 |
| **외부 시스템(B2B)** | API 키 | `Authorization: Bearer sk-pipe-...``/api/ai/v1/chat/completions` 또는 `/api/ai/v1/groups/:groupId` 호출 |
| **운영자(DevOps)** | 인프라 | docker-compose 기동, OpenClaw 컨테이너 헬스체크, Jenkins 배포 |
### 2.1 핵심 사용 시나리오 흐름
**S1. 회사관리자가 멀티 에이전트 그룹 생성**
1. `/admin/aiAssistant/agents` 에서 "재고 분석가"(Claude Sonnet 4.5) + "구매 추천가"(GPT-4o) 에이전트 2개 생성
2. `/admin/aiAssistant/workspace` 에서 그룹 `재고-구매 자동분석` 생성, 두 에이전트를 멤버로 추가, 각각 PostgreSQL 커넥터(외부 DB) 매핑, execution_mode = `mixed`
3. UI에서 "현재 재고 위험 품목 분석해줘" 입력 → 백엔드 MultiAgentExecutionEngine이 두 에이전트를 mixed 모드로 실행 → 최종 요약 반환 + 대화 이력 저장 + `ai_analysis_logs` 기록
**S2. 외부 시스템(B2B) 호출**
1. 관리자가 `/admin/aiAssistant/api-keys-manage` 에서 키 발급(키는 한 번만 표시, sha256 해시만 저장)
2. 외부 시스템: `POST /api/ai/v1/groups/123` with `Authorization: Bearer sk-pipe-...` and `{"message":"오늘 재고 위험"}`
3. AiApiKeyAuthFilter 통과 → 월간 토큰 한도 체크 → 그룹 실행 → 사용량 누적
**S3. cron 스케줄**
1. `/admin/aiAssistant/scheduler`(신규) 에서 `매일 09:00 - 그룹 X 실행 - 슬랙 웹훅 알림`
2. Spring `@Scheduled` 또는 Quartz가 등록된 cron 표현식으로 그룹 실행 → `ai_analysis_logs` + `system_notice` + 슬랙 발송
---
## 3. 기능 요구사항 (FR)
### 3.1 에이전트 관리 (P0)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-A-1 | 에이전트 CRUD(`agent_id`, `name`, `model`, `system_prompt`, `tools`, `config`, `company_code`) | P0 |
| FR-A-2 | 검색·상태(active/inactive/archived)·회사별 필터 | P0 |
| FR-A-3 | `agent_id` 유니크 제약 | P0 |
| FR-A-4 | 에이전트별 `config.knowledge_files`(라이브러리 참조 또는 인라인 콘텐츠) | P2 |
| FR-A-5 | tools 정의(JSON, MCP 도구 명세) | P1 |
### 3.2 멀티 에이전트 그룹 (P0)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-G-1 | 그룹 CRUD(`group_id`, `name`, `execution_mode` = parallel/sequential/mixed) | P0 |
| FR-G-2 | 그룹 멤버 추가·수정·삭제(`role_name`, `connectors`, `execution_order`) | P0 |
| FR-G-3 | 가용 커넥터 조회 — DB(`external_db_connections`) / REST(`external_rest_api_connections`) / PLC(`pipeline_device_connections`) / 크롤러(`crawl_configs`) | P1 |
| FR-G-4 | 그룹 실행 = MultiAgentExecutionEngine — sequential/parallel/mixed 분기 처리 + 이전 컨텍스트 누적 + 최종 요약 | P0 |
| FR-G-5 | 그룹 실행 시 과거 분석 이력 5건 자동 컨텍스트 주입 | P2 |
### 3.3 LLM 프로바이더 관리 (P0)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-P-1 | 프로바이더 CRUD(`name`, `display_name`, `model_name`, `endpoint`, `priority`, `cost_per_1k_input/output`) | P0 |
| FR-P-2 | API 키 AES-256 암호화 저장(`api_key_encrypted`) — 응답 시 `****<last 4>` 마스킹 | P0 |
| FR-P-3 | 다중 프로바이더 지원(Anthropic / OpenAI / Google / DeepSeek / Ollama) | P1 |
| FR-P-4 | 우선순위 기반 default 모델 자동 선정 | P1 |
### 3.4 외부 API 키 관리 (P0)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-K-1 | `sk-pipe-{64hex}` 키 발급, sha256 해시 저장, prefix(16자) 표시용 저장 | P0 |
| FR-K-2 | 키 목록(SUPER/COMPANY 관리자는 전체, 일반 사용자는 자기 키만) | P0 |
| FR-K-3 | 키 폐기(SOFT delete: status=revoked 또는 hard delete — vexplor는 hard delete) | P0 |
| FR-K-4 | rate_limit, monthly_token_limit, expires_at, permissions(JSON) | P0 |
| FR-K-5 | 키 검증 시 last_used_at, usage_count 자동 갱신 | P0 |
| FR-K-6 | 월간 토큰 한도 초과 시 HTTP 429 | P0 |
### 3.5 외부 호출 게이트웨이 (P0)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-X-1 | `POST /api/ai/v1/chat/completions` — OpenAI 호환(스트리밍 포함) | P0 |
| FR-X-2 | `POST /api/ai/v1/groups/:groupId` — 멀티 에이전트 그룹 실행 | P0 |
| FR-X-3 | `GET /api/ai/v1/groups` — 사용 가능 그룹 목록 | P1 |
| FR-X-4 | `GET /api/ai/v1/models` — 모델 목록(프로바이더 기반) | P1 |
| FR-X-5 | `GET /api/ai/v1/health` — 헬스체크 | P1 |
| FR-X-6 | API 키 + JWT 동시 인증 허용(내부/외부 모두 사용 가능) | P0 |
### 3.6 사용량 추적 (P1)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-U-1 | 모든 LLM 호출에 대해 `ai_agent_usage_logs` 적재(prompt/completion/total tokens, cost_usd, 응답시간, success, error) | P1 |
| FR-U-2 | 요약: 오늘/이번 달 토큰·요청·비용 + active 에이전트/키 수 | P1 |
| FR-U-3 | 일별 차트(최근 30일) | P1 |
| FR-U-4 | 페이징 로그 조회 | P1 |
### 3.7 대화 모니터링 (P2)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-C-1 | 그룹 실행 시 대화 자동 저장(conversation + 사용자 메시지 + 각 step 응답) | P2 |
| FR-C-2 | 대화 목록 페이징·에이전트별 필터 | P2 |
| FR-C-3 | 대화 상세(메시지 시퀀스) 조회 | P2 |
| FR-C-4 | 대화 삭제 | P2 |
### 3.8 스케줄 실행 (P1)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-S-1 | 스케줄 CRUD(`cron_expression`, `group_id`, `input_message`, `notification`) | P1 |
| FR-S-2 | cron 표현식 검증 + 재등록 | P1 |
| FR-S-3 | 알림 채널: 시스템 공지(`system_notice`) / 웹훅(슬랙) / 이메일(기존 mailSend 서비스 재사용) | P1 |
| FR-S-4 | 서버 기동 시 활성 스케줄 자동 등록 | P1 |
| FR-S-5 | last_run_at, run_count 누적 | P1 |
### 3.9 OpenClaw 통합 (P1)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-O-1 | OpenClaw 컨테이너로 기동(invyone docker-compose에 서비스 추가, 기본 port 18789) | P1 |
| FR-O-2 | OpenClaw config 동기화 — 프로바이더(authProfiles) + 에이전트(agents) → JSON 파일 | P1 |
| FR-O-3 | 프로바이더/에이전트 변경 시 트리거(저장 후 sync 호출) | P1 |
| FR-O-4 | OpenClaw 다운 시 graceful — 관리 기능은 동작 유지, 외부 호출만 503 | P1 |
### 3.10 지식 라이브러리 (P2)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-N-1 | 지식 파일 CRUD(`ai_knowledge_files`: name, file_name, category, content) | P2 |
| FR-N-2 | 에이전트 config.knowledge_files에서 `library_id` 또는 인라인 `content` 참조 | P2 |
| FR-N-3 | 검색·카테고리 필터 | P2 |
### 3.11 메뉴/권한 등록 (P0)
| FR | 설명 | 우선순위 |
|---|---|---|
| FR-M-1 | 새 AI 메뉴 7개를 부서/권한 그룹 시스템(직전 커밋 `2c57dc8c`)에 등록 | P0 |
| FR-M-2 | 슈퍼관리자 전용 메뉴(LLM 관리, LLM 사용량 통계 등) 분리 | P0 |
| FR-M-3 | 기존 INVION 메뉴 가이드(`AI_어시스턴트_메뉴_등록_가이드.md`) 갱신 | P1 |
---
## 4. 비기능 요구사항 (NFR)
### 4.1 멀티테넌시 (강제)
- 모든 AI 테이블에 `company_code VARCHAR(20)` 컬럼 — `NULL` 또는 `*` 은 슈퍼관리자 전용 / 글로벌 공유
- 모든 list/get/update/delete 쿼리에 `(company_code = :myCompany OR company_code IS NULL)` 적용
- SUPER_ADMIN 만 회사 간 자료 조회 가능 (`company_code=*` 쿼리 매개변수 허용)
### 4.2 보안
- API 키: 평문 저장 금지. SHA-256 해시 + 16자 prefix 만 저장. 발급 시점에만 평문 1회 노출
- LLM 프로바이더 API 키: AES-256 대칭 암호화(EncryptUtil 포팅 필요). 응답에서 마스킹
- JWT: 기존 invyone JJWT 0.12.3 그대로 사용
- API 키 인증 필터(`AiApiKeyAuthFilter`): Spring Security 필터 체인에 추가, JWT 필터보다 앞 또는 같은 layer
- Rate limit: 키당 분당 N회(기본 60), 월간 토큰 한도(기본 100만)
- CORS: 기존 invyone 정책 따름
### 4.3 성능
- 그룹 parallel 실행: `CompletableFuture.allOf` 기반(JDK 21 가상 스레드 권장)
- 그룹 sequential 실행: 직렬 await
- 그룹 mixed 실행: execution_order 동률 → 병렬, 다른 order → 순차 await
- LLM 호출 타임아웃: 60초 default, 스트리밍은 무제한 (커넥션 keep-alive)
- DB index: `ai_agent_usage_logs(created_at)`, `ai_agent_api_keys(key_hash)`, `ai_agents(agent_id)`, `ai_agent_groups(group_id)`
### 4.4 가용성
- OpenClaw 다운 시:
- 외부 LLM 호출은 invyone 자체 LlmClient(직접 프로바이더 HTTP 호출)로 fallback
- 관리 화면은 정상 동작
- DB 다운 시: 통상 invyone 정책(=실패) 따름
- 헬스체크: `/api/ai/v1/health` 가 프로바이더 수 응답
### 4.5 마이그레이션 안전성
- ai-assistant Sequelize 테이블(users, api_keys, llm_providers, usage_logs) **drop 가능** (사용자 확인됨, 운영 데이터 없음)
- 새 vexplor 스키마는 별도 마이그레이션 스크립트로 적용. 기존 invyone PostgreSQL과 같은 DB 사용
- 명명 충돌 위험: ai-assistant의 `users`, `api_keys` 등 일반 테이블명 — invyone 본 스키마와 동명 충돌 가능 → drop 시점에 보존 검증 필수
- 새 스키마는 모두 `ai_` prefix 사용 (vexplor 컨벤션) → 충돌 없음
### 4.6 관찰성
- 모든 핵심 호출에 SLF4J + Spring Boot logback 로깅(요청/응답/에러)
- ai_agent_usage_logs 가 사실상 1차 audit
- 에러는 invyone 표준 ApiResponse(success/error code/message) 포맷
### 4.7 호환성
- Java 21, Spring Boot 3.3.5, MyBatis 3.0.3, PostgreSQL 16, Lombok
- Frontend: Next.js (invyone 기존), shadcn/ui, dnd-kit (vexplor에서 사용)
- vexplor의 `Promise.all`, `node-cron`, `axios` → Java 등가물(`CompletableFuture`, Spring `@Scheduled`/Quartz, RestClient/WebClient)
---
## 5. 이식 작업 분해 (WBS)
### Epic A — 기존 AI 어시스턴트 제거 (P0)
**목표**: invyone 측의 ai-assistant 모듈을 흔적 없이 걷어낸다.
| Story | 작업 |
|---|---|
| **A1**: `ai-assistant/` 디렉토리 통째 삭제 | `C:\Dev\projects\invyone\ai-assistant\` 폴더 삭제. README, package.json, src/ 전부 |
| **A2**: Spring 프록시 컨트롤러 삭제 | `AiAssistantProxyController.java`, `AiAssistantProxyService.java` 삭제 + Spring 컴포넌트 스캔 영향 확인 |
| **A3**: docker-compose 정리 | `docker-compose.backend.win.yml`, `docker-compose.frontend.win.yml`(있다면), `Dockerfile`에서 ai-assistant 참조 항목 제거. 현재 파일에는 명시적 ai-assistant 서비스 없음(확인됨) — 환경변수 `AI_ASSISTANT_SERVICE_URL` 등 잔재만 정리 |
| **A4**: Jenkinsfile 정리 | 현재 Jenkinsfile은 단일 Build 단계만 있고 ai-assistant 별도 stage 없음(확인됨). 변경 불필요 — 단, helm-charts 측 `values_invyone.yaml` 의 ai-assistant 이미지 참조가 있다면 별도 PR 필요(미확인 사항으로 표기) |
| **A5**: 프론트엔드 메뉴/페이지 제거 | `frontend/app/(main)/admin/aiAssistant/` 8개 페이지 + `frontend/lib/api/aiAssistant/` (client.ts, index.ts) + `frontend/components/layout/AdminPageRenderer.tsx` 라인 128, 142~147 제거 후 vexplor 신규 페이지로 교체(Epic F) |
| **A6**: 메뉴 가이드 문서 제거/갱신 | `frontend/docs/AI_어시스턴트_메뉴_등록_가이드.md`, `docs/leeheejin/AI_어시스턴트_사용가이드.md` 폐기 또는 신규 가이드로 대체 |
| **A7**: 기존 ai-assistant DB 테이블 drop | `users`, `api_keys`, `llm_providers`, `usage_logs` (Sequelize 자동 생성). **주의**: invyone 본 시스템에 동명 테이블 없는지 검증 후 drop. 마이그레이션 스크립트로 작성 |
**Story 수: 7**
### Epic B — DB 스키마 마이그레이션 (P0)
**목표**: vexplor의 AI 스키마를 invyone PostgreSQL에 적용.
> ⚠️ 사용자 프롬프트에 명시된 `db/migrations/300_create_openclaw_tables.sql` 파일은 vexplor 리포지토리에 **물리적으로 존재하지 않음**(전체 검색 확인). 따라서 vexplor 서비스/타입 코드(`aiAgent*.ts` 9개 + `aiAgent.ts` 타입)에서 **스키마를 역공학**해야 한다 — 이 작업이 Epic B의 핵심이며 Story B1에서 수행한다.
| Story | 작업 |
|---|---|
| **B1**: vexplor 코드 역공학으로 스키마 재구성 | 9개 서비스 + types/aiAgent.ts 분석 → 13개 테이블 DDL 작성: `ai_agents`, `ai_agent_groups`, `ai_agent_group_members`, `ai_agent_api_keys`, `ai_llm_providers`, `ai_agent_conversations`, `ai_agent_messages`, `ai_agent_usage_logs`, `ai_agent_schedules`, `ai_analysis_logs`, `ai_knowledge_files`, (선택) `ai_agent_tools` (P3) |
| **B2**: 마이그레이션 스크립트 작성 | invyone DB 마이그레이션 컨벤션에 맞춰(MyBatis용 .sql 또는 별도 도구) `V20260427__ai_orchestration.sql` 형태. 멱등 보장(IF NOT EXISTS) |
| **B3**: 인덱스 / FK / 제약 정의 | 5.3 NFR 인덱스 + cascade 정책(예: 그룹 삭제 시 멤버 cascade) |
| **B4**: 시드 데이터 | 기본 LLM 프로바이더 row 1~2개(키 비활성 placeholder), 슈퍼관리자용 샘플 에이전트 1개(선택 P2) |
| **B5**: 마이그레이션 적용 절차 문서화 | dev/staging/prod 적용 가이드 + 롤백 스크립트 |
**Story 수: 5**
### Epic C — Spring 백엔드 포팅 (P0)
**목표**: TS 9개 서비스 + 컨트롤러 + 라우트 → Java 21 / Spring / MyBatis 1:1 매핑
| Story | 작업 |
|---|---|
| **C1**: 공통 인프라 | `EncryptUtil`(AES-256), `LlmClient`(Anthropic / OpenAI / Google / DeepSeek / Ollama HTTP 클라이언트, 스트리밍 포함), DTO 패키지(`com.erp.dto.ai.*`), MyBatis Mapper 인터페이스 + XML 13개 |
| **C2**: AiAgentService | 에이전트 CRUD + 검색·필터 + company_code 필터 |
| **C3**: AiAgentGroupService | 그룹 CRUD + 멤버 CRUD + 가용 커넥터 조회(invyone에 동등 테이블 존재 여부 확인 필요 — `external_db_connections`, `external_rest_api_connections`, `crawl_configs`, `pipeline_device_connections` 가 invyone에 있는지 architect가 결정) |
| **C4**: AiAgentApiKeyService | sk-pipe 키 발급/검증/폐기 + 토큰 누적 |
| **C5**: AiAgentProviderService | 프로바이더 CRUD + 암복호화 + 마스킹 |
| **C6**: AiAgentConversationService | 대화 + 메시지 저장/조회/삭제 |
| **C7**: AiAgentUsageService | 사용량 적재 + 요약 + 일별/페이징 |
| **C8**: AiAnalysisLogService | 분석 이력 CRUD + 평균 정확도 |
| **C9**: MultiAgentExecutionEngine | sequential/parallel/mixed 분기 + 컨텍스트 누적 + 커넥터 실행 + 지식 파일 주입 + 최종 요약 — `CompletableFuture` 기반 |
| **C10**: AiSchedulerService | Spring `@Scheduled` 또는 Quartz로 cron 등록/해제, 알림 발송(시스템 공지 / 슬랙 웹훅 / 이메일) |
| **C11**: OpenClawSyncService | 프로바이더/에이전트 → OpenClaw config(JSON) 동기화 |
| **C12**: 컨트롤러 — 관리(JWT) | `/api/ai-agents`, `/api/ai-agents/keys/*`, `/api/ai-agents/providers/*`, `/api/ai-agents/conversations/*`, `/api/ai-agents/usage/*` |
| **C13**: 컨트롤러 — 그룹(JWT) | `/api/ai-agent-groups`(+ /:id/members, /connectors) |
| **C14**: 컨트롤러 — 외부(API Key) | `/api/ai/v1/chat/completions`(스트리밍 포함), `/api/ai/v1/groups/:groupId`, `/api/ai/v1/groups`, `/api/ai/v1/models`, `/api/ai/v1/health` |
| **C15**: 컨트롤러 — 스케줄(JWT) | `/api/ai-agent-schedules` CRUD |
| **C16**: 컨트롤러 — 지식(JWT) | `/api/ai-knowledge` CRUD |
**Story 수: 16**
### Epic D — 인증 미들웨어 (P0)
| Story | 작업 |
|---|---|
| **D1**: AiApiKeyAuthFilter | Spring Security `OncePerRequestFilter` 로 작성 — `Authorization: Bearer sk-pipe-*` 만 처리, JWT 는 기존 필터로 위임 |
| **D2**: SecurityConfig 조정 | `/api/ai/v1/**` 경로는 AiApiKeyAuthFilter 우선 적용. 관리 경로(`/api/ai-agents/**` 등)는 JWT 유지 |
| **D3**: 동시 인증 허용 | 외부 게이트웨이는 sk-pipe 키 + JWT 둘 다 허용 (vexplor 동일 동작) |
**Story 수: 3**
### Epic E — OpenClaw 컨테이너 통합 (P1)
| Story | 작업 |
|---|---|
| **E1**: OpenClaw 배포 방식 결정 | Dockerfile 또는 외부 이미지 사용 여부 조사. Helm chart 추가 또는 docker-compose 신규 서비스 |
| **E2**: docker-compose에 openclaw 서비스 추가 | `docker-compose.backend.win.yml` 에 openclaw 서비스(포트 18789), 환경변수, 볼륨(config 공유) |
| **E3**: invyone Spring → OpenClaw URL 환경변수 | `OPENCLAW_BASE_URL`, `OPENCLAW_ENABLED` 추가 |
| **E4**: Config 공유 볼륨 | `OpenClawSyncService` 가 쓰는 JSON 파일 경로를 컨테이너 간 공유(또는 HTTP 동기화 API로 변경 architect 결정) |
| **E5**: K8s 매니페스트 | `k8s/` 폴더에 OpenClaw 매니페스트 추가(필요 시) |
**Story 수: 5**
### Epic F — 프론트엔드 통합 (P0)
| Story | 작업 |
|---|---|
| **F1**: vexplor 7개 페이지 이식 | `agents/`, `providers/`, `conversations/`, `workspace/`, `api-keys-manage/`, `knowledge/`, `layout.tsx`, `page.tsx` → invyone `frontend/app/(main)/admin/aiAssistant/` 로 복사. 기존 invyone aiAssistant 폴더 통째 교체 |
| **F2**: API 클라이언트 이식 | vexplor `frontend/lib/api/aiAgent.ts` → invyone `frontend/lib/api/aiAgent.ts` 신규. invyone 기존 `lib/api/aiAssistant/` 폴더 삭제 |
| **F3**: BASE URL 정리 | invyone apiClient(`/api`)와 일치하는지 검증, vexplor의 `/ai-agents`, `/ai-agent-groups`, `/ai-knowledge` 경로 그대로 사용 |
| **F4**: AdminPageRenderer 라우팅 갱신 | 기존 `aiAssistant/dashboard|history|api-keys|api-test|usage|chat` 7개 라인 제거, 신규 `agents|providers|conversations|workspace|api-keys-manage|knowledge` 6~7개 등록 |
| **F5**: shadcn 의존성 점검 | dnd-kit(`@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`), sonner(toast) — invyone에 없으면 추가 |
| **F6**: 다국어/스타일 점검 | vexplor 페이지의 한국어 메시지, dark mode 클래스가 invyone 디자인 시스템과 호환되는지 확인 |
| **F7**: 스케줄러 페이지 신규 작성 (vexplor에 미포함) | `/admin/aiAssistant/scheduler` — 백엔드 `AiSchedulerService` 와 짝 (아키텍트 결정 후 Story 추가 가능) |
**Story 수: 6~7**
### Epic G — 메뉴/권한 등록 (P0)
| Story | 작업 |
|---|---|
| **G1**: 신규 AI 메뉴 7개를 메뉴 마스터에 등록 | `2c57dc8c 부서 권한 그룹 권한 관리` 커밋의 메뉴 시스템에 다음 URL 등록: `/admin/aiAssistant`, `/admin/aiAssistant/agents`, `/admin/aiAssistant/workspace`, `/admin/aiAssistant/providers`(SUPER), `/admin/aiAssistant/api-keys-manage`, `/admin/aiAssistant/conversations`, `/admin/aiAssistant/knowledge` |
| **G2**: 권한 그룹 매핑 | SUPER_ADMIN/COMPANY_ADMIN/일반사용자 별 메뉴 노출 범위 정의 |
| **G3**: 사이드바 아이콘 | Bot 아이콘(기존 가이드 따라) — `frontend/components/layout/Sidebar` 등에서 키워드 매핑 |
| **G4**: 가이드 문서 갱신 | `frontend/docs/AI_어시스턴트_메뉴_등록_가이드.md` 신규 메뉴로 교체 |
**Story 수: 4**
### Epic H — 검증/배포 (P0)
| Story | 작업 |
|---|---|
| **H1**: 단위 테스트 — Service 계층 | JUnit 5 + MyBatis 테스트(testcontainers PostgreSQL). 각 서비스 핵심 시나리오 |
| **H2**: 통합 테스트 — 외부 게이트웨이 | sk-pipe 키 발급 → `/api/ai/v1/chat/completions` 호출 → 사용량 적재 검증(MockServer로 LLM 응답 stub) |
| **H3**: E2E — 그룹 실행 | mixed 모드, 2 멤버 + 커넥터 1개, 응답 + 대화 저장 + 분석 이력 검증 |
| **H4**: 컨테이너 기동 검증 | docker-compose up → backend + openclaw + frontend 헬스체크 통과 |
| **H5**: 마이그레이션 dry-run | dev DB에 마이그레이션 적용 → 전체 화면 + API 동작 확인 |
| **H6**: 성능 smoke | 동시 그룹 실행 10개 + parallel 멤버 5개 → 토큰 누적·DB write 정합성 |
| **H7**: 보안 점검 | API 키 평문 비노출, 프로바이더 키 마스킹, JWT 누락 시 401 |
| **H8**: 운영 가이드 | 배포 / 롤백 / 트러블슈팅 / OpenClaw 관리 문서 |
**Story 수: 8**
---
### 5.x WBS 요약
| Epic | Story 수 | 주요 위험 |
|---|---|---|
| A. 기존 제거 | 7 | helm-charts 측 잔재, 메뉴 깨짐 |
| B. DB 스키마 | 5 | SQL 미존재 → 역공학 정확도 |
| C. Spring 포팅 | 16 | LlmClient 스트리밍, ExecutionEngine 동시성 |
| D. 인증 | 3 | 필터 체인 충돌 |
| E. OpenClaw | 5 | 외부 엔진 라이선스/배포 가능성 |
| F. 프론트엔드 | 6~7 | 디자인 시스템 호환성 |
| G. 메뉴/권한 | 4 | 권한 매핑 누락 |
| H. 검증 | 8 | LLM mock 정확도 |
| **합계** | **54~55** | — |
> 참고: 사용자 프롬프트에서 "39 Story 추정"으로 시작했으나, 실제 분석 결과 54~55개가 더 정확함. 일부 Story는 architect 단계에서 묶거나 분할될 수 있다.
---
## 6. Story 상세 (대표 5개 발췌)
### 6.1 Story C9 — MultiAgentExecutionEngine 포팅
- **입력**: `vexplor_pipeline/backend-node/src/services/multiAgentExecutionEngine.ts` (478라인)
- **변경 작업**:
- `executeSequential` → 직렬 await 루프
- `executeParallel``CompletableFuture.allOf` (Java 21 가상 스레드 권장)
- `executeMixed` → execution_order 그룹핑 후 직렬+병렬 혼합
- `executeSingleAgent` → 에이전트 조회 + 커넥터 실행 + 지식 파일 주입 + LlmClient 호출
- `executeConnector` → DB / REST / PLC / 크롤러 / 파일 — invyone에 해당 테이블 존재 여부 확인 후 적용 (architect가 mapping 결정)
- `buildFinalSummary` → 동일
- 사이드 이펙트: `AiAgentConversationService.createConversation/addMessage`, `AiAnalysisLogService.save`, `AiAgentUsageService.log`
- **DoD**:
- 동일 입력에 대해 vexplor와 응답 메시지 구조 동일
- parallel 모드에서 5개 에이전트 동시 실행 시 응답시간 ≤ max(개별 응답시간) × 1.2
- 사용량 / 대화 / 분석 이력 모두 적재
- 에이전트 실행 실패 1건은 그룹 전체 실패로 이어지지 않음(개별 step에 [실행 실패] 응답 기록)
- **의존성**: C1(LlmClient, EncryptUtil), C2~C7
- **위험**: Java/Node 동시성 모델 차이로 race condition 발생 가능. JDK 21 가상 스레드 사용 시 MyBatis SqlSession 스레드 안전성 검증 필요
### 6.2 Story B1 — 스키마 역공학
- **입력**: vexplor 9개 서비스 + types/aiAgent.ts
- **변경 작업**: 13개 테이블 DDL 작성. 각 테이블 컬럼 추론 근거(서비스 SQL 쿼리 라인) 주석 첨부
- **DoD**: vexplor 백엔드를 invyone DB 스키마로 띄웠을 때(가상 시나리오) 모든 INSERT/UPDATE/SELECT 가 동작
- **의존성**: 없음 (Epic B 시작점)
- **위험**: 일부 컬럼이 서비스에서 SELECT/INSERT 모두 사용되지 않으면 역공학 누락. architect 검토 단계에서 vexplor DB dump 또는 별도 schema 문서 확보 권장
### 6.3 Story D1 — AiApiKeyAuthFilter
- **입력**: `vexplor/backend-node/src/middleware/aiApiKeyAuthMiddleware.ts`
- **변경 작업**:
- `OncePerRequestFilter` 상속, `/api/ai/v1/**` 경로 매칭
- sk-pipe-* 토큰 → AiAgentApiKeyService.validateKey
- 월간 토큰 한도 초과 → 429
- JWT 토큰은 기존 JwtAuthenticationFilter 로 위임 (이중 필터)
- **DoD**: 키로 호출 시 `req.apiKey` 정보가 컨트롤러에 전달, JWT로 호출 시 `Authentication` 객체 정상
- **의존성**: C4(AiAgentApiKeyService)
- **위험**: 필터 순서. Spring Security 6 의 `addFilterBefore` 위치 결정
### 6.4 Story A7 — 기존 ai-assistant DB 테이블 drop
- **입력**: ai-assistant Sequelize 모델 4개(`users`, `api_keys`, `llm_providers`, `usage_logs`)
- **변경 작업**:
- 사전 검증: invyone 본 시스템에 `users`, `api_keys` 동명 테이블이 있는지 (있을 가능성 높음 — `users`는 ERP 핵심 테이블일 수 있음)
- **위험 해소**: ai-assistant가 사용한 DB 스키마(예: `ai_assistant` 스키마) 존재 여부 확인 필요. 만약 동일 schema 라면 컬럼 충돌 / 데이터 덮어쓰기 위험
- drop 스크립트(별도 schema에 있다면 DROP SCHEMA IF EXISTS ai_assistant CASCADE)
- **DoD**: 메인 invyone 기능에 영향 없이 잔여 테이블/스키마 제거
- **의존성**: 없음 (Epic A 마지막)
- **위험**: 메인 `users`/`api_keys` 와 충돌. **반드시 architect 단계에서 ai-assistant DB 분리 여부 확인**
### 6.5 Story F1 — vexplor 페이지 이식
- **입력**: vexplor 7개 페이지(`agents`, `providers`, `conversations`, `workspace`, `api-keys-manage`, `knowledge`, `layout.tsx`, `page.tsx`)
- **변경 작업**:
- 기존 invyone `aiAssistant/` 폴더 8개 페이지 삭제
- vexplor 페이지 그대로 복사 + import path 조정(`@/lib/api/aiAgent` 신규)
- shadcn 컴포넌트 의존성 검증
- **DoD**: 7개 페이지 모두 build / type-check / lint 통과 + 핵심 화면 렌더링
- **의존성**: F2(API 클라이언트), C12~C16(백엔드 컨트롤러)
- **위험**: invyone vs vexplor의 shadcn 컴포넌트 버전 차이로 prop signature 불일치
---
## 7. 순서 / 의존성 그래프
```
┌──────────┐
│ A │ (기존 제거 — 어디서나 시작 가능, A7은 최후)
└────┬─────┘
│ A1~A6 병행
┌──────────┐ ┌──────────┐
│ B │───▶│ C │
│ (스키마) │ │ (Spring) │
└────┬─────┘ └────┬─────┘
│ │
│ ▼
│ ┌──────────┐
│ │ D │ (Security 통합)
│ └────┬─────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ E │ │ F │ (Frontend) — C12~C16 후
│ (Claw) │ └────┬─────┘
└────┬─────┘ │
│ ▼
│ ┌──────────┐
│ │ G │ (메뉴/권한)
│ └────┬─────┘
▼ │
┌────────────────────┴──────┐
│ H │ (검증/배포)
└────────────────────────────┘
```
**Critical path**: B → C → F → G → H (백엔드 + 프론트가 직렬에 가까움)
**병행 가능**: A(제거)는 B/C와 병행 가능, E(OpenClaw)는 C9(엔진)이 끝나면 시작
---
## 8. 위험 / 가정
### 8.1 위험 (Risk)
| ID | 위험 | 영향 | 대응 |
|---|---|---|---|
| R1 | `300_create_openclaw_tables.sql` 미존재 → 역공학 오류 | High | architect에 vexplor DB dump 요청. 또는 vexplor 저자 인터뷰 |
| R2 | OpenClaw 외부 엔진 라이선스/배포 불명 | High | architect 단계에서 OpenClaw 패키징 방식 결정 (npm global / Docker 이미지) |
| R3 | invyone에 vexplor 커넥터 테이블 부재 (`external_db_connections` 등) | Medium | 커넥터 기능 P2로 후순위 또는 invyone 측에 동등 테이블 신설 결정 |
| R4 | ai-assistant `users`/`api_keys` 와 invyone 본 테이블 충돌 | High | A7 전 schema 분리 검증 필수 |
| R5 | LLM 스트리밍 응답 Spring 구현 난이도 | Medium | Spring WebFlux SSE 또는 RestClient 스트림 응답 사용 |
| R6 | 컨테이너 추가에 따른 운영 비용 증가(OpenClaw) | Low | OPENCLAW_ENABLED=false 토글로 단계 도입 |
| R7 | vexplor와 invyone의 shadcn/Next.js 버전 차이 | Medium | F5 의존성 점검 단계에서 잠재 호환 이슈 포착 |
| R8 | Spring AI MCP 도입 결정 보류 → 향후 재작업 가능성 | Medium | 인터페이스를 LlmClient 추상화로 만들어 교체 용이성 확보 |
| R9 | 멀티테넌시 정책 누락 → 회사 간 데이터 노출 | Critical | 모든 쿼리에 `company_code` 강제, 단위 테스트로 검증 |
| R10 | Jenkins helm-charts 측 ai-assistant 잔재 | Low | helm-charts 별도 PR(범위 외) |
### 8.2 가정 (Assumption)
- (A1) vexplor 백엔드의 PostgreSQL JSONB 사용은 invyone PostgreSQL에서도 동일하게 지원됨(같은 PG 16)
- (A2) invyone 운영자가 OpenClaw 컨테이너를 추가하는 것에 동의함
- (A3) AES-256 암호화 키는 환경변수로 주입(`AI_PROVIDER_ENCRYPTION_KEY`)
- (A4) 기존 invyone JWT 발급 시 `userType` claim 에 `SUPER_ADMIN` / `COMPANY_ADMIN` / 일반 값이 들어 있음 (vexplor 컨트롤러가 의존)
- (A5) `company_code` claim 도 JWT에 포함되어 있음
- (A6) invyone PostgreSQL 마이그레이션 도구가 정해져 있음(MyBatis 자체 또는 Flyway/Liquibase 중 — architect 결정)
- (A7) 외부 호출(B2B) 트래픽은 작음(분당 100 req 미만) — Rate limit 단순 구현 가능
---
## 9. Architect / Executor 가 결정해야 할 미정 사항
### 9.1 Architect 결정 필요
1. **마이그레이션 도구**: MyBatis 수동 SQL? Flyway? Liquibase?
2. **OpenClaw 배포**: npm global 컨테이너 vs Docker Hub 공식 이미지(존재 여부 확인 필요) vs vexplor 측 Dockerfile 재사용
3. **OpenClaw config 동기화 방식**: 공유 볼륨 파일? HTTP API? gRPC?
4. **Spring AI / Spring AI MCP 도입**: 직접 HTTP 클라이언트 유지 vs Spring AI 채택
5. **스케줄러 구현**: Spring `@Scheduled` (단순) vs Quartz (DB 영속화 + 클러스터링)
6. **커넥터 테이블 존재 여부**: invyone에 `external_db_connections`, `external_rest_api_connections`, `crawl_configs`, `pipeline_device_connections` 가 있는지 확인 → 없으면 P2 디그레이드
7. **API 키 폐기**: hard delete (vexplor) vs soft delete (status='revoked') — 감사 로그 정책에 따라 결정
8. **AES-256 키 관리**: 환경변수 vs Vault vs KMS
9. **스트리밍 응답**: Spring MVC SSE vs WebFlux
10. **Rate limit 구현**: 인메모리(단일 인스턴스) vs Redis(클러스터)
11. **테스트 전략**: testcontainers PostgreSQL + WireMock LLM, 또는 별도 dev DB
12. **Schedule**: `ai_agent_schedules` 의 cron 타임존(서버 시간 vs UTC)
### 9.2 Executor 단계 결정 가능
- 커밋 단위 분리 전략 (Epic 단위 vs Story 단위)
- 기존 ai-assistant 제거 시점(B 시작 전 vs C 진행 중)
- 페이지별 점진 이식 vs 일괄 교체
### 9.3 검증/협의 필요(사용자에게 한 번 더)
- (Q1) helm-charts 리포지토리(`gitlab.kpslp.kr/root/helm-charts/values_invyone.yaml`)에 ai-assistant 이미지 참조 잔재 존재 여부 — 확인 후 별도 PR 필요한지
- (Q2) ai-assistant가 사용한 DB schema 이름 / 호스트 — invyone 본 DB와 동일하면 `users`/`api_keys` 충돌 위험. **A7 실행 전 반드시 확인 필요**
- (Q3) 스케줄러 페이지 vexplor에 부재 — invyone에 신규 작성 필요한지(F7 Story 활성화 여부)
---
## 10. 산출물 및 다음 단계
### 10.1 본 PRD 결과물
- 파일: `C:\Dev\projects\invyone\.omc\plans\ai-orchestration-replacement-prd.md` (본 문서)
- 추가 산출물 (이번 단계 외):
- `architecture.md` (architect)
- `tasks/*.md` (executor가 Story 분해)
- `verification.md` (verifier)
### 10.2 권장 다음 단계
1. 사용자에게 9.3 미정 질문 답변 요청 (특히 Q2 ai-assistant DB 분리 여부)
2. **architect** 에이전트 호출:
- 9.1 결정 사항 12개에 대해 기술 결정
- C9 ExecutionEngine 동시성 모델 상세 설계
- 스키마 ERD 작성
3. architect 산출물을 바탕으로 **executor** 가 Story 단위 구현
---
## 11. 부록 — invyone 측 영향 파일 인덱스
### 11.1 삭제 대상
```
C:\Dev\projects\invyone\ai-assistant\ (전체)
C:\Dev\projects\invyone\backend-spring\src\main\java\com\erp\controller\AiAssistantProxyController.java
C:\Dev\projects\invyone\backend-spring\src\main\java\com\erp\service\AiAssistantProxyService.java
C:\Dev\projects\invyone\frontend\app\(main)\admin\aiAssistant\ (전체 8 파일/폴더)
C:\Dev\projects\invyone\frontend\lib\api\aiAssistant\ (client.ts, index.ts)
C:\Dev\projects\invyone\frontend\docs\AI_어시스턴트_메뉴_등록_가이드.md
C:\Dev\projects\invyone\docs\leeheejin\AI_어시스턴트_사용가이드.md (선택)
```
### 11.2 수정 대상
```
C:\Dev\projects\invyone\frontend\components\layout\AdminPageRenderer.tsx (라인 128, 142~147)
C:\Dev\projects\invyone\docker-compose.backend.win.yml (openclaw 서비스 추가)
C:\Dev\projects\invyone\backend-spring\src\main\java\com\erp\security\SecurityConfig.java (필터 체인)
C:\Dev\projects\invyone\backend-spring\build.gradle (의존성 추가 — RestClient, ScheduledTasks 등 필요시)
```
### 11.3 신규 작성 대상 (Spring 백엔드)
```
backend-spring/src/main/java/com/erp/
├── ai/
│ ├── controller/
│ │ ├── AiAgentController.java
│ │ ├── AiAgentGroupController.java
│ │ ├── AiAgentApiKeyController.java
│ │ ├── AiAgentProviderController.java
│ │ ├── AiAgentConversationController.java
│ │ ├── AiAgentUsageController.java
│ │ ├── AiAgentScheduleController.java
│ │ ├── AiKnowledgeController.java
│ │ └── AiV1GatewayController.java (외부 API 키 진입)
│ ├── service/
│ │ ├── AiAgentService.java
│ │ ├── AiAgentGroupService.java
│ │ ├── AiAgentApiKeyService.java
│ │ ├── AiAgentProviderService.java
│ │ ├── AiAgentConversationService.java
│ │ ├── AiAgentUsageService.java
│ │ ├── AiAnalysisLogService.java
│ │ ├── AiSchedulerService.java
│ │ ├── AiKnowledgeService.java
│ │ ├── MultiAgentExecutionEngine.java
│ │ ├── OpenClawSyncService.java
│ │ └── LlmClient.java
│ ├── mapper/ (MyBatis Mappers)
│ ├── dto/ (Request/Response DTOs)
│ ├── domain/ (Domain models)
│ ├── filter/AiApiKeyAuthFilter.java
│ └── util/EncryptUtil.java
└── ...
backend-spring/src/main/resources/mapper/ai/ (MyBatis XMLs)
backend-spring/src/main/resources/db/migration/V20260427__ai_orchestration.sql
```
### 11.4 신규 작성 대상 (프론트엔드)
```
frontend/app/(main)/admin/aiAssistant/
├── agents/page.tsx
├── workspace/page.tsx
├── providers/page.tsx
├── conversations/page.tsx
├── api-keys-manage/page.tsx
├── knowledge/page.tsx
├── (option) scheduler/page.tsx
├── layout.tsx
└── page.tsx
frontend/lib/api/aiAgent.ts
```
---
## 12. 변경 이력
| 일자 | 작성자 | 변경 |
|---|---|---|
| 2026-04-27 | planner | 최초 작성 |
+33
View File
@@ -0,0 +1,33 @@
# Open Questions
## ai-orchestration-replacement-prd - 2026-04-27
### Architect 결정 필요 (Resolve in next BMAD step)
- [ ] DB 마이그레이션 도구 선택 — MyBatis 수동 SQL vs Flyway vs Liquibase. 영향: Epic B 전반
- [ ] OpenClaw 배포 방식 — npm global 컨테이너 vs Docker Hub 이미지 vs Dockerfile 작성. 영향: Epic E 시작 가능 여부
- [ ] OpenClaw config 동기화 방식 — 공유 볼륨 파일 vs HTTP API. 영향: Story C11, E4
- [ ] Spring AI / Spring AI MCP 도입 여부 — 직접 HTTP 클라이언트 유지 vs Spring AI 채택. 영향: Story C1 (LlmClient 추상화 깊이)
- [ ] 스케줄러 구현 — Spring `@Scheduled` vs Quartz. 영향: Story C10, NFR(클러스터링)
- [ ] invyone에 vexplor 커넥터 테이블 존재 여부 — `external_db_connections`/`external_rest_api_connections`/`crawl_configs`/`pipeline_device_connections`. 영향: Story C3, C9 (커넥터 미존재 시 P2 디그레이드)
- [ ] API 키 폐기 정책 — hard delete (vexplor) vs soft delete. 영향: 감사/규제 요구사항
- [ ] AES-256 암호화 키 관리 — 환경변수 vs Vault vs KMS. 영향: 보안 NFR
- [ ] LLM 스트리밍 응답 구현 — Spring MVC SSE vs WebFlux. 영향: 빌드 의존성
- [ ] Rate limit 구현 — 인메모리 vs Redis. 영향: 클러스터 배포 가능성
- [ ] 테스트 전략 — testcontainers PostgreSQL + WireMock LLM vs 별도 dev DB
- [ ] cron 타임존 — 서버 시간 vs UTC. 영향: 스케줄 표시/실행 일관성
### 사용자 확인 필요 (Block before proceeding)
- [ ] (Q1) helm-charts 리포지토리(`gitlab.kpslp.kr/root/helm-charts/values_invyone.yaml`)에 ai-assistant 이미지 참조 잔재 존재 여부 — Epic A 완료 시점에 별도 PR 필요 여부 결정
- [ ] (Q2) **CRITICAL** — ai-assistant Sequelize가 사용한 DB schema 이름 / 호스트. invyone 본 DB와 동일 schema에서 `users`/`api_keys` 테이블이 동작했다면 drop 시 본 시스템 영향 가능. A7 실행 전 필수 확인
- [ ] (Q3) 스케줄러 페이지 — vexplor에 미포함, invyone에서 신규 작성 필요한지 (F7 Story 활성화 여부)
### Executor 단계에서 결정 가능
- [ ] 커밋 분할 전략 — Epic 단위 vs Story 단위
- [ ] ai-assistant 제거 시점 — B 시작 전 vs C 진행 중 (병행 가능)
- [ ] 페이지 점진 이식 vs 일괄 교체
### 산출물 검증 가정
- [ ] vexplor 측 PostgreSQL JSONB 사용이 invyone PG 16에서 동일 동작
- [ ] invyone JWT claim에 `userType`(SUPER_ADMIN/COMPANY_ADMIN), `companyCode` 포함 확인
- [ ] OpenClaw 컨테이너 추가에 대한 운영팀 승인
- [ ] 외부 호출 트래픽이 분당 100 req 미만(rate limit 인메모리 가능)
+596
View File
@@ -0,0 +1,596 @@
{
"version": "1.0.0",
"lastScanned": 1777285161379,
"projectRoot": "C:\\Dev\\projects\\invyone",
"techStack": {
"languages": [
{
"name": "JavaScript/TypeScript",
"version": null,
"confidence": "high",
"markers": [
"package.json"
]
}
],
"frameworks": [
{
"name": "playwright",
"version": "1.58.2",
"category": "testing"
}
],
"packageManager": "npm",
"runtime": null
},
"build": {
"buildCommand": null,
"testCommand": null,
"lintCommand": null,
"devCommand": null,
"scripts": {}
},
"conventions": {
"namingStyle": null,
"importStyle": null,
"testPattern": null,
"fileOrganization": null
},
"structure": {
"isMonorepo": false,
"workspaces": [],
"mainDirectories": [
"docs",
"scripts"
],
"gitBranches": {
"defaultBranch": "gbpark-node",
"branchingStrategy": null
}
},
"customNotes": [],
"directoryMap": {
"ai-assistant": {
"path": "ai-assistant",
"purpose": null,
"fileCount": 5,
"lastAccessed": 1777285161317,
"keyFiles": [
"Dockerfile.win",
"package-lock.json",
"package.json",
"README.md"
]
},
"backend": {
"path": "backend",
"purpose": null,
"fileCount": 0,
"lastAccessed": 1777285161317,
"keyFiles": []
},
"backend-node": {
"path": "backend-node",
"purpose": null,
"fileCount": 14,
"lastAccessed": 1777285161330,
"keyFiles": [
"API_연동_가이드.md",
"API_키_정리.md",
"install-multer.js",
"jest.config.js",
"nodemon.json"
]
},
"backend-spring": {
"path": "backend-spring",
"purpose": null,
"fileCount": 4,
"lastAccessed": 1777285161331,
"keyFiles": [
"build.gradle",
"gradlew",
"gradlew.bat",
"settings.gradle"
]
},
"db": {
"path": "db",
"purpose": null,
"fileCount": 2,
"lastAccessed": 1777285161332,
"keyFiles": [
"00-create-roles.sh",
"migrate_company13_export.sh"
]
},
"docker": {
"path": "docker",
"purpose": null,
"fileCount": 0,
"lastAccessed": 1777285161332,
"keyFiles": []
},
"docs": {
"path": "docs",
"purpose": "Documentation",
"fileCount": 33,
"lastAccessed": 1777285161333,
"keyFiles": [
"backend-analysis-README.md",
"backend-analysis-response.json",
"backend-api-route-mapping.md",
"backend-architecture-analysis.md",
"backend-architecture-detailed-analysis.md"
]
},
"frontend": {
"path": "frontend",
"purpose": null,
"fileCount": 26,
"lastAccessed": 1777285161333,
"keyFiles": [
"admin-test-result.md",
"approval-box-result.png",
"components.json",
"cosmic-preview.html",
"eslint.config.mjs"
]
},
"k8s": {
"path": "k8s",
"purpose": null,
"fileCount": 10,
"lastAccessed": 1777285161334,
"keyFiles": [
"backend-node.yaml",
"backend-spring.yaml",
"configmap.yaml",
"deploy.sh",
"frontend.yaml"
]
},
"mcp-agent-orchestrator": {
"path": "mcp-agent-orchestrator",
"purpose": null,
"fileCount": 4,
"lastAccessed": 1777285161335,
"keyFiles": [
"package-lock.json",
"package.json",
"README.md",
"tsconfig.json"
]
},
"notes": {
"path": "notes",
"purpose": null,
"fileCount": 0,
"lastAccessed": 1777285161337,
"keyFiles": []
},
"scripts": {
"path": "scripts",
"purpose": "Build/utility scripts",
"fileCount": 11,
"lastAccessed": 1777285161337,
"keyFiles": [
"add-modal-ids.py",
"analyze-company-info-layout.js",
"browser-test-admin-switch-button.js",
"browser-test-customer-crud.js",
"browser-test-customer-via-menu.js"
]
},
"test-output": {
"path": "test-output",
"purpose": null,
"fileCount": 2,
"lastAccessed": 1777285161338,
"keyFiles": [
"screen-149-field-type-verification-guide.md",
"unified-field-type-config-panel-test-guide.md"
]
},
"test-results": {
"path": "test-results",
"purpose": null,
"fileCount": 1,
"lastAccessed": 1777285161338,
"keyFiles": []
},
"_pipeline_backup": {
"path": "_pipeline_backup",
"purpose": null,
"fileCount": 1,
"lastAccessed": 1777285161339,
"keyFiles": [
"pipeline-state.json"
]
},
"ai-assistant\\src": {
"path": "ai-assistant\\src",
"purpose": "Source code",
"fileCount": 1,
"lastAccessed": 1777285161340,
"keyFiles": [
"app.js"
]
},
"backend-node\\data": {
"path": "backend-node\\data",
"purpose": "Data files",
"fileCount": 0,
"lastAccessed": 1777285161341,
"keyFiles": []
},
"backend-spring\\build": {
"path": "backend-spring\\build",
"purpose": "Build output",
"fileCount": 1,
"lastAccessed": 1777285161341,
"keyFiles": [
"resolvedMainClassName"
]
},
"backend-spring\\src": {
"path": "backend-spring\\src",
"purpose": "Source code",
"fileCount": 0,
"lastAccessed": 1777285161342,
"keyFiles": []
},
"db\\migrations": {
"path": "db\\migrations",
"purpose": "Database migrations",
"fileCount": 16,
"lastAccessed": 1777285161343,
"keyFiles": [
"046_MIGRATION_FIX.md",
"046_QUICK_FIX.md",
"README_1003.md"
]
},
"db\\scripts": {
"path": "db\\scripts",
"purpose": "Build/utility scripts",
"fileCount": 1,
"lastAccessed": 1777285161343,
"keyFiles": [
"README_cleanup.md"
]
},
"frontend\\app": {
"path": "frontend\\app",
"purpose": "Application code",
"fileCount": 5,
"lastAccessed": 1777285161344,
"keyFiles": [
"favicon.ico",
"globals.css",
"layout.tsx"
]
},
"frontend\\components": {
"path": "frontend\\components",
"purpose": "UI components",
"fileCount": 1,
"lastAccessed": 1777285161345,
"keyFiles": [
"GlobalFileViewer.tsx"
]
},
"mcp-agent-orchestrator\\src": {
"path": "mcp-agent-orchestrator\\src",
"purpose": "Source code",
"fileCount": 1,
"lastAccessed": 1777285161345,
"keyFiles": [
"index.ts"
]
}
},
"hotPaths": [
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\security\\SecurityConfig.java",
"accessCount": 10,
"lastAccessed": 1777297528541,
"type": "file"
},
{
"path": "frontend\\components\\layout\\AdminPageRenderer.tsx",
"accessCount": 8,
"lastAccessed": 1777297133296,
"type": "directory"
},
{
"path": "backend-spring\\src\\main\\resources\\application.yml",
"accessCount": 6,
"lastAccessed": 1777296723408,
"type": "file"
},
{
"path": "docker-compose.backend.win.yml",
"accessCount": 6,
"lastAccessed": 1777328965297,
"type": "file"
},
{
"path": "",
"accessCount": 6,
"lastAccessed": 1777329340127,
"type": "directory"
},
{
"path": "backend-spring\\src",
"accessCount": 5,
"lastAccessed": 1777296597185,
"type": "directory"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\service\\AiAssistantProxyService.java",
"accessCount": 3,
"lastAccessed": 1777296304775,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\ai\\health\\OpenClawHealthIndicator.java",
"accessCount": 3,
"lastAccessed": 1777296500455,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\ai\\client\\OpenClawClient.java",
"accessCount": 3,
"lastAccessed": 1777297100879,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\controller\\AiAssistantProxyController.java",
"accessCount": 2,
"lastAccessed": 1777292122594,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\ai\\config\\OpenClawProperties.java",
"accessCount": 2,
"lastAccessed": 1777296453350,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\ai\\exception\\OpenClawException.java",
"accessCount": 2,
"lastAccessed": 1777296454246,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp",
"accessCount": 2,
"lastAccessed": 1777296462858,
"type": "directory"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\config\\MyBatisConfig.java",
"accessCount": 2,
"lastAccessed": 1777296476543,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V002__create_ai_agents.sql",
"accessCount": 2,
"lastAccessed": 1777296498694,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V004__create_ai_agent_group_members.sql",
"accessCount": 2,
"lastAccessed": 1777296499056,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V005__create_ai_agent_api_keys.sql",
"accessCount": 2,
"lastAccessed": 1777296499857,
"type": "file"
},
{
"path": "frontend",
"accessCount": 2,
"lastAccessed": 1777296636910,
"type": "directory"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V001__create_ai_llm_providers.sql",
"accessCount": 2,
"lastAccessed": 1777297213582,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources",
"accessCount": 2,
"lastAccessed": 1777297282052,
"type": "directory"
},
{
"path": "docker-compose.frontend.win.yml",
"accessCount": 2,
"lastAccessed": 1777328980655,
"type": "directory"
},
{
"path": "ai-assistant\\src\\app.js",
"accessCount": 1,
"lastAccessed": 1777291729219,
"type": "file"
},
{
"path": "Jenkinsfile",
"accessCount": 1,
"lastAccessed": 1777292137142,
"type": "file"
},
{
"path": "frontend\\docs\\AI_어시스턴트_메뉴_등록_가이드.md",
"accessCount": 1,
"lastAccessed": 1777292137475,
"type": "file"
},
{
"path": "ai-assistant\\package.json",
"accessCount": 1,
"lastAccessed": 1777292156893,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java",
"accessCount": 1,
"lastAccessed": 1777292158334,
"type": "directory"
},
{
"path": "ai-assistant\\src\\models",
"accessCount": 1,
"lastAccessed": 1777292170037,
"type": "directory"
},
{
"path": "frontend\\lib\\api\\aiAssistant",
"accessCount": 1,
"lastAccessed": 1777292171252,
"type": "directory"
},
{
"path": "ai-assistant\\.env.example",
"accessCount": 1,
"lastAccessed": 1777293988045,
"type": "file"
},
{
"path": "ai-assistant\\src\\models\\index.js",
"accessCount": 1,
"lastAccessed": 1777293989107,
"type": "file"
},
{
"path": "ai-assistant\\src\\models\\user.model.js",
"accessCount": 1,
"lastAccessed": 1777293989222,
"type": "file"
},
{
"path": "ai-assistant\\src\\models\\api-key.model.js",
"accessCount": 1,
"lastAccessed": 1777293990287,
"type": "file"
},
{
"path": "ai-assistant\\src\\models\\llm-provider.model.js",
"accessCount": 1,
"lastAccessed": 1777293990609,
"type": "file"
},
{
"path": "ai-assistant\\src\\models\\usage-log.model.js",
"accessCount": 1,
"lastAccessed": 1777293990839,
"type": "file"
},
{
"path": "ai-assistant",
"accessCount": 1,
"lastAccessed": 1777293992318,
"type": "directory"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\security",
"accessCount": 1,
"lastAccessed": 1777294130003,
"type": "directory"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V003__create_ai_agent_groups.sql",
"accessCount": 1,
"lastAccessed": 1777296304942,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V006__create_ai_agent_conversations.sql",
"accessCount": 1,
"lastAccessed": 1777296317538,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V007__create_ai_agent_messages.sql",
"accessCount": 1,
"lastAccessed": 1777296319127,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V008__create_ai_agent_usage_logs.sql",
"accessCount": 1,
"lastAccessed": 1777296323195,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V009__create_ai_agent_schedules.sql",
"accessCount": 1,
"lastAccessed": 1777296326054,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V010__create_ai_analysis_logs.sql",
"accessCount": 1,
"lastAccessed": 1777296329376,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V011__create_ai_knowledge_files.sql",
"accessCount": 1,
"lastAccessed": 1777296334986,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V012__create_quartz_tables.sql",
"accessCount": 1,
"lastAccessed": 1777296359719,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\migration\\V013__create_ai_indexes.sql",
"accessCount": 1,
"lastAccessed": 1777296364743,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\resources\\db\\cleanup\\drop_ai_assistant_db.sql",
"accessCount": 1,
"lastAccessed": 1777296381320,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\controller\\RoleController.java",
"accessCount": 1,
"lastAccessed": 1777296382933,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\dto\\ApiResponse.java",
"accessCount": 1,
"lastAccessed": 1777296431064,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\service\\AdminService.java",
"accessCount": 1,
"lastAccessed": 1777296435916,
"type": "file"
},
{
"path": "backend-spring\\src\\main\\java\\com\\erp\\controller\\AdminController.java",
"accessCount": 1,
"lastAccessed": 1777296436288,
"type": "file"
}
],
"userDirectives": []
}
@@ -0,0 +1,34 @@
{"t":0,"agent":"a781850","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a7d7327","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a89e2b1","agent_type":"planner","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"a89e2b1","agent_type":"planner","event":"agent_stop","success":true,"duration_ms":487318}
{"t":0,"agent":"ae04d8f","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a0c44dd","agent_type":"architect","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"a0c44dd","agent_type":"architect","event":"agent_stop","success":true,"duration_ms":432075}
{"t":0,"agent":"a0c2e53","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"a0c2e53","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":51763}
{"t":0,"agent":"a997ceb","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"system","event":"keyword_detected","keyword":"autopilot"}
{"t":0,"agent":"system","event":"mode_change","mode_from":"none","mode_to":"autopilot"}
{"t":0,"agent":"system","event":"skill_invoked","skill_name":"oh-my-claudecode:autopilot"}
{"t":0,"agent":"a51e4eb","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"ab16ada","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"acea08b","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"aa78406","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"acea08b","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":228388}
{"t":0,"agent":"ab16ada","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":296000}
{"t":0,"agent":"a51e4eb","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":337914}
{"t":0,"agent":"a8cc251","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"a317ee0","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"a8cc251","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":140638}
{"t":0,"agent":"a317ee0","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":529803}
{"t":0,"agent":"ada3d4e","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"aa78406","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":847770}
{"t":0,"agent":"ada3d4e","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":191403}
{"t":0,"agent":"a94527b","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"a94527b","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":412139}
{"t":0,"agent":"system","event":"skill_invoked","skill_name":"oh-my-claudecode:cancel"}
{"t":0,"agent":"a1aced0","agent_type":"executor","event":"agent_start","parent_mode":"none"}
{"t":0,"agent":"a1aced0","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":139387}
{"t":0,"agent":"a0d3cd3","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a4fba8f","agent_type":"unknown","event":"agent_stop","success":true}
@@ -0,0 +1,13 @@
{"t":0,"agent":"aa9ac85","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a09bc21","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"system","event":"keyword_detected","keyword":"ultrathink"}
{"t":0,"agent":"a38f657","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"ae1a7b4","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"system","event":"keyword_detected","keyword":"ultrathink"}
{"t":0,"agent":"a86b238","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"system","event":"keyword_detected","keyword":"ultrathink"}
{"t":0,"agent":"a8210f1","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"acfaccf","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"system","event":"keyword_detected","keyword":"ultrathink"}
{"t":0,"agent":"a154f20","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"system","event":"keyword_detected","keyword":"ultrathink"}
+1
View File
@@ -0,0 +1 @@
{"session_id":"ac02431b-1ea5-4997-8204-b2bbbcab174f","transcript_path":"C:\\Users\\ramse\\.claude\\projects\\C--Dev-projects-invyone\\ac02431b-1ea5-4997-8204-b2bbbcab174f.jsonl","cwd":"C:\\Dev\\projects\\invyone","model":{"id":"claude-opus-4-7[1m]","display_name":"Opus 4.7 (1M context)"},"workspace":{"current_dir":"C:\\Dev\\projects\\invyone","project_dir":"C:\\Dev\\projects\\invyone","added_dirs":[]},"version":"2.1.119","output_style":{"name":"default"},"cost":{"total_cost_usd":55.235730299999986,"total_duration_ms":44379071,"total_api_duration_ms":4841115,"total_lines_added":9232,"total_lines_removed":239},"context_window":{"total_input_tokens":2511,"total_output_tokens":370081,"context_window_size":1000000,"current_usage":{"input_tokens":6,"output_tokens":894,"cache_creation_input_tokens":943,"cache_read_input_tokens":303528},"used_percentage":30,"remaining_percentage":70},"exceeds_200k_tokens":true,"fast_mode":false,"effort":{"level":"xhigh"},"thinking":{"enabled":true},"rate_limits":{"five_hour":{"used_percentage":19,"resets_at":1777339800},"seven_day":{"used_percentage":32,"resets_at":1777482000}}}
+7
View File
@@ -0,0 +1,7 @@
{
"tool_name": "Bash",
"tool_input_preview": "{\"command\":\"docker ps --format \\\"table {{.Names}}\\\\t{{.Status}}\\\\t{{.Ports}}\\\"\",\"description\":\"Check container statuses\"}",
"error": "Exit code 1\nerror during connect: Get \"http://%2F%2F.%2Fpipe%2FdockerDesktopLinuxEngine/v1.48/containers/json\": open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified.",
"timestamp": "2026-04-27T22:30:37.764Z",
"retry_count": 2
}
+195
View File
@@ -0,0 +1,195 @@
{
"updatedAt": "2026-04-27T22:00:14.488Z",
"missions": [
{
"id": "session:ac02431b-1ea5-4997-8204-b2bbbcab174f:none",
"source": "session",
"name": "none",
"objective": "Session mission",
"createdAt": "2026-04-27T12:13:45.719Z",
"updatedAt": "2026-04-27T22:00:14.488Z",
"status": "done",
"workerCount": 12,
"taskCounts": {
"total": 12,
"pending": 0,
"blocked": 0,
"inProgress": 0,
"completed": 12,
"failed": 0
},
"agents": [
{
"name": "planner:a89e2b1",
"role": "planner",
"ownership": "a89e2b13a07d3a60a",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T22:00:14.488Z"
},
{
"name": "architect:a0c44dd",
"role": "architect",
"ownership": "a0c44ddcc9f1ae1f2",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T12:55:16.251Z"
},
{
"name": "executor:a0c2e53",
"role": "executor",
"ownership": "a0c2e531739cf1a5f",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T12:56:56.423Z"
},
{
"name": "executor:a51e4eb",
"role": "executor",
"ownership": "a51e4eba42b48da04",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:29:16.906Z"
},
{
"name": "executor:ab16ada",
"role": "executor",
"ownership": "ab16ada53ac819e74",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:28:59.529Z"
},
{
"name": "executor:acea08b",
"role": "executor",
"ownership": "acea08b23eb620b14",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:28:21.439Z"
},
{
"name": "executor:aa78406",
"role": "executor",
"ownership": "aa78406b52bf55c8f",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:40:10.074Z"
},
{
"name": "executor:a8cc251",
"role": "executor",
"ownership": "a8cc25174fe56dc0c",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:31:58.008Z"
},
{
"name": "executor:a317ee0",
"role": "executor",
"ownership": "a317ee06d14085a4f",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:39:12.228Z"
},
{
"name": "executor:ada3d4e",
"role": "executor",
"ownership": "ada3d4ebf0605ba01",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:43:05.657Z"
},
{
"name": "executor:a94527b",
"role": "executor",
"ownership": "a94527b228b96ca4d",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T13:50:54.260Z"
},
{
"name": "executor:a1aced0",
"role": "executor",
"ownership": "a1aced03cfcd64304",
"status": "done",
"currentStep": null,
"latestUpdate": "completed",
"completedSummary": null,
"updatedAt": "2026-04-27T20:44:52.394Z"
}
],
"timeline": [
{
"id": "session-start:a94527b228b96ca4d:2026-04-27T13:44:02.121Z",
"at": "2026-04-27T13:44:02.121Z",
"kind": "update",
"agent": "executor:a94527b",
"detail": "started executor:a94527b",
"sourceKey": "session-start:a94527b228b96ca4d"
},
{
"id": "session-stop:a94527b228b96ca4d:2026-04-27T13:50:54.260Z",
"at": "2026-04-27T13:50:54.260Z",
"kind": "completion",
"agent": "executor:a94527b",
"detail": "completed",
"sourceKey": "session-stop:a94527b228b96ca4d"
},
{
"id": "session-start:a1aced03cfcd64304:2026-04-27T20:42:33.007Z",
"at": "2026-04-27T20:42:33.007Z",
"kind": "update",
"agent": "executor:a1aced0",
"detail": "started executor:a1aced0",
"sourceKey": "session-start:a1aced03cfcd64304"
},
{
"id": "session-stop:a1aced03cfcd64304:2026-04-27T20:44:52.394Z",
"at": "2026-04-27T20:44:52.394Z",
"kind": "completion",
"agent": "executor:a1aced0",
"detail": "completed",
"sourceKey": "session-stop:a1aced03cfcd64304"
},
{
"id": "session-stop:a0d3cd37785105e78:2026-04-27T20:50:01.582Z",
"at": "2026-04-27T20:50:01.582Z",
"kind": "completion",
"agent": "planner:a89e2b1",
"detail": "completed",
"sourceKey": "session-stop:a0d3cd37785105e78"
},
{
"id": "session-stop:a4fba8fcce9081ce9:2026-04-27T22:00:14.488Z",
"at": "2026-04-27T22:00:14.488Z",
"kind": "completion",
"agent": "planner:a89e2b1",
"detail": "completed",
"sourceKey": "session-stop:a4fba8fcce9081ce9"
}
]
}
]
}
@@ -0,0 +1,6 @@
{
"timestamp": "2026-04-27T11:33:26.939Z",
"backgroundTasks": [],
"sessionStartTimestamp": "2026-04-27T10:19:21.354Z",
"sessionId": "ac02431b-1ea5-4997-8204-b2bbbcab174f"
}
@@ -0,0 +1,6 @@
{
"timestamp": "2026-04-21T23:16:09.612Z",
"backgroundTasks": [],
"sessionStartTimestamp": "2026-04-21T23:14:36.455Z",
"sessionId": "c7af2efd-4576-4263-a2ec-53492f6628e2"
}
+116
View File
@@ -0,0 +1,116 @@
{
"agents": [
{
"agent_id": "a89e2b13a07d3a60a",
"agent_type": "planner",
"started_at": "2026-04-27T12:13:45.719Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T12:21:53.037Z",
"duration_ms": 487318
},
{
"agent_id": "a0c44ddcc9f1ae1f2",
"agent_type": "architect",
"started_at": "2026-04-27T12:48:04.176Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T12:55:16.251Z",
"duration_ms": 432075
},
{
"agent_id": "a0c2e531739cf1a5f",
"agent_type": "executor",
"started_at": "2026-04-27T12:56:04.660Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T12:56:56.423Z",
"duration_ms": 51763
},
{
"agent_id": "a51e4eba42b48da04",
"agent_type": "executor",
"started_at": "2026-04-27T13:23:38.992Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:29:16.906Z",
"duration_ms": 337914
},
{
"agent_id": "ab16ada53ac819e74",
"agent_type": "executor",
"started_at": "2026-04-27T13:24:03.529Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:28:59.529Z",
"duration_ms": 296000
},
{
"agent_id": "acea08b23eb620b14",
"agent_type": "executor",
"started_at": "2026-04-27T13:24:33.051Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:28:21.439Z",
"duration_ms": 228388
},
{
"agent_id": "aa78406b52bf55c8f",
"agent_type": "executor",
"started_at": "2026-04-27T13:26:02.304Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:40:10.074Z",
"duration_ms": 847770
},
{
"agent_id": "a8cc25174fe56dc0c",
"agent_type": "executor",
"started_at": "2026-04-27T13:29:37.370Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:31:58.008Z",
"duration_ms": 140638
},
{
"agent_id": "a317ee06d14085a4f",
"agent_type": "executor",
"started_at": "2026-04-27T13:30:22.425Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:39:12.228Z",
"duration_ms": 529803
},
{
"agent_id": "ada3d4ebf0605ba01",
"agent_type": "executor",
"started_at": "2026-04-27T13:39:54.254Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:43:05.657Z",
"duration_ms": 191403
},
{
"agent_id": "a94527b228b96ca4d",
"agent_type": "executor",
"started_at": "2026-04-27T13:44:02.121Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T13:50:54.260Z",
"duration_ms": 412139
},
{
"agent_id": "a1aced03cfcd64304",
"agent_type": "executor",
"started_at": "2026-04-27T20:42:33.007Z",
"parent_mode": "none",
"status": "completed",
"completed_at": "2026-04-27T20:44:52.394Z",
"duration_ms": 139387
}
],
"total_spawned": 12,
"total_completed": 12,
"total_failed": 0,
"last_updated": "2026-04-27T22:00:14.590Z"
}
@@ -39,7 +39,7 @@ public class SecurityConfig {
private final AiAgentApiKeyService aiAgentApiKeyService; private final AiAgentApiKeyService aiAgentApiKeyService;
/** /**
* CORS 화이트리스트. 콤마 구분 문자열로 주입 (예: "http://localhost:3000,https://v1.invion.com"). * CORS 화이트리스트. 콤마 구분 문자열로 주입 (예: "http://localhost:3000,https://v1.invyone.com").
* application.yml 또는 환경변수 CORS_ALLOWED_ORIGINS 로 설정. * application.yml 또는 환경변수 CORS_ALLOWED_ORIGINS 로 설정.
*/ */
@Value("${cors.allowed-origins}") @Value("${cors.allowed-origins}")
@@ -0,0 +1,23 @@
-- V001: ai_llm_providers
-- LLM 프로바이더 테이블 (anthropic/openai/google/deepseek/ollama)
CREATE TABLE ai_llm_providers (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
display_name VARCHAR(200) NOT NULL,
api_key_encrypted TEXT NOT NULL,
model_name VARCHAR(100) NOT NULL,
endpoint VARCHAR(500),
priority INTEGER NOT NULL DEFAULT 1,
max_tokens INTEGER NOT NULL DEFAULT 4096,
temperature NUMERIC(3,2) NOT NULL DEFAULT 0.70,
cost_per_1k_input NUMERIC(10,6) NOT NULL DEFAULT 0,
cost_per_1k_output NUMERIC(10,6) NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_providers_priority ON ai_llm_providers (priority);
CREATE INDEX idx_providers_active ON ai_llm_providers (is_active);
@@ -0,0 +1,23 @@
-- V002: ai_agents
-- AI 에이전트 테이블
CREATE TABLE ai_agents (
id BIGSERIAL PRIMARY KEY,
agent_id VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
model VARCHAR(100) NOT NULL DEFAULT 'claude-sonnet-4-20250514',
system_prompt TEXT,
tools JSONB NOT NULL DEFAULT '[]'::jsonb,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active','inactive','archived')),
company_code VARCHAR(20),
created_by VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_ai_agents_company ON ai_agents (company_code);
CREATE INDEX idx_ai_agents_status ON ai_agents (status);
CREATE INDEX idx_ai_agents_created ON ai_agents (created_at DESC);
@@ -0,0 +1,20 @@
-- V003: ai_agent_groups
-- AI 에이전트 그룹 테이블
CREATE TABLE ai_agent_groups (
id BIGSERIAL PRIMARY KEY,
group_id VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
execution_mode VARCHAR(20) NOT NULL DEFAULT 'mixed'
CHECK (execution_mode IN ('parallel','sequential','mixed')),
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active','inactive','archived')),
company_code VARCHAR(20),
created_by VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_ai_groups_company ON ai_agent_groups (company_code);
CREATE INDEX idx_ai_groups_status ON ai_agent_groups (status);
@@ -0,0 +1,16 @@
-- V004: ai_agent_group_members
-- AI 에이전트 그룹 멤버 테이블 (execution_order 로 mixed 모드 지원)
CREATE TABLE ai_agent_group_members (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES ai_agent_groups(id) ON DELETE CASCADE,
agent_id BIGINT NOT NULL REFERENCES ai_agents(id) ON DELETE RESTRICT,
role_name VARCHAR(100) NOT NULL,
connectors JSONB NOT NULL DEFAULT '[]'::jsonb,
execution_order INTEGER NOT NULL DEFAULT 1,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_group_members_group ON ai_agent_group_members (group_id, execution_order);
@@ -0,0 +1,27 @@
-- V005: ai_agent_api_keys
-- AI 에이전트 API 키 테이블 (sk-pipe-* 형식)
CREATE TABLE ai_agent_api_keys (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
key_hash CHAR(64) NOT NULL UNIQUE,
key_prefix VARCHAR(16) NOT NULL,
user_id VARCHAR(64) NOT NULL,
company_code VARCHAR(20),
agent_id BIGINT REFERENCES ai_agents(id) ON DELETE SET NULL,
permissions JSONB NOT NULL DEFAULT '["chat"]'::jsonb,
rate_limit INTEGER NOT NULL DEFAULT 60,
monthly_token_limit BIGINT NOT NULL DEFAULT 1000000,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active','revoked')),
last_used_at TIMESTAMPTZ,
usage_count BIGINT NOT NULL DEFAULT 0,
total_tokens BIGINT NOT NULL DEFAULT 0,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_api_keys_hash ON ai_agent_api_keys (key_hash);
CREATE INDEX idx_api_keys_user ON ai_agent_api_keys (user_id);
CREATE INDEX idx_api_keys_company ON ai_agent_api_keys (company_code);
CREATE INDEX idx_api_keys_status ON ai_agent_api_keys (status);
@@ -0,0 +1,22 @@
-- V006: ai_agent_conversations
-- AI 에이전트 대화 세션 테이블
CREATE TABLE ai_agent_conversations (
id BIGSERIAL PRIMARY KEY,
conversation_id VARCHAR(64) NOT NULL UNIQUE,
agent_id BIGINT REFERENCES ai_agents(id) ON DELETE SET NULL,
user_id VARCHAR(64),
api_key_id BIGINT REFERENCES ai_agent_api_keys(id) ON DELETE SET NULL,
title VARCHAR(500),
message_count INTEGER NOT NULL DEFAULT 0,
total_tokens BIGINT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_conv_agent ON ai_agent_conversations (agent_id);
CREATE INDEX idx_conv_user ON ai_agent_conversations (user_id);
CREATE INDEX idx_conv_apikey ON ai_agent_conversations (api_key_id);
CREATE INDEX idx_conv_updated ON ai_agent_conversations (updated_at DESC);
@@ -0,0 +1,15 @@
-- V007: ai_agent_messages
-- AI 에이전트 대화 메시지 테이블
CREATE TABLE ai_agent_messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES ai_agent_conversations(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL CHECK (role IN ('system','user','assistant','tool')),
content TEXT NOT NULL,
tool_calls JSONB,
token_count INTEGER NOT NULL DEFAULT 0,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_msg_conv ON ai_agent_messages (conversation_id, created_at);
@@ -0,0 +1,28 @@
-- V008: ai_agent_usage_logs
-- AI 에이전트 사용량 로그 테이블 (토큰/비용 추적)
CREATE TABLE ai_agent_usage_logs (
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(64),
api_key_id BIGINT REFERENCES ai_agent_api_keys(id) ON DELETE SET NULL,
agent_id BIGINT REFERENCES ai_agents(id) ON DELETE SET NULL,
conversation_id BIGINT REFERENCES ai_agent_conversations(id) ON DELETE SET NULL,
provider_name VARCHAR(50),
model_name VARCHAR(100),
prompt_tokens INTEGER NOT NULL DEFAULT 0,
completion_tokens INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL DEFAULT 0,
cost_usd NUMERIC(12,6) NOT NULL DEFAULT 0,
response_time_ms INTEGER,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_message TEXT,
request_path VARCHAR(500),
ip_address INET,
company_code VARCHAR(20),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_usage_created ON ai_agent_usage_logs (created_at DESC);
CREATE INDEX idx_usage_apikey ON ai_agent_usage_logs (api_key_id, created_at DESC);
CREATE INDEX idx_usage_agent ON ai_agent_usage_logs (agent_id, created_at DESC);
CREATE INDEX idx_usage_company ON ai_agent_usage_logs (company_code, created_at DESC);
@@ -0,0 +1,22 @@
-- V009: ai_agent_schedules
-- AI 에이전트 스케줄 테이블 (Quartz JDBC 연동)
CREATE TABLE ai_agent_schedules (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
group_id BIGINT NOT NULL REFERENCES ai_agent_groups(id) ON DELETE CASCADE,
cron_expression VARCHAR(100) NOT NULL,
timezone VARCHAR(50) NOT NULL DEFAULT 'Asia/Seoul',
input_message TEXT NOT NULL,
notification JSONB NOT NULL DEFAULT '{}'::jsonb,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_run_at TIMESTAMPTZ,
run_count BIGINT NOT NULL DEFAULT 0,
company_code VARCHAR(20),
created_by VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sched_active ON ai_agent_schedules (is_active);
CREATE INDEX idx_sched_company ON ai_agent_schedules (company_code);
@@ -0,0 +1,24 @@
-- V010: ai_analysis_logs
-- AI 분석 실행 이력 테이블 (정확도 추적 포함)
CREATE TABLE ai_analysis_logs (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT REFERENCES ai_agent_groups(id) ON DELETE SET NULL,
agent_id BIGINT REFERENCES ai_agents(id) ON DELETE SET NULL,
schedule_id BIGINT REFERENCES ai_agent_schedules(id) ON DELETE SET NULL,
execution_type VARCHAR(20) NOT NULL
CHECK (execution_type IN ('manual','api','schedule')),
input_message TEXT NOT NULL,
analysis_result TEXT NOT NULL,
prediction JSONB,
actual_result JSONB,
accuracy_score NUMERIC(5,2),
tokens_used INTEGER NOT NULL DEFAULT 0,
duration_ms INTEGER,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
company_code VARCHAR(20),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_analysis_group_created ON ai_analysis_logs (group_id, created_at DESC);
CREATE INDEX idx_analysis_company ON ai_analysis_logs (company_code, created_at DESC);
@@ -0,0 +1,20 @@
-- V011: ai_knowledge_files
-- AI 지식 파일 테이블 (RAG 문서 저장)
CREATE TABLE ai_knowledge_files (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(300) NOT NULL,
file_name VARCHAR(300),
category VARCHAR(100),
description TEXT,
content TEXT NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0,
mime_type VARCHAR(100),
company_code VARCHAR(20),
created_by VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_knowledge_category ON ai_knowledge_files (category);
CREATE INDEX idx_knowledge_company ON ai_knowledge_files (company_code);
@@ -0,0 +1,156 @@
-- V012: Quartz JDBC JobStore 영구 테이블 (PostgreSQL)
-- 출처: spring-boot-starter-quartz / quartz tables_postgres.sql
CREATE TABLE QRTZ_JOB_DETAILS (
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250),
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE BOOLEAN NOT NULL,
IS_NONCONCURRENT BOOLEAN NOT NULL,
IS_UPDATE_DATA BOOLEAN NOT NULL,
REQUESTS_RECOVERY BOOLEAN NOT NULL,
JOB_DATA BYTEA,
CONSTRAINT PK_QRTZ_JOB_DETAILS PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
);
CREATE TABLE QRTZ_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250),
NEXT_FIRE_TIME BIGINT,
PREV_FIRE_TIME BIGINT,
PRIORITY INTEGER,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT NOT NULL,
END_TIME BIGINT,
CALENDAR_NAME VARCHAR(200),
MISFIRE_INSTR SMALLINT,
JOB_DATA BYTEA,
CONSTRAINT PK_QRTZ_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
CONSTRAINT FK_QRTZ_TRIGGERS_JOB_DETAILS FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
);
CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT NOT NULL,
REPEAT_INTERVAL BIGINT NOT NULL,
TIMES_TRIGGERED BIGINT NOT NULL,
CONSTRAINT PK_QRTZ_SIMPLE_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CRON_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
CONSTRAINT PK_QRTZ_CRON_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
CONSTRAINT FK_QRTZ_CRON_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_SIMPROP_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512),
STR_PROP_2 VARCHAR(512),
STR_PROP_3 VARCHAR(512),
INT_PROP_1 INTEGER,
INT_PROP_2 INTEGER,
LONG_PROP_1 BIGINT,
LONG_PROP_2 BIGINT,
DEC_PROP_1 NUMERIC(13,4),
DEC_PROP_2 NUMERIC(13,4),
BOOL_PROP_1 BOOLEAN,
BOOL_PROP_2 BOOLEAN,
CONSTRAINT PK_QRTZ_SIMPROP_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_BLOB_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BYTEA,
CONSTRAINT PK_QRTZ_BLOB_TRIGGERS PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
CONSTRAINT FK_QRTZ_BLOB_TRIGGERS FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CALENDARS (
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BYTEA NOT NULL,
CONSTRAINT PK_QRTZ_CALENDARS PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
);
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CONSTRAINT PK_QRTZ_PAUSED_TRIGGER_GRPS PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_FIRED_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT NOT NULL,
SCHED_TIME BIGINT NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200),
JOB_GROUP VARCHAR(200),
IS_NONCONCURRENT BOOLEAN,
REQUESTS_RECOVERY BOOLEAN,
CONSTRAINT PK_QRTZ_FIRED_TRIGGERS PRIMARY KEY (SCHED_NAME, ENTRY_ID)
);
CREATE TABLE QRTZ_SCHEDULER_STATE (
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT NOT NULL,
CHECKIN_INTERVAL BIGINT NOT NULL,
CONSTRAINT PK_QRTZ_SCHEDULER_STATE PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
);
CREATE TABLE QRTZ_LOCKS (
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
CONSTRAINT PK_QRTZ_LOCKS PRIMARY KEY (SCHED_NAME, LOCK_NAME)
);
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
@@ -0,0 +1,29 @@
-- V013: AI 테이블 추가 성능 인덱스
-- 복합 인덱스 및 자주 조회되는 컬럼에 대한 보조 인덱스
-- ai_agents: agent_id 문자열 조회 (ApiKey 검증 시)
CREATE INDEX idx_ai_agents_agent_id ON ai_agents (agent_id);
-- ai_agent_groups: group_id 문자열 조회
CREATE INDEX idx_ai_groups_group_id ON ai_agent_groups (group_id);
-- ai_agent_group_members: agent_id 역방향 조회 (에이전트 삭제 전 그룹 확인)
CREATE INDEX idx_group_members_agent ON ai_agent_group_members (agent_id);
-- ai_agent_conversations: conversation_id 문자열 조회
CREATE INDEX idx_conv_conv_id ON ai_agent_conversations (conversation_id);
-- ai_agent_conversations: company_code 조회 (멀티테넌시)
CREATE INDEX idx_conv_company ON ai_agent_conversations (user_id, updated_at DESC);
-- ai_analysis_logs: schedule_id 조회 (스케줄 실행 이력)
CREATE INDEX idx_analysis_schedule ON ai_analysis_logs (schedule_id, created_at DESC);
-- ai_analysis_logs: agent_id 조회
CREATE INDEX idx_analysis_agent ON ai_analysis_logs (agent_id, created_at DESC);
-- ai_agent_usage_logs: user_id + 기간 조회
CREATE INDEX idx_usage_user ON ai_agent_usage_logs (user_id, created_at DESC);
-- ai_knowledge_files: company_code + category 복합 조회
CREATE INDEX idx_knowledge_company_category ON ai_knowledge_files (company_code, category);
@@ -0,0 +1,200 @@
-- V014: AI 어시스턴트 메뉴 등록
-- 관리자 메뉴(MENU_TYPE='0')에 AI 어시스턴트 그룹 및 하위 7개 메뉴를 등록합니다.
-- SCREEN_GROUPS에 메뉴 그룹도 함께 등록하여 화면 그룹 트리와 연결합니다.
-- 멱등성: INSERT ... WHERE NOT EXISTS 사용 (unique constraint 없는 기존 테이블 대응).
-- ══════════════════════════════════════════════════════════════
-- 1. SCREEN_GROUPS: AI 어시스턴트 루트 그룹
-- ══════════════════════════════════════════════════════════════
INSERT INTO SCREEN_GROUPS (
GROUP_NAME, GROUP_CODE, PARENT_GROUP_ID, GROUP_LEVEL,
DISPLAY_ORDER, COMPANY_CODE, WRITER, HIERARCHY_PATH, DESCRIPTION, IS_ACTIVE, ICON
)
SELECT
'AI 어시스턴트', 'AI_ASSISTANT', NULL, 0,
9900, '*', 'system', 'AI_ASSISTANT', 'AI 멀티 에이전트 관리 메뉴 그룹', 'Y', 'robot'
WHERE NOT EXISTS (
SELECT 1 FROM SCREEN_GROUPS
WHERE GROUP_CODE = 'AI_ASSISTANT' AND COMPANY_CODE = '*'
);
-- ══════════════════════════════════════════════════════════════
-- 2. MENU_INFO: AI 어시스턴트 부모 메뉴 (관리자 메뉴, 루트)
-- ══════════════════════════════════════════════════════════════
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT
'AI_ASSISTANT_ROOT', '0', '0', 'AI 어시스턴트',
'/admin/aiAssistant', 'AI 멀티 에이전트 관리',
9900, 'system', NOW(), 'active', '*', 'robot'
WHERE NOT EXISTS (
SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_ASSISTANT_ROOT'
);
-- ══════════════════════════════════════════════════════════════
-- 3. SCREEN_GROUPS: 하위 그룹 6개
-- PARENT_GROUP_ID는 위에서 삽입한 AI_ASSISTANT 그룹을 서브쿼리로 참조
-- ══════════════════════════════════════════════════════════════
-- 3-1. 에이전트 관리
INSERT INTO SCREEN_GROUPS (
GROUP_NAME, GROUP_CODE, PARENT_GROUP_ID, GROUP_LEVEL,
DISPLAY_ORDER, COMPANY_CODE, WRITER, HIERARCHY_PATH, DESCRIPTION, IS_ACTIVE, ICON
)
SELECT
'에이전트 관리', 'AI_AGENTS', SG.ID, 1,
9901, '*', 'system', 'AI_ASSISTANT/AI_AGENTS', 'LLM 에이전트 CRUD', 'Y', 'cpu'
FROM SCREEN_GROUPS SG
WHERE SG.GROUP_CODE = 'AI_ASSISTANT' AND SG.COMPANY_CODE = '*'
AND NOT EXISTS (
SELECT 1 FROM SCREEN_GROUPS WHERE GROUP_CODE = 'AI_AGENTS' AND COMPANY_CODE = '*'
);
-- 3-2. LLM 프로바이더
INSERT INTO SCREEN_GROUPS (
GROUP_NAME, GROUP_CODE, PARENT_GROUP_ID, GROUP_LEVEL,
DISPLAY_ORDER, COMPANY_CODE, WRITER, HIERARCHY_PATH, DESCRIPTION, IS_ACTIVE, ICON
)
SELECT
'LLM 프로바이더', 'AI_PROVIDERS', SG.ID, 1,
9902, '*', 'system', 'AI_ASSISTANT/AI_PROVIDERS',
'Anthropic/OpenAI/Google/Ollama 프로바이더 관리', 'Y', 'cloud'
FROM SCREEN_GROUPS SG
WHERE SG.GROUP_CODE = 'AI_ASSISTANT' AND SG.COMPANY_CODE = '*'
AND NOT EXISTS (
SELECT 1 FROM SCREEN_GROUPS WHERE GROUP_CODE = 'AI_PROVIDERS' AND COMPANY_CODE = '*'
);
-- 3-3. 멀티에이전트 워크스페이스
INSERT INTO SCREEN_GROUPS (
GROUP_NAME, GROUP_CODE, PARENT_GROUP_ID, GROUP_LEVEL,
DISPLAY_ORDER, COMPANY_CODE, WRITER, HIERARCHY_PATH, DESCRIPTION, IS_ACTIVE, ICON
)
SELECT
'멀티에이전트 워크스페이스', 'AI_WORKSPACE', SG.ID, 1,
9903, '*', 'system', 'AI_ASSISTANT/AI_WORKSPACE', '에이전트 그룹 조립 및 실행', 'Y', 'diagram-3'
FROM SCREEN_GROUPS SG
WHERE SG.GROUP_CODE = 'AI_ASSISTANT' AND SG.COMPANY_CODE = '*'
AND NOT EXISTS (
SELECT 1 FROM SCREEN_GROUPS WHERE GROUP_CODE = 'AI_WORKSPACE' AND COMPANY_CODE = '*'
);
-- 3-4. 대화 모니터링
INSERT INTO SCREEN_GROUPS (
GROUP_NAME, GROUP_CODE, PARENT_GROUP_ID, GROUP_LEVEL,
DISPLAY_ORDER, COMPANY_CODE, WRITER, HIERARCHY_PATH, DESCRIPTION, IS_ACTIVE, ICON
)
SELECT
'대화 모니터링', 'AI_CONVERSATIONS', SG.ID, 1,
9904, '*', 'system', 'AI_ASSISTANT/AI_CONVERSATIONS', '에이전트 대화 메시지 열람', 'Y', 'chat-dots'
FROM SCREEN_GROUPS SG
WHERE SG.GROUP_CODE = 'AI_ASSISTANT' AND SG.COMPANY_CODE = '*'
AND NOT EXISTS (
SELECT 1 FROM SCREEN_GROUPS WHERE GROUP_CODE = 'AI_CONVERSATIONS' AND COMPANY_CODE = '*'
);
-- 3-5. API 키 관리
INSERT INTO SCREEN_GROUPS (
GROUP_NAME, GROUP_CODE, PARENT_GROUP_ID, GROUP_LEVEL,
DISPLAY_ORDER, COMPANY_CODE, WRITER, HIERARCHY_PATH, DESCRIPTION, IS_ACTIVE, ICON
)
SELECT
'API 키 관리', 'AI_API_KEYS', SG.ID, 1,
9905, '*', 'system', 'AI_ASSISTANT/AI_API_KEYS', 'sk-pipe-* API 키 발급 및 폐기', 'Y', 'key'
FROM SCREEN_GROUPS SG
WHERE SG.GROUP_CODE = 'AI_ASSISTANT' AND SG.COMPANY_CODE = '*'
AND NOT EXISTS (
SELECT 1 FROM SCREEN_GROUPS WHERE GROUP_CODE = 'AI_API_KEYS' AND COMPANY_CODE = '*'
);
-- 3-6. 지식 라이브러리
INSERT INTO SCREEN_GROUPS (
GROUP_NAME, GROUP_CODE, PARENT_GROUP_ID, GROUP_LEVEL,
DISPLAY_ORDER, COMPANY_CODE, WRITER, HIERARCHY_PATH, DESCRIPTION, IS_ACTIVE, ICON
)
SELECT
'지식 라이브러리', 'AI_KNOWLEDGE', SG.ID, 1,
9906, '*', 'system', 'AI_ASSISTANT/AI_KNOWLEDGE', '지식 파일 업로드 및 관리', 'Y', 'book'
FROM SCREEN_GROUPS SG
WHERE SG.GROUP_CODE = 'AI_ASSISTANT' AND SG.COMPANY_CODE = '*'
AND NOT EXISTS (
SELECT 1 FROM SCREEN_GROUPS WHERE GROUP_CODE = 'AI_KNOWLEDGE' AND COMPANY_CODE = '*'
);
-- ══════════════════════════════════════════════════════════════
-- 4. MENU_INFO: 하위 메뉴 7개 (MENU_TYPE='0', 관리자 메뉴)
-- ══════════════════════════════════════════════════════════════
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT 'AI_MENU_ASSISTANT', '0', 'AI_ASSISTANT_ROOT', 'AI 어시스턴트',
'/admin/aiAssistant', 'AI 어시스턴트 대시보드 (워크스페이스 리다이렉트)',
9901, 'system', NOW(), 'active', '*', 'robot'
WHERE NOT EXISTS (SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_MENU_ASSISTANT');
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT 'AI_MENU_AGENTS', '0', 'AI_ASSISTANT_ROOT', '에이전트 관리',
'/admin/aiAssistant/agents', 'LLM 에이전트 CRUD',
9902, 'system', NOW(), 'active', '*', 'cpu'
WHERE NOT EXISTS (SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_MENU_AGENTS');
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT 'AI_MENU_PROVIDERS', '0', 'AI_ASSISTANT_ROOT', 'LLM 프로바이더',
'/admin/aiAssistant/providers', 'Anthropic/OpenAI/Google/Ollama 프로바이더 설정',
9903, 'system', NOW(), 'active', '*', 'cloud'
WHERE NOT EXISTS (SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_MENU_PROVIDERS');
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT 'AI_MENU_WORKSPACE', '0', 'AI_ASSISTANT_ROOT', '멀티에이전트 워크스페이스',
'/admin/aiAssistant/workspace', '에이전트 그룹 조립 및 실행',
9904, 'system', NOW(), 'active', '*', 'diagram-3'
WHERE NOT EXISTS (SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_MENU_WORKSPACE');
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT 'AI_MENU_CONVERSATIONS', '0', 'AI_ASSISTANT_ROOT', '대화 모니터링',
'/admin/aiAssistant/conversations', '에이전트 대화 메시지 열람',
9905, 'system', NOW(), 'active', '*', 'chat-dots'
WHERE NOT EXISTS (SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_MENU_CONVERSATIONS');
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT 'AI_MENU_API_KEYS', '0', 'AI_ASSISTANT_ROOT', 'API 키 관리',
'/admin/aiAssistant/api-keys-manage', 'sk-pipe-* API 키 발급 및 폐기',
9906, 'system', NOW(), 'active', '*', 'key'
WHERE NOT EXISTS (SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_MENU_API_KEYS');
INSERT INTO MENU_INFO (
OBJID, MENU_TYPE, PARENT_OBJ_ID, MENU_NAME_KOR,
MENU_URL, MENU_DESC, SEQ, WRITER, CREATED_DATE, STATUS, COMPANY_CODE, MENU_ICON
)
SELECT 'AI_MENU_KNOWLEDGE', '0', 'AI_ASSISTANT_ROOT', '지식 라이브러리',
'/admin/aiAssistant/knowledge', '지식 파일 업로드 및 관리',
9907, 'system', NOW(), 'active', '*', 'book'
WHERE NOT EXISTS (SELECT 1 FROM MENU_INFO WHERE OBJID = 'AI_MENU_KNOWLEDGE');
-- ══════════════════════════════════════════════════════════════
-- 5. SCREEN_GROUPS.MENU_OBJID 연결 (루트 그룹 → 루트 메뉴)
-- ══════════════════════════════════════════════════════════════
UPDATE SCREEN_GROUPS
SET MENU_OBJID = 'AI_ASSISTANT_ROOT'
WHERE GROUP_CODE = 'AI_ASSISTANT'
AND COMPANY_CODE = '*'
AND MENU_OBJID IS NULL;
+4 -4
View File
@@ -22,7 +22,7 @@ services:
- backend_data:/app/data - backend_data:/app/data
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.backend.rule=Host(`api.invion.com`) - traefik.http.routers.backend.rule=Host(`api.invyone.com`)
- traefik.http.routers.backend.entrypoints=websecure,web - traefik.http.routers.backend.entrypoints=websecure,web
- traefik.http.routers.backend.tls=true - traefik.http.routers.backend.tls=true
- traefik.http.routers.backend.tls.certresolver=le - traefik.http.routers.backend.tls.certresolver=le
@@ -34,12 +34,12 @@ services:
context: ../../frontend context: ../../frontend
dockerfile: ../docker/deploy/frontend.Dockerfile dockerfile: ../docker/deploy/frontend.Dockerfile
args: args:
- NEXT_PUBLIC_API_URL=https://api.invion.com/api - NEXT_PUBLIC_API_URL=https://api.invyone.com/api
container_name: pms-frontend-prod container_name: pms-frontend-prod
restart: always restart: always
environment: environment:
NODE_ENV: production NODE_ENV: production
NEXT_PUBLIC_API_URL: https://api.invion.com/api NEXT_PUBLIC_API_URL: https://api.invyone.com/api
SERVER_API_URL: "http://backend:8081" SERVER_API_URL: "http://backend:8081"
NEXT_TELEMETRY_DISABLED: "1" NEXT_TELEMETRY_DISABLED: "1"
PORT: "3000" PORT: "3000"
@@ -48,7 +48,7 @@ services:
- frontend_data:/app/data - frontend_data:/app/data
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.frontend.rule=Host(`v1.invion.com`) - traefik.http.routers.frontend.rule=Host(`v1.invyone.com`)
- traefik.http.routers.frontend.entrypoints=websecure,web - traefik.http.routers.frontend.entrypoints=websecure,web
- traefik.http.routers.frontend.tls=true - traefik.http.routers.frontend.tls=true
- traefik.http.routers.frontend.tls.certresolver=le - traefik.http.routers.frontend.tls.certresolver=le
+64
View File
@@ -0,0 +1,64 @@
# 도메인 매핑 (운영/개발 환경)
> ⚠️ AI 에이전트와 신규 개발자가 헷갈리지 않도록 영구 기록.
> 변경 시 반드시 본 문서 + `.cursor/rules/api-client-usage.mdc` + `.cursor/rules/project-conventions.mdc` 를 함께 갱신.
## 운영 환경
| 역할 | 도메인 | 비고 |
|---|---|---|
| Frontend (정식) | `solution.invyone.com` | 사용자 진입 URL |
| Frontend (alias) | `v1.invyone.com` | 같은 frontend, 백업/구버전 alias |
| Backend Spring API | `api.invyone.com` | `https://api.invyone.com/api/...` |
| Backend Node API | `node-api.invyone.com` | (legacy / 일부 endpoint) |
DNS 검증 (확인 일자: 2026-04-28)
- `solution.invyone.com` → 200
- `v1.invyone.com` → 200
- `api.invyone.com` → 200
- `node-api.invyone.com` → 200
## 폐기된 도메인 (사용 금지)
| 도메인 | 상태 |
|---|---|
| `v1.invion.com` | DNS 미존재. 이전 코드/문서에 박혀 있어 2026-04-28 일괄 정리 |
| `api.invion.com` | 동일 |
| `node-api.invion.com` | 동일 |
| `solution.invion.com` | 동일 |
> "invion" → "invyone" 일괄 치환 완료. 신규 코드/문서는 절대 `invion.com` 사용 금지.
## 개발 환경
| 역할 | 주소 | 비고 |
|---|---|---|
| Frontend dev | `http://localhost:9771` | docker-compose.frontend.win.yml |
| Backend Spring | `http://localhost:8081` | docker-compose.backend.win.yml |
## 인증 정보
### 로컬 (`.cursor/rules/web-verify-login.mdc`)
- 아이디: `wace` / 비밀번호: `qlalfqjsgh11`**로컬 전용**
### 운영 (`solution.invyone.com`)
- 운영 비밀번호는 코드/문서에 평문 저장 금지.
- 사용자에게 직접 문의해서 입력 받을 것.
- AI 에이전트는 로컬 비밀번호로 운영 로그인 시도 X.
## API 호출 흐름
```
브라우저 (solution.invyone.com)
↓ apiClient (lib/api/client.ts)
↓ baseURL = https://${currentHost}/api (1순위, *.invyone.com 자동 매칭)
운영 Traefik (k8s/traefik-dynamic.yaml)
↓ Host(`solution.invyone.com`) && PathPrefix(`/api`) → backend
Spring Boot (api.invyone.com 직접 OR Traefik 내부)
```
`apiClient` 우선순위:
1. `*.invyone.com` 호스트 → `https://${currentHost}/api` (서브도메인 테넌트 대응)
2. `v1.invyone.com` / `solution.invyone.com` 직접 매칭 → `https://api.invyone.com/api`
3. `NEXT_PUBLIC_API_URL` 환경변수
4. `http://localhost:8081/api` (로컬 기본)
+2 -2
View File
@@ -241,8 +241,8 @@ cors:
if (currentHost.endsWith(".invyone.com")) { if (currentHost.endsWith(".invyone.com")) {
return `https://${currentHost}/api`; return `https://${currentHost}/api`;
} }
// 2) (레거시) invion.com 메인 도메인 // 2) (레거시) invyone.com 메인 도메인
if (currentHost === "v1.invion.com") return "https://api.invion.com/api"; if (currentHost === "v1.invyone.com") return "https://api.invyone.com/api";
// 3) NEXT_PUBLIC_API_URL (docker-compose 주입) // 3) NEXT_PUBLIC_API_URL (docker-compose 주입)
// 4) localhost 기본값 // 4) localhost 기본값
``` ```
+1 -1
View File
@@ -327,7 +327,7 @@ const res = await getFlowDefinitions(); // ✅
| 환경 | 프론트엔드 | 백엔드 API | | 환경 | 프론트엔드 | 백엔드 API |
|------|-----------|-----------| |------|-----------|-----------|
| 로컬 개발 | localhost:9771 | localhost:8080/api | | 로컬 개발 | localhost:9771 | localhost:8080/api |
| 운영 | v1.invion.com | api.invion.com/api | | 운영 | v1.invyone.com | api.invyone.com/api |
### 5.5 상태 관리 체계 ### 5.5 상태 관리 체계
@@ -1695,8 +1695,8 @@ const getCorsOrigin = () => {
return [ return [
'http://localhost:9771', 'http://localhost:9771',
'http://39.117.244.52:5555', 'http://39.117.244.52:5555',
'https://v1.invion.com', 'https://v1.invyone.com',
'https://api.invion.com' 'https://api.invyone.com'
]; ];
}; };
+3 -3
View File
@@ -699,9 +699,9 @@ const getApiBaseUrl = (): string => {
// 환경변수 우선 // 환경변수 우선
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL; if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
// 프로덕션: v1.invion.com → api.invion.com // 프로덕션: v1.invyone.com → api.invyone.com
if (currentHost === "v1.invion.com") { if (currentHost === "v1.invyone.com") {
return "https://api.invion.com/api"; return "https://api.invyone.com/api";
} }
// 로컬: localhost:9771 → localhost:8080 // 로컬: localhost:9771 → localhost:8080
+1 -1
View File
@@ -535,7 +535,7 @@ html:not(.dark) .neb-4{width:600px;height:600px;top:-10%;right:20%;bottom:auto;
<div class="avatar-dd" id="avatar-dd"> <div class="avatar-dd" id="avatar-dd">
<div class="av-profile"> <div class="av-profile">
<div class="av-avatar">P</div> <div class="av-avatar">P</div>
<div><div class="av-name">Park님</div><div class="av-email">park@invion.com</div></div> <div><div class="av-name">Park님</div><div class="av-email">park@invyone.com</div></div>
</div> </div>
<div class="av-item"><span class="av-ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span>내 정보</div> <div class="av-item"><span class="av-ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span>내 정보</div>
<div class="av-item"><span class="av-ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>비밀번호 변경</div> <div class="av-item"><span class="av-ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>비밀번호 변경</div>
+6 -5
View File
@@ -24,9 +24,10 @@ const getApiBaseUrl = (): string => {
return `https://${currentHost}/api`; return `https://${currentHost}/api`;
} }
// 2. 프로덕션 메인 도메인 // 2. 프로덕션 메인 도메인 fallback (1번에서 endsWith 로 이미 처리되므로 dead-code 가깝지만,
if (currentHost === "v1.invion.com") { // invyone.com 루트 도메인 등 예외 케이스 보호용으로 유지)
return "https://api.invion.com/api"; if (currentHost === "v1.invyone.com" || currentHost === "solution.invyone.com") {
return "https://api.invyone.com/api";
} }
} }
@@ -53,8 +54,8 @@ export const getFullImageUrl = (imagePath: string): string => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const currentHost = window.location.hostname; const currentHost = window.location.hostname;
if (currentHost === "v1.invion.com") { if (currentHost.endsWith(".invyone.com")) {
return `https://api.invion.com${imagePath}`; return `https://api.invyone.com${imagePath}`;
} }
if (currentHost === "localhost" || currentHost === "127.0.0.1") { if (currentHost === "localhost" || currentHost === "127.0.0.1") {
+3 -3
View File
@@ -10,9 +10,9 @@ function getApiBaseUrl(): string {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const hostname = window.location.hostname; const hostname = window.location.hostname;
// 프로덕션: v1.invion.com → https://api.invion.com/api // 프로덕션: v1.invyone.com → https://api.invyone.com/api
if (hostname === "v1.invion.com") { if (hostname === "v1.invyone.com") {
return "https://api.invion.com/api"; return "https://api.invyone.com/api";
} }
// 로컬 개발: localhost → http://localhost:8081/api // 로컬 개발: localhost → http://localhost:8081/api
+4 -4
View File
@@ -262,9 +262,9 @@ export const getDirectFileUrl = (filePath: string): string => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const currentHost = window.location.hostname; const currentHost = window.location.hostname;
// 프로덕션 환경: v1.invion.com → api.invion.com // 프로덕션 환경: v1.invyone.com → api.invyone.com
if (currentHost === "v1.invion.com") { if (currentHost === "v1.invyone.com") {
return `https://api.invion.com${filePath}`; return `https://api.invyone.com${filePath}`;
} }
// 로컬 개발환경 // 로컬 개발환경
@@ -274,7 +274,7 @@ export const getDirectFileUrl = (filePath: string): string => {
} }
// SSR 또는 알 수 없는 환경에서는 환경변수 사용 (fallback) // SSR 또는 알 수 없는 환경에서는 환경변수 사용 (fallback)
// 주의: 프로덕션 URL이 https://api.invion.com/api 이므로 // 주의: 프로덕션 URL이 https://api.invyone.com/api 이므로
// 단순 .replace("/api", "")는 호스트명의 /api까지 제거하는 버그 발생 // 단순 .replace("/api", "")는 호스트명의 /api까지 제거하는 버그 발생
const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace(/\/api$/, "") || ""; const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace(/\/api$/, "") || "";
if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) { if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) {
+3 -3
View File
@@ -30,9 +30,9 @@ const getApiBaseUrl = (): string => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const currentHost = window.location.hostname; const currentHost = window.location.hostname;
// 프로덕션 환경: v1.invion.com → api.invion.com // 프로덕션 환경: v1.invyone.com → api.invyone.com
if (currentHost === "v1.invion.com") { if (currentHost === "v1.invyone.com") {
return "https://api.invion.com/api"; return "https://api.invyone.com/api";
} }
// 로컬 개발환경 // 로컬 개발환경
@@ -284,7 +284,7 @@ export const FileViewerModal: React.FC<FileViewerModalProps> = ({ file, isOpen,
} }
} else { } else {
// 기타 파일은 다운로드 URL 사용 // 기타 파일은 다운로드 URL 사용
// 주의: 프로덕션 URL이 https://api.invion.com/api 이므로 // 주의: 프로덕션 URL이 https://api.invyone.com/api 이므로
// 끝의 /api만 제거해야 호스트명이 깨지지 않음 // 끝의 /api만 제거해야 호스트명이 깨지지 않음
const url = `${API_BASE_URL.replace(/\/api$/, "")}/api/files/download/${file.objid}`; const url = `${API_BASE_URL.replace(/\/api$/, "")}/api/files/download/${file.objid}`;
setPreviewUrl(url); setPreviewUrl(url);
@@ -284,7 +284,7 @@ export const FileViewerModal: React.FC<FileViewerModalProps> = ({ file, isOpen,
} }
} else { } else {
// 기타 파일은 다운로드 URL 사용 // 기타 파일은 다운로드 URL 사용
// 주의: 프로덕션 URL이 https://api.invion.com/api 이므로 // 주의: 프로덕션 URL이 https://api.invyone.com/api 이므로
// 끝의 /api만 제거해야 호스트명이 깨지지 않음 // 끝의 /api만 제거해야 호스트명이 깨지지 않음
const url = `${API_BASE_URL.replace(/\/api$/, "")}/api/files/download/${file.objid}`; const url = `${API_BASE_URL.replace(/\/api$/, "")}/api/files/download/${file.objid}`;
setPreviewUrl(url); setPreviewUrl(url);
+3 -3
View File
@@ -8,9 +8,9 @@ export function getApiUrl(endpoint: string): string {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const hostname = window.location.hostname; const hostname = window.location.hostname;
// 프로덕션: v1.invion.com → https://api.invion.com // 프로덕션: *.invyone.com (solution, v1 등 모든 alias) → https://api.invyone.com
if (hostname === "v1.invion.com") { if (hostname.endsWith(".invyone.com")) {
return `https://api.invion.com${endpoint}`; return `https://api.invyone.com${endpoint}`;
} }
// 로컬 개발: localhost → http://localhost:8081 // 로컬 개발: localhost → http://localhost:8081
+3 -3
View File
@@ -6,7 +6,7 @@ http:
routers: routers:
# Frontend (Next.js) # Frontend (Next.js)
invyone-frontend: invyone-frontend:
rule: "Host(`v1.invion.com`)" rule: "Host(`v1.invyone.com`)"
entryPoints: entryPoints:
- web - web
- websecure - websecure
@@ -16,7 +16,7 @@ http:
# Backend Spring Boot API # Backend Spring Boot API
invyone-api: invyone-api:
rule: "Host(`api.invion.com`)" rule: "Host(`api.invyone.com`)"
entryPoints: entryPoints:
- web - web
- websecure - websecure
@@ -26,7 +26,7 @@ http:
# Backend Node.js API (필요시) # Backend Node.js API (필요시)
invyone-node-api: invyone-node-api:
rule: "Host(`node-api.invion.com`)" rule: "Host(`node-api.invyone.com`)"
entryPoints: entryPoints:
- web - web
- websecure - websecure
@@ -70,7 +70,7 @@ config.setAllowedOriginPatterns(List.of("*"));
config.setAllowCredentials(true); config.setAllowCredentials(true);
``` ```
`*` + `allowCredentials=true` 조합은 모든 출처에서 쿠키 포함 요청을 허용. `*` + `allowCredentials=true` 조합은 모든 출처에서 쿠키 포함 요청을 허용.
**조치:** 운영용 화이트리스트 (`v1.invion.com`, 사내 IP 등) 로 좁힐 것. **조치:** 운영용 화이트리스트 (`v1.invyone.com`, 사내 IP 등) 로 좁힐 것.
### 6. JWT 를 localStorage 저장 + 비-HttpOnly 쿠키 중복 ### 6. JWT 를 localStorage 저장 + 비-HttpOnly 쿠키 중복
**파일:** `frontend/lib/api/client.ts:83-87`, `frontend/hooks/useAuth.ts:58-64` **파일:** `frontend/lib/api/client.ts:83-87`, `frontend/hooks/useAuth.ts:58-64`
@@ -154,7 +154,7 @@ const strictProtectedPaths = ["/admin"];
## 🟢 자잘한 정리 ## 🟢 자잘한 정리
- **`frontend/lib/api/client.ts:14-36`** — API base URL 결정 로직이 환경변수 + hostname 분기 + 포트 분기로 길어짐. hostname 하드코딩 (`v1.invion.com`) 은 환경변수로 분리 권장. - **`frontend/lib/api/client.ts:14-36`** — API base URL 결정 로직이 환경변수 + hostname 분기 + 포트 분기로 길어짐. hostname 하드코딩 (`v1.invyone.com`) 은 환경변수로 분리 권장.
- **`useAuth.ts:196`, `AuthService.java:273`** — `"plm_admin" === userId` 식의 매직 ID 가 프론트/백 양쪽에 박힘. role 기반으로 일원화. - **`useAuth.ts:196`, `AuthService.java:273`** — `"plm_admin" === userId` 식의 매직 ID 가 프론트/백 양쪽에 박힘. role 기반으로 일원화.
- **`JwtAuthenticationFilter`** — 토큰 만료/위조 시 401 응답 없이 그냥 다음 필터로 넘김 (#4 결함과 맞물려 동작 불일치). - **`JwtAuthenticationFilter`** — 토큰 만료/위조 시 401 응답 없이 그냥 다음 필터로 넘김 (#4 결함과 맞물려 동작 불일치).
- **`frontend/hooks/useLogin.ts:81-95`** — `checkExistingAuth` 가 401 받으면 인터셉터의 자동 redirectToLogin 과 충돌 여지. 현재는 `pathname === "/login"` 가드로 막혀있긴 함. - **`frontend/hooks/useLogin.ts:81-95`** — `checkExistingAuth` 가 401 받으면 인터셉터의 자동 redirectToLogin 과 충돌 여지. 현재는 `pathname === "/login"` 가드로 막혀있긴 함.
+1 -1
View File
@@ -92,7 +92,7 @@ WHERE table_name = 'user_info' AND column_name = 'user_password';
- 현재는 **dev 도커** (`docker/dev/.env`) 만 작성 - 현재는 **dev 도커** (`docker/dev/.env`) 만 작성
- 운영 도커 컴포즈 (`docker/prod/...`) 가 있다면 거기에도 동일한 패턴으로 `.env` + secret + CORS 화이트리스트 적용 필요 - 운영 도커 컴포즈 (`docker/prod/...`) 가 있다면 거기에도 동일한 패턴으로 `.env` + secret + CORS 화이트리스트 적용 필요
- 운영 CORS 화이트리스트: `https://v1.invion.com,https://api.invion.com` 식으로 변경 - 운영 CORS 화이트리스트: `https://v1.invyone.com,https://api.invyone.com` 식으로 변경
### D. 마스터 패스워드 의존성 확인 ### D. 마스터 패스워드 의존성 확인
@@ -799,7 +799,7 @@ html.dark .v5-cm-tg{background:var(--v5-cm-sunk);}
</div> </div>
<div class="v5-cm-form-row"> <div class="v5-cm-form-row">
<label>도메인</label> <label>도메인</label>
<input class="v5-cm-inp" value="v1.invion.com" /> <input class="v5-cm-inp" value="v1.invyone.com" />
</div> </div>
</div> </div>
<div class="v5-cm-form-row"> <div class="v5-cm-form-row">
+2 -2
View File
@@ -51,8 +51,8 @@ echo "======================================"
echo "배포 완료!" echo "배포 완료!"
echo "======================================" echo "======================================"
echo "" echo ""
echo "Frontend: https://v1.invion.com" echo "Frontend: https://v1.invyone.com"
echo "Backend: https://api.invion.com" echo "Backend: https://api.invyone.com"
echo "" echo ""
docker-compose -f "$COMPOSE_FILE" ps docker-compose -f "$COMPOSE_FILE" ps
echo "" echo ""