"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
}}
/>
) : (
미리보기
)
}