summaryrefslogtreecommitdiff
path: root/lib/email-whitelist/table/update-whitelist-dialog.tsx
blob: 2a798e3086194374403cb1db2767cccf6a21565d (plain)
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
'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 { updateEmailWhitelistAction, type EmailWhitelist } from "../service"

// Validation Schema
const updateWhitelistSchema = 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 UpdateWhitelistSchema = z.infer<typeof updateWhitelistSchema>

interface UpdateWhitelistDialogProps
    extends React.ComponentPropsWithRef<typeof Dialog> {
    whitelist?: EmailWhitelist | null
}

export function UpdateWhitelistDialog({ whitelist, ...props }: UpdateWhitelistDialogProps) {
    const [isUpdatePending, startUpdateTransition] = React.useTransition()
    const { data: session } = useSession();

    const form = useForm<UpdateWhitelistSchema>({
        resolver: zodResolver(updateWhitelistSchema),
        defaultValues: {
            value: "",
            description: "",
        },
    })

    // 화이트리스트 데이터가 변경되면 폼 초기화
    React.useEffect(() => {
        if (whitelist) {
            form.reset({
                value: whitelist.displayValue || "",
                description: whitelist.description || "",
            })
        }
    }, [whitelist, form])

    // 입력값 소문자 변환
    React.useEffect(() => {
        const watchedValue = form.watch("value")
        if (watchedValue && watchedValue !== watchedValue.toLowerCase()) {
            form.setValue("value", watchedValue.toLowerCase())
        }
    }, [form])

    function onSubmit(input: UpdateWhitelistSchema) {
        startUpdateTransition(async () => {
            if (!session?.user?.id) {
                toast.error("로그인이 필요합니다")
                return
            }

            if (!whitelist) {
                toast.error("수정할 데이터를 찾을 수 없습니다")
                return
            }

            const { error } = await updateEmailWhitelistAction({
                id: whitelist.id,
                value: input.value,
                description: input.description,
            })

            if (error) {
                toast.error(error)
                return
            }

            props.onOpenChange?.(false)
            toast.success("화이트리스트 도메인이 수정되었습니다")
        })
    }

    return (
        <Dialog {...props}>
            <DialogContent className="sm:max-w-[425px]">
                <DialogHeader>
                    <DialogTitle>화이트리스트 수정</DialogTitle>
                    <DialogDescription>
                        화이트리스트 정보를 수정합니다.
                    </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={isUpdatePending}
                            >
                                취소
                            </Button>
                            <Button disabled={isUpdatePending}>
                                {isUpdatePending && (
                                    <Loader
                                        className="mr-2 size-4 animate-spin"
                                        aria-hidden="true"
                                    />
                                )}
                                수정
                            </Button>
                        </DialogFooter>
                    </form>
                </Form>
            </DialogContent>
        </Dialog>
    )
}