diff --git a/src/api/postApi.ts b/src/api/postApi.ts index 905c57370..09af036c6 100644 --- a/src/api/postApi.ts +++ b/src/api/postApi.ts @@ -39,6 +39,22 @@ const useUploadPostMutation = () => { return useMutation(fetcher); }; +const useUploadPostImageMutation = () => { + const fetcher = async ({ file }: { file: Blob }) => { + const formData = new FormData(); + formData.append('file', file); + + const { data } = await axios.post('/posts/files', formData, { + headers: { + 'content-type': 'multipart/form-data', + }, + }); + return data; + }; + + return useMutation(fetcher); +}; + const useGetPostListQuery = ({ categoryId, searchType, search, page, size }: BoardSearch) => { const fetcher = () => axios.get('/posts', { params: { categoryId, searchType, search, page, size } }).then(({ data }) => data); @@ -251,6 +267,7 @@ const useGetMemberTempPostsQuery = ({ page, size = 10 }: PageAndSize) => { export { useUploadPostMutation, + useUploadPostImageMutation, useGetPostListQuery, useGetRecentPostsQuery, useGetTrendPostsQuery, diff --git a/src/components/Editor/StandardEditor.tsx b/src/components/Editor/StandardEditor.tsx index ff45e05b0..c40cab208 100644 --- a/src/components/Editor/StandardEditor.tsx +++ b/src/components/Editor/StandardEditor.tsx @@ -1,6 +1,11 @@ import React from 'react'; +import toast from 'react-hot-toast'; import { useMediaQuery, useTheme } from '@mui/material'; +import { HookMap } from '@toast-ui/editor'; import { Editor, EditorProps } from '@toast-ui/react-editor'; +import { useUploadPostImageMutation } from '@api/postApi'; +import { FILE, MAX_FILE_SIZE } from '@constants/apiResponseMessage'; +import { getServerImgUrl } from '@utils/converter'; import '@toast-ui/editor/dist/toastui-editor.css'; import '@toast-ui/editor/dist/theme/toastui-editor-dark.css'; @@ -13,11 +18,52 @@ const StandardEditor = ({ forwardedRef, ...props }: StandardEditorProps) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const { mutate: uploadPostImageMutation } = useUploadPostImageMutation(); + + const handleImageUpload: HookMap['addImageBlobHook'] = (blob) => { + if (blob.size > MAX_FILE_SIZE) { + toast.error(FILE.error.exceedFileSize, { + style: { + maxWidth: 1500, + }, + }); + return; + } + + const editor = forwardedRef?.current.getInstance(); + if (!editor) return; + + const [startPos] = editor.getSelection(); + + const IMAGE_MARKDOWN_LOADING_MSG = `![Uploading image...]()`; + editor.insertText(`${IMAGE_MARKDOWN_LOADING_MSG}\n`); + + // selection 타입을 명확히 하여 마크다운 위치 계산 + const [startLinePos, startCharPos] = startPos as Exclude; + const endPos = [startLinePos, startCharPos + IMAGE_MARKDOWN_LOADING_MSG.length] as Exclude; + + uploadPostImageMutation( + { file: blob }, + { + onSuccess: ({ fileName, filePath }) => { + editor.replaceSelection(`![${fileName}](${getServerImgUrl(filePath)})`, startPos, endPos); + }, + onError: () => { + editor.deleteSelection(startPos, endPos); + toast.error(FILE.error.uploadFail); + }, + }, + ); + }; + return (