diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-29 08:32:16 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-29 08:32:16 +0000 |
| commit | d49238b9a7d5bc4b396c5fea57ce9e545677007c (patch) | |
| tree | e8ec49c4d70a8268a0085a0df64e8e7e288e85ca /lib/email-template/editor/template-variable-manager.tsx | |
| parent | c7d37ec3e60c9197abc79738316ddae7c5bf8817 (diff) | |
(고건) 이메일 템플릿 변수 수정 및 삭제 로직 추가
Diffstat (limited to 'lib/email-template/editor/template-variable-manager.tsx')
| -rw-r--r-- | lib/email-template/editor/template-variable-manager.tsx | 1067 |
1 files changed, 541 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> + ) +} |
