From a070f833d132e6370311c0bbdad03beb51d595df Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Wed, 15 Oct 2025 21:38:21 +0900
Subject: (김준회) 이메일 화이트리스트 (SMS 우회) 기능 추가 및 기존 로그인 과정
 통합
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../table/create-whitelist-dialog.tsx              | 179 +++++++++++++++++++++
 1 file changed, 179 insertions(+)
 create mode 100644 lib/email-whitelist/table/create-whitelist-dialog.tsx

(limited to 'lib/email-whitelist/table/create-whitelist-dialog.tsx')

diff --git a/lib/email-whitelist/table/create-whitelist-dialog.tsx b/lib/email-whitelist/table/create-whitelist-dialog.tsx
new file mode 100644
index 00000000..d82ac168
--- /dev/null
+++ b/lib/email-whitelist/table/create-whitelist-dialog.tsx
@@ -0,0 +1,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>
+    )
+}
-- 
cgit v1.2.3

