From 9da494b0e3bbe7b513521d0915510fe9ee376b8b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:19:52 +0000 Subject: (대표님, 최겸) 작업사항 - 이메일 템플릿, 메일링, 기술영업 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/email-template/editor/template-preview.tsx | 406 +++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 lib/email-template/editor/template-preview.tsx (limited to 'lib/email-template/editor/template-preview.tsx') diff --git a/lib/email-template/editor/template-preview.tsx b/lib/email-template/editor/template-preview.tsx new file mode 100644 index 00000000..df6b8461 --- /dev/null +++ b/lib/email-template/editor/template-preview.tsx @@ -0,0 +1,406 @@ +"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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" +import { + Eye, + Smartphone, + Monitor, + Tablet, + RefreshCw, + Copy, + Download, + Send +} from "lucide-react" +import { toast } from "sonner" +import { type TemplateWithVariables } from "@/db/schema" +import { previewTemplateAction } from "../service" +import { copyTextToClipboard } from "@/lib/utils" + +interface TemplatePreviewProps { + template: TemplateWithVariables +} + +type ViewMode = 'desktop' | 'tablet' | 'mobile' + +export function TemplatePreview({ template }: TemplatePreviewProps) { + const [previewData, setPreviewData] = React.useState>( + template.sampleData || {} + ) + const [previewHtml, setPreviewHtml] = React.useState("") + const [isLoading, setIsLoading] = React.useState(false) + const [viewMode, setViewMode] = React.useState('desktop') + const [lastUpdated, setLastUpdated] = React.useState(null) + + // 미리보기 생성 + const generatePreview = async () => { + setIsLoading(true) + try { + const result = await previewTemplateAction( + template.slug, + previewData, + template.content + ) + + if (result.success) { + setPreviewHtml(result.data.html) + setLastUpdated(new Date()) + toast.success('미리보기가 생성되었습니다.') + } else { + toast.error("샘플데이터를 먼저 생성해주세요." || '미리보기 생성에 실패했습니다.') + } + } catch (error) { + toast.error('미리보기 생성 중 오류가 발생했습니다.') + } finally { + setIsLoading(false) + } + } + + // 초기 미리보기 생성 + React.useEffect(() => { + generatePreview() + }, []) + + // 샘플 데이터 업데이트 + const updatePreviewData = (key: string, value: any) => { + setPreviewData(prev => ({ ...prev, [key]: value })) + } + + // 샘플 데이터 리셋 + const resetToDefaults = () => { + const defaultData: Record = {} + + template.variables.forEach(variable => { + if (variable.defaultValue) { + switch (variable.variableType) { + case 'number': + defaultData[variable.variableName] = parseFloat(variable.defaultValue) || 0 + break + case 'boolean': + defaultData[variable.variableName] = variable.defaultValue.toLowerCase() === 'true' + break + default: + defaultData[variable.variableName] = variable.defaultValue + } + } else { + // 기본 샘플 값 + switch (variable.variableType) { + case 'string': + defaultData[variable.variableName] = `샘플 ${variable.variableName}` + break + case 'number': + defaultData[variable.variableName] = 123 + break + case 'boolean': + defaultData[variable.variableName] = true + break + case 'date': + defaultData[variable.variableName] = new Date().toLocaleDateString('ko-KR') + break + } + } + }) + + setPreviewData(defaultData) + toast.success('샘플 데이터가 초기화되었습니다.') + } + + // HTML 복사 + const copyHtml = async () => { + if (!previewHtml) { + toast.error("미리보기를 먼저 생성해주세요.") + return + } + + const ok = await copyTextToClipboard(previewHtml) + ok + ? toast.success("HTML이 클립보드에 복사되었습니다.") + : toast.error("복사에 실패했습니다.") + } + // HTML 다운로드 + const downloadHtml = () => { + if (!previewHtml) { + toast.error('미리보기를 먼저 생성해주세요.') + return + } + + const blob = new Blob([previewHtml], { type: 'text/html' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `${template.slug}-preview.html` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + + toast.success('HTML 파일이 다운로드되었습니다.') + } + + // 뷰포트 크기 + const getViewportStyle = (): React.CSSProperties => { + switch (viewMode) { + case 'mobile': + return { width: '375px', height: '667px' } + case 'tablet': + return { width: '768px', height: '1024px' } + case 'desktop': + default: + return { width: '100%', height: '600px' } + } + } + + // 입력 컴포넌트 렌더링 + const renderInputForVariable = (variable: any) => { + const value = previewData[variable.variableName] || '' + + switch (variable.variableType) { + case 'boolean': + return ( + + ) + + case 'number': + return ( + updatePreviewData(variable.variableName, parseFloat(e.target.value) || 0)} + placeholder="숫자를 입력하세요" + /> + ) + + case 'date': + return ( + updatePreviewData(variable.variableName, e.target.value)} + /> + ) + + default: + // string 타입이거나 긴 텍스트인 경우 + if (variable.variableName.toLowerCase().includes('message') || + variable.variableName.toLowerCase().includes('content') || + variable.variableName.toLowerCase().includes('description')) { + return ( +