Skip to content

Commit

Permalink
Merge branch 'development' into feat/client/#175
Browse files Browse the repository at this point in the history
  • Loading branch information
p1n9d3v authored Dec 3, 2024
2 parents 07f2e69 + f823e3e commit 8c14bf4
Show file tree
Hide file tree
Showing 22 changed files with 475 additions and 129 deletions.
178 changes: 124 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,166 @@
<p align="middle" >
<img src="https://github.com/user-attachments/assets/10ca29c8-f363-431e-8e80-4ac5e869745b" alt="Cloud Canvas" width="300" height="300"/>
<img width="" alt="cicd" src="https://github.com/user-attachments/assets/a01828bf-7603-4262-9761-6a44b361c2bd">
</p>

<h2 align="center">🎨 Cloud Canvas 🎨</h2>
<p align="middle">쉽고 빠르게, 누구나 클라우드를 설계하는 즐거운 경험을!</p>
<p align="middle">쉽고 빠르게, 누구나 클라우드를 설계하는 즐거운 경험을!(배너로 대체 예정)</p>

## **Cloud Canvas란?**
<div align=center>
<a href="https://hits.seeyoufarm.com">
<img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fboostcampwm-2024%2Fweb37-cloud-canvas&count_bg=%2390EE90&title_bg=%2332CD32&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false"/>
</a>
</div>

Cloud Canvas는 클라우드 인프라 설계를 **그래픽 인터페이스**로 간단하고 직관적으로 할 수 있는 **혁신적인 도구**입니다.
국내 클라우드 플랫폼을 적극 지원하며, **Terraform 코드 변환**까지 가능한 **올인원 인프라 관리 플랫폼**입니다.
## Cloud Canvas ✨

> **국내 클라우드에도 이런 도구가 필요하지 않으셨나요?**
> Cloud Canvas와 함께 **더 쉽고, 더 빠르고, 더 즐겁게** 클라우드 인프라를 설계하세요!
Cloud Canvas는 클라우드 인프라를 **그래픽 인터페이스**로 손쉽게 설계하고, 이를 **Terraform 코드**로 자동 변환할 수 있는 혁신적인 도구입니다. 국내 클라우드 플랫폼을 지원하며, 사용자가 **직관적으로** 인프라를 설계하고 **빠르게 배포**할 수 있도록 돕습니다.

---
## 🌟 **주요 기능**

## **프로젝트 비전** 🚀
- **🎨 직관적인 UI/UX**
클릭 몇 번으로 누구나 쉽게 클라우드 인프라 설계!

### 왜 Cloud Canvas인가?
- **💻 Terraform 코드 변환**
설계한 인프라를 자동으로 Terraform 코드로 변환하여 다운로드 가능!

**개발자들의 공통된 고민**
- **🤝 협업 및 재활용**
**인프라 허브**를 통해 다른 사용자들과 설계도를 공유하고 수정하며 효율적으로 협업!

- 반복되는 수작업으로 클라우드 리소스를 관리해야 하는 불편함.
- 클라우드 플랫폼의 복잡한 인터페이스에 적응하는 데 필요한 시간과 노력.
- 국내 클라우드 서비스에 적합한 툴 부재.
## 기능 시연

**우리의 해답**
### GUI를 통한 인프라 설계

- **Cloud Canvas는 국내 클라우드 플랫폼을 목표로 하는 GUI 기반 도구**입니다.
클릭 몇 번으로 완성되는 설계부터 Terraform 코드 변환까지, Cloud Canvas가 여러분의 시간을 아껴드립니다.
시나리오

---
1. 허브 페이지에서 헤더에 있는 새 캔버스 버튼을 눌러 캔버스 페이지로 이동한다.
2. 간단한 인프라를 설계한다.

## **프로젝트 목표** 🎯
### 테라폼 코드 변환

### 🖌️ **직관적인 UX/UI**
시나리오

- 누구나 쉽게 클라우드 인프라를 설계할 수 있는 **직관적이고 세련된 인터페이스** 제공.
1. 캔버스 페이지에 완성된 인프라 아키텍처가 존재한다.
2. 캔버스 페이지 우상단에 있는 convertor 버튼을 누르면 현재 설계된 인프라를 바탕으로 변환된 테라폼 코드가 나온다.
3. 테라폼 코드를 통해 배포된 인프라를 확인한다.

### 🔧 **자동화된 Terraform 코드 변환**
### 인프라 아키텍처 허브 업로드(프라이빗)

