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: () => <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>
-- 
cgit v1.2.3

