diff --git a/.dockerignore b/.dockerignore index 91599c40..f017a928 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,10 @@ apps/server/.gitignore apps/client/Dockerfile apps/client/node_modules apps/client/test -apps/server/mocks +apps/client/mocks + +apps/hub/Dockerfile +apps/hub/node_modules +apps/hub/.env + +node_modules \ No newline at end of file diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index b2259ca7..0498e9d3 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -32,6 +32,16 @@ jobs: cloud-canvas.kr.ncr.ntruss.com/front:dev cloud-canvas.kr.ncr.ntruss.com/front:${{ github.sha }} + - name: Docker front-hub image build and push + uses: docker/build-push-action@v3 + with: + context: . + file: ./apps/hub/Dockerfile + push: true + tags: | + cloud-canvas.kr.ncr.ntruss.com/front-hub:dev + cloud-canvas.kr.ncr.ntruss.com/front-hub:${{ github.sha }} + - name: Docker back image build and push uses: docker/build-push-action@v3 with: diff --git a/.github/workflows/pr-build-test.yml b/.github/workflows/pr-build-test.yml index 27c5763d..afe7973c 100644 --- a/.github/workflows/pr-build-test.yml +++ b/.github/workflows/pr-build-test.yml @@ -25,7 +25,7 @@ jobs: cache: true - name: Install dependencies with pnpm - run: pnpm install + run: pnpm install --no-frozen-lockfile - name: Build the project run: pnpm build diff --git a/README.md b/README.md index bfc1b661..19045a13 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,176 @@ -# Cloud Canvas - -

- Cloud Canvas -

+

+cicd +

🎨 Cloud Canvas 🎨

+

쉽고 λΉ λ₯΄κ²Œ, λˆ„κ΅¬λ‚˜ ν΄λΌμš°λ“œλ₯Ό μ„€κ³„ν•˜λŠ” 즐거운 κ²½ν—˜μ„!(λ°°λ„ˆλ‘œ λŒ€μ²΄ μ˜ˆμ •)

+ +
+ + + +
+ +## Cloud Canvas ✨ + +Cloud CanvasλŠ” ν΄λΌμš°λ“œ 인프라λ₯Ό **κ·Έλž˜ν”½ μΈν„°νŽ˜μ΄μŠ€**둜 μ†μ‰½κ²Œ μ„€κ³„ν•˜κ³ , 이λ₯Ό **Terraform μ½”λ“œ**둜 μžλ™ λ³€ν™˜ν•  수 μžˆλŠ” ν˜μ‹ μ μΈ λ„κ΅¬μž…λ‹ˆλ‹€. κ΅­λ‚΄ ν΄λΌμš°λ“œ ν”Œλž«νΌμ„ μ§€μ›ν•˜λ©°, μ‚¬μš©μžκ°€ **μ§κ΄€μ μœΌλ‘œ** 인프라λ₯Ό μ„€κ³„ν•˜κ³  **λΉ λ₯΄κ²Œ 배포**ν•  수 μžˆλ„λ‘ λ•μŠ΅λ‹ˆλ‹€. + +## 🌟 **μ£Όμš” κΈ°λŠ₯** + +- **🎨 직관적인 UI/UX** + 클릭 λͺ‡ 번으둜 λˆ„κ΅¬λ‚˜ μ‰½κ²Œ ν΄λΌμš°λ“œ 인프라 섀계! + +- **πŸ’» Terraform μ½”λ“œ λ³€ν™˜** + μ„€κ³„ν•œ 인프라λ₯Ό μžλ™μœΌλ‘œ Terraform μ½”λ“œλ‘œ λ³€ν™˜ν•˜μ—¬ λ‹€μš΄λ‘œλ“œ κ°€λŠ₯! + +- **🀝 ν˜‘μ—… 및 μž¬ν™œμš©** + **인프라 ν—ˆλΈŒ**λ₯Ό 톡해 λ‹€λ₯Έ μ‚¬μš©μžλ“€κ³Ό 섀계도λ₯Ό κ³΅μœ ν•˜κ³  μˆ˜μ •ν•˜λ©° 효율적으둜 ν˜‘μ—…! + +## κΈ°λŠ₯ μ‹œμ—° + +### GUIλ₯Ό ν†΅ν•œ 인프라 섀계 + +μ‹œλ‚˜λ¦¬μ˜€ + +1. ν—ˆλΈŒ νŽ˜μ΄μ§€μ—μ„œ 헀더에 μžˆλŠ” μƒˆ μΊ”λ²„μŠ€ λ²„νŠΌμ„ 눌러 μΊ”λ²„μŠ€ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€. +2. κ°„λ‹¨ν•œ 인프라λ₯Ό μ„€κ³„ν•œλ‹€. + +### ν…ŒλΌνΌ μ½”λ“œ λ³€ν™˜ + +μ‹œλ‚˜λ¦¬μ˜€ + +1. μΊ”λ²„μŠ€ νŽ˜μ΄μ§€μ— μ™„μ„±λœ 인프라 μ•„ν‚€ν…μ²˜κ°€ μ‘΄μž¬ν•œλ‹€. +2. μΊ”λ²„μŠ€ νŽ˜μ΄μ§€ μš°μƒλ‹¨μ— μžˆλŠ” convertor λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ ν˜„μž¬ μ„€κ³„λœ 인프라λ₯Ό λ°”νƒ•μœΌλ‘œ λ³€ν™˜λœ ν…ŒλΌνΌ μ½”λ“œκ°€ λ‚˜μ˜¨λ‹€. +3. ν…ŒλΌνΌ μ½”λ“œλ₯Ό 톡해 배포된 인프라λ₯Ό ν™•μΈν•œλ‹€. + +### 인프라 μ•„ν‚€ν…μ²˜ ν—ˆλΈŒ μ—…λ‘œλ“œ(프라이빗) + +μ‹œλ‚˜λ¦¬μ˜€ -## πŸ“Œ ν”„λ‘œμ νŠΈ λ°°κ²½ +1. 인프라 μ•„ν‚€ν…μ²˜λ₯Ό μ™„μ„±ν–ˆλ‹€κ³  κ°€μ •ν•˜κ³  μΊ”λ²„μŠ€ νŽ˜μ΄μ§€μ—μ„œ μ €μž₯ λ²„νŠΌμ„ λˆ„λ₯Έλ‹€.(/canvas) +2. μ €μž₯이 μ™„λ£Œλ˜λ©΄, μƒˆλ‘œκ³ μΉ¨ 되며 λ°œκΈ‰λ°›μ€ 프라이빗 μ•„ν‚€ν…μ²˜λ₯Ό parameter λΆ™μ—¬ private architecture μΊ”λ²„μŠ€ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.(/canvas/private-architecutes/{id}) +3. ν•΄λ‹Ή μ•„ν‚€ν…μ²˜κ°€ μΊ”λ²„μŠ€μ— λ‹€μ‹œ λΆˆλŸ¬μ™€μ§„ 것을 ν™•μΈν•˜λ©΄ ν—ˆλΈŒ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.(/둜 이동) +4. ν—ˆλΈŒ νŽ˜μ΄μ§€μ—μ„œ λ§ˆμ΄νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•˜κ³  프라이빗 μ•„ν‚€ν…μ²˜ λͺ©λ‘μ—μ„œ μƒˆλ‘œμš΄ λͺ©λ‘μ΄ μΆ”κ°€λœ 것을 ν™•μΈν•˜κ³  ν΄λ¦­ν•œλ‹€. +5. μƒˆλ‘œ μΆ”κ°€λœ 프라이빗 μ•„ν‚€ν…μ²˜ λͺ©λ‘μ„ ν΄λ¦­ν•˜λ©΄ λ‹€μ‹œ μΊ”λ²„μŠ€ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€. -ν΄λΌμš°λ“œ 인프라 ꡬ좕 κ³Όμ •μ—μ„œ κ°œλ°œμžλ“€μ€ λ‹€μŒκ³Ό 같은 어렀움을 κ²ͺκ³  μžˆμŠ΅λ‹ˆλ‹€ +### 인프라 μ•„ν‚€ν…μ²˜ ν—ˆλΈŒ μ—…λ‘œλ“œ(퍼블릭) -- **반볡적인 μˆ˜μž‘μ—…**: ν΄λΌμš°λ“œ μ½˜μ†”μ—μ„œ 각 λ¦¬μ†ŒμŠ€λ§ˆλ‹€ λ³„λ„μ˜ νŽ˜μ΄μ§€μ— μ ‘μ†ν•˜μ—¬ μƒμ„±ν•˜λŠ” 과정을 λ°˜λ³΅ν•΄μ•Ό ν•©λ‹ˆλ‹€. -- **ν”Œλž«νΌ 적응 μ‹œκ°„**: ν΄λΌμš°λ“œ μ—…μ²΄λ³„λ‘œ μƒμ΄ν•œ μΈν„°νŽ˜μ΄μŠ€λ‘œ 인해, μƒˆλ‘œμš΄ ν”Œλž«νΌ μ‚¬μš© μ‹œ 좔가적인 ν•™μŠ΅ μ‹œκ°„μ΄ ν•„μš”ν•©λ‹ˆλ‹€. +μ‹œλ‚˜λ¦¬μ˜€ -μ΄λŸ¬ν•œ 인프라 관리 μž‘μ—…λ“€λ‘œ 인해 핡심 개발 업무에 μ§‘μ€‘ν•˜κΈ° μ–΄λ €μ›Œμ§‘λ‹ˆλ‹€. +1. 인프라 μ•„ν‚€ν…μ²˜λ₯Ό μ™„μ„±ν–ˆλ‹€κ³  κ°€μ •ν•˜κ³  μΊ”λ²„μŠ€ νŽ˜μ΄μ§€μ—μ„œ μ €μž₯ λ²„νŠΌμ„ λˆ„λ₯Έλ‹€.(/canvas) +2. μ €μž₯이 μ™„λ£Œλ˜λ©΄, μƒˆλ‘œκ³ μΉ¨ 되며 λ°œκΈ‰λ°›μ€ 프라이빗 μ•„ν‚€ν…μ²˜λ₯Ό parameter λΆ™μ—¬ private architecture μΊ”λ²„μŠ€ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.(/canvas/private-architecutes/{id}) +3. ν•΄λ‹Ή μ•„ν‚€ν…μ²˜κ°€ μΊ”λ²„μŠ€μ— λ‹€μ‹œ λΆˆλŸ¬μ™€μ§„ 것을 ν™•μΈν•˜λ©΄ ν—ˆλΈŒ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.(/둜 이동) +4. ν—ˆλΈŒ νŽ˜μ΄μ§€μ—μ„œ λ§ˆμ΄νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•˜κ³  프라이빗 μ•„ν‚€ν…μ²˜ λͺ©λ‘μ—μ„œ μƒˆλ‘œμš΄ λͺ©λ‘μ΄ μΆ”κ°€λœ 것을 ν™•μΈν•˜κ³  ν΄λ¦­ν•œλ‹€. +5. μƒˆλ‘œ μΆ”κ°€λœ 프라이빗 μ•„ν‚€ν…μ²˜ λͺ©λ‘μ„ ν΄λ¦­ν•˜λ©΄ λ‹€μ‹œ μΊ”λ²„μŠ€ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€. -μ΅œκ·Όμ—λŠ” μœ„μ˜ λ¬Έμ œλ“€μ„ ν•΄κ²°ν•˜κΈ° μœ„ν•œ 도ꡬ듀이 제곡되고 μžˆμ§€λ§Œ, λ‹€μŒκ³Ό 같은 ν•œκ³„μ μ„ 가지고 μžˆμŠ΅λ‹ˆλ‹€. +### 인프라 μ•„ν‚€ν…μ²˜ ν—ˆλΈŒ μž„ν¬νŠΈ -- **Terraform λ“± IaC 도ꡬ**: 높은 λŸ¬λ‹ μ»€λΈŒμ™€ Terraform을 ν™œμš©ν•˜μ—¬ ν”„λ‘œμ νŠΈλ₯Ό κ΄€λ¦¬ν•˜λŠ” 방법을 ν•™μŠ΅ν•˜λŠ” 데 κ°œλ°œμžλ“€μ΄ μ‹œκ°„μ„ νˆ¬μžν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€. -- **AWS CloudFormation Design, CloudCraft λ“± GUI 인프라 섀계 도ꡬ**: AWS CloudFormation Design은 AWS μ „μš©μœΌλ‘œ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯이며, CloudCraftλŠ” μ™Έκ΅­μ˜ ν΄λΌμš°λ“œ μ—…μ²΄λ§Œμ„ λŒ€μƒμœΌλ‘œ ν•˜κΈ°μ— κ΅­λ‚΄ ν΄λΌμš°λ“œ 업체λ₯Ό μ‚¬μš©ν•˜λŠ” κ°œλ°œμžλ“€μ΄ ν•΄λ‹Ή 도ꡬ듀을 μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. +μ‹œλ‚˜λ¦¬μ˜€ -λ”°λΌμ„œ 저희 Cloud-Canvas νŒ€μ€ κ΅­λ‚΄ ν΄λΌμš°λ“œ 업체도 μ§€μ›ν•˜λŠ” GUI 기반의 인프라 톡합 관리 μ‹œμŠ€ν…œμ— ν•„μš”μ„±μ„ 느끼게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€. +1. ν—ˆλΈŒ νŽ˜μ΄μ§€μ—μ„œ 아무 인프라 μ•„ν‚€ν…μ²˜ λͺ©λ‘μ„ ν΄λ¦­ν•œλ‹€. +2. 퍼블릭 인프라 μ•„ν‚€ν…μ²˜ 상세 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•˜κ³  μž„ν¬νŠΈ λ²„νŠΌμ„ ν΄λ¦­ν•œλ‹€. +3. μž„ν¬νŠΈκ°€ μ™„λ£Œλ˜λ©΄ μΊ”λ²„μŠ€ νŽ˜μ΄μ§€λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ ν•˜κ³  μž„ν¬νŠΈ λ˜μ–΄ ν•΄λ‹Ή μ•„ν‚€ν…μ²˜κ°€ μΊ”λ²„μŠ€λ‘œ 그렀진 것을 ν™•μΈν•œλ‹€. +4. λ§ˆμ΄νŽ˜μ΄μ§€λ‘œ λ“€μ–΄κ°€μ„œ, μž„ν¬νŠΈν•œ 퍼블릭 인프라 μ•„ν‚€ν…μ²˜κ°€ μž„ν¬νŠΈ λͺ©λ‘μ— μΆ”κ°€λœ 것을 ν™•μΈν•œλ‹€. -## πŸ“Œ ν”„λ‘œμ νŠΈ μ†Œκ°œ +
-Cloud-CanvasλŠ” μ΄λŸ¬ν•œ λ¬Έμ œμ μ„ ν•΄κ²°ν•˜κΈ° μœ„ν•œ GUI 기반 인프라 관리 λ„κ΅¬μž…λ‹ˆλ‹€. +## πŸš€ 기술 μŠ€νƒ -## πŸ“Œ ν”„λ‘œμ νŠΈ κΈ°λŒ€ 효과 +### πŸ’» Common -- κΈ°μ‘΄ AWS μ‚¬μš©μžλ“€μ˜ κ΅­λ‚΄ ν΄λΌμš°λ“œ μƒνƒœκ³„ μœ μž… 촉진 -- κ΅­λ‚΄ ν΄λΌμš°λ“œ μ„œλΉ„μŠ€ ν™œμ„±ν™” -- ν•œκ΅­ ν΄λΌμš°λ“œ μ‚°μ—…μ˜ 경쟁λ ₯ κ°•ν™” +

+ JavaScript + TypeScript + Prettier + ESLint + PNPM + TSUP +

-## πŸ“Œ ν”„λ‘œμ νŠΈ λͺ©ν‘œ +### πŸ–₯️ Frontend -- 직관적인 UX/UIλ₯Ό ν†΅ν•œ 인프라 섀계 κΈ°λŠ₯ 제곡 -- μ‹€μ‹œκ°„ λͺ¨λ‹ˆν„°λ§μ„ ν†΅ν•œ 효율적인 인프라 관리 κΈ°λŠ₯ 제곡 -- κ΅­λ‚΄μ™Έ ν΄λΌμš°λ“œ 업체 톡합 관리 κΈ°λŠ₯ 제곡 -- NPM λͺ¨λ“ˆμ„ 톡해 μ œκ³΅ν•¨μœΌλ‘œμ¨, μžλ°”μŠ€ν¬λ¦½νŠΈ κ°œλ°œμžλ“€μ—κ²Œ μΉœν™”μ μΈ 도ꡬλ₯Ό μ œμž‘ -- Infra Hubλ₯Ό 톡해 μ‚¬μš©μžκ°€ μžμ‹ μ΄ μ„€κ³„ν•œ 인프라λ₯Ό μ „ 세계 μ‚¬λžŒλ“€κ³Ό κ³΅μœ ν•  수 μžˆλ„λ‘ 함 +

+ Next.js + React + Tailwind CSS + Vite + TanStack Query +

-## πŸ“Œ 아킀텍쳐 +### πŸ”§ Backend -### μ „λ°˜μ μΈ 인프라 +

+ NestJS + MySQL + Redis + Prisma + Vitest +

