diff options
Diffstat (limited to 'lib/email-template/table/create-template-sheet.tsx')
| -rw-r--r-- | lib/email-template/table/create-template-sheet.tsx | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/lib/email-template/table/create-template-sheet.tsx b/lib/email-template/table/create-template-sheet.tsx new file mode 100644 index 00000000..199e20ab --- /dev/null +++ b/lib/email-template/table/create-template-sheet.tsx @@ -0,0 +1,381 @@ +"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 { useRouter } from "next/navigation" +import { useSession } from "next-auth/react" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" + +import { createTemplateAction } from "../service" +import { TEMPLATE_CATEGORY_OPTIONS } from "../validations" + +// Validation Schema (수정됨) +const createTemplateSchema = z.object({ + name: z.string().min(1, "템플릿 이름은 필수입니다").max(100, "템플릿 이름은 100자 이하여야 합니다"), + slug: z.string() + .min(1, "Slug는 필수입니다") + .max(50, "Slug는 50자 이하여야 합니다") + .regex(/^[a-z0-9-]+$/, "Slug는 소문자, 숫자, 하이픈만 사용 가능합니다"), + description: z.string().max(500, "설명은 500자 이하여야 합니다").optional(), + category: z.string().optional(), // 빈 문자열이나 undefined 모두 허용 +}) + + + +type CreateTemplateSchema = z.infer<typeof createTemplateSchema> + +interface CreateTemplateSheetProps + extends React.ComponentPropsWithRef<typeof Sheet> { +} + +export function CreateTemplateSheet({ ...props }: CreateTemplateSheetProps) { + const [isCreatePending, startCreateTransition] = React.useTransition() + const router = useRouter() + const { data: session } = useSession(); + + // 또는 더 안전하게 + if (!session?.user?.id) { + toast.error("로그인이 필요합니다") + return + } + + const form = useForm<CreateTemplateSchema>({ + resolver: zodResolver(createTemplateSchema), + defaultValues: { + name: "", + slug: "", + description: "", + category: undefined, // 기본값을 undefined로 설정 + }, + }) + + // 이름 입력 시 자동으로 slug 생성 + const watchedName = form.watch("name") + React.useEffect(() => { + if (watchedName && !form.formState.dirtyFields.slug) { + const autoSlug = watchedName + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') // 특수문자 제거 + .replace(/\s+/g, '-') // 공백을 하이픈으로 + .replace(/-+/g, '-') // 연속 하이픈 제거 + .trim() + .slice(0, 50) // 최대 50자 + + form.setValue("slug", autoSlug, { shouldValidate: false }) + } + }, [watchedName, form]) + + // 기본 템플릿 내용 생성 + const getDefaultContent = (category: string, name: string) => { + const templates = { + 'welcome-email': ` +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>환영합니다!</title> +</head> +<body> + <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;"> + <h1>안녕하세요, {{userName}}님!</h1> + <p>${name}에 오신 것을 환영합니다.</p> + <p>{{message}}</p> + </div> +</body> +</html>`, + 'password-reset': ` +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>비밀번호 재설정</title> +</head> +<body> + <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;"> + <h1>비밀번호 재설정</h1> + <p>안녕하세요, {{userName}}님.</p> + <p>비밀번호 재설정을 위해 아래 링크를 클릭해주세요.</p> + <a href="{{resetLink}}">비밀번호 재설정</a> + </div> +</body> +</html>`, + 'notification': ` +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>알림</title> +</head> +<body> + <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;"> + <h1>알림</h1> + <p>안녕하세요, {{userName}}님.</p> + <p>{{message}}</p> + </div> +</body> +</html>`, + } + + return templates[category as keyof typeof templates] || ` +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>${name}</title> +</head> +<body> + <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;"> + <h1>${name}</h1> + <p>안녕하세요, {{userName}}님.</p> + <p>{{message}}</p> + </div> +</body> +</html>` + } + + // 기본 변수 생성 + const getDefaultVariables = (category: string) => { + const variableTemplates = { + 'welcome-email': [ + { variableName: 'userName', variableType: 'string', isRequired: true, description: '사용자 이름' }, + { variableName: 'email', variableType: 'string', isRequired: true, description: '사용자 이메일' }, + { variableName: 'message', variableType: 'string', isRequired: false, description: '환영 메시지' }, + ], + 'password-reset': [ + { variableName: 'userName', variableType: 'string', isRequired: true, description: '사용자 이름' }, + { variableName: 'expiryTime', variableType: 'string', isRequired: true, description: '링크 유효 시간' }, + { variableName: 'resetLink', variableType: 'string', isRequired: true, description: '재설정 URL' }, + { variableName: 'supportEmail', variableType: 'string', isRequired: true, description: 'eVCP 서포터 이메일' }, + ], + 'notification': [ + { variableName: 'userName', variableType: 'string', isRequired: true, description: '사용자 이름' }, + { variableName: 'email', variableType: 'string', isRequired: true, description: '사용자 이메일' }, + { variableName: 'message', variableType: 'string', isRequired: true, description: '알림 메시지' }, + ], + } + + return variableTemplates[category as keyof typeof variableTemplates] || [ + { variableName: 'userName', variableType: 'string', isRequired: true, description: '사용자 이름' }, + { variableName: 'message', variableType: 'string', isRequired: false, description: '메시지 내용' }, + ] + } + + const getDefaultSubject = (category: string, name: string) => { + const subjectTemplates = { + 'welcome-email': '{{siteName}}에 오신 것을 환영합니다, {{userName}}님!', + 'password-reset': '{{userName}}님의 비밀번호 재설정 요청', + 'notification': '[{{notificationType}}] {{title}}', + 'invoice': '{{companyName}} 인보이스 #{{invoiceNumber}}', + 'marketing': '{{title}} - {{siteName}}', + 'system': '[시스템] {{title}}' + } + + return subjectTemplates[category as keyof typeof subjectTemplates] || + `${name} - {{siteName}}` + } + + + function onSubmit(input: CreateTemplateSchema) { + startCreateTransition(async () => { + if (!session?.user?.id) { + toast.error("로그인이 필요합니다") + return + } + + + const defaultContent = getDefaultContent(input.category || '', input.name) + const defaultVariables = getDefaultVariables(input.category || '') + const defaultSubject = getDefaultSubject(input.category || '', input.name) + + const { error, data } = await createTemplateAction({ + name: input.name, + slug: input.slug, + subject: defaultSubject, + content: defaultContent, + description: input.description, + category: input.category || undefined, // 빈 문자열 대신 undefined 전달 + sampleData: { + userName: '홍길동', + email: 'user@example.com', + message: '샘플 메시지입니다.', + }, + createdBy: Number(session.user.id), + variables: defaultVariables, + }) + + if (error) { + toast.error(error) + return + } + + form.reset() + props.onOpenChange?.(false) + toast.success("템플릿이 생성되었습니다") + + // 생성된 템플릿의 세부 페이지로 이동 + if (data?.slug) { + router.push(`/evcp/email-template/${data.slug}`) + } else { + window.location.reload() + } + }) + } + + return ( + <Sheet {...props}> + <SheetContent className="flex flex-col gap-6 sm:max-w-md"> + <SheetHeader className="text-left"> + <SheetTitle>새 템플릿 생성</SheetTitle> + <SheetDescription> + 새로운 이메일 템플릿을 생성합니다. 기본 구조가 자동으로 생성되며, 생성 후 세부 내용을 편집할 수 있습니다. + </SheetDescription> + </SheetHeader> + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="flex flex-col gap-4" + > + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>템플릿 이름</FormLabel> + <FormControl> + <Input + placeholder="예: 신규 회원 환영 메일" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="slug" + render={({ field }) => ( + <FormItem> + <FormLabel>Slug</FormLabel> + <FormControl> + <Input + placeholder="예: welcome-new-member" + {...field} + /> + </FormControl> + <FormDescription> + URL에 사용될 고유 식별자입니다. 소문자, 숫자, 하이픈만 사용 가능합니다. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="category" + render={({ field }) => ( + <FormItem> + <FormLabel>카테고리</FormLabel> + <Select + onValueChange={(value) => { + // "none" 값이 선택되면 undefined로 설정 + field.onChange(value === "none" ? undefined : value) + }} + value={field.value || "none"} // undefined인 경우 "none"으로 표시 + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="카테고리를 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="none">카테고리 없음</SelectItem> + {TEMPLATE_CATEGORY_OPTIONS.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormDescription> + 카테고리에 따라 기본 템플릿과 변수가 자동으로 생성됩니다. + </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> + )} + /> + + <SheetFooter className="gap-2 pt-2 sm:space-x-0"> + <SheetClose asChild> + <Button type="button" variant="outline"> + 취소 + </Button> + </SheetClose> + <Button disabled={isCreatePending}> + {isCreatePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 생성 후 편집하기 + </Button> + </SheetFooter> + </form> + </Form> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
