'use client'; import { useState, useEffect, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { createPost, updatePost, getPost } from '@/api/posts'; import { getCategories } from '@/api/category'; import { uploadImage } from '@/api/image'; import { useAuthStore } from '@/store/authStore'; import { Loader2, Save, Image as ImageIcon, ArrowLeft, Folder, UploadCloud } from 'lucide-react'; // 🎨 UX κ°œμ„ : ν† μŠ€νŠΈ μ•Œλ¦Ό μ‚¬μš© import toast from 'react-hot-toast'; import dynamic from 'next/dynamic'; const MDEditor = dynamic( () => import('@uiw/react-md-editor').then((mod) => mod.default), { ssr: false } ); function WritePageContent() { const router = useRouter(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const { role, _hasHydrated } = useAuthStore(); const editSlug = searchParams.get('slug'); const isEditMode = !!editSlug; const [title, setTitle] = useState(''); const [content, setContent] = useState('**Hello world!**'); const [categoryId, setCategoryId] = useState(''); const [isUploading, setIsUploading] = useState(false); useEffect(() => { if (_hasHydrated && (!role || !role.includes('ADMIN'))) { toast.error('κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€.'); // 🎨 Alert λŒ€μ²΄ router.push('/'); } }, [role, _hasHydrated, router]); const { data: categories } = useQuery({ queryKey: ['categories'], queryFn: getCategories, }); const { data: existingPost, isLoading: isLoadingPost } = useQuery({ queryKey: ['post', editSlug], queryFn: () => getPost(editSlug!), enabled: isEditMode, }); useEffect(() => { if (existingPost) { setTitle(existingPost.title); setContent(existingPost.content || ''); if (categories && existingPost.categoryName) { const found = findCategoryByName(categories, existingPost.categoryName); if (found) setCategoryId(found.id); } } }, [existingPost, categories]); const findCategoryByName = (cats: any[], name: string): any => { for (const cat of cats) { if (cat.name === name) return cat; if (cat.children) { const found = findCategoryByName(cat.children, name); if (found) return found; } } return null; }; const mutation = useMutation({ mutationFn: (data: any) => isEditMode ? updatePost(existingPost!.id, data) : createPost(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['posts'] }); if (isEditMode) { queryClient.invalidateQueries({ queryKey: ['post', editSlug] }); } // 🎨 UX κ°œμ„ : 성곡 λ©”μ‹œμ§€ ν† μŠ€νŠΈ toast.success(isEditMode ? 'κ²Œμ‹œκΈ€μ΄ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.' : 'κ²Œμ‹œκΈ€μ΄ λ°œν–‰λ˜μ—ˆμŠ΅λ‹ˆλ‹€!'); router.push(isEditMode ? `/posts/${editSlug}` : '/'); }, onError: (err: any) => { // 🎨 UX κ°œμ„ : μ—λŸ¬ λ©”μ‹œμ§€ ν† μŠ€νŠΈ toast.error('μ €μž₯ μ‹€νŒ¨: ' + (err.response?.data?.message || err.message)); }, }); const handleSubmit = async () => { if (!title.trim() || !content.trim()) { toast.error('제λͺ©κ³Ό λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”.'); return; } if (categoryId === '') { toast.error('μΉ΄ν…Œκ³ λ¦¬λ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”.'); return; } mutation.mutate({ title, content, categoryId: Number(categoryId), }); }; const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setIsUploading(true); const uploadToast = toast.loading('이미지 μ—…λ‘œλ“œ 쀑...'); // 🎨 μ—…λ‘œλ“œ λ‘œλ”© ν‘œμ‹œ try { const res = await uploadImage(file); if (res.code === 'SUCCESS' && res.data) { const imageUrl = res.data; const markdownImage = `![image](${imageUrl})`; setContent((prev) => prev + '\n' + markdownImage); toast.success('이미지가 μ—…λ‘œλ“œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', { id: uploadToast }); // λ‘œλ”© ν† μŠ€νŠΈλ₯Ό μ„±κ³΅μœΌλ‘œ λ³€κ²½ } } catch (error) { toast.error('이미지 μ—…λ‘œλ“œ μ‹€νŒ¨', { id: uploadToast }); } finally { setIsUploading(false); e.target.value = ''; } }; const onPaste = async (event: any) => { const items = event.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.indexOf('image') !== -1) { event.preventDefault(); const file = item.getAsFile(); if (!file) return; setIsUploading(true); const uploadToast = toast.loading('이미지 μ—…λ‘œλ“œ 쀑...'); try { const res = await uploadImage(file); if (res.code === 'SUCCESS' && res.data) { const imageUrl = res.data; const markdownImage = `![image](${imageUrl})`; setContent((prev) => prev + '\n' + markdownImage); toast.success('이미지 λΆ™μ—¬λ„£κΈ° μ™„λ£Œ!', { id: uploadToast }); } } catch (error) { toast.error('이미지 μ—…λ‘œλ“œ μ‹€νŒ¨', { id: uploadToast }); } finally { setIsUploading(false); } } } }; if (isEditMode && isLoadingPost) { return (
); } const renderCategoryOptions = (cats: any[], depth = 0) => { return cats.map((cat) => (
{cat.children && renderCategoryOptions(cat.children, depth + 1)}
)); }; return (

{isEditMode ? 'κ²Œμ‹œκΈ€ μˆ˜μ •' : 'μƒˆ κΈ€ μž‘μ„±'}

setTitle(e.target.value)} placeholder="제λͺ©μ„ μž…λ ₯ν•˜μ„Έμš”" className="w-full text-3xl font-bold placeholder:text-gray-300 border-none outline-none py-2 bg-transparent" />
setContent(val || '')} height={600} preview="edit" className="border border-gray-200 rounded-lg shadow-sm !font-sans" />

μΉ΄ν…Œκ³ λ¦¬

{categories ? renderCategoryOptions(categories) :

λ‘œλ”© 쀑...

}

이미지 μ—…λ‘œλ“œ

에디터에 이미지λ₯Ό
볡사 & λΆ™μ—¬λ„£κΈ°(Ctrl+V) ν•˜κ±°λ‚˜
μ•„λž˜ λ²„νŠΌμ„ μ‚¬μš©ν•˜μ„Έμš”.

); } export default function WritePage() { return ( }> ); }