summaryrefslogtreecommitdiff
path: root/components/mail/mail-template-editor-client.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/mail/mail-template-editor-client.tsx')
-rw-r--r--components/mail/mail-template-editor-client.tsx255
1 files changed, 255 insertions, 0 deletions
diff --git a/components/mail/mail-template-editor-client.tsx b/components/mail/mail-template-editor-client.tsx
new file mode 100644
index 00000000..dfbeb4e0
--- /dev/null
+++ b/components/mail/mail-template-editor-client.tsx
@@ -0,0 +1,255 @@
+'use client';
+
+import { useState, useEffect, useTransition } from 'react';
+import { useRouter, useParams } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import { Save, Eye } from 'lucide-react';
+import { toast } from 'sonner';
+import Link from 'next/link';
+import { getTemplateAction, updateTemplateAction, previewTemplateAction, TemplateFile } from '@/lib/mail/service';
+
+type Template = TemplateFile;
+
+interface MailTemplateEditorClientProps {
+ templateName: string;
+ initialTemplate?: Template | null;
+}
+
+export default function MailTemplateEditorClient({
+ templateName,
+ initialTemplate
+}: MailTemplateEditorClientProps) {
+ const router = useRouter();
+ const params = useParams();
+
+ const lng = (params?.lng as string) || 'ko';
+
+ const [template, setTemplate] = useState<Template | null>(initialTemplate || null);
+ const [content, setContent] = useState(initialTemplate?.content || '');
+ const [loading, setLoading] = useState(!initialTemplate);
+ const [saving, setSaving] = useState(false);
+ const [previewLoading, setPreviewLoading] = useState(false);
+ const [previewHtml, setPreviewHtml] = useState<string | null>(null);
+ const [, startTransition] = useTransition();
+
+ // 템플릿 조회
+ const fetchTemplate = async () => {
+ if (!templateName) {
+ toast.error('잘못된 접근입니다.');
+ router.push(`/${lng}/evcp/email-template`);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ startTransition(async () => {
+ const result = await getTemplateAction(templateName);
+
+ if (result.success && result.data) {
+ setTemplate(result.data);
+ setContent(result.data.content);
+ } else {
+ toast.error(result.error || '템플릿을 찾을 수 없습니다.');
+ router.push(`/${lng}/evcp/email-template`);
+ }
+ setLoading(false);
+ });
+ } catch (error) {
+ console.error('Error fetching template:', error);
+ toast.error('템플릿을 불러오는데 실패했습니다.');
+ router.push(`/${lng}/evcp/email-template`);
+ setLoading(false);
+ }
+ };
+
+ // 템플릿 저장
+ const handleSave = async () => {
+ if (!content.trim()) {
+ toast.error('템플릿 내용을 입력해주세요.');
+ return;
+ }
+
+ try {
+ setSaving(true);
+ startTransition(async () => {
+ const result = await updateTemplateAction(templateName, content);
+
+ if (result.success && result.data) {
+ toast.success('템플릿이 성공적으로 저장되었습니다.');
+ setTemplate(result.data);
+ } else {
+ toast.error(result.error || '템플릿 저장에 실패했습니다.');
+ }
+ setSaving(false);
+ });
+ } catch (error) {
+ console.error('Error saving template:', error);
+ toast.error('템플릿 저장에 실패했습니다.');
+ setSaving(false);
+ }
+ };
+
+ // 미리보기 생성
+ const handlePreview = async () => {
+ try {
+ setPreviewLoading(true);
+ startTransition(async () => {
+ const result = await previewTemplateAction(
+ templateName,
+ {
+ userName: '홍길동',
+ companyName: 'EVCP',
+ email: 'user@example.com',
+ date: new Date().toLocaleDateString('ko-KR'),
+ projectName: '샘플 프로젝트',
+ message: '이것은 샘플 메시지입니다.',
+ currentYear: new Date().getFullYear(),
+ language: 'ko',
+ name: '홍길동',
+ loginUrl: 'https://example.com/login'
+ }
+ );
+
+ if (result.success && result.data) {
+ setPreviewHtml(result.data.html);
+ } else {
+ toast.error(result.error || '미리보기 생성에 실패했습니다.');
+ }
+ setPreviewLoading(false);
+ });
+ } catch (error) {
+ console.error('Error generating preview:', error);
+ toast.error('미리보기 생성에 실패했습니다.');
+ setPreviewLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!initialTemplate) {
+ fetchTemplate();
+ }
+ }, [templateName, initialTemplate]);
+
+ if (loading) {
+ return (
+ <div className="text-center py-20">
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
+ <p className="mt-4 text-gray-600">템플릿을 불러오는 중...</p>
+ </div>
+ );
+ }
+
+ if (!template) {
+ return (
+ <div className="text-center py-20">
+ <p className="text-gray-600">템플릿을 찾을 수 없습니다.</p>
+ <Link href={`/${lng}/evcp/email-template`}>
+ <Button className="mt-4">목록으로 돌아가기</Button>
+ </Link>
+ </div>
+ );
+ }
+
+ return (
+ <div className="space-y-8">
+ {/* 헤더 */}
+ <div>
+ <div className="flex items-center gap-4 mb-4">
+ <h1 className="text-3xl font-bold text-gray-900">템플릿 편집</h1>
+ </div>
+ <div>
+ <p className="text-gray-600">
+ <span className="font-medium">{template.name}</span> 템플릿을 편집합니다.
+ </p>
+ <p className="text-sm text-gray-500">
+ 마지막 수정: {new Date(template.lastModified).toLocaleString('ko-KR')}
+ </p>
+ </div>
+ </div>
+
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
+ {/* 편집 영역 */}
+ <div className="space-y-6">
+ <div className="bg-white p-6 rounded-lg shadow">
+ <h2 className="text-xl font-semibold mb-4">템플릿 내용</h2>
+
+ <div className="space-y-4">
+ <div>
+ <Label htmlFor="content">Handlebars 템플릿</Label>
+ <Textarea
+ id="content"
+ value={content}
+ onChange={(e) => setContent(e.target.value)}
+ className="min-h-[500px] font-mono text-sm"
+ />
+ </div>
+
+ <div className="flex gap-2">
+ <Button onClick={handleSave} disabled={saving}>
+ <Save className="h-4 w-4 mr-2" />
+ {saving ? '저장 중...' : '저장'}
+ </Button>
+ <Button variant="outline" onClick={handlePreview} disabled={previewLoading}>
+ <Eye className="h-4 w-4 mr-2" />
+ {previewLoading ? '생성 중...' : '미리보기'}
+ </Button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* 미리보기 영역 */}
+ <div className="space-y-6">
+ <div className="bg-white p-6 rounded-lg shadow">
+ <h2 className="text-xl font-semibold mb-4">빠른 미리보기</h2>
+
+ <div className="border rounded-lg p-4 min-h-[500px] bg-gray-50 overflow-auto">
+ {previewHtml ? (
+ <div
+ className="preview-content"
+ dangerouslySetInnerHTML={{ __html: previewHtml }}
+ />
+ ) : (
+ <div className="text-center text-gray-500 py-20">
+ 미리보기 버튼을 클릭하여 결과를 확인하세요.
+ </div>
+ )}
+ </div>
+ </div>
+
+ {/* 도움말 */}
+ <div className="bg-blue-50 p-4 rounded-lg">
+ <h3 className="font-semibold text-blue-900 mb-2">Handlebars 문법 도움말</h3>
+ <div className="text-sm text-blue-800 space-y-1">
+ <p><code>{`{{variable}}`}</code> - 변수 출력</p>
+ <p><code>{`{{{html}}}`}</code> - HTML 출력 (이스케이프 없음)</p>
+ <p><code>{`{{#if condition}}`}</code> - 조건문</p>
+ <p><code>{`{{#each items}}`}</code> - 반복문</p>
+ </div>
+ </div>
+
+ {/* 샘플 데이터 */}
+ <div className="bg-gray-50 p-4 rounded-lg">
+ <h3 className="font-semibold text-gray-900 mb-2">미리보기 샘플 데이터</h3>
+ <pre className="text-xs text-gray-600 overflow-auto">
+{JSON.stringify({
+ userName: '홍길동',
+ companyName: 'EVCP',
+ email: 'user@example.com',
+ date: new Date().toLocaleDateString('ko-KR'),
+ projectName: '샘플 프로젝트',
+ message: '이것은 샘플 메시지입니다.',
+ currentYear: new Date().getFullYear(),
+ language: 'ko',
+ name: '홍길동',
+ loginUrl: 'https://example.com/login'
+}, null, 2)}
+ </pre>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+} \ No newline at end of file