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

feat: 웹 프록시 라우팅 등록 페이지 추가

parent 4f88ce51
No related branches found
No related tags found
No related merge requests found
......@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^4.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
......@@ -18,6 +19,7 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.0.9",
"class-variance-authority": "^0.7.1",
......@@ -26,11 +28,13 @@
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-router": "^7.2.0",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.9",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
......
......@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@hookform/resolvers':
specifier: ^4.1.2
version: 4.1.2(react-hook-form@7.54.2(react@19.0.0))
'@radix-ui/react-avatar':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
......@@ -32,6 +35,9 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.1.2
version: 1.1.2(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-switch':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-tooltip':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
......@@ -56,6 +62,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.0.0(react@19.0.0)
react-hook-form:
specifier: ^7.54.2
version: 7.54.2(react@19.0.0)
react-router:
specifier: ^7.2.0
version: 7.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
......@@ -71,6 +80,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.0.9)
zod:
specifier: ^3.24.2
version: 3.24.2
zustand:
specifier: ^5.0.3
version: 5.0.3(@types/react@19.0.10)(react@19.0.0)
......@@ -313,6 +325,11 @@ packages:
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@hookform/resolvers@4.1.2':
resolution: {integrity: sha512-wl6H9c9wLOZMJAqGLEVKzbCkxJuV+BYuLFZFCQtCwMe0b3qQk4kUBd/ZAj13SwcSqcx86rCgSCyngQfmA6DOWg==}
peerDependencies:
react-hook-form: ^7.0.0
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
......@@ -623,6 +640,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-switch@1.1.3':
resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.1.8':
resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
peerDependencies:
......@@ -672,6 +702,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.0':
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-rect@1.1.0':
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
peerDependencies:
......@@ -801,6 +840,9 @@ packages:
cpu: [x64]
os: [win32]
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
'@swc/core-darwin-arm64@1.11.4':
resolution: {integrity: sha512-Oi4lt4wqjpp80pcCh+vzvpsESJ8XXozYCE5EM/dDpr+9m2oRpkseds7Gq4ulzgdbUDPo1jJ1PonjjrKpfKY+sQ==}
engines: {node: '>=10'}
......@@ -1465,6 +1507,12 @@ packages:
peerDependencies:
react: ^19.0.0
react-hook-form@7.54.2:
resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
......@@ -1688,6 +1736,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zod@3.24.2:
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
zustand@5.0.3:
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
engines: {node: '>=12.20.0'}
......@@ -1842,6 +1893,11 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
'@hookform/resolvers@4.1.2(react-hook-form@7.54.2(react@19.0.0))':
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.54.2(react@19.0.0)
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
......@@ -2142,6 +2198,21 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
'@radix-ui/react-switch@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-size': 1.1.0(@types/react@19.0.10)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
'@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
......@@ -2188,6 +2259,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
'@radix-ui/react-use-previous@1.1.0(@types/react@19.0.10)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.10
'@radix-ui/react-use-rect@1.1.0(@types/react@19.0.10)(react@19.0.0)':
dependencies:
'@radix-ui/rect': 1.1.0
......@@ -2270,6 +2347,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.34.8':
optional: true
'@standard-schema/utils@0.3.0': {}
'@swc/core-darwin-arm64@1.11.4':
optional: true
......@@ -2903,6 +2982,10 @@ snapshots:
react: 19.0.0
scheduler: 0.25.0
react-hook-form@7.54.2(react@19.0.0):
dependencies:
react: 19.0.0
react-remove-scroll-bar@2.3.8(@types/react@19.0.10)(react@19.0.0):
dependencies:
react: 19.0.0
......@@ -3078,6 +3161,8 @@ snapshots:
yocto-queue@0.1.0: {}
zod@3.24.2: {}
zustand@5.0.3(@types/react@19.0.10)(react@19.0.0):
optionalDependencies:
'@types/react': 19.0.10
......
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
useFormState,
} from 'react-hook-form';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div data-slot="form-item" className={cn('grid gap-2', className)} {...props} />
</FormItemContext.Provider>
);
}
function FormLabel({
className,
required,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root> & { required?: boolean }) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn(
'data-[error=true]:text-destructive-foreground',
required && 'gap-0 after:ml-1 after:text-destructive after:content-["*"]',
className
)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn('text-muted-foreground text-[0.8rem]', className)}
{...props}
/>
);
}
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? '') : props.children;
if (!body) {
return null;
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn('text-destructive-foreground text-[0.8rem]', className)}
{...props}
>
{body}
</p>
);
}
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background pointer-events-none block size-4 rounded-full ring-0 shadow-lg transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Link } from 'react-router';
import { useForm } from 'react-hook-form';
import { Check, X } from 'lucide-react';
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 { toast } from 'sonner';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
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: '올바른 도메인 주소를 입력해주세요',
}),
instance_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 대역을 사용해야 합니다' }),
enable_ssl: z.boolean(),
enable_cache: z.boolean(),
});
export default function RoutingCreate() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
domain: '',
instance_ip: '',
enable_ssl: false,
enable_cache: false,
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log('제출된 데이터:', values);
toast.warning('라우팅 설정을 등록합니다');
}
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">
<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="instance_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>
)}
/>
</div>
</CardContent>
</Card>
<Card className="mb-6">
<CardHeader>
<CardTitle className="text-xl">추가 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="enable_ssl"
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>
)}
/>
<Separator />
<FormField
control={form.control}
name="enable_cache"
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>
);
}
......@@ -58,8 +58,10 @@ export default function RoutingList() {
<h1 className="scroll-m-20 text-3xl font-semibold first:mt-0">웹 라우팅 설정</h1>
<p className="mt-1 text-base text-gray-500">현재 3개의 웹 프록시가 설정되어 있습니다.</p>
</div>
<Button className="ml-2">
<Button className="ml-2" asChild>
<Link to="./create">
<Plus className="h-4 w-4" /> 새 프록시 추가
</Link>
</Button>
</div>
<Card>
......
......@@ -4,6 +4,7 @@ import NotFound from '@/pages/NotFound';
import Home from '@/pages/Home';
import Login from '@/pages/Login';
import RoutingList from '@/pages/routing/List';
import RoutingCreate from '@/pages/routing/Create';
export default function AppRoutes() {
return (
......@@ -13,6 +14,7 @@ export default function AppRoutes() {
<Route path="login" element={<Login />} />
<Route path="routing">
<Route index element={<RoutingList />} />
<Route path="create" element={<RoutingCreate />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment