22
This commit is contained in:
@@ -11,7 +11,7 @@ description: API 요청 시 항상 전용 API 클라이언트를 사용하도록
|
||||
|
||||
## 이유
|
||||
|
||||
1. **환경별 URL 자동 처리**: 프로덕션(`v1.vexplor.com`)과 개발(`localhost`) 환경에서 올바른 백엔드 서버로 요청
|
||||
1. **환경별 URL 자동 처리**: 프로덕션(`v1.invion.com`)과 개발(`localhost`) 환경에서 올바른 백엔드 서버로 요청
|
||||
2. **일관된 에러 처리**: 모든 API 호출에서 동일한 에러 핸들링
|
||||
3. **인증 토큰 자동 포함**: Authorization 헤더 자동 추가
|
||||
4. **유지보수성**: API 변경 시 한 곳에서만 수정
|
||||
@@ -116,9 +116,9 @@ const getApiBaseUrl = (): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
// 프로덕션: v1.vexplor.com → api.vexplor.com
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return "https://api.vexplor.com/api";
|
||||
// 프로덕션: v1.invion.com → api.invion.com
|
||||
if (currentHost === "v1.invion.com") {
|
||||
return "https://api.invion.com/api";
|
||||
}
|
||||
|
||||
// 로컬 개발
|
||||
@@ -155,7 +155,7 @@ API 클라이언트는 자동으로 환경을 감지합니다:
|
||||
|
||||
| 현재 호스트 | 백엔드 API URL |
|
||||
| ---------------- | ----------------------------- |
|
||||
| `v1.vexplor.com` | `https://api.vexplor.com/api` |
|
||||
| `v1.invion.com` | `https://api.invion.com/api` |
|
||||
| `localhost:9771` | `http://localhost:8080/api` |
|
||||
| `localhost:3000` | `http://localhost:8080/api` |
|
||||
|
||||
|
||||
@@ -661,7 +661,7 @@ if (req.user && req.user.companyCode !== "*") {
|
||||
|
||||
| 환경 | 프론트엔드 | 백엔드 API |
|
||||
|------|-----------|-----------|
|
||||
| 프로덕션 | `v1.vexplor.com` | `https://api.vexplor.com/api` |
|
||||
| 프로덕션 | `v1.invion.com` | `https://api.invion.com/api` |
|
||||
| 개발 (로컬) | `localhost:9771` 또는 `localhost:3000` | `http://localhost:8080/api` |
|
||||
|
||||
- 프론트엔드 `apiClient`가 `window.location.hostname` 기반으로 자동 판별
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
**INVION**은 제조업 특화 Low-Code ERP/PLM(Product Lifecycle Management) 플랫폼입니다.
|
||||
사용자가 런타임에 화면·테이블·워크플로우를 정의할 수 있는 **메타데이터 기반 설계**로, 코드 수정 없이 업무 화면을 구성합니다.
|
||||
|
||||
듀얼 백엔드(Node.js + Spring Boot) 아키텍처로 점진적 마이그레이션을 지원하며, 멀티테넌시(company_code 기반)로 복수 기업을 단일 인스턴스에서 운영합니다.
|
||||
Spring Boot + Next.js 풀스택 구조이며, 멀티테넌시(company_code 기반)로 복수 기업을 단일 인스턴스에서 운영합니다.
|
||||
|
||||
## 주요 특징
|
||||
|
||||
- **Low-Code 화면 디자이너**: 드래그앤드롭으로 업무 화면 구성, V2 컴포넌트 시스템
|
||||
- **듀얼 백엔드**: Node.js(Express) ↔ Spring Boot(MyBatis) 동일 API 스펙 유지
|
||||
- **Spring Boot 백엔드**: Java 21 + MyBatis (SqlSessionTemplate) + PostgreSQL
|
||||
- **v5 Cosmic Glassmorphism UI**: 코스믹 배경 + 글래스 블러 기반 모던 디자인 시스템
|
||||
- **멀티테넌시**: company_code 기반 데이터 격리, Super Admin 전사 접근
|
||||
- **멀티 DB 연결**: PostgreSQL(메인) + MSSQL + Oracle + MySQL 외부 DB 지원
|
||||
- **반응형 디자인**: 데스크톱 / 태블릿 / 모바일 완전 대응
|
||||
- **3D 시각화**: React Three Fiber 기반 Digital Twin / Yard Layout
|
||||
- **3D / 차트 / 지도**: React Three Fiber, Recharts, Leaflet 통합
|
||||
- **GitOps 배포**: Jenkins + Kaniko + Helm + Traefik (Kubernetes)
|
||||
|
||||
## 기술 스택
|
||||
|
||||
@@ -26,41 +26,34 @@
|
||||
| 프레임워크 | **Next.js 15** (App Router, Turbopack) | React 19 |
|
||||
| 언어 | **TypeScript 5** | strict mode |
|
||||
| 스타일링 | **Tailwind CSS v4** + v5 커스텀 CSS (`--v5-` prefix) | 글래스모피즘 테마 |
|
||||
| UI 라이브러리 | **shadcn/ui** + Radix UI | 커스텀 오버라이드 |
|
||||
| 상태 관리 | **Zustand** (글로벌) + **TanStack Query** (서버) | |
|
||||
| UI 라이브러리 | **shadcn/ui** + Radix UI (15개 컴포넌트) | 커스텀 오버라이드 |
|
||||
| 상태 관리 | **Zustand 5** (글로벌) + **TanStack Query 5** (서버) | |
|
||||
| 테이블 | **TanStack Table** + **TanStack Virtual** | 가상 스크롤 |
|
||||
| HTTP 클라이언트 | **Axios** | 70개 API 모듈 (`lib/api/`) |
|
||||
| 플로우 디자이너 | **XY Flow** (@xyflow/react) | 데이터플로우 |
|
||||
| 3D | **React Three Fiber** + Drei | Digital Twin |
|
||||
| 차트 | **Recharts** + **D3** | 대시보드 / 분석 |
|
||||
| 지도 | **Leaflet** + React Leaflet | 위치 기반 시각화 |
|
||||
| 리치 텍스트 | **Tiptap** | 에디터 |
|
||||
| 드래그앤드롭 | **DnD Kit** | 화면 디자이너 / 탭 정렬 |
|
||||
| 폼 | **React Hook Form** + Zod | 유효성 검증 |
|
||||
| 폼 | **React Hook Form** + **Zod** | 유효성 검증 |
|
||||
| 문서 생성 | **jsPDF** + **exceljs** + mammoth | PDF / Excel / Word |
|
||||
| 바코드 / QR | **jsbarcode** + **qrcode** + @zxing | 스캔 / 생성 |
|
||||
| 아이콘 | **Lucide React** | |
|
||||
|
||||
### Backend — Node.js (Express)
|
||||
|
||||
| 영역 | 기술 | 비고 |
|
||||
|------|------|------|
|
||||
| 런타임 | **Node.js 20+** | LTS |
|
||||
| 프레임워크 | **Express 4** | TypeScript |
|
||||
| 데이터베이스 | **PostgreSQL** (pg) + MSSQL + Oracle + MySQL | 멀티 DB |
|
||||
| 인증 | **JWT** (jsonwebtoken) + bcryptjs | |
|
||||
| 로깅 | **Winston** | |
|
||||
| 메일 | **Nodemailer** + IMAP | 수신/발신 |
|
||||
| 바코드 | **bwip-js** | |
|
||||
| 문서 생성 | **docx** + html-to-docx | |
|
||||
| 스케줄링 | **node-cron** | 배치 |
|
||||
|
||||
### Backend — Spring Boot (마이그레이션 대상)
|
||||
### Backend — Spring Boot
|
||||
|
||||
| 영역 | 기술 | 비고 |
|
||||
|------|------|------|
|
||||
| 언어 | **Java 21** | LTS |
|
||||
| 프레임워크 | **Spring Boot 3.3.x** | |
|
||||
| 프레임워크 | **Spring Boot 3.3.5** | |
|
||||
| 빌드 | **Gradle** (Groovy DSL) | |
|
||||
| SQL Mapper | **MyBatis 3** (SqlSessionTemplate 직접 사용) | Mapper Interface 미사용 |
|
||||
| 데이터베이스 | **PostgreSQL** + HikariCP | |
|
||||
| 보안 | **Spring Security** + JWT (jjwt) | |
|
||||
| JSON | **Jackson** (숫자→문자열 직렬화) | Node API 호환 |
|
||||
| 보안 | **Spring Security** + JWT (jjwt 0.12.3) | |
|
||||
| JSON | **Jackson** (숫자→문자열 직렬화, null 키 포함) | |
|
||||
| 메일 | **Spring Boot Starter Mail** | IMAP 수신 + SMTP 발신 |
|
||||
| 로깅 | **SLF4J + Logback** | Spring Boot 기본 |
|
||||
|
||||
> **아키텍처 핵심**: `Map<String, Object>` 기반 — Low-Code 플랫폼 특성상 테이블/컬럼이 런타임에 결정되므로 DTO 클래스를 사용하지 않습니다. Service에서 `sqlSession`으로 XML을 직접 호출하는 3레이어 구조입니다.
|
||||
>
|
||||
@@ -73,11 +66,12 @@
|
||||
| 영역 | 기술 |
|
||||
|------|------|
|
||||
| 컨테이너화 | Docker + Docker Compose |
|
||||
| E2E 테스트 | **Playwright** |
|
||||
| 단위 테스트 | Jest + Supertest |
|
||||
| 백엔드 핫리로드 | **Spring Boot DevTools** |
|
||||
| 코드 품질 | ESLint + Prettier |
|
||||
| 백엔드 핫리로드 | nodemon |
|
||||
| CI/CD | Jenkins |
|
||||
| CI/CD | **Jenkins** + **Kaniko** (이미지 빌드) |
|
||||
| 오케스트레이션 | **Kubernetes** + **Helm** (GitOps) |
|
||||
| 리버스 프록시 | **Traefik** (HTTPS, Let's Encrypt) |
|
||||
| 컨테이너 레지스트리 | `registry.kpslp.kr` (프라이빗) |
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
@@ -105,23 +99,16 @@ INVION/
|
||||
│ │ ├── animations/ # 애니메이션 컴포넌트
|
||||
│ │ └── ... # dashboard, vehicle, tax-invoice 등
|
||||
│ ├── lib/ # 유틸리티, API 클라이언트, 서비스
|
||||
│ │ ├── api/ # API 클라이언트 (fetch 직접 사용 금지)
|
||||
│ │ ├── api/ # Axios 기반 API 클라이언트 (70개 모듈)
|
||||
│ │ ├── stores/ # Zustand 스토어
|
||||
│ │ ├── schemas/ # Zod 스키마
|
||||
│ │ ├── services/ # 프론트 비즈니스 로직
|
||||
│ │ └── types/ # TypeScript 타입 정의
|
||||
│ ├── stores/ # 글로벌 Zustand 스토어
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ └── styles/ # v5-layout.css (글래스모피즘)
|
||||
│
|
||||
├── backend-node/ # Express + TypeScript 백엔드 (~87개 서비스)
|
||||
│ └── src/
|
||||
│ ├── app.ts # 엔트리포인트
|
||||
│ ├── controllers/ # API 컨트롤러
|
||||
│ ├── services/ # 비즈니스 로직
|
||||
│ ├── middleware/ # 인증, 에러처리
|
||||
│ ├── routes/ # 라우터
|
||||
│ └── config/ # DB 연결 설정
|
||||
│
|
||||
├── backend-spring/ # Spring Boot 백엔드 (~95개 컨트롤러, ~96개 XML)
|
||||
├── backend-spring/ # Spring Boot 백엔드 (95 컨트롤러, 97 서비스, 96 XML)
|
||||
│ └── src/main/
|
||||
│ ├── java/com/erp/
|
||||
│ │ ├── common/ # BaseService (sqlSession 주입)
|
||||
@@ -137,18 +124,17 @@ INVION/
|
||||
├── db/ # 데이터베이스
|
||||
│ └── migrations/ # 순차 마이그레이션 SQL
|
||||
├── docker/ # Docker 설정 (dev/prod/deploy)
|
||||
├── scripts/ # 개발/배포 스크립트
|
||||
├── docs/ # 프로젝트 문서
|
||||
├── scripts/ # 개발/배포 스크립트 (dev/prod)
|
||||
├── Dockerfile # 프로덕션 멀티스테이지 빌드 (Spring + Next.js)
|
||||
└── Jenkinsfile # CI/CD 파이프라인
|
||||
└── Jenkinsfile # CI/CD 파이프라인 (Kaniko + Helm)
|
||||
```
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 1. 필수 요구사항
|
||||
|
||||
- **Node.js**: 20.10+
|
||||
- **Java**: 21 (Spring Boot 백엔드 사용 시)
|
||||
- **Java**: 21
|
||||
- **Node.js**: 20.10+ (프론트엔드 빌드용)
|
||||
- **PostgreSQL**: 데이터베이스 서버
|
||||
- **npm**: 10.0+
|
||||
|
||||
@@ -158,9 +144,6 @@ INVION/
|
||||
# 프론트엔드 (Turbopack, port 9771)
|
||||
cd frontend && npm install && npm run dev
|
||||
|
||||
# 백엔드 — Node.js (port 8080)
|
||||
cd backend-node && npm install && npm run dev
|
||||
|
||||
# 백엔드 — Spring Boot (port 8081)
|
||||
cd backend-spring && ./gradlew bootRun
|
||||
```
|
||||
@@ -181,8 +164,9 @@ docker-compose -f docker/deploy/docker-compose.yml up -d
|
||||
| 서비스 | URL | 설명 |
|
||||
|--------|-----|------|
|
||||
| **프론트엔드** | http://localhost:9771 | Next.js UI |
|
||||
| **백엔드 (Node)** | http://localhost:8080 | Express REST API |
|
||||
| **백엔드 (Spring)** | http://localhost:8081 | Spring Boot REST API |
|
||||
| **백엔드 API** | http://localhost:8081 | Spring Boot REST API |
|
||||
|
||||
> 프론트엔드는 `next.config.mjs`의 rewrite 설정으로 `/api/*` 요청을 백엔드(8081)로 프록시합니다.
|
||||
|
||||
## 주요 기능
|
||||
|
||||
@@ -217,12 +201,15 @@ docker-compose -f docker/deploy/docker-compose.yml up -d
|
||||
|
||||
### 7. 기타 기능
|
||||
- 메일 연동 (IMAP 수신 + SMTP 발신)
|
||||
- 바코드/라벨 생성 (bwip-js)
|
||||
- 바코드/QR 생성 및 스캔 (jsbarcode + qrcode + @zxing)
|
||||
- 대시보드 차트 (Recharts + D3)
|
||||
- 지도 시각화 (Leaflet)
|
||||
- 리포트 / 세금계산서
|
||||
- 문서 생성 (PDF, Excel, Word)
|
||||
- 다국어 지원
|
||||
- 번호 채번 규칙
|
||||
- 배치 스케줄링 + 외부 DB 연동
|
||||
- 파일/문서 관리 (DOCX 생성)
|
||||
- 배치 스케줄링
|
||||
- 파일/문서 관리
|
||||
|
||||
## 디자인 시스템 — v5 Cosmic Glassmorphism
|
||||
|
||||
@@ -232,7 +219,7 @@ INVION v5는 **코스믹 글래스모피즘** 디자인 언어를 사용합니
|
||||
- **글래스 UI**: `backdrop-filter: blur()` 기반 반투명 패널
|
||||
- **CSS 변수**: `--v5-` prefix로 shadcn/Tailwind 변수와 충돌 방지
|
||||
- **다크/라이트 테마**: 크로스페이드 전환 애니메이션
|
||||
- **모션 디테일**: 모든 전환/호버/진입에 애니메이션 적용
|
||||
- **모션 디테일**: 모든 전환/호버/진입에 애니메이션 적용 (CSS 기반, framer-motion 미사용)
|
||||
|
||||
주요 스타일 파일:
|
||||
- `frontend/styles/v5-layout.css` — v5 전체 CSS
|
||||
@@ -240,19 +227,25 @@ INVION v5는 **코스믹 글래스모피즘** 디자인 언어를 사용합니
|
||||
|
||||
## 환경 변수
|
||||
|
||||
```bash
|
||||
# backend-node/.env
|
||||
DATABASE_URL=postgresql://postgres:password@localhost:5432/dbname
|
||||
JWT_SECRET=your-jwt-secret
|
||||
JWT_EXPIRES_IN=24h
|
||||
PORT=8080
|
||||
CORS_ORIGIN=http://localhost:9771
|
||||
|
||||
```yaml
|
||||
# backend-spring/src/main/resources/application.yml
|
||||
# spring.datasource.url, spring.datasource.username, etc.
|
||||
server:
|
||||
port: 8081
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://host:port/dbname
|
||||
username: postgres
|
||||
password: ****
|
||||
jwt:
|
||||
secret: your-jwt-secret
|
||||
expiration: 86400000
|
||||
file:
|
||||
upload-dir: ./uploads
|
||||
```
|
||||
|
||||
```bash
|
||||
# frontend/.env.local
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080/api
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8081/api
|
||||
```
|
||||
|
||||
## 배포
|
||||
@@ -260,13 +253,26 @@ NEXT_PUBLIC_API_URL=http://localhost:8080/api
|
||||
### 프로덕션 빌드
|
||||
|
||||
```bash
|
||||
# 멀티스테이지 Docker 빌드 (Spring Boot + Next.js)
|
||||
# 멀티스테이지 Docker 빌드 (Spring Boot + Next.js → 단일 컨테이너)
|
||||
docker build -t invion .
|
||||
```
|
||||
|
||||
### CI/CD
|
||||
멀티스테이지 빌드 과정:
|
||||
1. **Stage 1** — Spring Boot 빌드 (`eclipse-temurin:21-jdk-alpine`, Gradle → bootJar)
|
||||
2. **Stage 2** — Next.js 빌드 (`node:20.10-alpine`, npm → standalone)
|
||||
3. **Stage 3** — 런타임 (`eclipse-temurin:21-jre-alpine` + Node.js, 두 서비스 병렬 실행)
|
||||
|
||||
Jenkins 파이프라인 (`Jenkinsfile`)으로 자동 빌드 및 배포가 설정되어 있습니다.
|
||||
### CI/CD 파이프라인
|
||||
|
||||
```
|
||||
Git Push → Jenkins → Kaniko 이미지 빌드 → 프라이빗 레지스트리 푸시
|
||||
→ Helm 차트 태그 업데이트 → Kubernetes 자동 배포 (GitOps)
|
||||
```
|
||||
|
||||
- **이미지 빌드**: Kaniko (Docker-in-Docker 불필요)
|
||||
- **레지스트리**: `registry.kpslp.kr` (프라이빗)
|
||||
- **배포**: Helm 차트 + GitOps (이미지 태그 자동 업데이트)
|
||||
- **프로덕션 도메인**: Traefik 리버스 프록시 + Let's Encrypt HTTPS
|
||||
|
||||
## 코드 컨벤션
|
||||
|
||||
@@ -283,10 +289,11 @@ Jenkins 파이프라인 (`Jenkinsfile`)으로 자동 빌드 및 배포가 설정
|
||||
|
||||
### 공통 규칙
|
||||
|
||||
- **TypeScript**: 엄격한 타입 정의 사용
|
||||
- **TypeScript**: strict mode 활성화
|
||||
- **ESLint + Prettier**: 일관된 코드 스타일
|
||||
- **shadcn/ui**: UI 컴포넌트 표준
|
||||
- **API 클라이언트**: `frontend/lib/api/` 전용 클라이언트 사용 (fetch 직접 사용 금지)
|
||||
- **API 클라이언트**: `frontend/lib/api/` Axios 기반 전용 클라이언트 사용 (fetch 직접 사용 금지)
|
||||
- **멀티테넌시**: 모든 쿼리에 company_code 필터링 필수
|
||||
- **Map 기반**: Spring Boot 백엔드는 DTO 대신 `Map<String, Object>` 사용
|
||||
- **snake→camel 변환 금지**: `toCamelCaseKeys()` 등 변환 함수 사용 불가
|
||||
- **MyBatis 설정**: `map-underscore-to-camel-case: false` 유지
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# AI Assistant API (VEXPLOR 내장) - 환경 변수
|
||||
# AI Assistant API (INVION 내장) - 환경 변수
|
||||
# 이 파일을 .env 로 복사한 뒤 값 설정
|
||||
|
||||
NODE_ENV=development
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AI 어시스턴트 API (VEXPLOR 내장)
|
||||
# AI 어시스턴트 API (INVION 내장)
|
||||
|
||||
VEXPLOR와 **같은 서비스**로 동작하도록 이 API는 포트 3100에서 구동되고, backend-node가 `/api/ai/v1` 요청을 여기로 프록시합니다.
|
||||
INVION와 **같은 서비스**로 동작하도록 이 API는 포트 3100에서 구동되고, backend-node가 `/api/ai/v1` 요청을 여기로 프록시합니다.
|
||||
|
||||
## 동작 방식
|
||||
|
||||
@@ -8,7 +8,7 @@ VEXPLOR와 **같은 서비스**로 동작하도록 이 API는 포트 3100에서
|
||||
- **Next.js** → `8080/api/ai/v1/*` 로 rewrite
|
||||
- **backend-node(8080)** → `3100/api/v1/*` 로 프록시 → **이 서비스**
|
||||
|
||||
따라서 사용자는 **다른 포트를 쓰지 않고** VEXPLOR만 켜도 AI 기능을 사용할 수 있습니다.
|
||||
따라서 사용자는 **다른 포트를 쓰지 않고** INVION만 켜도 AI 기능을 사용할 수 있습니다.
|
||||
|
||||
## 서비스 올리는 순서 (한 번에 동작하게)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ai-assistant-api",
|
||||
"version": "1.0.0",
|
||||
"description": "AI Assistant API (VEXPLOR 내장) - 포트 3100에서 구동, backend-node가 /api/ai/v1 로 프록시",
|
||||
"description": "AI Assistant API (INVION 내장) - 포트 3100에서 구동, backend-node가 /api/ai/v1 로 프록시",
|
||||
"private": true,
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -17,7 +17,7 @@ const routes = require('./routes');
|
||||
const errorHandler = require('./middlewares/error-handler.middleware');
|
||||
|
||||
const app = express();
|
||||
// VEXPLOR 내장 시 backend-node가 이 포트로 프록시하므로 기본 3100 사용
|
||||
// INVION 내장 시 backend-node가 이 포트로 프록시하므로 기본 3100 사용
|
||||
const PORT = process.env.PORT || 3100;
|
||||
|
||||
// ===========================================
|
||||
|
||||
@@ -75,8 +75,8 @@ const getCorsOrigin = (): string[] | boolean => {
|
||||
"http://localhost:9771", // 로컬 개발 환경
|
||||
"http://192.168.0.70:5555", // 내부 네트워크 접근
|
||||
"http://39.117.244.52:5555", // 외부 네트워크 접근
|
||||
"https://v1.vexplor.com", // 운영 프론트엔드
|
||||
"https://api.vexplor.com", // 운영 백엔드
|
||||
"https://v1.invion.com", // 운영 프론트엔드
|
||||
"https://api.invion.com", // 운영 백엔드
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* AI 어시스턴트 API 프록시
|
||||
* - /api/ai/v1/* 요청을 AI 서비스(기본 3100 포트)로 전달
|
||||
* - VEXPLOR와 같은 서비스로 쓰려면: 프론트(9771) → 백엔드(8080) → 여기서 3100으로 프록시
|
||||
* - INVION와 같은 서비스로 쓰려면: 프론트(9771) → 백엔드(8080) → 여기서 3100으로 프록시
|
||||
*/
|
||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
- backend_data:/app/data
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.backend.rule=Host(`api.vexplor.com`)
|
||||
- traefik.http.routers.backend.rule=Host(`api.invion.com`)
|
||||
- traefik.http.routers.backend.entrypoints=websecure,web
|
||||
- traefik.http.routers.backend.tls=true
|
||||
- traefik.http.routers.backend.tls.certresolver=le
|
||||
@@ -34,12 +34,12 @@ services:
|
||||
context: ../../frontend
|
||||
dockerfile: ../docker/deploy/frontend.Dockerfile
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=https://api.vexplor.com/api
|
||||
- NEXT_PUBLIC_API_URL=https://api.invion.com/api
|
||||
container_name: pms-frontend-prod
|
||||
restart: always
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NEXT_PUBLIC_API_URL: https://api.vexplor.com/api
|
||||
NEXT_PUBLIC_API_URL: https://api.invion.com/api
|
||||
SERVER_API_URL: "http://backend:8081"
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
PORT: "3000"
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
- frontend_data:/app/data
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.frontend.rule=Host(`v1.vexplor.com`)
|
||||
- traefik.http.routers.frontend.rule=Host(`v1.invion.com`)
|
||||
- traefik.http.routers.frontend.entrypoints=websecure,web
|
||||
- traefik.http.routers.frontend.tls=true
|
||||
- traefik.http.routers.frontend.tls.certresolver=le
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# 빌드 시 환경변수 설정 (ARG로 받아서 ENV로 설정)
|
||||
ARG NEXT_PUBLIC_API_URL=https://api.vexplor.com/api
|
||||
ARG NEXT_PUBLIC_API_URL=https://api.invion.com/api
|
||||
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||
|
||||
# Build the application
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
volumes:
|
||||
- ../../backend-spring:/app
|
||||
networks:
|
||||
- test-vex-network
|
||||
- invion-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8082/health"]
|
||||
@@ -31,5 +31,5 @@ services:
|
||||
start_period: 90s
|
||||
|
||||
networks:
|
||||
test-vex-network:
|
||||
invion-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -18,9 +18,9 @@ services:
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
networks:
|
||||
- test-vex-network
|
||||
- invion-network
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
test-vex-network:
|
||||
invion-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Vexplor 구조 다이어그램
|
||||
# Invion 구조 다이어그램
|
||||
|
||||
> 생성일: 2026-01-22 | 총 테이블: 164개 | 코드 기반 관계 분석 완료
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
# INVY.ONE 전환 컨셉 문서
|
||||
|
||||
> VEXPLOR(현행) → INVY.ONE(목표) 전환을 위한 컨셉 정리
|
||||
> 작성일: 2026-04-06
|
||||
|
||||
---
|
||||
|
||||
## 1. 비전
|
||||
|
||||
**"사용자가 직접 조합하는 엔터프라이즈 대시보드 플랫폼"**
|
||||
|
||||
현재 VEXPLOR는 관리자가 admin 패널에서 스크린/메뉴를 설정하고, 사용자는 정해진 화면만 사용하는 구조다. INVY.ONE은 이걸 뒤집어서, 사용자가 직접 템플릿을 골라 대시보드를 구성하고, 개발자가 컴포넌트를 조합해 새 템플릿을 만드는 **노코드 대시보드 빌더**로 전환한다.
|
||||
|
||||
### 핵심 키워드
|
||||
- **대시보드 중심** (메뉴 중심 → 대시보드 중심)
|
||||
- **사용자 자율 구성** (admin 의존 → 셀프서비스)
|
||||
- **템플릿 마켓플레이스** (고정 페이지 → 선택/조합)
|
||||
- **전방위 커스터마이징** (테마 토글 → 네비/색상/폰트/배경 자유 설정)
|
||||
|
||||
---
|
||||
|
||||
## 2. 현행 시스템 (VEXPLOR) 요약
|
||||
|
||||
### 기술 스택
|
||||
| 레이어 | 스택 |
|
||||
|--------|------|
|
||||
| Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS 4, shadcn/ui |
|
||||
| Backend | Express.js (Node), TypeScript, JWT, RBAC |
|
||||
| DB | PostgreSQL (~280 테이블), 멀티테넌시 (company_code) |
|
||||
| AI | Express.js 마이크로서비스, Google GenAI |
|
||||
| Infra | Docker Compose, Traefik, Kubernetes (Helm) |
|
||||
|
||||
### 이미 갖고 있는 강점
|
||||
- **메타데이터 드리븐 스크린** — DB 메타데이터로 UI를 동적 렌더링
|
||||
- **컴포넌트 레지스트리** — 86개 동적 컴포넌트 (테이블, 차트, 카드, 위젯 등)
|
||||
- **풍부한 백엔드** — 결재, 데이터플로우, 바코드, 리포트, 멀티DB, 배치
|
||||
- **3단계 권한** — SUPER_ADMIN / COMPANY_ADMIN / USER
|
||||
- **다국어** — KR/EN 지원 인프라
|
||||
|
||||
### 현행의 한계
|
||||
- 대시보드가 고정형 (1~2개, 미리 정해진 레이아웃)
|
||||
- UI 구성 변경은 개발자/admin 패널 통해야만 가능
|
||||
- 커스터마이징이 라이트/다크 테마 토글 정도
|
||||
- 사용자가 자기 업무에 맞게 화면을 조합할 수 없음
|
||||
|
||||
---
|
||||
|
||||
## 3. 목표 시스템 (INVY.ONE) 컨셉
|
||||
|
||||
### 3.1 멀티 대시보드
|
||||
|
||||
```
|
||||
┌─ 대시보드 관리 (사이드바) ──────────┐
|
||||
│ │
|
||||
│ 📊 메인 대시보드 (기본) ← 활성 │
|
||||
│ 📋 인사관리 │
|
||||
│ 📦 재고현황 │
|
||||
│ 💰 매출분석 │
|
||||
│ + 새 대시보드 추가 │
|
||||
│ │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 사용자별로 여러 대시보드 생성/삭제/이름변경
|
||||
- 대시보드마다 독립적인 템플릿 그리드 배치
|
||||
- 기본 대시보드 설정 가능
|
||||
- 대시보드 데이터는 DB 저장 (현재 localStorage → DB로)
|
||||
|
||||
### 3.2 템플릿 마켓플레이스
|
||||
|
||||
사용자가 대시보드에 추가할 템플릿을 고르는 모달.
|
||||
|
||||
```
|
||||
┌─ 템플릿 추가 ────────────────────────────────────────┐
|
||||
│ 🔍 [컴포넌트, 템플릿 검색...] [검색] │
|
||||
│ │
|
||||
│ 카테고리: 🏠전체 🧩레이아웃 🗄️데이터 ✏️폼 │
|
||||
│ 📊차트 🧭네비 🛒커머스 💬커뮤니티 ⚙️시스템 │
|
||||
│ │
|
||||
│ 태그: [대시보드] [ERP] [MES] [인사/급여] [재고]... │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 인사관리 │ │ 영업대시 │ │ 재고현황 │ │
|
||||
│ │ 👥 │ │ 📈 │ │ 📦 │ │
|
||||
│ │ [추가] │ │ [추가] │ │ [추가] │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **카테고리 아이콘 탭** — 레이아웃, 데이터, 폼, 차트, 커머스 등
|
||||
- **태그 필터** — ERP, MES, SCM, 인사/급여, 재고/물류, 회계 등
|
||||
- **키워드 검색**
|
||||
- **템플릿 카드** — 미리보기 + 설명 + 추가 버튼
|
||||
- 기존 레지스트리의 86개 컴포넌트를 템플릿 단위로 묶어서 제공
|
||||
|
||||
### 3.3 대시보드 그리드 편집
|
||||
|
||||
```
|
||||
┌─ 편집 모드 ON ──────────────────────────────────┐
|
||||
│ [✏️편집] [📄추가] [💾저장] │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ 인사 현황 (2x1) │ │ 매출 차트 (1x1) │ │
|
||||
│ │ 드래그 이동 ↕ │ │ │ │
|
||||
│ │ 리사이즈 ↔ │ │ │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ 재고 테이블 (full-width) │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 편집 모드 토글 (평소에는 잠김)
|
||||
- 드래그앤드롭으로 위치 변경 (react-grid-layout 활용 — 이미 의존성 있음)
|
||||
- 리사이즈 핸들 (1x1, 2x1, 1x2, 2x2, 3x1, full-width)
|
||||
- 템플릿 삭제/추가
|
||||
- 저장 시 레이아웃 정보 DB 저장
|
||||
|
||||
### 3.4 개발자 모드
|
||||
|
||||
일반 사용자가 아닌 개발자/admin이 사용하는 템플릿 제작 도구.
|
||||
|
||||
```
|
||||
┌─ 개발자 모드 ─────────────────────────────────────────────┐
|
||||
│ invy.one [개발자] 템플릿명: [________] 태그: [___] [저장] │
|
||||
├───────────┬───────────────────────┬───────────────────────┤
|
||||
│ 컴포넌트 │ │ 속성 편집기 │
|
||||
│ 팔레트 │ 캔버스 (프리뷰) │ │
|
||||
│ │ │ - 컬럼 수 │
|
||||
│ 📊 차트 │ ┌───┐ ┌───┐ │ - 배경색 │
|
||||
│ 📋 테이블 │ │ │ │ │ │ - 패딩 │
|
||||
│ 📝 폼 │ └───┘ └───┘ │ - 데이터 소스 │
|
||||
│ 🔘 버튼 │ ┌───────────┐ │ │
|
||||
│ ... │ │ │ │ │
|
||||
│ │ └───────────┘ │ │
|
||||
├───────────┴───────────────────────┴───────────────────────┤
|
||||
│ [사용자모드로 돌아가기] │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 좌측: 원자 컴포넌트 팔레트 (기존 레지스트리 86개 활용)
|
||||
- 가운데: 드래그로 배치하는 캔버스
|
||||
- 우측: 선택된 컴포넌트 속성 편집 (데이터 바인딩 포함)
|
||||
- 저장하면 템플릿 마켓에 등록됨
|
||||
- 기존 스크린 메타데이터 시스템을 재활용할 수 있음
|
||||
|
||||
### 3.5 옵션/커스터마이징 패널
|
||||
|
||||
```
|
||||
┌─ ⚙ 화면 설정 ────────────────────┐
|
||||
│ │
|
||||
│ 네비게이션 바 위치 │
|
||||
│ [상단] [좌측] [우측] [하단] │
|
||||
│ │
|
||||
│ 테마 색상 │
|
||||
│ 🔵🟢🔴🟡🟣 + 커스텀 피커 │
|
||||
│ │
|
||||
│ 네비게이션 바 │
|
||||
│ 배경: 🔲⬛🟫🔵... │
|
||||
│ 글꼴: ⬛⬜🔵... │
|
||||
│ 아이콘: ⬛⬜🔵... │
|
||||
│ │
|
||||
│ 글꼴: [Pretendard ▾] │
|
||||
│ 글꼴 크기: [작게] [보통] [크게] │
|
||||
│ 배경 색상: 🔲⬜⬛🔷 │
|
||||
│ │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 네비바 위치 4방향
|
||||
- 테마 프라이머리 색상 (프리셋 8색 + 커스텀)
|
||||
- 네비바 배경/글꼴/아이콘 색상 각각 지정
|
||||
- 글꼴 패밀리 선택 (Pretendard, 맑은고딕, Noto Sans 등)
|
||||
- 글꼴 크기 (프리셋 + 직접 입력)
|
||||
- 배경 색상
|
||||
- 설정값은 사용자별 DB 저장
|
||||
|
||||
---
|
||||
|
||||
## 4. 현행 자산 활용 전략
|
||||
|
||||
INVY.ONE을 처음부터 만드는 게 아니라, 현행 시스템 위에 얹는다.
|
||||
|
||||
### 그대로 쓰는 것
|
||||
| 현행 자산 | INVY.ONE에서의 역할 |
|
||||
|----------|-------------------|
|
||||
| Express 백엔드 (94 라우트) | API 서버 그대로 유지 |
|
||||
| PostgreSQL 280 테이블 | 데이터 레이어 그대로 |
|
||||
| JWT + RBAC 인증 | 권한 체계 그대로 |
|
||||
| React Query + Zustand | 상태관리 그대로 |
|
||||
| shadcn/ui 35개 컴포넌트 | UI 기본 블록 그대로 |
|
||||
| 결재/데이터플로우/바코드/리포트 | 기능 모듈 그대로 |
|
||||
| AI Assistant 마이크로서비스 | AI 기능 그대로 |
|
||||
|
||||
### 확장/변형하는 것
|
||||
| 현행 자산 | 변형 방향 |
|
||||
|----------|----------|
|
||||
| 컴포넌트 레지스트리 (86개) | 템플릿 마켓의 원자 컴포넌트로 재포장 |
|
||||
| 스크린 메타데이터 시스템 | 개발자 모드의 템플릿 저장/로드에 활용 |
|
||||
| AppLayout (고정형) | 네비 위치 4방향 전환 가능하게 리팩토링 |
|
||||
| 테마 시스템 (라이트/다크) | 프라이머리 색상 + 네비 색상 + 폰트 확장 |
|
||||
| 대시보드 (고정 1~2개) | 멀티 대시보드 + 그리드 편집 |
|
||||
|
||||
### 새로 만드는 것
|
||||
| 신규 기능 | 설명 |
|
||||
|----------|------|
|
||||
| 대시보드 CRUD API | 사용자별 대시보드 생성/삭제/레이아웃 저장 |
|
||||
| 템플릿 마켓 모달 | 카테고리/태그/검색으로 템플릿 선택 |
|
||||
| 그리드 편집 모드 | 드래그/리사이즈/저장 (react-grid-layout) |
|
||||
| 개발자 모드 페이지 | 3패널 템플릿 빌더 |
|
||||
| 옵션 패널 | 네비위치/색상/폰트/배경 설정 UI |
|
||||
| 사용자 설정 API | 옵션 값 DB 저장/로드 |
|
||||
|
||||
---
|
||||
|
||||
## 5. DB 확장 (예상)
|
||||
|
||||
### 신규 테이블
|
||||
|
||||
```sql
|
||||
-- 사용자별 대시보드
|
||||
CREATE TABLE user_dashboard (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company_code VARCHAR(20),
|
||||
user_id VARCHAR(50),
|
||||
name VARCHAR(100), -- 대시보드 이름
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 대시보드에 배치된 템플릿
|
||||
CREATE TABLE dashboard_component (
|
||||
id SERIAL PRIMARY KEY,
|
||||
dashboard_id INT REFERENCES user_dashboard(id),
|
||||
template_id VARCHAR(50), -- 템플릿 ID
|
||||
grid_x INT DEFAULT 0, -- 그리드 X 위치
|
||||
grid_y INT DEFAULT 0, -- 그리드 Y 위치
|
||||
grid_w INT DEFAULT 1, -- 그리드 너비
|
||||
grid_h INT DEFAULT 1, -- 그리드 높이
|
||||
config JSONB DEFAULT '{}', -- 템플릿별 설정값
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 템플릿 정의 (개발자 모드에서 제작한 것)
|
||||
CREATE TABLE template_definition (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
company_code VARCHAR(20),
|
||||
name VARCHAR(100),
|
||||
description TEXT,
|
||||
category VARCHAR(50), -- 카테고리
|
||||
tags TEXT[], -- 태그 배열
|
||||
icon VARCHAR(10), -- 이모지 아이콘
|
||||
components JSONB, -- 구성 컴포넌트 정의
|
||||
is_system BOOLEAN DEFAULT false, -- 시스템 기본 vs 사용자 제작
|
||||
created_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 사용자별 UI 설정
|
||||
CREATE TABLE user_ui_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company_code VARCHAR(20),
|
||||
user_id VARCHAR(50) UNIQUE,
|
||||
nav_position VARCHAR(10) DEFAULT 'left', -- top/left/right/bottom
|
||||
theme_color VARCHAR(7) DEFAULT '#4a6cf7', -- 프라이머리 색상
|
||||
nav_bg_color VARCHAR(7) DEFAULT '#ffffff',
|
||||
nav_text_color VARCHAR(7) DEFAULT '#333333',
|
||||
nav_icon_color VARCHAR(7) DEFAULT '#333333',
|
||||
font_family VARCHAR(100) DEFAULT 'Pretendard',
|
||||
font_size INT DEFAULT 14,
|
||||
bg_color VARCHAR(7) DEFAULT '#f5f6f8',
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 화면 흐름
|
||||
|
||||
### 사용자 플로우
|
||||
|
||||
```
|
||||
로그인
|
||||
│
|
||||
▼
|
||||
메인 대시보드 (기본 대시보드 로드)
|
||||
│
|
||||
├─ 햄버거 → 사이드바 열기 → 대시보드 전환/추가/삭제
|
||||
│
|
||||
├─ [✏️편집] → 편집 모드 ON
|
||||
│ ├─ [📄추가] → 템플릿 마켓 모달
|
||||
│ │ ├─ 카테고리/태그/검색으로 필터
|
||||
│ │ └─ 템플릿 선택 → 그리드에 추가
|
||||
│ ├─ 드래그로 위치 변경
|
||||
│ ├─ 리사이즈 핸들로 크기 변경
|
||||
│ ├─ 템플릿 X 버튼으로 삭제
|
||||
│ └─ [💾저장] → 레이아웃 DB 저장
|
||||
│
|
||||
├─ ⚙️옵션 → 옵션 패널
|
||||
│ ├─ 네비 위치 변경
|
||||
│ ├─ 색상/폰트 변경
|
||||
│ └─ 즉시 반영 (저장 시 DB 저장)
|
||||
│
|
||||
├─ 🛠️개발자 → 개발자 모드 (admin/개발자만)
|
||||
│ ├─ 컴포넌트 드래그 → 캔버스 배치
|
||||
│ ├─ 속성 편집
|
||||
│ └─ 저장 → template_definition에 저장
|
||||
│
|
||||
├─ 👤프로필 → 프로필 패널
|
||||
│
|
||||
└─ 🏢로고 클릭 → 회사 정보 관리 (admin만)
|
||||
```
|
||||
|
||||
### 모드 구분
|
||||
|
||||
| 모드 | 대상 | 기능 |
|
||||
|------|------|------|
|
||||
| **사용자 모드** | 전체 사용자 | 대시보드 사용, 템플릿 배치/편집, 옵션 설정 |
|
||||
| **개발자 모드** | admin/개발자 | 컴포넌트 조합으로 새 템플릿 제작 |
|
||||
| **관리자 기능** | admin | 회사 정보, 로고/인장, 시스템 설정 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 구현 우선순위 (제안)
|
||||
|
||||
### Phase 1 — 기반 (대시보드 + 옵션)
|
||||
1. `user_dashboard`, `dashboard_component`, `user_ui_settings` 테이블 생성
|
||||
2. 멀티 대시보드 CRUD API
|
||||
3. 대시보드 그리드 편집 (react-grid-layout)
|
||||
4. 옵션 패널 (네비 위치, 색상, 폰트)
|
||||
5. AppLayout 리팩토링 (네비 4방향 전환)
|
||||
|
||||
### Phase 2 — 템플릿 마켓
|
||||
1. `template_definition` 테이블 + 시스템 기본 템플릿 시드
|
||||
2. 템플릿 마켓 모달 (카테고리/태그/검색)
|
||||
3. 기존 레지스트리 컴포넌트를 템플릿으로 패키징
|
||||
4. 템플릿 → 대시보드 추가 연동
|
||||
|
||||
### Phase 3 — 개발자 모드
|
||||
1. 개발자 모드 페이지 (3패널 레이아웃)
|
||||
2. 컴포넌트 팔레트 (드래그)
|
||||
3. 캔버스 (배치/프리뷰)
|
||||
4. 속성 편집기 (데이터 바인딩)
|
||||
5. 템플릿 저장 → 마켓 등록 연동
|
||||
|
||||
### Phase 4 — 고도화
|
||||
1. 템플릿 공유/복제 (회사 간)
|
||||
2. 대시보드 내보내기/가져오기
|
||||
3. 실시간 데이터 바인딩 (WebSocket)
|
||||
4. 모바일 대시보드 최적화
|
||||
5. AI 추천 (사용 패턴 기반 템플릿 추천)
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고
|
||||
|
||||
- INVY.ONE 프로토타입 위치: `~/다운로드/INVYONE개발/`
|
||||
- 기술: 순수 Vanilla HTML/CSS/JS (프레임워크 없음, localStorage 저장)
|
||||
- 프로토타입은 UX 방향 참고용이며, 실제 구현은 현행 Next.js + Express 위에 진행
|
||||
@@ -327,7 +327,7 @@ const res = await getFlowDefinitions(); // ✅
|
||||
| 환경 | 프론트엔드 | 백엔드 API |
|
||||
|------|-----------|-----------|
|
||||
| 로컬 개발 | localhost:9771 | localhost:8080/api |
|
||||
| 운영 | v1.vexplor.com | api.vexplor.com/api |
|
||||
| 운영 | v1.invion.com | api.invion.com/api |
|
||||
|
||||
### 5.5 상태 관리 체계
|
||||
|
||||
|
||||
@@ -1695,8 +1695,8 @@ const getCorsOrigin = () => {
|
||||
return [
|
||||
'http://localhost:9771',
|
||||
'http://39.117.244.52:5555',
|
||||
'https://v1.vexplor.com',
|
||||
'https://api.vexplor.com'
|
||||
'https://v1.invion.com',
|
||||
'https://api.invion.com'
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# vexplor 프로젝트 NCP Kubernetes 배포 가이드
|
||||
# invion 프로젝트 NCP Kubernetes 배포 가이드
|
||||
|
||||
## 배포 환경
|
||||
- **Kubernetes 클러스터**: NCP Kubernetes
|
||||
@@ -30,7 +30,7 @@
|
||||
```
|
||||
안녕하세요.
|
||||
|
||||
vexplor 프로젝트 배포를 위해 다음 작업이 필요합니다:
|
||||
invion 프로젝트 배포를 위해 다음 작업이 필요합니다:
|
||||
|
||||
1. helm-charts 레포지토리 접근 권한 부여
|
||||
- 레포지토리: https://gitlab.kpslp.kr/root/helm-charts
|
||||
@@ -38,19 +38,19 @@ vexplor 프로젝트 배포를 위해 다음 작업이 필요합니다:
|
||||
- 계정: [본인 GitLab 사용자명]
|
||||
|
||||
2. values 파일 업로드
|
||||
- 첨부된 values_vexplor.yaml 파일을
|
||||
- kpslp/values_vexplor.yaml 경로에 업로드해주시거나
|
||||
- 첨부된 values_invion.yaml 파일을
|
||||
- kpslp/values_invion.yaml 경로에 업로드해주시거나
|
||||
- 업로드 방법을 안내해주세요
|
||||
|
||||
3. Jenkins 프로젝트 생성
|
||||
- 프로젝트명: vexplor
|
||||
- 프로젝트명: invion
|
||||
- Git 레포지토리: [현재 프로젝트 GitLab URL]
|
||||
- Jenkinsfile: 프로젝트 루트에 이미 준비됨
|
||||
|
||||
감사합니다.
|
||||
```
|
||||
|
||||
**첨부 파일**: `values_vexplor.yaml` (프로젝트 루트에 생성됨)
|
||||
**첨부 파일**: `values_invion.yaml` (프로젝트 루트에 생성됨)
|
||||
|
||||
---
|
||||
|
||||
@@ -60,7 +60,7 @@ Jenkins에서 새 파이프라인 프로젝트를 생성합니다:
|
||||
|
||||
1. **Jenkins 접속** (URL은 담당자에게 문의)
|
||||
2. **New Item** 클릭
|
||||
3. **프로젝트명**: `vexplor`
|
||||
3. **프로젝트명**: `invion`
|
||||
4. **Pipeline** 선택
|
||||
5. **Pipeline 설정**:
|
||||
- Definition: `Pipeline script from SCM`
|
||||
@@ -77,7 +77,7 @@ Jenkins에서 새 파이프라인 프로젝트를 생성합니다:
|
||||
1. **Argo CD 접속**: https://argocd.kpslp.kr
|
||||
|
||||
2. **New App 생성**:
|
||||
- **Application Name**: `vexplor`
|
||||
- **Application Name**: `invion`
|
||||
- **Project**: `default`
|
||||
- **Sync Policy**: `Automatic` (자동 배포) 또는 `Manual` (수동 배포)
|
||||
- **Auto-Create Namespace**: ✓ (체크)
|
||||
@@ -86,7 +86,7 @@ Jenkins에서 새 파이프라인 프로젝트를 생성합니다:
|
||||
- **Repository URL**: `https://gitlab.kpslp.kr/root/helm-charts`
|
||||
- **Revision**: `HEAD` 또는 `main`
|
||||
- **Path**: `kpslp`
|
||||
- **Helm Values**: `values_vexplor.yaml`
|
||||
- **Helm Values**: `values_invion.yaml`
|
||||
|
||||
4. **Destination 설정**:
|
||||
- **Cluster URL**: `https://kubernetes.default.svc` (기본값)
|
||||
@@ -106,15 +106,15 @@ git push origin main
|
||||
```
|
||||
|
||||
#### 4-2. Jenkins 빌드 모니터링
|
||||
1. Jenkins에서 `vexplor` 프로젝트 열기
|
||||
1. Jenkins에서 `invion` 프로젝트 열기
|
||||
2. 빌드 시작 확인 (자동 트리거 또는 수동 빌드)
|
||||
3. 로그 확인:
|
||||
- **Checkout**: Git 소스 다운로드
|
||||
- **Build**: Docker 이미지 빌드 (`registry.kpslp.kr/slp/vexplor:xxxxx`)
|
||||
- **Build**: Docker 이미지 빌드 (`registry.kpslp.kr/slp/invion:xxxxx`)
|
||||
- **Update Image Tag**: helm-charts 레포의 values 파일 업데이트
|
||||
|
||||
#### 4-3. Argo CD 배포 확인
|
||||
1. Argo CD 대시보드에서 `vexplor` 앱 열기
|
||||
1. Argo CD 대시보드에서 `invion` 앱 열기
|
||||
2. **Sync Status**: `OutOfSync` → `Synced` 변경 확인
|
||||
3. **Health Status**: `Progressing` → `Healthy` 변경 확인
|
||||
4. Pod 상태 확인 (Running 상태여야 함)
|
||||
@@ -125,38 +125,38 @@ git push origin main
|
||||
|
||||
### 1. Pod 상태 확인
|
||||
```bash
|
||||
kubectl get pods -n apps | grep vexplor
|
||||
kubectl get pods -n apps | grep invion
|
||||
```
|
||||
**예상 출력**:
|
||||
```
|
||||
vexplor-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
|
||||
invion-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
|
||||
```
|
||||
|
||||
### 2. 서비스 확인
|
||||
```bash
|
||||
kubectl get svc -n apps | grep vexplor
|
||||
kubectl get svc -n apps | grep invion
|
||||
```
|
||||
|
||||
### 3. Ingress 확인
|
||||
```bash
|
||||
kubectl get ingress -n apps | grep vexplor
|
||||
kubectl get ingress -n apps | grep invion
|
||||
```
|
||||
|
||||
### 4. 로그 확인
|
||||
```bash
|
||||
# 전체 로그
|
||||
kubectl logs -n apps -l app=vexplor
|
||||
kubectl logs -n apps -l app=invion
|
||||
|
||||
# 최근 50줄
|
||||
kubectl logs -n apps -l app=vexplor --tail=50
|
||||
kubectl logs -n apps -l app=invion --tail=50
|
||||
|
||||
# 실시간 로그 (스트리밍)
|
||||
kubectl logs -n apps -l app=vexplor -f
|
||||
kubectl logs -n apps -l app=invion -f
|
||||
```
|
||||
|
||||
### 5. 애플리케이션 접속
|
||||
- **URL**: `https://vexplor.kpslp.kr` (values 파일에 설정한 도메인)
|
||||
- **헬스체크**: `https://vexplor.kpslp.kr/api/health`
|
||||
- **URL**: `https://invion.kpslp.kr` (values 파일에 설정한 도메인)
|
||||
- **헬스체크**: `https://invion.kpslp.kr/api/health`
|
||||
|
||||
---
|
||||
|
||||
@@ -173,7 +173,7 @@ kubectl logs -n apps -l app=vexplor -f
|
||||
**해결**:
|
||||
```bash
|
||||
# 로컬에서 Docker 빌드 테스트
|
||||
docker build -f Dockerfile -t vexplor:test .
|
||||
docker build -f Dockerfile -t invion:test .
|
||||
```
|
||||
|
||||
### 문제 2: helm-charts 레포 푸시 실패
|
||||
@@ -187,7 +187,7 @@ docker build -f Dockerfile -t vexplor:test .
|
||||
**증상**: `OutOfSync` 상태에서 변경 없음
|
||||
|
||||
**확인사항**:
|
||||
- values 파일이 올바른 경로에 있는지 (`kpslp/values_vexplor.yaml`)
|
||||
- values 파일이 올바른 경로에 있는지 (`kpslp/values_invion.yaml`)
|
||||
- Argo CD가 helm-charts 레포를 읽을 수 있는지
|
||||
|
||||
**해결**: Argo CD에서 수동 Sync 시도 또는 담당자에게 문의
|
||||
@@ -207,7 +207,7 @@ kubectl logs -n apps [pod-name] --previous
|
||||
- 포트 바인딩 문제
|
||||
|
||||
**해결**:
|
||||
1. `values_vexplor.yaml`의 `env` 섹션 확인
|
||||
1. `values_invion.yaml`의 `env` 섹션 확인
|
||||
2. 데이터베이스 서비스명 확인 (`postgres-service.apps.svc.cluster.local`)
|
||||
3. Secret 설정 확인 (DB 비밀번호 등)
|
||||
|
||||
@@ -244,7 +244,7 @@ git push origin main
|
||||
|
||||
- [ ] Jenkinsfile 수정 완료 (단일 이미지 빌드)
|
||||
- [ ] Dockerfile 확인 (멀티스테이지 빌드)
|
||||
- [ ] values_vexplor.yaml 작성 및 업로드
|
||||
- [ ] values_invion.yaml 작성 및 업로드
|
||||
- [ ] Jenkins 프로젝트 생성
|
||||
- [ ] Argo CD 애플리케이션 등록
|
||||
- [ ] 환경 변수 설정 (DATABASE_HOST 등)
|
||||
@@ -278,13 +278,13 @@ git push origin main
|
||||
클러스터 내부에 PostgreSQL이 없다면:
|
||||
|
||||
```yaml
|
||||
# values_vexplor.yaml 에 추가
|
||||
# values_invion.yaml 에 추가
|
||||
postgresql:
|
||||
enabled: true
|
||||
auth:
|
||||
username: vexplor
|
||||
username: invion
|
||||
password: changeme123 # Secret으로 관리 권장
|
||||
database: vexplor
|
||||
database: invion
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
@@ -293,7 +293,7 @@ postgresql:
|
||||
|
||||
### Secret 생성 (민감 정보)
|
||||
```bash
|
||||
kubectl create secret generic vexplor-secrets \
|
||||
kubectl create secret generic invion-secrets \
|
||||
--from-literal=db-password='your-secure-password' \
|
||||
--from-literal=jwt-secret='your-jwt-secret' \
|
||||
-n apps
|
||||
|
||||
@@ -699,9 +699,9 @@ const getApiBaseUrl = (): string => {
|
||||
// 환경변수 우선
|
||||
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// 프로덕션: v1.vexplor.com → api.vexplor.com
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return "https://api.vexplor.com/api";
|
||||
// 프로덕션: v1.invion.com → api.invion.com
|
||||
if (currentHost === "v1.invion.com") {
|
||||
return "https://api.invion.com/api";
|
||||
}
|
||||
|
||||
// 로컬: localhost:9771 → localhost:8080
|
||||
|
||||
@@ -107,7 +107,7 @@ logger.info("✅ 좌측 사이드바: 공통 메뉴만 표시");
|
||||
|
||||
# ❌ 표시되지 않음
|
||||
- ILSHIN 전용 메뉴
|
||||
- VEXPLOR 전용 메뉴
|
||||
- INVION 전용 메뉴
|
||||
```
|
||||
|
||||
#### 메뉴 관리 화면 (`/admin/menus`)
|
||||
@@ -115,7 +115,7 @@ logger.info("✅ 좌측 사이드바: 공통 메뉴만 표시");
|
||||
```bash
|
||||
# ✅ 모든 회사 메뉴 표시
|
||||
- ILSHIN 전용 메뉴 (company_code='ILSHIN')
|
||||
- VEXPLOR 전용 메뉴 (company_code='VEXPLOR')
|
||||
- INVION 전용 메뉴 (company_code='INVION')
|
||||
- 공통 메뉴 (company_code IS NULL)
|
||||
```
|
||||
|
||||
@@ -141,7 +141,7 @@ logger.info("✅ 좌측 사이드바: 공통 메뉴만 표시");
|
||||
- 공통 메뉴 (company_code IS NULL)
|
||||
|
||||
# ❌ 표시되지 않음
|
||||
- VEXPLOR 전용 메뉴
|
||||
- INVION 전용 메뉴
|
||||
```
|
||||
|
||||
### 시나리오 3: 일반 사용자 로그인
|
||||
@@ -216,7 +216,7 @@ logger.info("✅ 좌측 사이드바: 공통 메뉴만 표시");
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "menu_name_kor": "ILSHIN 메뉴", "company_code": "ILSHIN" }, // ✅ 전체
|
||||
{ "menu_name_kor": "VEXPLOR 메뉴", "company_code": "VEXPLOR" }, // ✅ 전체
|
||||
{ "menu_name_kor": "INVION 메뉴", "company_code": "INVION" }, // ✅ 전체
|
||||
{ "menu_name_kor": "공통 메뉴", "company_code": null } // ✅ 전체
|
||||
]
|
||||
}
|
||||
@@ -232,7 +232,7 @@ logger.info("✅ 좌측 사이드바: 공통 메뉴만 표시");
|
||||
"data": [
|
||||
{ "menu_name_kor": "ILSHIN 메뉴", "company_code": "ILSHIN" }, // ✅ 자기 회사
|
||||
{ "menu_name_kor": "공통 메뉴", "company_code": null } // ✅ 공통
|
||||
// ❌ VEXPLOR 메뉴 없음
|
||||
// ❌ INVION 메뉴 없음
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -246,8 +246,8 @@ SUPER_ADMIN 로그인
|
||||
└─ 좌측 사이드바
|
||||
├─ ILSHIN 대시보드 ← 혼란스러움
|
||||
├─ ILSHIN 보고서 ← 혼란스러움
|
||||
├─ VEXPLOR 대시보드 ← 혼란스러움
|
||||
├─ VEXPLOR 보고서 ← 혼란스러움
|
||||
├─ INVION 대시보드 ← 혼란스러움
|
||||
├─ INVION 보고서 ← 혼란스러움
|
||||
└─ 공통 설정
|
||||
```
|
||||
|
||||
@@ -263,8 +263,8 @@ SUPER_ADMIN 로그인
|
||||
└─ 메뉴 관리 화면 (/admin/menus)
|
||||
├─ ILSHIN 대시보드 ← 관리 목적
|
||||
├─ ILSHIN 보고서 ← 관리 목적
|
||||
├─ VEXPLOR 대시보드 ← 관리 목적
|
||||
├─ VEXPLOR 보고서 ← 관리 목적
|
||||
├─ INVION 대시보드 ← 관리 목적
|
||||
├─ INVION 보고서 ← 관리 목적
|
||||
└─ 공통 설정
|
||||
```
|
||||
|
||||
@@ -292,7 +292,7 @@ SUPER_ADMIN 로그인
|
||||
### 회사 전용 메뉴
|
||||
|
||||
- 특정 회사에서만 사용하는 메뉴
|
||||
- `company_code`에 회사 코드 지정 (예: `ILSHIN`, `VEXPLOR`)
|
||||
- `company_code`에 회사 코드 지정 (예: `ILSHIN`, `INVION`)
|
||||
- 좌측 사이드바에는 표시되지 않음
|
||||
- 메뉴 관리 화면에서만 표시됨
|
||||
|
||||
@@ -307,7 +307,7 @@ SUPER_ADMIN이 특정 회사 컨텍스트로 전환하여 해당 회사 메뉴
|
||||
<Select value={currentCompany} onChange={switchCompany}>
|
||||
<option value="*">전체 (관리자 모드)</option>
|
||||
<option value="ILSHIN">ILSHIN</option>
|
||||
<option value="VEXPLOR">VEXPLOR</option>
|
||||
<option value="INVION">INVION</option>
|
||||
</Select>
|
||||
```
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ CREATE TABLE menu_info (
|
||||
# 로그인 사용자: admin (SUPER_ADMIN, company_code='*')
|
||||
# 예상 메뉴:
|
||||
# - ILSHIN 회사 메뉴
|
||||
# - VEXPLOR 회사 메뉴
|
||||
# - INVION 회사 메뉴
|
||||
# - 공통 메뉴 (company_code IS NULL)
|
||||
```
|
||||
|
||||
@@ -107,17 +107,17 @@ CREATE TABLE menu_info (
|
||||
# 예상 메뉴:
|
||||
# - ILSHIN 회사 메뉴
|
||||
# - 공통 메뉴 (company_code IS NULL)
|
||||
# ❌ VEXPLOR 회사 메뉴 (표시 안 됨)
|
||||
# ❌ INVION 회사 메뉴 (표시 안 됨)
|
||||
```
|
||||
|
||||
### 테스트 3: COMPANY_ADMIN 로그인 (VEXPLOR)
|
||||
### 테스트 3: COMPANY_ADMIN 로그인 (INVION)
|
||||
|
||||
**기대 결과**: VEXPLOR 회사 메뉴 + 공통 메뉴만 표시
|
||||
**기대 결과**: INVION 회사 메뉴 + 공통 메뉴만 표시
|
||||
|
||||
```bash
|
||||
# 로그인 사용자: vexplor_admin (COMPANY_ADMIN, company_code='VEXPLOR')
|
||||
# 로그인 사용자: invion_admin (COMPANY_ADMIN, company_code='INVION')
|
||||
# 예상 메뉴:
|
||||
# - VEXPLOR 회사 메뉴
|
||||
# - INVION 회사 메뉴
|
||||
# - 공통 메뉴 (company_code IS NULL)
|
||||
# ❌ ILSHIN 회사 메뉴 (표시 안 됨)
|
||||
```
|
||||
@@ -213,7 +213,7 @@ GET /api/admin/menus?menuType=1
|
||||
"menu_name_kor": "공통 설정",
|
||||
"company_code": null // ✅ 공통 메뉴
|
||||
}
|
||||
// ❌ VEXPLOR 메뉴는 없음
|
||||
// ❌ INVION 메뉴는 없음
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ export function LoginHeader() {
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 flex items-center justify-center">
|
||||
<Image
|
||||
src="/images/vexplor.png"
|
||||
src="/images/invion.png"
|
||||
alt={UI_CONFIG.COMPANY_NAME}
|
||||
width={180}
|
||||
height={60}
|
||||
|
||||
@@ -622,15 +622,16 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 펼친 상태의 하위 메뉴 */}
|
||||
{!sidebarCollapsed && menu.hasChildren && isExpanded && (
|
||||
<div className="v5-si-child" style={{ paddingLeft: "1.5rem", display: "flex", flexDirection: "column", gap: "1px" }}>
|
||||
{menu.children?.map((child: any) => (
|
||||
{/* 하위 메뉴 — 항상 렌더링, CSS로 높이 제어 */}
|
||||
{!sidebarCollapsed && menu.hasChildren && (
|
||||
<div className={`v5-submenu ${isExpanded ? "expanded" : ""}`}>
|
||||
{menu.children?.map((child: any, idx: number) => (
|
||||
<div
|
||||
key={child.id}
|
||||
draggable={!child.hasChildren}
|
||||
onDragStart={(e) => handleMenuDragStart(e, child)}
|
||||
className={`v5-si ${isMenuActive(child) ? "on" : ""}`}
|
||||
className={`v5-si v5-sub-item ${isMenuActive(child) ? "on" : ""}`}
|
||||
style={{ transitionDelay: isExpanded ? `${idx * 30}ms` : "0ms" }}
|
||||
onClick={() => handleMenuClick(child)}
|
||||
>
|
||||
<span className="ic">{child.icon}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@ export function Logo() {
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<Image
|
||||
src="/images/vexplor.png"
|
||||
src="/images/invion.png"
|
||||
alt="WACE 솔루션 로고"
|
||||
width={120}
|
||||
height={32}
|
||||
|
||||
@@ -16,9 +16,9 @@ export const AUTH_CONFIG = {
|
||||
} as const;
|
||||
|
||||
export const UI_CONFIG = {
|
||||
COMPANY_NAME: "VEXPLOR",
|
||||
COPYRIGHT: "© 2024 VEXPLOR. All rights reserved.",
|
||||
POWERED_BY: "Powered by Vexplor",
|
||||
COMPANY_NAME: "INVION",
|
||||
COPYRIGHT: "© 2024 INVION. All rights reserved.",
|
||||
POWERED_BY: "Powered by Invion",
|
||||
} as const;
|
||||
|
||||
export const FORM_VALIDATION = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AI 어시스턴트 메뉴 등록 가이드 (VEXPLOR)
|
||||
# AI 어시스턴트 메뉴 등록 가이드 (INVION)
|
||||
|
||||
AI 어시스턴트는 **VEXPLOR와 같은 서비스/같은 포트**로 동작합니다.
|
||||
AI 어시스턴트는 **INVION와 같은 서비스/같은 포트**로 동작합니다.
|
||||
프론트는 `/api/ai/v1` 로 호출하고, backend-node가 AI 서비스(기본 3100 포트)로 프록시합니다.
|
||||
|
||||
## 서비스 기동
|
||||
@@ -13,7 +13,7 @@ AI 어시스턴트는 **VEXPLOR와 같은 서비스/같은 포트**로 동작합
|
||||
|
||||
---
|
||||
|
||||
## VEXPLOR 메뉴 URL 목록 (전체 탑재)
|
||||
## INVION 메뉴 URL 목록 (전체 탑재)
|
||||
|
||||
대메뉴 예: **AI 서비스** / **AI**
|
||||
소메뉴는 아래 표의 **메뉴명**과 **URL**로 등록하면 됩니다. (메뉴명에 "AI", "어시스턴트", "챗봇", "LLM" 포함 시 사이드바에 Bot 아이콘 표시)
|
||||
@@ -47,4 +47,4 @@ AI 어시스턴트는 **VEXPLOR와 같은 서비스/같은 포트**로 동작합
|
||||
- 예: 메뉴명 `대시보드`, URL `/admin/aiAssistant/dashboard`
|
||||
- 예: 메뉴명 `API 키 관리`, URL `/admin/aiAssistant/api-keys`
|
||||
|
||||
이렇게 등록하면 VEXPLOR 사이드바에서 각 메뉴 클릭 시 해당 AI 어시스턴트 화면이 열립니다.
|
||||
이렇게 등록하면 INVION 사이드바에서 각 메뉴 클릭 시 해당 AI 어시스턴트 화면이 열립니다.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* AI 어시스턴트 전용 API 클라이언트
|
||||
* - VEXPLOR와 같은 서비스/같은 포트: /api/ai/v1 로 호출 (Next → backend-node → AI 서비스 프록시)
|
||||
* - 인증 토큰은 sessionStorage 'ai-assistant-auth' 사용 (VEXPLOR 인증과 분리)
|
||||
* - INVION와 같은 서비스/같은 포트: /api/ai/v1 로 호출 (Next → backend-node → AI 서비스 프록시)
|
||||
* - 인증 토큰은 sessionStorage 'ai-assistant-auth' 사용 (INVION 인증과 분리)
|
||||
*/
|
||||
import axios, { AxiosError } from "axios";
|
||||
import type { AiAssistantAuthState } from "./types";
|
||||
|
||||
@@ -20,8 +20,8 @@ const getApiBaseUrl = (): string => {
|
||||
const currentHost = window.location.hostname;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return "https://api.vexplor.com/api";
|
||||
if (currentHost === "v1.invion.com") {
|
||||
return "https://api.invion.com/api";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -49,8 +49,8 @@ export const getFullImageUrl = (imagePath: string): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return `https://api.vexplor.com${imagePath}`;
|
||||
if (currentHost === "v1.invion.com") {
|
||||
return `https://api.invion.com${imagePath}`;
|
||||
}
|
||||
|
||||
if (currentHost === "localhost" || currentHost === "127.0.0.1") {
|
||||
|
||||
@@ -10,9 +10,9 @@ function getApiBaseUrl(): string {
|
||||
if (typeof window !== "undefined") {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 프로덕션: v1.vexplor.com → https://api.vexplor.com/api
|
||||
if (hostname === "v1.vexplor.com") {
|
||||
return "https://api.vexplor.com/api";
|
||||
// 프로덕션: v1.invion.com → https://api.invion.com/api
|
||||
if (hostname === "v1.invion.com") {
|
||||
return "https://api.invion.com/api";
|
||||
}
|
||||
|
||||
// 로컬 개발: localhost → http://localhost:8081/api
|
||||
|
||||
@@ -262,9 +262,9 @@ export const getDirectFileUrl = (filePath: string): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
// 프로덕션 환경: v1.vexplor.com → api.vexplor.com
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return `https://api.vexplor.com${filePath}`;
|
||||
// 프로덕션 환경: v1.invion.com → api.invion.com
|
||||
if (currentHost === "v1.invion.com") {
|
||||
return `https://api.invion.com${filePath}`;
|
||||
}
|
||||
|
||||
// 로컬 개발환경
|
||||
@@ -274,7 +274,7 @@ export const getDirectFileUrl = (filePath: string): string => {
|
||||
}
|
||||
|
||||
// SSR 또는 알 수 없는 환경에서는 환경변수 사용 (fallback)
|
||||
// 주의: 프로덕션 URL이 https://api.vexplor.com/api 이므로
|
||||
// 주의: 프로덕션 URL이 https://api.invion.com/api 이므로
|
||||
// 단순 .replace("/api", "")는 호스트명의 /api까지 제거하는 버그 발생
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace(/\/api$/, "") || "";
|
||||
if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) {
|
||||
|
||||
@@ -30,9 +30,9 @@ const getApiBaseUrl = (): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
// 프로덕션 환경: v1.vexplor.com → api.vexplor.com
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return "https://api.vexplor.com/api";
|
||||
// 프로덕션 환경: v1.invion.com → api.invion.com
|
||||
if (currentHost === "v1.invion.com") {
|
||||
return "https://api.invion.com/api";
|
||||
}
|
||||
|
||||
// 로컬 개발환경
|
||||
|
||||
@@ -284,7 +284,7 @@ export const FileViewerModal: React.FC<FileViewerModalProps> = ({ file, isOpen,
|
||||
}
|
||||
} else {
|
||||
// 기타 파일은 다운로드 URL 사용
|
||||
// 주의: 프로덕션 URL이 https://api.vexplor.com/api 이므로
|
||||
// 주의: 프로덕션 URL이 https://api.invion.com/api 이므로
|
||||
// 끝의 /api만 제거해야 호스트명이 깨지지 않음
|
||||
const url = `${API_BASE_URL.replace(/\/api$/, "")}/api/files/download/${file.objid}`;
|
||||
setPreviewUrl(url);
|
||||
|
||||
@@ -284,7 +284,7 @@ export const FileViewerModal: React.FC<FileViewerModalProps> = ({ file, isOpen,
|
||||
}
|
||||
} else {
|
||||
// 기타 파일은 다운로드 URL 사용
|
||||
// 주의: 프로덕션 URL이 https://api.vexplor.com/api 이므로
|
||||
// 주의: 프로덕션 URL이 https://api.invion.com/api 이므로
|
||||
// 끝의 /api만 제거해야 호스트명이 깨지지 않음
|
||||
const url = `${API_BASE_URL.replace(/\/api$/, "")}/api/files/download/${file.objid}`;
|
||||
setPreviewUrl(url);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// VEXPLOR Cosmic Design System — 우주 테마 정의
|
||||
// INVION Cosmic Design System — 우주 테마 정의
|
||||
export const theme = {
|
||||
// 색상 팔레트 (Stellar White — 라이트 모드)
|
||||
colors: {
|
||||
|
||||
@@ -8,9 +8,9 @@ export function getApiUrl(endpoint: string): string {
|
||||
if (typeof window !== "undefined") {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 프로덕션: v1.vexplor.com → https://api.vexplor.com
|
||||
if (hostname === "v1.vexplor.com") {
|
||||
return `https://api.vexplor.com${endpoint}`;
|
||||
// 프로덕션: v1.invion.com → https://api.invion.com
|
||||
if (hostname === "v1.invion.com") {
|
||||
return `https://api.invion.com${endpoint}`;
|
||||
}
|
||||
|
||||
// 로컬 개발: localhost → http://localhost:8081
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 9771",
|
||||
"dev": "NODE_OPTIONS='--max-old-space-size=8192' next dev --turbopack -p 9771",
|
||||
"dev:docker": "next dev -p 3000",
|
||||
"build": "next build",
|
||||
"build:no-lint": "DISABLE_ESLINT_PLUGIN=true next build",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -81,7 +81,7 @@ async function verifyScreen(page: any, screenId: number, name: string, type: Scr
|
||||
const hasFlexRow = (await page.locator(".flex-row, .md\\:flex-row, .flex").count()) > 0;
|
||||
const hasGrid = (await page.locator(".grid, [class*='grid-cols']").count()) > 0;
|
||||
const hasMain = (await page.locator("main, [role='main'], .flex-1, [class*='flex-1']").count()) > 0;
|
||||
const hasSidebar = (await page.getByText("현재 관리 회사").count()) > 0 || (await page.getByText("VEXPLOR").count()) > 0;
|
||||
const hasSidebar = (await page.getByText("현재 관리 회사").count()) > 0 || (await page.getByText("INVION").count()) > 0;
|
||||
result.layoutHorizontal = (hasMain && (hasFlexRow || hasGrid || result.tableVisible)) || hasSidebar;
|
||||
|
||||
// 컴포넌트 정상 배치 (테이블, 버튼, 또는 input/필터 중 하나라도 있으면 OK)
|
||||
|
||||
+113
-36
@@ -269,8 +269,7 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo
|
||||
.v5-body{display:flex;flex:1;overflow:hidden;position:relative;z-index:5;}
|
||||
.v5-side{width:var(--v5-sidebar-w);background:var(--v5-glass);backdrop-filter:blur(20px) saturate(1.3);
|
||||
-webkit-backdrop-filter:blur(20px) saturate(1.3);border-right:1px solid var(--v5-glass-border);
|
||||
padding:.85rem .6rem;overflow-y:auto;display:flex;flex-direction:column;gap:1px;flex-shrink:0;
|
||||
transition:width .4s cubic-bezier(.4,0,.2,1),padding .4s,transform .35s cubic-bezier(.16,1,.3,1),opacity .25s;}
|
||||
padding:.85rem .6rem;overflow-y:auto;display:flex;flex-direction:column;gap:1px;flex-shrink:0;}
|
||||
|
||||
.v5-side-sec{font-size:.55rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;
|
||||
color:var(--v5-text-muted);padding:1rem .65rem .35rem;
|
||||
@@ -287,8 +286,9 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo
|
||||
.v5-si.on .ic{opacity:1;}
|
||||
.dark .v5-si.on{background:linear-gradient(135deg,rgba(162,155,254,.14),rgba(162,155,254,.05));border-color:rgba(162,155,254,.15);}
|
||||
.v5-si::before{content:'';position:absolute;left:0;top:0;width:3px;height:100%;background:var(--v5-primary);
|
||||
border-radius:0 2px 2px 0;transform:scaleY(0);transition:transform .2s cubic-bezier(.4,0,.2,1);}
|
||||
.v5-si.on::before{transform:scaleY(1);}
|
||||
border-radius:0 2px 2px 0;transform:scaleY(0);transition:transform .3s cubic-bezier(.16,1,.3,1);}
|
||||
.v5-si:hover::before{transform:scaleY(.5);opacity:.4;}
|
||||
.v5-si.on::before{transform:scaleY(1);opacity:1;}
|
||||
|
||||
/* Sidebar enter animation */
|
||||
.v5-side-anim .v5-si{animation:v5-slideR .4s cubic-bezier(.16,1,.3,1) both;}
|
||||
@@ -303,20 +303,67 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo
|
||||
.v5-side-toggle svg{transition:transform .3s cubic-bezier(.4,0,.2,1);}
|
||||
|
||||
/* ===== SIDEBAR COLLAPSE ===== */
|
||||
.v5-side.collapsed{width:56px;padding:.85rem .4rem;overflow:visible;z-index:30;}
|
||||
.v5-side.collapsed .v5-si{justify-content:center;padding:.55rem;border-radius:10px;gap:0;}
|
||||
.v5-side.collapsed .v5-si span:not(.ic){width:0;overflow:hidden;opacity:0;transition:width .25s,opacity .2s;position:absolute;}
|
||||
.v5-side.collapsed .v5-si{position:relative;}
|
||||
/* Base transition for sidebar collapse/expand */
|
||||
.v5-side{transition:width .5s cubic-bezier(.4,0,.2,1),padding .5s cubic-bezier(.4,0,.2,1),
|
||||
transform .35s cubic-bezier(.16,1,.3,1),opacity .25s,
|
||||
border-color .3s,box-shadow .3s;}
|
||||
|
||||
/* Content area stretches smoothly with sidebar */
|
||||
.v5-body .v5-content{transition:all .5s cubic-bezier(.4,0,.2,1);}
|
||||
|
||||
.v5-side.collapsed{width:56px;padding:.85rem .4rem;overflow:visible;z-index:30;
|
||||
border-right-color:var(--v5-primary);box-shadow:var(--v5-glow-sm);}
|
||||
.v5-side.collapsed .v5-side-toggle{box-shadow:var(--v5-glow-sm);border-color:var(--v5-primary);color:var(--v5-primary);}
|
||||
|
||||
/* Collapsed menu items — center icon */
|
||||
.v5-side.collapsed .v5-si{justify-content:center;padding:.55rem;border-radius:10px;gap:0;position:relative;
|
||||
animation:v5-iconPop .35s cubic-bezier(.16,1,.3,1) both;}
|
||||
.v5-side.collapsed .v5-si:nth-child(1){animation-delay:.08s;}
|
||||
.v5-side.collapsed .v5-si:nth-child(2){animation-delay:.12s;}
|
||||
.v5-side.collapsed .v5-si:nth-child(3){animation-delay:.16s;}
|
||||
.v5-side.collapsed .v5-si:nth-child(4){animation-delay:.2s;}
|
||||
.v5-side.collapsed .v5-si:nth-child(5){animation-delay:.24s;}
|
||||
.v5-side.collapsed .v5-si:nth-child(6){animation-delay:.28s;}
|
||||
.v5-side.collapsed .v5-si:nth-child(7){animation-delay:.32s;}
|
||||
.v5-side.collapsed .v5-si:nth-child(8){animation-delay:.36s;}
|
||||
@keyframes v5-iconPop{from{opacity:0;transform:scale(.5)}to{opacity:1;transform:scale(1)}}
|
||||
|
||||
/* Hide text when collapsed */
|
||||
.v5-side.collapsed .v5-si span:not(.ic){width:0;overflow:hidden;opacity:0;
|
||||
transition:width .3s cubic-bezier(.4,0,.2,1),opacity .2s;position:absolute;}
|
||||
|
||||
/* Tooltip on hover */
|
||||
.v5-side.collapsed .v5-si:hover::after{content:attr(title);position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%);
|
||||
background:var(--v5-glass-strong);backdrop-filter:blur(12px);border:1px solid var(--v5-glass-border);
|
||||
background:var(--v5-surface-solid);backdrop-filter:blur(12px);border:1px solid var(--v5-glass-border);
|
||||
padding:.3rem .6rem;border-radius:8px;font-size:.68rem;font-weight:500;color:var(--v5-text);
|
||||
white-space:nowrap;z-index:100;box-shadow:0 4px 15px rgba(0,0,0,.1);pointer-events:none;}
|
||||
.v5-side.collapsed .v5-si .ic{opacity:.7;margin:0;transition:opacity .2s;}
|
||||
white-space:nowrap;z-index:100;box-shadow:0 4px 15px rgba(0,0,0,.1);pointer-events:none;
|
||||
animation:v5-tipIn .2s cubic-bezier(.16,1,.3,1) both;}
|
||||
@keyframes v5-tipIn{from{opacity:0;transform:translateX(-4px) translateY(-50%)}to{opacity:1;transform:translateX(0) translateY(-50%)}}
|
||||
|
||||
.v5-side.collapsed .v5-si .ic{opacity:.7;margin:0;transition:opacity .25s,transform .25s;}
|
||||
.v5-side.collapsed .v5-si.on .ic{opacity:1;}
|
||||
.v5-side.collapsed .v5-si:hover .ic{transform:scale(1.15);}
|
||||
.v5-side.collapsed .v5-si:hover{transform:none;}
|
||||
.v5-side.collapsed .v5-side-sec{height:0;overflow:hidden;padding:0;margin:0;opacity:0;transition:all .25s;}
|
||||
.v5-side:not(.collapsed) .v5-si span:not(.ic){opacity:1;transition:opacity .3s .15s;}
|
||||
.v5-side:not(.collapsed) .v5-side-sec{opacity:1;transition:opacity .3s .1s,height .3s,padding .3s;}
|
||||
|
||||
/* Section headers — collapse with animation */
|
||||
.v5-side.collapsed .v5-side-sec{height:0;overflow:hidden;padding:0;margin:0;opacity:0;
|
||||
transition:height .3s cubic-bezier(.4,0,.2,1),padding .3s,opacity .15s;}
|
||||
|
||||
/* Expand: text slides back in with stagger */
|
||||
.v5-side:not(.collapsed) .v5-si span:not(.ic){opacity:1;
|
||||
transition:opacity .35s .2s cubic-bezier(.16,1,.3,1),width .35s .15s;}
|
||||
.v5-side:not(.collapsed) .v5-si{animation:v5-menuSlideIn .4s cubic-bezier(.16,1,.3,1) both;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(1){animation-delay:.05s;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(2){animation-delay:.08s;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(3){animation-delay:.11s;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(4){animation-delay:.14s;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(5){animation-delay:.17s;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(6){animation-delay:.2s;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(7){animation-delay:.23s;}
|
||||
.v5-side:not(.collapsed) .v5-si:nth-child(8){animation-delay:.26s;}
|
||||
@keyframes v5-menuSlideIn{from{opacity:0;transform:translateX(-12px)}to{opacity:1;transform:none}}
|
||||
.v5-side:not(.collapsed) .v5-side-sec{opacity:1;
|
||||
transition:opacity .35s .1s,height .35s .05s,padding .35s .05s;}
|
||||
|
||||
/* Category groups */
|
||||
.v5-side-group{display:contents;}
|
||||
@@ -326,48 +373,78 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo
|
||||
.v5-side-cat{height:0;overflow:hidden;opacity:0;padding:0;margin:0;pointer-events:none;
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
border-radius:10px;cursor:pointer;position:relative;color:var(--v5-text-muted);
|
||||
transition:height .25s,opacity .2s,padding .25s,margin .25s;}
|
||||
transition:height .3s,opacity .2s,padding .3s,margin .3s;}
|
||||
.v5-side.collapsed .v5-side-cat{height:auto;overflow:visible;opacity:1;pointer-events:auto;
|
||||
padding:.55rem;margin-top:.4rem;transition:height .3s .05s,opacity .3s .1s,padding .3s .05s,margin .3s .05s;}
|
||||
padding:.55rem;margin-top:.4rem;
|
||||
animation:v5-catIn .4s cubic-bezier(.16,1,.3,1) both;}
|
||||
.v5-side.collapsed .v5-side-cat:first-child{margin-top:0;}
|
||||
.v5-side.collapsed .v5-side-cat:hover{background:var(--v5-surface-hover);color:var(--v5-primary);}
|
||||
.v5-side.collapsed .v5-side-cat:hover{background:var(--v5-surface-hover);color:var(--v5-primary);transform:scale(1.05);}
|
||||
.v5-side.collapsed .v5-side-cat.open{background:linear-gradient(135deg,rgba(108,92,231,.1),rgba(108,92,231,.04));color:var(--v5-primary);}
|
||||
.dark .v5-side.collapsed .v5-side-cat.open{background:linear-gradient(135deg,rgba(162,155,254,.1),rgba(162,155,254,.04));}
|
||||
.v5-side-cat .cat-label{font-size:.48rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;
|
||||
margin-top:.15rem;text-align:center;line-height:1;}
|
||||
.v5-side.collapsed .v5-side-cat{animation:v5-catIn .3s cubic-bezier(.16,1,.3,1) both;}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(1) .v5-side-cat{animation-delay:.05s;}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(2) .v5-side-cat{animation-delay:.1s;}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(3) .v5-side-cat{animation-delay:.15s;}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(4) .v5-side-cat{animation-delay:.2s;}
|
||||
@keyframes v5-catIn{from{opacity:0;transform:scale(.7)}to{opacity:1;transform:none}}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(1) .v5-side-cat{animation-delay:.06s;}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(2) .v5-side-cat{animation-delay:.12s;}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(3) .v5-side-cat{animation-delay:.18s;}
|
||||
.v5-side.collapsed .v5-side-group:nth-child(4) .v5-side-cat{animation-delay:.24s;}
|
||||
@keyframes v5-catIn{from{opacity:0;transform:scale(.6) translateY(8px)}to{opacity:1;transform:none}}
|
||||
|
||||
/* Hide items in collapsed (shown in flyout) */
|
||||
.v5-side.collapsed .v5-side-group .v5-si{height:0;padding:0;margin:0;overflow:hidden;opacity:0;
|
||||
transition:height .25s,padding .25s,opacity .15s,margin .25s;}
|
||||
.v5-side.collapsed .v5-side-group .v5-side-sec{height:0;padding:0;margin:0;overflow:hidden;opacity:0;}
|
||||
.v5-side:not(.collapsed) .v5-side-group .v5-si{transition:height .3s .1s,padding .3s .1s,opacity .3s .15s,margin .3s .1s;}
|
||||
transition:height .3s cubic-bezier(.4,0,.2,1),padding .3s,opacity .2s,margin .3s;}
|
||||
.v5-side.collapsed .v5-side-group .v5-side-sec{height:0;padding:0;margin:0;overflow:hidden;opacity:0;
|
||||
transition:all .3s cubic-bezier(.4,0,.2,1);}
|
||||
.v5-side:not(.collapsed) .v5-side-group .v5-si{
|
||||
transition:height .35s .12s cubic-bezier(.16,1,.3,1),padding .35s .12s,opacity .35s .15s,margin .35s .12s;}
|
||||
|
||||
/* Hide child menus when collapsed */
|
||||
.v5-side.collapsed .v5-si-child{height:0;padding:0;margin:0;overflow:hidden;opacity:0;pointer-events:none;}
|
||||
/* ===== SUBMENU EXPAND/COLLAPSE ===== */
|
||||
.v5-submenu{display:grid;grid-template-rows:0fr;overflow:hidden;padding-left:1.5rem;
|
||||
transition:grid-template-rows .35s cubic-bezier(.4,0,.2,1),opacity .25s;}
|
||||
.v5-submenu>*{overflow:hidden;}
|
||||
.v5-submenu.expanded{grid-template-rows:1fr;opacity:1;}
|
||||
.v5-submenu:not(.expanded){opacity:0;}
|
||||
|
||||
/* Sub items stagger slide in */
|
||||
.v5-sub-item{transform:translateX(-10px);opacity:0;
|
||||
transition:transform .3s cubic-bezier(.16,1,.3,1),opacity .25s,background .2s,color .2s;}
|
||||
.v5-submenu.expanded .v5-sub-item{transform:none;opacity:1;}
|
||||
|
||||
/* Sub items hover — indent effect */
|
||||
.v5-sub-item:hover{padding-left:1rem !important;}
|
||||
|
||||
/* Hide child menus when sidebar collapsed */
|
||||
.v5-side.collapsed .v5-si-child,
|
||||
.v5-side.collapsed .v5-submenu{height:0;padding:0;margin:0;overflow:hidden;opacity:0;pointer-events:none;
|
||||
grid-template-rows:0fr !important;transition:none;}
|
||||
/* Hide tooltip when flyout is open */
|
||||
.v5-si:has(> .v5-side-flyout.open):hover::after{display:none !important;}
|
||||
|
||||
/* Collapsed toggle */
|
||||
.v5-side.collapsed .v5-side-toggle span{width:0;overflow:hidden;opacity:0;transition:width .2s,opacity .15s;}
|
||||
/* Collapsed toggle — icon rotates, text fades */
|
||||
.v5-side.collapsed .v5-side-toggle span{width:0;overflow:hidden;opacity:0;
|
||||
transition:width .25s cubic-bezier(.4,0,.2,1),opacity .15s;}
|
||||
.v5-side.collapsed .v5-side-toggle{justify-content:center;padding:.55rem;}
|
||||
.v5-side.collapsed .v5-side-toggle svg{transform:rotate(180deg);}
|
||||
.v5-side:not(.collapsed) .v5-side-toggle span{opacity:1;
|
||||
transition:opacity .35s .25s cubic-bezier(.16,1,.3,1);}
|
||||
.v5-side:not(.collapsed) .v5-side-toggle span{opacity:1;transition:opacity .3s .2s;}
|
||||
|
||||
/* Flyout panel */
|
||||
.v5-side-flyout{position:absolute;left:calc(100% + 8px);top:0;width:170px;
|
||||
background:var(--v5-glass-strong);backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4);
|
||||
.v5-side-flyout{position:absolute;left:calc(100% + 8px);top:0;width:180px;
|
||||
background:var(--v5-surface-solid);backdrop-filter:blur(24px) saturate(1.5);-webkit-backdrop-filter:blur(24px) saturate(1.5);
|
||||
border:1px solid var(--v5-glass-border);border-radius:14px;padding:.4rem;
|
||||
box-shadow:0 12px 40px rgba(0,0,0,0.15),var(--v5-glow-sm);
|
||||
opacity:0;transform:translateX(-8px) scale(.96);pointer-events:none;
|
||||
transition:all .25s cubic-bezier(.16,1,.3,1);z-index:100;}
|
||||
.dark .v5-side-flyout{box-shadow:0 12px 40px rgba(0,0,0,0.5),var(--v5-glow-md);}
|
||||
box-shadow:0 8px 32px rgba(0,0,0,0.12),var(--v5-glow-sm);
|
||||
opacity:0;transform:translateX(-12px) scale(.92);pointer-events:none;
|
||||
transition:opacity .2s cubic-bezier(.16,1,.3,1),transform .3s cubic-bezier(.16,1,.3,1);z-index:100;}
|
||||
.dark .v5-side-flyout{box-shadow:0 8px 32px rgba(0,0,0,0.5),var(--v5-glow-md);}
|
||||
.v5-side-flyout.open{opacity:1;transform:none;pointer-events:auto;}
|
||||
.v5-side-flyout .fly-item{animation:v5-flyItemIn .25s cubic-bezier(.16,1,.3,1) both;}
|
||||
.v5-side-flyout .fly-item:nth-child(2){animation-delay:.03s;}
|
||||
.v5-side-flyout .fly-item:nth-child(3){animation-delay:.06s;}
|
||||
.v5-side-flyout .fly-item:nth-child(4){animation-delay:.09s;}
|
||||
.v5-side-flyout .fly-item:nth-child(5){animation-delay:.12s;}
|
||||
.v5-side-flyout .fly-item:nth-child(6){animation-delay:.15s;}
|
||||
.v5-side-flyout .fly-item:nth-child(7){animation-delay:.18s;}
|
||||
@keyframes v5-flyItemIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
|
||||
.v5-side-flyout .fly-title{font-size:.58rem;font-weight:700;color:var(--v5-text-muted);
|
||||
text-transform:uppercase;letter-spacing:.08em;padding:.3rem .6rem .45rem;}
|
||||
.v5-side-flyout .fly-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem;
|
||||
|
||||
@@ -18,8 +18,8 @@ echo "============================================"
|
||||
echo "0. 기존 컨테이너 정리 중..."
|
||||
echo "============================================"
|
||||
docker rm -f pms-backend-mac-v2 pms-frontend-mac-v2 2>/dev/null || echo "기존 컨테이너가 없습니다."
|
||||
docker network rm test-vex-network 2>/dev/null || echo "기존 네트워크가 없습니다."
|
||||
docker network create test-vex-network 2>/dev/null || echo "네트워크를 생성했습니다."
|
||||
docker network rm invion-network 2>/dev/null || echo "기존 네트워크가 없습니다."
|
||||
docker network create invion-network 2>/dev/null || echo "네트워크를 생성했습니다."
|
||||
echo ""
|
||||
|
||||
# 병렬 빌드 시작
|
||||
|
||||
@@ -51,8 +51,8 @@ echo "======================================"
|
||||
echo "배포 완료!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "Frontend: https://v1.vexplor.com"
|
||||
echo "Backend: https://api.vexplor.com"
|
||||
echo "Frontend: https://v1.invion.com"
|
||||
echo "Backend: https://api.invion.com"
|
||||
echo ""
|
||||
docker-compose -f "$COMPOSE_FILE" ps
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user