diff --git a/src/pages/forwarding/Edit.tsx b/src/pages/forwarding/Edit.tsx new file mode 100644 index 0000000000000000000000000000000000000000..68ae69e7ad4f8958acd706d55774d4861ec20523 --- /dev/null +++ b/src/pages/forwarding/Edit.tsx @@ -0,0 +1,182 @@ +import { z } from 'zod'; +import { useEffect, useState, useRef } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Link, useNavigate, useParams } from 'react-router'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { useAuthStore } from '@/stores/authStore'; + +const formSchema = z.object({ + name: z.string({ required_error: '서버 이름을 입력해주세요' }).min(1, { message: '서버 이름을 입력해주세요' }), + serverPort: z + .number({ required_error: '도메인 주소를 입력해주세요' }) + .min(20000, { message: '포트 번호는 20000 이상이어야 합니다' }) + .max(29999, { message: '포트 번호는 29999 이하여야 합니다' }), + instanceIp: z + .string({ required_error: '인스턴스 IP를 입력해주세요' }) + .regex(/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/, { + message: '올바른 IP 주소를 입력해주세요', + }) + .startsWith('10.16.', { message: '인스턴스 IP는 10.16.0.0/16 대역을 사용해야 합니다' }), +}); + +export default function ForwardingEdit() { + const navigate = useNavigate(); + const { id } = useParams(); + const { authFetch } = useAuthStore(); + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + }); + const initialData = useRef<z.infer<typeof formSchema> | null>(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + authFetch(`/api/forwarding?forwardingId=${id}`) + .then((response) => { + if (!response.ok) { + console.error(response); + throw Error(); + } + return response.json(); + }) + .then(({ name, serverPort, instanceIp }) => { + initialData.current = { name, serverPort: parseInt(serverPort), instanceIp }; + form.setValue('name', name); + form.setValue('serverPort', parseInt(serverPort)); + form.setValue('instanceIp', instanceIp); + setIsLoading(false); + }) + .catch((error) => { + console.error(error); + toast.error('포트포워딩 정보를 조회할 수 없습니다.'); + }); + }); + + async function onSubmit(values: z.infer<typeof formSchema>) { + if (!initialData.current) return; + + const payload: Partial<z.infer<typeof formSchema>> = {}; + if (values.name !== initialData.current.name) payload.name = values.name; + if (values.serverPort !== initialData.current.serverPort) payload.serverPort = values.serverPort; + if (values.instanceIp !== initialData.current.instanceIp) payload.instanceIp = values.instanceIp; + + const response = await authFetch(`/api/forwarding?forwardingId=${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + console.error(response); + + const { code } = await response.json(); + if (code == 'DUPLICATED_INSTANCE_INFO') { + form.setError('instanceIp', { type: 'custom' }); + toast.error('이미 존재하는 포트포워딩 설정입니다'); + } else if (code == 'DUPLICATED_SERVER_PORT') { + form.setError('serverPort', { type: 'custom' }); + toast.error('이미 사용 중인 포트 번호입니다'); + } else { + toast.error('포트포워딩 설정 수정에 실패했습니다'); + } + } else { + toast.success('포트포워딩 설정이 수정되었습니다'); + navigate('/forwarding'); + } + } + + return ( + <div className="flex flex-1 flex-col gap-4 p-6"> + <div className="mb-2"> + <h1 className="scroll-m-20 text-3xl font-semibold first:mt-0">포트포워딩 설정 수정</h1> + <p className="mt-1 text-base text-gray-500">기존 포트포워딩 설정을 수정합니다.</p> + </div> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <Card className="mb-4"> + <CardHeader> + {isLoading ? ( + <Skeleton className="h-[1.75rem] w-[16rem]" /> + ) : ( + <CardTitle className="text-xl">SSH 포트포워딩 설정</CardTitle> + )} + </CardHeader> + <CardContent className="space-y-4"> + {isLoading ? ( + <Skeleton className="h-[16rem] w-full" /> + ) : ( + <div className="grid grid-cols-1 gap-4"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel required>서버 이름</FormLabel> + <FormControl> + <Input placeholder="웹 서버 이름" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="serverPort" + render={({ field }) => ( + <FormItem> + <FormLabel required>포트 번호</FormLabel> + <FormControl> + <Input + type="number" + min={20000} + max={29999} + placeholder="20000 ~ 29999" + {...field} + onChange={(e) => field.onChange(+e.target.value)} + /> + </FormControl> + <FormDescription>ssh.aoldacloud.com:<포트번호>로 접속 가능합니다</FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="instanceIp" + render={({ field }) => ( + <FormItem> + <FormLabel required>인스턴스 IP</FormLabel> + <FormControl> + <Input placeholder="10.16.x.x" {...field} /> + </FormControl> + <FormDescription>인스턴스 IP는 10.16.0.0/16 대역을 사용합니다</FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </div> + )} + </CardContent> + </Card> + + <div className="flex justify-end gap-2"> + <Button variant="outline" asChild> + <Link to="..">취소</Link> + </Button> + <Button type="submit">저장</Button> + </div> + </form> + </Form> + </div> + ); +} diff --git a/src/routes.tsx b/src/routes.tsx index 1c837f082cfe76d0ad9175f703798abdba979009..22bf2419cc730017b230de187ccaf13b6e39721c 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -9,6 +9,7 @@ import CertificateList from './pages/certificate/List'; import CertificateCreate from './pages/certificate/Create'; import ForwardingList from '@/pages/forwarding/List'; import ForwardingCreate from './pages/forwarding/Create'; +import ForwardingEdit from './pages/forwarding/Edit'; export default function AppRoutes() { return ( @@ -27,6 +28,7 @@ export default function AppRoutes() { <Route path="forwarding"> <Route index element={<ForwardingList />} /> <Route path="create" element={<ForwardingCreate />} /> + <Route path="edit/:id" element={<ForwardingEdit />} /> </Route> <Route path="*" element={<NotFound />} /> </Route>