summaryrefslogtreecommitdiff
path: root/lib/email-template/editor/template-settings.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/email-template/editor/template-settings.tsx')
-rw-r--r--lib/email-template/editor/template-settings.tsx474
1 files changed, 474 insertions, 0 deletions
diff --git a/lib/email-template/editor/template-settings.tsx b/lib/email-template/editor/template-settings.tsx
new file mode 100644
index 00000000..f253f87d
--- /dev/null
+++ b/lib/email-template/editor/template-settings.tsx
@@ -0,0 +1,474 @@
+"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog"
+import { Badge } from "@/components/ui/badge"
+import { Separator } from "@/components/ui/separator"
+import {
+ Save,
+ Trash2,
+ AlertTriangle,
+ Info,
+ Calendar,
+ User,
+ Hash,
+ Copy
+} from "lucide-react"
+import { toast } from "sonner"
+import { useRouter } from "next/navigation"
+import { type TemplateWithVariables } from "@/db/schema"
+import { deleteTemplate, duplicateTemplate, updateTemplateAction } from "../service"
+import { TEMPLATE_CATEGORY_OPTIONS, getCategoryDisplayName } from "../validations"
+
+
+interface TemplateSettingsProps {
+ template: TemplateWithVariables
+ onUpdate: (template: TemplateWithVariables) => void
+}
+
+export function TemplateSettings({ template, onUpdate }: TemplateSettingsProps) {
+ const router = useRouter()
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [formData, setFormData] = React.useState({
+ name: template.name,
+ description: template.description || '',
+ category: template.category || '',
+ sampleData: JSON.stringify(template.sampleData || {}, null, 2)
+ })
+
+ // 폼 데이터 업데이트
+ const updateFormData = (field: keyof typeof formData, value: string) => {
+ setFormData(prev => ({ ...prev, [field]: value }))
+ }
+
+ // 기본 정보 저장
+ const handleSaveBasicInfo = async () => {
+ setIsLoading(true)
+ try {
+ // 샘플 데이터 JSON 파싱 검증
+ let parsedSampleData = {}
+ if (formData.sampleData.trim()) {
+ try {
+ parsedSampleData = JSON.parse(formData.sampleData)
+ } catch (error) {
+ toast.error('샘플 데이터 JSON 형식이 올바르지 않습니다.')
+ setIsLoading(false)
+ return
+ }
+ }
+
+ const result = await updateTemplateAction(template.slug, {
+ name: formData.name,
+ description: formData.description || undefined,
+ sampleData: parsedSampleData,
+ updatedBy: 'current-user-id' // TODO: 실제 사용자 ID
+ })
+
+ if (result.success) {
+ toast.success('템플릿 설정이 저장되었습니다.')
+ onUpdate({
+ ...template,
+ name: formData.name,
+ description: formData.description,
+ sampleData: parsedSampleData,
+ version: template.version + 1
+ })
+ } else {
+ toast.error(result.error || '저장에 실패했습니다.')
+ }
+ } catch (error) {
+ toast.error('저장 중 오류가 발생했습니다.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ // 템플릿 복제
+ const handleDuplicate = async () => {
+ setIsLoading(true)
+ try {
+ const copyName = `${template.name} (복사본)`
+ const copySlug = `${template.slug}-copy-${Date.now()}`
+
+ const result = await duplicateTemplate(
+ template.id,
+ copyName,
+ copySlug,
+ 'current-user-id' // TODO: 실제 사용자 ID
+ )
+
+ if (result.success && result.data) {
+ toast.success('템플릿이 복제되었습니다.')
+ router.push(`/evcp/templates/${result.data.slug}`)
+ } else {
+ toast.error(result.error || '복제에 실패했습니다.')
+ }
+ } catch (error) {
+ toast.error('복제 중 오류가 발생했습니다.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ // 템플릿 삭제
+ const handleDelete = async () => {
+ setIsLoading(true)
+ try {
+ const result = await deleteTemplate(template.id)
+
+ if (result.success) {
+ toast.success('템플릿이 삭제되었습니다.')
+ router.push('/evcp/templates')
+ } else {
+ toast.error(result.error || '삭제에 실패했습니다.')
+ }
+ } catch (error) {
+ toast.error('삭제 중 오류가 발생했습니다.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ // 샘플 데이터 포맷팅
+ const formatSampleData = () => {
+ try {
+ const parsed = JSON.parse(formData.sampleData)
+ const formatted = JSON.stringify(parsed, null, 2)
+ updateFormData('sampleData', formatted)
+ toast.success('JSON이 포맷팅되었습니다.')
+ } catch (error) {
+ toast.error('유효한 JSON이 아닙니다.')
+ }
+ }
+
+ // 기본 샘플 데이터 생성
+ const generateDefaultSampleData = () => {
+ const defaultData: Record<string, any> = {}
+
+ template.variables.forEach(variable => {
+ switch (variable.variableType) {
+ case 'string':
+ defaultData[variable.variableName] = variable.defaultValue || `샘플 ${variable.variableName}`
+ break
+ case 'number':
+ defaultData[variable.variableName] = variable.defaultValue ? parseFloat(variable.defaultValue) : 123
+ break
+ case 'boolean':
+ defaultData[variable.variableName] = variable.defaultValue ? variable.defaultValue === 'true' : true
+ break
+ case 'date':
+ defaultData[variable.variableName] = variable.defaultValue || new Date().toLocaleDateString('ko-KR')
+ break
+ }
+ })
+
+ const formatted = JSON.stringify(defaultData, null, 2)
+ updateFormData('sampleData', formatted)
+ toast.success('기본 샘플 데이터가 생성되었습니다.')
+ }
+
+ return (
+ <div className="space-y-6">
+ {/* 기본 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>기본 정보</CardTitle>
+ <CardDescription>
+ 템플릿의 기본 정보를 수정할 수 있습니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div>
+ <Label htmlFor="name">템플릿 이름</Label>
+ <Input
+ id="name"
+ value={formData.name}
+ onChange={(e) => updateFormData('name', e.target.value)}
+ placeholder="템플릿 이름을 입력하세요"
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="description">설명</Label>
+ <Textarea
+ id="description"
+ value={formData.description}
+ onChange={(e) => updateFormData('description', e.target.value)}
+ placeholder="템플릿에 대한 설명을 입력하세요"
+ className="min-h-[100px]"
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="category">카테고리</Label>
+ <Select
+ value={formData.category || "none"} // 빈 문자열일 때 "none"으로 표시
+ onValueChange={(value) => {
+ // "none"이 선택되면 빈 문자열로 변환
+ updateFormData('category', value === "none" ? "" : value)
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="카테고리를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="none">카테고리 없음</SelectItem> {/* ✅ "none" 사용 */}
+ {TEMPLATE_CATEGORY_OPTIONS.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+</div>
+
+ <div className="flex justify-end">
+ <Button onClick={handleSaveBasicInfo} disabled={isLoading}>
+ <Save className="mr-2 h-4 w-4" />
+ {isLoading ? '저장 중...' : '기본 정보 저장'}
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 샘플 데이터 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>샘플 데이터</CardTitle>
+ <CardDescription>
+ 미리보기에서 사용될 기본 샘플 데이터를 설정합니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="flex items-center gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={formatSampleData}
+ >
+ JSON 포맷팅
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={generateDefaultSampleData}
+ >
+ 기본 데이터 생성
+ </Button>
+ </div>
+
+ <div>
+ <Label htmlFor="sampleData">샘플 데이터 (JSON)</Label>
+ <Textarea
+ id="sampleData"
+ value={formData.sampleData}
+ onChange={(e) => updateFormData('sampleData', e.target.value)}
+ placeholder='{"userName": "홍길동", "email": "user@example.com"}'
+ className="min-h-[200px] font-mono text-sm"
+ />
+ </div>
+
+ <div className="bg-blue-50 p-3 rounded-lg">
+ <p className="text-sm text-blue-800">
+ <Info className="inline h-4 w-4 mr-1" />
+ 샘플 데이터는 템플릿 미리보기에서 기본값으로 사용됩니다.
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 메타 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>메타 정보</CardTitle>
+ <CardDescription>
+ 템플릿의 상세 정보를 확인할 수 있습니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-3">
+ <div className="flex items-center gap-2">
+ <Hash className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm font-medium">Slug:</span>
+ <code className="text-sm bg-muted px-2 py-1 rounded">
+ {template.slug}
+ </code>
+ </div>
+
+ <div className="flex items-center gap-2">
+ <Badge variant="outline">버전 {template.version}</Badge>
+ <Badge variant={template.category ? "default" : "secondary"}>
+ {getCategoryDisplayName(template.category)}
+ </Badge>
+ </div>
+
+ <div className="flex items-center gap-2">
+ <Calendar className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm">
+ 생성일: {new Date(template.createdAt).toLocaleString('ko-KR')}
+ </span>
+ </div>
+
+ <div className="flex items-center gap-2">
+ <Calendar className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm">
+ 수정일: {new Date(template.updatedAt).toLocaleString('ko-KR')}
+ </span>
+ </div>
+ </div>
+
+ <div className="space-y-3">
+ <div className="flex items-center gap-2">
+ <User className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm">
+ 생성자: {template.createdBy}
+ </span>
+ </div>
+
+ <div>
+ <span className="text-sm font-medium">변수 개수:</span>
+ <span className="ml-2 text-sm">{template.variables.length}개</span>
+ </div>
+
+ <div>
+ <span className="text-sm font-medium">필수 변수:</span>
+ <span className="ml-2 text-sm">
+ {template.variables.filter(v => v.isRequired).length}개
+ </span>
+ </div>
+
+ <div>
+ <span className="text-sm font-medium">콘텐츠 길이:</span>
+ <span className="ml-2 text-sm">{template.content.length} 문자</span>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ <Separator />
+
+ {/* 위험한 작업 */}
+ <Card className="border-destructive">
+ <CardHeader>
+ <CardTitle className="text-destructive">위험한 작업</CardTitle>
+ <CardDescription>
+ 다음 작업들은 신중히 수행해주세요. 일부는 되돌릴 수 없습니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="flex items-center justify-between p-4 border rounded-lg">
+ <div>
+ <h4 className="font-medium">템플릿 복제</h4>
+ <p className="text-sm text-muted-foreground">
+ 현재 템플릿을 복사하여 새로운 템플릿을 생성합니다.
+ </p>
+ </div>
+ <Button
+ variant="outline"
+ onClick={handleDuplicate}
+ disabled={isLoading}
+ >
+ <Copy className="mr-2 h-4 w-4" />
+ 복제
+ </Button>
+ </div>
+
+ <div className="flex items-center justify-between p-4 border border-destructive rounded-lg bg-destructive/5">
+ <div>
+ <h4 className="font-medium text-destructive">템플릿 삭제</h4>
+ <p className="text-sm text-muted-foreground">
+ 이 템플릿을 완전히 삭제합니다. 이 작업은 되돌릴 수 없습니다.
+ </p>
+ </div>
+ <AlertDialog>
+ <AlertDialogTrigger asChild>
+ <Button variant="destructive" disabled={isLoading}>
+ <Trash2 className="mr-2 h-4 w-4" />
+ 삭제
+ </Button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>템플릿 삭제 확인</AlertDialogTitle>
+ <AlertDialogDescription>
+ 정말로 <strong>"{template.name}"</strong> 템플릿을 삭제하시겠습니까?
+ <br />
+ <br />
+ 이 작업은 되돌릴 수 없으며, 다음 항목들이 함께 삭제됩니다:
+ <br />
+ • 템플릿 내용 및 설정
+ <br />
+ • 모든 변수 ({template.variables.length}개)
+ <br />
+ • 변경 이력
+ <br />
+ <br />
+ <span className="text-destructive font-medium">
+ 삭제하려면 "영구 삭제"를 클릭하세요.
+ </span>
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleDelete}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ <Trash2 className="mr-2 h-4 w-4" />
+ 영구 삭제
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 주의사항 */}
+ <div className="bg-amber-50 border border-amber-200 p-4 rounded-lg">
+ <div className="flex items-start gap-3">
+ <AlertTriangle className="h-5 w-5 text-amber-600 mt-0.5" />
+ <div>
+ <h3 className="font-semibold text-amber-800">주의사항</h3>
+ <div className="mt-2 text-sm text-amber-700 space-y-1">
+ <p>• 템플릿 설정 변경 시 기존 미리보기가 무효화될 수 있습니다.</p>
+ <p>• 샘플 데이터는 유효한 JSON 형식이어야 합니다.</p>
+ <p>• 템플릿 삭제는 되돌릴 수 없으니 신중히 결정하세요.</p>
+ <p>• 카테고리 변경 시 관련 기본 변수가 영향받을 수 있습니다.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+} \ No newline at end of file