Skip to content

Commit

Permalink
[FE] refactor: 이미지 압축 라이브러리 도입 (#786)
Browse files Browse the repository at this point in the history
* refactor: alert를 toast로 수정

* refactor: dialog 컨테이너 div 추가

* refactor: dialog 컨테이너 div 추가

* chore: 디자인 시스템 버전 업

* refactor: dialog 업데이트

* refactor: 이미지 압축 구현

* refactor: 이미지 압축 구현
  • Loading branch information
hae-on authored Oct 18, 2023
1 parent 87c5ac8 commit 6fc930b
Show file tree
Hide file tree
Showing 12 changed files with 66 additions and 24 deletions.
2 changes: 2 additions & 0 deletions frontend/.storybook/preview-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,6 @@
</symbol>
</svg>
</div>
<div id="dialog-container"></div>
<div id="toast-container"></div>

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
"test:coverage": "jest --watchAll --coverage"
},
"dependencies": {
"@fun-eat/design-system": "^0.3.15",
"@fun-eat/design-system": "^0.3.18",
"@tanstack/react-query": "^4.32.6",
"@tanstack/react-query-devtools": "^4.32.6",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down Expand Up @@ -46,7 +47,6 @@
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"babel-plugin-styled-components": "^2.1.4",
"browser-image-compression": "^2.0.2",
"copy-webpack-plugin": "^11.0.0",
"dotenv-webpack": "^8.0.1",
"eslint": "^8.44.0",
Expand Down
1 change: 1 addition & 0 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
</head>
<body>
<div id="root"></div>
<div id="dialog-container"></div>
<div id="toast-container"></div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from 'styled-components';

import { IMAGE_MAX_SIZE } from '@/constants';
import { useEnterKeyDown } from '@/hooks/common';
import { useToastActionContext } from '@/hooks/context';

interface ReviewImageUploaderProps {
previewImage: string;
Expand All @@ -13,6 +14,7 @@ interface ReviewImageUploaderProps {

const ImageUploader = ({ previewImage, uploadImage, deleteImage }: ReviewImageUploaderProps) => {
const { inputRef, handleKeydown } = useEnterKeyDown();
const { toast } = useToastActionContext();

const handleImageUpload: ChangeEventHandler<HTMLInputElement> = (event) => {
if (!event.target.files) {
Expand All @@ -22,7 +24,7 @@ const ImageUploader = ({ previewImage, uploadImage, deleteImage }: ReviewImageUp
const imageFile = event.target.files[0];

if (imageFile.size > IMAGE_MAX_SIZE) {
alert('이미지 크기가 너무 커요. 5MB 이하의 이미지를 골라주세요.');
toast.error('이미지 크기가 너무 커요. 5MB 이하의 이미지를 골라주세요.');
event.target.value = '';
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => {
const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0]);

useEffect(() => {
handleOpenBottomSheet();
}, []);

return (
<BottomSheet ref={ref} isClosing={isClosing} close={handleCloseBottomSheet}>
<BottomSheet isOpen={isOpen} isClosing={isClosing} close={handleCloseBottomSheet}>
<SortOptionList
options={PRODUCT_SORT_OPTIONS}
selectedOption={selectedOption}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ImageUploader, SvgIcon } from '@/components/Common';
import { ProductOverviewItem } from '@/components/Product';
import { MIN_DISPLAYED_TAGS_LENGTH } from '@/constants';
import { useFormData, useImageUploader, useScroll } from '@/hooks/common';
import { useReviewFormActionContext, useReviewFormValueContext } from '@/hooks/context';
import { useReviewFormActionContext, useReviewFormValueContext, useToastActionContext } from '@/hooks/context';
import { useProductDetailQuery } from '@/hooks/queries/product';
import { useReviewRegisterFormMutation } from '@/hooks/queries/review';
import type { ReviewRequest } from '@/types/review';
Expand All @@ -29,10 +29,11 @@ interface ReviewRegisterFormProps {

const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog, initTabMenu }: ReviewRegisterFormProps) => {
const { scrollToPosition } = useScroll();
const { previewImage, imageFile, uploadImage, deleteImage } = useImageUploader();
const { isImageUploading, previewImage, imageFile, uploadImage, deleteImage } = useImageUploader();

const reviewFormValue = useReviewFormValueContext();
const { resetReviewFormValue } = useReviewFormActionContext();
const { toast } = useToastActionContext();

const { data: productDetail } = useProductDetailQuery(productId);
const { mutate, isLoading } = useReviewRegisterFormMutation(productId);
Expand All @@ -41,7 +42,8 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog, initTabMe
reviewFormValue.rating > MIN_RATING_SCORE &&
reviewFormValue.tagIds.length >= MIN_SELECTED_TAGS_COUNT &&
reviewFormValue.tagIds.length <= MIN_DISPLAYED_TAGS_LENGTH &&
reviewFormValue.content.length > MIN_CONTENT_LENGTH;
reviewFormValue.content.length > MIN_CONTENT_LENGTH &&
!isImageUploading;

const formData = useFormData<ReviewRequest>({
imageKey: 'image',
Expand All @@ -64,15 +66,16 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog, initTabMe
resetAndCloseForm();
initTabMenu();
scrollToPosition(targetRef);
toast.success('📝 리뷰가 등록 됐어요');
},
onError: (error) => {
resetAndCloseForm();
if (error instanceof Error) {
alert(error.message);
toast.error(error.message);
return;
}

alert('리뷰 등록을 다시 시도해주세요');
toast.error('리뷰 등록을 다시 시도해주세요');
},
});
};
Expand Down
38 changes: 35 additions & 3 deletions frontend/src/hooks/common/useImageUploader.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,50 @@
import imageCompression from 'browser-image-compression';
import { useState } from 'react';

import { useToastActionContext } from '../context';

const isImageFile = (file: File) => file.type !== 'image/png' && file.type !== 'image/jpeg';

const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true,
};

const useImageUploader = () => {
const { toast } = useToastActionContext();

const [imageFile, setImageFile] = useState<File | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);
const [previewImage, setPreviewImage] = useState('');

const uploadImage = (imageFile: File) => {
const uploadImage = async (imageFile: File) => {
if (isImageFile(imageFile)) {
alert('이미지 파일만 업로드 가능합니다.');
toast.error('이미지 파일만 업로드 가능합니다.');
return;
}

setPreviewImage(URL.createObjectURL(imageFile));
setImageFile(imageFile);

try {
setIsImageUploading(true);

const compressedFile = await imageCompression(imageFile, options);
const compressedImageFilePromise = imageCompression.getFilefromDataUrl(
await imageCompression.getDataUrlFromFile(compressedFile),
compressedFile.name
);
compressedImageFilePromise
.then((result) => {
setImageFile(result);
})
.then(() => {
setIsImageUploading(false);
toast.success('이미지가 성공적으로 등록 됐습니다');
});
} catch (error) {
console.log(error);
}
};

const deleteImage = () => {
Expand All @@ -23,6 +54,7 @@ const useImageUploader = () => {
};

return {
isImageUploading,
previewImage,
imageFile,
uploadImage,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/pages/MemberModifyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SectionTitle, SvgIcon } from '@/components/Common';
import { MemberModifyInput } from '@/components/Members';
import { IMAGE_MAX_SIZE } from '@/constants';
import { useFormData, useImageUploader } from '@/hooks/common';
import { useToastActionContext } from '@/hooks/context';
import { useMemberModifyMutation, useMemberQuery } from '@/hooks/queries/members';
import type { MemberRequest } from '@/types/member';

Expand All @@ -16,6 +17,7 @@ export const MemberModifyPage = () => {
const { mutate } = useMemberModifyMutation();

const { previewImage, imageFile, uploadImage } = useImageUploader();
const { toast } = useToastActionContext();

const [nickname, setNickname] = useState(member?.nickname ?? '');
const navigate = useNavigate();
Expand Down Expand Up @@ -43,7 +45,7 @@ export const MemberModifyPage = () => {
const imageFile = event.target.files[0];

if (imageFile.size > IMAGE_MAX_SIZE) {
alert('이미지 크기가 너무 커요. 5MB 이하의 이미지를 골라주세요.');
toast.error('이미지 크기가 너무 커요. 5MB 이하의 이미지를 골라주세요.');
event.target.value = '';
return;
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/ProductDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ProductDetailPage = () => {
const tabRef = useRef<HTMLUListElement>(null);

const { selectedOption, selectSortOption } = useSortOption(REVIEW_SORT_OPTIONS[0]);
const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const [activeSheet, setActiveSheet] = useState<'registerReview' | 'sortOption'>('sortOption');
const { gaEvent } = useGA();

Expand Down Expand Up @@ -136,7 +136,7 @@ export const ProductDetailPage = () => {
/>
</ReviewRegisterButtonWrapper>
<ScrollButton targetRef={productDetailPageRef} />
<BottomSheet maxWidth="600px" ref={ref} isClosing={isClosing} close={handleCloseBottomSheet}>
<BottomSheet maxWidth="600px" isOpen={isOpen} isClosing={isClosing} close={handleCloseBottomSheet}>
{activeSheet === 'registerReview' ? (
<ReviewFormProvider>
<ReviewRegisterForm
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/ProductListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const ProductListPage = () => {
const { category } = useParams();
const productListRef = useRef<HTMLDivElement>(null);

const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0]);
const { reset } = useQueryErrorResetBoundary();
const { gaEvent } = useGA();
Expand Down Expand Up @@ -68,7 +68,7 @@ export const ProductListPage = () => {
</ProductListContainer>
</ProductListSection>
<ScrollButton targetRef={productListRef} />
<BottomSheet ref={ref} isClosing={isClosing} maxWidth="600px" close={handleCloseBottomSheet}>
<BottomSheet isOpen={isOpen} isClosing={isClosing} maxWidth="600px" close={handleCloseBottomSheet}>
<SortOptionList
options={PRODUCT_SORT_OPTIONS}
selectedOption={selectedOption}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/RecipePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const REGISTER_RECIPE_AFTER_LOGIN = '로그인 후 꿀조합을 작성할 수
export const RecipePage = () => {
const [activeSheet, setActiveSheet] = useState<'registerRecipe' | 'sortOption'>('sortOption');
const { selectedOption, selectSortOption } = useSortOption(RECIPE_SORT_OPTIONS[0]);
const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { reset } = useQueryErrorResetBoundary();
const { gaEvent } = useGA();

Expand Down Expand Up @@ -72,7 +72,7 @@ export const RecipePage = () => {
/>
</RecipeRegisterButtonWrapper>
<ScrollButton targetRef={recipeRef} isRecipePage />
<BottomSheet ref={ref} isClosing={isClosing} maxWidth="600px" close={handleCloseBottomSheet}>
<BottomSheet isOpen={isOpen} isClosing={isClosing} maxWidth="600px" close={handleCloseBottomSheet}>
{activeSheet === 'sortOption' ? (
<SortOptionList
options={RECIPE_SORT_OPTIONS}
Expand Down
8 changes: 4 additions & 4 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1375,10 +1375,10 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==

"@fun-eat/design-system@^0.3.15":
version "0.3.15"
resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.3.15.tgz#61a9a01a82f84fa5627c49bd646cb72ca9e648c8"
integrity sha512-uhn5UZWfvQhNz/2sOoMwDr7Hj7SSx94bN35jifuYpm7ju0A8LHfivmu0mAbrMojuQ6XKYf0ZUME8FMMHwpw9Fg==
"@fun-eat/design-system@^0.3.18":
version "0.3.18"
resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.3.18.tgz#0c930437cd47923a9daffbaec748ef5db3b4d0c1"
integrity sha512-d1yfTLJLKPakFzf/wiDcLkRi5cit16hDJClH4+Mj6nMtChxMeUu3VU+i4oCJNqaNjZHDw9wOa+7L4kmIcKQnRg==

"@humanwhocodes/config-array@^0.11.11":
version "0.11.11"
Expand Down

0 comments on commit 6fc930b

Please sign in to comment.