summaryrefslogtreecommitdiff
path: root/lib/approval-template/editor/approval-template-editor.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-23 18:13:41 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-23 18:13:41 +0900
commit78c471eec35182959e0029ded18f144974ccaca2 (patch)
tree914cdf1c8f406ca3e2aa639b8bb774f7f4e87023 /lib/approval-template/editor/approval-template-editor.tsx
parent0be8940580c4a4a4e098b649d198160f9b60420c (diff)
(김준회) 결재 템플릿 에디터 및 결재 워크플로 공통함수 작성, 실사의뢰 결재 연결 예시 작성
Diffstat (limited to 'lib/approval-template/editor/approval-template-editor.tsx')
-rw-r--r--lib/approval-template/editor/approval-template-editor.tsx146
1 files changed, 125 insertions, 21 deletions
diff --git a/lib/approval-template/editor/approval-template-editor.tsx b/lib/approval-template/editor/approval-template-editor.tsx
index 2c4ef65e..242504e7 100644
--- a/lib/approval-template/editor/approval-template-editor.tsx
+++ b/lib/approval-template/editor/approval-template-editor.tsx
@@ -2,13 +2,15 @@
import * as React from "react"
import { toast } from "sonner"
-import { Loader, Save, ArrowLeft } from "lucide-react"
+import { Loader, Save, ArrowLeft, Code2, Eye } from "lucide-react"
import Link from "next/link"
+import dynamic from "next/dynamic"
+import type { Editor as ToastEditor } from "@toast-ui/editor"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
-import RichTextEditor from "@/components/rich-text-editor/RichTextEditor"
+import { Textarea } from "@/components/ui/textarea"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge"
@@ -17,12 +19,26 @@ import { formatApprovalLine } from "@/lib/approval-line/utils/format"
import { getApprovalLineOptionsAction } from "@/lib/approval-line/service"
import { type ApprovalTemplate } from "@/lib/approval-template/service"
-import { type Editor } from "@tiptap/react"
import { updateApprovalTemplateAction } from "@/lib/approval-template/service"
import { getActiveApprovalTemplateCategories, type ApprovalTemplateCategory } from "@/lib/approval-template/category-service"
import { useSession } from "next-auth/react"
import { useRouter, usePathname } from "next/navigation"
+// Toast UI Editor를 dynamic import로 불러옴 (SSR 방지)
+const Editor = dynamic(
+ async () => {
+ // CSS를 먼저 로드
+ // @ts-expect-error - CSS 파일은 타입 선언이 없지만 런타임에 정상 작동
+ await import("@toast-ui/editor/dist/toastui-editor.css")
+ const mod = await import("@toast-ui/react-editor")
+ return mod.Editor
+ },
+ {
+ ssr: false,
+ loading: () => <div className="flex items-center justify-center h-[400px] text-muted-foreground">에디터 로드 중...</div>,
+ }
+)
+
interface ApprovalTemplateEditorProps {
templateId: string
initialTemplate: ApprovalTemplate
@@ -34,9 +50,13 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
const { data: session } = useSession()
const router = useRouter()
const pathname = usePathname()
- const [rte, setRte] = React.useState<Editor | null>(null)
+ const editorRef = React.useRef<ToastEditor | null>(null)
const [template, setTemplate] = React.useState(initialTemplate)
const [isSaving, startSaving] = React.useTransition()
+ const [previewContent, setPreviewContent] = React.useState(initialTemplate.content)
+ const [editMode, setEditMode] = React.useState<"wysiwyg" | "html">("wysiwyg")
+ const [htmlSource, setHtmlSource] = React.useState(initialTemplate.content)
+ const [editorKey, setEditorKey] = React.useState(0) // 에디터 재마운트를 위한 키
// 편집기에 전달할 변수 목록
// 템플릿(DB) 변수 + 정적(config) 변수 병합
@@ -53,7 +73,6 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
const [form, setForm] = React.useState({
name: template.name,
subject: template.subject,
- content: template.content,
description: template.description ?? "",
category: template.category ?? "",
approvalLineId: (template as { approvalLineId?: string | null }).approvalLineId ?? "",
@@ -93,6 +112,7 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
return () => {
active = false
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // 빈 의존성 배열로 초기 한 번만 실행
// 결재선 옵션 로드
@@ -120,6 +140,20 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
setForm((prev) => ({ ...prev, [name]: value }))
}
+ // 편집 모드 토글
+ function toggleEditMode() {
+ if (editMode === "wysiwyg") {
+ // WYSIWYG → HTML: 에디터에서 HTML 가져오기
+ const currentHtml = editorRef.current?.getHTML() || ""
+ setHtmlSource(currentHtml)
+ setEditMode("html")
+ } else {
+ // HTML → WYSIWYG: 에디터를 재마운트하여 수정된 HTML 반영
+ setEditorKey(prev => prev + 1)
+ setEditMode("wysiwyg")
+ }
+ }
+
function handleSave() {
startSaving(async () => {
if (!session?.user?.id) {
@@ -127,10 +161,15 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
return
}
+ // 현재 모드에 따라 HTML 가져오기
+ const content = editMode === "wysiwyg"
+ ? editorRef.current?.getHTML() || ""
+ : htmlSource
+
const { success, error, data } = await updateApprovalTemplateAction(templateId, {
name: form.name,
subject: form.subject,
- content: form.content,
+ content,
description: form.description,
category: form.category || undefined,
approvalLineId: form.approvalLineId ? form.approvalLineId : null,
@@ -146,10 +185,12 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
toast.success("저장되었습니다")
// 저장 후 목록 페이지로 이동 (back-button.tsx 로직 참고)
- const segments = pathname.split('/').filter(Boolean)
- const newSegments = segments.slice(0, -1) // 마지막 세그먼트(ID) 제거
- const targetPath = newSegments.length > 0 ? `/${newSegments.join('/')}` : '/'
- router.push(targetPath)
+ if (pathname) {
+ const segments = pathname.split('/').filter(Boolean)
+ const newSegments = segments.slice(0, -1) // 마지막 세그먼트(ID) 제거
+ const targetPath = newSegments.length > 0 ? `/${newSegments.join('/')}` : '/'
+ router.push(targetPath)
+ }
})
}
@@ -182,7 +223,18 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
<Separator />
- <Tabs defaultValue="editor" className="flex-1">
+ <Tabs
+ defaultValue="editor"
+ className="flex-1"
+ onValueChange={(value) => {
+ if (value === "preview") {
+ const currentHtml = editMode === "wysiwyg"
+ ? editorRef.current?.getHTML() || ""
+ : htmlSource
+ setPreviewContent(currentHtml)
+ }
+ }}
+ >
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="editor">편집</TabsTrigger>
<TabsTrigger value="preview">미리보기</TabsTrigger>
@@ -261,8 +313,30 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
<Card>
<CardHeader>
- <CardTitle>HTML 내용</CardTitle>
- <CardDescription>표, 이미지, 변수 등을 자유롭게 편집하세요.</CardDescription>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle>HTML 내용</CardTitle>
+ <CardDescription>표, 이미지, 변수 등을 자유롭게 편집하세요.</CardDescription>
+ </div>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={toggleEditMode}
+ className="flex items-center gap-2"
+ >
+ {editMode === "wysiwyg" ? (
+ <>
+ <Code2 className="h-4 w-4" />
+ HTML로 수정
+ </>
+ ) : (
+ <>
+ <Eye className="h-4 w-4" />
+ 에디터로 수정
+ </>
+ )}
+ </Button>
+ </div>
</CardHeader>
<CardContent>
{variableOptions.length > 0 && (
@@ -272,19 +346,49 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
key={v.label}
variant="outline"
size="sm"
- onClick={() => rte?.chain().focus().insertContent(v.html).run()}
+ onClick={() => {
+ if (editMode === "wysiwyg" && editorRef.current) {
+ editorRef.current.insertText(v.html)
+ } else if (editMode === "html") {
+ // HTML 모드에서는 텍스트 끝에 추가
+ setHtmlSource((prev) => prev + v.html)
+ }
+ }}
>
{`{{${v.label}}}`}
</Button>
))}
</div>
)}
- <RichTextEditor
- value={form.content}
- onChange={(val) => setForm((prev) => ({ ...prev, content: val }))}
- onReady={(editor) => setRte(editor)}
- height="400px"
- />
+
+ {editMode === "wysiwyg" ? (
+ <Editor
+ key={editorKey}
+ initialValue={htmlSource}
+ previewStyle="vertical"
+ height="500px"
+ initialEditType="wysiwyg"
+ useCommandShortcut={true}
+ hideModeSwitch={true}
+ toolbarItems={[
+ ["heading", "bold", "italic", "strike"],
+ ["hr", "quote"],
+ ["ul", "ol", "task"],
+ ["table", "link"],
+ ["code", "codeblock"],
+ ]}
+ onLoad={(editor) => {
+ editorRef.current = editor
+ }}
+ />
+ ) : (
+ <Textarea
+ value={htmlSource}
+ onChange={(e) => setHtmlSource(e.target.value)}
+ className="font-mono text-sm min-h-[500px] resize-y"
+ placeholder="HTML 소스를 직접 편집하세요..."
+ />
+ )}
</CardContent>
</Card>
</TabsContent>
@@ -295,7 +399,7 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
<CardTitle>미리보기</CardTitle>
</CardHeader>
<CardContent className="border rounded-md p-4 overflow-auto bg-background">
- <div dangerouslySetInnerHTML={{ __html: form.content }} />
+ <div dangerouslySetInnerHTML={{ __html: previewContent }} />
</CardContent>
</Card>
</TabsContent>