From 78c471eec35182959e0029ded18f144974ccaca2 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Thu, 23 Oct 2025 18:13:41 +0900
Subject: (김준회) 결재 템플릿 에디터 및 결재 워크플로 공통함수 작성, 실사의뢰
결재 연결 예시 작성
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../editor/approval-template-editor.tsx | 146 ++++++++++++++++++---
1 file changed, 125 insertions(+), 21 deletions(-)
(limited to 'lib/approval-template/editor/approval-template-editor.tsx')
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: () =>
에디터 로드 중...
,
+ }
+)
+
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(null)
+ const editorRef = React.useRef(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
-
+ {
+ if (value === "preview") {
+ const currentHtml = editMode === "wysiwyg"
+ ? editorRef.current?.getHTML() || ""
+ : htmlSource
+ setPreviewContent(currentHtml)
+ }
+ }}
+ >
편집
미리보기
@@ -261,8 +313,30 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
- HTML 내용
- 표, 이미지, 변수 등을 자유롭게 편집하세요.
+
+
+ HTML 내용
+ 표, 이미지, 변수 등을 자유롭게 편집하세요.
+
+
+
{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}}}`}
))}
)}
- setForm((prev) => ({ ...prev, content: val }))}
- onReady={(editor) => setRte(editor)}
- height="400px"
- />
+
+ {editMode === "wysiwyg" ? (
+ {
+ editorRef.current = editor
+ }}
+ />
+ ) : (
+
@@ -295,7 +399,7 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari
미리보기
-
+
--
cgit v1.2.3