Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/toast UI 이미지 핸들러 #454 #895

Merged
merged 7 commits into from
May 23, 2024
17 changes: 17 additions & 0 deletions src/api/postApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -251,6 +267,7 @@ const useGetMemberTempPostsQuery = ({ page, size = 10 }: PageAndSize) => {

export {
useUploadPostMutation,
useUploadPostImageMutation,
useGetPostListQuery,
useGetRecentPostsQuery,
useGetTrendPostsQuery,
Expand Down
46 changes: 46 additions & 0 deletions src/components/Editor/StandardEditor.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<typeof startPos, number>;
const endPos = [startLinePos, startCharPos + IMAGE_MARKDOWN_LOADING_MSG.length] as Exclude<typeof startPos, number>;

uploadPostImageMutation(
{ file: blob },
{
onSuccess: ({ fileName, filePath }) => {
editor.replaceSelection(`![${fileName}](${getServerImgUrl(filePath)})`, startPos, endPos);
},
onError: () => {
editor.deleteSelection(startPos, endPos);
toast.error(FILE.error.uploadFail);
},
},
);
};

return (
<Editor
ref={forwardedRef}
initialValue={props.initialValue ?? ''}
placeholder="내용을 입력해주세요."
hooks={{
addImageBlobHook: handleImageUpload,
}}
previewStyle={isMobile ? 'tab' : 'vertical'}
minHeight="300px"
initialEditType={isMobile ? 'wysiwyg' : 'markdown'}
Expand Down
15 changes: 13 additions & 2 deletions src/constants/apiResponseMessage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { formatFileSize } from '@utils/converter';

export const COMMON = {} as const;

export const PASSWORD = {
Expand Down Expand Up @@ -47,11 +49,20 @@ export const LOGIN_ID = {
error: {
existing: '이미 존재하는 아이디입니다.',
},
};
} as const;

export const STUDENT_ID = {
success: {},
error: {
existing: '이미 존재하는 학번입니다.',
},
};
} as const;

export const MAX_FILE_SIZE = 30 * 1024 * 1024; // Byte
export const FILE = {
success: {},
error: {
uploadFail: '업로드가 실패하였습니다.',
exceedFileSize: `파일이 제한된 크기(${formatFileSize(MAX_FILE_SIZE)})를 초과하였습니다.`,
},
} as const;
Comment on lines +61 to +68
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@redzzzi

  • 파일 업로더 세부 기능 구현 #479 assignee 맡으셨던데 해당 이슈 처리중인가요? 백엔드에 문의해봤는데 이미지 포함한 파일 요청 모두 동일한 크기로 제한 걸려있다고 하더라구요! 해당 이슈 처리하면서 요부분 가져다 사용하시면 될 것 같아요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 감사합니다!

Loading