summaryrefslogtreecommitdiff
path: root/lib/email-template
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-29 08:32:16 +0000
committerjoonhoekim <26rote@gmail.com>2025-09-29 08:32:16 +0000
commitd49238b9a7d5bc4b396c5fea57ce9e545677007c (patch)
treee8ec49c4d70a8268a0085a0df64e8e7e288e85ca /lib/email-template
parentc7d37ec3e60c9197abc79738316ddae7c5bf8817 (diff)
(고건) 이메일 템플릿 변수 수정 및 삭제 로직 추가
Diffstat (limited to 'lib/email-template')
-rw-r--r--lib/email-template/editor/template-variable-manager.tsx1067
-rw-r--r--lib/email-template/service.ts123
2 files changed, 664 insertions, 526 deletions
diff --git a/lib/email-template/editor/template-variable-manager.tsx b/lib/email-template/editor/template-variable-manager.tsx
index 9b86dd5e..b7daf7bb 100644
--- a/lib/email-template/editor/template-variable-manager.tsx
+++ b/lib/email-template/editor/template-variable-manager.tsx
@@ -1,562 +1,577 @@
-"use client"
+'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 */
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Checkbox } from "@/components/ui/checkbox"
+ addTemplateVariableAction,
+ modifyTemplateVariableAction,
+ removeTemplateVariableAction,
+} from '../service';
import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Copy, Edit, GripVertical, Plus, Trash2 } from 'lucide-react';
import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "@/components/ui/alert-dialog"
-import { Badge } from "@/components/ui/badge"
-import { Plus, Edit, Trash2, GripVertical, Copy } from "lucide-react"
-import { toast } from "sonner"
-import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"
-import { type TemplateWithVariables, type TemplateVariable } from "@/db/schema"
-import { addTemplateVariableAction } from "../service"
-
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { Textarea } from '@/components/ui/textarea';
+import { toast } from 'sonner';
+import { type TemplateWithVariables, type TemplateVariable } from '@/db/schema';
+import { useState } from 'react';
+
+// ----------------------------------------------------------------------------------------------------
+
+/* TYPES */
interface TemplateVariableManagerProps {
- template: TemplateWithVariables
- onUpdate: (template: TemplateWithVariables) => void
+ template: TemplateWithVariables;
+ onUpdate: (template: TemplateWithVariables) => void;
}
interface VariableFormData {
- variableName: string
- variableType: 'string' | 'number' | 'boolean' | 'date'
- defaultValue: string
- isRequired: boolean
- description: string
+ variableName: string;
+ variableType: 'string' | 'number' | 'boolean' | 'date';
+ defaultValue: string;
+ isRequired: boolean;
+ description: string;
}
+// ----------------------------------------------------------------------------------------------------
+
export function TemplateVariableManager({ template, onUpdate }: TemplateVariableManagerProps) {
- const [variables, setVariables] = React.useState(template.variables)
- // const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false)
- const [editingVariable, setEditingVariable] = React.useState<TemplateVariable | null>(null)
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [isDialogOpen, setIsDialogOpen] = React.useState(false) // 이름 변경
-
- const [formData, setFormData] = React.useState<VariableFormData>({
- variableName: '',
- variableType: 'string',
- defaultValue: '',
- isRequired: false,
- description: ''
+ const [variables, setVariables] = useState(template.variables)
+ // const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
+ const [editingVariable, setEditingVariable] = useState<TemplateVariable | null>(null);
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isDialogOpen, setIsDialogOpen] = useState(false) // 이름 변경
+
+ const [formData, setFormData] = useState<VariableFormData>({
+ variableName: '',
+ variableType: 'string',
+ defaultValue: '',
+ isRequired: false,
+ description: ''
+ })
+
+ const isEditMode = editingVariable !== null;
+
+ // 폼 초기화
+ const resetForm = () => {
+ setFormData({
+ variableName: '',
+ variableType: 'string',
+ defaultValue: '',
+ isRequired: false,
+ description: ''
+ })
+ setEditingVariable(null)
+ }
+
+ const handleEditVariable = (variable: TemplateVariable) => {
+ setEditingVariable(variable);
+ setFormData({
+ variableName: variable.variableName,
+ variableType: variable.variableType as any,
+ defaultValue: variable.defaultValue || '',
+ isRequired: variable.isRequired || false,
+ description: variable.description || ''
})
+ setIsDialogOpen(true);
+ }
- const isEditMode = editingVariable !== null
-
- // 폼 초기화
- const resetForm = () => {
- setFormData({
- variableName: '',
- variableType: 'string',
- defaultValue: '',
- isRequired: false,
- description: ''
- })
- setEditingVariable(null)
+ const handleSubmitVariable = async () => {
+ if (!formData.variableName.trim()) {
+ toast.error('변수명을 입력해주세요.');
+ return;
}
- const handleEditVariable = (variable: TemplateVariable) => {
- setEditingVariable(variable)
- setFormData({
- variableName: variable.variableName,
- variableType: variable.variableType as any,
- defaultValue: variable.defaultValue || '',
- isRequired: variable.isRequired || false,
- description: variable.description || ''
- })
- setIsDialogOpen(true)
+ // 편집 모드가 아닐 때만 중복 검사
+ if (!isEditMode && variables.some(v => v.variableName === formData.variableName)) {
+ toast.error('이미 존재하는 변수명입니다.');
+ return;
}
+ // 편집 모드일 때 다른 변수와 중복되는지 검사
+ if (isEditMode && variables.some(v => v.id !== editingVariable!.id && v.variableName === formData.variableName)) {
+ toast.error('이미 존재하는 변수명입니다.');
+ return;
+ }
- const handleSubmitVariable = async () => {
- if (!formData.variableName.trim()) {
- toast.error('변수명을 입력해주세요.')
- return
- }
-
- // 편집 모드가 아닐 때만 중복 검사
- if (!isEditMode && variables.some(v => v.variableName === formData.variableName)) {
- toast.error('이미 존재하는 변수명입니다.')
- return
- }
+ // 변수명 유효성 검사
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.variableName)) {
+ toast.error('변수명은 영문자, 숫자, 언더스코어만 사용 가능하며 숫자로 시작할 수 없습니다.');
+ return;
+ }
- // 편집 모드일 때 다른 변수와 중복되는지 검사
- if (isEditMode && variables.some(v => v.id !== editingVariable!.id && v.variableName === formData.variableName)) {
- toast.error('이미 존재하는 변수명입니다.')
- return
+ setIsSubmitting(true);
+
+ try {
+ if (isEditMode) {
+ const modifyRes = await modifyTemplateVariableAction(template.slug, editingVariable!.id, formData);
+
+ if (modifyRes.success && modifyRes.data) {
+ const modifiedVariable = modifyRes.data;
+ const modifiedVariables = variables.map(v =>
+ v.id === modifiedVariable.id
+ ? { ...v, ...formData }
+ : v
+ );
+ setVariables(modifiedVariables);
+ onUpdate({ ...template, variables: modifiedVariables });
+ toast.success('변수가 수정되었습니다.');
+ } else {
+ toast.error(modifyRes.error || '변수 수정에 실패했습니다.');
+ return;
}
-
- // 변수명 유효성 검사
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.variableName)) {
- toast.error('변수명은 영문자, 숫자, 언더스코어만 사용 가능하며 숫자로 시작할 수 없습니다.')
- return
+ } else {
+ const addRes = await addTemplateVariableAction(template.slug, {
+ variableName: formData.variableName,
+ variableType: formData.variableType,
+ defaultValue: formData.defaultValue || undefined,
+ isRequired: formData.isRequired,
+ description: formData.description || undefined,
+ });
+
+ if (addRes.success && addRes.data) {
+ const addedVariable = addRes.data;
+ const addedVariables = [...variables, addedVariable];
+ setVariables(addedVariables);
+ onUpdate({ ...template, variables: addedVariables });
+ toast.success('변수가 추가되었습니다.');
+ } else {
+ toast.error(addRes.error || '변수 추가에 실패했습니다.');
+ return;
}
+ }
- setIsSubmitting(true)
- try {
- if (isEditMode) {
- // 편집 모드 - TODO: updateTemplateVariableAction 구현 필요
- // const result = await updateTemplateVariableAction(template.slug, editingVariable!.id, formData)
-
- // 임시로 클라이언트 사이드에서 업데이트
- const updatedVariables = variables.map(v =>
- v.id === editingVariable!.id
- ? { ...v, ...formData }
- : v
- )
- setVariables(updatedVariables)
- onUpdate({ ...template, variables: updatedVariables })
- toast.success('변수가 수정되었습니다.')
- } else {
- // 추가 모드
- const result = await addTemplateVariableAction(template.slug, {
- variableName: formData.variableName,
- variableType: formData.variableType,
- defaultValue: formData.defaultValue || undefined,
- isRequired: formData.isRequired,
- description: formData.description || undefined,
- })
-
- if (result.success) {
- const newVariable = result.data
- const updatedVariables = [...variables, newVariable]
- setVariables(updatedVariables)
- onUpdate({ ...template, variables: updatedVariables })
- toast.success('변수가 추가되었습니다.')
- } else {
- toast.error(result.error || '변수 추가에 실패했습니다.')
- return
- }
- }
-
- setIsDialogOpen(false)
- resetForm()
- } catch (error) {
- toast.error(`변수 ${isEditMode ? '수정' : '추가'} 중 오류가 발생했습니다.`)
- } finally {
- setIsSubmitting(false)
- }
- }
+ setIsDialogOpen(false);
+ resetForm();
- // Dialog 닫기 처리
- const handleDialogClose = (open: boolean) => {
- setIsDialogOpen(open)
- if (!open) {
- resetForm()
- }
- }
+ } catch (error) {
+ toast.error(`변수 ${isEditMode ? '수정' : '추가'} 중 오류가 발생했습니다.`);
- // 변수 추가
- // const handleAddVariable = async () => {
- // if (!formData.variableName.trim()) {
- // toast.error('변수명을 입력해주세요.')
- // return
- // }
-
- // // 변수명 중복 검사
- // if (variables.some(v => v.variableName === formData.variableName)) {
- // toast.error('이미 존재하는 변수명입니다.')
- // return
- // }
-
- // // 변수명 유효성 검사
- // if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.variableName)) {
- // toast.error('변수명은 영문자, 숫자, 언더스코어만 사용 가능하며 숫자로 시작할 수 없습니다.')
- // return
- // }
-
- // setIsSubmitting(true)
- // try {
- // const result = await addTemplateVariableAction(template.slug, {
- // variableName: formData.variableName,
- // variableType: formData.variableType,
- // defaultValue: formData.defaultValue || undefined,
- // isRequired: formData.isRequired,
- // description: formData.description || undefined,
- // })
-
- // if (result.success) {
- // const newVariable = result.data
- // const updatedVariables = [...variables, newVariable]
- // setVariables(updatedVariables)
- // onUpdate({ ...template, variables: updatedVariables })
-
- // toast.success('변수가 추가되었습니다.')
- // setIsAddDialogOpen(false)
- // resetForm()
- // } else {
- // toast.error(result.error || '변수 추가에 실패했습니다.')
- // }
- // } catch (error) {
- // toast.error('변수 추가 중 오류가 발생했습니다.')
- // } finally {
- // setIsSubmitting(false)
- // }
- // }
-
- // 변수 순서 변경
- const handleDragEnd = (result: any) => {
- if (!result.destination) return
-
- const items = Array.from(variables)
- const [reorderedItem] = items.splice(result.source.index, 1)
- items.splice(result.destination.index, 0, reorderedItem)
-
- // displayOrder 업데이트
- const updatedItems = items.map((item, index) => ({
- ...item,
- displayOrder: index
- }))
-
- setVariables(updatedItems)
- onUpdate({ ...template, variables: updatedItems })
-
- // TODO: 서버에 순서 변경 요청
- toast.success('변수 순서가 변경되었습니다.')
+ } finally {
+ setIsSubmitting(false);
}
+ }
- // 변수 복사
- const handleCopyVariable = (variable: TemplateVariable) => {
- const copyName = `${variable.variableName}_copy`
- setFormData({
- variableName: copyName,
- variableType: variable.variableType as any,
- defaultValue: variable.defaultValue || '',
- isRequired: variable.isRequired || false,
- description: variable.description || ''
- })
- setIsDialogOpen(true)
+ // Dialog 닫기 처리
+ const handleDialogClose = (open: boolean) => {
+ setIsDialogOpen(open);
+ if (!open) {
+ resetForm();
}
-
- // 변수 삭제
- const handleDeleteVariable = async (variableId: string) => {
- // TODO: 서버에서 변수 삭제 구현
- const updatedVariables = variables.filter(v => v.id !== variableId)
- setVariables(updatedVariables)
- onUpdate({ ...template, variables: updatedVariables })
- toast.success('변수가 삭제되었습니다.')
+ }
+
+ // 변수 추가
+ // const handleAddVariable = async () => {
+ // if (!formData.variableName.trim()) {
+ // toast.error('변수명을 입력해주세요.')
+ // return
+ // }
+
+ // // 변수명 중복 검사
+ // if (variables.some(v => v.variableName === formData.variableName)) {
+ // toast.error('이미 존재하는 변수명입니다.')
+ // return
+ // }
+
+ // // 변수명 유효성 검사
+ // if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.variableName)) {
+ // toast.error('변수명은 영문자, 숫자, 언더스코어만 사용 가능하며 숫자로 시작할 수 없습니다.')
+ // return
+ // }
+
+ // setIsSubmitting(true)
+ // try {
+ // const result = await addTemplateVariableAction(template.slug, {
+ // variableName: formData.variableName,
+ // variableType: formData.variableType,
+ // defaultValue: formData.defaultValue || undefined,
+ // isRequired: formData.isRequired,
+ // description: formData.description || undefined,
+ // })
+
+ // if (result.success) {
+ // const newVariable = result.data
+ // const updatedVariables = [...variables, newVariable]
+ // setVariables(updatedVariables)
+ // onUpdate({ ...template, variables: updatedVariables })
+
+ // toast.success('변수가 추가되었습니다.')
+ // setIsAddDialogOpen(false)
+ // resetForm()
+ // } else {
+ // toast.error(result.error || '변수 추가에 실패했습니다.')
+ // }
+ // } catch (error) {
+ // toast.error('변수 추가 중 오류가 발생했습니다.')
+ // } finally {
+ // setIsSubmitting(false)
+ // }
+ // }
+
+ // 변수 순서 변경
+ const handleDragEnd = (result: any) => {
+ if (!result.destination) return
+
+ const items = Array.from(variables)
+ const [reorderedItem] = items.splice(result.source.index, 1)
+ items.splice(result.destination.index, 0, reorderedItem)
+
+ // displayOrder 업데이트
+ const updatedItems = items.map((item, index) => ({
+ ...item,
+ displayOrder: index
+ }))
+
+ setVariables(updatedItems)
+ onUpdate({ ...template, variables: updatedItems })
+
+ // TODO: 서버에 순서 변경 요청
+ toast.success('변수 순서가 변경되었습니다.')
+ }
+
+ // 변수 복사
+ const handleCopyVariable = (variable: TemplateVariable) => {
+ const copyName = `${variable.variableName}_copy`
+ setFormData({
+ variableName: copyName,
+ variableType: variable.variableType as any,
+ defaultValue: variable.defaultValue || '',
+ isRequired: variable.isRequired || false,
+ description: variable.description || ''
+ })
+ setIsDialogOpen(true)
+ }
+
+ // 변수 삭제
+ const handleDeleteVariable = async (variableId: string) => {
+ const removeRes = await removeTemplateVariableAction(template.slug, variableId);
+
+ if (removeRes.success) {
+ const removedVariables = variables.filter(v => v.id !== variableId);
+ setVariables(removedVariables);
+ onUpdate({ ...template, variables: removedVariables });
+ toast.success('변수가 삭제되었습니다.');
+ } else {
+ toast.error(removeRes.error || '변수 삭제에 실패했습니다.');
}
-
- // 변수 타입에 따른 기본값 예시
- const getDefaultValuePlaceholder = (type: string) => {
- switch (type) {
- case 'string': return '예: 홍길동'
- case 'number': return '예: 123'
- case 'boolean': return 'true 또는 false'
- case 'date': return '예: 2025-01-01'
- default: return ''
- }
+ }
+
+ // 변수 타입에 따른 기본값 예시
+ const getDefaultValuePlaceholder = (type: string) => {
+ switch (type) {
+ case 'string': return '예: 홍길동'
+ case 'number': return '예: 123'
+ case 'boolean': return 'true 또는 false'
+ case 'date': return '예: 2025-01-01'
+ default: return ''
}
-
- // 변수 타입별 아이콘
- const getVariableTypeIcon = (type: string) => {
- switch (type) {
- case 'string': return '📝'
- case 'number': return '🔢'
- case 'boolean': return '✅'
- case 'date': return '📅'
- default: return '❓'
- }
+ }
+
+ // 변수 타입별 아이콘
+ const getVariableTypeIcon = (type: string) => {
+ switch (type) {
+ case 'string': return '📝'
+ case 'number': return '🔢'
+ case 'boolean': return '✅'
+ case 'date': return '📅'
+ default: return '❓'
}
+ }
+
+ return (
+ <div className="space-y-6">
+ {/* 헤더 */}
+ <div className="flex items-center justify-between">
+ <div>
+ {/* <h3 className="text-lg font-semibold">변수 관리</h3> */}
+ <p className="text-sm text-muted-foreground">
+ 총 {variables.length}개의 변수가 등록되어 있습니다.
+ </p>
+ </div>
- return (
- <div className="space-y-6">
- {/* 헤더 */}
- <div className="flex items-center justify-between">
- <div>
- {/* <h3 className="text-lg font-semibold">변수 관리</h3> */}
- <p className="text-sm text-muted-foreground">
- 총 {variables.length}개의 변수가 등록되어 있습니다.
- </p>
- </div>
-
- <Dialog open={isDialogOpen} onOpenChange={handleDialogClose}>
- <DialogTrigger asChild>
- <Button onClick={() => { resetForm(); setIsDialogOpen(true) }}>
- <Plus className="mr-2 h-4 w-4" />
- 변수 추가
- </Button>
- </DialogTrigger>
- <DialogContent className="sm:max-w-md">
- <DialogHeader>
- <DialogTitle>
- {isEditMode ? '변수 수정' : '새 변수 추가'}
- </DialogTitle>
- <DialogDescription>
- {isEditMode
- ? '기존 변수의 정보를 수정합니다.'
- : '템플릿에서 사용할 새로운 변수를 추가합니다.'
- }
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4">
- <div>
- <Label htmlFor="variableName">변수명</Label>
- <Input
- id="variableName"
- value={formData.variableName}
- onChange={(e) => setFormData(prev => ({ ...prev, variableName: e.target.value }))}
- placeholder="예: userName, orderDate"
- />
- </div>
-
- <div>
- <Label htmlFor="variableType">타입</Label>
- <Select
- value={formData.variableType}
- onValueChange={(value) => setFormData(prev => ({ ...prev, variableType: value as any }))}
- >
- <SelectTrigger>
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="string">📝 문자열</SelectItem>
- <SelectItem value="number">🔢 숫자</SelectItem>
- <SelectItem value="boolean">✅ 불린</SelectItem>
- <SelectItem value="date">📅 날짜</SelectItem>
- </SelectContent>
- </Select>
- </div>
-
- <div>
- <Label htmlFor="defaultValue">기본값</Label>
- <Input
- id="defaultValue"
- value={formData.defaultValue}
- onChange={(e) => setFormData(prev => ({ ...prev, defaultValue: e.target.value }))}
- placeholder={getDefaultValuePlaceholder(formData.variableType)}
- />
- </div>
-
- <div>
- <Label htmlFor="description">설명</Label>
- <Textarea
- id="description"
- value={formData.description}
- onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
- placeholder="변수에 대한 설명을 입력하세요"
- className="min-h-[80px]"
- />
- </div>
-
- <div className="flex items-center space-x-2">
- <Checkbox
- id="isRequired"
- checked={formData.isRequired}
- onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isRequired: !!checked }))}
- />
- <Label htmlFor="isRequired">필수 변수</Label>
- </div>
- </div>
-
- <DialogFooter>
- <Button
- variant="outline"
- onClick={() => handleDialogClose(false)}
- >
- 취소
- </Button>
- <Button
- onClick={handleSubmitVariable}
- disabled={isSubmitting}
- >
- {isSubmitting
- ? `${isEditMode ? '수정' : '추가'} 중...`
- : isEditMode ? '수정' : '추가'
- }
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
+ <Dialog open={isDialogOpen} onOpenChange={handleDialogClose}>
+ <DialogTrigger asChild>
+ <Button onClick={() => { resetForm(); setIsDialogOpen(true) }}>
+ <Plus className="mr-2 h-4 w-4" />
+ 변수 추가
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle>
+ {isEditMode ? '변수 수정' : '새 변수 추가'}
+ </DialogTitle>
+ <DialogDescription>
+ {isEditMode
+ ? '기존 변수의 정보를 수정합니다.'
+ : '템플릿에서 사용할 새로운 변수를 추가합니다.'
+ }
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div>
+ <Label htmlFor="variableName">변수명</Label>
+ <Input
+ id="variableName"
+ value={formData.variableName}
+ onChange={(e) => setFormData(prev => ({ ...prev, variableName: e.target.value }))}
+ placeholder="예: userName, orderDate"
+ />
+ </div>
+ <div>
+ <Label htmlFor="variableType">타입</Label>
+ <Select
+ value={formData.variableType}
+ onValueChange={(value) => setFormData(prev => ({ ...prev, variableType: value as any }))}
+ >
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="string">📝 문자열</SelectItem>
+ <SelectItem value="number">🔢 숫자</SelectItem>
+ <SelectItem value="boolean">✅ 불린</SelectItem>
+ <SelectItem value="date">📅 날짜</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ <div>
+ <Label htmlFor="defaultValue">기본값</Label>
+ <Input
+ id="defaultValue"
+ value={formData.defaultValue}
+ onChange={(e) => setFormData(prev => ({ ...prev, defaultValue: e.target.value }))}
+ placeholder={getDefaultValuePlaceholder(formData.variableType)}
+ />
+ </div>
+ <div>
+ <Label htmlFor="description">설명</Label>
+ <Textarea
+ id="description"
+ value={formData.description}
+ onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
+ placeholder="변수에 대한 설명을 입력하세요"
+ className="min-h-[80px]"
+ />
+ </div>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="isRequired"
+ checked={formData.isRequired}
+ onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isRequired: !!checked }))}
+ />
+ <Label htmlFor="isRequired">필수 변수</Label>
+ </div>
</div>
-
- {/* 변수 목록 */}
- {variables.length === 0 ? (
- <div className="text-center py-12 border border-dashed rounded-lg">
- <div className="text-muted-foreground">
- <Plus className="h-12 w-12 mx-auto mb-4 opacity-50" />
- <p className="text-lg font-medium">등록된 변수가 없습니다</p>
- <p className="text-sm">첫 번째 변수를 추가해보세요.</p>
- </div>
- </div>
- ) : (
- <DragDropContext onDragEnd={handleDragEnd}>
- <Droppable droppableId="variables">
- {(provided) => (
- <div
- {...provided.droppableProps}
- ref={provided.innerRef}
- className="border rounded-lg"
- >
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead className="w-10"></TableHead>
- <TableHead>변수명</TableHead>
- <TableHead>타입</TableHead>
- <TableHead>기본값</TableHead>
- <TableHead>필수</TableHead>
- <TableHead>설명</TableHead>
- <TableHead className="w-24">작업</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {variables.map((variable, index) => (
- <Draggable
- key={variable.id}
- draggableId={variable.id}
- index={index}
- >
- {(provided, snapshot) => (
- <TableRow
- ref={provided.innerRef}
- {...provided.draggableProps}
- className={snapshot.isDragging ? 'bg-muted' : ''}
- >
- <TableCell>
- <div
- {...provided.dragHandleProps}
- className="cursor-grab hover:cursor-grabbing"
- >
- <GripVertical className="h-4 w-4 text-muted-foreground" />
- </div>
- </TableCell>
- <TableCell>
- <div className="font-mono text-sm">
- {variable.variableName}
- </div>
- </TableCell>
- <TableCell>
- <Badge variant="outline" className="gap-1">
- {getVariableTypeIcon(variable.variableType)}
- {variable.variableType}
- </Badge>
- </TableCell>
- <TableCell>
- <div className="text-sm text-muted-foreground max-w-[100px] truncate">
- {variable.defaultValue || '-'}
- </div>
- </TableCell>
- <TableCell>
- {variable.isRequired ? (
- <Badge variant="destructive" className="text-xs">필수</Badge>
- ) : (
- <Badge variant="secondary" className="text-xs">선택</Badge>
- )}
- </TableCell>
- <TableCell>
- <div className="text-sm text-muted-foreground max-w-[200px] truncate">
- {variable.description || '-'}
- </div>
- </TableCell>
- <TableCell>
- <div className="flex items-center gap-1">
- <Button
- variant="ghost"
- size="icon"
- className="h-8 w-8"
- onClick={() => handleCopyVariable(variable)}
- >
- <Copy className="h-3 w-3" />
- </Button>
- <Button
- variant="ghost"
- size="icon"
- className="h-8 w-8"
- onClick={() => handleEditVariable(variable)}
- >
- <Edit className="h-3 w-3" />
- </Button>
- <AlertDialog>
- <AlertDialogTrigger asChild>
- <Button
- variant="ghost"
- size="icon"
- className="h-8 w-8 text-destructive hover:text-destructive"
- >
- <Trash2 className="h-3 w-3" />
- </Button>
- </AlertDialogTrigger>
- <AlertDialogContent>
- <AlertDialogHeader>
- <AlertDialogTitle>변수 삭제</AlertDialogTitle>
- <AlertDialogDescription>
- 정말로 '{variable.variableName}' 변수를 삭제하시겠습니까?
- 이 작업은 되돌릴 수 없습니다.
- </AlertDialogDescription>
- </AlertDialogHeader>
- <AlertDialogFooter>
- <AlertDialogCancel>취소</AlertDialogCancel>
- <AlertDialogAction
- onClick={() => handleDeleteVariable(variable.id)}
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
- >
- 삭제
- </AlertDialogAction>
- </AlertDialogFooter>
- </AlertDialogContent>
- </AlertDialog>
- </div>
- </TableCell>
- </TableRow>
- )}
- </Draggable>
- ))}
- {provided.placeholder}
- </TableBody>
- </Table>
- </div>
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => handleDialogClose(false)}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleSubmitVariable}
+ disabled={isSubmitting}
+ >
+ {isSubmitting
+ ? `${isEditMode ? '수정' : '추가'} 중...`
+ : isEditMode ? '수정' : '추가'
+ }
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </div>
+
+ {/* 변수 목록 */}
+ {variables.length === 0 ? (
+ <div className="text-center py-12 border border-dashed rounded-lg">
+ <div className="text-muted-foreground">
+ <Plus className="h-12 w-12 mx-auto mb-4 opacity-50" />
+ <p className="text-lg font-medium">등록된 변수가 없습니다</p>
+ <p className="text-sm">첫 번째 변수를 추가해보세요.</p>
+ </div>
+ </div>
+ ) : (
+ <DragDropContext onDragEnd={handleDragEnd}>
+ <Droppable droppableId="variables">
+ {(provided) => (
+ <div
+ {...provided.droppableProps}
+ ref={provided.innerRef}
+ className="border rounded-lg"
+ >
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-10"></TableHead>
+ <TableHead>변수명</TableHead>
+ <TableHead>타입</TableHead>
+ <TableHead>기본값</TableHead>
+ <TableHead>필수</TableHead>
+ <TableHead>설명</TableHead>
+ <TableHead className="w-24">작업</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {variables.map((variable, index) => (
+ <Draggable
+ key={variable.id}
+ draggableId={variable.id}
+ index={index}
+ >
+ {(provided, snapshot) => (
+ <TableRow
+ ref={provided.innerRef}
+ {...provided.draggableProps}
+ className={snapshot.isDragging ? 'bg-muted' : ''}
+ >
+ <TableCell>
+ <div
+ {...provided.dragHandleProps}
+ className="cursor-grab hover:cursor-grabbing"
+ >
+ <GripVertical className="h-4 w-4 text-muted-foreground" />
+ </div>
+ </TableCell>
+ <TableCell>
+ <div className="font-mono text-sm">
+ {variable.variableName}
+ </div>
+ </TableCell>
+ <TableCell>
+ <Badge variant="outline" className="gap-1">
+ {getVariableTypeIcon(variable.variableType)}
+ {variable.variableType}
+ </Badge>
+ </TableCell>
+ <TableCell>
+ <div className="text-sm text-muted-foreground max-w-[100px] truncate">
+ {variable.defaultValue || '-'}
+ </div>
+ </TableCell>
+ <TableCell>
+ {variable.isRequired ? (
+ <Badge variant="destructive" className="text-xs">필수</Badge>
+ ) : (
+ <Badge variant="secondary" className="text-xs">선택</Badge>
+ )}
+ </TableCell>
+ <TableCell>
+ <div className="text-sm text-muted-foreground max-w-[200px] truncate">
+ {variable.description || '-'}
+ </div>
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-1">
+ <Button
+ variant="ghost"
+ size="icon"
+ className="h-8 w-8"
+ onClick={() => handleCopyVariable(variable)}
+ >
+ <Copy className="h-3 w-3" />
+ </Button>
+ <Button
+ variant="ghost"
+ size="icon"
+ className="h-8 w-8"
+ onClick={() => handleEditVariable(variable)}
+ >
+ <Edit className="h-3 w-3" />
+ </Button>
+ <AlertDialog>
+ <AlertDialogTrigger asChild>
+ <Button
+ variant="ghost"
+ size="icon"
+ className="h-8 w-8 text-destructive hover:text-destructive"
+ >
+ <Trash2 className="h-3 w-3" />
+ </Button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>변수 삭제</AlertDialogTitle>
+ <AlertDialogDescription>
+ 정말로 '{variable.variableName}' 변수를 삭제하시겠습니까?
+ 이 작업은 되돌릴 수 없습니다.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={() => handleDeleteVariable(variable.id)}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ 삭제
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ </div>
+ </TableCell>
+ </TableRow>
)}
- </Droppable>
- </DragDropContext>
+ </Draggable>
+ ))}
+ {provided.placeholder}
+ </TableBody>
+ </Table>
+ </div>
)}
-
- {/* 도움말 */}
- <div className="bg-blue-50 p-4 rounded-lg">
- <h4 className="font-medium text-blue-900 mb-2">변수 사용법</h4>
- <div className="text-sm text-blue-800 space-y-1">
- <p>• 템플릿에서 <code className="bg-blue-100 px-1 rounded">{`{{variableName}}`}</code> 형태로 사용</p>
- <p>• 드래그 앤 드롭으로 변수 순서 변경 가능</p>
- <p>• 필수 변수는 반드시 값이 제공되어야 함</p>
- <p>• 변수명은 영문자, 숫자, 언더스코어만 사용 가능</p>
- </div>
- </div>
+ </Droppable>
+ </DragDropContext>
+ )}
+
+ {/* 도움말 */}
+ <div className="bg-blue-50 p-4 rounded-lg">
+ <h4 className="font-medium text-blue-900 mb-2">변수 사용법</h4>
+ <div className="text-sm text-blue-800 space-y-1">
+ <p>• 템플릿에서 <code className="bg-blue-100 px-1 rounded">{`{{variableName}}`}</code> 형태로 사용</p>
+ <p>• 드래그 앤 드롭으로 변수 순서 변경 가능</p>
+ <p>• 필수 변수는 반드시 값이 제공되어야 함</p>
+ <p>• 변수명은 영문자, 숫자, 언더스코어만 사용 가능</p>
</div>
- )
-} \ No newline at end of file
+ </div>
+ </div>
+ )
+}
diff --git a/lib/email-template/service.ts b/lib/email-template/service.ts
index e3ab9bed..c492410a 100644
--- a/lib/email-template/service.ts
+++ b/lib/email-template/service.ts
@@ -520,6 +520,85 @@ export async function addTemplateVariable(slug: string, variable: {
}
/**
+ * 템플릿 변수 수정
+ */
+export async function modifyTemplateVariable(
+ slug: string,
+ variableId: string,
+ updates: {
+ variableName?: string;
+ variableType?: string;
+ defaultValue?: string;
+ isRequired?: boolean;
+ description?: string;
+ }
+): Promise<TemplateVariable> {
+ try {
+ const template = await getTemplate(slug);
+ if (!template) {
+ throw new Error('템플릿을 찾을 수 없습니다.');
+ }
+
+ // 수정 대상 변수 존재 여부 확인
+ const existingVariable = template.variables.find(v => v.id === variableId);
+ if (!existingVariable) {
+ throw new Error('해당 변수를 찾을 수 없습니다.');
+ }
+
+ // 변수명 중복 확인
+ if (
+ updates.variableName &&
+ template.variables.some(
+ v => v.variableName === updates.variableName && v.id !== variableId
+ )
+ ) {
+ throw new Error('이미 존재하는 변수명입니다.');
+ }
+
+ // 업데이트 실행
+ const [updatedVariable] = await db
+ .update(templateVariables)
+ .set(updates)
+ .where(eq(templateVariables.id, variableId))
+ .returning();
+
+ return updatedVariable;
+ } catch (error) {
+ console.error('Error updating template variable:', error);
+ throw error;
+ }
+}
+
+/**
+ * 템플릿 변수 삭제
+ */
+export async function removeTemplateVariable(
+ slug: string,
+ variableId: string
+): Promise<void> {
+ try {
+ const template = await getTemplate(slug);
+ if (!template) {
+ throw new Error('템플릿을 찾을 수 없습니다.');
+ }
+
+ // 삭제 대상 변수 확인
+ const existingVariable = template.variables.find(v => v.id === variableId);
+ if (!existingVariable) {
+ throw new Error('해당 변수를 찾을 수 없습니다.');
+ }
+
+ // DB에서 삭제 실행
+ await db
+ .delete(templateVariables)
+ .where(eq(templateVariables.id, variableId));
+ } catch (error) {
+ console.error('Error removing template variable:', error);
+ throw error;
+ }
+}
+
+/**
* 템플릿 삭제 (소프트 삭제)
*/
export async function deleteTemplate(id: string): Promise<{ success: boolean; error?: string }> {
@@ -846,6 +925,50 @@ export async function addTemplateVariableAction(slug: string, variable: {
}
}
+export async function modifyTemplateVariableAction(
+ slug: string,
+ variableId: string,
+ updates: {
+ variableName?: string;
+ variableType?: string;
+ defaultValue?: string;
+ isRequired?: boolean;
+ description?: string;
+ }
+) {
+ try {
+ const modifiedVariable = await modifyTemplateVariable(slug, variableId, updates);
+
+ return {
+ success: true,
+ data: modifiedVariable,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '변수 수정에 실패했습니다.',
+ };
+ }
+}
+
+export async function removeTemplateVariableAction(
+ slug: string,
+ variableId: string
+) {
+ try {
+ await removeTemplateVariable(slug, variableId);
+
+ return {
+ success: true,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '변수 삭제에 실패했습니다.',
+ };
+ }
+}
+
export async function previewTemplateAction(
slug: string,
data: Record<string, any>,