From e3fe7b640fb75676ba6e3b8bea9e63f485366f9c Mon Sep 17 00:00:00 2001 From: gbpark Date: Tue, 7 Apr 2026 13:39:14 +0900 Subject: [PATCH] 22 --- .cursor/rules/api-client-usage.mdc | 10 +- .cursor/rules/project-conventions.mdc | 2 +- README.md | 145 +++---- ai-assistant/.env.example | 2 +- ai-assistant/README.md | 6 +- ai-assistant/package.json | 2 +- ai-assistant/src/app.js | 2 +- backend-node/src/config/environment.ts | 4 +- backend-node/src/routes/aiAssistantProxy.ts | 2 +- docker/deploy/docker-compose.yml | 8 +- docker/deploy/frontend.Dockerfile | 2 +- docker/dev/docker-compose.backend.mac.yml | 4 +- docker/dev/docker-compose.frontend.mac.yml | 4 +- docs/DDD1542/DB_STRUCTURE_DIAGRAM.md | 2 +- docs/INVYONE_CONCEPT.md | 363 ++++++++++++++++++ docs/WACE_SYSTEM_WORKFLOW.md | 2 +- .../backend-architecture-detailed-analysis.md | 4 +- docs/dohyeons/DEPLOYMENT_GUIDE_KPSLP.md | 58 +-- docs/frontend-architecture-analysis.md | 6 +- docs/kjs/메뉴_회사별_필터링_개선_완료.md | 22 +- docs/kjs/메뉴_회사별_필터링_구현_완료.md | 14 +- frontend/components/auth/LoginHeader.tsx | 2 +- frontend/components/layout/AppLayout.tsx | 11 +- frontend/components/layout/Logo.tsx | 2 +- frontend/constants/auth.ts | 6 +- .../docs/AI_어시스턴트_메뉴_등록_가이드.md | 8 +- frontend/lib/api/aiAssistant/client.ts | 4 +- frontend/lib/api/client.ts | 8 +- frontend/lib/api/dashboard.ts | 6 +- frontend/lib/api/file.ts | 8 +- frontend/lib/api/flow.ts | 6 +- .../file-upload/FileViewerModal.tsx | 2 +- .../v2-file-upload/FileViewerModal.tsx | 2 +- frontend/lib/theme.ts | 2 +- frontend/lib/utils/apiUrl.ts | 6 +- frontend/package.json | 2 +- frontend/public/images/vexplor.png | Bin 6866 -> 0 bytes frontend/scripts/screen94-124-verification.ts | 2 +- frontend/styles/v5-layout.css | 149 +++++-- scripts/dev/start-all-parallel.sh | 4 +- scripts/prod/deploy.sh | 4 +- 41 files changed, 673 insertions(+), 225 deletions(-) create mode 100644 docs/INVYONE_CONCEPT.md delete mode 100644 frontend/public/images/vexplor.png diff --git a/.cursor/rules/api-client-usage.mdc b/.cursor/rules/api-client-usage.mdc index ff7e2e69..e7fc2810 100644 --- a/.cursor/rules/api-client-usage.mdc +++ b/.cursor/rules/api-client-usage.mdc @@ -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` | diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc index 2f34326f..67461cff 100644 --- a/.cursor/rules/project-conventions.mdc +++ b/.cursor/rules/project-conventions.mdc @@ -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` 기반으로 자동 판별 diff --git a/README.md b/README.md index d738bb67..2abd0f24 100644 --- a/README.md +++ b/README.md @@ -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` 기반 — 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` 사용 - **snake→camel 변환 금지**: `toCamelCaseKeys()` 등 변환 함수 사용 불가 +- **MyBatis 설정**: `map-underscore-to-camel-case: false` 유지 diff --git a/ai-assistant/.env.example b/ai-assistant/.env.example index 3ecc1ba0..18bae847 100644 --- a/ai-assistant/.env.example +++ b/ai-assistant/.env.example @@ -1,4 +1,4 @@ -# AI Assistant API (VEXPLOR 내장) - 환경 변수 +# AI Assistant API (INVION 내장) - 환경 변수 # 이 파일을 .env 로 복사한 뒤 값 설정 NODE_ENV=development diff --git a/ai-assistant/README.md b/ai-assistant/README.md index 8eb5c2ec..b33996e0 100644 --- a/ai-assistant/README.md +++ b/ai-assistant/README.md @@ -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 기능을 사용할 수 있습니다. ## 서비스 올리는 순서 (한 번에 동작하게) diff --git a/ai-assistant/package.json b/ai-assistant/package.json index 58fcec47..b57aec23 100644 --- a/ai-assistant/package.json +++ b/ai-assistant/package.json @@ -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": { diff --git a/ai-assistant/src/app.js b/ai-assistant/src/app.js index 845217c6..70b40964 100644 --- a/ai-assistant/src/app.js +++ b/ai-assistant/src/app.js @@ -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; // =========================================== diff --git a/backend-node/src/config/environment.ts b/backend-node/src/config/environment.ts index e350642d..c4f8b26f 100644 --- a/backend-node/src/config/environment.ts +++ b/backend-node/src/config/environment.ts @@ -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", // 운영 백엔드 ]; }; diff --git a/backend-node/src/routes/aiAssistantProxy.ts b/backend-node/src/routes/aiAssistantProxy.ts index 64012976..d667fa19 100644 --- a/backend-node/src/routes/aiAssistantProxy.ts +++ b/backend-node/src/routes/aiAssistantProxy.ts @@ -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"; diff --git a/docker/deploy/docker-compose.yml b/docker/deploy/docker-compose.yml index 8123cf53..abdc4122 100644 --- a/docker/deploy/docker-compose.yml +++ b/docker/deploy/docker-compose.yml @@ -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 diff --git a/docker/deploy/frontend.Dockerfile b/docker/deploy/frontend.Dockerfile index 5accb6c4..8772a830 100644 --- a/docker/deploy/frontend.Dockerfile +++ b/docker/deploy/frontend.Dockerfile @@ -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 diff --git a/docker/dev/docker-compose.backend.mac.yml b/docker/dev/docker-compose.backend.mac.yml index 5e22f741..e4bf4f1f 100644 --- a/docker/dev/docker-compose.backend.mac.yml +++ b/docker/dev/docker-compose.backend.mac.yml @@ -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 diff --git a/docker/dev/docker-compose.frontend.mac.yml b/docker/dev/docker-compose.frontend.mac.yml index ecbcc240..008304fb 100644 --- a/docker/dev/docker-compose.frontend.mac.yml +++ b/docker/dev/docker-compose.frontend.mac.yml @@ -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 diff --git a/docs/DDD1542/DB_STRUCTURE_DIAGRAM.md b/docs/DDD1542/DB_STRUCTURE_DIAGRAM.md index 541e2983..fc7f0bfb 100644 --- a/docs/DDD1542/DB_STRUCTURE_DIAGRAM.md +++ b/docs/DDD1542/DB_STRUCTURE_DIAGRAM.md @@ -1,4 +1,4 @@ -# Vexplor 구조 다이어그램 +# Invion 구조 다이어그램 > 생성일: 2026-01-22 | 총 테이블: 164개 | 코드 기반 관계 분석 완료 diff --git a/docs/INVYONE_CONCEPT.md b/docs/INVYONE_CONCEPT.md new file mode 100644 index 00000000..33abf9a0 --- /dev/null +++ b/docs/INVYONE_CONCEPT.md @@ -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 위에 진행 diff --git a/docs/WACE_SYSTEM_WORKFLOW.md b/docs/WACE_SYSTEM_WORKFLOW.md index b9cd9f23..6957a770 100644 --- a/docs/WACE_SYSTEM_WORKFLOW.md +++ b/docs/WACE_SYSTEM_WORKFLOW.md @@ -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 상태 관리 체계 diff --git a/docs/backend-architecture-detailed-analysis.md b/docs/backend-architecture-detailed-analysis.md index 534cf7a3..e2d758f2 100644 --- a/docs/backend-architecture-detailed-analysis.md +++ b/docs/backend-architecture-detailed-analysis.md @@ -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' ]; }; diff --git a/docs/dohyeons/DEPLOYMENT_GUIDE_KPSLP.md b/docs/dohyeons/DEPLOYMENT_GUIDE_KPSLP.md index 5d6480bb..7670afae 100644 --- a/docs/dohyeons/DEPLOYMENT_GUIDE_KPSLP.md +++ b/docs/dohyeons/DEPLOYMENT_GUIDE_KPSLP.md @@ -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 diff --git a/docs/frontend-architecture-analysis.md b/docs/frontend-architecture-analysis.md index fb367585..cf3a6eb0 100644 --- a/docs/frontend-architecture-analysis.md +++ b/docs/frontend-architecture-analysis.md @@ -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 diff --git a/docs/kjs/메뉴_회사별_필터링_개선_완료.md b/docs/kjs/메뉴_회사별_필터링_개선_완료.md index d20d9a62..d788e6d2 100644 --- a/docs/kjs/메뉴_회사별_필터링_개선_완료.md +++ b/docs/kjs/메뉴_회사별_필터링_개선_완료.md @@ -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이 특정 회사 컨텍스트로 전환하여 해당 회사 메뉴 ``` diff --git a/docs/kjs/메뉴_회사별_필터링_구현_완료.md b/docs/kjs/메뉴_회사별_필터링_구현_완료.md index 32427f73..576b336a 100644 --- a/docs/kjs/메뉴_회사별_필터링_구현_완료.md +++ b/docs/kjs/메뉴_회사별_필터링_구현_완료.md @@ -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 메뉴는 없음 ] } ``` diff --git a/frontend/components/auth/LoginHeader.tsx b/frontend/components/auth/LoginHeader.tsx index ea02f782..da167e3c 100644 --- a/frontend/components/auth/LoginHeader.tsx +++ b/frontend/components/auth/LoginHeader.tsx @@ -9,7 +9,7 @@ export function LoginHeader() {
{UI_CONFIG.COMPANY_NAME} )} - {/* 펼친 상태의 하위 메뉴 */} - {!sidebarCollapsed && menu.hasChildren && isExpanded && ( -
- {menu.children?.map((child: any) => ( + {/* 하위 메뉴 — 항상 렌더링, CSS로 높이 제어 */} + {!sidebarCollapsed && menu.hasChildren && ( +
+ {menu.children?.map((child: any, idx: number) => (
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)} > {child.icon} diff --git a/frontend/components/layout/Logo.tsx b/frontend/components/layout/Logo.tsx index 0cdb687c..284bddd4 100644 --- a/frontend/components/layout/Logo.tsx +++ b/frontend/components/layout/Logo.tsx @@ -9,7 +9,7 @@ export function Logo() {
WACE 솔루션 로고 { 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") { diff --git a/frontend/lib/api/dashboard.ts b/frontend/lib/api/dashboard.ts index 536fa6a8..c3e3112c 100644 --- a/frontend/lib/api/dashboard.ts +++ b/frontend/lib/api/dashboard.ts @@ -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 diff --git a/frontend/lib/api/file.ts b/frontend/lib/api/file.ts index 91d28d87..ef3dadef 100644 --- a/frontend/lib/api/file.ts +++ b/frontend/lib/api/file.ts @@ -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://")) { diff --git a/frontend/lib/api/flow.ts b/frontend/lib/api/flow.ts index d0627a4d..a49f02b2 100644 --- a/frontend/lib/api/flow.ts +++ b/frontend/lib/api/flow.ts @@ -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"; } // 로컬 개발환경 diff --git a/frontend/lib/registry/components/file-upload/FileViewerModal.tsx b/frontend/lib/registry/components/file-upload/FileViewerModal.tsx index 0695b730..57b7e5e1 100644 --- a/frontend/lib/registry/components/file-upload/FileViewerModal.tsx +++ b/frontend/lib/registry/components/file-upload/FileViewerModal.tsx @@ -284,7 +284,7 @@ export const FileViewerModal: React.FC = ({ 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); diff --git a/frontend/lib/registry/components/v2-file-upload/FileViewerModal.tsx b/frontend/lib/registry/components/v2-file-upload/FileViewerModal.tsx index 0695b730..57b7e5e1 100644 --- a/frontend/lib/registry/components/v2-file-upload/FileViewerModal.tsx +++ b/frontend/lib/registry/components/v2-file-upload/FileViewerModal.tsx @@ -284,7 +284,7 @@ export const FileViewerModal: React.FC = ({ 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); diff --git a/frontend/lib/theme.ts b/frontend/lib/theme.ts index 83194dd7..bd5b7136 100644 --- a/frontend/lib/theme.ts +++ b/frontend/lib/theme.ts @@ -1,4 +1,4 @@ -// VEXPLOR Cosmic Design System — 우주 테마 정의 +// INVION Cosmic Design System — 우주 테마 정의 export const theme = { // 색상 팔레트 (Stellar White — 라이트 모드) colors: { diff --git a/frontend/lib/utils/apiUrl.ts b/frontend/lib/utils/apiUrl.ts index e8704ff8..f11dd2dc 100644 --- a/frontend/lib/utils/apiUrl.ts +++ b/frontend/lib/utils/apiUrl.ts @@ -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 diff --git a/frontend/package.json b/frontend/package.json index 76773512..b4041213 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/images/vexplor.png b/frontend/public/images/vexplor.png deleted file mode 100644 index 25c45a925fc7d3a10c2a11f9dec5dc47fc60cbc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6866 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 diff --git a/frontend/scripts/screen94-124-verification.ts b/frontend/scripts/screen94-124-verification.ts index f422a607..ba363cb0 100644 --- a/frontend/scripts/screen94-124-verification.ts +++ b/frontend/scripts/screen94-124-verification.ts @@ -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) diff --git a/frontend/styles/v5-layout.css b/frontend/styles/v5-layout.css index 60225675..760ea120 100644 --- a/frontend/styles/v5-layout.css +++ b/frontend/styles/v5-layout.css @@ -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; diff --git a/scripts/dev/start-all-parallel.sh b/scripts/dev/start-all-parallel.sh index 4e618ee4..f9f2ebc8 100755 --- a/scripts/dev/start-all-parallel.sh +++ b/scripts/dev/start-all-parallel.sh @@ -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 "" # 병렬 빌드 시작 diff --git a/scripts/prod/deploy.sh b/scripts/prod/deploy.sh index d8430e3e..04c7be9f 100755 --- a/scripts/prod/deploy.sh +++ b/scripts/prod/deploy.sh @@ -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 ""