Skip to content
Snippets Groups Projects
Commit 3e3bf82c authored by 한동현's avatar 한동현
Browse files

feat: 라우팅 설정 페이지 추가

parent c67b69aa
Branches
No related tags found
No related merge requests found
...@@ -103,8 +103,13 @@ export default function RoutingCreate() { ...@@ -103,8 +103,13 @@ export default function RoutingCreate() {
}); });
if (!response.ok) { if (!response.ok) {
console.error(response); const { code } = await response.json();
if (code == 'DUPLICATED_DOMAIN_NAME') {
form.setError('domain', { type: 'custom', message: '이미 사용중인 도메인입니다' });
toast.error('이미 사용중인 도메인입니다');
} else {
toast.error('라우팅 설정을 등록할 수 없습니다'); toast.error('라우팅 설정을 등록할 수 없습니다');
}
} else { } else {
toast.success('라우팅 설정을 등록합니다'); toast.success('라우팅 설정을 등록합니다');
navigate('/routing'); navigate('/routing');
......
import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router';
import { useForm, useWatch } from 'react-hook-form';
import { Check, X } from 'lucide-react';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Separator } from '@/components/ui/separator';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Skeleton } from '@/components/ui/skeleton';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Certificate } from '@/types/certificate';
import { useAuthStore } from '@/stores/authStore';
import useDebounce from '@/hooks/useDebounce';
const formSchema = z
.object({
name: z.string({ required_error: '서버 이름을 입력해주세요' }).min(1, { message: '서버 이름을 입력해주세요' }),
domain: z
.string({ required_error: '도메인 주소를 입력해주세요' })
.regex(/^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
message: '올바른 도메인 주소를 입력해주세요',
}),
ip: 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 대역을 사용해야 합니다' }),
port: z.coerce
.number({ invalid_type_error: '포트 번호를 입력해주세요' })
.min(1, { message: '포트 번호를 입력해주세요' })
.max(65535, { message: '올바른 포트 번호를 입력해주세요' }),
enableSSL: z.boolean(),
certificateId: z.coerce.number().optional(),
caching: z.boolean(),
})
.refine((data) => !data.enableSSL || (data.enableSSL && data.certificateId !== undefined), {
message: 'SSL 인증서를 선택해주세요',
path: ['certificateId'],
});
export default function RoutingEdit() {
const navigate = useNavigate();
const { id } = useParams();
const { authFetch, selectedProject } = useAuthStore();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: undefined,
domain: undefined,
ip: undefined,
port: undefined,
enableSSL: false,
certificateId: undefined,
caching: false,
},
});
const domain = useWatch({ control: form.control, name: 'domain' });
const enableSSL = useWatch({ control: form.control, name: 'enableSSL' });
const debouncedDomain = useDebounce(domain, 500);
const [certificates, setCertificates] = useState<Certificate[]>([]);
const initData = useRef<z.infer<typeof formSchema> | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
authFetch(`/api/routing?routingId=${id}`)
.then((response) => {
if (!response.ok) {
console.error(response);
throw Error();
}
return response.json();
})
.then(({ name, domain, ip, port, certificateId, caching }) => {
initData.current = {
name,
domain,
ip,
port: parseInt(port),
enableSSL: certificateId ? true : false,
certificateId: certificateId || -1,
caching,
};
form.setValue('name', initData.current.name);
form.setValue('domain', initData.current.domain);
form.setValue('ip', initData.current.ip);
form.setValue('port', initData.current.port);
form.setValue('enableSSL', initData.current.enableSSL);
form.setValue('certificateId', initData.current.certificateId);
form.setValue('caching', initData.current.caching);
setIsLoading(false);
})
.catch((error) => {
console.error(error);
toast.error('라우팅 설정 정보를 조회할 수 없습니다.');
});
}, []);
useEffect(() => {
if (enableSSL) {
authFetch(`/api/certificates?projectId=${selectedProject?.id}&domain=${debouncedDomain}`)
.then((response) => {
if (!response.ok) {
toast.error('SSL 인증서 목록을 불러올 수 없습니다');
return { contents: [] };
}
return response.json();
})
.then(({ contents }) => {
setCertificates(contents);
});
}
}, [authFetch, enableSSL, selectedProject, debouncedDomain]);
async function onSubmit(values: z.infer<typeof formSchema>) {
if (!initData.current) return;
const payload: Partial<z.infer<typeof formSchema>> = {};
if (values.name !== initData.current.name) payload.name = values.name;
if (values.domain !== initData.current.domain) payload.domain = values.domain;
if (values.ip !== initData.current.ip) payload.ip = values.ip;
if (values.port !== initData.current.port) payload.port = values.port;
if (values.caching !== initData.current.caching) payload.caching = values.caching;
if (!values.enableSSL && values.enableSSL !== initData.current.enableSSL) payload.certificateId = -1;
if (values.enableSSL && values.certificateId !== initData.current.certificateId)
payload.certificateId = values.certificateId;
const response = await authFetch(`/api/routing?routingId=${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const { code } = await response.json();
if (code == 'DUPLICATED_DOMAIN_NAME') {
form.setError('domain', { type: 'custom' });
toast.error('이미 사용중인 도메인입니다');
} else {
toast.error('라우팅 설정 수정에 실패하였습니다');
}
} else {
toast.success('라우팅 설정이 수정되었습니다');
navigate('/routing');
}
}
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>
<CardTitle className="text-xl">라우팅 기본 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{isLoading ? (
<Skeleton className="h-[24rem] 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="domain"
render={({ field }) => (
<FormItem>
<FormLabel required>도메인</FormLabel>
<FormControl>
<Input placeholder="example.ajou.app" {...field} />
</FormControl>
<FormDescription>
*.ajou.app 도메인은 별도 설정 없이 자유롭게 사용할 수 있습니다
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="ip"
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>
)}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel required>인스턴스 포트</FormLabel>
<FormControl>
<Input type="number" placeholder="8080" {...field} />
</FormControl>
<FormDescription>연결할 웹 서비스의 포트 번호를 입력해주세요</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</CardContent>
</Card>
<Card className="mb-6">
<CardHeader>
<CardTitle className="text-xl">추가 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{isLoading ? (
<Skeleton className="h-[8rem] w-full" />
) : (
<>
<FormField
control={form.control}
name="enableSSL"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<FormLabel className="text-base font-medium">SSL 보안 연결</FormLabel>
<FormDescription className="text-sm text-gray-500">
HTTPS 프로토콜을 사용하여 보안 연결을 활성화합니다
</FormDescription>
</div>
<FormControl>
<div className="flex items-center space-x-2">
{!field.value && <X className="h-4 w-4 text-gray-500" />}
{field.value && <Check className="h-4 w-4" />}
<Switch checked={field.value} onCheckedChange={field.onChange} />
</div>
</FormControl>
</FormItem>
)}
/>
{form.watch('enableSSL') && (
<FormField
control={form.control}
name="certificateId"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="SSL 인증서 선택" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
{certificates.length === 0 ? (
<SelectLabel className="text-muted-foreground">
사용 가능한 SSL 인증서가 없습니다. &nbsp;
<Link to="/certificate/create" className="font-medium underline underline-offset-4">
추가하기
</Link>
</SelectLabel>
) : (
certificates.map((certificate) => (
<SelectItem value={certificate.id.toString()}>{certificate.domain}</SelectItem>
))
)}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<Separator />
<FormField
control={form.control}
name="caching"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<FormLabel className="text-base font-medium">정적 파일 캐싱 활성화</FormLabel>
<FormDescription className="text-sm text-gray-500">
빠른 응답을 위해 정적 파일에 대한 캐싱을 활성화합니다
</FormDescription>
</div>
<FormControl>
<div className="flex items-center space-x-2">
{!field.value && <X className="h-4 w-4 text-gray-500" />}
{field.value && <Check className="h-4 w-4" />}
<Switch checked={field.value} onCheckedChange={field.onChange} />
</div>
</FormControl>
</FormItem>
)}
/>
</>
)}
</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>
);
}
...@@ -5,6 +5,7 @@ import Home from '@/pages/Home'; ...@@ -5,6 +5,7 @@ import Home from '@/pages/Home';
import Login from '@/pages/Login'; import Login from '@/pages/Login';
import RoutingList from '@/pages/routing/List'; import RoutingList from '@/pages/routing/List';
import RoutingCreate from '@/pages/routing/Create'; import RoutingCreate from '@/pages/routing/Create';
import RoutingEdit from '@/pages/routing/Edit';
import CertificateList from './pages/certificate/List'; import CertificateList from './pages/certificate/List';
import CertificateCreate from './pages/certificate/Create'; import CertificateCreate from './pages/certificate/Create';
import ForwardingList from '@/pages/forwarding/List'; import ForwardingList from '@/pages/forwarding/List';
...@@ -20,6 +21,7 @@ export default function AppRoutes() { ...@@ -20,6 +21,7 @@ export default function AppRoutes() {
<Route path="routing"> <Route path="routing">
<Route index element={<RoutingList />} /> <Route index element={<RoutingList />} />
<Route path="create" element={<RoutingCreate />} /> <Route path="create" element={<RoutingCreate />} />
<Route path="edit/:id" element={<RoutingEdit />} />
</Route> </Route>
<Route path="certificate"> <Route path="certificate">
<Route index element={<CertificateList />} /> <Route index element={<CertificateList />} />
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment