"use client" import * as React from "react" import { toast } from "sonner" 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 { 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" import { formatApprovalLine } from "@/lib/approval-line/utils/format" import { getApprovalLineOptionsAction } from "@/lib/approval-line/service" import { type ApprovalTemplate } from "@/lib/approval-template/service" 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 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) 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 [form, setForm] = React.useState({ name: template.name, subject: template.subject, description: template.description ?? "", category: template.category ?? "", approvalLineId: (template as { approvalLineId?: string | null }).approvalLineId ?? "", }) const [categories, setCategories] = React.useState([]) const [isLoadingCategories, setIsLoadingCategories] = React.useState(false) const [category, setCategory] = React.useState(form.category ?? "") const [isInitialLoad, setIsInitialLoad] = React.useState(true) // eslint-disable-next-line @typescript-eslint/no-explicit-any const [lineOptions, setLineOptions] = React.useState(approvalLineOptions as Array<{ id: string; name: string; aplns?: any[]; category?: string | null }>) const [isLoadingLines, setIsLoadingLines] = React.useState(false) // 카테고리 목록 로드 (초기 한 번만) React.useEffect(() => { let active = true const loadCategories = async () => { setIsLoadingCategories(true) try { const data = await getActiveApprovalTemplateCategories() if (active) { setCategories(data) // 초기 로드 시에만 기본 카테고리 설정 (템플릿의 카테고리가 없고 카테고리가 선택되지 않은 경우) if (isInitialLoad && !category && data.length > 0) { const defaultCategory = form.category || data[0].name setCategory(defaultCategory) } setIsInitialLoad(false) } } catch (error) { console.error('카테고리 로드 실패:', error) } finally { if (active) setIsLoadingCategories(false) } } loadCategories() return () => { active = false } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // 빈 의존성 배열로 초기 한 번만 실행 // 결재선 옵션 로드 React.useEffect(() => { let active = true const run = async () => { setIsLoadingLines(true) const { success, data } = await getApprovalLineOptionsAction(category || undefined) if (active) { setIsLoadingLines(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any if (success && data) setLineOptions(data as any) // 카테고리 바뀌면 결재선 선택 초기화 setForm((prev) => ({ ...prev, category, approvalLineId: "" })) } } run() return () => { active = false } }, [category]) function handleChange(e: React.ChangeEvent) { const { name, value } = e.target 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) { toast.error("로그인이 필요합니다") return } // 현재 모드에 따라 HTML 가져오기 const content = editMode === "wysiwyg" ? editorRef.current?.getHTML() || "" : htmlSource const { success, error, data } = await updateApprovalTemplateAction(templateId, { name: form.name, subject: form.subject, content, description: form.description, category: form.category || undefined, approvalLineId: form.approvalLineId ? form.approvalLineId : null, updatedBy: Number(session.user.id), }) if (!success || error || !data) { toast.error(error ?? "저장에 실패했습니다") return } setTemplate(data) toast.success("저장되었습니다") // 저장 후 목록 페이지로 이동 (back-button.tsx 로직 참고) 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) } }) } return (
{/* Header */}

{template.name}

최근 수정: {new Date(template.updatedAt).toLocaleDateString("ko-KR")} {template.category && ( {template.category} )}

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

{ if (value === "preview") { const currentHtml = editMode === "wysiwyg" ? editorRef.current?.getHTML() || "" : htmlSource setPreviewContent(currentHtml) } }} > 편집 미리보기 기본 정보
HTML 내용 표, 이미지, 변수 등을 자유롭게 편집하세요.
{variableOptions.length > 0 && (
{variableOptions.map((v: { label: string; html: string }) => ( ))}
)} {editMode === "wysiwyg" ? ( { editorRef.current = editor }} /> ) : (