- 설계된 인프라를 **Terraform 코드로 변환**하여 다운로드 가능.
시나리오

### 🌐 **인프라 허브 기능**
1. 인프라 아키텍처를 완성했다고 가정하고 캔버스 페이지에서 저장 버튼을 누른다.(/canvas)
2. 저장이 완료되면, 새로고침 되며 발급받은 프라이빗 아키텍처를 parameter 붙여 private architecture 캔버스 페이지로 이동한다.(/canvas/private-architecutes/{id})
3. 해당 아키텍처가 캔버스에 다시 불러와진 것을 확인하면 허브 페이지로 이동한다.(/로 이동)
4. 허브 페이지에서 마이페이지로 이동하고 프라이빗 아키텍처 목록에서 새로운 목록이 추가된 것을 확인하고 클릭한다.
5. 새로 추가된 프라이빗 아키텍처 목록을 클릭하면 다시 캔버스 페이지로 이동한다.

- 설계한 인프라를 **공유하고 재활용**할 수 있는 커뮤니티 제공.
- 다른 사용자의 인프라를 **수정, 활용**하여 자신만의 설계를 완성.
### 인프라 아키텍처 허브 업로드(퍼블릭)

---
시나리오

## **Cloud Canvas의 기대 효과** 🌟
1. 인프라 아키텍처를 완성했다고 가정하고 캔버스 페이지에서 저장 버튼을 누른다.(/canvas)
2. 저장이 완료되면, 새로고침 되며 발급받은 프라이빗 아키텍처를 parameter 붙여 private architecture 캔버스 페이지로 이동한다.(/canvas/private-architecutes/{id})
3. 해당 아키텍처가 캔버스에 다시 불러와진 것을 확인하면 허브 페이지로 이동한다.(/로 이동)
4. 허브 페이지에서 마이페이지로 이동하고 프라이빗 아키텍처 목록에서 새로운 목록이 추가된 것을 확인하고 클릭한다.
5. 새로 추가된 프라이빗 아키텍처 목록을 클릭하면 다시 캔버스 페이지로 이동한다.

1. **반복 작업 최소화**
- GUI로 빠르고 효율적인 설계 가능.
2. **국내 클라우드 생태계 활성화**
- AWS 사용자들이 국내 클라우드 플랫폼으로 쉽게 유입.
3. **한국 클라우드 산업 경쟁력 강화**
- 글로벌 시장에서도 경쟁 가능한 혁신적 도구.
### 인프라 아키텍처 허브 임포트

---
시나리오

1. 허브 페이지에서 아무 인프라 아키텍처 목록을 클릭한다.
2. 퍼블릭 인프라 아키텍처 상세 페이지로 이동하고 임포트 버튼을 클릭한다.
3. 임포트가 완료되면 캔버스 페이지로 리다이렉트 하고 임포트 되어 해당 아키텍처가 캔버스로 그려진 것을 확인한다.
4. 마이페이지로 들어가서, 임포트한 퍼블릭 인프라 아키텍처가 임포트 목록에 추가된 것을 확인한다.

## **기술 스택** 🛠
<div align="center">

### 📌 **Frontend**
## 🚀 기술 스택

- **React 18.3.1**
- **Vite 5.4.9**
### 💻 Common

### 📌 **Backend**
<p>
<img src="https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black" alt="JavaScript"/>
<img src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript"/>
<img src="https://img.shields.io/badge/Prettier-F7B93E?style=for-the-badge&logo=prettier&logoColor=black" alt="Prettier"/>
<img src="https://img.shields.io/badge/ESLint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white" alt="ESLint"/>
<img src="https://img.shields.io/badge/PNPM-F69220?style=for-the-badge&logo=pnpm&logoColor=white" alt="PNPM"/>
<img src="https://img.shields.io/badge/TSUP-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TSUP"/>
</p>

### 🖥️ Frontend

<p>
<img src="https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white" alt="Next.js"/>
<img src="https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black" alt="React"/>
<img src="https://img.shields.io/badge/Tailwind%20CSS-06B6D4?style=for-the-badge&logo=tailwindcss&logoColor=white" alt="Tailwind CSS"/>
<img src="https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=vite&logoColor=white" alt="Vite"/>
<img src="https://img.shields.io/badge/TanStack%20Query-FF4154?style=for-the-badge&logo=reactquery&logoColor=white" alt="TanStack Query"/>
</p>

- **TypeScript 5.1.3**
- **NestJS 10.0.0**
- **Prisma 5.22.0**
- **Vitest 2.1.4**
- **MySQL**
### 🔧 Backend

<p>
<img src="https://img.shields.io/badge/NestJS-E0234E?style=for-the-badge&logo=nestjs&logoColor=white" alt="NestJS"/>
<img src="https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=white" alt="MySQL"/>
<img src="https://img.shields.io/badge/Redis-DC382D?style=for-the-badge&logo=redis&logoColor=white" alt="Redis"/>
<img src="https://img.shields.io/badge/Prisma-2D3748?style=for-the-badge&logo=prisma&logoColor=white" alt="Prisma"/>
<img src="https://img.shields.io/badge/Vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white" alt="Vitest"/>
</p>

### 🌐 Infrastructure

<p>
<img src="https://img.shields.io/badge/Turborepo-000000?style=for-the-badge&logo=turborepo&logoColor=white" alt="Turborepo"/>
<img src="https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"/>
<img src="https://img.shields.io/badge/Docker_Compose-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker Compose"/>
<img src="https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=githubactions&logoColor=white" alt="GitHub Actions"/>
<img src="https://img.shields.io/badge/Naver_Cloud-03C75A?style=for-the-badge&logo=naver&logoColor=white" alt="Naver Cloud"/>
<img src="https://img.shields.io/badge/Nginx-009639?style=for-the-badge&logo=nginx&logoColor=white" alt="Nginx"/>
</p>

### 🔍 DevOps & Monitoring

<p>
<img src="https://img.shields.io/badge/Terraform-7B42BC?style=for-the-badge&logo=terraform&logoColor=white" alt="Terraform"/>
<img src="https://img.shields.io/badge/Terraform%20Cloud-7B42BC?style=for-the-badge&logo=terraform&logoColor=white" alt="Terraform Cloud"/>
<img src="https://img.shields.io/badge/Elasticsearch-005571?style=for-the-badge&logo=elasticsearch&logoColor=white" alt="Elasticsearch"/>
<img src="https://img.shields.io/badge/FluentD-0E83C8?style=for-the-badge&logo=fluentd&logoColor=white" alt="FluentD"/>
<img src="https://img.shields.io/badge/Kibana-005571?style=for-the-badge&logo=kibana&logoColor=white" alt="Kibana"/>
<img src="https://img.shields.io/badge/Grafana-F46800?style=for-the-badge&logo=grafana&logoColor=white" alt="Grafana"/>
<img src="https://img.shields.io/badge/Prometheus-E6522C?style=for-the-badge&logo=prometheus&logoColor=white" alt="Prometheus"/>
</p>

### 💬 Communication Tools

<p>
<img src="https://img.shields.io/badge/Slack-4A154B?style=for-the-badge&logo=slack&logoColor=white" alt="Slack"/>
<img src="https://img.shields.io/badge/Zoom-2D8CFF?style=for-the-badge&logo=zoom&logoColor=white" alt="Zoom"/>
<img src="https://img.shields.io/badge/Gather_Town-6E5494?style=for-the-badge&logo=googlemeet&logoColor=white" alt="Gather Town"/>
<img src="https://img.shields.io/badge/Figma-F24E1E?style=for-the-badge&logo=figma&logoColor=white" alt="Figma"/>
</p>

</div>

---

## **아키텍처** 🌐

### **전반적인 인프라 설계**
### **인프라 설계**

![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)

![image](https://github.com/user-attachments/assets/5901b688-0d3d-4698-ad22-a4d4bb7aa8fd)
## 우리의 Next!

### **CI/CD 파이프라인**
## 🌈 **함께하세요!**

<img width="1024" alt="cicd" src="https://github.com/user-attachments/assets/286d7d2d-bb6a-4315-bcff-4a6ea7569077">
> **Cloud Canvas로 클라우드 설계의 새로운 가능성을 경험해보세요!**
> 프로젝트의 진행 상황과 더 많은 정보를 원하신다면 [GitHub Wiki](https://github.com/boostcampwm-2024/web37-cloud-canvas/wiki)에서 확인하세요. 😊
## **팀 소개** 👩‍💻

Expand All @@ -99,8 +174,3 @@ Cloud Canvas는 클라우드 인프라 설계를 **그래픽 인터페이스**
| 커피 || 고기 | 국수 |

---

## 🌈 **함께하세요!**

> **Cloud Canvas로 클라우드 설계의 새로운 가능성을 경험해보세요!**
> 프로젝트의 진행 상황과 더 많은 정보를 원하신다면 [GitHub Wiki](https://github.com/boostcampwm-2024/web37-cloud-canvas/wiki)에서 확인하세요. 😊
47 changes: 38 additions & 9 deletions apps/hub/src/app/architectures/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
'use client';
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';
Expand All @@ -17,6 +19,7 @@ interface PublicArchitecture {
cost: number;
tags: { tag: { name: string } }[];
stars: any[];
isAuthor: boolean;
_count: {
stars: number;
imports: number;
Expand All @@ -40,12 +43,27 @@ export default function ArchitectureDetailPage() {
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`,
Expand Down Expand Up @@ -81,25 +99,36 @@ export default function ArchitectureDetailPage() {
<Tag key={name} tag={name} />
))}
</div>
<h2 className="text-4xl font-extrabold">{title}</h2>
</div>
<div className="flex gap-6 text-gray-500 text-sm">
<div className="flex gap-1">
<span>by</span>
<span className="text-black">{author}</span>
<div className="flex gap-2">
<h2 className="text-4xl font-extrabold flex-1">
{title}
</h2>
{isAuthor && (
<>
<button>
<EditIcon />
</button>
<button onClick={handleDelete}>
<DeleteIcon />
</button>
</>
)}
</div>
</div>
<div className="flex gap-4 text-gray-500 text-sm">
<div>{new Date(createdAt).toLocaleString()}</div>
<div>{author}</div>
<div className="flex gap-1">
<span className="text-black">{imports}</span>
<span>{imports}</span>
<span>imported</span>
</div>
</div>
<div className="flex gap-4 justify-end items-center">
<div className="mr-2">
<span className="font-black text-xl text-emerald-600">
${cost}
{cost}
</span>
<span className="text-xs"> / month</span>
<span> / month</span>
</div>
<button
className={`flex items-center gap-1 ${isLoggedIn && isStarred ? 'text-yellow-400' : 'text-gray-300'}`}
Expand Down
Binary file added apps/hub/src/app/favicon.ico
Binary file not shown.
6 changes: 6 additions & 0 deletions apps/hub/src/app/fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import localFont from 'next/font/local';

export const gamjaFlower = localFont({
src: '../fonts/GamjaFlower-Regular.ttf',
variable: '--font-gamja-flower',
});
7 changes: 4 additions & 3 deletions apps/hub/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
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({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="ko">
<body className={`antialiased`}>
<body className={`antialiased ${gamjaFlower.variable}`}>
<GlobalHeader />
<main className="max-w-7xl mx-auto mt-10">{children}</main>
</body>
Expand Down
14 changes: 6 additions & 8 deletions apps/hub/src/components/ArchitectureBoard/ArchitectureItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,24 @@ export const ArchitectureItem = ({
}) => {
const { imports, stars } = _count;
return (
<li className="hover:bg-gray-50 border-b flex px-3 py-2 pl-4">
<li className="hover:bg-gray-50 border-b flex px-3 py-2 pl-4 items-center">
<div className="flex flex-col w-full">
<div>
<Link href={`/architectures/${id}`}>{title}</Link>
</div>
<div className="text-xs text-gray-400 flex">
<div className="text-xs text-gray-400 flex gap-3">
<div>{new Date(createdAt).toLocaleString()}</div>
<div className="ml-2">{author.name}</div>
<div>{author.name}</div>
</div>
<div className="flex gap-1 mt-1">
{tags?.map(({ tag: { name } }) => (
<Tag key={name} tag={name} />
))}
</div>
</div>
<div className="flex items-center text-sm">
<div className="w-28">{cost}</div>
<div className="w-28">{imports}</div>
<div className="w-28">{stars}</div>
</div>
<div className="w-52">{cost}</div>
<div className="w-40">{stars}</div>
<div className="w-40">{imports}</div>
</li>
);
};
6 changes: 3 additions & 3 deletions apps/hub/src/components/ArchitectureBoard/BoardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -27,7 +27,7 @@ export const BoardHeader = ({
};

return (
<div className="bg-gray-50 flex border-b p-4 font-semibold">
<div className="bg-slate-50 flex border-b p-4 font-semibold">
{columns.map((column) => (
<div
key={column.key}
Expand Down
Loading

0 comments on commit 8c14bf4

Please sign in to comment.