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

게시글 작성 시 이미지 복사 붙혀넣기로 이미지 첨부 가능하도록 구현 #624

Merged
merged 3 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/components/PostForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export default function PostForm({ data, mutate }: PostFormProps) {
const contentImageHook = useContentImage(
serverImageUrl && convertImageUrlToServerUrl(serverImageUrl)
);
const { handlePasteImage } = contentImageHook;

const writingOptionHook = useWritingOption(
serverVoteInfo?.options.map(option => ({
...option,
Expand Down Expand Up @@ -233,6 +235,7 @@ export default function PostForm({ data, mutate }: PostFormProps) {
placeholder={CONTENT_PLACEHOLDER}
maxLength={POST_CONTENT.MAX_LENGTH}
minLength={POST_CONTENT.MIN_LENGTH}
onPaste={handlePasteImage}
required
/>
<S.ContentLinkButtonWrapper>
Expand Down
51 changes: 23 additions & 28 deletions frontend/src/hooks/useContentImage.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,43 @@
import { ChangeEvent, useRef, useState } from 'react';
import { ChangeEvent, ClipboardEvent, useRef, useState } from 'react';

import { MAX_FILE_SIZE } from '@components/PostForm/constants';

import { convertImageToWebP } from '@utils/resizeImage';
import { uploadImage } from '@utils/post/uploadImage';

export const useContentImage = (imageUrl: string = '') => {
const [contentImage, setContentImage] = useState(imageUrl);
const contentInputRef = useRef<HTMLInputElement | null>(null);

const handlePasteImage = (event: ClipboardEvent<HTMLTextAreaElement>) => {
const file = event.clipboardData.files[0];

if (file.type.slice(0, 5) === 'image') {
event.preventDefault();
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 해당 file.type이 뭐라고 나오나요??
찾아도 관련없는 것들이 많이 나오네요
참고하신 자료가 있다면 공유 부탁들려요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

'image/png' , 'image/webp'와 같이 나와요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

참고한 자료는 따로 없고 console.log를 통해 파일의 정보를 확인하고 코드를 작성했었습니다 😃

Copy link
Collaborator

Choose a reason for hiding this comment

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

bb 대단합니다


uploadImage({
imageFile: file,
inputElement: contentInputRef.current,
setPreviewImageUrl: setContentImage,
});
}
};

const removeImage = () => {
setContentImage('');
if (contentInputRef.current) contentInputRef.current.value = '';
};

const handleUploadImage = async (event: ChangeEvent<HTMLInputElement>) => {
const handleUploadImage = (event: ChangeEvent<HTMLInputElement>) => {
const { files } = event.target;

if (!files) return;

const file = files[0];

const webpFileList = await convertImageToWebP(file);

event.target.files = webpFileList;

const reader = new FileReader();

const webpFile = webpFileList[0];

reader.readAsDataURL(webpFile);

event.target.setCustomValidity('');

if (file.size > MAX_FILE_SIZE) {
event.target.setCustomValidity('사진의 용량은 1.5MB 이하만 가능합니다.');
event.target.reportValidity();

return;
}

reader.onloadend = () => {
setContentImage(reader.result?.toString() ?? '');
};
uploadImage({
imageFile: file,
inputElement: contentInputRef.current,
setPreviewImageUrl: setContentImage,
});
};

return { contentImage, contentInputRef, removeImage, handleUploadImage };
return { contentImage, contentInputRef, removeImage, handleUploadImage, handlePasteImage };
};
51 changes: 18 additions & 33 deletions frontend/src/hooks/useWritingOption.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, { ChangeEvent, useState } from 'react';

import { MAX_FILE_SIZE } from '@components/PostForm/constants';

import { convertImageToWebP } from '@utils/resizeImage';
import { uploadImage } from '@utils/post/uploadImage';

const MAX_WRITING_LENGTH = 50;

Expand Down Expand Up @@ -80,6 +78,18 @@ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = IN
setOptionList(updatedOptionList);
};

const setPreviewImageUrl = (optionId: number) => (imageUrl: string) => {
const updatedOptionList = optionList.map(optionItem => {
if (optionItem.id === optionId) {
return { ...optionItem, imageUrl };
}

return optionItem;
});

setOptionList(updatedOptionList);
};

const handleUploadImage = async (
event: React.ChangeEvent<HTMLInputElement>,
optionId: number
Expand All @@ -90,36 +100,11 @@ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = IN

const file = files[0];

const webpFileList = await convertImageToWebP(file);

event.target.files = webpFileList;

const reader = new FileReader();

const webpFile = webpFileList[0];

reader.readAsDataURL(webpFile);

event.target.setCustomValidity('');

if (file.size > MAX_FILE_SIZE) {
event.target.setCustomValidity('사진의 용량은 1.5MB 이하만 가능합니다.');
event.target.reportValidity();

return;
}

reader.onloadend = () => {
const updatedOptionList = optionList.map(optionItem => {
if (optionItem.id === optionId) {
return { ...optionItem, imageUrl: reader.result?.toString() ?? '' };
}

return optionItem;
});

setOptionList(updatedOptionList);
};
uploadImage({
imageFile: file,
inputElement: event.target,
setPreviewImageUrl: setPreviewImageUrl(optionId),
});
};

return { optionList, addOption, writingOption, deleteOption, removeImage, handleUploadImage };
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/utils/post/uploadImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MAX_FILE_SIZE } from '@constants/post';

import { convertImageToWebP } from '@utils/resizeImage';

export const uploadImage = async ({
Copy link
Member

Choose a reason for hiding this comment

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

👍👍

imageFile,
inputElement,
setPreviewImageUrl,
}: {
imageFile: File;
inputElement: HTMLInputElement | null;
setPreviewImageUrl: (previewUrl: string) => void;
}) => {
if (!inputElement) return;

const webpFileList = await convertImageToWebP(imageFile);
Copy link
Member

Choose a reason for hiding this comment

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

오 혹시 정말 사소하게 궁금한 부분인데, 변환하는데 몇초가 걸리는지 궁금해요! 만약 시간이 걸리는 작업이거나, slow 3g 환경이라면 변환 중.. 같은 문구나 로딩 스피너를 보여줘도 좋겠네요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

너무 짧은 시간이라서 따로 설정을 안해줘도 될 것 같아요. 그리고 미리보기는 webP로 변환된 이미지가 아닌 처음 입력받은 이미지로 미리보기 URL을 만들어서 변환 시간은 포함이 안되도록 했습니당

메모리 6배 부하, slow3G일 때

걸린 시간 : 19ms , 0.019초

image

메모리 부하 X, slow3G일 때

걸린 시간 : 8ms , 0.008초

image

메모리 부하 X, 노 쓰로틀링일 때

걸린 시간 : 6ms , 0.006초

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

제로의 제안으로 webp로 이미지 변환 속도를 측정해보았습니다 ~

fe-리뷰요청


inputElement.files = webpFileList;

const reader = new FileReader();

const webpFile = webpFileList[0];

reader.readAsDataURL(webpFile);

inputElement.setCustomValidity('');

if (imageFile.size > MAX_FILE_SIZE) {
inputElement.setCustomValidity('사진의 용량은 1.5MB 이하만 가능합니다.');
inputElement.reportValidity();

return;
}

reader.onloadend = () => {
setPreviewImageUrl(reader.result?.toString() ?? '');
};
};
Loading