-
- 이메일:{" "}
+
+
+
+
+
+
+ {/* 왼쪽 큰 카드 */}
+
+
+

+
+
+ 브라우저로{" "}
+ 즉시 관리
+
+
+
+ 언제 어디서나 웹 콘솔을 통해 인스턴스의
+
상태를 확인하고 원격 제어
+
+
+
+ {/* 오른쪽 위 가로 카드 */}
+
+
+ {/* 오른쪽 아래 작은 카드들 */}
+
+
+
+
+
+
+
+
+
+
+ 생생한 실제 사용 리뷰
+
+
+ 도들을 실제로 사용해본 사용자의 리뷰
-
+ {/*
*/}
+
+
+
+
+
+
+ 🔽 사전신청 알림받기
+ 🔽
+
+
+
+
+ 서비스가 오픈되면 작성해주신 이메일로 알림을 보내드립니다.
+
+
-
-
- 서비스가 오픈되면 작성해주신 이메일로 알림을 보내드립니다.
-
-
+
도들에게 문의해 주세요.
- 연락처: contact@doddle.kr
+ 연락처: kwangwoonwebservice@gmail.com
@@ -110,19 +220,28 @@ const Landing = () => {
제목:
-
+
diff --git a/src/pages/VMCreate.css b/src/pages/VMCreate.css
index 1da05ba..4355ea2 100644
--- a/src/pages/VMCreate.css
+++ b/src/pages/VMCreate.css
@@ -6,3 +6,42 @@
border-radius: 10px;
box-shadow: 0px 0px 25px -8px rgba(0, 0, 0, 0.2);
}
+
+.vm-create .inline-grid > div {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 1px solid #e6e7eb;
+ border-radius: 8px;
+ padding: 16px;
+ height: auto;
+ min-height: 140px;
+ background: #fff;
+ transition: all 0.3s ease;
+}
+
+.vm-create .inline-grid img {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+ margin-bottom: 8px;
+}
+
+.vm-create .inline-grid span,
+.vm-create .inline-grid p {
+ font-weight: 500;
+ margin-bottom: 6px;
+ font-size: 15px;
+}
+
+.vm-create select,
+.vm-create .hw-dropdown-root {
+ width: 100% !important;
+ min-height: 20px;
+ margin-top: auto;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ color: #666;
+ padding: 4px 0;
+}
diff --git a/src/pages/VMCreate.tsx b/src/pages/VMCreate.tsx
index 5809e39..ee7f691 100644
--- a/src/pages/VMCreate.tsx
+++ b/src/pages/VMCreate.tsx
@@ -3,6 +3,7 @@ import { AxiosError } from "axios";
import { useContext, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
+import debian from "@/assets/image/vmCreate/debian.svg";
import ubuntu from "@/assets/image/vmCreate/ubuntu.svg";
import addIcon from "@/assets/image/vmManage/button/add.svg";
import AuthTextFieldV2 from "@/components/auth/textField/AuthTextFieldV2";
@@ -22,45 +23,56 @@ interface RequiredInput {
showError: boolean;
}
-const VMCreateContent: React.FC = () => {
+interface InstanceTypes {
+ id: number;
+ typename: string;
+ vcpu: number;
+ ram: number;
+ dsk: number;
+}
+
+const VMCreateContent = () => {
const [vmName, setVmName] = useState({
value: "",
showError: false,
});
- const osList: OsList[] = [
- {
- name: "Ubuntu",
- img: ubuntu,
- version: [
- { "24.04 LTS": "ubuntu-cloud-24.04.img" },
- { "22.04 LTS": "ubuntu-cloud-22.04.img" },
- // "20.04 LTS",
- // "24.10",
- // "23.10",
- // "23.04",
- ],
- hardware: ["Light (Server)" /*, "Heavy (Storage)", "GPU (AI/ML)"*/],
- },
- // {
- // name: "CentOS",
- // img: ubuntu,
- // version: ["8 Stream", "7 Stream"],
- // hardware: ["Light (Server)", "Heavy (Storage)"],
- // },
- ];
-
- const { os, osVersion, hw, setHw, openSharedUser, setOpenSharedUser } =
- useContext(VMCreateContext)!;
+ const {
+ os,
+ osVersion,
+ osVersionImgName,
+ hw,
+ setHw,
+ openSharedUser,
+ setOpenSharedUser,
+ } = useContext(VMCreateContext)!;
const navigate = useNavigate();
// Backend에서 제공하는 인스턴스 타입/OS 목록 (id 매핑용)
- const [instanceTypes, setInstanceTypes] = useState<
- { id: number; typename: string; vcpu: number; ram: number; dsk: number }[]
- >([]);
+ const [instanceTypes, setInstanceTypes] = useState([]);
const [osOptions, setOsOptions] = useState<{ id: number; name: string }[]>(
- []
+ [],
);
+ //생성버튼 누른 후 생성버튼 비활성화
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // ubuntu-cloud-24.04.img -> nameLabel: ubuntu, versionLabel: 24.04
+ const computedOsList: OsList[] = osOptions.map((o) => {
+ const dotIdx = o.name.lastIndexOf(".");
+ const noExt = dotIdx > -1 ? o.name.slice(0, dotIdx) : o.name;
+ const parts = noExt.split("-");
+ const nameLabel = parts[0] || noExt;
+ const versionLabel = parts.length > 1 ? parts[parts.length - 1] : "";
+ return {
+ id: o.id,
+ name: nameLabel,
+ img: nameLabel === "ubuntu" ? ubuntu : debian,
+ // label: last segment (version), value: original filename
+ version: [{ [versionLabel || noExt]: o.name }],
+ hardware: instanceTypes.map((t) => t.typename),
+ };
+ });
+
useEffect(() => {
const fetchVmRequirements = async () => {
try {
@@ -76,17 +88,18 @@ const VMCreateContent: React.FC = () => {
const onCreateVM = async () => {
try {
- // 선택된 OS 이름(`os`)과 매칭되는 id를 찾고, 없으면 첫 번째 항목 사용
+ // 선택된 OS 이미지 파일명(`osVersionImgName`)과 매칭되는 id를 찾고, 없으면 첫 번째 항목 사용
const selectedOsId =
- osOptions.find((o) => o.name === os)?.id ?? osOptions[0]?.id;
+ osOptions.find((o) => o.name === osVersionImgName)?.id ??
+ osOptions[0]?.id;
- // 인스턴스 타입 id: 현재 UI와 타입 매핑이 없으므로 일단 첫 번째 항목 사용
- // 추후 UI에서 실제 타입 선택과 매핑 필요
- const selectedTypeId = instanceTypes[0]?.id;
+ const selectedTypeId =
+ instanceTypes.find((t) => t.typename === hw)?.id ??
+ instanceTypes[0]?.id;
if (!selectedOsId || !selectedTypeId) {
alert(
- "VM 생성에 필요한 정보(OS/Instance Type)가 준비되지 않았습니다. 잠시 후 다시 시도해주세요."
+ "VM 생성에 필요한 정보(OS/Instance Type)가 준비되지 않았습니다. 잠시 후 다시 시도해주세요.",
);
return;
}
@@ -99,7 +112,7 @@ const VMCreateContent: React.FC = () => {
is_public: openSharedUser === "public",
});
- navigate("/");
+ navigate("/manage");
} catch (error) {
if (error instanceof AxiosError) {
console.log(error.response?.data);
@@ -111,21 +124,19 @@ const VMCreateContent: React.FC = () => {
return (
- 인스턴스 생성
+ 인스턴스 생성
-
+
-
- 인스턴스 이름
-
+
인스턴스 이름
{
/>
-
OS 선택
-
- {osList.map((item) => (
-
+
OS 선택
+
+ {computedOsList.map((item) => (
+
))}
-
하드웨어 선택
+
하드웨어 선택
item.name === os)?.hardware || []
- }
+ hardwareList={instanceTypes.map((t) => t.typename)}
hw={hw}
setHw={setHw}
disabled={!osVersion}
@@ -162,7 +174,7 @@ const VMCreateContent: React.FC = () => {
-
Shard User 공개
+
Shard User 공개
{
t.typename === hw)?.vcpu ||
+ "(비어 있음)"
+ }
/>
t.typename === hw)?.ram ||
+ "(비어 있음)"
+ }
/>
t.typename === hw)?.dsk ||
+ "(비어 있음)"
+ }
/>
@@ -249,17 +270,26 @@ const VMCreateContent: React.FC = () => {
{
- navigate("/");
+ navigate("/manage");
}}
>
취소
{
+ if (isSubmitting) return;
+ setIsSubmitting(true);
+ try {
+ await onCreateVM();
+ } finally {
+ setIsSubmitting(false);
+ }
+ }}
>
- 생성
+ {isSubmitting ? "생성중" : "생성"}
diff --git a/src/pages/VMManage.tsx b/src/pages/VMManage.tsx
index a5e9637..6876967 100644
--- a/src/pages/VMManage.tsx
+++ b/src/pages/VMManage.tsx
@@ -15,8 +15,9 @@ import {
TableHead,
TableRow,
} from "@mui/material";
-import { useEffect, useState } from "react";
+import { PropsWithChildren, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
+import { twJoin, twMerge } from "tailwind-merge";
import booting from "../assets/image/vmManage/booting.svg";
import addIcon from "../assets/image/vmManage/button/add.svg";
@@ -28,12 +29,23 @@ import MuiBtn from "../components/button/MuiBtn";
import VMManageBtn from "../components/vmManage/VMManageBtn";
import VMDetailModal from "../components/vmManage/VMManageModal";
import axiosClient from "../services/api";
-import { Status, VM } from "../types/vm";
+import { CurrentStatus, VM } from "../types/vm";
import { currentStatusMapping, userTypeMapping } from "../utils/MappingToKor";
import "./VMManage.css";
-const VMManage: React.FC = () => {
+interface VMInitStatus {
+ vm_id: string;
+ vm_name: string;
+ is_owner: string;
+ instance_type: string;
+ os: string;
+ ip: string;
+ status: CurrentStatus;
+ uptime: string;
+}
+
+const VMManage = () => {
const [vmList, setVmList] = useState
([]);
const [checkedVMs, setCheckedVMs] = useState([]);
@@ -47,50 +59,35 @@ const VMManage: React.FC = () => {
const [showDetailModal, setShowDetailModal] = useState(false);
const [selectedVMId, setSelectedVMId] = useState(null);
- const selectedVM = vmList.find((vm) => vm.id === selectedVMId) || null;
+ // const selectedVM = vmList.find((vm) => vm.id === selectedVMId) || null;
const navigate = useNavigate();
- useEffect(() => {
+ const fetchData = async () => {
try {
- const fetchData = async () => {
- // const response = await axiosClient.get("/vm/status");
- // console.log(response);
+ const { data } = await axiosClient.get("/vm/status");
- const vmList: VM[] = [
- {
- id: "123e4567-e89b-12d3-a456-426614174000",
- vmName: "VM1",
- currentStatus: "booting",
- status: "시작",
- instanceType: "t2.micro",
- publicIP: "192.168.1.1",
- key: "key example",
- os: "Ubuntu 24.04 LTS",
- startTime: "2025-01-18 10:00",
- runTime: "5.5h",
- userType: "admin",
- },
- {
- id: "123e4567-e89b-12d3-a456-426614174001",
- vmName: "VM2",
- currentStatus: "launching",
- status: "시작",
- instanceType: "t2.medium",
- publicIP: "192.168.1.1",
- key: "key example",
- os: "Ubuntu 24.04 LTS",
- startTime: "2025-01-18 10:00",
- runTime: "5.5h",
- userType: "user",
- },
- ];
- setVmList(vmList);
- };
- fetchData();
+ setVmList(
+ data.map((vm: VMInitStatus) => ({
+ id: vm.vm_id,
+ vmName: vm.vm_name,
+ status: vm.status,
+ instanceType: vm.instance_type,
+ publicIP: vm.ip,
+ key: vm.is_owner,
+ os: vm.os,
+ startTime: vm.uptime,
+ runTime: vm.uptime,
+ userType: vm.is_owner,
+ })),
+ );
} catch (error) {
console.error(error);
}
+ };
+
+ useEffect(() => {
+ fetchData();
}, []);
const onCloseDeleteDialog = () => {
@@ -101,7 +98,7 @@ const VMManage: React.FC = () => {
try {
// 삭제 요청 병렬 수행
const results = await Promise.allSettled(
- checkedVMs.map((vmId) => axiosClient.delete(`/vm/${vmId}`))
+ checkedVMs.map((vmId) => axiosClient.delete(`/vm/${vmId}`)),
);
const succeeded: string[] = [];
@@ -131,9 +128,7 @@ const VMManage: React.FC = () => {
setDeleteAlert({
show: true,
success: false,
- message: `총 ${
- failed.length
- }개의 VM 삭제에 실패했습니다: ${failed.join(", ")}`,
+ message: `총 ${failed.length}개의 VM 삭제에 실패했습니다: ${failed.join(", ")}`,
});
} else {
setDeleteAlert({
@@ -159,21 +154,21 @@ const VMManage: React.FC = () => {
const onChangeName = (id: string, newName: string) => {
setVmList((prevList) =>
- prevList.map((vm) => (vm.id === id ? { ...vm, vmName: newName } : vm))
+ prevList.map((vm) => (vm.id === id ? { ...vm, vmName: newName } : vm)),
);
};
- const onChangeStatus = (id: string, newStatus: Status) => {
+ const onChangeStatus = (id: string, newStatus: CurrentStatus) => {
setVmList((prevList) =>
- prevList.map((vm) => (vm.id === id ? { ...vm, status: newStatus } : vm))
+ prevList.map((vm) => (vm.id === id ? { ...vm, status: newStatus } : vm)),
);
};
return (
-
VM 리스트
+
인스턴스 리스트
-
+
{checkedVMs.length > 0 && (
{
id="alert-dialog-title"
sx={{ marginBottom: "calc(31px - 16px)" }}
>
- VM 삭제
+ VM 삭제
-
-
+
+
{checkedVMs.length}개의 항목을 정말 삭제하시겠습니까?
-
- 삭제 시
되돌릴 수 없습니다.
+
+ 삭제 시
되돌릴 수 없습니다.
@@ -262,7 +257,7 @@ const VMManage: React.FC = () => {
{showDetailModal && (
{
// id가 이미 선택된 상태일 경우 제거, 아닐 경우 추가
prevSelected.includes(vm.id)
? prevSelected.filter((vmId) => vmId !== vm.id) // 현재의 id를 제외한 나머지 id들만 filter
- : [...prevSelected, vm.id]
+ : [...prevSelected, vm.id],
);
}}
sx={{ color: "var(--Grey1, #808B96)" }}
/>
- {vm.vmName}
+ {vm.vmName}
{vm.instanceType}
{vm.publicIP || "-"}
-
+
{userTypeMapping(vm.userType)}
-
+

- {currentStatusMapping(vm.currentStatus)}
+ {currentStatusMapping(vm.status)}
-
+

-
{vm.runTime}
+
{vm.runTime}
@@ -372,12 +375,10 @@ const VMManage: React.FC = () => {
};
export default VMManage;
-const TableCellAttribute: React.FC<{ children: React.ReactNode }> = ({
- children,
-}) => {
+const TableCellAttribute = ({ children }: PropsWithChildren) => {
return (
- {children}
+ {children}
);
};
diff --git a/src/pages/auth/FindPw.tsx b/src/pages/auth/FindPw.tsx
index d790837..01f9e9f 100644
--- a/src/pages/auth/FindPw.tsx
+++ b/src/pages/auth/FindPw.tsx
@@ -62,9 +62,7 @@ const FindPw = () => {
return (
-
- 비밀번호 재설정
-
+ 비밀번호 재설정