1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
'use client'
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Loader } from "lucide-react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { useSession } from "next-auth/react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { createEmailWhitelistAction } from "../service"
// Validation Schema
const createWhitelistSchema = z.object({
value: z.string()
.min(1, "값은 필수입니다")
.max(255, "값은 255자를 초과할 수 없습니다")
.refine((value) => {
// 이메일 형식 또는 도메인 형식 중 하나여야 함
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// 도메인 검증: 최소 하나의 .이 있어야 하고, TLD가 있어야 함
// 예: company.com, sub.company.com, company.co.kr
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
return emailRegex.test(value) || domainRegex.test(value);
}, "올바른 이메일 주소 또는 도메인 형식이 아닙니다 (도메인은 최소 1개의 TLD가 필요합니다)"),
description: z.string().max(500, "설명은 500자 이하여야 합니다").optional(),
})
type CreateWhitelistSchema = z.infer<typeof createWhitelistSchema>
interface CreateWhitelistDialogProps
extends Omit<React.ComponentPropsWithRef<typeof Dialog>, 'children'> {
open?: boolean
onOpenChange?: (open: boolean) => void
}
export function CreateWhitelistDialog({ ...props }: CreateWhitelistDialogProps) {
const [isCreatePending, startCreateTransition] = React.useTransition()
const { data: session } = useSession();
const form = useForm<CreateWhitelistSchema>({
resolver: zodResolver(createWhitelistSchema),
defaultValues: {
value: "",
description: "",
},
})
// 입력값 소문자 변환
React.useEffect(() => {
const watchedValue = form.watch("value")
if (watchedValue && watchedValue !== watchedValue.toLowerCase()) {
form.setValue("value", watchedValue.toLowerCase())
}
}, [form])
function onSubmit(input: CreateWhitelistSchema) {
startCreateTransition(async () => {
if (!session?.user?.id) {
toast.error("로그인이 필요합니다")
return
}
const { error } = await createEmailWhitelistAction({
value: input.value,
description: input.description,
})
if (error) {
toast.error(error)
return
}
form.reset()
props.onOpenChange?.(false)
toast.success("화이트리스트 도메인이 추가되었습니다")
})
}
return (
<Dialog {...props}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>화이트리스트 추가</DialogTitle>
<DialogDescription>
SMS 인증을 우회할 이메일 주소 또는 도메인을 추가합니다. 등록된 이메일이나 도메인의 주소로 로그인 시 SMS 인증을 건너뜁니다.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="value"
render={({ field }) => (
<FormItem>
<FormLabel>이메일 주소 또는 도메인</FormLabel>
<FormControl>
<Input
placeholder="예: user@company.com 또는 company.com"
{...field}
/>
</FormControl>
<FormDescription>
이메일 주소나 도메인을 입력하세요. @가 포함되면 개별 이메일로, 그렇지 않으면 도메인 전체로 등록됩니다. 도메인은 최소 1개의 TLD가 필요합니다.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>설명 (선택사항)</FormLabel>
<FormControl>
<Textarea
placeholder="도메인에 대한 설명을 입력하세요"
className="min-h-[80px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => props.onOpenChange?.(false)}
disabled={isCreatePending}
>
취소
</Button>
<Button disabled={isCreatePending}>
{isCreatePending && (
<Loader
className="mr-2 size-4 animate-spin"
aria-hidden="true"
/>
)}
추가
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
|