From d49238b9a7d5bc4b396c5fea57ce9e545677007c Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 29 Sep 2025 08:32:16 +0000 Subject: (고건) 이메일 템플릿 변수 수정 및 삭제 로직 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/template-variable-manager.tsx | 1067 ++++++++++---------- 1 file changed, 541 insertions(+), 526 deletions(-) (limited to 'lib/email-template/editor/template-variable-manager.tsx') 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(null) - const [isSubmitting, setIsSubmitting] = React.useState(false) - const [isDialogOpen, setIsDialogOpen] = React.useState(false) // 이름 변경 - - const [formData, setFormData] = React.useState({ - variableName: '', - variableType: 'string', - defaultValue: '', - isRequired: false, - description: '' + const [variables, setVariables] = useState(template.variables) + // const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [editingVariable, setEditingVariable] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false) + const [isDialogOpen, setIsDialogOpen] = useState(false) // 이름 변경 + + const [formData, setFormData] = useState({ + 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 ( +
+ {/* 헤더 */} +
+
+ {/*

변수 관리

*/} +

+ 총 {variables.length}개의 변수가 등록되어 있습니다. +

+
- return ( -
- {/* 헤더 */} -
-
- {/*

변수 관리

*/} -

- 총 {variables.length}개의 변수가 등록되어 있습니다. -

-
- - - - - - - - - {isEditMode ? '변수 수정' : '새 변수 추가'} - - - {isEditMode - ? '기존 변수의 정보를 수정합니다.' - : '템플릿에서 사용할 새로운 변수를 추가합니다.' - } - - - -
-
- - setFormData(prev => ({ ...prev, variableName: e.target.value }))} - placeholder="예: userName, orderDate" - /> -
- -
- - -
- -
- - setFormData(prev => ({ ...prev, defaultValue: e.target.value }))} - placeholder={getDefaultValuePlaceholder(formData.variableType)} - /> -
- -
- -