-![image](https://github.com/user-attachments/assets/5901b688-0d3d-4698-ad22-a4d4bb7aa8fd) +### 🌐 Infrastructure -### CI/CD +

+ Turborepo + Docker + Docker Compose + GitHub Actions + Naver Cloud + Nginx +

-cicd +### πŸ” DevOps & Monitoring -# νŒ€ +

+ Terraform + Terraform Cloud + Elasticsearch + FluentD + Kibana + Grafana + Prometheus +

-| κΉ€λ²”μ€€ | 고동민 | 졜재영 | μ„œκ±΄ν˜ | +### πŸ’¬ Communication Tools + +

+ Slack + Zoom + Gather Town + Figma +

+ +
+ +--- + +## **μ•„ν‚€ν…μ²˜** 🌐 + +### **인프라 섀계** + +![image](https://github.com/user-attachments/assets/e8bd555e-ae84-4989-a520-800a61b3da54) + +![image](https://github.com/user-attachments/assets/b18b1048-5fe8-43ee-a33f-8fbe2b38e873) + +### **μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 섀계** + +![image](https://github.com/user-attachments/assets/04145d8b-61b0-401a-8943-7494a0f9aed5) + +## 우리의 Next! + +## 🌈 **ν•¨κ»˜ν•˜μ„Έμš”!** + +> **Cloud Canvas둜 ν΄λΌμš°λ“œ μ„€κ³„μ˜ μƒˆλ‘œμš΄ κ°€λŠ₯성을 κ²½ν—˜ν•΄λ³΄μ„Έμš”!** +> ν”„λ‘œμ νŠΈμ˜ 진행 상황과 더 λ§Žμ€ 정보λ₯Ό μ›ν•˜μ‹ λ‹€λ©΄ [GitHub Wiki](https://github.com/boostcampwm-2024/web37-cloud-canvas/wiki)μ—μ„œ ν™•μΈν•˜μ„Έμš”. 😊 + +## **νŒ€ μ†Œκ°œ** πŸ‘©β€πŸ’» + +> λ‹€μ–‘ν•œ λ°°κ²½κ³Ό κ²½ν—˜μ„ 가진 λ„€ λͺ…μ˜ νŒ€μ›μ΄ Cloud Canvasλ₯Ό λ§Œλ“€κ³  μžˆμŠ΅λ‹ˆλ‹€. + +| **κΉ€λ²”μ€€** | **고동민** | **졜재영** | **μ„œκ±΄ν˜** | | :--------------------------------------------------------: | :-------------------------------------------------------: | :-------------------------------------------------------: | :-------------------------------------------------------: | -| FE | BE | BE | BE | +| **FE** | **BE** | **BE** | **BE** | | [p1n9](https://github.com/p1n9d3v) | [Gdm0714](https://github.com/Gdm0714) | [paulcjy](https://github.com/paulcjy) | [SeoGeonhyuk](https://github.com/SeoGeonhyuk) | | ![](https://avatars.githubusercontent.com/u/152015839?v=4) | ![](https://avatars.githubusercontent.com/u/50660440?v=4) | ![](https://avatars.githubusercontent.com/u/86853786?v=4) | ![](https://avatars.githubusercontent.com/u/60954160?v=4) | | 컀피 | λΉ΅ | κ³ κΈ° | ꡭ수 | + +--- diff --git a/apps/client/.env.development b/apps/client/.env.development new file mode 100644 index 00000000..7a4131e6 --- /dev/null +++ b/apps/client/.env.development @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:3000 +VITE_MODE=dev diff --git a/apps/client/.env.production b/apps/client/.env.production new file mode 100644 index 00000000..a02ef3a3 --- /dev/null +++ b/apps/client/.env.production @@ -0,0 +1,2 @@ +VITE_API_URL=https://api.cloudcanvas.kro.kr +VITE_MODE=prod diff --git a/apps/client/Dockerfile b/apps/client/Dockerfile index 3b645bc4..f975f72c 100644 --- a/apps/client/Dockerfile +++ b/apps/client/Dockerfile @@ -1,19 +1,35 @@ -FROM node:20 AS development -WORKDIR /development -COPY ./pnpm-lock.yaml ./apps/client/package.json . -RUN npm install -g pnpm && pnpm install +# FROM node:20 AS development +# WORKDIR /development +# COPY ./pnpm-lock.yaml ./apps/client/package.json . +# RUN npm install -g pnpm && pnpm install + +# FROM node:20 AS build +# WORKDIR /build +# RUN mkdir -p /build/apps/client +# RUN mkdir -p /build/config +# COPY --from=development /development/node_modules/ /build/apps/client/node_modules +# COPY --from=development /development/pnpm-lock.yaml /build/apps/client/ +# RUN npm install -g pnpm typescript +# COPY ./config/ /build/config/ +# COPY ./apps/client/ /build/apps/client/ +# WORKDIR /build/apps/client +# RUN npm run build && rm -rf node_modules && pnpm install --frozen-lockfile --prod + +# FROM nginx:alpine AS production +# COPY --from=build /build/apps/client/dist /usr/share/nginx/html +# COPY ./apps/nginx/nginx.conf /etc/nginx/conf.d/default.conf + +# ENV PORT=5000 +# EXPOSE 5000 +# CMD ["nginx", "-g", "daemon off;"] FROM node:20 AS build + WORKDIR /build -RUN mkdir -p /build/apps/client -RUN mkdir -p /build/config -COPY --from=development /development/node_modules/ /build/apps/client/node_modules -COPY --from=development /development/pnpm-lock.yaml /build/apps/client/ -RUN npm install -g pnpm typescript -COPY ./config/ /build/config/ -COPY ./apps/client/ /build/apps/client/ -WORKDIR /build/apps/client -RUN npm run build && rm -rf node_modules && pnpm install --frozen-lockfile --prod + +COPY . . + +RUN npm install -g pnpm && pnpm install && pnpm build FROM nginx:alpine AS production COPY --from=build /build/apps/client/dist /usr/share/nginx/html diff --git a/apps/client/mocks.ts b/apps/client/mocks.ts index a58dfd0e..e7815429 100644 --- a/apps/client/mocks.ts +++ b/apps/client/mocks.ts @@ -4,7 +4,6 @@ import { nanoid } from 'nanoid'; const CloudFunctionNode: Node = { id: `node-${nanoid()}`, type: 'cloud-function', - name: 'CloudFunction1', point: { x: 270, y: 270 }, size: { '2d': { width: 90, height: 90 }, @@ -20,7 +19,6 @@ const CloudFunctionNode: Node = { const ObjectStorageNode: Node = { id: `node-${nanoid()}`, type: 'object-storage', - name: 'ObjectStorage1', point: { x: 100, y: 0 }, size: { '2d': { width: 90, height: 90 }, @@ -36,7 +34,6 @@ const ObjectStorageNode: Node = { const MySQLDBNode: Node = { id: `node-${nanoid()}`, type: 'db-mysql', - name: 'MySQLDB1', point: { x: 0, y: 0 }, size: { '2d': { width: 90, height: 90 }, @@ -52,7 +49,6 @@ const MySQLDBNode: Node = { const ServerNode: Node = { id: `node-${nanoid()}`, type: 'server', - name: 'WebServer1', point: { x: 90, y: 90 }, size: { '2d': { width: 90, height: 90 }, @@ -69,7 +65,6 @@ const ServerNode: Node = { const ServerNode2: Node = { id: `node-${nanoid()}`, type: 'server', - name: 'WebServer2', point: { x: 90, y: 90 }, size: { '2d': { width: 90, height: 90 }, @@ -86,60 +81,61 @@ const ServerNode2: Node = { const SubnetGroup: Group = { id: 'subnet1', type: 'subnet', - name: 'Subnet-1', nodeIds: [ServerNode.id, MySQLDBNode.id, ObjectStorageNode.id], properties: { cidr: '', }, + childGroupIds: [], + parentGroupId: '', }; const VpcGroup: Group = { id: 'vpc1', type: 'vpc', - name: 'VPC-1', nodeIds: [CloudFunctionNode.id], properties: { cidr: '', }, childGroupIds: [SubnetGroup.id], + parentGroupId: '', }; const RegionGroup: Group = { id: 'seoul', type: 'region', - name: 'region', nodeIds: [], properties: { regionCode: 'KR-1', }, childGroupIds: [VpcGroup.id], + parentGroupId: '', }; -const mockNodes = [ - ServerNode, - CloudFunctionNode, - MySQLDBNode, - ObjectStorageNode, -]; +// const mockNodes = [ +// ServerNode, +// CloudFunctionNode, +// MySQLDBNode, +// ObjectStorageNode, +// ]; -const mockGroups = [RegionGroup, VpcGroup, SubnetGroup]; +// const mockGroups = [RegionGroup, VpcGroup, SubnetGroup]; -mockGroups.forEach((group) => { - // set properties for each group - group.nodeIds.forEach((nodeId: string) => { - const node = mockNodes.find((n) => n.id === nodeId); - if (node) { - node.properties[group.type] = group.id; - } - }); -}); +// mockGroups.forEach((group) => { +// // set properties for each group +// group.nodeIds.forEach((nodeId: string) => { +// const node = mockNodes.find((n) => n.id === nodeId); +// if (node) { +// node.properties[group.type] = group.id; +// } +// }); +// }); -export const mockInitialState = { - nodes: mockNodes.reduce((acc, node) => ({ ...acc, [node.id]: node }), {}), - groups: mockGroups.reduce( - (acc, group) => ({ ...acc, [group.id]: group }), - {}, - ), - edges: {}, -}; +// export const mockInitialState = { +// nodes: mockNodes.reduce((acc, node) => ({ ...acc, [node.id]: node }), {}), +// groups: mockGroups.reduce( +// (acc, group) => ({ ...acc, [group.id]: group }), +// {}, +// ), +// edges: {}, +// }; diff --git a/apps/client/package.json b/apps/client/package.json index 28f934bb..76da3029 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -6,23 +6,32 @@ "keywords": [], "type": "module", "scripts": { - "start": "serve dist", - "dev": "vite", - "build": "tsc -b && vite build", - "clean": "rm -rf dist" + "dev": "vite --mode development", + "build:prod": "tsc -b && vite build --mode production", + "build:dev": "tsc -b && vite build --mode development", + "clean": "rm -rf dist", + "preview": "vite preview" }, "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.5", "@mui/material": "^6.1.5", + "@types/validator": "^13.12.2", "nanoid": "^5.0.8", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^7.0.1", + "react-select": "^5.8.3", + "react-syntax-highlighter": "^15.6.1", + "react-type-animation": "^3.2.0", + "terraform": "file:../../packages/terraform", + "validator": "^13.12.0" }, "devDependencies": { "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.3.3", "serve": "^14.2.4", "vite": "^5.4.9", diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index d069c7ef..a95cc57b 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -1,36 +1,13 @@ import CloudGraph from '@/src/CloudGraph'; -import ErrorBoundary from '@components/ErrorBoundary'; -import Header from '@components/Layout/Header'; -import Sidebar from '@components/Layout/Sidebar'; import NetworksBar from '@components/NCloud/NetworksBar/index'; -import Box from '@mui/material/Box'; +import PropertiesBar from '@components/NCloud/PropertiesBar'; -function App() { +export const App = () => { return ( <> - - - -
- - - - + + ); -} - -export default App; +}; diff --git a/apps/client/src/CloudGraph.tsx b/apps/client/src/CloudGraph.tsx index 6dc84614..e2991ba9 100644 --- a/apps/client/src/CloudGraph.tsx +++ b/apps/client/src/CloudGraph.tsx @@ -6,13 +6,15 @@ import Graph from '@components/Graph'; import GridBackground from '@components/GridBackground'; import Group from '@components/Group'; import Node from '@components/Node'; +import NodeActions from '@components/NodeActions'; import { useEdgeContext } from '@contexts/EdgeContext'; +import { useGraphContext } from '@contexts/GraphConetxt'; import { useGroupContext } from '@contexts/GroupContext'; import { useNodeContext } from '@contexts/NodeContext'; import useConnection from '@hooks/useConnection'; import useGraph from '@hooks/useGraph'; import useSelection from '@hooks/useSelection'; -import { useEffect } from 'react'; +import { useEffect, useLayoutEffect, useRef } from 'react'; export default () => { const { @@ -24,6 +26,11 @@ export default () => { const { state: { groups }, } = useGroupContext(); + const { + state: { viewBox }, + dispatch: graphDispatch, + } = useGraphContext(); + const { selectedNodeId, selectedEdge, @@ -36,14 +43,13 @@ export default () => { const { svgRef, - prevDimension, dimension, moveNode, addEdge, splitEdge, - updatedPointForDimension, moveBendingPointer, getGroupBounds, + updateNodePointForDimension, moveGroup, removeNode, removeEdge, @@ -59,98 +65,129 @@ export default () => { updateEdgeFn: addEdge, }); + const nodesRef = useRef(nodes); + const prevDimensionRef = useRef(dimension); + useEffect(() => { const handleContextMenu = (e: MouseEvent) => e.preventDefault(); const handleMouseDown = (e: MouseEvent) => { - if (!(e.target as HTMLElement).closest('.graph-ignore-select')) + if ( + !(e.target as HTMLElement).closest('.graph-ignore-select') || + isConnecting + ) clearSelection(); }; + const handleClick = (e: MouseEvent) => { + if (isConnecting) { + closeConnection(); + clearSelection(); + document.body.style.cursor = 'default'; + } + }; document.addEventListener('contextmenu', handleContextMenu); document.addEventListener('mousedown', handleMouseDown); + document.addEventListener('click', handleClick); return () => { document.removeEventListener('contextmenu', handleContextMenu); document.removeEventListener('mousedown', handleMouseDown); + document.removeEventListener('click', handleClick); }; - }, []); + }, [isConnecting, selectedNodeId]); useEffect(() => { - if (dimension === prevDimension) return; + prevDimensionRef.current = dimension; + }, [dimension]); + []; - updatedPointForDimension(); + useEffect(() => { + nodesRef.current = nodes; + }, [nodes]); + + useLayoutEffect(() => { + if (prevDimensionRef.current === dimension) return; + updateNodePointForDimension(dimension); }, [dimension]); + return ( - - - {Object.values(groups).map((group) => ( - - ))} - {Object.values(nodes).map((node) => ( - - - + + + {Object.values(groups).map((group) => ( + - - ))} - {connection && ( - - )} - - {edges && - Object.values(edges).map((edge) => ( - - - {edge.bendingPoints.map((point, index) => ( - - moveBendingPointer(edge.id, index, newPoint) + ))} + {edges && + Object.values(edges).map((edge) => ( + + - ))} + {edge.bendingPoints.map((point, index) => ( + + moveBendingPointer( + edge.id, + index, + newPoint, + ) + } + /> + ))} + + ))} + {connection && ( + + )} + {Object.values(nodes).map((node) => ( + + + ))} - + + {selectedNodeId && ( + + )} + ); }; diff --git a/apps/client/src/Root.tsx b/apps/client/src/Root.tsx new file mode 100644 index 00000000..f7ef7377 --- /dev/null +++ b/apps/client/src/Root.tsx @@ -0,0 +1,15 @@ +import { useLoaderData } from 'react-router-dom'; +import CloudGraphProvider from '@components/CloudGraphProvider'; +import Layout from '@components/Layout'; + +function Root() { + const loader = useLoaderData(); + + return ( + + + + ); +} + +export default Root; diff --git a/apps/client/src/apis/index.ts b/apps/client/src/apis/index.ts new file mode 100644 index 00000000..89cebaac --- /dev/null +++ b/apps/client/src/apis/index.ts @@ -0,0 +1,15 @@ +const BASE_URL = import.meta.env.VITE_API_URL; +export const URLS = { + login: 'auth/login', + share: 'public-architectures', // POST + privateArchi: (id: string) => `private-architectures/${id}`, +}; + +export const urls = (path: keyof typeof URLS, slug?: any) => { + const urls = URLS[path]; + if (typeof urls === 'function') { + return `${BASE_URL}/${urls(slug)}`; + } + + return `${BASE_URL}/${urls}`; +}; diff --git a/apps/client/src/apis/loaders.ts b/apps/client/src/apis/loaders.ts new file mode 100644 index 00000000..aa3ab4b1 --- /dev/null +++ b/apps/client/src/apis/loaders.ts @@ -0,0 +1,43 @@ +import { LoaderFunctionArgs } from 'react-router-dom'; +import { urls } from '.'; +import { undefinedReviver } from '@utils'; + +export const rootLoader = async ({ params }: LoaderFunctionArgs) => { + const loginResponse = await fetch(urls('login'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + credentials: 'include', + }); + + if (!loginResponse.ok) { + throw new Response('Login failed', { status: loginResponse.status }); + } + + if (!params.id) return null; + const archiResponse = await fetch(urls('privateArchi', params.id), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + + if (!archiResponse.ok) { + throw new Response('Data fetch failed', { + status: archiResponse.status, + }); + } + + const text = await archiResponse.text(); + let data; + try { + data = JSON.parse(text, undefinedReviver); + } catch (error) { + throw new Response('Invalid JSON response', { status: 500 }); + } + + return { data }; +}; diff --git a/apps/client/src/components/CloudGraphProvider.tsx b/apps/client/src/components/CloudGraphProvider.tsx new file mode 100644 index 00000000..0d1f0c2c --- /dev/null +++ b/apps/client/src/components/CloudGraphProvider.tsx @@ -0,0 +1,58 @@ +import { DimensionProvider } from '@contexts/DimensionContext'; +import { EdgeProvider } from '@contexts/EdgeContext'; +import { GraphProvider } from '@contexts/GraphConetxt'; +import { GroupProvider } from '@contexts/GroupContext'; +import { NodeProvider } from '@contexts/NodeContext'; +import { SelectionProvider } from '@contexts/SelectionContext'; +import { SvgProvider } from '@contexts/SvgContext'; +import { calcViewBoxBounds } from '@helpers/viewBox'; +import { Edge, Group, Node } from '@types'; +import { ReactNode } from 'react'; + +type Props = { + children: ReactNode; + initialData: { + nodes: Record; + edges: Record; + groups: Record; + }; +}; +export default ({ children, initialData }: Props) => { + return ( + + + + + + + + {children} + + + + + + + + ); +}; diff --git a/apps/client/src/components/CodeDrawer.tsx b/apps/client/src/components/CodeDrawer.tsx new file mode 100644 index 00000000..974d19bc --- /dev/null +++ b/apps/client/src/components/CodeDrawer.tsx @@ -0,0 +1,91 @@ +import CloseIcon from '@mui/icons-material/Close'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { + Box, + Button, + Drawer, + IconButton, + Typography, + useTheme, +} from '@mui/material'; +import { useEffect, useRef, useState } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +type Props = { + code: string; + open: boolean; + onClose: () => void; +}; + +export default ({ code, open, onClose }: Props) => { + const theme = useTheme(); + const [copied, setCopied] = useState(false); + const containerRef = useRef(null); + const [isLoaded, setIsLoaded] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); // 2초 ν›„ 볡사 μƒνƒœ 리셋 + }; + + const scrollToBottom = () => { + if (containerRef.current && !isLoaded) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + setIsLoaded(true); + } + }; + + useEffect(() => { + if (isLoaded) setIsLoaded(false); + }, [open]); + + return ( + scrollToBottom()} + > + + + Terraform Code Viewer + + + + + + + {code} + + + + + + + ); +}; diff --git a/apps/client/src/components/Connectors/Connector.tsx b/apps/client/src/components/Connectors/Connector.tsx index 0ad2b247..7adfe19e 100644 --- a/apps/client/src/components/Connectors/Connector.tsx +++ b/apps/client/src/components/Connectors/Connector.tsx @@ -4,10 +4,9 @@ import { Point } from '@types'; type Props = { visible: boolean; point: Point; - onMouseDown: (e: React.MouseEvent) => void; }; -export default ({ point, visible, onMouseDown }: Props) => { +export default ({ point, visible }: Props) => { const theme = useTheme(); return ( @@ -19,7 +18,6 @@ export default ({ point, visible, onMouseDown }: Props) => { style={{ visibility: visible ? 'visible' : 'hidden', }} - onMouseDown={onMouseDown} /> ); }; diff --git a/apps/client/src/components/Connectors/index.tsx b/apps/client/src/components/Connectors/index.tsx index 4cb94ea4..6072ce2a 100644 --- a/apps/client/src/components/Connectors/index.tsx +++ b/apps/client/src/components/Connectors/index.tsx @@ -1,68 +1,18 @@ import Connector from '@components/Connectors/Connector'; -import { Connection, Node, Point } from '@types'; -import { useEffect } from 'react'; +import { Node } from '@types'; type Props = { node: Node; - isSelected: boolean; - isConnecting: boolean; - onOpenConnection: (from: Connection) => void; - onConnectConnection: (point: Point) => void; - onCloseConnection: () => void; }; -export default ({ - node, - isSelected, - isConnecting, - onOpenConnection, - onConnectConnection, - onCloseConnection, -}: Props) => { - const handleMouseDown = ( - e: React.MouseEvent, - connectorType: string, - point: Point, - ) => { - e.stopPropagation(); - onOpenConnection({ - id: node.id, - connectorType, - point, - }); - document.body.style.cursor = 'move'; - }; - - const handleMouseMove = (e: MouseEvent) => { - const { clientX, clientY } = e; - onConnectConnection({ x: clientX, y: clientY }); - }; - - const handleCloseConnection = () => { - onCloseConnection(); - document.body.style.cursor = 'default'; - }; - - useEffect(() => { - if (isConnecting) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleCloseConnection); - } - - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleCloseConnection); - }; - }, [isConnecting]); - +export default ({ node }: Props) => { return ( <> {Object.entries(node.connectors).map(([type, point]) => ( handleMouseDown(e, type, point)} + visible={false} /> ))} diff --git a/apps/client/src/components/Graph/index.tsx b/apps/client/src/components/Graph/index.tsx index 8518b7c7..069bf1cd 100644 --- a/apps/client/src/components/Graph/index.tsx +++ b/apps/client/src/components/Graph/index.tsx @@ -3,12 +3,12 @@ import { useSvgContext } from '@contexts/SvgContext'; import useKey from '@hooks/useKey'; import { Point } from '@types'; import { getSvgPoint } from '@utils'; -import { PropsWithChildren, useRef } from 'react'; +import { PropsWithChildren, useRef, useEffect } from 'react'; export default ({ children }: PropsWithChildren) => { const { svgRef } = useSvgContext(); const { - state: { viewBox }, + state: { viewBox, initialViewBox }, dispatch, } = useGraphContext(); @@ -16,6 +16,15 @@ export default ({ children }: PropsWithChildren) => { const startPoint = useRef({ x: 0, y: 0 }); const spaceActiveKey = useKey('space'); + const MIN_ZOOM = 0.1; + const MAX_ZOOM = 3; + + const zoomEndTimer = useRef | null>(null); + + const handleZoomEnd = () => { + document.body.style.cursor = 'default'; + }; + const zoom = (wheelY: number, point: Point) => { if (!svgRef.current) return; @@ -23,6 +32,15 @@ export default ({ children }: PropsWithChildren) => { const cursorSvgPoint = getSvgPoint(svgRef.current, point); if (!cursorSvgPoint) return; + const currentZoom = initialViewBox.width / viewBox.width; + const newZoom = currentZoom * (1 / zoomFactor); + + if (newZoom < MIN_ZOOM || newZoom > MAX_ZOOM) { + return; + } + + const newWidth = viewBox.width * zoomFactor; + const newHeight = viewBox.height * zoomFactor; dispatch({ type: 'SET_VIEWBOX', payload: { @@ -32,8 +50,8 @@ export default ({ children }: PropsWithChildren) => { y: viewBox.y + (cursorSvgPoint.y - viewBox.y) * (1 - zoomFactor), - width: viewBox.width * zoomFactor, - height: viewBox.height * zoomFactor, + width: newWidth, + height: newHeight, }, }); }; @@ -71,8 +89,20 @@ export default ({ children }: PropsWithChildren) => { const handleWheel = (e: React.WheelEvent) => { const { deltaY, clientX, clientY } = e; zoom(deltaY, { x: clientX, y: clientY }); - if (deltaY > 0) document.body.style.cursor = 'zoom-out'; - else document.body.style.cursor = 'zoom-in'; + if (deltaY > 0) { + document.body.style.cursor = 'zoom-out'; + } else { + document.body.style.cursor = 'zoom-in'; + } + + if (zoomEndTimer.current) { + clearTimeout(zoomEndTimer.current); + } + + zoomEndTimer.current = setTimeout(() => { + handleZoomEnd(); + zoomEndTimer.current = null; + }, 200); }; const handleMouseDown = (e: React.MouseEvent) => { @@ -93,12 +123,21 @@ export default ({ children }: PropsWithChildren) => { document.body.style.cursor = 'default'; }; + useEffect(() => { + return () => { + if (zoomEndTimer.current) { + clearTimeout(zoomEndTimer.current); + } + }; + }, []); + return ( { const { dimension } = useDimensionContext(); const { - state: { viewBox }, + state: { viewBox, initialViewBox }, } = useGraphContext(); const { x, y, width, height } = viewBox; + const adjustWidth = viewBox.width + initialViewBox.width; + const adjustHeight = viewBox.height + initialViewBox.height; + const points = [ - `${x},${y}`, - `${x + width},${y}`, - `${x + width},${y + height}`, - `${x},${y + height}`, + `${x - adjustWidth},${y - adjustHeight}`, + `${x + adjustWidth * 2},${y - adjustHeight}`, + `${x + adjustWidth * 2},${y + adjustHeight * 2}`, + `${x - adjustWidth},${y + adjustHeight * 2}`, ].join(' '); return ( diff --git a/apps/client/src/components/GridBackground/patterns/GridPatternMinor.tsx b/apps/client/src/components/GridBackground/patterns/GridPatternMinor.tsx index 81987491..0195a8cc 100644 --- a/apps/client/src/components/GridBackground/patterns/GridPatternMinor.tsx +++ b/apps/client/src/components/GridBackground/patterns/GridPatternMinor.tsx @@ -28,6 +28,7 @@ export default ({ points, dimension }: Props) => { width={width} height={height} patternUnits="userSpaceOnUse" + patternTransform={`scale(${1 / 5})`} > { + const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); + const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); + const bottomRightGrid = screenToGrid2d({ + x: bounds.width, + y: bounds.height, + }); + const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); + + const point1 = gridToScreen3d({ + col: topLeftGrid.col + 1, + row: topLeftGrid.row, + }); + const point2 = gridToScreen3d({ + col: topRightGrid.col + 1, + row: topRightGrid.row, + }); + const point3 = gridToScreen3d({ + col: bottomRightGrid.col + 1, + row: bottomRightGrid.row, + }); + const point4 = gridToScreen3d({ + col: bottomLeftGrid.col + 1, + row: bottomLeftGrid.row, + }); + + const points = ` + ${point1.x} ${point1.y}, + ${point2.x} ${point2.y}, + ${point3.x} ${point3.y}, + ${point4.x} ${point4.y} + `; + + return ( + <> + + {children} + + ); +}; diff --git a/apps/client/src/components/Group/ncloud/RegionGroup.tsx b/apps/client/src/components/Group/ncloud/RegionGroup.tsx index 2e30ebeb..91c9b11f 100644 --- a/apps/client/src/components/Group/ncloud/RegionGroup.tsx +++ b/apps/client/src/components/Group/ncloud/RegionGroup.tsx @@ -1,61 +1,24 @@ import Text from '@components/Group/ncloud/Title'; import { useDimensionContext } from '@contexts/DimensionContext'; import { Bounds, Group } from '@types'; -import { generateRandomRGB, gridToScreen3d, screenToGrid2d } from '@utils'; +import { generateRandomRGB } from '@utils'; import { useMemo } from 'react'; +import Rect3D from './Rect3D'; interface Props extends Partial { color: string; bounds: Bounds; } -const Region3D = ({ bounds, name, color }: Props) => { - const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); - const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); - const bottomRightGrid = screenToGrid2d({ - x: bounds.width, - y: bounds.height, - }); - const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); - - const point1 = gridToScreen3d({ - col: topLeftGrid.col + 1, - row: topLeftGrid.row, - }); - const point2 = gridToScreen3d({ - col: topRightGrid.col + 1, - row: topRightGrid.row, - }); - const point3 = gridToScreen3d({ - col: bottomRightGrid.col + 1, - row: bottomRightGrid.row, - }); - const point4 = gridToScreen3d({ - col: bottomLeftGrid.col + 1, - row: bottomLeftGrid.row, - }); - - const points = ` - ${point1.x} ${point1.y}, - ${point2.x} ${point2.y}, - ${point3.x} ${point3.y}, - ${point4.x} ${point4.y} - `; - +const Region3D = ({ bounds, properties, color }: Props) => { return ( - <> - - - + + + ); }; -const Region2D = ({ bounds, color, name }: Props) => { +const Region2D = ({ bounds, color, properties }: Props) => { const points = `0 0, 0 ${bounds.height}, ${bounds.width} ${bounds.height}, ${bounds.width} 0`; return ( @@ -66,18 +29,18 @@ const Region2D = ({ bounds, color, name }: Props) => { strokeWidth="8" fill="none" > - + ); }; -export default ({ bounds, name }: Pick) => { +export default ({ bounds, properties }: Omit) => { const { dimension } = useDimensionContext(); const color = useMemo(() => generateRandomRGB(), []); return dimension === '2d' ? ( - + ) : ( - + ); }; diff --git a/apps/client/src/components/Group/ncloud/SubnetGroup.tsx b/apps/client/src/components/Group/ncloud/SubnetGroup.tsx index 4fa4a922..8f43ff75 100644 --- a/apps/client/src/components/Group/ncloud/SubnetGroup.tsx +++ b/apps/client/src/components/Group/ncloud/SubnetGroup.tsx @@ -1,61 +1,24 @@ import Text from '@components/Group/ncloud/Title'; import { useDimensionContext } from '@contexts/DimensionContext'; import { Bounds, Group } from '@types'; -import { generateRandomRGB, gridToScreen3d, screenToGrid2d } from '@utils'; +import { calcIsoMatrixPoint, generateRandomRGB } from '@utils'; import { useMemo } from 'react'; +import Rect3D from './Rect3D'; interface Props extends Partial { color: string; bounds: Bounds; } -const Subnet3D = ({ bounds, name, color }: Props) => { - const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); - const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); - const bottomRightGrid = screenToGrid2d({ - x: bounds.width, - y: bounds.height, - }); - const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); - - const point1 = gridToScreen3d({ - col: topLeftGrid.col + 1, - row: topLeftGrid.row, - }); - const point2 = gridToScreen3d({ - col: topRightGrid.col + 1, - row: topRightGrid.row, - }); - const point3 = gridToScreen3d({ - col: bottomRightGrid.col + 1, - row: bottomRightGrid.row, - }); - const point4 = gridToScreen3d({ - col: bottomLeftGrid.col + 1, - row: bottomLeftGrid.row, - }); - - const points = ` - ${point1.x} ${point1.y}, - ${point2.x} ${point2.y}, - ${point3.x} ${point3.y}, - ${point4.x} ${point4.y} - `; - +const Subnet3D = ({ bounds, properties, color }: Props) => { return ( - <> - - - + + + ); }; -const Subnet2D = ({ bounds, color, name }: Props) => { +const Subnet2D = ({ bounds, color, properties }: Props) => { const points = `0 0, 0 ${bounds.height}, ${bounds.width} ${bounds.height}, ${bounds.width} 0`; return ( @@ -66,18 +29,18 @@ const Subnet2D = ({ bounds, color, name }: Props) => { strokeWidth="8" fill="none" > - + ); }; -export default ({ bounds, name }: Pick) => { +export default ({ bounds, properties }: Omit) => { const { dimension } = useDimensionContext(); const color = useMemo(() => generateRandomRGB(), []); return dimension === '2d' ? ( - + ) : ( - + ); }; diff --git a/apps/client/src/components/Group/ncloud/VPCGroup.tsx b/apps/client/src/components/Group/ncloud/VPCGroup.tsx index d9e595d8..567a61c3 100644 --- a/apps/client/src/components/Group/ncloud/VPCGroup.tsx +++ b/apps/client/src/components/Group/ncloud/VPCGroup.tsx @@ -1,61 +1,24 @@ import Text from '@components/Group/ncloud/Title'; import { useDimensionContext } from '@contexts/DimensionContext'; import { Bounds, Group } from '@types'; -import { generateRandomRGB, gridToScreen3d, screenToGrid2d } from '@utils'; +import { calcIsoMatrixPoint, generateRandomRGB } from '@utils'; import { useMemo } from 'react'; +import Rect3D from './Rect3D'; interface Props extends Partial { color: string; bounds: Bounds; } -const VPC3D = ({ bounds, name, color }: Props) => { - const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); - const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); - const bottomRightGrid = screenToGrid2d({ - x: bounds.width, - y: bounds.height, - }); - const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); - - const point1 = gridToScreen3d({ - col: topLeftGrid.col + 1, - row: topLeftGrid.row, - }); - const point2 = gridToScreen3d({ - col: topRightGrid.col + 1, - row: topRightGrid.row, - }); - const point3 = gridToScreen3d({ - col: bottomRightGrid.col + 1, - row: bottomRightGrid.row, - }); - const point4 = gridToScreen3d({ - col: bottomLeftGrid.col + 1, - row: bottomLeftGrid.row, - }); - - const points = ` - ${point1.x} ${point1.y}, - ${point2.x} ${point2.y}, - ${point3.x} ${point3.y}, - ${point4.x} ${point4.y} - `; - +const VPC3D = ({ bounds, properties, color }: Props) => { return ( - <> - - - + + + ); }; -const VPC2D = ({ bounds, color, name }: Props) => { +const VPC2D = ({ bounds, color, properties }: Props) => { const points = `0 0, 0 ${bounds.height}, ${bounds.width} ${bounds.height}, ${bounds.width} 0`; return ( @@ -66,18 +29,18 @@ const VPC2D = ({ bounds, color, name }: Props) => { strokeWidth="8" fill="none" > - + ); }; -export default ({ bounds, name }: Pick) => { +export default ({ bounds, properties }: Omit) => { const { dimension } = useDimensionContext(); const color = useMemo(() => generateRandomRGB(), []); return dimension === '2d' ? ( - + ) : ( - + ); }; diff --git a/apps/client/src/components/Layout/Header/ActionsButtons.tsx b/apps/client/src/components/Layout/Header/ActionsButtons.tsx new file mode 100644 index 00000000..f3db25cc --- /dev/null +++ b/apps/client/src/components/Layout/Header/ActionsButtons.tsx @@ -0,0 +1,153 @@ +import { urls } from '@/src/apis'; +import { getPropertyFilters } from '@/src/models/ncloud'; +import { transformObject, validateObject } from '@/src/models/ncloud/utils'; +import CodeDrawer from '@components/CodeDrawer'; +import ShareDialog from '@components/ShareDialog'; +import { useDimensionContext } from '@contexts/DimensionContext'; +import { useEdgeContext } from '@contexts/EdgeContext'; +import { useGroupContext } from '@contexts/GroupContext'; +import { useNodeContext } from '@contexts/NodeContext'; +import useFetch from '@hooks/useFetch'; +import useNCloud from '@hooks/useNCloud'; +import { Button, ToggleButton, ToggleButtonGroup } from '@mui/material'; +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { TerraformConverter } from 'terraform/converter/TerraformConverter'; + +const CURRENT_ALLOWED_RESOURCE_TYPES = ['server', 'object-storage', 'db-mysql']; +export default () => { + const { + state: { nodes }, + } = useNodeContext(); + const { + state: { groups }, + } = useGroupContext(); + const { + state: { edges }, + } = useEdgeContext(); + + const { selectedResource } = useNCloud(); + const { dimension, changeDimension } = useDimensionContext(); + const [openDrawer, setOpenDrawer] = useState(false); + const [terraformCode, setTerraformCode] = useState(''); + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + const params = useParams(); + + const handleOpenShareDialog = () => { + setOpen(true); + }; + const handleCloseShareDialog = () => { + setOpen(false); + }; + + const { execute: saveArchitecture } = useFetch( + urls('privateArchi', params?.id ?? ''), + { + method: params?.id ? 'PATCH' : 'POST', + }, + ); + + const validateResource = ( + resources: { + type: string; + properties: any; + }[], + ) => { + const validResult: { type: string; isValid: boolean }[] = []; + resources.forEach((resource) => { + if ( + !validateObject( + resource.properties, + getPropertyFilters(resource.type), + ) + ) { + validResult.push({ type: resource.type, isValid: false }); + } + }); + + return validResult; + }; + const handleConvertTerraform = () => { + let resources = selectedResource + ? [ + { + type: selectedResource.type, + properties: transformObject(selectedResource.properties), + }, + ] + : Object.values(nodes) + .filter((node) => + CURRENT_ALLOWED_RESOURCE_TYPES.includes(node.type), + ) + .map((node) => ({ + type: node.type, + properties: transformObject(node.properties), + })); + + const validResult = validateResource(resources); + const isValid = validResult.every((result) => result.isValid); + if (!isValid) { + let errorMessages = ''; + validResult.forEach((result) => { + errorMessages += `${result.type}, `; + }); + + alert(`${errorMessages.slice(0, -2)} properties are not valid`); + return; + } + + const Converter = new TerraformConverter(); + Converter.addResourceFromJson(resources); + setTerraformCode(Converter.generate()); + setOpenDrawer(true); + }; + + const handleSave = async () => { + const resp = await saveArchitecture({ + cost: 0, + architecture: { + nodes, + groups, + edges, + }, + title: 'Test', + }); + if (resp.id) { + navigate(`${resp.id}`); + } + }; + + return ( + <> + + + + + changeDimension(dimension === '2d' ? '3d' : '2d') + } + sx={{ + height: '38px', + }} + > + 2D + 3D + + + setOpenDrawer(false)} + /> + + + ); +}; diff --git a/apps/client/src/components/Layout/Header/UtilityButtons.tsx b/apps/client/src/components/Layout/Header/UtilityButtons.tsx new file mode 100644 index 00000000..d882225b --- /dev/null +++ b/apps/client/src/components/Layout/Header/UtilityButtons.tsx @@ -0,0 +1,44 @@ +import { styled, useColorScheme } from '@mui/material/styles'; +import { IconButton, ButtonGroup, Divider } from '@mui/material'; +import DarkModeIcon from '@mui/icons-material/DarkMode'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import LightModeIcon from '@mui/icons-material/LightMode'; +import ManageHistoryIcon from '@mui/icons-material/ManageHistory'; + +const GITHUB_URL = 'https://github.com/boostcampwm-2024/web37-cloud-canvas'; +const NOTION_URL = + 'https://pleasant-muenster-8f5.notion.site/Boostcamp-Web37-cloud-canvas-12a389341f0a806dbb98d597fd7b4e52?pvs=4'; + +const StyledIconButton = styled(IconButton)(({ theme }) => ({ + borderRadius: '12px', + border: `1px solid ${theme.palette.divider}`, + scale: 0.8, + [`&:hover`]: { + border: `1px solid ${theme.palette.primary.main}`, + color: theme.palette.primary.main, + transition: 'border 0.3s linear', + }, +})); + +export default () => { + const { mode: themeMode, setMode: setThemeMode } = useColorScheme(); + const handleToggleTheme = () => + setThemeMode(themeMode === 'dark' ? 'light' : 'dark'); + + const openWindow = (url: string) => window.open(url, '_blank')?.focus(); + return ( + + openWindow(GITHUB_URL)}> + + + + openWindow(NOTION_URL)}> + + + + + {themeMode === 'dark' ? : } + + + ); +}; diff --git a/apps/client/src/components/Layout/Header/index.tsx b/apps/client/src/components/Layout/Header/index.tsx index c24a4d7a..121caf22 100644 --- a/apps/client/src/components/Layout/Header/index.tsx +++ b/apps/client/src/components/Layout/Header/index.tsx @@ -1,16 +1,10 @@ -import { useDimensionContext } from '@contexts/DimensionContext'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import LightModeIcon from '@mui/icons-material/LightMode'; -import ManageHistoryIcon from '@mui/icons-material/ManageHistory'; -import { ToggleButton, ToggleButtonGroup } from '@mui/material'; import Box from '@mui/material/Box'; -import ButtonGroup from '@mui/material/ButtonGroup'; -import Divider from '@mui/material/Divider'; -import IconButton from '@mui/material/IconButton'; import Stack from '@mui/material/Stack'; -import { styled, useColorScheme } from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; +import { useLocation, useNavigate } from 'react-router-dom'; +import ActionsButtons from './ActionsButtons'; +import UtilityButtons from './UtilityButtons'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'flex', @@ -20,67 +14,20 @@ const StyledBox = styled(Box)(({ theme }) => ({ padding: theme.spacing(1, 2), })); -const GITHUB_URL = 'https://github.com/boostcampwm-2024/web37-cloud-canvas'; -const NOTION_URL = - 'https://pleasant-muenster-8f5.notion.site/Boostcamp-Web37-cloud-canvas-12a389341f0a806dbb98d597fd7b4e52?pvs=4'; - -const StyledIconButton = styled(IconButton)(({ theme }) => ({ - borderRadius: '12px', - border: `1px solid ${theme.palette.divider}`, - scale: 0.8, - [`&:hover`]: { - border: `1px solid ${theme.palette.primary.main}`, - color: theme.palette.primary.main, - transition: 'border 0.3s linear', - }, -})); - export default () => { - const { mode: themeMode, setMode: setThemeMode } = useColorScheme(); - const { dimension, toggleDimension } = useDimensionContext(); - - const handleToggleTheme = () => - setThemeMode(themeMode === 'dark' ? 'light' : 'dark'); - - const openWindow = (url: string) => window.open(url, '_blank')?.focus(); - return ( - - - - Cloud Canvas - - - - - 2D - 3D - - - openWindow(GITHUB_URL)}> - - - - openWindow(NOTION_URL)}> - - - - - {themeMode === 'dark' ? ( - - ) : ( - - )} - - - - + <> + + + + Cloud Canvas + + + + + + + + ); }; diff --git a/apps/client/src/components/Layout/Sidebar/ServiceInstance.tsx b/apps/client/src/components/Layout/Sidebar/ServiceInstance.tsx index 7b7b8b3e..2fc3ce82 100644 --- a/apps/client/src/components/Layout/Sidebar/ServiceInstance.tsx +++ b/apps/client/src/components/Layout/Sidebar/ServiceInstance.tsx @@ -25,10 +25,10 @@ export default ({ desc, ...props }: { title: string; desc: string; type: string } & ListItemProps) => { - const { addResource } = useNCloud(); + const { createResource } = useNCloud(); return ( - addResource(type)}> + createResource(type)}> ); diff --git a/apps/client/src/components/Layout/Sidebar/index.tsx b/apps/client/src/components/Layout/Sidebar/index.tsx index 1b2faced..294262de 100644 --- a/apps/client/src/components/Layout/Sidebar/index.tsx +++ b/apps/client/src/components/Layout/Sidebar/index.tsx @@ -17,11 +17,6 @@ const CLOUD_PLATFORMS = [ title: 'Naver Cloud Platform', imgUrl: 'https://pbs.twimg.com/profile_images/1513858761076604929/O7RUa3BX_400x400.jpg', }, - { - value: 'kakao', - title: 'Kakao Cloud Platform', - imgUrl: 'https://i.pinimg.com/474x/63/43/0d/63430dc35b9b01336ecf35584bd4b7e5.jpg', - }, ]; const SidebarPaper = styled(Paper)(({ theme }) => ({ @@ -43,15 +38,15 @@ export default () => { > - + + + { }} > - {REGION_OPTIONS.map((option) => ( + {Object.values(REGIONS).map((option) => ( - handleListItemClick(option.value as Region) + handleListItemClick( + option.id, + option.value as Region, + ) } > diff --git a/apps/client/src/components/NCloud/NetworksBar/SubnetSelect.tsx b/apps/client/src/components/NCloud/NetworksBar/SubnetSelect.tsx index df21b793..be0c913b 100644 --- a/apps/client/src/components/NCloud/NetworksBar/SubnetSelect.tsx +++ b/apps/client/src/components/NCloud/NetworksBar/SubnetSelect.tsx @@ -12,13 +12,14 @@ import Popover from '@mui/material/Popover'; import Typography from '@mui/material/Typography'; import { useTheme } from '@mui/material/styles'; import { useState } from 'react'; +import { nanoid } from 'nanoid'; +import { Tooltip } from '@mui/material'; type Props = { - subnet: string; + subnet: { [id: string]: string } | undefined; subnetList: { [id: string]: string }; disabled?: boolean; - disabledRemove?: boolean; - onUpdateSubnet: (subnet: string) => void; + onChangeSubnet: (id: string, newSubnet: string) => void; onRemoveSubnet: (subnet: string) => void; }; @@ -26,8 +27,7 @@ export default ({ subnet, subnetList, disabled = false, - disabledRemove = false, - onUpdateSubnet, + onChangeSubnet, onRemoveSubnet, }: Props) => { const theme = useTheme(); @@ -41,22 +41,24 @@ export default ({ setAnchorEl(null); }; - const handleListItemClick = (value: string) => { - onUpdateSubnet(value); + const handleListItemClick = (id: string, value: string) => { + if (subnet?.id !== id) { + onChangeSubnet(id, value); + } setAnchorEl(null); }; - const handleAddVPC = (e: React.FormEvent) => { + const handleAddSubnet = (e: React.FormEvent) => { e.preventDefault(); - const vpc = e.currentTarget.vpc.value; - if (vpc) { - onUpdateSubnet(vpc); + const newSubnet = e.currentTarget.subnet.value; + if (newSubnet) { + onChangeSubnet(`subnet-${nanoid()}`, newSubnet); } setAnchorEl(null); }; const open = Boolean(anchorEl); - const id = open ? 'vpc' : undefined; + const id = open ? 'subnet' : undefined; return ( @@ -70,15 +72,22 @@ export default ({ > Subnet - + + + + -
+ e.stopPropagation()} @@ -114,12 +123,14 @@ export default ({ {Object.entries(subnetList).map(([id, value]) => ( handleListItemClick(value as string)} + onClick={() => + handleListItemClick(id, value as string) + } secondaryAction={ onRemoveSubnet(id)} > @@ -127,7 +138,7 @@ export default ({ } style={{ backgroundColor: - subnet === value + subnet?.id === id ? theme.palette.action.selected : undefined, }} diff --git a/apps/client/src/components/NCloud/NetworksBar/VpcSelect.tsx b/apps/client/src/components/NCloud/NetworksBar/VpcSelect.tsx index 05d9db71..a352bda7 100644 --- a/apps/client/src/components/NCloud/NetworksBar/VpcSelect.tsx +++ b/apps/client/src/components/NCloud/NetworksBar/VpcSelect.tsx @@ -13,13 +13,14 @@ import Popover from '@mui/material/Popover'; import Typography from '@mui/material/Typography'; import { useState } from 'react'; import { Tooltip } from '@mui/material'; +import { nanoid } from 'nanoid'; +import { findKeyByValue } from '@utils'; type Props = { - vpc: string; + vpc: { [id: string]: string } | undefined; vpcList: { [id: string]: string }; disabled?: boolean; - disabledRemove?: boolean; - onUpdateVpc: (vpc: string) => void; + onChangeVpc: (id: string, newVpc: string) => void; onRemoveVpc: (vpc: string) => void; }; @@ -27,8 +28,7 @@ export default ({ vpc, vpcList, disabled = false, - disabledRemove = false, - onUpdateVpc, + onChangeVpc, onRemoveVpc, }: Props) => { const theme = useTheme(); @@ -42,16 +42,20 @@ export default ({ setAnchorEl(null); }; - const handleListItemClick = (value: string) => { - onUpdateVpc(value); + const handleListItemClick = (id: string, value: string) => { + if (vpc?.id !== id) { + onChangeVpc(id, value); + } setAnchorEl(null); }; - const handleAddVPC = (e: React.FormEvent) => { + //TODO: 숫자 validation + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - const vpc = e.currentTarget.vpc.value; - if (vpc) { - onUpdateVpc(vpc); + const newVpc = e.currentTarget.vpc.value; + if (newVpc) { + const id = findKeyByValue(newVpc, vpcList) ?? `vpc-${nanoid()}`; + onChangeVpc(id, newVpc); } setAnchorEl(null); }; @@ -61,7 +65,7 @@ export default ({ return ( - + VPC - - + + + + + - + e.stopPropagation()} endAdornment={ - + } @@ -115,27 +128,24 @@ export default ({ {Object.entries(vpcList).map(([id, value]) => ( handleListItemClick(value as string)} + onClick={() => + handleListItemClick(id, value as string) + } secondaryAction={ - - - onRemoveVpc(id)} - > - - - - + + onRemoveVpc(id)} + > + + + } style={{ backgroundColor: - vpc === value + vpc?.id === id ? theme.palette.action.selected : undefined, }} diff --git a/apps/client/src/components/NCloud/NetworksBar/index.tsx b/apps/client/src/components/NCloud/NetworksBar/index.tsx index 0f62b92b..b8c605e1 100644 --- a/apps/client/src/components/NCloud/NetworksBar/index.tsx +++ b/apps/client/src/components/NCloud/NetworksBar/index.tsx @@ -2,23 +2,29 @@ import RegionSelect from '@components/NCloud/NetworksBar/RegionSelect'; import SubnetSelect from '@components/NCloud/NetworksBar/SubnetSelect'; import VpcSelect from '@components/NCloud/NetworksBar/VpcSelect'; import useNCloud from '@hooks/useNCloud'; -import { AppBar, Divider, Stack, Toolbar, Typography } from '@mui/material'; +import AppBar from '@mui/material/AppBar'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { REGIONS } from '@/src/models/ncloud/constants'; export default () => { const { selectedResource, - region, - subnet, - subnetList, - vpc, vpcList, - updateVpc, - updateRegion, - updateSubnet, + subnetList, + changeVpc, + changeSubnet, removeVpc, removeSubnet, + changeRegion, } = useNCloud(); + if (!selectedResource) return; + const { properties } = selectedResource; + return ( { paddingY: 2, }} > - {' '} { whiteSpace: 'nowrap', }} > - {selectedResource?.type.toUpperCase()} + {selectedResource.type.toUpperCase()} } + divider={} spacing={2} + sx={{ + display: 'flex', + alignItems: 'center', + }} > - {selectedResource && - Object.hasOwn(selectedResource?.properties, 'vpc') && ( - - )} - {selectedResource && - Object.hasOwn( - selectedResource?.properties, - 'subnet', - ) && ( - - )} + {Object.hasOwn(properties, 'vpc') && properties.region && ( + + )} + {Object.hasOwn(properties, 'subnet') && properties.vpc && ( + + )} diff --git a/apps/client/src/components/NCloud/PropertiesBar/MySQLDBProperties.tsx b/apps/client/src/components/NCloud/PropertiesBar/MySQLDBProperties.tsx new file mode 100644 index 00000000..6b85e8e0 --- /dev/null +++ b/apps/client/src/components/NCloud/PropertiesBar/MySQLDBProperties.tsx @@ -0,0 +1,173 @@ +import { + MYSQLDBProp, + validateMySQLDB, + ValidationErrors, +} from '@/src/models/ncloud/MySQLDB'; +import { NETWORKS_CATEGORIES } from '@/src/models/ncloud/Networks'; +import useNCloud from '@hooks/useNCloud'; +import { FormHelperText } from '@mui/material'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import Input from '@mui/material/Input'; +import InputLabel from '@mui/material/InputLabel'; +import Stack from '@mui/material/Stack'; +import { useEffect, useState } from 'react'; +import validator from 'validator'; + +type Props = {}; + +export default ({}: Props) => { + const { selectedResource, updateProperties } = useNCloud(); + + const [properties, setProperties] = useState({ + serverName: '', + serverNamePrefix: '', + userName: '', + userPassword: '', + hostIp: '', + databaseName: '', + serviceName: '', + }); + + const [errors, setErrors] = useState({}); + + useEffect(() => { + if (!selectedResource) return; + const { properties } = selectedResource; + const { + serverName, + serverNamePrefix, + userName, + userPassword, + hostIp, + serviceName, + databaseName, + } = properties; + setProperties((prev) => ({ + ...prev, + serverName, + serverNamePrefix, + userName, + serviceName, + userPassword, + hostIp: '192.168.0.1', + databaseName, + })); + }, [selectedResource]); + + // 192.168.0.01 μž„μ‹œμš© + const handleChange = (e: React.ChangeEvent, type: string) => { + const value = (e.target as HTMLInputElement).value; + if (!selectedResource) return; + const updatedProperties = { + ...properties, + [type]: value, + }; + setProperties(updatedProperties); + updateProperties(selectedResource.id, { + [type]: value, + hostIp: '192.168.0.1', + }); + + const currentErrors = validateMySQLDB(updatedProperties); + setErrors(currentErrors); + }; + + const propertiesWithoutNetworks = Object.keys( + selectedResource?.properties ?? {}, + ).filter((prop) => !NETWORKS_CATEGORIES.includes(prop)); + + const getLabel = (prop: string) => { + switch (prop) { + case 'serverName': + return `Server Name`; + case 'serverNamePrefix': + return `Server Name Prefix`; + case 'userName': + return `User Name`; + case 'userPassword': + return `User Password`; + case 'hostIp': + return `Host IP`; + case 'databaseName': + return `Database Name`; + case 'serviceName': + return 'Service Name'; + default: + return 'Unsupport Type'; + } + }; + + return ( + + } + spacing={2} + > + {propertiesWithoutNetworks.slice(0, 4).map((prop) => ( + + {getLabel(prop)} + e.stopPropagation()} + onChange={(e) => handleChange(e, prop)} + /> + {errors[prop as keyof MYSQLDBProp] && ( + + {errors[prop as keyof MYSQLDBProp]} + + )} + + ))} + + } + spacing={2} + > + {propertiesWithoutNetworks.slice(4).map((prop) => ( + + {getLabel(prop)} + e.stopPropagation()} + onChange={(e) => handleChange(e, prop)} + /> + {errors[prop as keyof MYSQLDBProp] && ( + + {errors[prop as keyof MYSQLDBProp]} + + )} + + ))} + + + ); +}; diff --git a/apps/client/src/components/NCloud/PropertiesBar/ObjectStorageProperties.tsx b/apps/client/src/components/NCloud/PropertiesBar/ObjectStorageProperties.tsx new file mode 100644 index 00000000..faf24aa9 --- /dev/null +++ b/apps/client/src/components/NCloud/PropertiesBar/ObjectStorageProperties.tsx @@ -0,0 +1,53 @@ +import useNCloud from '@hooks/useNCloud'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import Input from '@mui/material/Input'; +import InputLabel from '@mui/material/InputLabel'; +import Stack from '@mui/material/Stack'; +import { useEffect, useState } from 'react'; + +type Props = {}; + +export default ({}: Props) => { + const { selectedResource, updateProperties } = useNCloud(); + + const [bucketName, setBucketName] = useState(''); + + useEffect(() => { + if (!selectedResource) return; + const { properties } = selectedResource; + setBucketName(properties.bucketName ?? ''); + }, [selectedResource]); + + const handleChangeName = (e: React.ChangeEvent) => { + const newName = e.target.value; + setBucketName(newName); + if (!selectedResource) return; + updateProperties(selectedResource.id, { bucketName: newName }); + }; + + return ( + } + spacing={2} + > + + Resource Name + e.stopPropagation()} + onChange={handleChangeName} + /> + + + ); +}; diff --git a/apps/client/src/components/NCloud/PropertiesBar/ServerProperties.tsx b/apps/client/src/components/NCloud/PropertiesBar/ServerProperties.tsx new file mode 100644 index 00000000..cfaa23a8 --- /dev/null +++ b/apps/client/src/components/NCloud/PropertiesBar/ServerProperties.tsx @@ -0,0 +1,137 @@ +import { + SERVER_IMAGE_SPEC_CODE, + SERVER_OS_IMAGES, +} from '@/src/models/ncloud/constants'; +import useNCloud from '@hooks/useNCloud'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import Input from '@mui/material/Input'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import { useEffect, useState } from 'react'; + +type Props = {}; + +export default ({}: Props) => { + const { selectedResource, updateProperties } = useNCloud(); + + const [serverImageCode, setServerImageCode] = useState( + selectedResource?.properties.server_image_number ?? '', + ); + const [specCode, setSpecCode] = useState( + selectedResource?.properties.server_spec_code ?? '', + ); + const [name, setName] = useState(selectedResource?.properties.name ?? ''); + + useEffect(() => { + if (!selectedResource) return; + const { properties } = selectedResource; + setName(properties.name ?? ''); + setServerImageCode(properties.server_image_number ?? ''); + setSpecCode(properties.server_spec_code ?? ''); + }, [selectedResource]); + + const handleChangeImage = (e: SelectChangeEvent) => { + const code = e.target.value as string; + if (!selectedResource) return; + setServerImageCode(code); + const firstSpecCode = SERVER_IMAGE_SPEC_CODE[code][0]?.code; + + setSpecCode(firstSpecCode); + updateProperties(selectedResource.id, { + server_image_number: code.toLowerCase(), + server_spec_code: firstSpecCode.toLowerCase(), + }); + }; + + const handleChangeSpec = (e: SelectChangeEvent) => { + const code = e.target.value as string; + if (!selectedResource) return; + setSpecCode(code); + updateProperties(selectedResource.id, { server_spec_code: code }); + }; + + const handleChangeName = (e: React.ChangeEvent) => { + const newName = e.target.value; + setName(newName); + if (!selectedResource) return; + updateProperties(selectedResource.id, { name: newName }); + }; + + return ( + } + spacing={2} + > + + Resource Name + e.stopPropagation()} + onChange={handleChangeName} + /> + + + Server Image + + + + Spec + + + + ); +}; diff --git a/apps/client/src/components/NCloud/PropertiesBar/index.tsx b/apps/client/src/components/NCloud/PropertiesBar/index.tsx new file mode 100644 index 00000000..59b9e881 --- /dev/null +++ b/apps/client/src/components/NCloud/PropertiesBar/index.tsx @@ -0,0 +1,60 @@ +import ServerProperties from '@components/NCloud/PropertiesBar/ServerProperties'; +import useNCloud from '@hooks/useNCloud'; +import { AppBar, Toolbar, Typography } from '@mui/material'; +import ObjectStorageProperties from './ObjectStorageProperties'; +import MySQLDBProperties from './MySQLDBProperties'; + +const PropertiesFactory = (type: string) => { + switch (type) { + case 'server': { + return ; + } + case 'object-storage': + return ; + case 'db-mysql': + return ; + default: { + return ( + + 아직 κ°œλ°œμ€‘ μž…λ‹ˆλ‹€. + + ); + } + } +}; + +export default () => { + const { selectedResource } = useNCloud(); + + if (!selectedResource) return; + + return ( + + + {PropertiesFactory(selectedResource.type)} + + + ); +}; diff --git a/apps/client/src/components/NCloud/PropertySelect.tsx b/apps/client/src/components/NCloud/PropertySelect.tsx deleted file mode 100644 index 0b347fbd..00000000 --- a/apps/client/src/components/NCloud/PropertySelect.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; - -type Props = {}; - -const REGION_OPTIONS = [ - { value: 'kr', label: 'Korea' }, - { value: 'jp', label: 'Japan' }, -]; - -export default ({}: Props) => { - const [region, setRegion] = React.useState(''); - - const handleChange = (event: SelectChangeEvent) => { - setRegion(event.target.value as string); - }; - - return ( - - - Region - - - - ); -}; diff --git a/apps/client/src/components/Node/index.tsx b/apps/client/src/components/Node/index.tsx index 7dfefed9..40ae3b9e 100644 --- a/apps/client/src/components/Node/index.tsx +++ b/apps/client/src/components/Node/index.tsx @@ -1,10 +1,14 @@ import CloudFunctionNode from '@components/Node/ncloud/CloudFunctionNode'; -import DBMySQLNode from '@components/Node/ncloud/DBMySQLNode'; +import ContainerRegistryNode from '@components/Node/ncloud/ContainerRegistry'; +import MySQLDBNode from '@components/Node/ncloud/MySQLDBNode'; import ObjectStorageNode from '@components/Node/ncloud/ObjectStorageNode'; import ServerNode from '@components/Node/ncloud/ServerNode'; import useDrag from '@hooks/useDrag'; import { Node, Point } from '@types'; import { useEffect } from 'react'; +import LoadBalancerNode from './ncloud/LoadBalancer'; +import NatGatewayNode from './ncloud/NatGateway'; +import useGraph from '@hooks/useGraph'; const nodeFactory = (node: Node) => { switch (node.type) { @@ -15,7 +19,13 @@ const nodeFactory = (node: Node) => { case 'object-storage': return ; case 'db-mysql': - return ; + return ; + case 'load-balancer': + return ; + case 'container-registry': + return ; + case 'nat-gateway': + return ; default: null; } @@ -30,6 +40,7 @@ type Props = { export default ({ node, isSelected, onMove, onSelect, onRemove }: Props) => { const { id, point } = node; + const { svgRef } = useGraph(); const { isDragging, startDrag, drag, stopDrag } = useDrag({ initialPoint: point, updateFn: (newPoint) => onMove(id, newPoint), @@ -47,7 +58,8 @@ export default ({ node, isSelected, onMove, onSelect, onRemove }: Props) => { drag({ x: e.clientX, y: e.clientY }); }; - const handleMouseUp = () => { + const handleMouseUp = (e: MouseEvent) => { + if ((e.relatedTarget as HTMLElement)?.closest('.node-actions')) return; stopDrag(); document.body.style.cursor = 'default'; }; @@ -59,14 +71,17 @@ export default ({ node, isSelected, onMove, onSelect, onRemove }: Props) => { }; useEffect(() => { + if (!svgRef.current) return; if (isDragging) { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); + svgRef.current.addEventListener('mouseleave', handleMouseUp); } return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + document?.removeEventListener('mousemove', handleMouseMove); + document?.removeEventListener('mouseup', handleMouseUp); + svgRef.current?.removeEventListener('mouseleave', handleMouseUp); }; }, [isDragging]); diff --git a/apps/client/src/components/Node/ncloud/ContainerRegistry.tsx b/apps/client/src/components/Node/ncloud/ContainerRegistry.tsx new file mode 100644 index 00000000..30cfb8de --- /dev/null +++ b/apps/client/src/components/Node/ncloud/ContainerRegistry.tsx @@ -0,0 +1,147 @@ +import { useDimensionContext } from '@contexts/DimensionContext'; +import { Node } from '@types'; + +type Props = Partial; +//TODO: +const Node3D = ({ properties }: Props) => { + return ( + + + + + + + + + + + + + + + + + + + + Container + + + Container + + + + + ); +}; + +const Node2D = ({ properties }: Props) => { + return ( + + + + + + + + + + + + + + + Container + + + Container + + + + + + ); +}; +export default ({ properties }: Props) => { + const { dimension } = useDimensionContext(); + return dimension === '2d' ? ( + + ) : ( + + ); +}; diff --git a/apps/client/src/components/Node/ncloud/LoadBalancer.tsx b/apps/client/src/components/Node/ncloud/LoadBalancer.tsx new file mode 100644 index 00000000..9ae23f1d --- /dev/null +++ b/apps/client/src/components/Node/ncloud/LoadBalancer.tsx @@ -0,0 +1,104 @@ +import { useDimensionContext } from '@contexts/DimensionContext'; +import { Node } from '@types'; + +type Props = Partial; +const Node3D = ({ properties }: Props) => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; + +const Node2D = ({ properties }: Props) => { + return ( + + + + + ); +}; +export default ({ properties }: Props) => { + const { dimension } = useDimensionContext(); + return dimension === '2d' ? ( + + ) : ( + + ); +}; diff --git a/apps/client/src/components/Node/ncloud/LoadBalancerNode.tsx b/apps/client/src/components/Node/ncloud/LoadBalancerNode.tsx new file mode 100644 index 00000000..9ae23f1d --- /dev/null +++ b/apps/client/src/components/Node/ncloud/LoadBalancerNode.tsx @@ -0,0 +1,104 @@ +import { useDimensionContext } from '@contexts/DimensionContext'; +import { Node } from '@types'; + +type Props = Partial; +const Node3D = ({ properties }: Props) => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; + +const Node2D = ({ properties }: Props) => { + return ( + + + + + ); +}; +export default ({ properties }: Props) => { + const { dimension } = useDimensionContext(); + return dimension === '2d' ? ( + + ) : ( + + ); +}; diff --git a/apps/client/src/components/Node/ncloud/DBMySQLNode.tsx b/apps/client/src/components/Node/ncloud/MySQLDBNode.tsx similarity index 100% rename from apps/client/src/components/Node/ncloud/DBMySQLNode.tsx rename to apps/client/src/components/Node/ncloud/MySQLDBNode.tsx diff --git a/apps/client/src/components/Node/ncloud/NatGateway.tsx b/apps/client/src/components/Node/ncloud/NatGateway.tsx new file mode 100644 index 00000000..1129fcc0 --- /dev/null +++ b/apps/client/src/components/Node/ncloud/NatGateway.tsx @@ -0,0 +1,108 @@ +import { useDimensionContext } from '@contexts/DimensionContext'; +import { Node } from '@types'; + +type Props = Partial; + +const convertToIsoMatrix = (x: number, y: number) => { + const isoMatrix = new DOMMatrix() + .rotate(30) + .skewX(-30) + .scale(1, 0.8602) + .translate(x, y); + + return isoMatrix; // κ²°κ³Ό ν–‰λ ¬ λ°˜ν™˜ +}; +const Node3D = ({ properties }: Props) => { + const matrix = convertToIsoMatrix(0, 0); + + return ( + + + + + + + + + + + + ); +}; + +const Node2D = ({ properties }: Props) => { + return ( + + + + + + + + + + ); +}; + +export default ({ properties }: Partial) => { + const { dimension } = useDimensionContext(); + return dimension === '2d' ? ( + + ) : ( + + ); +}; diff --git a/apps/client/src/components/Node/ncloud/ServerNode.tsx b/apps/client/src/components/Node/ncloud/ServerNode.tsx index 73cc3f8f..1d38a11e 100644 --- a/apps/client/src/components/Node/ncloud/ServerNode.tsx +++ b/apps/client/src/components/Node/ncloud/ServerNode.tsx @@ -1,9 +1,20 @@ import { useDimensionContext } from '@contexts/DimensionContext'; import { Node } from '@types'; -type Props = {}; +type Props = Partial; + +const convertToIsoMatrix = (x: number, y: number) => { + const isoMatrix = new DOMMatrix() + .rotate(30) + .skewX(-30) + .scale(1, 0.8602) + .translate(x, y); + + return isoMatrix; // κ²°κ³Ό ν–‰λ ¬ λ°˜ν™˜ +}; +const Node3D = ({ properties }: Props) => { + const matrix = convertToIsoMatrix(0, 0); -const Node3D = () => { return ( <> { d="M128 37v37l-64 37L0 74V37L64 0l64 37ZM2.054 38.185v34.63L64 108.627l61.946-35.812v-34.63L64 2.373 2.054 38.185Z" > - + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + { fill="#ffffff" style={{ userSelect: 'none' }} > - M5 - {/* {node.properties.instanceType} */} + {properties?.server_spec_code?.split('-')[0].slice(0, 4)} ); }; -const Node2D = () => { +const Node2D = ({ properties }: Props) => { return ( <> @@ -72,25 +98,41 @@ const Node2D = () => { opacity=".05" > + {/* */} + {/* file_type_nginx */} + {/* */} + {/* */} + {/* */} - M5 + {properties?.server_spec_code?.split('-')[0].slice(0, 4)} ); }; -export default ({}: Partial) => { +export default ({ properties }: Partial) => { const { dimension } = useDimensionContext(); - return dimension === '2d' ? : ; + return dimension === '2d' ? ( + + ) : ( + + ); }; diff --git a/apps/client/src/components/NodeActions.tsx b/apps/client/src/components/NodeActions.tsx new file mode 100644 index 00000000..35205a34 --- /dev/null +++ b/apps/client/src/components/NodeActions.tsx @@ -0,0 +1,96 @@ +import { useGraphContext } from '@contexts/GraphConetxt'; +import { useSvgContext } from '@contexts/SvgContext'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import DeleteIcon from '@mui/icons-material/Delete'; +import InsightsIcon from '@mui/icons-material/Insights'; +import { Connection, Node, Point } from '@types'; +import { useEffect, useMemo } from 'react'; +import SpeedDial from './SpeedDial'; + +type Props = { + node: Node; + isConnecting: boolean; + onOpenConnection: (from: Connection) => void; + onConnectConnection: (point: Point) => void; + onRemoveNode: (id: string) => void; +}; + +const actions = [ + { icon: , name: 'Edge', type: 'edge' }, + { icon: , name: 'Remove', type: 'remove' }, +]; +export default ({ + node, + isConnecting, + onOpenConnection, + onConnectConnection, + onRemoveNode, +}: Props) => { + const { id: selectedNodeId, connectors } = node; + const { svgRef } = useSvgContext(); + const { + state: { viewBox }, + } = useGraphContext(); + const handleClickActions = (e: React.MouseEvent, type: string) => { + switch (type) { + case 'edge': { + openConnection(e, 'right', node.connectors.right); + return; + } + case 'remove': { + onRemoveNode(selectedNodeId); + return; + } + default: { + console.error('Not supported action type'); + } + } + }; + const openConnection = ( + e: React.MouseEvent, + connectorType: string, + point: Point, + ) => { + e.stopPropagation(); + onOpenConnection({ + id: selectedNodeId, + connectorType, + point, + }); + document.body.style.cursor = 'move'; + }; + + const handleMouseMove = (e: MouseEvent) => { + const { clientX, clientY } = e; + onConnectConnection({ x: clientX, y: clientY }); + }; + + useEffect(() => { + if (isConnecting) { + document.addEventListener('mousemove', handleMouseMove); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + }; + }, [isConnecting]); + + const point = useMemo(() => { + if (!svgRef.current) return null; + const nodeDOM = svgRef.current.getElementById(selectedNodeId); + if (!nodeDOM) return null; + const rect = nodeDOM.getBoundingClientRect(); + return { + top: rect.y, + left: rect.x + rect.width, + }; + }, [node, viewBox]); + return ( + + ); +}; diff --git a/apps/client/src/components/ShareDialog.tsx b/apps/client/src/components/ShareDialog.tsx new file mode 100644 index 00000000..82efd0af --- /dev/null +++ b/apps/client/src/components/ShareDialog.tsx @@ -0,0 +1,125 @@ +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Select, { OnChangeValue } from 'react-select'; +import { useState } from 'react'; +import Creatable, { useCreatable } from 'react-select/creatable'; +import { Stack, FormControl } from '@mui/material'; +import { useEdgeContext } from '@contexts/EdgeContext'; +import { useGroupContext } from '@contexts/GroupContext'; +import { useNodeContext } from '@contexts/NodeContext'; +import useFetch from '@hooks/useFetch'; +import { urls } from '../apis'; +type Props = { + open: boolean; + onClose: () => void; +}; + +export default ({ open, onClose }: Props) => { + const { + state: { nodes }, + } = useNodeContext(); + const { + state: { groups }, + } = useGroupContext(); + const { + state: { edges }, + } = useEdgeContext(); + + const [tags, setTags] = useState<{ label: string; value: string }[]>([]); + + const { error, execute: shareArchitecture } = useFetch(urls('share'), { + method: 'POST', + }); + + const handleClose = () => { + onClose(); + }; + + const handleChange = (values: OnChangeValue) => setTags(values); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const formJson = Object.fromEntries((formData as any).entries()); + const title = formJson.title; + const cloudTags = tags.map((tag) => tag.value); + + await shareArchitecture({ + cost: 0, + tags: cloudTags, + architecture: { + nodes, + groups, + edges, + }, + title, + }); + + if (error) { + alert('Failed to share'); + } else { + alert('Successfully shared'); + } + handleClose(); + }; + + return ( + + Share + + + + + + + + + ({ + ...provided, + display: 'none', + }), + dropdownIndicator: () => ({ display: 'none' }), + indicatorSeparator: () => ({ display: 'none' }), + }} + /> + + + + + + + + + ); +}; diff --git a/apps/client/src/components/SpeedDial.tsx b/apps/client/src/components/SpeedDial.tsx new file mode 100644 index 00000000..ed27e509 --- /dev/null +++ b/apps/client/src/components/SpeedDial.tsx @@ -0,0 +1,77 @@ +import { useSvgContext } from '@contexts/SvgContext'; +import SpeedDial from '@mui/material/SpeedDial'; +import SpeedDialAction from '@mui/material/SpeedDialAction'; +import SpeedDialIcon from '@mui/material/SpeedDialIcon'; +import { styled } from '@mui/material/styles'; +import React, { memo, ReactNode, useState } from 'react'; + +type Props = { + actions: { + icon: ReactNode; + name: string; + type: string; + }[]; + point: { top: number; left: number }; + selectedNodeId: string; + onClickActions: (e: React.MouseEvent, type: string) => void; +}; + +const StyledSpeedDial = styled(SpeedDial)(({ theme }) => ({ + [`& .MuiSpeedDial-fab`]: { + width: 38, + height: 32, + }, + [`& .MuiSpeedDial-actions`]: { + paddingTop: 30, + }, +})); + +const StyledSpeedDialAction = styled(SpeedDialAction)(({ theme }) => ({ + [`& .MuiSpeedDialAction-fab`]: { + width: 20, + height: 20, + }, +})); + +export default ({ actions, point, onClickActions }: Props) => { + const [open, setOpen] = useState(false); + const [hidden, setHidden] = useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + const offset = 10; + + const handleClickActions = (e: React.MouseEvent, type: string) => { + e.stopPropagation(); + onClickActions(e, type); + handleClose(); + setHidden(true); + }; + + return ( + } + onClose={handleClose} + hidden={hidden} + onOpen={handleOpen} + direction="down" + open={open} + > + {actions.map((action) => ( + handleClickActions(e, action.type)} + /> + ))} + + ); +}; diff --git a/apps/client/src/constants/index.ts b/apps/client/src/constants/index.ts index 429e8f61..1b53a7be 100644 --- a/apps/client/src/constants/index.ts +++ b/apps/client/src/constants/index.ts @@ -22,6 +22,31 @@ export const NCLOUD_SERVICES = [ }, ], }, + { + title: 'container', + items: [ + { + title: 'Container Registry', + desc: 'Container Registry', + type: 'container-registry', + }, + // { + // title: 'Kubernetes', + // desc: 'NCloud Kubernetes Service', + // type: 'Kubernetes', + // }, + ], + }, + { + title: 'storage', + items: [ + { + title: 'Object Storage', + desc: 'Object Storage', + type: 'object-storage', + }, + ], + }, { title: 'database', items: [ @@ -30,13 +55,37 @@ export const NCLOUD_SERVICES = [ desc: 'Managed MySQL database', type: 'db-mysql', }, + // { + // title: 'DB for Redis', + // desc: 'Managed Redis database', + // type: 'db-redis', + // }, + // { + // title: 'DB for MSSQL', + // desc: 'Managed MSSQL database', + // type: 'db-mssql', + // }, + // { + // title: 'DB for MongoDB', + // desc: 'Managed MongoDB database', + // type: 'db-mongo', + // }, + // { + // title: 'DB for PostgreSQL', + // desc: 'Managed PostgreSQL database', + // type: 'db-postgres', + // }, + ], + }, + { + title: 'networks', + items: [ + { + title: 'Load Balancer', + desc: 'load balancing', + type: 'load-balancer', + }, + { title: 'Nat Gateway', desc: 'nat gateway', type: 'nat-gateway' }, ], }, -]; - -export const NETWORKS_CATEGORIES = [ - 'region', - 'vpc', - 'subnet', - 'security-group', ]; diff --git a/apps/client/src/contexts/DimensionContext.tsx b/apps/client/src/contexts/DimensionContext.tsx index 274a5931..b174b9ba 100644 --- a/apps/client/src/contexts/DimensionContext.tsx +++ b/apps/client/src/contexts/DimensionContext.tsx @@ -3,27 +3,24 @@ import { createContext, ReactNode, useContext, useRef, useState } from 'react'; type DimensionState = { dimension: Dimension; - prevDimension: Dimension; - toggleDimension: () => void; + changeDimension: (newDimension: Dimension) => void; }; const DimensionContext = createContext(null); export const DimensionProvider = ({ children }: { children: ReactNode }) => { const [dimension, setDimension] = useState('2d'); - const prevDimensionRef = useRef('2d'); - const toggleDimension = () => { - prevDimensionRef.current = dimension; - setDimension((prev) => (prev === '2d' ? '3d' : '2d')); + const changeDimension = (newDimension: Dimension) => { + if (dimension === newDimension) return; + setDimension(newDimension); }; return ( {children} diff --git a/apps/client/src/contexts/EdgeContext/index.tsx b/apps/client/src/contexts/EdgeContext/index.tsx index 08184430..798c023e 100644 --- a/apps/client/src/contexts/EdgeContext/index.tsx +++ b/apps/client/src/contexts/EdgeContext/index.tsx @@ -3,6 +3,8 @@ import { edgeReducer, EdgeState, } from '@contexts/EdgeContext/reducer'; +import { Edge } from '@types'; +import { isEmpty } from '@utils'; import { createContext, ReactNode, useContext, useReducer } from 'react'; type EdgeContextProps = { @@ -17,8 +19,19 @@ const initialState: EdgeState = { connection: null, }; -export const EdgeProvider = ({ children }: { children: ReactNode }) => { - const [state, dispatch] = useReducer(edgeReducer, initialState); +export const EdgeProvider = ({ + children, + initialEdges, +}: { + children: ReactNode; + initialEdges?: EdgeState['edges']; +}) => { + const [state, dispatch] = useReducer(edgeReducer, { + edges: isEmpty(initialEdges) + ? initialState.edges + : (initialEdges as EdgeState['edges']), + connection: null, + }); return ( diff --git a/apps/client/src/contexts/GraphConetxt/index.tsx b/apps/client/src/contexts/GraphConetxt/index.tsx index 7d0b2971..2e1b7e3b 100644 --- a/apps/client/src/contexts/GraphConetxt/index.tsx +++ b/apps/client/src/contexts/GraphConetxt/index.tsx @@ -3,6 +3,7 @@ import { graphReducer, GraphState, } from '@contexts/GraphConetxt/reducer'; +import { ViewBox } from '@types'; import { createContext, Dispatch, @@ -21,32 +22,22 @@ const CanvasContext = createContext(undefined); const initialState = { viewBox: { x: 0, y: 0, width: 0, height: 0 }, + initialViewBox: { x: 0, y: 0, width: 0, height: 0 }, }; -export const GraphProvider = ({ children }: { children: ReactNode }) => { - const [state, dispatch] = useReducer(graphReducer, initialState); +export const GraphProvider = ({ + children, + initialViewBox, +}: { + children: ReactNode; + initialViewBox: ViewBox; +}) => { + const [state, dispatch] = useReducer(graphReducer, { + ...initialState, + viewBox: initialViewBox, + initialViewBox, + }); - useLayoutEffect(() => { - const svg = document.getElementById('cloud-graph'); - if (!svg) return; - const updateViewBoxSize = () => { - dispatch({ - type: 'SET_VIEWBOX', - payload: { - x: state.viewBox.x || 0, - y: state.viewBox.y || 0, - width: state.viewBox.width || svg.clientWidth, - height: state.viewBox.height || svg.clientHeight, - }, - }); - }; - updateViewBoxSize(); - window.addEventListener('resize', updateViewBoxSize); - - return () => { - window.removeEventListener('resize', updateViewBoxSize); - }; - }, []); return ( {children} diff --git a/apps/client/src/contexts/GraphConetxt/reducer.ts b/apps/client/src/contexts/GraphConetxt/reducer.ts index f4e5563b..7f0d8bce 100644 --- a/apps/client/src/contexts/GraphConetxt/reducer.ts +++ b/apps/client/src/contexts/GraphConetxt/reducer.ts @@ -2,12 +2,17 @@ import { ViewBox } from '@types'; export type GraphState = { viewBox: ViewBox; + initialViewBox: ViewBox; }; export type GraphAction = { type: 'SET_VIEWBOX'; payload: GraphState['viewBox']; }; +// | { +// type: 'INITIAL_VIEWBOX'; +// payload: GraphState['initialViewBox']; +// }; export const graphReducer = ( state: GraphState, @@ -16,6 +21,13 @@ export const graphReducer = ( switch (action.type) { case 'SET_VIEWBOX': return { ...state, viewBox: action.payload }; + // case 'INITIAL_VIEWBOX': { + // return { + // ...state, + // viewBox: action.payload, + // initialViewBox: action.payload, + // }; + // } default: return state; } diff --git a/apps/client/src/contexts/GroupContext/index.tsx b/apps/client/src/contexts/GroupContext/index.tsx index 986bdb6b..1049e221 100644 --- a/apps/client/src/contexts/GroupContext/index.tsx +++ b/apps/client/src/contexts/GroupContext/index.tsx @@ -1,15 +1,11 @@ -import { mockInitialState } from '../../../mocks'; +// import { mockInitialState } from '../../../mocks'; import { GroupAction, groupReducer, GroupState, } from '@contexts/GroupContext/reducer'; -import { - createContext, - PropsWithChildren, - useContext, - useReducer, -} from 'react'; +import { isEmpty } from '@utils'; +import { createContext, ReactNode, useContext, useReducer } from 'react'; type GroupContextProps = { state: GroupState; @@ -22,8 +18,19 @@ const initialState: GroupState = { groups: {}, }; -export const GroupProvider = ({ children }: PropsWithChildren) => { - const [state, dispatch] = useReducer(groupReducer, initialState); +export const GroupProvider = ({ + children, + initialGroups, +}: { + children: ReactNode; + initialGroups?: GroupState['groups']; +}) => { + const [state, dispatch] = useReducer( + groupReducer, + isEmpty(initialGroups) + ? initialState + : { groups: initialGroups as GroupState['groups'] }, + ); return ( diff --git a/apps/client/src/contexts/NCloudContext.tsx b/apps/client/src/contexts/NCloudContext.tsx deleted file mode 100644 index 7f48df8a..00000000 --- a/apps/client/src/contexts/NCloudContext.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { - createContext, - Dispatch, - PropsWithChildren, - SetStateAction, - useContext, - useState, -} from 'react'; - -type SelectedResource = { - id: string; - type: string; - properties: {}; -}; - -type NCloudContextProps = { - region: string; - vpc: string; - vpcList: { [id: string]: string }; - subnet: string; - subnetList: { [id: string]: string }; - selectedResource: SelectedResource | undefined; - setSelectedResource: Dispatch>; - setRegion: Dispatch>; - setVpc: Dispatch>; - setVpcList: Dispatch>; - setSubnet: Dispatch>; - setSubnetList: Dispatch>; -}; - -const NCloudContext = createContext(undefined); - -export const NCloudProvider = ({ children }: PropsWithChildren) => { - const [region, setRegion] = useState('kr'); - const [vpc, setVpc] = useState(''); - const [vpcList, setVpcList] = useState<{ - [id: string]: string; - }>({}); - const [subnet, setSubnet] = useState(''); - const [subnetList, setSubnetList] = useState<{ - [id: string]: string; - }>({}); - - const [selectedResource, setSelectedResource] = useState< - SelectedResource | undefined - >(undefined); - - return ( - - {children} - - ); -}; - -export const useNCloudContext = () => { - const context = useContext(NCloudContext); - if (!context) { - throw new Error('NCloudContext: context is undefined'); - } - return context; -}; diff --git a/apps/client/src/contexts/NodeContext/index.tsx b/apps/client/src/contexts/NodeContext/index.tsx index 4512c4cf..7848936e 100644 --- a/apps/client/src/contexts/NodeContext/index.tsx +++ b/apps/client/src/contexts/NodeContext/index.tsx @@ -1,9 +1,9 @@ -import { mockInitialState } from '../../../mocks'; import { NodeAction, nodeReducer, NodeState, } from '@contexts/NodeContext/reducer'; +import { isEmpty } from '@utils'; import { createContext, Dispatch, @@ -23,8 +23,19 @@ const initialState: NodeState = { nodes: {}, }; -export const NodeProvider = ({ children }: { children: ReactNode }) => { - const [state, dispatch] = useReducer(nodeReducer, initialState); +export const NodeProvider = ({ + children, + initialNodes, +}: { + children: ReactNode; + initialNodes?: NodeState['nodes']; +}) => { + const [state, dispatch] = useReducer( + nodeReducer, + isEmpty(initialNodes) + ? initialState + : { nodes: initialNodes as NodeState['nodes'] }, + ); return ( diff --git a/apps/client/src/helpers/cloud.ts b/apps/client/src/helpers/cloud.ts deleted file mode 100644 index d5409788..00000000 --- a/apps/client/src/helpers/cloud.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getSvgPoint } from '@utils'; - -export const getInitPoint = (svg: SVGSVGElement) => { - const svgRect = svg.getBoundingClientRect(); - - const leftCenterSvg = getSvgPoint(svg, { - x: svgRect.left, - y: svgRect.top, - }); - - return { - x: leftCenterSvg.x + 200, - y: svgRect.height / 3, - }; -}; diff --git a/apps/client/src/helpers/edge.ts b/apps/client/src/helpers/edge.ts index f5b624a2..aef3e6ba 100644 --- a/apps/client/src/helpers/edge.ts +++ b/apps/client/src/helpers/edge.ts @@ -112,6 +112,7 @@ export const updateNearestConnectorPair = ( const lastBendPoint = edge.bendingPoints[edge.bendingPoints.length - 1]; const bendConnector = generateBendConnector(lastBendPoint); + //TODO: 이름 λ³€κ²½ ν•„μš” const { movingConnector } = findNearestConnectorPair( movingConnectors, [bendConnector], diff --git a/apps/client/src/helpers/group.ts b/apps/client/src/helpers/group.ts index f3ab4183..cc3dc50f 100644 --- a/apps/client/src/helpers/group.ts +++ b/apps/client/src/helpers/group.ts @@ -1,45 +1,76 @@ -import { GRID_2D_SIZE } from '@constants'; -import { Bounds, Dimension, Group } from '@types'; +import { NODE_BASE_SIZE } from '@constants'; +import { Dimension, Group, Node, Size3D } from '@types'; import { convert2dTo3dPoint, convert3dTo2dPoint } from '@utils'; +import { getNodeOffsetForDimension } from './node'; -export const computeBounds = (_bounds: Bounds[], dimension: Dimension) => { - const padding = GRID_2D_SIZE * 2; - let bounds = _bounds; - if (dimension === '3d') { - bounds = bounds.map((bound) => ({ - ...bound, - ...convert3dTo2dPoint({ - x: bound.x, - y: bound.y, - }), - })); - } +export const GraphGroup = { + id: '', + type: '', + nodeIds: [], + properties: {}, + childGroupIds: [], + parentGroupId: '', +}; - const minX = Math.min(...bounds.map((bounds) => bounds.x)); - const minY = Math.min(...bounds.map((bounds) => bounds.y)); - const maxX = Math.max(...bounds.map((bounds) => bounds.x + bounds.width)); - const maxY = Math.max(...bounds.map((bounds) => bounds.y + bounds.height)); +export const computeBounds = ( + nodes: Node[], + dimension: Dimension, + paddingSize: number = 1, +) => { + const padding = 90 * paddingSize; - let x = minX - padding; - let y = minY - padding; - let width = maxX - minX + padding * 2; - let height = maxY - minY + padding * 2; + if (dimension === '2d') { + const minX = Math.min(...nodes.map((node) => node.point.x)); + const minY = Math.min(...nodes.map((node) => node.point.y)); + const maxX = Math.max( + ...nodes.map((node) => node.point.x + node.size['2d'].width), + ); + const maxY = Math.max( + ...nodes.map((node) => node.point.y + node.size['2d'].height), + ); - if (dimension === '3d') { - const minPoint = convert2dTo3dPoint({ + return { x: minX - padding, y: minY - padding, + width: maxX - minX + padding * 2, + height: maxY - minY + padding * 2, + }; + } else { + //2d + nodes = nodes.map((node) => { + const offset = getNodeOffsetForDimension( + node.size['3d'], + NODE_BASE_SIZE['3d'] as Size3D, + ); + const pos = convert3dTo2dPoint({ + x: node.point.x - offset.x, + y: node.point.y - offset.y, + }); + return { + ...node, + point: { x: pos.x, y: pos.y }, + }; }); - x = minPoint.x; - y = minPoint.y; - } + const minX = Math.min(...nodes.map((node) => node.point.x)); + const minY = Math.min(...nodes.map((node) => node.point.y)); + const maxX = Math.max( + ...nodes.map((node) => node.point.x + node.size['2d'].width), + ); + const maxY = Math.max( + ...nodes.map((node) => node.point.y + node.size['2d'].height), + ); - return { - x, - y, - width, - height, - }; + const { x, y } = convert2dTo3dPoint({ + x: minX - padding, + y: minY - padding, + }); + return { + x, + y, + width: maxX - minX + padding * 2, + height: maxY - minY + padding * 2, + }; + } }; export const findParentGroup = ( diff --git a/apps/client/src/helpers/node.ts b/apps/client/src/helpers/node.ts index a99ab6d9..ef28980a 100644 --- a/apps/client/src/helpers/node.ts +++ b/apps/client/src/helpers/node.ts @@ -1,5 +1,5 @@ import { NODE_BASE_SIZE } from '@constants'; -import { Dimension, Node, Point, Size } from '@types'; +import { Dimension, Node, Point, Size, Size3D } from '@types'; import { alignPoint2d, alignPoint3d, @@ -7,7 +7,23 @@ import { convert3dTo2dPoint, } from '@utils'; -const getNodeOffsetForDimension = (nodeSize: Size, baseSize: Size) => { +export const GraphNode = { + id: '', + type: '', + point: { x: 0, y: 0 }, + connectors: {}, +}; + +export const getNodeOffsetForDimension = ( + nodeSize: Size & Size3D, + baseSize: Size, +) => { + if (nodeSize.width % 128 === 0 && nodeSize.height % 111 === 0) { + return { + x: 0, + y: 0, + }; + } return { x: (baseSize.width - nodeSize.width) / 2, y: baseSize.height - nodeSize.height - (nodeSize.offset || 0), @@ -17,12 +33,14 @@ const getNodeOffsetForDimension = (nodeSize: Size, baseSize: Size) => { //INFO: 처음이 2d둜 μ‹œμž‘ν•˜κΈ° λ•Œλ¬Έμ— nodeSize : 3d , baseSize : 3d둜 해야함. λ‹€λ₯Έ 방법은 잘 λͺ¨λ₯΄κ³˜μŒ. //2dμ—μ„œ 3d둜 λ³€ν™˜ν•  λ•ŒλŠ” 3dμ—μ„œ 2d둜 λ³€ν™˜ν•  λ•Œμ™€ 달리 baseSize와 nodeSizeκ°€ 2d μ‚¬μ΄μ¦ˆ λ“€μ–΄κ°€μ•Ό ν•  것 κ°™μŒ export const adjustNodePointForDimension = ( - node: Node, + point: Point, + size: Size3D, dimension: Dimension, ) => { - const { point, size } = node; - - const offset = getNodeOffsetForDimension(size['3d'], NODE_BASE_SIZE['3d']); + const offset = getNodeOffsetForDimension( + size, + NODE_BASE_SIZE['3d'] as Size3D, + ); let result; if (dimension === '2d') { result = convert3dTo2dPoint({ @@ -66,11 +84,24 @@ export const alignNodePoint = ( return result; }; -export const getNodeBounds = (node: Node, dimension: Dimension) => { - return { - x: node.point.x, - y: node.point.y, - width: node.size[dimension].width, - height: node.size[dimension].height, - }; +export const calculateNodeBoundingBox = ( + nodes: Record, + dimension: Dimension, +) => { + const nodesArr = Object.values(nodes); + + const minX = Math.min(...nodesArr.map((node) => node.point.x)); + const minY = Math.min(...nodesArr.map((node) => node.point.y)); + + const maxX = Math.max( + ...nodesArr.map((node) => node.point.x + node.size[dimension].width), + ); + const maxY = Math.max( + ...nodesArr.map((node) => node.point.y + node.size[dimension].height), + ); + + const width = maxX - minX; + const height = maxY - minY; + + return { minX, minY, width, height }; }; diff --git a/apps/client/src/helpers/viewBox.ts b/apps/client/src/helpers/viewBox.ts new file mode 100644 index 00000000..7c3c1b97 --- /dev/null +++ b/apps/client/src/helpers/viewBox.ts @@ -0,0 +1,41 @@ +import { GRID_2D_SIZE } from '@constants'; +import { Dimension, Node, ViewBox } from '@types'; +import { calculateNodeBoundingBox } from './node'; + +export const calcViewBoxBounds = ( + nodes: Record, + viewBox: ViewBox, + dimension: Dimension, +) => { + const allNodeBounds = calculateNodeBoundingBox(nodes, dimension); + const viewBoxCenter = { + x: viewBox.x + viewBox.width / 2, + y: viewBox.y + viewBox.height / 2, + }; + const allNodeCenter = { + x: allNodeBounds.minX + allNodeBounds.width / 2, + y: allNodeBounds.minY + allNodeBounds.height / 2, + }; + + const diff = { + x: viewBoxCenter.x - allNodeCenter.x, + y: viewBoxCenter.y - allNodeCenter.y, + }; + + const padding = GRID_2D_SIZE * 8; + let newWidth = + viewBox.width < allNodeBounds.width + ? allNodeBounds.width + padding + : viewBox.width; + let newHeight = + viewBox.height < allNodeBounds.height + ? allNodeBounds.height + padding + : viewBox.height; + + return { + x: viewBox.x - diff.x, + y: viewBox.y - diff.y, + width: newWidth, + height: newHeight, + }; +}; diff --git a/apps/client/src/hooks/useConnection.ts b/apps/client/src/hooks/useConnection.ts index d8afb030..0dd54f65 100644 --- a/apps/client/src/hooks/useConnection.ts +++ b/apps/client/src/hooks/useConnection.ts @@ -98,6 +98,7 @@ export default ({ updateEdgeFn }: Props) => { ) { updateEdgeFn(sourceRef.current, targetRef.current); } + targetRef.current = null; }; return { diff --git a/apps/client/src/hooks/useFetch.ts b/apps/client/src/hooks/useFetch.ts new file mode 100644 index 00000000..3c44afa0 --- /dev/null +++ b/apps/client/src/hooks/useFetch.ts @@ -0,0 +1,74 @@ +import { undefinedReplacer } from '@utils'; +import { useState, useEffect } from 'react'; + +type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; + +interface UseFetchOptions { + method?: HttpMethod; + headers?: HeadersInit; + body?: any; + trigger?: any; + credentials?: RequestCredentials; +} + +interface UseFetchResult { + data: T | null; + loading: boolean; + error: Error | null; + execute: (body?: any) => Promise; +} + +function useFetch( + url: string, + options: UseFetchOptions = {}, +): UseFetchResult { + const { method = 'GET', headers, body, trigger, credentials } = options; + + const [data, setData] = useState(null); + const [loading, setLoading] = useState(method === 'GET'); + const [error, setError] = useState(null); + + const execute = async (executeBody?: any) => { + setLoading(true); + setError(null); + + try { + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + body: executeBody + ? JSON.stringify(executeBody, undefinedReplacer) + : body + ? JSON.stringify(body, undefinedReplacer) + : null, + credentials: credentials ?? 'include', + }); + + if (!response.ok) { + throw new Error(`Request Errors: ${response.status}`); + } + + const result: T = await response.json(); + setData(result); + return result; + } catch (err: any) { + setError(err); + return null; + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (method === 'GET') { + execute(); + } + }, [url, method, trigger]); + + return { data, loading, error, execute }; +} + +export default useFetch; diff --git a/apps/client/src/hooks/useGraph.ts b/apps/client/src/hooks/useGraph.ts index a0a93582..3d19a8c7 100644 --- a/apps/client/src/hooks/useGraph.ts +++ b/apps/client/src/hooks/useGraph.ts @@ -1,5 +1,7 @@ +import { GRID_2D_SIZE } from '@constants'; import { useDimensionContext } from '@contexts/DimensionContext'; import { useEdgeContext } from '@contexts/EdgeContext'; +import { useGraphContext } from '@contexts/GraphConetxt'; import { useGroupContext } from '@contexts/GroupContext'; import { useNodeContext } from '@contexts/NodeContext'; import { useSvgContext } from '@contexts/SvgContext'; @@ -12,10 +14,11 @@ import { computeBounds } from '@helpers/group'; import { adjustNodePointForDimension, alignNodePoint, - getNodeBounds, + calculateNodeBoundingBox, } from '@helpers/node'; +import { calcViewBoxBounds } from '@helpers/viewBox'; import useSelection from '@hooks/useSelection'; -import { Connection, Edge, Group, Node, Point } from '@types'; +import { Connection, Dimension, Edge, Group, Node, Point } from '@types'; import { alignPoint2d, alignPoint3d, @@ -40,8 +43,13 @@ export default () => { dispatch: groupDispatch, } = useGroupContext(); + const { + state: { viewBox }, + dispatch: graphDispatch, + } = useGraphContext(); + const { clearSelection } = useSelection(); - const { dimension, prevDimension } = useDimensionContext(); + const { dimension } = useDimensionContext(); const { svgRef } = useSvgContext(); //INFO: Node @@ -118,27 +126,73 @@ export default () => { clearSelection(); }; - const updateNodePointForDimension = () => { - const updatedNodes = Object.entries(nodes).reduce((acc, [id, node]) => { - const adjustedPoint = adjustNodePointForDimension(node, dimension); - const connectors = getConnectorPoints( - { ...node, point: adjustedPoint }, - dimension, + //TODO: Refactoringν•„μš” + const updateNodePointForDimension = (dimension: Dimension) => { + //INFO: update node + const updatedNodes: Record = Object.entries(nodes).reduce( + (acc, [id, node]) => { + const adjustedPoint = adjustNodePointForDimension( + node.point, + node.size['3d'], + dimension, + ); + + const connectors = getConnectorPoints( + { ...node, point: adjustedPoint }, + dimension, + ); + + return { + ...acc, + [id]: { + ...node, + point: adjustedPoint, + connectors, + }, + }; + }, + {}, + ); + + //INFO:update edge + let updatedEdges = Object.entries(edges).reduce((acc, [id, edge]) => { + const adjustedBendingPoints = edge.bendingPoints.map((point) => + dimension === '2d' + ? convert3dTo2dPoint(point) + : convert2dTo3dPoint(point), ); + return { ...acc, [id]: { - ...node, - point: adjustedPoint, - connectors, + ...edge, + bendingPoints: adjustedBendingPoints, }, }; }, {}); + //INFO: update ViewBox + if (Object.keys(updatedNodes).length === 0 || !svgRef.current) return; + const updatedViewBox = calcViewBoxBounds( + updatedNodes, + viewBox, + dimension, + ); + + graphDispatch({ + type: 'SET_VIEWBOX', + payload: updatedViewBox, + }); + nodeDispatch({ type: 'UPDATE_NODES', payload: updatedNodes, }); + + edgeDispatch({ + type: 'UPDATE_EDGES', + payload: updatedEdges, + }); }; //INFO: Edge @@ -256,29 +310,6 @@ export default () => { }); }; - const updateEdgePointForDimension = () => { - const updatedEdges = Object.entries(edges).reduce((acc, [id, edge]) => { - const adjustedBendingPoints = edge.bendingPoints.map((point) => - dimension === '2d' - ? convert3dTo2dPoint(point) - : convert2dTo3dPoint(point), - ); - - return { - ...acc, - [id]: { - ...edge, - bendingPoints: adjustedBendingPoints, - }, - }; - }, {}); - - edgeDispatch({ - type: 'UPDATE_EDGES', - payload: updatedEdges, - }); - }; - //INFO: Group const addGroup = (group: Group) => { @@ -374,36 +405,44 @@ export default () => { const recursiveGroupBounds = (group: Group): any => { if (group.childGroupIds.length === 0) { - const nodesBounds = group.nodeIds.map((nodeId) => - getNodeBounds(nodes[nodeId], dimension), - ); + const innerNodes = group.nodeIds.map((nodeId) => nodes[nodeId]); - return computeBounds(nodesBounds, dimension); + return computeBounds(innerNodes, dimension, 2); } - const childGroupsBounds = group.childGroupIds.map((childGroupId) => - recursiveGroupBounds(groups[childGroupId]), - ); - - const currentNodesBounds = group.nodeIds.map((nodeId) => - getNodeBounds(nodes[nodeId], dimension), - ); + const childGroups = group.childGroupIds.map((childGroupId) => { + const bounds = recursiveGroupBounds(groups[childGroupId]); + //TODO: Group μƒμ„±μ‹œ bounds λ„£μ–΄μ€˜μ•Όλ  것 κ°™μŒ + return { + point: { + x: bounds.x, + y: bounds.y, + }, + size: { + '2d': { + width: bounds.width, + height: bounds.height, + }, + '3d': { + width: 0, + height: 0, + }, + }, + }; + }) as Node[]; + + const currentNodes = group.nodeIds.map((nodeId) => nodes[nodeId]); return computeBounds( - [...currentNodesBounds, ...childGroupsBounds], + [...currentNodes, ...childGroups], dimension, + 2, ); }; return recursiveGroupBounds(group); }; - const updatedPointForDimension = () => { - updateNodePointForDimension(); - updateEdgePointForDimension(); - }; - return { - prevDimension, dimension, svgRef, nodes, @@ -415,6 +454,7 @@ export default () => { addEdge, removeEdge, splitEdge, + updateNodePointForDimension, moveBendingPointer, addGroup, addChildGroup, @@ -426,7 +466,6 @@ export default () => { moveGroup, removeGroup, removeNodeFromGroup, - updatedPointForDimension, excludeNodeFromGroup, }; }; diff --git a/apps/client/src/hooks/useNCloud.ts b/apps/client/src/hooks/useNCloud.ts index b8106742..093e8b57 100644 --- a/apps/client/src/hooks/useNCloud.ts +++ b/apps/client/src/hooks/useNCloud.ts @@ -1,33 +1,28 @@ -import { - NcloudGroupFactory, - NcloudNodeFactory, - Regions, -} from '@/src/models/ncloud'; -import { NETWORKS_CATEGORIES } from '@constants'; -import { useNCloudContext } from '@contexts/NCloudContext'; -import { getInitPoint } from '@helpers/cloud'; +import { NcloudGroupFactory, NcloudNodeFactory } from '@/src/models/ncloud'; +import { DEFAULT_REGION, REGIONS } from '@/src/models/ncloud/constants'; +import { useDimensionContext } from '@contexts/DimensionContext'; +import { useGraphContext } from '@contexts/GraphConetxt'; import useGraph from '@hooks/useGraph'; import useSelection from '@hooks/useSelection'; -import { Node, Region } from '@types'; +import { Region } from '@types'; +import { findKeyByValue } from '@utils'; import { nanoid } from 'nanoid'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; export default () => { const { selectedNodeId, selectedGroupId } = useSelection(); - const { - region, - vpc, - selectedResource, - vpcList, - subnet, - subnetList, - setRegion, - setVpc, - setSubnet, - setSubnetList, - setVpcList, - setSelectedResource, - } = useNCloudContext(); + const [selectedResource, setSelectedResource] = useState< + | { + id: string; + type: string; + properties: { [key: string]: any }; + } + | undefined + >(undefined); + + const [vpcList, setVpcList] = useState<{ [id: string]: string }>({}); + const [subnetList, setSubnetList] = useState<{ [id: string]: string }>({}); + const currentRegion = useRef(DEFAULT_REGION); const { nodes, @@ -43,197 +38,224 @@ export default () => { isExistGroup, } = useGraph(); + const { + state: { viewBox }, + } = useGraphContext(); + const { dimension } = useDimensionContext(); + useEffect(() => { - if (selectedNodeId) { - const node = nodes[selectedNodeId]; - if (!node) return; - setRegion(node.properties.region); - setVpc(node.properties.vpc); - setSubnet(node.properties.subnet); - setSelectedResource({ - id: selectedNodeId, - type: node.type, - properties: node.properties, - }); - } else { + if (!selectedNodeId || !nodes[selectedNodeId]) { setSelectedResource(undefined); + return; } + + const node = nodes[selectedNodeId]; + + setSelectedResource({ + id: selectedNodeId, + type: node.type, + properties: node.properties, + }); }, [selectedNodeId, nodes]); useEffect(() => { - const regionGroup = groups[region]; + if (!selectedNodeId) return; + const node = nodes[selectedNodeId]; + const regionGroup = groups[node.properties.region?.id]; if (!regionGroup) return; setVpcList({ ...Object.fromEntries( - regionGroup.childGroupIds.map((id) => [id, groups[id].name]), + regionGroup.childGroupIds.map((id) => [ + id, + groups[id].properties.name, + ]), ), }); - const vpcGroup = groups[vpc]; + const vpcGroup = groups[node.properties.vpc?.id]; if (!vpcGroup) return; setSubnetList({ ...Object.fromEntries( - vpcGroup.childGroupIds.map((id) => [id, groups[id].name]), + vpcGroup.childGroupIds.map((id) => [ + id, + groups[id].properties.name, + ]), ), }); - }, [region, groups]); + }, [selectedNodeId, nodes]); + + useEffect(() => { + currentRegion.current = + selectedResource?.properties.region?.value ?? currentRegion.current; + }, [selectedResource]); + + const findRegionGroup = (region: string) => { + const prefixId = (id: string) => id.split('-')[0].toLowerCase(); + return Object.values(groups).find( + (group) => prefixId(group.id) === region.toLowerCase(), + ); + }; - const addResource = (type: string) => { + const createResource = (type: string) => { if (!svgRef.current) return; const node = NcloudNodeFactory(type); const id = `node-${nanoid()}`; + const region = REGIONS[currentRegion.current]; + const regionGroup = findRegionGroup(region.value); + const regionId = regionGroup ? regionGroup.id : region.id; + const regionValue = regionGroup + ? regionGroup.properties.value + : region.value; + addNode({ ...node, id, properties: { ...node.properties, - region, + region: { + id: regionId, + value: regionValue, + }, + }, + point: { + x: viewBox.x + 300, + y: viewBox.y + viewBox.height / 2, }, - point: getInitPoint(svgRef.current!), }); - if (!isExistGroup(region)) { - createRegion(region, id); - } else { - addNodeToGroup(region, id); + if (!regionGroup) { + createRegion(regionId, regionValue); } + addNodeToGroup(regionId, id); }; - const createRegion = (region: string, nodeId?: string) => { + const createRegion = (id: string, region: string) => { addGroup({ ...NcloudGroupFactory('region'), - id: region, - name: Regions[region].toUpperCase(), - nodeIds: nodeId ? [nodeId] : [], - }); - }; - - const createVpc = (vpc: string, nodeId?: string) => { - addGroup({ - ...NcloudGroupFactory('vpc'), - id: vpc, - name: vpc, - nodeIds: nodeId ? [nodeId] : [], + id, + nodeIds: [], + properties: { + name: REGIONS[region].label, + value: region, + }, }); }; - const removeNodeRelatedGroup = (node: Node, groupCategories: string[]) => { - const properties = node.properties; - const relatedGroupIds = groupCategories - .map((type) => properties[type]) - .filter(Boolean); - - relatedGroupIds.forEach((groupId) => - removeNodeFromGroup(groupId, node.id), - ); - }; + const changeRegion = (id: string, newRegion: Region) => { + if (!selectedNodeId) return; + const regionGroup = findRegionGroup(newRegion); - const updateRegion = (newRegion: Region) => { - if (selectedNodeId && region !== newRegion) { - if (isExistGroup(newRegion)) { - addNodeToGroup(newRegion, selectedNodeId); - } else { - createRegion(newRegion, selectedNodeId); - } + if (!regionGroup) { + createRegion(id, newRegion); + } - removeNodeRelatedGroup(nodes[selectedNodeId], NETWORKS_CATEGORIES); + const regionId = regionGroup ? regionGroup.id : id; - const updatedProperties = NETWORKS_CATEGORIES.reduce((acc, cur) => { - return { - ...acc, - [cur]: '', - }; - }, {}); + addNodeToGroup(regionId, selectedNodeId); - updateProperties(selectedNodeId, { - ...updatedProperties, - region: newRegion, - }); + const { properties } = nodes[selectedNodeId]; + if (properties.region) { + const prevRegionId = findRegionGroup(properties.region.value)?.id; + removeNodeFromGroup(prevRegionId!, selectedNodeId); } + + updateProperties(selectedNodeId, { + region: { + id: regionId, + value: REGIONS[newRegion].value, + }, + }); }; - const updateVpc = (newVpc: string) => { - if (selectedNodeId && vpc !== newVpc) { - if (!isExistGroup(newVpc)) { - createVpc(newVpc); - } + const createVpc = (id: string, newVpc: string) => { + addGroup({ + ...NcloudGroupFactory('vpc'), + id, + nodeIds: [], + properties: { + name: newVpc, + }, + }); + }; - addNodeToGroup(newVpc, selectedNodeId); - addChildGroup(newVpc, region, selectedNodeId); - const node = nodes[selectedNodeId]; - removeNodeRelatedGroup(node, NETWORKS_CATEGORIES); + const changeVpc = (id: string, newVpc: string) => { + if (!selectedNodeId) return; + const node = nodes[selectedNodeId]; + const prevVpcId = findKeyByValue(newVpc, vpcList); + if (!prevVpcId) { + createVpc(id, newVpc); + } - const updatedProperties = NETWORKS_CATEGORIES.reduce((acc, cur) => { - return { - ...acc, - [cur]: '', - }; - }, {}); + const idToUpdate = prevVpcId ?? id; + const { properties } = node; + addNodeToGroup(idToUpdate, selectedNodeId); + addChildGroup(idToUpdate, properties.region.id, selectedNodeId); - updateProperties(selectedNodeId, { - ...updatedProperties, - region, - vpc: newVpc, - }); + if (properties.vpc) { + removeNodeFromGroup(properties.vpc.id, selectedNodeId); } + + updateProperties(selectedNodeId, { + vpc: { + id: idToUpdate, + value: newVpc, + }, + }); }; - const createSubnet = (subnet: string, nodeId?: string) => { + const createSubnet = (id: string, newSubnet: string) => { addGroup({ ...NcloudGroupFactory('subnet'), - id: subnet, - name: subnet, - nodeIds: nodeId ? [nodeId] : [], + id, + nodeIds: [], + properties: { + name: newSubnet, + }, }); }; - const updateSubnet = (newSubnet: string) => { - if (selectedNodeId && subnet !== newSubnet) { - if (!isExistGroup(newSubnet)) { - createSubnet(newSubnet); - } - - addNodeToGroup(newSubnet, selectedNodeId); - if (vpc || region) { - addChildGroup(newSubnet, vpc || region, selectedNodeId); - } - const node = nodes[selectedNodeId]; - removeNodeRelatedGroup(node, NETWORKS_CATEGORIES); - - const updatedProperties = NETWORKS_CATEGORIES.reduce((acc, cur) => { - return { - ...acc, - [cur]: '', - }; - }, {}); + const changeSubnet = (id: string, newSubnet: string) => { + if (!selectedNodeId) return; + const node = nodes[selectedNodeId]; + const prevSubnetId = findKeyByValue(newSubnet, subnetList); + if (!prevSubnetId) { + createSubnet(id, newSubnet); + } - updateProperties(selectedNodeId, { - ...updatedProperties, - region, - vpc, - subnet: newSubnet, - }); + const idToUpdate = prevSubnetId ?? id; + const { properties } = node; + addNodeToGroup(idToUpdate, selectedNodeId); + addChildGroup(idToUpdate, properties.vpc.id, selectedNodeId); + + if (properties.subnet) { + removeNodeFromGroup(properties.subnet.id, selectedNodeId); } + + updateProperties(selectedNodeId, { + subnet: { + id: idToUpdate, + value: newSubnet, + }, + }); }; - const removeVpc = (vpc: string) => { - if (selectedNodeId) { - excludeNodeFromGroup(vpc, selectedNodeId); - updateProperties(selectedNodeId, { - ...nodes[selectedNodeId].properties, - vpc: '', - }); - } + const removeVpc = (vpcId: string) => { + if (!selectedNodeId) return; + excludeNodeFromGroup(vpcId, selectedNodeId); + updateProperties(selectedNodeId, { + ...nodes[selectedNodeId].properties, + vpc: undefined, + }); }; - const removeSubnet = (subnet: string) => { + const removeSubnet = (subnetId: string) => { if (selectedNodeId) { - excludeNodeFromGroup(subnet, selectedNodeId); + excludeNodeFromGroup(subnetId, selectedNodeId); updateProperties(selectedNodeId, { ...nodes[selectedNodeId].properties, - subnet: '', + subnet: undefined, }); } }; @@ -248,19 +270,17 @@ export default () => { }; return { - region, - vpc, - vpcList, - subnet, subnetList, + vpcList, selectedResource, - updateVpc, - addResource, + createResource, createRegion, + createVpc, + changeVpc, + changeSubnet, + updateProperties, removeVpc, removeSubnet, - updateRegion, - createSubnet, - updateSubnet, + changeRegion, }; }; diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index 5e0f537f..bffa8646 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -1,38 +1,37 @@ -import { DimensionProvider } from '@contexts/DimensionContext'; -import { EdgeProvider } from '@contexts/EdgeContext/index.tsx'; -import { GraphProvider } from '@contexts/GraphConetxt'; -import { GroupProvider } from '@contexts/GroupContext/index.tsx'; -import { NCloudProvider } from '@contexts/NCloudContext.tsx'; -import { NodeProvider } from '@contexts/NodeContext/index.tsx'; -import { SelectionProvider } from '@contexts/SelectionContext/index.tsx'; -import { SvgProvider } from '@contexts/SvgContext.tsx'; import { CssBaseline, ThemeProvider } from '@mui/material'; import theme from '@theme'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { App } from './App.tsx'; +import Root from './Root.tsx'; +import { rootLoader } from './apis/loaders.ts'; +const router = createBrowserRouter([ + { + element: , + loader: rootLoader, + children: [ + { + path: '/', + element: , + }, + { + path: ':id', + element: , + }, + { + path: '*', + element:
Not Found
, + }, + ], + }, +]); createRoot(document.getElementById('root')!).render( - - - - - - - - - - - - - - - - - + , ); diff --git a/apps/client/src/models/ncloud/CloudFunction.ts b/apps/client/src/models/ncloud/CloudFunction.ts new file mode 100644 index 00000000..55b0a4bd --- /dev/null +++ b/apps/client/src/models/ncloud/CloudFunction.ts @@ -0,0 +1,21 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; + +export interface CloudFunctionProp extends NetworksProp { + //TODO: +} + +export const CloudFunctionNode: Node & { + properties: CloudFunctionProp; +} = { + ...GraphNode, + type: 'cloud-function', + size: { + '2d': { width: 90, height: 90 }, + '3d': { width: 96, height: 113.438, offset: 12.5 }, + }, + properties: { + ...Networks, + }, +}; diff --git a/apps/client/src/models/ncloud/ContainerRegistry.ts b/apps/client/src/models/ncloud/ContainerRegistry.ts new file mode 100644 index 00000000..34ec696b --- /dev/null +++ b/apps/client/src/models/ncloud/ContainerRegistry.ts @@ -0,0 +1,23 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; + +export interface ContainerRegistryProp extends NetworksProp { + //TODO: +} + +export const ContainerRegistryNode: Node & { + properties: ContainerRegistryProp; +} = { + ...GraphNode, + type: 'container-registry', + size: { + '2d': { width: 360, height: 360 }, + '3d': { width: 512, height: 333, offset: 0 }, + }, + properties: { + ...Networks, + }, +}; + +export const ContainerRegistryRequiredFields = {}; diff --git a/apps/client/src/models/ncloud/LoadBalancer.ts b/apps/client/src/models/ncloud/LoadBalancer.ts new file mode 100644 index 00000000..dfad49c3 --- /dev/null +++ b/apps/client/src/models/ncloud/LoadBalancer.ts @@ -0,0 +1,32 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; + +export interface LoadBalancerProp extends NetworksProp { + name: string | null; + networkType: string | null; + subnetNoList: [] | null; +} + +export const LoadBalancerNode: Node & { + properties: LoadBalancerProp; +} = { + ...GraphNode, + type: 'load-balancer', + size: { + '2d': { width: 90, height: 90 }, + '3d': { width: 97, height: 94, offset: 10 }, + }, + properties: { + ...Networks, + name: null, + networkType: null, + subnetNoList: null, + }, +}; + +export const LoadBalancerRequiredFields = { + name: true, + networkType: true, + subnetNoList: true, +}; diff --git a/apps/client/src/models/ncloud/MySQLDB.ts b/apps/client/src/models/ncloud/MySQLDB.ts new file mode 100644 index 00000000..3c7577b8 --- /dev/null +++ b/apps/client/src/models/ncloud/MySQLDB.ts @@ -0,0 +1,97 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; +import validator from 'validator'; + +export type MYSQLDBProp = { + serverName: string | null; + serverNamePrefix: string | null; + userName: string | null; + userPassword: string | null; + hostIp: string | null; + databaseName: string | null; + serviceName: string | null; +}; + +export const MySQLDBNode: Node & { + properties: MYSQLDBProp & NetworksProp; +} = { + ...GraphNode, + type: 'db-mysql', + size: { + '2d': { width: 90, height: 90 }, + '3d': { width: 128, height: 137.5, offset: 0 }, + }, + properties: { + ...Networks, + serverName: null, + serverNamePrefix: null, + userName: null, + userPassword: null, + hostIp: null, + databaseName: null, + serviceName: null, + }, +}; + +export const MySQLDBRequiredFields = { + serviceName: true, + serverName: true, + serverNamePrefix: true, + userName: true, + userPassword: true, + hostIp: true, + databaseName: true, + vpc: true, + subnet: true, + region: true, +}; + +export type ValidationErrors = Partial>; + +export const validateMySQLDB = ( + json: Partial, +): ValidationErrors => { + const errors: ValidationErrors = {}; + + if ( + !json.serviceName || + !validator.isLength(json.serviceName, { min: 3, max: 30 }) + ) { + errors.serviceName = 'Service Name 3-30 characters'; + } + + if ( + !json.serverNamePrefix || + !validator.isLength(json.serverNamePrefix, { min: 3, max: 20 }) + ) { + errors.serverNamePrefix = 'Server Name Prefix 3-20 characters'; + } + + if ( + !json.userName || + !validator.isLength(json.userName, { min: 4, max: 16 }) + ) { + errors.userName = 'User Name 4-16 characters'; + } + + if ( + !json.userPassword || + !validator.isLength(json.userPassword, { min: 8, max: 20 }) + ) { + errors.userPassword = 'User Password 8-20 characters'; + } + + if (!json.hostIp || !validator.isIP(json.hostIp, 4)) { + errors.hostIp = 'Host IP must be a valid IPv4 address'; + } + + if ( + !json.databaseName || + !validator.isLength(json.databaseName, { min: 1, max: 30 }) + ) { + errors.databaseName = 'Database Name 1-30 characters'; + } + + return errors; +}; diff --git a/apps/client/src/models/ncloud/NatGateway.ts b/apps/client/src/models/ncloud/NatGateway.ts new file mode 100644 index 00000000..c6f3ac6c --- /dev/null +++ b/apps/client/src/models/ncloud/NatGateway.ts @@ -0,0 +1,21 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; + +export interface NatGatewayProp extends NetworksProp { + //TODO: +} + +export const NatGatewayNode: Node = { + ...GraphNode, + type: 'nat-gateway', + size: { + '2d': { width: 90, height: 90 }, + '3d': { width: 123, height: 108.05, offset: 0 }, + }, + properties: { + ...Networks, + }, +}; + +export const NatGatewayRequiredFields = {}; diff --git a/apps/client/src/models/ncloud/Networks.ts b/apps/client/src/models/ncloud/Networks.ts new file mode 100644 index 00000000..cc0b4450 --- /dev/null +++ b/apps/client/src/models/ncloud/Networks.ts @@ -0,0 +1,51 @@ +import { GraphGroup } from '@helpers/group'; +import { Group } from '@types'; + +export type NetworksProp = { + region: { key: string; value: string } | null; + subnet: { key: string; value: string } | null; + vpc: { key: string; value: string } | null; +}; + +export const Networks: NetworksProp = { + region: null, + subnet: null, + vpc: null, +}; + +export const NetworksRequiredFields = { + region: true, + subnet: true, + vpc: true, +}; + +export const RegionGroup: Group = { + ...GraphGroup, + type: 'region', + properties: { + name: '', + }, +}; + +export const VpcGroup: Group = { + ...GraphGroup, + type: 'vpc', + properties: { + name: '', + }, +}; + +export const SubnetGroup: Group = { + ...GraphGroup, + type: 'subnet', + properties: { + name: '', + }, +}; + +export const NETWORKS_CATEGORIES = [ + 'region', + 'vpc', + 'subnet', + // 'security-group', +]; diff --git a/apps/client/src/models/ncloud/ObjectStorage.ts b/apps/client/src/models/ncloud/ObjectStorage.ts new file mode 100644 index 00000000..8a51762f --- /dev/null +++ b/apps/client/src/models/ncloud/ObjectStorage.ts @@ -0,0 +1,29 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; + +export interface ObjectStorageProp extends NetworksProp { + bucketName: string | null; +} + +export const ObjectStorageNode: Node & { + properties: ObjectStorageProp; +} = { + ...GraphNode, + type: 'object-storage', + size: { + '2d': { width: 90, height: 90 }, + '3d': { width: 100.626, height: 115.695, offset: 20 }, + }, + properties: { + ...Networks, + bucketName: null, + }, +}; + +export const ObjectStorageRequiredFields = { + bucketName: true, + vpc: true, + subnet: true, + region: true, +}; diff --git a/apps/client/src/models/ncloud/Server.ts b/apps/client/src/models/ncloud/Server.ts new file mode 100644 index 00000000..6438dc21 --- /dev/null +++ b/apps/client/src/models/ncloud/Server.ts @@ -0,0 +1,35 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; + +export interface ServerProp extends NetworksProp { + name: string | null; + server_image_number: string | null; + server_spec_code: string | null; +} + +export const ServerNode: Node & { + properties: ServerProp; +} = { + ...GraphNode, + type: 'server', + size: { + '2d': { width: 90, height: 90 }, + '3d': { width: 128, height: 111, offset: 0 }, + }, + properties: { + ...Networks, + name: null, + server_image_number: null, + server_spec_code: null, + }, +}; + +export const ServerRequiredFields = { + name: true, + server_image_number: true, + server_spec_code: true, + vpc: true, + subnet: true, + region: true, +}; diff --git a/apps/client/src/models/ncloud/constants.ts b/apps/client/src/models/ncloud/constants.ts new file mode 100644 index 00000000..91e4f0a0 --- /dev/null +++ b/apps/client/src/models/ncloud/constants.ts @@ -0,0 +1,75 @@ +import { nanoid } from 'nanoid'; + +export const SERVER_OS_IMAGES = [ + { code: '19463675', name: 'rokcy-8.8-base', hyperVisor: 'KVM' }, + { code: '25624115', name: 'rocky-8.10-base', hyperVisor: 'XEN' }, + { code: '23221307', name: 'win-2022-en', hyperVisor: 'KVM' }, + { code: '23214590', name: 'ubuntu-22.04', hyperVisor: 'KVM' }, +]; + +export const SERVER_IMAGE_SPEC_CODE: { + [key: string]: { code: string; info: string }[]; +} = { + '25624115': [ + { code: 'c2-g2-s50', info: 'vCPU 2EA, Memory 4GB, [SSD]Disk 50GB' }, + { code: 'c4-g2-s50', info: 'vCPU 4EA, Memory 8GB, [SSD]Disk 50GB' }, + { code: 'c8-g2-s50', info: 'vCPU 8EA, Memory 16GB, [SSD]Disk 50GB' }, + { code: 'c16-g2-s50', info: 'vCPU 16EA, Memory 32GB, [SSD]Disk 50GB' }, + { code: 'c32-g2-s50', info: 'vCPU 32EA, Memory 64GB, [SSD]Disk 50GB' }, + { code: 'c2-g2-h50', info: 'vCPU 2EA, Memory 4GB, Disk 50GB' }, + { code: 'c4-g2-h50', info: 'vCPU 4EA, Memory 8GB, Disk 50GB' }, + { code: 'c8-g2-h50', info: 'vCPU 8EA, Memory 16GB, Disk 50GB' }, + { code: 'c16-g2-h50', info: 'vCPU 16EA, Memory 32GB, Disk 50GB' }, + { code: 'c32-g2-h50', info: 'vCPU 32EA, Memory 64GB, Disk 50GB' }, + ], + '19463675': [ + { code: 'ci2-g3', info: 'vCPU 2EA, Memory 4GB' }, + { code: 'ci4-g3', info: 'vCPU 4EA, Memory 8GB' }, + { code: 'ci8-g3', info: 'vCPU 8EA, Memory 16GB' }, + { code: 'ci16-g3', info: 'vCPU 16EA, Memory 32GB' }, + { code: 'ci32-g3', info: 'vCPU 32EA, Memory 64GB' }, + { code: 'ci48-g3', info: 'vCPU 48EA, Memory 96GB' }, + { code: 'ci64-g3', info: 'vCPU 64EA, Memory 128GB' }, + ], + '23221307': [ + { code: 'ci2-g3', info: 'vCPU 2EA, Memory 4GB' }, + { code: 'ci4-g3', info: 'vCPU 4EA, Memory 8GB' }, + { code: 'ci8-g3', info: 'vCPU 8EA, Memory 16GB' }, + { code: 'ci16-g3', info: 'vCPU 16EA, Memory 32GB' }, + { code: 'ci32-g3', info: 'vCPU 32EA, Memory 64GB' }, + { code: 'ci48-g3', info: 'vCPU 48EA, Memory 96GB' }, + { code: 'ci64-g3', info: 'vCPU 64EA, Memory 128GB' }, + ], + '23214590': [ + { code: 'ci2-g3', info: 'vCPU 2EA, Memory 4GB' }, + { code: 'ci4-g3', info: 'vCPU 4EA, Memory 8GB' }, + { code: 'ci8-g3', info: 'vCPU 8EA, Memory 16GB' }, + { code: 'ci16-g3', info: 'vCPU 16EA, Memory 32GB' }, + { code: 'ci32-g3', info: 'vCPU 32EA, Memory 64GB' }, + { code: 'ci48-g3', info: 'vCPU 48EA, Memory 96GB' }, + { code: 'ci64-g3', info: 'vCPU 64EA, Memory 128GB' }, + ], +}; + +const krRegionId = `kr-region-unique`; +const jpRegionId = `jp-region-unique`; +const sgRegionId = `sg-region-unique`; +export const REGIONS: { [key: string]: any } = { + KR: { + id: krRegionId, + value: 'KR', + label: 'Korea', + }, + JP: { + id: jpRegionId, + value: 'JP', + label: 'Japan', + }, + SG: { + id: sgRegionId, + value: 'SG', + label: 'Singapore', + }, +}; + +export const DEFAULT_REGION = 'KR'; diff --git a/apps/client/src/models/ncloud/index.ts b/apps/client/src/models/ncloud/index.ts index f808e498..5c046092 100644 --- a/apps/client/src/models/ncloud/index.ts +++ b/apps/client/src/models/ncloud/index.ts @@ -1,13 +1,31 @@ -import { Group, Node } from '@types'; +import { CloudFunctionNode } from './CloudFunction'; +import { ContainerRegistryNode } from './ContainerRegistry'; +import { LoadBalancerNode } from './LoadBalancer'; +import { MySQLDBNode, MySQLDBRequiredFields } from './MySQLDB'; +import { NatGatewayNode } from './NatGateway'; +import { RegionGroup, SubnetGroup, VpcGroup } from './Networks'; +import { + ObjectStorageNode, + ObjectStorageRequiredFields, +} from './ObjectStorage'; +import { ServerNode, ServerRequiredFields } from './Server'; export const NcloudNodeFactory = (type: string) => { switch (type) { case 'server': - return Server; + return ServerNode; case 'cloud-function': - return CloudFunction; + return CloudFunctionNode; case 'db-mysql': - return MySQLDB; + return MySQLDBNode; + case 'load-balancer': + return LoadBalancerNode; + case 'container-registry': + return ContainerRegistryNode; + case 'object-storage': + return ObjectStorageNode; + case 'nat-gateway': + return NatGatewayNode; default: { throw new Error(`Unknown type: ${type}`); } @@ -17,11 +35,11 @@ export const NcloudNodeFactory = (type: string) => { export const NcloudGroupFactory = (type: string) => { switch (type) { case 'region': - return Region; + return RegionGroup; case 'vpc': - return Vpc; + return VpcGroup; case 'subnet': - return Subnet; + return SubnetGroup; default: { throw new Error(`Unknown type: ${type}`); @@ -29,86 +47,15 @@ export const NcloudGroupFactory = (type: string) => { } }; -const GraphNodeProperties = { - id: '', - name: '', - type: '', - point: { x: 0, y: 0 }, - connectors: {}, -}; - -const GraphGroupProperties = { - id: '', - name: '', - type: '', - nodeIds: [], - properties: {}, - childGroupIds: [], - parentGroupId: '', -}; - -const Server: Node = { - ...GraphNodeProperties, - type: 'server', - size: { - '2d': { width: 90, height: 90 }, - '3d': { width: 128, height: 111 }, - }, - properties: { - region: '', - subnet: '', - vpc: '', - acg: '', - server_image_product_code: '', - server_product_code: '', - }, -}; - -const CloudFunction: Node = { - ...GraphNodeProperties, - type: 'cloud-function', - size: { - '2d': { width: 90, height: 90 }, - '3d': { width: 96, height: 113.438, offset: 10 }, - }, - properties: { - region: '', - vpc: '', - subnet: '', - }, -}; - -const MySQLDB: Node = { - ...GraphNodeProperties, - type: 'db-mysql', - size: { - '2d': { width: 90, height: 90 }, - '3d': { width: 128, height: 137.5 }, - }, - properties: { - region: '', - vpc: '', - subnet: '', - }, -}; - -const Region: Group = { - ...GraphGroupProperties, - type: 'region', -}; - -const Vpc: Group = { - ...GraphGroupProperties, - type: 'vpc', -}; - -const Subnet: Group = { - ...GraphGroupProperties, - type: 'subnet', -}; - -export const Regions: { [key: string]: string } = { - kr: 'korea', - jp: 'japan', - sg: 'singapore', +export const getPropertyFilters = (type: string) => { + switch (type) { + case 'server': + return ServerRequiredFields; + case 'object-storage': + return ObjectStorageRequiredFields; + case 'db-mysql': + return MySQLDBRequiredFields; + default: + return {}; + } }; diff --git a/apps/client/src/models/ncloud/utils.ts b/apps/client/src/models/ncloud/utils.ts new file mode 100644 index 00000000..4cac3cb7 --- /dev/null +++ b/apps/client/src/models/ncloud/utils.ts @@ -0,0 +1,31 @@ +export const transformObject = (obj: any) => { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => { + if ( + typeof value === 'object' && + value !== null && + 'value' in value + ) { + return [key, value.value]; + } + return [key, value]; + }), + ); +}; + +export const validateObject = ( + obj: any, + requiredFields: Record, +) => { + return Object.entries(requiredFields).every(([key, isRequired]) => { + if (!isRequired) { + return true; + } + + const value = obj[key]; + if (typeof value === 'object' && value !== null && 'value' in value) { + return Boolean(value.value); + } + return Boolean(value); + }); +}; diff --git a/apps/client/src/theme/index.ts b/apps/client/src/theme/index.ts index 5ef6d032..2d48a4ee 100644 --- a/apps/client/src/theme/index.ts +++ b/apps/client/src/theme/index.ts @@ -63,6 +63,7 @@ const theme = createTheme({ html: { height: '100%', width: '100%', + overflow: 'hidden', }, body: { height: '100%', diff --git a/apps/client/src/types/index.ts b/apps/client/src/types/index.ts index 3988f6f3..84592070 100644 --- a/apps/client/src/types/index.ts +++ b/apps/client/src/types/index.ts @@ -4,7 +4,14 @@ export type Point = { x: number; y: number }; export type GridPoint = { col: number; row: number }; -export type Size = { width: number; height: number; offset?: number }; +export type Size = { + width: number; + height: number; +}; + +export type Size3D = { + offset: number; +} & Size; export type ViewBox = Point & Size; @@ -13,13 +20,12 @@ export type Bounds = Point & Size; export type Node = { id: string; type: string; - name: string; point: Point; size: { '2d': Size; - '3d': Size; + '3d': Size3D; }; - properties: { [key: string]: any }; + properties: { [id: string]: any }; connectors: { [key: string]: Point }; }; @@ -40,9 +46,8 @@ export type Edge = { export type Group = { id: string; type: string; - name: string; nodeIds: string[]; - properties: { [key: string]: any }; + properties: { [id: string]: any }; childGroupIds: string[]; parentGroupId: string; }; diff --git a/apps/client/src/utils/index.ts b/apps/client/src/utils/index.ts index d894f785..2cc4ee36 100644 --- a/apps/client/src/utils/index.ts +++ b/apps/client/src/utils/index.ts @@ -3,7 +3,16 @@ import { GRID_3D_HEIGHT_SIZE, GRID_3D_WIDTH_SIZE, } from '@constants'; -import { ConnectorMap, Dimension, GridPoint, Node, Point } from '@types'; +import { + Bounds, + ConnectorMap, + Dimension, + GridPoint, + Node, + Point, + Size, + Size3D, +} from '@types'; export const getDistance = (point1: Point, point2: Point) => { return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2); @@ -17,6 +26,16 @@ export const getSvgPoint = (svg: SVGSVGElement, point: Point) => { return svgPoint.matrixTransform(screenCTM!.inverse()); }; +export const getScreenPoint = (svg: SVGSVGElement, point: Point): DOMPoint => { + const svgPoint = svg.createSVGPoint(); + svgPoint.x = point.x; + svgPoint.y = point.y; + + const screenCTM = svg.getScreenCTM(); + + return svgPoint.matrixTransform(screenCTM!); +}; + export const gridToScreen3d = (gridPoint: GridPoint): Point => { const { col, row } = gridPoint; @@ -91,31 +110,87 @@ export const generateRandomRGB = () => { return `rgb(${r},${g},${b})`; }; +export const get3DBasePoint = (point: Point, size: Size3D) => { + const grid = screenToGrid3d({ + x: point.x, + y: point.y, + }); + + const ratio = size.width / 128; + const base = gridToScreen3d({ + col: grid.col + (ratio > 1 ? 1 : ratio), + row: grid.row, + }); + + return { + x: base.x, + y: point.y + size.height + (size.offset ?? 0) - 74, + }; +}; + +const calcConnectorFor3D = (node: Node) => { + const point = node.point; + const nodeSize = node.size['3d'] as Size3D; + const base = get3DBasePoint(point, nodeSize); + + const _ratio = nodeSize.width / 128; + const ratio = _ratio < 1 ? 1 : _ratio; + + const center = { + x: base.x, + y: base.y + 74 - 37 * ratio, + }; + + const GRID_WIDTH_QUARTER_SIZE = GRID_3D_WIDTH_SIZE / 4; + const GRID_HEIGHT_QUARTER_SIZE = GRID_3D_HEIGHT_SIZE / 4; + const top = { + x: center.x + GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y - GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + const left = { + x: center.x - GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y - GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + const right = { + x: center.x + GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y + GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + const bottom = { + x: center.x - GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y + GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + return { + top, + right, + left, + bottom, + center, + }; +}; + export const getConnectorPoints = ( node: Node, dimension: Dimension, -): Omit => { +): ConnectorMap => { const point = node.point; - const { width, height } = node.size[dimension]; - const depth = GRID_3D_HEIGHT_SIZE / 2; - return { - top: { x: point.x + width / 2, y: point.y }, - right: - dimension === '2d' - ? { x: point.x + width, y: point.y + height / 2 } - : { - x: point.x + width, - y: point.y + (height - depth) / 2, - }, - left: - dimension === '2d' - ? { x: point.x, y: point.y + height / 2 } - : { - x: point.x, - y: point.y + (height - depth) / 2, - }, - bottom: { x: point.x + width / 2, y: point.y + height }, - }; + const nodeSize = node.size[dimension]; + const { width, height } = nodeSize; + + if (dimension === '2d') { + return { + top: { x: point.x + width / 2, y: point.y }, + right: { x: point.x + width, y: point.y + height / 2 }, + left: { x: point.x, y: point.y + height / 2 }, + bottom: { x: point.x + width / 2, y: point.y + height }, + center: { x: point.x + width / 2, y: point.y + height / 2 }, + }; + } + + return calcConnectorFor3D(node) as any; }; //INFO: μ„ λΆ„κ³Ό 내적/외적 μ‚¬μ΄μ˜ μ΅œλ‹¨ 거리λ₯Ό 계산(For Bend Point) @@ -151,3 +226,52 @@ export const getDistanceToSegment = ( const dy = p.y - yy; return Math.sqrt(dx * dx + dy * dy); }; + +const debounce = (func: (...args: any[]) => void, delay: number) => { + let timeout: ReturnType | null = null; + + return (...args: any[]) => { + if (timeout) clearTimeout(timeout); // 이전 타이머 제거 + timeout = setTimeout(() => { + func(...args); // μ§€μ •λœ μ‹œκ°„ ν›„ ν•¨μˆ˜ μ‹€ν–‰ + }, delay); + }; +}; + +export const findKeyByValue = ( + value: string, + list: { [id: string]: string }, +) => { + return Object.keys(list).find((key) => list[key] === value); +}; + +export const calcIsoMatrixPoint = (point: Point) => { + const isoMatrix = new DOMMatrix() + .rotate(30) + .skewX(-30) + .scale(1, 0.8602) + .translate(point.x, point.y); + + return isoMatrix; // κ²°κ³Ό ν–‰λ ¬ λ°˜ν™˜ +}; + +export const isEmpty = (something: any) => { + if (!something) return true; + if (Array.isArray(something) && something.length === 0) return true; + if (Object.keys(something).length === 0) return true; + return false; +}; + +export const undefinedReplacer = (_: string, value: any) => { + if (value === undefined) { + return { __undefined__: true }; + } + return value; +}; + +export const undefinedReviver = (_: string, value: any) => { + if (value && typeof value === 'object' && value.__undefined__) { + return null; + } + return value; +}; diff --git a/apps/client/src/vite-env.d.ts b/apps/client/src/vite-env.d.ts index e69de29b..d5e866e4 100644 --- a/apps/client/src/vite-env.d.ts +++ b/apps/client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index e8f2a5b1..0ba4ab4a 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -5,4 +5,7 @@ import tsconfigPaths from 'vite-tsconfig-paths'; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tsconfigPaths()], + server: { + port: 3001, + }, }); diff --git a/apps/hub/.eslintrc.json b/apps/hub/.eslintrc.json index 98ef693e..ee7d699d 100644 --- a/apps/hub/.eslintrc.json +++ b/apps/hub/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": ["next/core-web-vitals", "next/typescript"] + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } } diff --git a/apps/hub/Dockerfile b/apps/hub/Dockerfile new file mode 100644 index 00000000..4ce43f43 --- /dev/null +++ b/apps/hub/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20 AS development +WORKDIR /development +COPY ./pnpm-lock.yaml ./apps/hub/package*.json . +RUN npm install -g pnpm && pnpm install + +FROM node:20 AS build +WORKDIR /build +COPY --from=development /development/node_modules/ ./node_modules +COPY ./apps/hub/ . +RUN npm run build + +FROM node:20-alpine AS production +WORKDIR /app +RUN mkdir -p /app/server +COPY --from=build /build/.next/standalone/ ./standalone/ +COPY --from=build /build/.next/static/ ./standalone/.next/static + +ENV BACK_URL=https://api.cloudcanvas.kro.kr +ENTRYPOINT ["sh", "-c", "node standalone/server.js"] \ No newline at end of file diff --git a/apps/hub/next.config.ts b/apps/hub/next.config.ts index bb668f40..c46e7b66 100644 --- a/apps/hub/next.config.ts +++ b/apps/hub/next.config.ts @@ -2,6 +2,10 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ + output: 'standalone', + env: { + BACK_URL: 'https://api.cloudcanvas.kro.kr', + }, }; export default nextConfig; diff --git a/apps/hub/src/api/architectures.api.ts b/apps/hub/src/api/architectures.api.ts deleted file mode 100644 index f0404bb8..00000000 --- a/apps/hub/src/api/architectures.api.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { mockFetch } from '@/mocks/architectures'; - -export interface ArchitectureQueryParams { - page?: number; - search?: string; - sort?: string; - order?: string; -} - -export const fetchArchitectures = async (params: ArchitectureQueryParams) => { - const response = await mockFetch(params); - return response; -}; diff --git a/apps/hub/src/app/architectures/[id]/page.tsx b/apps/hub/src/app/architectures/[id]/page.tsx index 85651c12..ebeaa62d 100644 --- a/apps/hub/src/app/architectures/[id]/page.tsx +++ b/apps/hub/src/app/architectures/[id]/page.tsx @@ -1,68 +1,146 @@ 'use client'; -import { Button } from '@/ui/Button'; +import { DeleteIcon } from '@/ui/DeleteIcon'; +import { EditIcon } from '@/ui/EditIcon'; +import { ErrorMessage } from '@/ui/ErrorMessage'; +import { ImportIcon } from '@/ui/ImportIcon'; +import { LoadingSpinner } from '@/ui/LoadingSpinner'; +import { StarIcon } from '@/ui/StarIcon'; +import { Tag } from '@/ui/Tag'; +import { fetcher } from '@/utils/fetcher'; import { useParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import useSWR from 'swr'; interface PublicArchitecture { - id: string; + id: number; title: string; - author: string; - createdAt: Date; - architecture: string; - stars: number; - imports: number; + author: { id: number; name: string }; + createdAt: string; + architecture: Record; + cost: number; + tags: { tag: { name: string } }[]; + stars: any[]; + isAuthor: boolean; + _count: { + stars: number; + imports: number; + }; } export default function ArchitectureDetailPage() { const params = useParams<{ id: string }>(); - const [architecture, setArchitecture] = useState( - {} as any, + const { data, error, isLoading, mutate } = useSWR( + `${process.env.BACK_URL}/public-architectures/${params.id}`, + fetcher, ); - useEffect(() => { - const fetchedArchitecture = { - id: params.id, - title: 'Architecture for Cloud Canvas', - author: 'Web37-team', - createdAt: new Date(), - architecture: 'Architecture Content', - stars: 4, - imports: 2, - }; - setArchitecture(fetchedArchitecture); - }, []); + if (isLoading) return ; + if (error) return ; + + const { + title, + author: { name: author }, + createdAt, + cost, + tags, + stars: starData, + isAuthor, + _count: { stars, imports }, + } = data!; + + const isStarred = starData?.length > 0; + const isLoggedIn = localStorage.getItem('isLoggedIn') !== null; + + const handleDelete = async () => { + const shouldDelete = confirm('μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'); + if (!shouldDelete) return; + await fetch( + `${process.env.BACK_URL}/public-architectures/${params.id}`, + { + method: 'DELETE', + credentials: 'include', + }, + ); + alert('μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + location.href = '/'; + }; + + const toggleStar = async () => { + await fetch( + `${process.env.BACK_URL}/public-architectures/${params.id}/stars`, + { + method: data!.stars.length > 0 ? 'DELETE' : 'POST', + credentials: 'include', + }, + ); + mutate({ ...data!, stars: data!.stars.length > 0 ? [] : [{}] }); + }; - const handleImport = () => { - console.log('Imported!'); + const handleImport = async () => { + await fetch( + `${process.env.BACK_URL}/public-architectures/${params.id}/imports`, + { + method: 'POST', + credentials: 'include', + }, + ); + mutate({ + ...data!, + _count: { ...data!._count, imports: data!._count.imports + 1 }, + }); + alert('Imported!'); }; return (
-

- {architecture.title} -

-
-
- by - - {architecture.author} - +
+
+ {tags.map(({ tag: { name } }) => ( + + ))}
-
{architecture.createdAt?.toLocaleString()}
+
+

+ {title} +

+ {isAuthor && ( + <> + + + + )} +
+
+
+
{new Date(createdAt).toLocaleString()}
+
{author}
- - {architecture.imports} - + {imports} imported
-
- - +
+
+ + β‚©{cost} + + / month +
+ +

@@ -78,12 +156,12 @@ const ArchitectureImageExample = () => ( fill="none" xmlns="http://www.w3.org/2000/svg" > - + ); diff --git a/apps/hub/src/app/favicon.ico b/apps/hub/src/app/favicon.ico new file mode 100644 index 00000000..356af84c Binary files /dev/null and b/apps/hub/src/app/favicon.ico differ diff --git a/apps/hub/src/app/fonts.ts b/apps/hub/src/app/fonts.ts new file mode 100644 index 00000000..7a1a3c29 --- /dev/null +++ b/apps/hub/src/app/fonts.ts @@ -0,0 +1,6 @@ +import localFont from 'next/font/local'; + +export const gamjaFlower = localFont({ + src: '../fonts/GamjaFlower-Regular.ttf', + variable: '--font-gamja-flower', +}); diff --git a/apps/hub/src/app/layout.tsx b/apps/hub/src/app/layout.tsx index 4dc592ff..a508ad63 100644 --- a/apps/hub/src/app/layout.tsx +++ b/apps/hub/src/app/layout.tsx @@ -1,10 +1,11 @@ import type { Metadata } from 'next'; import './globals.css'; import { GlobalHeader } from '@/components/GlobalHeader'; +import { gamjaFlower } from './fonts'; export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', + title: 'Cloud Canvas', + description: 'Draw your cloud architecture with ease', }; export default function RootLayout({ @@ -12,7 +13,7 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode }>) { return ( - +
{children}
diff --git a/apps/hub/src/app/my/architectures/page.tsx b/apps/hub/src/app/my/architectures/page.tsx index c1cf92a1..f2530565 100644 --- a/apps/hub/src/app/my/architectures/page.tsx +++ b/apps/hub/src/app/my/architectures/page.tsx @@ -1,5 +1,14 @@ -import { Architectures } from '@/components/Architectures'; +'use client'; +import { PrivateArchitectureBoard } from '@/components/PrivateArchitectureBoard'; + +import { Suspense } from 'react'; export default function MyArchitecturesPage() { - return ; + return ( + + + + ); } diff --git a/apps/hub/src/app/my/shared/page.tsx b/apps/hub/src/app/my/shared/page.tsx index 81e94ad2..a9f63aee 100644 --- a/apps/hub/src/app/my/shared/page.tsx +++ b/apps/hub/src/app/my/shared/page.tsx @@ -1,5 +1,13 @@ -import { Architectures } from '@/components/Architectures'; +'use client'; +import { ArchitectureBoard } from '@/components/ArchitectureBoard'; +import { Suspense } from 'react'; export default function MySharedPage() { - return ; + return ( + + + + ); } diff --git a/apps/hub/src/app/my/starred/page.tsx b/apps/hub/src/app/my/starred/page.tsx index 9d432eec..9f98c2a1 100644 --- a/apps/hub/src/app/my/starred/page.tsx +++ b/apps/hub/src/app/my/starred/page.tsx @@ -1,5 +1,13 @@ -import { Architectures } from '@/components/Architectures'; +'use client'; +import { ArchitectureBoard } from '@/components/ArchitectureBoard'; +import { Suspense } from 'react'; export default function MyStarredPage() { - return ; + return ( + + + + ); } diff --git a/apps/hub/src/app/page.tsx b/apps/hub/src/app/page.tsx index 6777118c..60ecebd7 100644 --- a/apps/hub/src/app/page.tsx +++ b/apps/hub/src/app/page.tsx @@ -1,6 +1,13 @@ 'use client'; import { ArchitectureBoard } from '@/components/ArchitectureBoard'; +import { Suspense } from 'react'; export default function Home() { - return ; + return ( + + + + ); } diff --git a/apps/hub/src/components/ArchitectureBoard/ArchitectureItem.tsx b/apps/hub/src/components/ArchitectureBoard/ArchitectureItem.tsx index 7700ed47..8eab1b50 100644 --- a/apps/hub/src/components/ArchitectureBoard/ArchitectureItem.tsx +++ b/apps/hub/src/components/ArchitectureBoard/ArchitectureItem.tsx @@ -7,40 +7,40 @@ export const ArchitectureItem = ({ author, cost, createdAt, - stars, - imports, tags, + _count, }: { id: number; title: string; - author: string; + author: { id: number; name: string }; cost: number; createdAt: string; - stars: number; - imports: number; - tags: string[]; + tags: { tag: { name: string } }[]; + _count: { + imports: number; + stars: number; + }; }) => { + const { imports, stars } = _count; return ( -
  • +
  • {title}
    -
    -
    {createdAt}
    -
    {author}
    +
    +
    {new Date(createdAt).toLocaleString()}
    +
    {author.name}
    -
    - {tags.map((tag) => ( - +
    + {tags?.map(({ tag: { name } }) => ( + ))}
    -
    -
    {cost}
    -
    {imports}
    -
    {stars}
    -
    +
    β‚© {cost}
    +
    {stars}
    +
    {imports}
  • ); }; diff --git a/apps/hub/src/components/ArchitectureBoard/ArchitectureList.tsx b/apps/hub/src/components/ArchitectureBoard/ArchitectureList.tsx index cc37f07d..c8bd053e 100644 --- a/apps/hub/src/components/ArchitectureBoard/ArchitectureList.tsx +++ b/apps/hub/src/components/ArchitectureBoard/ArchitectureList.tsx @@ -1,6 +1,6 @@ import { ArchitectureItem } from './ArchitectureItem'; -export const ArchitectureList = ({ data }) => { +export const ArchitectureList = ({ data }: { data: Array }) => { if (!data?.length) { return (
    @@ -13,13 +13,9 @@ export const ArchitectureList = ({ data }) => { return (
    - {/*
    */} - {/*
    */} {data.map((item) => ( ))} - {/*
    */} - {/*
    */}
    ); }; diff --git a/apps/hub/src/components/ArchitectureBoard/BoardHeader.tsx b/apps/hub/src/components/ArchitectureBoard/BoardHeader.tsx index a7ebcb9f..9cf3d7a4 100644 --- a/apps/hub/src/components/ArchitectureBoard/BoardHeader.tsx +++ b/apps/hub/src/components/ArchitectureBoard/BoardHeader.tsx @@ -13,9 +13,9 @@ export const BoardHeader = ({ }) => { const columns = [ { key: 'name', title: 'Architecture', width: 'w-full' }, - { key: 'cost', title: 'Costs', width: 'w-40' }, - { key: 'imports', title: 'Imports', width: 'w-40' }, + { key: 'cost', title: 'Costs', width: 'w-52' }, { key: 'stars', title: 'Stars', width: 'w-40' }, + { key: 'imports', title: 'Imports', width: 'w-40' }, ]; const getSortIcon = (columnKey: string) => { @@ -27,7 +27,7 @@ export const BoardHeader = ({ }; return ( -
    +
    {columns.map((column) => (
    { if (error) return ; + // return
    {JSON.stringify(data)}
    ; + return (
    diff --git a/apps/hub/src/components/GlobalHeader.tsx b/apps/hub/src/components/GlobalHeader/index.tsx similarity index 63% rename from apps/hub/src/components/GlobalHeader.tsx rename to apps/hub/src/components/GlobalHeader/index.tsx index ca779588..2c5d8afb 100644 --- a/apps/hub/src/components/GlobalHeader.tsx +++ b/apps/hub/src/components/GlobalHeader/index.tsx @@ -2,27 +2,52 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { LinkButton } from '../ui/LinkButton'; +import { useEffect, useState } from 'react'; +import { LinkButton } from '@/ui/LinkButton'; +import { CloudCanvasIcon } from '@/ui/CloudCanvasIcon'; export const GlobalHeader = () => { const router = useRouter(); const [isLoggedIn, setIsLoggedIn] = useState(false); - const handleLogin = () => { - setIsLoggedIn(true); + useEffect(() => { + if (localStorage.getItem('isLoggedIn') === 'true') { + setIsLoggedIn(true); + } + }, []); + + const handleLogin = async () => { + const res = await fetch(`${process.env.BACK_URL}/auth/login`, { + method: 'POST', + credentials: 'include', + }); + if (res.ok) { + setIsLoggedIn(true); + localStorage.setItem('isLoggedIn', 'true'); + router.refresh(); + router.push('/'); + } }; const handleLogout = () => { + fetch(`${process.env.BACK_URL}/auth/logout`, { + method: 'POST', + credentials: 'include', + }); setIsLoggedIn(false); + localStorage.removeItem('isLoggedIn'); + router.refresh(); router.push('/'); }; return ( -
    +
    - -

    Cloud Canvas

    + + +

    + Cloud Canvas +

    {/* TODO: 검색창 μΆ”κ°€(μƒˆ μ»΄ν¬λ„ŒνŠΈλ‘œ) */}
    ); diff --git a/apps/hub/src/ui/ImportIcon.tsx b/apps/hub/src/ui/ImportIcon.tsx new file mode 100644 index 00000000..a863de8a --- /dev/null +++ b/apps/hub/src/ui/ImportIcon.tsx @@ -0,0 +1,10 @@ +export const ImportIcon = ({ size = 28 }: { size?: number }) => ( + + + +); diff --git a/apps/hub/src/ui/LinkButton.tsx b/apps/hub/src/ui/LinkButton.tsx index 09c6c19a..bd78e29c 100644 --- a/apps/hub/src/ui/LinkButton.tsx +++ b/apps/hub/src/ui/LinkButton.tsx @@ -4,7 +4,7 @@ export const LinkButton = ({ text, href }: { text: string; href: string }) => { return ( {text} diff --git a/apps/hub/src/ui/StarIcon.tsx b/apps/hub/src/ui/StarIcon.tsx new file mode 100644 index 00000000..ec1b948f --- /dev/null +++ b/apps/hub/src/ui/StarIcon.tsx @@ -0,0 +1,10 @@ +export const StarIcon = ({ size = 20 }: { size?: number }) => ( + + + +); diff --git a/apps/hub/src/ui/Tag.tsx b/apps/hub/src/ui/Tag.tsx index 23e4aaad..5c0bad35 100644 --- a/apps/hub/src/ui/Tag.tsx +++ b/apps/hub/src/ui/Tag.tsx @@ -2,8 +2,9 @@ import Link from 'next/link'; export const Tag = ({ tag }: { tag: string }) => ( {tag} diff --git a/apps/hub/src/ui/logo.svg b/apps/hub/src/ui/logo.svg new file mode 100644 index 00000000..691d44dc --- /dev/null +++ b/apps/hub/src/ui/logo.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/hub/src/utils/fetcher.ts b/apps/hub/src/utils/fetcher.ts index 01f6b73c..54758d5a 100644 --- a/apps/hub/src/utils/fetcher.ts +++ b/apps/hub/src/utils/fetcher.ts @@ -1,8 +1,8 @@ -import { mockFetch } from '@/mocks/architectures'; - export const fetcher = async (url: string) => { - const res = await mockFetch(url); - // if (!res.ok) throw new Error('Failed to fetch data'); - // return res.json(); - return res; + const res = await fetch(url, { + credentials: 'include', + }); + if (!res.ok) throw new Error('Failed to fetch data'); + const data = await res.json(); + return data; }; diff --git a/apps/hub/tailwind.config.ts b/apps/hub/tailwind.config.ts index 586a4007..f2afec58 100644 --- a/apps/hub/tailwind.config.ts +++ b/apps/hub/tailwind.config.ts @@ -9,6 +9,9 @@ export default { ], theme: { extend: { + fontFamily: { + gamjaFlower: ['var(--font-gamja-flower)', 'cursive'], + }, colors: { background: 'var(--background)', foreground: 'var(--foreground)', diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 9f2c8c35..0a4989a6 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -1,27 +1,170 @@ -FROM node:20 AS development -WORKDIR /development -COPY ./pnpm-lock.yaml ./apps/server/package*.json . -RUN npm install -g pnpm && pnpm install - -FROM node:20 AS build -WORKDIR /build -RUN mkdir -p /build/server && npm install -g pnpm -COPY --from=development /development/node_modules/ /build/server/node_modules -COPY --from=development /development/pnpm-lock.yaml /build/server/ -COPY ./apps/server/ /build/server/ -WORKDIR /build/server -RUN npx prisma generate && npm run build && rm -rf node_modules && pnpm install --frozen-lockfile --prod - -FROM node:20-alpine AS production -WORKDIR /app -RUN apk add --no-cache openssl -COPY ./apps/server/package.json . -COPY --from=build /build/server/node_modules ./node_modules -COPY --from=build /build/server/dist ./dist -RUN mkdir -p /app/prisma -COPY ./apps/server/prisma/ /app/prisma/ - -ENV DATABASE_URL=mysql://johndoe:randompassword@localhost:3306/mydb +# FROM node:20 AS development +# WORKDIR /development +# COPY . . +# RUN npm install -g pnpm && pnpm install +# RUN pnpm build + +# FROM node:20 AS build +# WORKDIR /build +# RUN mkdir -p /build/server && mkdir -p /build/packages && npm install -g pnpm +# COPY --from=development /development/apps/server/ /build/server/ +# COPY --from=development /development/pnpm-lock.yaml . +# COPY --from=development /development/packages/ ./packages/ +# COPY --from=development /development/pnpm-workspace.yaml . +# WORKDIR /build/server +# RUN rm -rf node_modules && pnpm install --prod + +# FROM node:20-alpine AS production +# WORKDIR /app +# RUN apk add --no-cache openssl +# RUN mkdir -p /app/node_modules +# COPY ./apps/server/package.json . +# COPY --from=build /build/server/node_modules ./node_modules +# COPY --from=build /build/server/dist ./dist +# RUN mkdir -p /app/prisma +# COPY ./apps/server/prisma/ /app/prisma/ + +# λΉŒλ“œ 단계: μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ€€λΉ„ 및 ν”„λ‘œλ•μ…˜ μ˜μ‘΄μ„± μ„€μΉ˜ +# FROM node:20 AS build +# WORKDIR /build + +# pnpm을 μ „μ—­μœΌλ‘œ μ„€μΉ˜ +# RUN npm install -g pnpm + +# # 개발 λ‹¨κ³„μ—μ„œ 파일 볡사 +# COPY --from=development /development/apps/server/ /build/server/ +# COPY --from=development /development/pnpm-lock.yaml . +# COPY --from=development /development/packages/ ./packages/ +# COPY --from=development /development/pnpm-workspace.yaml . + +# ν”„λ‘œλ•μ…˜ μ˜μ‘΄μ„±λ§Œ μ„€μΉ˜ +# WORKDIR /build/server +# RUN pnpm install + +# ν”„λ‘œλ•μ…˜ 단계: λŸ°νƒ€μž„ ν™˜κ²½ μ„€μ • +# FROM node:20-alpine AS production +# WORKDIR /app + +# # ν•„μš”ν•œ μ‹œμŠ€ν…œ μ˜μ‘΄μ„± μ„€μΉ˜ (Prisma λ“±) +# RUN apk add --no-cache openssl + +# # ν•„μš”ν•œ 디렉토리 생성 +# RUN mkdir -p /app/node_modules /app/prisma + +# # λΉŒλ“œ λ‹¨κ³„μ—μ„œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 파일과 λΉŒλ“œ 좜λ ₯ 볡사 +# COPY ./apps/server/package.json . +# COPY --from=build /build/server/node_modules ./node_modules +# COPY --from=build /build/server ./server +# COPY ./apps/server/prisma/ /app/prisma/ + +# # ν”„λ‘œλ•μ…˜ ν™˜κ²½ λ³€μˆ˜ μ„€μ • +# ENV DATABASE_URL=mysql://johndoe:randompassword@localhost:3306/mydb +# ENV PORT=3000 +# ENV NODE_ENV=production +# EXPOSE 3000 + +# # μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘ +# ENTRYPOINT ["sh", "-c", "node ./server/dist/src/main.js"] + + +# ENV DATABASE_URL=mysql://johndoe:randompassword@localhost:3306/mydb +# ENV PORT=3000 +# ENV NODE_ENV=development +# EXPOSE 3000 +# ENTRYPOINT ["sh", "-c", "npx prisma generate && node ./dist/src/main.js"] +# # Development stage: Install dependencies and build the project +# FROM node:20 AS development +# WORKDIR /development + +# FROM node:20 AS development +# WORKDIR /development +# COPY . . +# RUN npm install -g pnpm && pnpm install +# RUN pnpm build + +# # Build stage: Prepare production build +# FROM node:20 AS build +# WORKDIR /build + +# # Install pnpm and prepare the workspace +# RUN npm install -g pnpm +# COPY --from=development /development/apps/server/package.json ./apps/server/ +# COPY --from=development /development/pnpm-workspace.yaml ./ +# COPY --from=development /development/pnpm-lock.yaml ./ +# COPY --from=development /development/apps/server/ ./apps/server/ +# COPY --from=development /development/packages/ ./packages/ + +# # Install production-only dependencies +# WORKDIR /build/apps/server +# RUN pnpm install --frozen-lockfile --prod + +# # Production stage: Setup runtime environment +# FROM node:20-alpine AS production +# WORKDIR /app + +# # Install required system packages +# RUN apk add --no-cache openssl + +# # Copy necessary files for the application +# COPY ./apps/server/package.json . +# COPY --from=build /build/apps/server/node_modules ./node_modules +# COPY --from=build /build/apps/server/dist ./dist +# RUN mkdir -p /app/prisma +# COPY ./apps/server/prisma/ /app/prisma/ + +# # Environment variables and entrypoint +# ENV DATABASE_URL=mysql://johndoe:randompassword@localhost:3306/mydb +# ENV PORT=3000 +# ENV NODE_ENV=production +# EXPOSE 3000 + +# FROM node:20 AS mono +# WORKDIR /mono +# COPY . . +# RUN npm install -g pnpm && pnpm install && pnpm build + +# FROM node:20 AS development +# WORKDIR /development +# COPY ./packages/ ./packages/ +# COPY ./pnpm-lock.yaml ./apps/server/package*.json ./pnpm-workspace.yaml . +# RUN npm install -g pnpm && pnpm install --prod + +# FROM node:20-alpine AS production +# WORKDIR /app +# RUN apk add --no-cache openssl +# COPY ./apps/server/package.json ./apps/server/ +# COPY --from=development /development/node_modules ./apps/server/node_modules +# COPY --from=mono /mono/apps/server/dist ./apps/server/dist +# RUN mkdir -p /app/packages +# COPY --from=mono /mono/packages /app/packages +# COPY ./apps/server/prisma/ ./apps/server/prisma/ +# COPY ./pnpm-workspace.yaml . + +# ENV DATABASE_URL=mysql://johndoe:randompassword@localhost:3306/mydb +# ENV PORT=3000 +# ENV NODE_ENV=development +# EXPOSE 3000 +# ENTRYPOINT ["sh", "-c", "node apps/server/dist/src/main.js"] + +FROM node:20 + +WORKDIR /usr/src/app + +COPY . . + +RUN npm install -g pnpm && pnpm install && pnpm build + +# Copy app source + +ENV DATABASE_URL=mysql://seogeonhyuk:rhdrhdCLF@192.168.64.3:3306/cloud_canvas +ENV MYSQL_HOST=1 +ENV MYSQL_PORT=3306 +ENV REDIS_HOST=3 +ENV REDIS_PORT=6379 +ENV NCLOUD_ACCESS_KEY=0 +ENV NCLOUD_SECRET_KEY=0 ENV PORT=3000 +ENV NODE_ENV=development EXPOSE 3000 -ENTRYPOINT ["sh", "-c", "npx prisma generate && node ./dist/src/main.js"] \ No newline at end of file + +CMD [ "node", "apps/server/dist/src/main.js" ] \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 5bb0d10a..b57dd49a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -5,9 +5,8 @@ "author": "", "private": true, "license": "UNLICENSED", - "type": "module", "scripts": { - "build": "nest build", + "build": "pnpm prisma-generate && nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "prisma-generate": "prisma generate", @@ -33,6 +32,7 @@ "@nestjs/mapped-types": "*", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.1.1", "@nestjs/swagger": "^8.0.5", "@prisma/client": "^5.22.0", "class-transformer": "^0.5.1", diff --git a/apps/server/prisma/migrations/20241125182942_init/migration.sql b/apps/server/prisma/migrations/20241128085427_/migration.sql similarity index 82% rename from apps/server/prisma/migrations/20241125182942_init/migration.sql rename to apps/server/prisma/migrations/20241128085427_/migration.sql index a4abcef0..b9941b71 100644 --- a/apps/server/prisma/migrations/20241125182942_init/migration.sql +++ b/apps/server/prisma/migrations/20241128085427_/migration.sql @@ -5,7 +5,27 @@ CREATE TABLE `import` ( `user_id` INTEGER NOT NULL, `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), - UNIQUE INDEX `import_public_architecture_id_user_id_key`(`public_architecture_id`, `user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ncloud_server_resource` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `server_resource_type_id` INTEGER NOT NULL, + `server_spec_code` CHAR(50) NOT NULL, + `hour_cost` DOUBLE NOT NULL, + `month_cost` DOUBLE NOT NULL, + `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + `updated_at` TIMESTAMP(0) NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ncloud_server_resource_type` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `type` CHAR(50) NOT NULL, + PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -61,6 +81,7 @@ CREATE TABLE `tag` ( `name` VARCHAR(15) NOT NULL, `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + UNIQUE INDEX `tag_name_key`(`name`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -91,6 +112,9 @@ ALTER TABLE `import` ADD CONSTRAINT `import_public_architecture_id_fkey` FOREIGN -- AddForeignKey ALTER TABLE `import` ADD CONSTRAINT `import_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE `ncloud_server_resource` ADD CONSTRAINT `ncloud_server_resource_server_resource_type_id_fkey` FOREIGN KEY (`server_resource_type_id`) REFERENCES `ncloud_server_resource_type`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE `private_architecture` ADD CONSTRAINT `private_architecture_author_id_fkey` FOREIGN KEY (`author_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/server/prisma/schema/import.prisma b/apps/server/prisma/schema/import.prisma index 88c57513..000d21f1 100644 --- a/apps/server/prisma/schema/import.prisma +++ b/apps/server/prisma/schema/import.prisma @@ -7,6 +7,5 @@ model Import { publicArchitecture PublicArchitecture @relation(fields: [publicArchitectureId], references: [id]) user User @relation(fields: [userId], references: [id]) - @@unique([publicArchitectureId, userId], name: "unique_import") @@map("import") } diff --git a/apps/server/prisma/schema/ncloud-server-resoruce.prisma b/apps/server/prisma/schema/ncloud-server-resoruce.prisma new file mode 100644 index 00000000..773ecf15 --- /dev/null +++ b/apps/server/prisma/schema/ncloud-server-resoruce.prisma @@ -0,0 +1,13 @@ +model NcloudServerResource { + id Int @id @default(autoincrement()) + serverResourceTypeId Int @map("server_resource_type_id") + serverSpecCode String @map("server_spec_code") @db.Char(50) + hourCost Float @map("hour_cost") + monthCost Float @map("month_cost") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) + + serverResoruceType NcloudServerResourceType @relation(fields: [serverResourceTypeId], references: [id]) + + @@map("ncloud_server_resource") +} diff --git a/apps/server/prisma/schema/ncloud-server-resource-type.prisma b/apps/server/prisma/schema/ncloud-server-resource-type.prisma new file mode 100644 index 00000000..1e728c09 --- /dev/null +++ b/apps/server/prisma/schema/ncloud-server-resource-type.prisma @@ -0,0 +1,8 @@ +model NcloudServerResourceType { + id Int @id @default(autoincrement()) + type String @db.Char(50) + + NcloudServerResources NcloudServerResource[] + + @@map("ncloud_server_resource_type") +} diff --git a/apps/server/prisma/schema/public-architecture.prisma b/apps/server/prisma/schema/public-architecture.prisma index 6e603503..fef8534f 100644 --- a/apps/server/prisma/schema/public-architecture.prisma +++ b/apps/server/prisma/schema/public-architecture.prisma @@ -6,10 +6,10 @@ model PublicArchitecture { createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) cost Float @default(0) - author User @relation(fields: [authorId], references: [id]) - stars Star[] - imports Import[] - PublicArchitectureTag PublicArchitectureTag[] + author User @relation(fields: [authorId], references: [id]) + stars Star[] + imports Import[] + tags PublicArchitectureTag[] @@map("public_architecture") } diff --git a/apps/server/prisma/schema/tag.prisma b/apps/server/prisma/schema/tag.prisma index d50ce89f..e9d63c3f 100644 --- a/apps/server/prisma/schema/tag.prisma +++ b/apps/server/prisma/schema/tag.prisma @@ -1,8 +1,8 @@ model Tag { - id Int @id @default(autoincrement()) - name String @db.VarChar(15) - createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) - PublicArchitectureTag PublicArchitectureTag[] + id Int @id @default(autoincrement()) + name String @unique @db.VarChar(15) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) + publicArchitectures PublicArchitectureTag[] @@map("tag") } diff --git a/apps/server/prisma/seed.js b/apps/server/prisma/seed.js index ba173c62..d782a5cd 100644 --- a/apps/server/prisma/seed.js +++ b/apps/server/prisma/seed.js @@ -118,7 +118,7 @@ async function main() { const tags = []; for (let i = 0; i < tagAmount; i++) { const tag = { - name: faker.word.verb({ length: { max: 15 } }), + name: faker.word.verb({ length: { max: 10 } }), }; tags.push(tag); } diff --git a/apps/server/src/app.controller.spec.ts b/apps/server/src/app.controller.spec.ts index a8a2812d..8c9aa9d8 100644 --- a/apps/server/src/app.controller.spec.ts +++ b/apps/server/src/app.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller.js'; -import { AppService } from './app.service.js'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; import { describe, beforeEach, it, expect } from 'vitest'; describe('AppController', () => { diff --git a/apps/server/src/app.controller.ts b/apps/server/src/app.controller.ts index fbb8e61c..a325e8ba 100644 --- a/apps/server/src/app.controller.ts +++ b/apps/server/src/app.controller.ts @@ -1,12 +1,5 @@ -import { - Controller, - Get, - HttpException, - NotFoundException, -} from '@nestjs/common'; -import { AppService } from './app.service.js'; - -class CustomError extends Error {} +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; @Controller() export class AppController { @@ -14,8 +7,6 @@ export class AppController { @Get() getHello(): string { - try { - return this.appService.getHello(); - } catch (error) {} + return this.appService.getHello(); } } diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 8a4d8543..d1df3742 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -1,14 +1,15 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { AppController } from './app.controller.js'; -import { AppService } from './app.service.js'; -import { AuthModule } from './auth/auth.module.js'; -import { UserModule } from './user/user.module.js'; -import { PublicArchitectureModule } from './public-architecture/public-architecture.module.js'; -// import { PrivateArchitectureModule } from 'src/private-architecture/private-architecture.module'; -import { PrismaService } from './prisma/prisma.service.js'; -// import { routes } from './routes'; -import { MyModule } from './my/my.module.js'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { AuthModule } from 'src/auth/auth.module'; +import { UserModule } from 'src/user/user.module'; +import { PublicArchitectureModule } from 'src/public-architecture/public-architecture.module'; +import { PrivateArchitectureModule } from 'src/private-architecture/private-architecture.module'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { MyModule } from './my/my.module'; +import { ScheduleModule } from '@nestjs/schedule'; +import { NcloudResourcesService } from './ncloud-resources/ncloud-resources.service.js'; @Module({ imports: [ @@ -16,10 +17,11 @@ import { MyModule } from './my/my.module.js'; AuthModule, UserModule, PublicArchitectureModule, - // PrivateArchitectureModule, + PrivateArchitectureModule, MyModule, + ScheduleModule.forRoot(), ], controllers: [AppController], - providers: [AppService, PrismaService], + providers: [AppService, PrismaService, NcloudResourcesService], }) export class AppModule {} diff --git a/apps/server/src/auth/auth.controller.ts b/apps/server/src/auth/auth.controller.ts index 5a81a40b..4cb2fca0 100644 --- a/apps/server/src/auth/auth.controller.ts +++ b/apps/server/src/auth/auth.controller.ts @@ -6,10 +6,10 @@ import { Res, UseGuards, } from '@nestjs/common'; -import { AuthService } from './auth.service.js'; -import { JwtAuthGuard } from '../guards/jwt-auth.guard.js'; -import { User } from '../decorators/user.decorator.js'; -import { AuthenticatedUser } from 'src/types/authenticated-user.interface.js'; +import { AuthService } from './auth.service'; +import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; +import { User } from 'src/decorators/user.decorator'; +import { AuthenticatedUser } from 'src/types/authenticated-user.interface'; import { Response } from 'express'; @Controller('auth') @@ -17,8 +17,8 @@ export class AuthController { constructor(private readonly authService: AuthService) {} @Post('login') - login(@Res({ passthrough: true }) res: Response) { - const token = this.authService.login(); + async login(@Res({ passthrough: true }) res: Response) { + const token = await this.authService.login(); res.cookie('Authentication', token, { httpOnly: true, secure: true, diff --git a/apps/server/src/auth/auth.module.ts b/apps/server/src/auth/auth.module.ts index aa9d0852..4a819acc 100644 --- a/apps/server/src/auth/auth.module.ts +++ b/apps/server/src/auth/auth.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; -import { AuthService } from './auth.service.js'; -import { AuthController } from './auth.controller.js'; -import { UserModule } from '../user/user.module.js'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { UserModule } from 'src/user/user.module'; import { JwtModule } from '@nestjs/jwt'; -import { JwtStrategy } from './strategies/jwt.strategy.js'; +import { JwtStrategy } from './strategies/jwt.strategy'; @Module({ imports: [ diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index eeef2be4..4a4b7c67 100644 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { UserService } from '../user/user.service.js'; +import { UserService } from 'src/user/user.service'; @Injectable() export class AuthService { @@ -10,7 +10,7 @@ export class AuthService { ) {} async login() { - const user = this.userService.getTestUser(); + const user = await this.userService.getTestUser(); if (!user) { throw new UnauthorizedException(); } diff --git a/apps/server/src/decorators/user.decorator.ts b/apps/server/src/decorators/user.decorator.ts index 2a1c7c35..b9b12c33 100644 --- a/apps/server/src/decorators/user.decorator.ts +++ b/apps/server/src/decorators/user.decorator.ts @@ -1,9 +1,4 @@ -import { - createParamDecorator, - ExecutionContext, - InternalServerErrorException, - UnauthorizedException, -} from '@nestjs/common'; +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { User as UserEntity } from '@prisma/client'; export const User = createParamDecorator( @@ -11,14 +6,6 @@ export const User = createParamDecorator( const request = ctx.switchToHttp().getRequest(); const user = request.user; - if (!user) { - throw new UnauthorizedException('μ‚¬μš©μž 정보가 μ—†μŠ΅λ‹ˆλ‹€.'); - } - - if (!data) return user; - if (data in user) return user[data]; - throw new InternalServerErrorException( - `μ‚¬μš©μž 정보에 ${data}이/κ°€ μ—†μŠ΅λ‹ˆλ‹€.`, - ); + return data ? user?.[data] : user; }, ); diff --git a/apps/server/src/filters/prisma-exception.filter.spec.ts b/apps/server/src/filters/prisma-exception.filter.spec.ts index d7cfaced..0175abc4 100644 --- a/apps/server/src/filters/prisma-exception.filter.spec.ts +++ b/apps/server/src/filters/prisma-exception.filter.spec.ts @@ -1,4 +1,4 @@ -import { PrismaExceptionFilter } from './prisma-exception.filter.js'; +import { PrismaExceptionFilter } from './prisma-exception.filter'; describe('PrismaExceptionFilter', () => { it('should be defined', () => { diff --git a/apps/server/src/guards/optional-auth.guard.ts b/apps/server/src/guards/optional-auth.guard.ts new file mode 100644 index 00000000..66f2b99e --- /dev/null +++ b/apps/server/src/guards/optional-auth.guard.ts @@ -0,0 +1,15 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class OptionalAuthGuard extends AuthGuard('jwt') { + handleRequest( + err: any, + user: any, + info: any, + context: ExecutionContext, + status?: any, + ): TUser { + return user; + } +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 61ba2289..2c81382f 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,19 +1,15 @@ import { HttpAdapterHost, NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module.js'; -import { swaggerConfig } from './swagger/swagger.config.js'; +import { AppModule } from './app.module'; +import { swaggerConfig } from './swagger/swagger.config'; import { ValidationPipe } from '@nestjs/common'; import helmet from 'helmet'; -import cookieParser from 'cookie-parser'; -import { PrismaExceptionFilter } from './filters/prisma-exception.filter.js'; -import { ApiKeyCredentials, Ncloud, PriceApi } from '@cloud-canvas/ncloud-sdk'; +import * as cookieParser from 'cookie-parser'; +import { PrismaExceptionFilter } from './filters/prisma-exception.filter'; +import { ConfigService } from '@nestjs/config'; async function bootstrap() { const app = await NestFactory.create(AppModule); - const ncloud = new Ncloud(); - const priceApi = new PriceApi(ncloud.keys() as ApiKeyCredentials); - console.log(priceApi); - const result = await priceApi.getProductCategoryList({}); - console.log(result.getProductCategoryListResponse.productCategoryList); + swaggerConfig(app); app.use(helmet()); @@ -33,6 +29,40 @@ async function bootstrap() { const { httpAdapter } = app.get(HttpAdapterHost); app.useGlobalFilters(new PrismaExceptionFilter(httpAdapter)); + const configService = app.get(ConfigService); + const environment = configService.get('NODE_ENV'); + + if (environment !== 'production') { + app.enableCors({ + origin: 'http://localhost:3001', + methods: [ + 'GET', + 'HEAD', + 'PUT', + 'PATCH', + 'POST', + 'DELETE', + 'OPTIONS', + ], + allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], + credentials: true, + }); + } else { + app.enableCors({ + origin: 'https://cloudcanvas.kro.kr', + methods: [ + 'GET', + 'HEAD', + 'PUT', + 'PATCH', + 'POST', + 'DELETE', + 'OPTIONS', + ], + allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], + credentials: true, + }); + } await app.listen(3000); } bootstrap(); diff --git a/apps/server/src/my/dto/create-my.dto.ts b/apps/server/src/my/dto/create-my.dto.ts deleted file mode 100644 index 5f3779b9..00000000 --- a/apps/server/src/my/dto/create-my.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreateMyDto {} diff --git a/apps/server/src/my/dto/find-my-architectures.dto.ts b/apps/server/src/my/dto/find-my-architectures.dto.ts new file mode 100644 index 00000000..9cf6780d --- /dev/null +++ b/apps/server/src/my/dto/find-my-architectures.dto.ts @@ -0,0 +1,8 @@ +export interface FindMyArchitecturesDto { + page?: number; + limit?: number; + search?: string; + sort?: string; + order?: string; + userId: number; +} diff --git a/apps/server/src/my/dto/update-my.dto.ts b/apps/server/src/my/dto/update-my.dto.ts deleted file mode 100644 index 1508e499..00000000 --- a/apps/server/src/my/dto/update-my.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateMyDto } from './create-my.dto.js'; - -export class UpdateMyDto extends PartialType(CreateMyDto) {} diff --git a/apps/server/src/my/my.controller.ts b/apps/server/src/my/my.controller.ts index 4a10369c..0d3185bc 100644 --- a/apps/server/src/my/my.controller.ts +++ b/apps/server/src/my/my.controller.ts @@ -1,23 +1,43 @@ -import { Controller, Get, Query } from '@nestjs/common'; -import { MyService } from './my.service.js'; -import { QueryParamsDto } from '../types/query-params.dto.js'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { MyService } from './my.service'; +import { QueryParamsDto } from 'src/types/query-params.dto'; +import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; +import { User } from 'src/decorators/user.decorator'; @Controller('my') export class MyController { constructor(private readonly service: MyService) {} @Get('private-architectures') - getMyPrivateArchitectures(@Query() queryParams: QueryParamsDto) { - return this.service.findMyPrivateArchitectures(queryParams); + @UseGuards(JwtAuthGuard) + getMyPrivateArchitectures( + @User('id') userId: number, + @Query() queryParams: QueryParamsDto, + ) { + return this.service.findMyPrivateArchitectures({ + ...queryParams, + userId, + }); } @Get('public-architectures') - getMyPublicArchitectures(@Query() queryParams: QueryParamsDto) { - return this.service.findMyPublicArchitectures(queryParams); + @UseGuards(JwtAuthGuard) + getMyPublicArchitectures( + @User('id') userId: number, + @Query() queryParams: QueryParamsDto, + ) { + return this.service.findMyPublicArchitectures({ + ...queryParams, + userId, + }); } @Get('public-architectures/stars') - getMyStars(@Query() queryParams: QueryParamsDto) { - return this.service.findMyStars(queryParams); + @UseGuards(JwtAuthGuard) + getMyStars( + @User('id') userId: number, + @Query() queryParams: QueryParamsDto, + ) { + return this.service.findMyStars({ ...queryParams, userId }); } } diff --git a/apps/server/src/my/my.module.ts b/apps/server/src/my/my.module.ts index 6f884f69..51871deb 100644 --- a/apps/server/src/my/my.module.ts +++ b/apps/server/src/my/my.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { MyService } from './my.service.js'; -import { MyController } from './my.controller.js'; -import { PrismaModule } from '../prisma/prisma.module.js'; +import { MyService } from './my.service'; +import { MyController } from './my.controller'; +import { PrismaModule } from 'src/prisma/prisma.module'; @Module({ imports: [PrismaModule], diff --git a/apps/server/src/my/my.service.ts b/apps/server/src/my/my.service.ts index ce9a9582..be3e7f21 100644 --- a/apps/server/src/my/my.service.ts +++ b/apps/server/src/my/my.service.ts @@ -1,45 +1,117 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service.js'; -import { QueryParamsDto } from '../types/query-params.dto.js'; -import { buildQueryOptions } from '../utils/build-query-options.js'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { buildPaginationOptions } from 'src/utils/build-query-options'; +import { FindMyArchitecturesDto } from './dto/find-my-architectures.dto'; +import { buildFilterOptions } from 'src/utils/build-query-options'; +import { buildSortOptions } from 'src/utils/build-query-options'; @Injectable() export class MyService { constructor(private readonly prisma: PrismaService) {} - async findMyPrivateArchitectures(queryParams: QueryParamsDto) { - return this.prisma.privateArchitecture.findMany({ - ...buildQueryOptions(queryParams), - }); + async findMyPrivateArchitectures(queryParams: FindMyArchitecturesDto) { + const [data, total] = await this.prisma.$transaction([ + this.prisma.privateArchitecture.findMany({ + ...buildPaginationOptions(queryParams), + ...buildSortOptions(queryParams), + ...buildFilterOptions(queryParams), + }), + this.prisma.privateArchitecture.count({ + ...buildFilterOptions(queryParams), + }), + ]); + return { data, total }; } - async findMyPublicArchitectures(queryParams: QueryParamsDto) { - return this.prisma.publicArchitecture.findMany({ - include: { - author: true, - _count: { - select: { - stars: true, - imports: true, + async findMyPublicArchitectures(queryParams: FindMyArchitecturesDto) { + const [data, total] = await this.prisma.$transaction([ + this.prisma.publicArchitecture.findMany({ + include: { + author: { + select: { + id: true, + name: true, + }, + }, + tags: { + select: { + tag: { + select: { + name: true, + }, + }, + }, + }, + _count: { + select: { + stars: true, + imports: true, + }, }, }, - }, - ...buildQueryOptions(queryParams), - }); + ...buildPaginationOptions(queryParams), + ...buildSortOptions(queryParams), + ...buildFilterOptions(queryParams), + }), + this.prisma.publicArchitecture.count({ + ...buildFilterOptions(queryParams), + }), + ]); + return { data, total }; } - async findMyStars(queryParams: QueryParamsDto) { - return this.prisma.publicArchitecture.findMany({ - include: { - author: true, - _count: { - select: { - stars: true, - imports: true, + async findMyStars(queryParams: FindMyArchitecturesDto) { + const [data, total] = await this.prisma.$transaction([ + this.prisma.publicArchitecture.findMany({ + include: { + author: { + select: { + id: true, + name: true, + }, + }, + tags: { + select: { + tag: { + select: { + name: true, + }, + }, + }, + }, + _count: { + select: { + stars: true, + imports: true, + }, + }, + }, + ...buildPaginationOptions(queryParams), + ...buildSortOptions(queryParams), + where: { + stars: { + some: { + userId: queryParams.userId, + }, + }, + title: queryParams.search + ? { contains: queryParams.search } + : undefined, + }, + }), + this.prisma.publicArchitecture.count({ + where: { + stars: { + some: { + userId: queryParams.userId, + }, }, + title: queryParams.search + ? { contains: queryParams.search } + : undefined, }, - }, - ...buildQueryOptions(queryParams), - }); + }), + ]); + return { data, total }; } } diff --git a/apps/server/src/ncloud-resources/ncloud-resources.service.spec.ts b/apps/server/src/ncloud-resources/ncloud-resources.service.spec.ts new file mode 100644 index 00000000..ff55a8ca --- /dev/null +++ b/apps/server/src/ncloud-resources/ncloud-resources.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NcloudResourcesService } from './ncloud-resources.service'; + +describe('NcloudResourcesService', () => { + let service: NcloudResourcesService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [NcloudResourcesService], + }).compile(); + + service = module.get(NcloudResourcesService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server/src/ncloud-resources/ncloud-resources.service.ts b/apps/server/src/ncloud-resources/ncloud-resources.service.ts new file mode 100644 index 00000000..e8a402be --- /dev/null +++ b/apps/server/src/ncloud-resources/ncloud-resources.service.ts @@ -0,0 +1,103 @@ +import { Ncloud, PriceApi, ApiKeyCredentials } from '@cloud-canvas/ncloud-sdk'; +import { Injectable } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Injectable() +export class NcloudResourcesService { + constructor(private readonly prisma: PrismaService) {} + + @Cron(CronExpression.EVERY_DAY_AT_3AM, { + name: 'Insert Ncloud Resource Cron Job', + }) + async insertNcloudResource() { + const ncloud = new Ncloud(); + const priceApi = new PriceApi(ncloud.keys() as ApiKeyCredentials); + const result = await priceApi.getProductPriceList({ + regionCode: 'KR', + payCurrencyCode: 'KRW', + productCategoryCode: 'COMPUTE', + }); + const ncloudServerResourceMap: Map< + string, + Record[] + > = new Map(); + result.productPriceList.forEach((product) => { + if ( + product.productItemKind.code === 'SVR' && + product.serverProductCode && + product.serverProductCode.endsWith('50') + ) { + if (!ncloudServerResourceMap.has(product.productType.codeName)) + ncloudServerResourceMap.set( + product.productType.codeName, + [], + ); + const { + serverProductCode, + priceList: [{ price: monthPrice }, { price: hourPrice }], + }: { + serverProductCode: string; + priceList: { price: number }[]; + } = product; + ncloudServerResourceMap.get(product.productType.codeName).push({ + serverProductCode: serverProductCode.toLowerCase(), + monthPrice, + hourPrice, + }); + } + }); + await this.prisma.$transaction(async (tx) => { + await tx.ncloudServerResource.deleteMany({}); + await tx.ncloudServerResourceType.deleteMany({}); + + const ncloudServerResourceTypes = [ + ...ncloudServerResourceMap.keys(), + ].map((key) => ({ type: key })); + + await tx.ncloudServerResourceType.createMany({ + data: ncloudServerResourceTypes, + }); + + const ncloudServerResources = await Promise.all( + [...ncloudServerResourceMap.values()].map( + async (ncloudServerResourceList, index) => { + const serverResourceTypeId = + await tx.ncloudServerResourceType.findFirst({ + select: { id: true }, + where: { + type: ncloudServerResourceTypes[index].type, + }, + }); + + return ncloudServerResourceList.map( + (ncloudServerResource) => ({ + serverResourceTypeId: serverResourceTypeId?.id, + serverSpecCode: + ncloudServerResource.serverProductCode as string, + hourCost: parseFloat( + '' + ncloudServerResource.hourPrice, + ), + monthCost: parseFloat( + '' + ncloudServerResource.monthPrice, + ), + }), + ); + }, + ), + ); + + const flattenedResources = ncloudServerResources.flat(); + + await tx.ncloudServerResource.createMany({ + data: flattenedResources, + }); + console.log(await tx.ncloudServerResourceType.findMany({})); + console.log(await tx.ncloudServerResource.findMany({})); + }); + } + + async onApplicationBootstrap() { + await this.insertNcloudResource(); + } +} diff --git a/apps/server/src/prisma/prisma.module.ts b/apps/server/src/prisma/prisma.module.ts index 6d6b8203..575f92f6 100644 --- a/apps/server/src/prisma/prisma.module.ts +++ b/apps/server/src/prisma/prisma.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { PrismaService } from './prisma.service.js'; +import { PrismaService } from './prisma.service'; @Module({ providers: [PrismaService], diff --git a/apps/server/src/private-architecture/dto/update-private-architecture.dto.ts b/apps/server/src/private-architecture/dto/update-private-architecture.dto.ts index 40d31385..2cb50355 100644 --- a/apps/server/src/private-architecture/dto/update-private-architecture.dto.ts +++ b/apps/server/src/private-architecture/dto/update-private-architecture.dto.ts @@ -1,5 +1,5 @@ import { PartialType } from '@nestjs/mapped-types'; -import { CreatePrivateArchiectureDto } from './create-private-architecture.dto.js'; +import { CreatePrivateArchiectureDto } from './create-private-architecture.dto'; export class UpdatePrivateArchiectureDto extends PartialType( CreatePrivateArchiectureDto, diff --git a/apps/server/src/private-architecture/private-architecture.controller.ts b/apps/server/src/private-architecture/private-architecture.controller.ts index 1d7f05ea..228a89ba 100644 --- a/apps/server/src/private-architecture/private-architecture.controller.ts +++ b/apps/server/src/private-architecture/private-architecture.controller.ts @@ -9,12 +9,12 @@ import { ParseIntPipe, UseGuards, } from '@nestjs/common'; -import { PrivateArchitectureService } from './private-architecture.service.js'; -import { CreatePrivateArchiectureDto } from './dto/create-private-architecture.dto.js'; -import { UpdatePrivateArchiectureDto } from './dto/update-private-architecture.dto.js'; -import { CreateVersionDto } from './dto/create-version.dto.js'; -import { JwtAuthGuard } from 'src/guards/jwt-auth.guard.js'; -import { User } from 'src/decorators/user.decorator.js'; +import { PrivateArchitectureService } from './private-architecture.service'; +import { CreatePrivateArchiectureDto } from './dto/create-private-architecture.dto'; +import { UpdatePrivateArchiectureDto } from './dto/update-private-architecture.dto'; +import { CreateVersionDto } from './dto/create-version.dto'; +import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; +import { User } from 'src/decorators/user.decorator'; @Controller('private-architectures') export class PrivateArchitectureController { diff --git a/apps/server/src/private-architecture/private-architecture.module.ts b/apps/server/src/private-architecture/private-architecture.module.ts index 9f77c34e..31b13f88 100644 --- a/apps/server/src/private-architecture/private-architecture.module.ts +++ b/apps/server/src/private-architecture/private-architecture.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { PrivateArchitectureService } from './private-architecture.service.js'; -import { PrivateArchitectureController } from './private-architecture.controller.js'; -import { PrismaModule } from 'src/prisma/prisma.module.js'; +import { PrivateArchitectureService } from './private-architecture.service'; +import { PrivateArchitectureController } from './private-architecture.controller'; +import { PrismaModule } from 'src/prisma/prisma.module'; @Module({ imports: [PrismaModule], diff --git a/apps/server/src/private-architecture/private-architecture.service.ts b/apps/server/src/private-architecture/private-architecture.service.ts index 70c2e2fd..b9b44cb7 100644 --- a/apps/server/src/private-architecture/private-architecture.service.ts +++ b/apps/server/src/private-architecture/private-architecture.service.ts @@ -1,13 +1,13 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service.js'; -import { FindArchitectureDto } from './dto/find-architecture.dto.js'; -import { FindVersionDto } from './dto/find-version.dto.js'; -import { FindVersionsDto } from './dto/find-versions.dto.js'; -import { ModifyArchitectureDto } from './dto/modify-architecture.dto.js'; -import { RemoveArchitectureDto } from './dto/remove-architecture.dto.js'; -import { RemoveVersionDto } from './dto/remove-version.dto.js'; -import { SaveArchitectureDto } from './dto/save-architecture.dto.js'; -import { SaveVersionDto } from './dto/save-version.dto.js'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { FindArchitectureDto } from './dto/find-architecture.dto'; +import { FindVersionDto } from './dto/find-version.dto'; +import { FindVersionsDto } from './dto/find-versions.dto'; +import { ModifyArchitectureDto } from './dto/modify-architecture.dto'; +import { RemoveArchitectureDto } from './dto/remove-architecture.dto'; +import { RemoveVersionDto } from './dto/remove-version.dto'; +import { SaveArchitectureDto } from './dto/save-architecture.dto'; +import { SaveVersionDto } from './dto/save-version.dto'; @Injectable() export class PrivateArchitectureService { @@ -55,14 +55,8 @@ export class PrivateArchitectureService { architecture, cost, }: ModifyArchitectureDto) { - const privateArchitecture = - await this.prisma.privateArchitecture.findUnique({ - where: { - id, - authorId, - }, - }); - if (!privateArchitecture) throw new ForbiddenException(); + const isAuthor = await this.isArchitectureAuthor(id, authorId); + if (!isAuthor) throw new ForbiddenException(); return this.prisma.privateArchitecture.update({ where: { id, @@ -77,16 +71,9 @@ export class PrivateArchitectureService { } async removeArchitecture({ id, userId: authorId }: RemoveArchitectureDto) { + const isAuthor = await this.isArchitectureAuthor(id, authorId); + if (!isAuthor) throw new ForbiddenException(); return await this.prisma.$transaction(async (tx) => { - const privateArchitecture = await tx.privateArchitecture.findUnique( - { - where: { - id, - authorId, - }, - }, - ); - if (!privateArchitecture) throw new ForbiddenException(); await tx.version.deleteMany({ where: { privateArchitectureId: id, @@ -102,14 +89,8 @@ export class PrivateArchitectureService { } async findVersions({ id, userId: authorId }: FindVersionsDto) { - const privateArchitecture = - await this.prisma.privateArchitecture.findUnique({ - where: { - id, - authorId, - }, - }); - if (!privateArchitecture) throw new ForbiddenException(); + const isAuthor = await this.isArchitectureAuthor(id, authorId); + if (!isAuthor) throw new ForbiddenException(); return await this.prisma.version.findMany({ select: { id: true, @@ -129,20 +110,11 @@ export class PrivateArchitectureService { architecture, cost, }: SaveVersionDto) { - const privateArchitecture = - await this.prisma.privateArchitecture.findUnique({ - select: { - id: true, - }, - where: { - id, - authorId, - }, - }); - if (!privateArchitecture) throw new ForbiddenException(); + const isAuthor = await this.isArchitectureAuthor(id, authorId); + if (!isAuthor) throw new ForbiddenException(); return this.prisma.version.create({ data: { - privateArchitectureId: privateArchitecture.id, + privateArchitectureId: id, title, architecture, cost, @@ -151,17 +123,8 @@ export class PrivateArchitectureService { } async removeVersion({ userId: authorId, id, versionId }: RemoveVersionDto) { - const privateArchitecture = - await this.prisma.privateArchitecture.findUnique({ - select: { - id: true, - }, - where: { - id, - authorId, - }, - }); - if (!privateArchitecture) throw new ForbiddenException(); + const isAuthor = await this.isArchitectureAuthor(id, authorId); + if (!isAuthor) throw new ForbiddenException(); const privateArchitectureVersion = await this.prisma.version.findUnique( { where: { @@ -178,17 +141,8 @@ export class PrivateArchitectureService { } async findVersion({ userId: authorId, id, versionId }: FindVersionDto) { - const privateArchitecture = - await this.prisma.privateArchitecture.findUnique({ - select: { - id: true, - }, - where: { - id, - authorId, - }, - }); - if (!privateArchitecture) throw new ForbiddenException(); + const isAuthor = await this.isArchitectureAuthor(id, authorId); + if (!isAuthor) throw new ForbiddenException(); const privateArchitectureVersion = await this.prisma.version.findUnique( { select: { @@ -202,4 +156,11 @@ export class PrivateArchitectureService { if (!privateArchitectureVersion) throw new ForbiddenException(); return privateArchitectureVersion; } + + async isArchitectureAuthor(id: number, authorId: number): Promise { + return !!(await this.prisma.privateArchitecture.findUnique({ + select: { id: true }, + where: { id, authorId }, + })); + } } diff --git a/apps/server/src/public-architecture/dto/create-public-architecture.dto.ts b/apps/server/src/public-architecture/dto/create-public-architecture.dto.ts index e1a6b884..9ed0ebac 100644 --- a/apps/server/src/public-architecture/dto/create-public-architecture.dto.ts +++ b/apps/server/src/public-architecture/dto/create-public-architecture.dto.ts @@ -23,5 +23,5 @@ export class CreatePublicArchitectureDto { @IsOptional() @IsArray() @IsString({ each: true }) - tag?: string[]; + tags?: string[]; } diff --git a/apps/server/src/public-architecture/dto/find-architecture.dto.ts b/apps/server/src/public-architecture/dto/find-architecture.dto.ts new file mode 100644 index 00000000..ea7ef58f --- /dev/null +++ b/apps/server/src/public-architecture/dto/find-architecture.dto.ts @@ -0,0 +1,4 @@ +export interface FindArchitectureDto { + id: number; + userId?: number; +} diff --git a/apps/server/src/public-architecture/dto/find-architectures.dto.ts b/apps/server/src/public-architecture/dto/find-architectures.dto.ts new file mode 100644 index 00000000..1dd83366 --- /dev/null +++ b/apps/server/src/public-architecture/dto/find-architectures.dto.ts @@ -0,0 +1,7 @@ +export interface FindArchitecturesDto { + page?: number; + limit?: number; + search?: string; + sort?: string; + order?: string; +} diff --git a/apps/server/src/public-architecture/dto/import.dto.ts b/apps/server/src/public-architecture/dto/import.dto.ts new file mode 100644 index 00000000..500a65dd --- /dev/null +++ b/apps/server/src/public-architecture/dto/import.dto.ts @@ -0,0 +1,4 @@ +export interface ImportDto { + id: number; + userId: number; +} diff --git a/apps/server/src/public-architecture/dto/modify-architecture.dto.ts b/apps/server/src/public-architecture/dto/modify-architecture.dto.ts new file mode 100644 index 00000000..1caa7415 --- /dev/null +++ b/apps/server/src/public-architecture/dto/modify-architecture.dto.ts @@ -0,0 +1,5 @@ +export interface ModifyArchitectureDto { + id: number; + userId: number; + title: string; +} diff --git a/apps/server/src/public-architecture/dto/remove-architecture.dto.ts b/apps/server/src/public-architecture/dto/remove-architecture.dto.ts new file mode 100644 index 00000000..3e77e893 --- /dev/null +++ b/apps/server/src/public-architecture/dto/remove-architecture.dto.ts @@ -0,0 +1,4 @@ +export interface RemoveArchitectureDto { + id: number; + userId: number; +} diff --git a/apps/server/src/public-architecture/dto/save-architecture.dto.ts b/apps/server/src/public-architecture/dto/save-architecture.dto.ts new file mode 100644 index 00000000..8eb2cab6 --- /dev/null +++ b/apps/server/src/public-architecture/dto/save-architecture.dto.ts @@ -0,0 +1,7 @@ +export interface SaveArchitectureDto { + title: string; + architecture: Record; + cost: number; + tags?: string[]; + userId: number; +} diff --git a/apps/server/src/public-architecture/dto/star.dto.ts b/apps/server/src/public-architecture/dto/star.dto.ts new file mode 100644 index 00000000..dda4977a --- /dev/null +++ b/apps/server/src/public-architecture/dto/star.dto.ts @@ -0,0 +1,4 @@ +export interface StarDto { + id: number; + userId: number; +} diff --git a/apps/server/src/public-architecture/dto/unstar.dto.ts b/apps/server/src/public-architecture/dto/unstar.dto.ts new file mode 100644 index 00000000..ebd0245d --- /dev/null +++ b/apps/server/src/public-architecture/dto/unstar.dto.ts @@ -0,0 +1,4 @@ +export interface UnstarDto { + id: number; + userId: number; +} diff --git a/apps/server/src/public-architecture/public-architecture.controller.ts b/apps/server/src/public-architecture/public-architecture.controller.ts index 6c448a8c..e7ed70cc 100644 --- a/apps/server/src/public-architecture/public-architecture.controller.ts +++ b/apps/server/src/public-architecture/public-architecture.controller.ts @@ -10,68 +10,103 @@ import { Query, UseGuards, } from '@nestjs/common'; -import { PublicArchitectureService } from './public-architecture.service.js'; -import { CreatePublicArchitectureDto } from './dto/create-public-architecture.dto.js'; -import { UpdatePublicArchitectureDto } from './dto/update-public-architecture.dto.js'; -import { QueryParamsDto } from '../types/query-params.dto.js'; -import { JwtAuthGuard } from '../guards/jwt-auth.guard.js'; +import { PublicArchitectureService } from './public-architecture.service'; +import { CreatePublicArchitectureDto } from './dto/create-public-architecture.dto'; +import { UpdatePublicArchitectureDto } from './dto/update-public-architecture.dto'; +import { QueryParamsDto } from 'src/types/query-params.dto'; +import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; +import { User } from 'src/decorators/user.decorator'; +import { OptionalAuthGuard } from 'src/guards/optional-auth.guard'; @Controller('public-architectures') export class PublicArchitectureController { constructor(private readonly service: PublicArchitectureService) {} @Get() - getMany(@Query() query: QueryParamsDto) { - return this.service.getMany(query); + getPublicArchitectures(@Query() queryParams: QueryParamsDto) { + return this.service.findArchitectures(queryParams); } @Post() @UseGuards(JwtAuthGuard) - create(@Body() createPublicDto: CreatePublicArchitectureDto) { - const userId = 1; - return this.service.create(userId, createPublicDto); + createPublicArchitecture( + @User('id') userId: number, + @Body() createPublicArchitectureDto: CreatePublicArchitectureDto, + ) { + return this.service.saveArchitecture({ + userId, + ...createPublicArchitectureDto, + }); } @Get(':id') - getOne(@Param('id', ParseIntPipe) id: number) { - return this.service.getOne(id); + @UseGuards(OptionalAuthGuard) + getPublicArchitecture( + @User('id') userId: number, + @Param('id', ParseIntPipe) id: number, + ) { + return this.service.findArchitecture({ id, userId }); } @Patch(':id') @UseGuards(JwtAuthGuard) - update( + updatePublicArchitecture( + @User('id') userId: number, @Param('id', ParseIntPipe) id: number, - @Body() updatePublicDto: UpdatePublicArchitectureDto, + @Body() updatePublicArchitectureDto: UpdatePublicArchitectureDto, ) { - const userId = 1; - return this.service.update(id, userId, updatePublicDto); + return this.service.modifyArchitecture({ + id, + userId, + ...updatePublicArchitectureDto, + }); } @Delete(':id') @UseGuards(JwtAuthGuard) - delete(@Param('id', ParseIntPipe) id: number) { - const userId = 1; - return this.service.delete(id, userId); + deletePublicArchitecture( + @User('id') userId: number, + @Param('id', ParseIntPipe) id: number, + ) { + return this.service.removeArchitecture({ id, userId }); } + // @Get(':id/stars') + // @UseGuards(OptionalAuthGuard) + // async getStars( + // @User('id') userId: number, + // @Param('id', ParseIntPipe) id: number, + // ) { + // if (!userId) return { isStarred: false }; + // const star = await this.service.findStar({ id, userId }); + // if (!star) return { isStarred: false }; + // return { isStarred: true }; + // } + @Post(':id/stars') @UseGuards(JwtAuthGuard) - star(@Param('id', ParseIntPipe) id: number) { - const userId = 1; - return this.service.star(id, userId); + starPublicArchitecture( + @User('id') userId: number, + @Param('id', ParseIntPipe) id: number, + ) { + return this.service.star({ id, userId }); } @Delete(':id/stars') @UseGuards(JwtAuthGuard) - unstar(@Param('id', ParseIntPipe) id: number) { - const userId = 1; - return this.service.unstar(id, userId); + unstarPublicArchitecture( + @User('id') userId: number, + @Param('id', ParseIntPipe) id: number, + ) { + return this.service.unstar({ id, userId }); } @Post(':id/imports') @UseGuards(JwtAuthGuard) - import(@Param('id', ParseIntPipe) id: number) { - const userId = 1; - return this.service.import(id, userId); + importPublicArchitecture( + @User('id') userId: number, + @Param('id', ParseIntPipe) id: number, + ) { + return this.service.import({ id, userId }); } } diff --git a/apps/server/src/public-architecture/public-architecture.module.ts b/apps/server/src/public-architecture/public-architecture.module.ts index 366eb8e3..1cf85520 100644 --- a/apps/server/src/public-architecture/public-architecture.module.ts +++ b/apps/server/src/public-architecture/public-architecture.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { PublicArchitectureService } from './public-architecture.service.js'; -import { PublicArchitectureController } from './public-architecture.controller.js'; -import { PrismaModule } from '../prisma/prisma.module.js'; +import { PublicArchitectureService } from './public-architecture.service'; +import { PublicArchitectureController } from './public-architecture.controller'; +import { PrismaModule } from 'src/prisma/prisma.module'; @Module({ imports: [PrismaModule], diff --git a/apps/server/src/public-architecture/public-architecture.service.ts b/apps/server/src/public-architecture/public-architecture.service.ts index 9e7c7dca..74d7a44b 100644 --- a/apps/server/src/public-architecture/public-architecture.service.ts +++ b/apps/server/src/public-architecture/public-architecture.service.ts @@ -1,62 +1,113 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreatePublicArchitectureDto } from './dto/create-public-architecture.dto.js'; -import { UpdatePublicArchitectureDto } from './dto/update-public-architecture.dto.js'; -import { PrismaService } from '../prisma/prisma.service.js'; -import { QueryParamsDto } from '../types/query-params.dto.js'; +import { Injectable } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { + buildFilterOptions, + buildPaginationOptions, + buildSortOptions, +} from 'src/utils/build-query-options'; +import { FindArchitecturesDto } from './dto/find-architectures.dto'; +import { SaveArchitectureDto } from './dto/save-architecture.dto'; +import { FindArchitectureDto } from './dto/find-architecture.dto'; +import { ModifyArchitectureDto } from './dto/modify-architecture.dto'; +import { RemoveArchitectureDto } from './dto/remove-architecture.dto'; +import { UnstarDto } from './dto/unstar.dto'; +import { StarDto } from './dto/star.dto'; +import { ImportDto } from './dto/import.dto'; @Injectable() export class PublicArchitectureService { constructor(private readonly prisma: PrismaService) {} - async getMany(query: QueryParamsDto) { - const { page, limit, search, sort, order } = query; - - return this.prisma.publicArchitecture.findMany({ - include: { - author: true, - _count: { - select: { - stars: true, - imports: true, + async findArchitectures(queryParams: FindArchitecturesDto) { + const [data, total] = await this.prisma.$transaction([ + this.prisma.publicArchitecture.findMany({ + include: { + author: { + select: { + id: true, + name: true, + }, + }, + tags: { + select: { + tag: { + select: { + name: true, + }, + }, + }, + }, + _count: { + select: { + stars: true, + imports: true, + }, }, }, - }, - skip: (page - 1) * limit, - take: limit, - where: search - ? { - title: { - contains: search, - }, - } - : undefined, - orderBy: sort - ? { - [sort]: order, - } - : { - createdAt: 'desc', - }, - }); + ...buildPaginationOptions(queryParams), + ...buildSortOptions(queryParams), + ...buildFilterOptions(queryParams), + }), + this.prisma.publicArchitecture.count({ + ...buildFilterOptions(queryParams), + }), + ]); + return { data, total }; } - async create(userId: number, dto: CreatePublicArchitectureDto) { - return await this.prisma.publicArchitecture.create({ + saveArchitecture({ + title, + architecture, + cost, + tags, + userId: authorId, + }: SaveArchitectureDto) { + return this.prisma.publicArchitecture.create({ data: { - ...dto, - authorId: userId, - }, - include: { - author: true, + title, + architecture, + cost, + authorId, + tags: { + create: tags.map((name) => ({ + tag: { + connectOrCreate: { + where: { name }, + create: { name }, + }, + }, + })), + }, }, }); } - async getOne(id: number) { - const item = await this.prisma.publicArchitecture.findUnique({ + findArchitecture({ id, userId }: FindArchitectureDto) { + return this.prisma.publicArchitecture.findUniqueOrThrow({ where: { id }, include: { - author: true, + author: { + select: { + id: true, + name: true, + }, + }, + tags: { + select: { + tag: { + select: { + name: true, + }, + }, + }, + }, + stars: userId + ? { + where: { + userId, + }, + } + : undefined, _count: { select: { stars: true, @@ -65,81 +116,89 @@ export class PublicArchitectureService { }, }, }); - - if (!item) { - throw new NotFoundException('Architecture not found'); - } - - return item; } - async update(id: number, userId: number, dto: UpdatePublicArchitectureDto) { - // #TODO check if user is the author + modifyArchitecture({ id, userId: authorId, title }: ModifyArchitectureDto) { return this.prisma.publicArchitecture.update({ - where: { id }, - data: dto, - include: { - author: true, - }, + where: { id, authorId }, + data: { title }, }); } - async delete(id: number, userId: number) { - // #TODO check if user is the author - // #TODO delete all stars and imports - return this.prisma.publicArchitecture.delete({ where: { id } }); + async removeArchitecture({ id, userId: authorId }: RemoveArchitectureDto) { + return this.prisma.$transaction(async (tx) => { + await tx.publicArchitecture.findUniqueOrThrow({ + select: { id: true }, + where: { id, authorId }, + }); + await tx.publicArchitectureTag.deleteMany({ + where: { publicArchitectureId: id }, + }); + await tx.star.deleteMany({ where: { publicArchitectureId: id } }); + await tx.import.deleteMany({ where: { publicArchitectureId: id } }); + return await tx.publicArchitecture.delete({ where: { id } }); + }); } - async star(id: number, userId: number) { - const exists = await this.architectureExists(id); - - if (!exists) { - throw new NotFoundException('Architecture not found'); - } + findStar({ id, userId }: StarDto) { + return this.prisma.star.findUnique({ + where: { + unique_star: { + publicArchitectureId: id, + userId, + }, + }, + }); + } + star({ id, userId }: StarDto) { return this.prisma.star.create({ data: { - userId, publicArchitectureId: id, + userId, }, }); } - async unstar(id: number, userId: number) { - const exists = await this.architectureExists(id); - - if (!exists) { - throw new NotFoundException('Architecture not found'); - } - + unstar({ id, userId }: UnstarDto) { return this.prisma.star.delete({ where: { unique_star: { - userId, publicArchitectureId: id, + userId, }, }, }); } - async import(id: number, userId: number) { - const exists = await this.architectureExists(id); - - if (!exists) { - throw new NotFoundException('Architecture not found'); - } - - return this.prisma.import.create({ - data: { - userId, - publicArchitectureId: id, - }, - }); - } + async import({ id, userId }: ImportDto) { + const { title, architecture, cost } = + await this.prisma.publicArchitecture.findUniqueOrThrow({ + select: { + title: true, + architecture: true, + cost: true, + }, + where: { id }, + }); + + const [privateArchitecture] = await this.prisma.$transaction([ + this.prisma.privateArchitecture.create({ + data: { + title, + architecture, + cost, + authorId: userId, + }, + }), + this.prisma.import.create({ + data: { + publicArchitectureId: id, + userId, + }, + }), + ]); - architectureExists(id: number) { - return this.prisma.publicArchitecture.findUnique({ - where: { id }, - }); + return privateArchitecture; } } diff --git a/apps/server/src/public-architecture/test/public-architecture.service.spec.ts b/apps/server/src/public-architecture/test/public-architecture.service.spec.ts index a3610bae..7bc23247 100644 --- a/apps/server/src/public-architecture/test/public-architecture.service.spec.ts +++ b/apps/server/src/public-architecture/test/public-architecture.service.spec.ts @@ -55,7 +55,7 @@ describe('PublicService', () => { it('should return all architectures', async () => { repository.findAll.mockReturnValue([mockArchitecture]); - const result = await service.getMany(); + const result = await service.findArchitectures(); expect(result).toEqual([mockArchitecture]); expect(repository.findAll).toHaveBeenCalled(); @@ -66,7 +66,7 @@ describe('PublicService', () => { it('should return an architecture by id', async () => { repository.findById.mockReturnValue(mockArchitecture); - const result = await service.getOne(1); + const result = await service.findArchitecture(1); expect(result).toEqual(mockArchitecture); expect(repository.findById).toHaveBeenCalledWith(1); @@ -75,7 +75,9 @@ describe('PublicService', () => { it('should throw NotFoundException when not found', async () => { repository.findById.mockReturnValue(null); - await expect(service.getOne(1)).rejects.toThrow(NotFoundException); + await expect(service.findArchitecture(1)).rejects.toThrow( + NotFoundException, + ); }); }); @@ -90,7 +92,7 @@ describe('PublicService', () => { it('should create an architecture', async () => { repository.create.mockReturnValue(mockArchitecture); - const result = await service.create(1, createDto); + const result = await service.saveArchitecture(1, createDto); expect(result).toEqual(mockArchitecture); expect(repository.create).toHaveBeenCalledWith(1, createDto); @@ -107,7 +109,7 @@ describe('PublicService', () => { ...updateDto, }); - const result = await service.update(1, updateDto); + const result = await service.modifyArchitecture(1, updateDto); expect(result.title).toBe('Updated'); expect(repository.update).toHaveBeenCalledWith(1, updateDto); @@ -116,9 +118,9 @@ describe('PublicService', () => { it('should throw NotFoundException when not found', async () => { repository.findById.mockReturnValue(null); - await expect(service.update(1, updateDto)).rejects.toThrow( - NotFoundException, - ); + await expect( + service.modifyArchitecture(1, updateDto), + ).rejects.toThrow(NotFoundException); }); }); @@ -127,7 +129,7 @@ describe('PublicService', () => { repository.findById.mockReturnValue(mockArchitecture); repository.delete.mockReturnValue(mockArchitecture); - const result = await service.delete(1); + const result = await service.removeArchitecture(1); expect(result).toEqual(mockArchitecture); expect(repository.delete).toHaveBeenCalledWith(1); diff --git a/apps/server/src/swagger/swagger.config.ts b/apps/server/src/swagger/swagger.config.ts index 48166268..536b5671 100644 --- a/apps/server/src/swagger/swagger.config.ts +++ b/apps/server/src/swagger/swagger.config.ts @@ -1,5 +1,5 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { SWAGGER_README } from './swagger.readme.js'; +import { SWAGGER_README } from './swagger.readme'; export const swaggerConfig = (app) => { const config = new DocumentBuilder() diff --git a/apps/server/src/user/dto/update-user.dto.ts b/apps/server/src/user/dto/update-user.dto.ts index 912cdc52..dfd37fb1 100644 --- a/apps/server/src/user/dto/update-user.dto.ts +++ b/apps/server/src/user/dto/update-user.dto.ts @@ -1,4 +1,4 @@ import { PartialType } from '@nestjs/mapped-types'; -import { CreateUserDto } from './create-user.dto.js'; +import { CreateUserDto } from './create-user.dto'; export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/apps/server/src/user/user.controller.ts b/apps/server/src/user/user.controller.ts index e3a2086b..3a1782cc 100644 --- a/apps/server/src/user/user.controller.ts +++ b/apps/server/src/user/user.controller.ts @@ -7,9 +7,9 @@ import { Param, Delete, } from '@nestjs/common'; -import { UserService } from './user.service.js'; -import { CreateUserDto } from './dto/create-user.dto.js'; -import { UpdateUserDto } from './dto/update-user.dto.js'; +import { UserService } from './user.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; @Controller('user') export class UserController { diff --git a/apps/server/src/user/user.module.ts b/apps/server/src/user/user.module.ts index c5b585a1..cfe500d0 100644 --- a/apps/server/src/user/user.module.ts +++ b/apps/server/src/user/user.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { UserService } from './user.service.js'; -import { UserController } from './user.controller.js'; +import { UserService } from './user.service'; +import { UserController } from './user.controller'; @Module({ controllers: [UserController], diff --git a/apps/server/src/user/user.service.ts b/apps/server/src/user/user.service.ts index ded37f85..b226f14f 100644 --- a/apps/server/src/user/user.service.ts +++ b/apps/server/src/user/user.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { CreateUserDto } from './dto/create-user.dto.js'; -import { UpdateUserDto } from './dto/update-user.dto.js'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; @Injectable() export class UserService { diff --git a/apps/server/src/utils/build-query-options.ts b/apps/server/src/utils/build-query-options.ts index 57d6c139..48707a64 100644 --- a/apps/server/src/utils/build-query-options.ts +++ b/apps/server/src/utils/build-query-options.ts @@ -1,16 +1,44 @@ -import { QueryParamsDto } from 'src/types/query-params.dto.js'; +import { QueryParamsDto } from 'src/types/query-params.dto'; -export const buildQueryOptions = ({ - page, - limit, - search, - sort, - order, - userId, -}: QueryParamsDto & { userId?: number }) => { +export const buildPaginationOptions = ({ page, limit }: QueryParamsDto) => { return { skip: (page - 1) * limit, take: limit, + } as any; +}; + +export const buildSortOptions = ({ sort, order }: QueryParamsDto) => { + if (sort === 'name') { + return { + orderBy: { + title: order, + }, + }; + } else if (sort === 'cost') { + return { + orderBy: { + cost: order, + }, + }; + } else if (sort === 'stars' || sort === 'imports') { + return { + orderBy: { + [sort]: { + _count: order, + }, + }, + }; + } +}; + +export const buildFilterOptions = ({ + search, + userId, +}: { + search?: string; + userId?: number; +}) => { + return { where: search || userId ? { @@ -20,12 +48,5 @@ export const buildQueryOptions = ({ authorId: userId, } : undefined, - orderBy: sort - ? { - [sort]: order, - } - : { - createdAt: 'desc', - }, - } as any; + }; }; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 33fd272a..dd264f7e 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -1,8 +1,6 @@ { "compilerOptions": { - "module": "Node16", - "moduleResolution": "node16", - "esModuleInterop": true, + "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, diff --git a/docker-composes/cloud-canvas-back.yml b/docker-composes/cloud-canvas-back.yml index 5d7148ea..95535f4c 100644 --- a/docker-composes/cloud-canvas-back.yml +++ b/docker-composes/cloud-canvas-back.yml @@ -3,12 +3,15 @@ services: image: t84ar7xr.kr.private-ncr.ntruss.com/back:dev container_name: back environment: + NODE_ENV: production DATABASE_URL: ${DATABASE_URL} + NCLOUD_ACCESS_KEY: ${NCLOUD_ACCESS_KEY} + NCLOUD_SECRET_KEY: ${NCLOUD_SECRET_KEY} REDIS_HOST: ${REDIS_HOST} REDIS_PORT: ${REDIS_PORT} ports: - '3000:3000' - entrypoint: sh -c "npx prisma generate && npx prisma migrate reset --force && node ./dist/src/main.js" + entrypoint: sh -c "cd apps/server && npx prisma migrate reset --force && node ./dist/src/main.js" networks: - cloud-canvas-network restart: unless-stopped diff --git a/docker-composes/cloud-canvas-front-hub.yml b/docker-composes/cloud-canvas-front-hub.yml new file mode 100644 index 00000000..df9d9107 --- /dev/null +++ b/docker-composes/cloud-canvas-front-hub.yml @@ -0,0 +1,15 @@ +services: + front-hub: + image: cloud-canvas.kr.ncr.ntruss.com/front-hub:dev + container_name: front-hub + environment: + BACK_URL: ${BACK_URL} + ports: + - '3000:3000' + networks: + - cloud-canvas-network + restart: unless-stopped + pull_policy: always +networks: + cloud-canvas-network: + driver: bridge diff --git a/docker-composes/cloud-canvas-local.yml b/docker-composes/cloud-canvas-local.yml index ef19377b..4b209bd5 100644 --- a/docker-composes/cloud-canvas-local.yml +++ b/docker-composes/cloud-canvas-local.yml @@ -25,7 +25,6 @@ services: container_name: mysql environment: MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_DATABASE: cloud_canvas MYSQL_USER: cloud_canvas_user MYSQL_PASSWORD: password ports: @@ -65,6 +64,8 @@ services: container_name: back environment: DATABASE_URL: mysql://cloud_canvas_user:password@mysql:3306/cloud_canvas + NCLOUD_ACCESS_KEY: ${NCLOUD_ACCESS_KEY} + NCLOUD_SECRET_KEY: ${NCLOUD_SECRET_KEY} ports: - '3000:3000' depends_on: @@ -72,7 +73,7 @@ services: condition: service_healthy redis: condition: service_healthy - entrypoint: sh -c "npx prisma migrate deploy && npx prisma db seed && node ./dist/src/main.js" + entrypoint: sh -c "cd apps/server && npx prisma migrate dev && node ./dist/src/main.js" networks: - cloud-canvas-network restart: unless-stopped @@ -89,10 +90,10 @@ services: restart: unless-stopped fluentd: - build: ./logging/fluentd/ + build: ./monitorin/logging/fluentd/ container_name: fluentd volumes: - - ./logging/fluentd/conf/fluent.conf:/fluentd/etc/fluent.conf + - ./monitoring/logging/fluentd/conf/fluent.conf:/fluentd/etc/fluent.conf ports: - '24224:24224' - '24224:24224/udp' diff --git a/infra/modules/server/outputs.tf b/infra/modules/server/outputs.tf index 88b33bfa..603fd5ca 100644 --- a/infra/modules/server/outputs.tf +++ b/infra/modules/server/outputs.tf @@ -1,4 +1,9 @@ -output "servers_login_key" { - value = ncloud_login_key.servers_login_key.private_key - sensitive = true +output "server_publics" { + value = ncloud_server.public_servers[*] + description = "public server infos" +} + +output "server_privates" { + value = ncloud_server.private_servers[*] + description = "private server infos" } \ No newline at end of file diff --git a/infra/modules/vpc_subnet/outputs.tf b/infra/modules/vpc_subnet/outputs.tf index 35ea1168..01e6c8e3 100644 --- a/infra/modules/vpc_subnet/outputs.tf +++ b/infra/modules/vpc_subnet/outputs.tf @@ -4,11 +4,11 @@ output "vpc_id" { } output "public_subnets" { - description = "public subnets id" + description = "public subnets infos" value = ncloud_subnet.public_subnets[*] } output "private_subnets" { - description = "public subnets id" + description = "public subnets infos" value = ncloud_subnet.private_subnets[*] } \ No newline at end of file diff --git a/packages/ncloud-sdk/package.json b/packages/ncloud-sdk/package.json index a8723f8f..bb89ec0a 100644 --- a/packages/ncloud-sdk/package.json +++ b/packages/ncloud-sdk/package.json @@ -4,12 +4,6 @@ "description": "", "private": true, "keywords": [], - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./dist/index.js" - } - }, "scripts": { "start": "cross-env NODE_ENV=development node dist/index.js", "dev": "tsx watch src/index.ts", @@ -29,5 +23,18 @@ "dotenv": "^16.4.5", "tsup": "^8.3.5", "tsx": "^4.19.2" + }, + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } } } diff --git a/packages/ncloud-sdk/src/services/price/PriceApi.ts b/packages/ncloud-sdk/src/services/price/PriceApi.ts index 3ba3930b..7a761626 100644 --- a/packages/ncloud-sdk/src/services/price/PriceApi.ts +++ b/packages/ncloud-sdk/src/services/price/PriceApi.ts @@ -18,38 +18,42 @@ export class PriceApi { } async getPriceList(getPriceRequest: GetPriceListRequest) { - return await this.client.request({ + const response = await this.client.request({ method: 'POST', url: this.resourcePath + '/product/getPriceList', params: getPriceRequest, }); + return response.getPriceListResponse; } async getProductCategoryList( getProductCategoryListRequest: GetProductCategoryListRequest, ) { - return await this.client.request({ + const response = await this.client.request({ method: 'POST', url: this.resourcePath + '/product/getProductCategoryList', params: getProductCategoryListRequest, }); + return response.getProductCategoryListResponse; } async getProductList(getProductListRequest: GetProductListRequest) { - return await this.client.request({ + const response = await this.client.request({ method: 'POST', url: this.resourcePath + '/product/getProductList', params: getProductListRequest, }); + return response.getProductListResponse; } async getProductPriceList( getProductPriceListRequest: GetProductPriceListRequest, ) { - return await this.client.request({ + const response = await this.client.request({ method: 'POST', url: this.resourcePath + '/product/getProductPriceList', params: getProductPriceListRequest, }); + return response.getProductPriceListResponse; } } diff --git a/packages/ncloud-sdk/src/signature.ts b/packages/ncloud-sdk/src/signature.ts index 9a365db2..13239abe 100644 --- a/packages/ncloud-sdk/src/signature.ts +++ b/packages/ncloud-sdk/src/signature.ts @@ -19,7 +19,6 @@ export function generateSignature({ ? `${url}?${new URLSearchParams(params).toString()}` : url; - console.log(fullUrl); const message = [ method, space, diff --git a/packages/ncloud-sdk/tsconfig.json b/packages/ncloud-sdk/tsconfig.json deleted file mode 100644 index 58feafa6..00000000 --- a/packages/ncloud-sdk/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "module": "CommonJS", - "target": "ES2020" - } -} diff --git a/packages/terraform/util/resource.ts b/packages/terraform/util/resource.ts index 293965c3..30480b98 100644 --- a/packages/terraform/util/resource.ts +++ b/packages/terraform/util/resource.ts @@ -10,7 +10,8 @@ import { export const processDependencies = (node: any): any[] => { if ( - !['server', 'loadbalancer', 'mysql', 'redis', 'nkscluster'].includes( + + !['server', 'loadbalancer', 'db-mysql', 'redis'].includes( node.type.toLowerCase(), ) ) @@ -42,8 +43,8 @@ export const processDependencies = (node: any): any[] => { return dependencies; }; -export const processNodes = (nodes: any[]): any[] => - nodes.reduce( +export const processNodes = (nodes: any[]): any[] => { + return nodes.reduce( (acc: CloudCanvasNode[], node) => [ ...acc, ...processDependencies(node), @@ -51,3 +52,4 @@ export const processNodes = (nodes: any[]): any[] => ], [], ); +}; diff --git a/packages/terraform/util/resourceParser.ts b/packages/terraform/util/resourceParser.ts index cee23b04..08775324 100644 --- a/packages/terraform/util/resourceParser.ts +++ b/packages/terraform/util/resourceParser.ts @@ -16,24 +16,24 @@ import { NCloudRedis } from '../model/NCloudRedis'; import { NCloudNKsCluster } from '../model/NCloudNKsCluster'; export function parseToNCloudModel(resource: any): NCloudModel { - const { type, name, properties } = resource; + const { type, properties } = resource; switch (type.toLowerCase()) { case 'vpc': return new NCloudVPC({ - name: name || 'vpc', + name: properties.name || 'vpc', ipv4CidrBlock: properties.cidrBlock, }); case 'networkacl': return new NCloudNetworkACL({ - name: name || 'nacl', + name: properties.name || 'nacl', vpcName: properties.vpcName, }); case 'subnet': return new NCloudSubnet({ - name: name || 'subnet', + name: properties.name || 'subnet', subnet: properties.subnet, zone: properties.zone, subnetType: properties.subnetType, @@ -45,7 +45,7 @@ export function parseToNCloudModel(resource: any): NCloudModel { case 'acg': case 'accesscontrolgroup': return new NCloudACG({ - name: name || 'acg', + name: properties.name || 'acg', description: properties.description, vpcName: properties.vpcName, }); @@ -63,19 +63,19 @@ export function parseToNCloudModel(resource: any): NCloudModel { case 'loginkey': return new NCloudLoginKey({ - name: name || 'login-key', + name: properties.name || 'login-key', }); case 'networkinterface': return new NCloudNetworkInterface({ - name: name || 'nic', + name: properties.name || 'nic', subnetName: properties.subnetName, acgName: properties.acgName, }); case 'server': return new NCloudServer({ - name: name || 'server', + name: properties.name || 'server', serverImageNumber: properties.server_image_number, serverSpecCode: properties.server_spec_code, subnetName: properties.subnet, @@ -83,14 +83,14 @@ export function parseToNCloudModel(resource: any): NCloudModel { case 'publicip': return new NCloudPublicIP({ - name: name || 'public-ip', + name: properties.name || 'public-ip', description: properties.description, serverName: properties.serverName, }); case 'loadbalancer': return new NCloudLoadBalancer({ - name: name || 'load-balancer', + name: properties.name || 'load-balancer', networkType: properties.networkType, type: properties.type, subnetName: properties.subnet, @@ -99,12 +99,12 @@ export function parseToNCloudModel(resource: any): NCloudModel { case 'launchconfiguration': return new NCloudLaunchConfiguration({ - name: name || 'launch-config', + name: properties.name || 'launch-config', serverImageProductCode: properties.serverImageProductCode, serverProductCode: properties.serverProductCode, }); - case 'mysql': + case 'db-mysql': if ( !properties.serverNamePrefix || !properties.userName || @@ -117,7 +117,7 @@ export function parseToNCloudModel(resource: any): NCloudModel { ); } return new NCloudMySQL({ - serviceName: name || 'mysql', + serviceName: properties.serviceName || 'mysql', serverNamePrefix: properties.serverNamePrefix, userName: properties.userName, userPassword: properties.userPassword, @@ -127,14 +127,14 @@ export function parseToNCloudModel(resource: any): NCloudModel { vpc: properties.vpc, }); - case 'objectstoragebucket': + case 'object-storage': return new NCloudObjectStorageBucket({ bucketName: properties.bucketName, }); case 'redis': return new NCloudRedis({ - serviceName: name || 'redis', + serviceName: properties.serviceName || 'redis', serverNamePrefix: properties.serverNamePrefix, vpcNo: properties.vpc, subnetNo: properties.subnet, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce51f364..007e1069 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@mui/material': specifier: ^6.1.5 version: 6.1.6(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/validator': + specifier: ^13.12.2 + version: 13.12.2 nanoid: specifier: ^5.0.8 version: 5.0.8 @@ -56,6 +59,24 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^7.0.1 + version: 7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-select: + specifier: ^5.8.3 + version: 5.8.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-syntax-highlighter: + specifier: ^15.6.1 + version: 15.6.1(react@18.3.1) + react-type-animation: + specifier: ^3.2.0 + version: 3.2.0(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + terraform: + specifier: file:../../packages/terraform + version: link:../../packages/terraform + validator: + specifier: ^13.12.0 + version: 13.12.0 devDependencies: '@types/react': specifier: ^18.3.11 @@ -63,6 +84,9 @@ importers: '@types/react-dom': specifier: ^18.3.1 version: 18.3.1 + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 '@vitejs/plugin-react': specifier: ^4.3.3 version: 4.3.3(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) @@ -129,7 +153,7 @@ importers: version: 9.2.0 '@nestjs-modules/ioredis': specifier: ^2.0.2 - version: 2.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@prisma/client@5.22.0(prisma@5.22.0))(ioredis@5.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 2.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(ioredis@5.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/common': specifier: ^10.0.0 version: 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -151,9 +175,12 @@ importers: '@nestjs/platform-express': specifier: ^10.0.0 version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7) + '@nestjs/schedule': + specifier: ^4.1.1 + version: 4.1.1(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/swagger': specifier: ^8.0.5 - version: 8.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 8.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@prisma/client': specifier: ^5.22.0 version: 5.22.0(prisma@5.22.0) @@ -193,7 +220,7 @@ importers: version: 10.2.3(chokidar@3.6.0)(typescript@5.6.3) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7) + version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)) '@swc/core': specifier: ^1.9.1 version: 1.9.2(@swc/helpers@0.5.13) @@ -1191,6 +1218,15 @@ packages: resolution: {integrity: sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + + '@floating-ui/dom@1.6.12': + resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==} + + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1608,6 +1644,12 @@ packages: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 + '@nestjs/schedule@4.1.1': + resolution: {integrity: sha512-VxAnCiU4HP0wWw8IdWAVfsGC/FGjyToNjjUtXDEQL6oj+w/N5QDd2VT9k6d7Jbr8PlZuBZNdWtDKSkH5bZ+RXQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/schematics@10.2.3': resolution: {integrity: sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==} peerDependencies: @@ -2032,6 +2074,9 @@ packages: '@types/cookie-parser@1.4.7': resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} @@ -2056,6 +2101,9 @@ packages: '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + '@types/http-errors@2.0.4': resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} @@ -2080,6 +2128,9 @@ packages: '@types/jsonwebtoken@9.0.5': resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -2119,6 +2170,9 @@ packages: '@types/react-dom@18.3.1': resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + '@types/react-syntax-highlighter@15.5.13': + resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} + '@types/react-transition-group@4.4.11': resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} @@ -2143,6 +2197,9 @@ packages: '@types/supertest@6.0.2': resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/validator@13.12.2': resolution: {integrity: sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==} @@ -2682,6 +2739,15 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -2818,6 +2884,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -2899,6 +2968,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -2947,6 +3020,9 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cron@3.1.7: + resolution: {integrity: sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==} + cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -3489,6 +3565,9 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -3579,6 +3658,10 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} @@ -3763,6 +3846,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-parse-selector@2.2.5: + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + + hastscript@6.0.0: + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + helmet@8.0.0: resolution: {integrity: sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==} engines: {node: '>=18.0.0'} @@ -3775,6 +3864,12 @@ packages: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + highlightjs-vue@1.0.0: + resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -3874,6 +3969,12 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -3918,6 +4019,9 @@ packages: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -3963,6 +4067,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -4453,6 +4560,9 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4462,6 +4572,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -4490,6 +4604,9 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -4811,6 +4928,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -5014,6 +5134,14 @@ packages: engines: {node: '>=16.13'} hasBin: true + prismjs@1.27.0: + resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} + engines: {node: '>=6'} + + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -5024,6 +5152,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5087,12 +5218,47 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react-router-dom@7.0.1: + resolution: {integrity: sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.0.1: + resolution: {integrity: sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-select@5.8.3: + resolution: {integrity: sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-syntax-highlighter@15.6.1: + resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==} + peerDependencies: + react: '>= 0.14.0' + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: react: '>=16.6.0' react-dom: '>=16.6.0' + react-type-animation@3.2.0: + resolution: {integrity: sha512-WXTe0i3rRNKjmggPvT5ntye1QBt0ATGbijeW6V3cQe2W0jaMABXXlPPEdtofnS9tM7wSRHchEvI9SUw+0kUohw==} + peerDependencies: + prop-types: ^15.5.4 + react: '>= 15.0.0' + react-dom: '>= 15.0.0' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -5138,6 +5304,9 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} + refractor@3.6.0: + resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -5306,6 +5475,9 @@ packages: engines: {node: '>= 14'} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5395,6 +5567,9 @@ packages: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} @@ -5814,6 +5989,9 @@ packages: cpu: [arm64] os: [linux] + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + turbo-windows-64@2.2.3: resolution: {integrity: sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ==} cpu: [x64] @@ -5924,6 +6102,15 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-isomorphic-layout-effect@1.1.2: + resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -5940,6 +6127,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -6929,6 +7120,17 @@ snapshots: '@faker-js/faker@9.2.0': {} + '@floating-ui/core@1.6.8': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.12': + dependencies: + '@floating-ui/core': 1.6.8 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/utils@0.2.8': {} + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -7335,13 +7537,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@nestjs-modules/ioredis@2.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@prisma/client@5.22.0(prisma@5.22.0))(ioredis@5.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs-modules/ioredis@2.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(ioredis@5.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) ioredis: 5.4.1 optionalDependencies: - '@nestjs/terminus': 10.2.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@prisma/client@5.22.0(prisma@5.22.0))(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/terminus': 10.2.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(reflect-metadata@0.2.2)(rxjs@7.8.1) transitivePeerDependencies: - '@grpc/grpc-js' - '@grpc/proto-loader' @@ -7453,6 +7655,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/schedule@4.1.1(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + cron: 3.1.7 + uuid: 10.0.0 + '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.6.3)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -7464,7 +7673,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@8.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + '@nestjs/swagger@8.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.15.0 '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7479,7 +7688,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.1 - '@nestjs/terminus@10.2.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@prisma/client@5.22.0(prisma@5.22.0))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/terminus@10.2.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7491,7 +7700,7 @@ snapshots: '@prisma/client': 5.22.0(prisma@5.22.0) optional: true - '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)': + '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))': dependencies: '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7765,6 +7974,8 @@ snapshots: dependencies: '@types/express': 4.17.21 + '@types/cookie@0.6.0': {} + '@types/cookiejar@2.1.5': {} '@types/crypto-js@4.2.2': {} @@ -7799,6 +8010,10 @@ snapshots: dependencies: '@types/node': 22.9.0 + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + '@types/http-errors@2.0.4': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -7824,6 +8039,8 @@ snapshots: dependencies: '@types/node': 22.9.0 + '@types/luxon@3.4.2': {} + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} @@ -7864,6 +8081,10 @@ snapshots: dependencies: '@types/react': 18.3.12 + '@types/react-syntax-highlighter@15.5.13': + dependencies: + '@types/react': 18.3.12 + '@types/react-transition-group@4.4.11': dependencies: '@types/react': 18.3.12 @@ -7900,6 +8121,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/unist@2.0.11': {} + '@types/validator@13.12.2': {} '@types/yargs-parser@21.0.3': {} @@ -8574,6 +8797,12 @@ snapshots: char-regex@1.0.2: {} + character-entities-legacy@1.1.4: {} + + character-entities@1.2.4: {} + + character-reference-invalid@1.1.4: {} + chardet@0.7.0: {} check-disk-space@3.4.0: @@ -8701,6 +8930,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@1.0.8: {} + commander@12.1.0: {} commander@2.20.3: {} @@ -8791,6 +9022,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.0.2: {} + cookiejar@2.1.4: {} core-util-is@1.0.3: {} @@ -8852,6 +9085,11 @@ snapshots: create-require@1.1.1: {} + cron@3.1.7: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.4.4 + cross-env@7.0.3: dependencies: cross-spawn: 7.0.5 @@ -9259,8 +9497,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0(eslint@8.57.1) @@ -9283,37 +9521,37 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -9324,7 +9562,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -9603,6 +9841,10 @@ snapshots: dependencies: reusify: 1.0.4 + fault@1.0.4: + dependencies: + format: 0.2.2 + fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -9713,6 +9955,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + format@0.2.2: {} + formidable@2.1.2: dependencies: dezalgo: 1.0.4 @@ -9913,12 +10157,26 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-parse-selector@2.2.5: {} + + hastscript@6.0.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 1.0.8 + hast-util-parse-selector: 2.2.5 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + helmet@8.0.0: {} hexoid@1.0.0: {} hexoid@2.0.0: {} + highlight.js@10.7.3: {} + + highlightjs-vue@1.0.0: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -10072,6 +10330,13 @@ snapshots: ipaddr.js@1.9.1: {} + is-alphabetical@1.0.4: {} + + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -10117,6 +10382,8 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-decimal@1.0.4: {} + is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -10147,6 +10414,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@1.0.4: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -10814,6 +11083,11 @@ snapshots: loupe@3.1.2: {} + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + lru-cache@10.4.3: {} lru-cache@4.1.5: @@ -10825,6 +11099,8 @@ snapshots: dependencies: yallist: 3.1.1 + luxon@3.4.4: {} + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -10855,6 +11131,8 @@ snapshots: dependencies: fs-monkey: 1.0.6 + memoize-one@6.0.0: {} + merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} @@ -11141,6 +11419,15 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@2.0.0: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.26.2 @@ -11293,6 +11580,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + prismjs@1.27.0: {} + + prismjs@1.29.0: {} + process-nextick-args@2.0.1: {} prompts@2.4.2: @@ -11306,6 +11597,10 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@5.6.0: + dependencies: + xtend: 4.0.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11364,6 +11659,49 @@ snapshots: react-refresh@0.14.2: {} + react-router-dom@7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router@7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 18.3.1 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-select@5.8.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + '@emotion/cache': 11.13.1 + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) + '@floating-ui/dom': 1.6.12 + '@types/react-transition-group': 4.4.11 + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.12)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-syntax-highlighter@15.6.1(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.29.0 + react: 18.3.1 + refractor: 3.6.0 + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.0 @@ -11373,6 +11711,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-type-animation@3.2.0(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -11430,6 +11774,12 @@ snapshots: globalthis: 1.0.4 which-builtin-type: 1.1.4 + refractor@3.6.0: + dependencies: + hastscript: 6.0.0 + parse-entities: 2.0.0 + prismjs: 1.27.0 + regenerator-runtime@0.14.1: {} regexp.prototype.flags@1.5.3: @@ -11644,6 +11994,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -11755,6 +12107,8 @@ snapshots: dependencies: whatwg-url: 7.1.0 + space-separated-tokens@1.1.5: {} + spawndamnit@2.0.0: dependencies: cross-spawn: 5.1.0 @@ -12236,6 +12590,8 @@ snapshots: turbo-linux-arm64@2.2.3: optional: true + turbo-stream@2.4.0: {} + turbo-windows-64@2.2.3: optional: true @@ -12355,6 +12711,12 @@ snapshots: dependencies: punycode: 2.3.1 + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + use-sync-external-store@1.2.2(react@19.0.0-rc-66855b96-20241106): dependencies: react: 19.0.0-rc-66855b96-20241106 @@ -12367,6 +12729,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: