feat: V2 WebView 컴포넌트 + SSO 연동 구현
- V2WebView 컴포넌트: iframe 기반 외부 웹 임베딩 - SSO 연동: 현재 로그인 JWT를 sso_token 파라미터로 자동 전달 - /api/system/raw-token: 범용 JWT 토큰 조회 API - V2WebViewConfigPanel: URL, SSO, sandbox 등 설정 UI + 개발자 가이드 Made-with: Cursor
This commit is contained in:
328
.omc/project-memory.json
Normal file
328
.omc/project-memory.json
Normal file
@@ -0,0 +1,328 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"lastScanned": 1772609393905,
|
||||
"projectRoot": "/Users/johngreen/Dev/vexplor",
|
||||
"techStack": {
|
||||
"languages": [
|
||||
{
|
||||
"name": "JavaScript/TypeScript",
|
||||
"version": null,
|
||||
"confidence": "high",
|
||||
"markers": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"frameworks": [],
|
||||
"packageManager": "npm",
|
||||
"runtime": null
|
||||
},
|
||||
"build": {
|
||||
"buildCommand": null,
|
||||
"testCommand": null,
|
||||
"lintCommand": null,
|
||||
"devCommand": null,
|
||||
"scripts": {}
|
||||
},
|
||||
"conventions": {
|
||||
"namingStyle": null,
|
||||
"importStyle": null,
|
||||
"testPattern": null,
|
||||
"fileOrganization": "type-based"
|
||||
},
|
||||
"structure": {
|
||||
"isMonorepo": false,
|
||||
"workspaces": [],
|
||||
"mainDirectories": [
|
||||
"docs",
|
||||
"lib",
|
||||
"scripts",
|
||||
"src"
|
||||
],
|
||||
"gitBranches": {
|
||||
"defaultBranch": "main",
|
||||
"branchingStrategy": null
|
||||
}
|
||||
},
|
||||
"customNotes": [],
|
||||
"directoryMap": {
|
||||
"WebContent": {
|
||||
"path": "WebContent",
|
||||
"purpose": null,
|
||||
"fileCount": 5,
|
||||
"lastAccessed": 1772609393856,
|
||||
"keyFiles": [
|
||||
"init.jsp",
|
||||
"init_jqGrid.jsp",
|
||||
"init_no_login.jsp",
|
||||
"init_toastGrid.jsp",
|
||||
"viewImage.jsp"
|
||||
]
|
||||
},
|
||||
"backend": {
|
||||
"path": "backend",
|
||||
"purpose": null,
|
||||
"fileCount": 6,
|
||||
"lastAccessed": 1772609393857,
|
||||
"keyFiles": [
|
||||
"Dockerfile",
|
||||
"Dockerfile.mac",
|
||||
"build.gradle",
|
||||
"gradlew",
|
||||
"gradlew.bat"
|
||||
]
|
||||
},
|
||||
"backend-node": {
|
||||
"path": "backend-node",
|
||||
"purpose": null,
|
||||
"fileCount": 14,
|
||||
"lastAccessed": 1772609393872,
|
||||
"keyFiles": [
|
||||
"API_연동_가이드.md",
|
||||
"API_키_정리.md",
|
||||
"Dockerfile.win",
|
||||
"PHASE1_USAGE_GUIDE.md",
|
||||
"README.md"
|
||||
]
|
||||
},
|
||||
"db": {
|
||||
"path": "db",
|
||||
"purpose": null,
|
||||
"fileCount": 2,
|
||||
"lastAccessed": 1772609393873,
|
||||
"keyFiles": [
|
||||
"00-create-roles.sh",
|
||||
"migrate_company13_export.sh"
|
||||
]
|
||||
},
|
||||
"deploy": {
|
||||
"path": "deploy",
|
||||
"purpose": null,
|
||||
"fileCount": 0,
|
||||
"lastAccessed": 1772609393873,
|
||||
"keyFiles": []
|
||||
},
|
||||
"docker": {
|
||||
"path": "docker",
|
||||
"purpose": null,
|
||||
"fileCount": 0,
|
||||
"lastAccessed": 1772609393873,
|
||||
"keyFiles": []
|
||||
},
|
||||
"docs": {
|
||||
"path": "docs",
|
||||
"purpose": "Documentation",
|
||||
"fileCount": 23,
|
||||
"lastAccessed": 1772609393873,
|
||||
"keyFiles": [
|
||||
"AI_화면생성_시스템_설계서.md",
|
||||
"DB_ARCHITECTURE_ANALYSIS.md",
|
||||
"DB_STRUCTURE_DIAGRAM.html",
|
||||
"DB_WORKFLOW_ANALYSIS.md",
|
||||
"KUBERNETES_DEPLOYMENT_GUIDE.md"
|
||||
]
|
||||
},
|
||||
"frontend": {
|
||||
"path": "frontend",
|
||||
"purpose": null,
|
||||
"fileCount": 14,
|
||||
"lastAccessed": 1772609393873,
|
||||
"keyFiles": [
|
||||
"MODAL_REPEATER_TABLE_DEBUG.md",
|
||||
"README.md",
|
||||
"components.json",
|
||||
"eslint.config.mjs",
|
||||
"middleware.ts"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"path": "hooks",
|
||||
"purpose": null,
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393879,
|
||||
"keyFiles": [
|
||||
"useScreenStandards.ts"
|
||||
]
|
||||
},
|
||||
"k8s": {
|
||||
"path": "k8s",
|
||||
"purpose": null,
|
||||
"fileCount": 7,
|
||||
"lastAccessed": 1772609393882,
|
||||
"keyFiles": [
|
||||
"local-path-provisioner.yaml",
|
||||
"namespace.yaml",
|
||||
"vexplor-backend-deployment.yaml",
|
||||
"vexplor-config.yaml",
|
||||
"vexplor-frontend-deployment.yaml"
|
||||
]
|
||||
},
|
||||
"lib": {
|
||||
"path": "lib",
|
||||
"purpose": "Library code",
|
||||
"fileCount": 0,
|
||||
"lastAccessed": 1772609393883,
|
||||
"keyFiles": []
|
||||
},
|
||||
"mcp-agent-orchestrator": {
|
||||
"path": "mcp-agent-orchestrator",
|
||||
"purpose": null,
|
||||
"fileCount": 4,
|
||||
"lastAccessed": 1772609393883,
|
||||
"keyFiles": [
|
||||
"README.md",
|
||||
"package-lock.json",
|
||||
"package.json",
|
||||
"tsconfig.json"
|
||||
]
|
||||
},
|
||||
"popdocs": {
|
||||
"path": "popdocs",
|
||||
"purpose": null,
|
||||
"fileCount": 12,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": [
|
||||
"ARCHITECTURE.md",
|
||||
"CHANGELOG.md",
|
||||
"FILES.md",
|
||||
"INDEX.md",
|
||||
"PLAN.md"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"path": "scripts",
|
||||
"purpose": "Build/utility scripts",
|
||||
"fileCount": 2,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": [
|
||||
"add-modal-ids.py",
|
||||
"remove-logs.js"
|
||||
]
|
||||
},
|
||||
"src": {
|
||||
"path": "src",
|
||||
"purpose": "Source code",
|
||||
"fileCount": 0,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": []
|
||||
},
|
||||
"tomcat-conf": {
|
||||
"path": "tomcat-conf",
|
||||
"purpose": null,
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": [
|
||||
"context.xml"
|
||||
]
|
||||
},
|
||||
"backend/build": {
|
||||
"path": "backend/build",
|
||||
"purpose": "Build output",
|
||||
"fileCount": 0,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": []
|
||||
},
|
||||
"backend/src": {
|
||||
"path": "backend/src",
|
||||
"purpose": "Source code",
|
||||
"fileCount": 0,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": []
|
||||
},
|
||||
"backend-node/data": {
|
||||
"path": "backend-node/data",
|
||||
"purpose": "Data files",
|
||||
"fileCount": 0,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": []
|
||||
},
|
||||
"db/migrations": {
|
||||
"path": "db/migrations",
|
||||
"purpose": "Database migrations",
|
||||
"fileCount": 16,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": [
|
||||
"046_MIGRATION_FIX.md",
|
||||
"046_QUICK_FIX.md",
|
||||
"README_1003.md"
|
||||
]
|
||||
},
|
||||
"db/scripts": {
|
||||
"path": "db/scripts",
|
||||
"purpose": "Build/utility scripts",
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393884,
|
||||
"keyFiles": [
|
||||
"README_cleanup.md"
|
||||
]
|
||||
},
|
||||
"frontend/app": {
|
||||
"path": "frontend/app",
|
||||
"purpose": "Application code",
|
||||
"fileCount": 5,
|
||||
"lastAccessed": 1772609393885,
|
||||
"keyFiles": [
|
||||
"favicon.ico",
|
||||
"globals.css",
|
||||
"layout.tsx"
|
||||
]
|
||||
},
|
||||
"frontend/components": {
|
||||
"path": "frontend/components",
|
||||
"purpose": "UI components",
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393885,
|
||||
"keyFiles": [
|
||||
"GlobalFileViewer.tsx"
|
||||
]
|
||||
},
|
||||
"mcp-agent-orchestrator/src": {
|
||||
"path": "mcp-agent-orchestrator/src",
|
||||
"purpose": "Source code",
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393885,
|
||||
"keyFiles": [
|
||||
"index.ts"
|
||||
]
|
||||
},
|
||||
"src/controllers": {
|
||||
"path": "src/controllers",
|
||||
"purpose": "Controllers",
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393885,
|
||||
"keyFiles": [
|
||||
"dataflowDiagramController.ts"
|
||||
]
|
||||
},
|
||||
"src/routes": {
|
||||
"path": "src/routes",
|
||||
"purpose": "Route handlers",
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393885,
|
||||
"keyFiles": [
|
||||
"dataflowDiagramRoutes.ts"
|
||||
]
|
||||
},
|
||||
"src/services": {
|
||||
"path": "src/services",
|
||||
"purpose": "Business logic services",
|
||||
"fileCount": 1,
|
||||
"lastAccessed": 1772609393885,
|
||||
"keyFiles": [
|
||||
"dataflowDiagramService.ts"
|
||||
]
|
||||
},
|
||||
"src/utils": {
|
||||
"path": "src/utils",
|
||||
"purpose": "Utility functions",
|
||||
"fileCount": 2,
|
||||
"lastAccessed": 1772609393885,
|
||||
"keyFiles": [
|
||||
"databaseValidator.ts",
|
||||
"queryBuilder.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"hotPaths": [],
|
||||
"userDirectives": []
|
||||
}
|
||||
8
.omc/sessions/591d357c-df9d-4bbc-8dfa-1b98a9184e23.json
Normal file
8
.omc/sessions/591d357c-df9d-4bbc-8dfa-1b98a9184e23.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"session_id": "591d357c-df9d-4bbc-8dfa-1b98a9184e23",
|
||||
"ended_at": "2026-03-04T08:10:16.810Z",
|
||||
"reason": "other",
|
||||
"agents_spawned": 0,
|
||||
"agents_completed": 0,
|
||||
"modes_used": []
|
||||
}
|
||||
6
.omc/state/hud-state.json
Normal file
6
.omc/state/hud-state.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"timestamp": "2026-03-04T07:29:57.315Z",
|
||||
"backgroundTasks": [],
|
||||
"sessionStartTimestamp": "2026-03-04T07:29:53.176Z",
|
||||
"sessionId": "591d357c-df9d-4bbc-8dfa-1b98a9184e23"
|
||||
}
|
||||
1
.omc/state/hud-stdin-cache.json
Normal file
1
.omc/state/hud-stdin-cache.json
Normal file
@@ -0,0 +1 @@
|
||||
{"session_id":"591d357c-df9d-4bbc-8dfa-1b98a9184e23","transcript_path":"/Users/johngreen/.claude/projects/-Users-johngreen-Dev-vexplor/591d357c-df9d-4bbc-8dfa-1b98a9184e23.jsonl","cwd":"/Users/johngreen/Dev/vexplor","model":{"id":"claude-opus-4-6","display_name":"Opus 4.6"},"workspace":{"current_dir":"/Users/johngreen/Dev/vexplor","project_dir":"/Users/johngreen/Dev/vexplor","added_dirs":[]},"version":"2.1.66","output_style":{"name":"default"},"cost":{"total_cost_usd":0.516748,"total_duration_ms":65256,"total_api_duration_ms":28107,"total_lines_added":0,"total_lines_removed":0},"context_window":{"total_input_tokens":604,"total_output_tokens":838,"context_window_size":200000,"current_usage":{"input_tokens":1,"output_tokens":277,"cache_creation_input_tokens":1836,"cache_read_input_tokens":55498},"used_percentage":29,"remaining_percentage":71},"exceeds_200k_tokens":false}
|
||||
3
.omc/state/idle-notif-cooldown.json
Normal file
3
.omc/state/idle-notif-cooldown.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"lastSentAt": "2026-03-04T07:30:30.883Z"
|
||||
}
|
||||
451
docs/AI_화면생성_시스템_설계서.md
Normal file
451
docs/AI_화면생성_시스템_설계서.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# AI 기반 화면 자동 생성 시스템 설계서
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목표
|
||||
|
||||
사용자가 자연어로 "자재입고페이지 만들어줘"라고 요청하면, AI가 자동으로 완성된 화면과 워크플로우를 생성
|
||||
|
||||
### 1.2 핵심 가치
|
||||
|
||||
- **제로 코딩**: 대화만으로 화면 생성
|
||||
- **워크플로우 포함**: 저장 시 재고 업데이트 등 자동 설정
|
||||
- **기존 시스템 활용**: 새 인프라 최소화, 기존 API 재사용
|
||||
|
||||
### 1.3 예상 시나리오
|
||||
|
||||
```
|
||||
사용자: "자재입고페이지 만들어줘"
|
||||
|
||||
AI: "입고관리 화면 41개를 분석했습니다.
|
||||
|
||||
✅ 자동 적용 (90%+ 사용)
|
||||
- 레이아웃: 분할화면 (좌: 목록, 우: 상세)
|
||||
- 필수 필드: 입고번호, 입고일자, 품목코드, 수량
|
||||
|
||||
💡 추천 기능 (60% 사용)
|
||||
- 저장 시 재고 자동 증가
|
||||
[추가하기]
|
||||
|
||||
➕ 선택 필드
|
||||
[ ] 창고 (68%) [ ] 공급업체 (61%)
|
||||
|
||||
[화면 생성하기]"
|
||||
|
||||
사용자: "재고 연동 추가하고, 창고 필드도 넣어줘"
|
||||
|
||||
AI: "화면을 생성했습니다.
|
||||
- 화면명: 자재 입고관리
|
||||
- 레이아웃: 분할화면
|
||||
- 필드: 입고번호, 입고일자, 품목, 수량, 창고
|
||||
- 워크플로우: 저장 시 inventory_stock +qty
|
||||
|
||||
[미리보기] [메뉴에 추가]"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 시스템 아키텍처
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┬───────────────────────────────┐
|
||||
│ 신규 개발 (AI 담당) │ 기존 드래그앤드랍 시스템 │
|
||||
│ ┌─────────────┐ ┌──────────────────────────────┐ │ ┌─────────────────────┐ │
|
||||
│ │ Chat UI │────▶│ AI 서비스 │ │ │ D&D UI Builder │ │
|
||||
│ └─────────────┘ │ • LLM 호출 (Claude/GPT) │ │ └─────────┬──────────┘ │
|
||||
│ │ • 패턴 분석 + RAG 검색 │ │ │ │
|
||||
│ │ • JSON 생성 │ │ ▼ │
|
||||
│ └──────────────┬──────────────┘ │ ┌─────────────────────┐ │
|
||||
└─────────────────────────────────────┼─────────────────────---──┴─┼─────────────────────┬─────┘
|
||||
│ 기존 API 호출 │ 기존 API 호출 │
|
||||
▼ ┼─────────────────────┘
|
||||
▼
|
||||
┌-------------------------------------------------------------------------------┐
|
||||
│ 기존 시스템 (vexplor) │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Screen API │ │ Flow API │ │ Dataflow API│ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────┴──────┐ │
|
||||
│ │ PostgreSQL │ │
|
||||
│ └─────────────┘ │
|
||||
└-------------------------------------------------------------------------------┘
|
||||
```
|
||||
|
||||
### 설계 원칙
|
||||
|
||||
| 원칙 | 설명 |
|
||||
| --------------------- | --------------------------------- |
|
||||
| 기존 코드 변경 최소화 | 기존 백엔드/프론트 수정 없음 |
|
||||
| 기존 API 재사용 | AI가 기존 API를 "사용자처럼" 호출 |
|
||||
| RAG 기반 지식 주입 | 필요한 패턴만 동적으로 LLM에 주입 |
|
||||
| **Assistive AI** | AI는 결정하지 않고, 사용자의 결정을 돕는다 |
|
||||
|
||||
---
|
||||
|
||||
## 3. AI가 화면을 "알아서" 만드는 방법
|
||||
|
||||
### 3.1 지식 소스
|
||||
|
||||
| 데이터 | 테이블 | 활용 |
|
||||
| --------------- | ---------------------------------- | ------------------- |
|
||||
| 기존 화면 | screen_definitions, screen_layouts | 패턴 학습 |
|
||||
| 테이블 라벨 | table_labels | 테이블 검색 |
|
||||
| 컬럼 라벨 | column_labels | 필드→컴포넌트 매핑 |
|
||||
| 워크플로우 패턴 | workflow_patterns (신규) | 비즈니스 로직 |
|
||||
|
||||
### 3.2 통계 기반 패턴 분석
|
||||
|
||||
> **핵심 아이디어**: 사용자들이 이미 만든 화면들을 분석해서 "입고 화면은 보통 이렇게 생겼더라"를 알아내는 것
|
||||
|
||||
#### 예시: "입고페이지 만들어줘" 요청 시
|
||||
|
||||
**Step 1. 테이블 찾기**
|
||||
|
||||
```sql
|
||||
-- "입고"라는 단어로 table_labels 검색
|
||||
SELECT table_name, table_label FROM table_labels
|
||||
WHERE table_label LIKE '%입고%';
|
||||
|
||||
-- 결과: inbound_mng (입고관리)
|
||||
```
|
||||
|
||||
**Step 2. 이 테이블을 쓰는 기존 화면 찾기**
|
||||
|
||||
```sql
|
||||
-- inbound_mng 테이블을 사용하는 화면들 조회
|
||||
SELECT * FROM screen_definitions
|
||||
WHERE table_name = 'inbound_mng';
|
||||
|
||||
-- 결과: 41개 화면 발견!
|
||||
```
|
||||
|
||||
**Step 3. 41개 화면의 "공통점" 분석**
|
||||
|
||||
레이아웃 통계:
|
||||
|
||||
| 레이아웃 | 개수 | 비율 |
|
||||
| -------------------------------- | ----- | -------- |
|
||||
| split-panel (좌: 목록, 우: 상세) | 32개 | **78%** |
|
||||
| table-list (목록만) | 6개 | 15% |
|
||||
| form (폼만) | 3개 | 7% |
|
||||
|
||||
필드 사용 통계:
|
||||
|
||||
| 필드 | 사용 화면 수 | 비율 |
|
||||
| ---------------------- | ------------ | -------- |
|
||||
| inbound_no (입고번호) | 41개 | **100%** |
|
||||
| inbound_date (입고일자)| 41개 | **100%** |
|
||||
| item_code (품목코드) | 40개 | **98%** |
|
||||
| qty (수량) | 39개 | **95%** |
|
||||
| warehouse_code (창고) | 28개 | 68% |
|
||||
| supplier_code (공급업체)| 25개 | 61% |
|
||||
|
||||
**Step 4. 확신도(Confidence)에 따라 다르게 처리**
|
||||
|
||||
AI는 통계 결과의 확신도에 따라 행동을 다르게 합니다:
|
||||
|
||||
| 확신도 | 기준 | AI 행동 | 예시 |
|
||||
| ------ | ---- | ------- | ---- |
|
||||
| **높음** | 90%+ | 자동 적용 | 필수 필드(입고번호, 일자) 자동 추가 |
|
||||
| **중간** | 60~90% | 추천하며 확인 | "분할화면으로 만들까요? (78% 사용)" |
|
||||
| **낮음** | 60% 미만 | 옵션 나열 | "창고 필드 추가할까요? (68%)" |
|
||||
|
||||
```
|
||||
AI 판단 예시:
|
||||
- 레이아웃: split-panel (78%) → "분할화면으로 생성합니다"
|
||||
- 필수 필드: 입고번호, 입고일자 (100%) → 자동 추가
|
||||
- 워크플로우: 입고→재고 (60%) → "💡 재고 자동 연동 추가할까요?"
|
||||
- 선택 필드: 창고 (68%) → "추가 필드: [ ] 창고 [ ] 공급업체"
|
||||
```
|
||||
|
||||
**핵심**: 확실한 것은 빠르게 처리하고, 애매한 것은 사용자에게 물어본다.
|
||||
|
||||
#### 비유: "맛집 추천 AI"와 같은 원리
|
||||
|
||||
| 맛집 추천 AI | 화면 생성 AI |
|
||||
| ------------ | ------------ |
|
||||
| "강남에서 점심 뭐 먹지?" | "입고페이지 만들어줘" |
|
||||
| 강남 식당 1000개 리뷰 분석 | 기존 입고 화면 41개 분석 |
|
||||
| "70%가 파스타집, 평균 1.5만원" | "78%가 분할화면, 100%가 입고번호 사용" |
|
||||
| "파스타집 추천드릴까요?" | "분할화면으로 만들까요?" |
|
||||
|
||||
### 3.3 RAG 기반 동적 지식 주입 (핵심)
|
||||
|
||||
> **문제**: 모든 도메인 지식을 프롬프트에 넣으면 Context Window 초과
|
||||
|
||||
> **해결**: 필요한 지식만 검색해서 동적 주입
|
||||
|
||||
```
|
||||
사용자: "입고페이지 만들어줘"
|
||||
↓
|
||||
1. 키워드 추출: "입고"
|
||||
↓
|
||||
2. workflow_patterns 검색 → "입고→재고 증가" 패턴 발견
|
||||
↓
|
||||
3. LLM 프롬프트에 해당 패턴만 주입
|
||||
↓
|
||||
4. 재고 증가 로직 포함된 화면 생성
|
||||
```
|
||||
|
||||
#### 장점
|
||||
|
||||
| 장점 | 설명 |
|
||||
| ------------- | ---------------------------------- |
|
||||
| 토큰 절약 | 관련 패턴 1-2개만 주입 |
|
||||
| 확장성 | 패턴 1000개여도 프롬프트 길이 동일 |
|
||||
| 회사별 커스텀 | company_code로 회사별 패턴 적용 |
|
||||
|
||||
### 3.4 멀티테넌트 & Fallback 전략
|
||||
|
||||
회사마다 테이블명이 다르거나, 신규 회사라 기존 화면이 없을 수 있습니다.
|
||||
|
||||
**데이터 검색 우선순위:**
|
||||
|
||||
```
|
||||
1순위: 해당 회사의 기존 화면 (가장 정확)
|
||||
↓ 없거나 부족하면
|
||||
2순위: 전체 회사의 익명화된 통계 (company_code 제외, 패턴만)
|
||||
↓ 그래도 부족하면
|
||||
3순위: vexplor 표준 템플릿 (기본 레이아웃 + 필수 필드)
|
||||
```
|
||||
|
||||
**여러 테이블이 검색될 때:**
|
||||
|
||||
```
|
||||
사용자: "입고페이지 만들어줘"
|
||||
|
||||
AI: "입고 관련 테이블이 3개 있습니다:
|
||||
1. 자재입고관리 (material_inbound)
|
||||
2. 제품입고관리 (product_inbound)
|
||||
3. 반품입고관리 (return_inbound)
|
||||
|
||||
어떤 테이블로 만들까요?"
|
||||
```
|
||||
|
||||
### 3.5 기존 시스템 분석 결과
|
||||
|
||||
**발견된 학습 가능 데이터:**
|
||||
|
||||
- `transferData` 액션: 14개 (발주→입고, 수주→출고 등)
|
||||
- 제어관리 프레임워크: `dataflowControlService.ts` 존재
|
||||
- 입고→재고 규칙: 아직 정의되지 않음 → AI가 생성하면 됨
|
||||
|
||||
---
|
||||
|
||||
## 4. 개발 범위
|
||||
|
||||
### 4.1 AI 담당 (신규)
|
||||
|
||||
| 작업 | 파일 | 우선순위 |
|
||||
| ------------------------------------ | ----------------------------- | ------------ |
|
||||
| AI 채팅 API | `aiRoutes.ts` | P0 |
|
||||
| 화면 패턴 분석 | `screenAnalyzer.ts` | P0 |
|
||||
| LLM 호출 | `llmService.ts` | P0 |
|
||||
| **워크플로우 패턴 검색 (RAG)** | `workflowPatternService.ts` | **P0** |
|
||||
| 채팅 UI | `AIChatPanel.tsx` | P0 |
|
||||
| **workflow_patterns 테이블** | DB | **P0** |
|
||||
|
||||
```typescript
|
||||
// 패턴 검색 핵심 로직
|
||||
export async function searchWorkflowPatterns(userIntent: string, companyCode: string) {
|
||||
const keywords = extractKeywords(userIntent);
|
||||
|
||||
return await query(`
|
||||
SELECT * FROM workflow_patterns
|
||||
WHERE intent_keywords && $1::text[]
|
||||
AND (company_code = $2 OR company_code = '*')
|
||||
ORDER BY priority DESC
|
||||
LIMIT 3
|
||||
`, [keywords, companyCode]);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 vexplor 담당 (기존 보완)
|
||||
|
||||
| 작업 | 현재 상태 | 필요 작업 |
|
||||
| -------------- | --------- | ------------------- |
|
||||
| 화면 생성 API | ✅ 존재 | 문서화 |
|
||||
| 워크플로우 API | ✅ 존재 | 문서화 |
|
||||
| 제어관리 API | ✅ 존재 | AI 활용 가능 |
|
||||
| table_labels | 부분 존재 | 주요 테이블 한글 라벨 |
|
||||
| column_labels | 부분 존재 | web_type 보완 |
|
||||
|
||||
#### 4.2.1 table_labels가 필요한 이유
|
||||
|
||||
AI가 "입고"라는 단어로 테이블을 찾으려면 한글 라벨이 있어야 합니다.
|
||||
|
||||
**현재 상태:**
|
||||
|
||||
| table_name | table_label | AI 검색 |
|
||||
| ---------- | ----------- | ------- |
|
||||
| inbound_mng | **입고관리** | ✅ "입고" 검색 가능 |
|
||||
| outbound_mng | **출고관리** | ✅ "출고" 검색 가능 |
|
||||
| production_record | production_record | ❌ "생산" 검색 불가 |
|
||||
| purchase_order_master | purchase_order_master | ❌ "발주" 검색 불가 |
|
||||
|
||||
**필요 작업**: 주요 업무 테이블에 한글 라벨 추가
|
||||
|
||||
```sql
|
||||
UPDATE table_labels SET table_label = '생산실적' WHERE table_name = 'production_record';
|
||||
UPDATE table_labels SET table_label = '발주관리' WHERE table_name = 'purchase_order_master';
|
||||
```
|
||||
|
||||
#### 4.2.2 column_labels의 web_type이 필요한 이유
|
||||
|
||||
AI가 컬럼을 보고 **어떤 컴포넌트를 생성할지** 결정해야 합니다.
|
||||
|
||||
**현재 상태** (inbound_mng):
|
||||
|
||||
| column_name | column_label | web_type |
|
||||
| ----------- | ------------ | -------- |
|
||||
| inbound_date | 입고일 | **null** |
|
||||
| inbound_qty | 입고수량 | **null** |
|
||||
| item_code | 품목코드 | **null** |
|
||||
|
||||
**web_type이 null이면?** → AI가 모든 필드를 text-input으로 만들어버림
|
||||
|
||||
**web_type이 있으면:**
|
||||
|
||||
| column_name | web_type | AI가 생성할 컴포넌트 |
|
||||
| ----------- | -------- | ------------------- |
|
||||
| inbound_date | **date** | 📅 날짜 선택기 |
|
||||
| inbound_qty | **number** | 🔢 숫자 입력 |
|
||||
| item_code | **entity** | 🔍 품목 검색 팝업 |
|
||||
| memo | **textarea** | 📝 여러 줄 텍스트 |
|
||||
|
||||
**필요 작업**: 주요 테이블 컬럼에 web_type 추가
|
||||
|
||||
```sql
|
||||
UPDATE column_labels SET web_type = 'date' WHERE column_name LIKE '%_date';
|
||||
UPDATE column_labels SET web_type = 'number' WHERE column_name LIKE '%_qty';
|
||||
UPDATE column_labels SET web_type = 'entity' WHERE column_name LIKE '%_code' AND column_name != 'company_code';
|
||||
UPDATE column_labels SET web_type = 'textarea' WHERE column_name = 'memo';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터베이스 스키마 (AI 전용)
|
||||
|
||||
```sql
|
||||
-- 핵심: 워크플로우 패턴 (RAG 지식 베이스)
|
||||
CREATE TABLE workflow_patterns (
|
||||
pattern_id SERIAL PRIMARY KEY,
|
||||
category VARCHAR(50) NOT NULL, -- 'inventory', 'sales'
|
||||
pattern_name VARCHAR(200) NOT NULL, -- '입고→재고 증가'
|
||||
intent_keywords TEXT[] NOT NULL, -- ['입고', '자재입고', 'inbound']
|
||||
description TEXT,
|
||||
source_table_hint VARCHAR(100), -- 'inbound_mng'
|
||||
target_table_hint VARCHAR(100), -- 'inventory_stock'
|
||||
logic_template JSONB NOT NULL, -- 제어관리 생성 템플릿
|
||||
company_code VARCHAR(20) DEFAULT '*',
|
||||
priority INTEGER DEFAULT 100,
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
CREATE INDEX idx_workflow_patterns_keywords ON workflow_patterns USING GIN(intent_keywords);
|
||||
|
||||
-- 초기 데이터
|
||||
INSERT INTO workflow_patterns (category, pattern_name, intent_keywords, description, source_table_hint, target_table_hint, logic_template) VALUES
|
||||
('inventory', '입고→재고 증가',
|
||||
ARRAY['입고', '구매입고', '자재입고', 'inbound'],
|
||||
'입고 저장 시 재고 수량 증가',
|
||||
'inbound_mng', 'inventory_stock',
|
||||
'{"actionType": "upsert", "operation": "increment", "fieldMappings": [{"source": "item_code", "target": "item_code", "type": "key"}, {"source": "inbound_qty", "target": "qty", "type": "increment"}]}'::jsonb
|
||||
),
|
||||
('inventory', '출고→재고 감소',
|
||||
ARRAY['출고', '판매출고', 'outbound'],
|
||||
'출고 저장 시 재고 수량 감소',
|
||||
'outbound_mng', 'inventory_stock',
|
||||
'{"actionType": "update", "operation": "decrement", "fieldMappings": [{"source": "item_code", "target": "item_code", "type": "key"}, {"source": "outbound_qty", "target": "qty", "type": "decrement"}]}'::jsonb
|
||||
);
|
||||
|
||||
-- 동의어 매핑 (P1)
|
||||
CREATE TABLE keyword_mapping (
|
||||
id SERIAL PRIMARY KEY,
|
||||
keyword VARCHAR(100) NOT NULL,
|
||||
table_name VARCHAR(100) NOT NULL,
|
||||
company_code VARCHAR(20) DEFAULT '*'
|
||||
);
|
||||
|
||||
-- AI 대화 이력 (P2)
|
||||
CREATE TABLE ai_conversations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
session_id VARCHAR(100) NOT NULL,
|
||||
company_code VARCHAR(20) NOT NULL,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE ai_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER REFERENCES ai_conversations(id),
|
||||
role VARCHAR(20) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 개발 로드맵
|
||||
|
||||
### Phase 1: MVP
|
||||
|
||||
**AI:**
|
||||
|
||||
- 채팅 UI + LLM 연동
|
||||
- 화면 패턴 분석
|
||||
- workflow_patterns 테이블 + RAG 검색
|
||||
|
||||
**vexplor:**
|
||||
|
||||
- API 스펙 문서화 (screen, flow)
|
||||
- 주요 테이블 한글 라벨 20개
|
||||
|
||||
### Phase 2: 워크플로우
|
||||
|
||||
**AI:**
|
||||
|
||||
- 자연어 → 워크플로우 변환
|
||||
- dataflow_diagrams 자동 생성
|
||||
|
||||
**vexplor:**
|
||||
|
||||
- 워크플로우 API 문서화
|
||||
|
||||
### Phase 3: 고도화
|
||||
|
||||
- 대화형 수정 ("왼쪽 패널 넓혀줘")
|
||||
- 멀티턴 컨텍스트
|
||||
- 사용자 피드백 학습
|
||||
|
||||
---
|
||||
|
||||
## 7. vexplor 체크리스트
|
||||
|
||||
### 즉시 (P0)
|
||||
|
||||
- [ ] POST /api/screen/create 스펙
|
||||
- [ ] POST /api/flow/definitions 스펙
|
||||
- [ ] 화면 레이아웃 JSON 예시
|
||||
|
||||
### 1주 내 (P1)
|
||||
|
||||
- [ ] 주요 테이블 20개 한글 라벨
|
||||
- inbound_mng, outbound_mng, inventory_stock
|
||||
- item_info, customer_mng, supplier_mng
|
||||
- sales_order_mng, purchase_order_mng
|
||||
- [ ] column_labels web_type 보완
|
||||
|
||||
---
|
||||
|
||||
## 8. 성공 지표
|
||||
|
||||
| 지표 | 목표 |
|
||||
| -------------------- | --------- |
|
||||
| 화면 생성 성공률 | 90%+ |
|
||||
| 평균 생성 시간 | 10초 이내 |
|
||||
| 수정 없이 사용 | 70%+ |
|
||||
| 워크플로우 자동 연결 | 80%+ |
|
||||
66
frontend/app/api/fleet-sso/token/route.ts
Normal file
66
frontend/app/api/fleet-sso/token/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const FLEET_API_URL = process.env.FLEET_API_URL || "https://fleet-api.vexplor.com";
|
||||
const SSO_SHARED_SECRET = process.env.SSO_SHARED_SECRET || "change_this_sso_secret";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// V1 로그인 세션에서 사용자 정보 추출
|
||||
const cookieStore = await cookies();
|
||||
const sessionToken = cookieStore.get("session_token")?.value
|
||||
|| cookieStore.get("token")?.value;
|
||||
|
||||
if (!sessionToken) {
|
||||
return NextResponse.json({ error: "로그인이 필요합니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
// 세션에서 사용자 정보 파싱 (JWT 디코딩)
|
||||
let userId = "unknown";
|
||||
let userName = "unknown";
|
||||
let companyId = "";
|
||||
try {
|
||||
const payload = JSON.parse(atob(sessionToken.split(".")[1]));
|
||||
userId = payload.userId || payload.user_id || payload.sub || payload.id || "unknown";
|
||||
userName = payload.userName || payload.user_name || payload.name || "unknown";
|
||||
companyId = payload.companyId || payload.company_id || payload.companyCode || "";
|
||||
} catch {
|
||||
return NextResponse.json({ error: "세션이 유효하지 않습니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
// Fleet API로 SSO 토큰 요청 (HMAC 서명)
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const signPayload = `${userId}|${userName}|${companyId}|${timestamp}`;
|
||||
const signature = crypto
|
||||
.createHmac("sha256", SSO_SHARED_SECRET)
|
||||
.update(signPayload)
|
||||
.digest("hex");
|
||||
|
||||
const response = await fetch(`${FLEET_API_URL}/api/auth/sso`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
user_name: userName,
|
||||
company_id: companyId,
|
||||
timestamp,
|
||||
signature,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
return NextResponse.json(
|
||||
{ error: data.message || "SSO 토큰 발급 실패" },
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ token: data.data.token });
|
||||
} catch (error) {
|
||||
console.error("[fleet-sso] 토큰 발급 에러:", error);
|
||||
return NextResponse.json({ error: "서버 오류" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
18
frontend/app/api/system/raw-token/route.ts
Normal file
18
frontend/app/api/system/raw-token/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token =
|
||||
cookieStore.get("authToken")?.value || cookieStore.get("session_token")?.value || cookieStore.get("token")?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "로그인이 필요합니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ token });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "서버 오류" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
236
frontend/components/v2/config-panels/V2WebViewConfigPanel.tsx
Normal file
236
frontend/components/v2/config-panels/V2WebViewConfigPanel.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Globe, Shield, Settings, ChevronDown, Info, Copy, Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { V2WebViewConfig } from "@/lib/registry/components/v2-web-view/types";
|
||||
|
||||
interface V2WebViewConfigPanelProps {
|
||||
config: V2WebViewConfig;
|
||||
onChange: (config: Partial<V2WebViewConfig>) => void;
|
||||
}
|
||||
|
||||
const SSO_GUIDE_SNIPPET = `// URL에서 sso_token 파라미터를 읽어 JWT를 디코딩하세요
|
||||
const token = url.searchParams.get("sso_token");
|
||||
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||
// payload.userId, payload.userName, payload.companyCode`;
|
||||
|
||||
export const V2WebViewConfigPanel: React.FC<V2WebViewConfigPanelProps> = ({ config, onChange }) => {
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [guideOpen, setGuideOpen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopySnippet = () => {
|
||||
navigator.clipboard.writeText(SSO_GUIDE_SNIPPET).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const updateConfig = (field: keyof V2WebViewConfig, value: any) => {
|
||||
const newConfig = { ...config, [field]: value };
|
||||
onChange({ [field]: value });
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("componentConfigChanged", {
|
||||
detail: { config: newConfig },
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* ─── 1단계: URL 입력 ─── */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="text-primary h-4 w-4" />
|
||||
<p className="text-sm font-medium">웹페이지 URL</p>
|
||||
</div>
|
||||
<Input
|
||||
value={config.url || ""}
|
||||
onChange={(e) => updateConfig("url", e.target.value)}
|
||||
placeholder="https://example.com"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<p className="text-muted-foreground text-[11px]">임베드할 외부 웹페이지 주소를 입력하세요</p>
|
||||
</div>
|
||||
|
||||
{/* ─── 2단계: SSO 연동 ─── */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="text-muted-foreground h-4 w-4" />
|
||||
<div>
|
||||
<p className="text-sm">SSO 연동</p>
|
||||
<p className="text-muted-foreground text-[11px]">현재 로그인 토큰을 URL에 자동 전달해요</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={config.useSSO || false} onCheckedChange={(checked) => updateConfig("useSSO", checked)} />
|
||||
</div>
|
||||
|
||||
{config.useSSO && (
|
||||
<Collapsible open={guideOpen} onOpenChange={setGuideOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-left transition-colors hover:bg-blue-100 dark:border-blue-900 dark:bg-blue-950 dark:hover:bg-blue-900"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5 shrink-0 text-blue-500" />
|
||||
<span className="text-[11px] font-medium text-blue-700 dark:text-blue-300">연동 개발자 가이드</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"ml-auto h-3.5 w-3.5 text-blue-400 transition-transform duration-200",
|
||||
guideOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-2.5 rounded-b-lg border border-t-0 border-blue-200 bg-blue-50/50 px-3 py-3 dark:border-blue-900 dark:bg-blue-950/50">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-medium text-blue-800 dark:text-blue-200">전달 방식</p>
|
||||
<p className="text-muted-foreground text-[10px]">URL 쿼리 파라미터로 JWT가 전달됩니다.</p>
|
||||
<code className="bg-muted/80 block rounded px-2 py-1 text-[10px]">?sso_token=eyJhbGciOi...</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-medium text-blue-800 dark:text-blue-200">JWT Payload 구조</p>
|
||||
<div className="bg-muted/80 rounded px-2 py-1.5 text-[10px] leading-relaxed">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">userId</span>
|
||||
<span>사용자 ID</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">userName</span>
|
||||
<span>사용자 이름</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">companyCode</span>
|
||||
<span>회사 코드</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">role</span>
|
||||
<span>권한</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[11px] font-medium text-blue-800 dark:text-blue-200">수신측 예시 코드</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopySnippet}
|
||||
className="flex items-center gap-1 text-[10px] text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
||||
{copied ? "복사됨" : "복사"}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="bg-muted/80 overflow-x-auto rounded px-2 py-1.5 text-[10px] leading-relaxed whitespace-pre-wrap">
|
||||
{`const token = url.searchParams
|
||||
.get("sso_token");
|
||||
const payload = JSON.parse(
|
||||
atob(token.split(".")[1])
|
||||
);
|
||||
// payload.userId
|
||||
// payload.companyCode`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── 3단계: 표시 옵션 ─── */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">테두리 표시</p>
|
||||
<p className="text-muted-foreground text-[11px]">웹 뷰 주변에 테두리를 표시해요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.showBorder !== false}
|
||||
onCheckedChange={(checked) => updateConfig("showBorder", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">전체 화면 허용</p>
|
||||
<p className="text-muted-foreground text-[11px]">임베드된 페이지에서 전체 화면 전환이 가능해요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.allowFullscreen || false}
|
||||
onCheckedChange={(checked) => updateConfig("allowFullscreen", checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 4단계: 고급 설정 (기본 접혀있음) ─── */}
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="text-muted-foreground h-4 w-4" />
|
||||
<span className="text-sm font-medium">고급 설정</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
||||
advancedOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-3 rounded-b-lg border border-t-0 p-4">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">샌드박스 모드</p>
|
||||
<p className="text-muted-foreground text-[11px]">보안을 위해 iframe 실행 환경을 제한해요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.sandbox !== false}
|
||||
onCheckedChange={(checked) => updateConfig("sandbox", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-muted-foreground text-xs">모서리 둥글기</span>
|
||||
<Input
|
||||
value={config.borderRadius || "8px"}
|
||||
onChange={(e) => updateConfig("borderRadius", e.target.value)}
|
||||
placeholder="8px"
|
||||
className="h-7 w-[100px] text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-muted-foreground text-xs">로딩 텍스트</span>
|
||||
<Input
|
||||
value={config.loadingText || ""}
|
||||
onChange={(e) => updateConfig("loadingText", e.target.value)}
|
||||
placeholder="로딩 중..."
|
||||
className="h-7 w-[140px] text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
V2WebViewConfigPanel.displayName = "V2WebViewConfigPanel";
|
||||
|
||||
export default V2WebViewConfigPanel;
|
||||
@@ -119,6 +119,7 @@ import "./v2-approval-step/ApprovalStepRenderer"; // 결재 단계 시각화
|
||||
import "./v2-status-count/StatusCountRenderer"; // 상태별 카운트 카드
|
||||
import "./v2-process-work-standard/ProcessWorkStandardRenderer"; // 공정 작업기준
|
||||
import "./v2-item-routing/ItemRoutingRenderer"; // 품목별 라우팅
|
||||
import "./v2-web-view/V2WebViewRenderer"; // 외부 웹페이지 임베딩 (SSO 지원)
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화 함수
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { ComponentRendererProps } from "../../types";
|
||||
import { V2WebViewConfig } from "./types";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
|
||||
export interface V2WebViewComponentProps extends ComponentRendererProps {}
|
||||
|
||||
export const V2WebViewComponent: React.FC<V2WebViewComponentProps> = ({
|
||||
component,
|
||||
isDesignMode = false,
|
||||
isSelected = false,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
const config = (component.componentConfig || {}) as V2WebViewConfig;
|
||||
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const baseUrl = config.url ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
if (!baseUrl) {
|
||||
setIframeSrc(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.useSSO) {
|
||||
setIframeSrc(baseUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const paramName = "sso_token";
|
||||
|
||||
fetch("/api/system/raw-token")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
if (data.token) {
|
||||
const separator = baseUrl.includes("?") ? "&" : "?";
|
||||
setIframeSrc(`${baseUrl}${separator}${encodeURIComponent(paramName)}=${encodeURIComponent(data.token)}`);
|
||||
} else {
|
||||
setError(data.error ?? "토큰을 가져올 수 없습니다");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setError("토큰 조회 실패");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [baseUrl, config.useSSO]);
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: "absolute",
|
||||
left: `${component.style?.positionX || 0}px`,
|
||||
top: `${component.style?.positionY || 0}px`,
|
||||
width: `${component.style?.width || 400}px`,
|
||||
height: `${component.style?.height || 300}px`,
|
||||
zIndex: component.style?.positionZ || 1,
|
||||
cursor: isDesignMode ? "pointer" : "default",
|
||||
border: isSelected ? "2px solid #3b82f6" : config.showBorder ? "1px solid #e0e0e0" : "none",
|
||||
borderRadius: config.borderRadius || "8px",
|
||||
overflow: "hidden",
|
||||
background: "#fafafa",
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isDesignMode) {
|
||||
e.stopPropagation();
|
||||
onClick?.(e);
|
||||
}
|
||||
};
|
||||
|
||||
const domProps = filterDOMProps(props);
|
||||
|
||||
// 디자인 모드: URL 미리보기 표시
|
||||
if (isDesignMode) {
|
||||
return (
|
||||
<div style={containerStyle} className="v2-web-view-component" onClick={handleClick} {...domProps}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "8px",
|
||||
color: "#666",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
</svg>
|
||||
<span style={{ fontWeight: 500 }}>웹 뷰</span>
|
||||
{baseUrl ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "#999",
|
||||
maxWidth: "90%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{baseUrl}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: "11px", color: "#bbb" }}>URL을 설정하세요</span>
|
||||
)}
|
||||
{config.useSSO && <span style={{ fontSize: "10px", color: "#4caf50" }}>SSO: ?sso_token=JWT</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 런타임 모드
|
||||
return (
|
||||
<div style={containerStyle} className="v2-web-view-component" {...domProps}>
|
||||
{loading && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#999",
|
||||
}}
|
||||
>
|
||||
{config.loadingText || "로딩 중..."}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#f44336",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && iframeSrc && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={iframeSrc}
|
||||
style={{ width: "100%", height: "100%", border: "none" }}
|
||||
sandbox={config.sandbox ? "allow-scripts allow-same-origin allow-forms allow-popups" : undefined}
|
||||
allowFullScreen={config.allowFullscreen}
|
||||
title="Web View"
|
||||
/>
|
||||
)}
|
||||
{!loading && !error && !iframeSrc && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#bbb",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
URL이 설정되지 않았습니다
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const V2WebViewWrapper: React.FC<V2WebViewComponentProps> = (props) => {
|
||||
return <V2WebViewComponent {...props} />;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||
import { V2WebViewDefinition } from "./index";
|
||||
import { V2WebViewComponent } from "./V2WebViewComponent";
|
||||
|
||||
export class V2WebViewRenderer extends AutoRegisteringComponentRenderer {
|
||||
static componentDefinition = V2WebViewDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
return <V2WebViewComponent {...this.props} renderer={this} />;
|
||||
}
|
||||
}
|
||||
|
||||
V2WebViewRenderer.registerSelf();
|
||||
34
frontend/lib/registry/components/v2-web-view/index.ts
Normal file
34
frontend/lib/registry/components/v2-web-view/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||
import { ComponentCategory } from "@/types/component";
|
||||
import { V2WebViewWrapper } from "./V2WebViewComponent";
|
||||
import { V2WebViewConfigPanel } from "@/components/v2/config-panels/V2WebViewConfigPanel";
|
||||
import type { V2WebViewConfig } from "./types";
|
||||
|
||||
export const V2WebViewDefinition = createComponentDefinition({
|
||||
id: "v2-web-view",
|
||||
name: "V2 웹 뷰",
|
||||
nameEng: "V2 WebView Component",
|
||||
description: "외부 웹페이지를 iframe으로 임베드하여 표시하는 컴포넌트 (SSO 지원)",
|
||||
category: ComponentCategory.DISPLAY,
|
||||
webType: "custom",
|
||||
component: V2WebViewWrapper,
|
||||
defaultConfig: {
|
||||
url: "",
|
||||
useSSO: false,
|
||||
sandbox: true,
|
||||
allowFullscreen: false,
|
||||
showBorder: true,
|
||||
loadingText: "로딩 중...",
|
||||
} as V2WebViewConfig,
|
||||
defaultSize: { width: 600, height: 400 },
|
||||
configPanel: V2WebViewConfigPanel,
|
||||
icon: "Globe",
|
||||
tags: ["v2", "웹", "뷰", "iframe", "임베드", "외부", "SSO", "fleet"],
|
||||
version: "1.0.0",
|
||||
author: "개발팀",
|
||||
});
|
||||
|
||||
export type { V2WebViewConfig } from "./types";
|
||||
13
frontend/lib/registry/components/v2-web-view/types.ts
Normal file
13
frontend/lib/registry/components/v2-web-view/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ComponentConfig } from "@/types/component";
|
||||
|
||||
export interface V2WebViewConfig extends ComponentConfig {
|
||||
url?: string;
|
||||
useSSO?: boolean;
|
||||
sandbox?: boolean;
|
||||
allowFullscreen?: boolean;
|
||||
borderRadius?: string;
|
||||
showBorder?: boolean;
|
||||
loadingText?: string;
|
||||
}
|
||||
42
frontend/package-lock.json
generated
42
frontend/package-lock.json
generated
@@ -266,7 +266,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -308,7 +307,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -342,7 +340,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -3058,7 +3055,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz",
|
||||
"integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/react-reconciler": "^0.32.0",
|
||||
@@ -3712,7 +3708,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz",
|
||||
"integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.6"
|
||||
},
|
||||
@@ -3807,7 +3802,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz",
|
||||
"integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -4121,7 +4115,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz",
|
||||
"integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
@@ -6622,7 +6615,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -6633,7 +6625,6 @@
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -6676,7 +6667,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
||||
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
@@ -6759,7 +6749,6 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -7392,7 +7381,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8543,8 +8531,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3": {
|
||||
"version": "7.9.0",
|
||||
@@ -8866,7 +8853,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -9626,7 +9612,6 @@
|
||||
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9715,7 +9700,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -9817,7 +9801,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -10989,7 +10972,6 @@
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -11770,8 +11752,7 @@
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
@@ -13110,7 +13091,6 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -13404,7 +13384,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
@@ -13434,7 +13413,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
@@ -13483,7 +13461,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
|
||||
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
@@ -13687,7 +13664,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13757,7 +13733,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -13808,7 +13783,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
|
||||
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -13841,8 +13815,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "5.0.0",
|
||||
@@ -14150,7 +14123,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -14173,8 +14145,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/recharts/node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -15204,8 +15175,7 @@
|
||||
"version": "0.180.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.8.3",
|
||||
@@ -15293,7 +15263,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15642,7 +15611,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
Reference in New Issue
Block a user