From 08b73d56c2d887931cecdf2b0af6b277381763e6 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 6 Nov 2025 17:44:59 +0900 Subject: (김준회) 결재 프리뷰 공통컴포넌트 작성 및 index.ts --> client.ts 분리 (서버사이드 코드가 번들링되어 클라측에서 실행되는 문제 해결 목적) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(system)/approval/template/[id]/page.tsx | 2 - .../editor/approval-template-editor.tsx | 358 ++++++----------- lib/approval/approval-preview-dialog.tsx | 435 +++++++++++++++++++++ lib/approval/client.ts | 23 ++ lib/approval/index.ts | 3 + lib/approval/template-utils.ts | 30 +- ...\271\354\235\270 \354\232\224\354\262\255.html" | 233 +++++++++++ ...\230\353\242\260 \354\232\224\354\262\255.html" | 316 +++++++++++++++ lib/vendors/approval-actions.ts | 177 ++++++--- lib/vendors/blacklist-check.ts | 170 ++++++++ lib/vendors/table/approve-vendor-dialog.tsx | 185 +++++++-- 11 files changed, 1599 insertions(+), 333 deletions(-) create mode 100644 lib/approval/approval-preview-dialog.tsx create mode 100644 lib/approval/client.ts create mode 100644 "lib/approval/templates/\353\262\244\353\215\224 \352\260\200\354\236\205 \354\212\271\354\235\270 \354\232\224\354\262\255.html" create mode 100644 "lib/approval/templates/\354\213\244\354\202\254\354\235\230\353\242\260 \353\260\217 \354\213\244\354\202\254\354\236\254\354\235\230\353\242\260 \354\232\224\354\262\255.html" create mode 100644 lib/vendors/blacklist-check.ts diff --git a/app/[lng]/evcp/(evcp)/(system)/approval/template/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(system)/approval/template/[id]/page.tsx index 4ce13b42..3ea0fb09 100644 --- a/app/[lng]/evcp/(evcp)/(system)/approval/template/[id]/page.tsx +++ b/app/[lng]/evcp/(evcp)/(system)/approval/template/[id]/page.tsx @@ -5,7 +5,6 @@ import { notFound } from "next/navigation" import { getApprovalTemplate } from "@/lib/approval-template/service" import { getApprovalLineOptions } from "@/lib/approval-line/service" import { ApprovalTemplateEditor } from "@/lib/approval-template/editor/approval-template-editor" -import { variables as configVariables } from "./config" interface ApprovalTemplateDetailPageProps { params: Promise<{ @@ -46,7 +45,6 @@ export default async function ApprovalTemplateDetailPage({ params }: ApprovalTem } approvalLineOptions={approvalLineOptions} /> )} diff --git a/lib/approval-template/editor/approval-template-editor.tsx b/lib/approval-template/editor/approval-template-editor.tsx index 242504e7..7fe7c700 100644 --- a/lib/approval-template/editor/approval-template-editor.tsx +++ b/lib/approval-template/editor/approval-template-editor.tsx @@ -2,16 +2,13 @@ import * as React from "react" import { toast } from "sonner" -import { Loader, Save, ArrowLeft, Code2, Eye } from "lucide-react" +import { Loader, Save, ArrowLeft, 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 { 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" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" @@ -24,51 +21,20 @@ import { getActiveApprovalTemplateCategories, type ApprovalTemplateCategory } fr 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 - staticVariables?: Array<{ variableName: string }> approvalLineOptions: Array<{ id: string; name: string }> } -export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVariables = [], approvalLineOptions }: ApprovalTemplateEditorProps) { - const { data: session } = useSession() - const router = useRouter() - const pathname = usePathname() - const editorRef = React.useRef(null) +export function ApprovalTemplateEditor({ templateId, initialTemplate, approvalLineOptions }: ApprovalTemplateEditorProps) { + const { data: session } = useSession() + const router = useRouter() + const pathname = usePathname() 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) 변수 병합 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dbVariables: string[] = Array.isArray((template as any).variables) - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - (template as any).variables.map((v: any) => v.variableName) - : [] - - const mergedVariables = Array.from(new Set([...dbVariables, ...staticVariables.map((v) => v.variableName)])) - - const variableOptions = mergedVariables.map((name) => ({ label: name, html: `{{${name}}}` })) + const [htmlContent, setHtmlContent] = React.useState(initialTemplate.content) + const [previewKey, setPreviewKey] = React.useState(0) // 미리보기 업데이트용 const [form, setForm] = React.useState({ name: template.name, @@ -140,18 +106,10 @@ 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 refreshPreview() { + setPreviewKey((prev) => prev + 1) + toast.success("미리보기를 업데이트했습니다") } function handleSave() { @@ -161,15 +119,10 @@ 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, + content: htmlContent, description: form.description, category: form.category || undefined, approvalLineId: form.approvalLineId ? form.approvalLineId : null, @@ -215,6 +168,9 @@ export function ApprovalTemplateEditor({ templateId, initialTemplate, staticVari

{template.description || "결재 템플릿 편집"}

+ + {/* 2컬럼 레이아웃: 왼쪽 HTML 편집기, 오른쪽 미리보기 */} +
+ {/* 왼쪽: HTML 편집기 */} + + +
+
+ HTML 편집기 + HTML 코드를 직접 편집하세요
- - - {variableOptions.length > 0 && ( -
- {variableOptions.map((v: { label: string; html: string }) => ( - - ))} -
- )} - - {editMode === "wysiwyg" ? ( - { - editorRef.current = editor - }} - /> - ) : ( -