diff --git a/src/common/libs/firebase/firebaseManager.js b/src/common/libs/firebase/firebaseManager.ts similarity index 60% rename from src/common/libs/firebase/firebaseManager.js rename to src/common/libs/firebase/firebaseManager.ts index 8007773b..088ff2fa 100644 --- a/src/common/libs/firebase/firebaseManager.js +++ b/src/common/libs/firebase/firebaseManager.ts @@ -1,12 +1,16 @@ import { ref as firebaseRef, getDownloadURL, - uploadString, + uploadBytes, } from 'firebase/storage'; +import { uniqueId } from 'lodash'; import { firebaseStorage } from './firebaseConfig'; -export const uploadFirebase = async (userId, file, dir) => { - let storageRef; +export const uploadFirebase = async ( + userId: number, + file: File, + dir = 'posting', +) => { const date = new Date(); const year = date.getFullYear(); const month = ('0' + (1 + date.getMonth())).slice(-2); @@ -16,11 +20,10 @@ export const uploadFirebase = async (userId, file, dir) => { const seconds = ('0' + date.getSeconds()).slice(-2); const now = year + month + day + hours + minutes + seconds; - storageRef = firebaseRef(firebaseStorage, `${dir}/${userId}_${now}`); - try { - const snapshot = await uploadString(storageRef, file, 'data_url'); - return getDownloadURL(snapshot.ref); - } catch (e) { - return ''; - } + const storageRef = firebaseRef( + firebaseStorage, + `${dir}/${userId}_${now}_${uniqueId()}`, + ); + const snapshot = await uploadBytes(storageRef, file); + return getDownloadURL(snapshot.ref); }; diff --git a/src/features/posts/components/post-form/ImagePreview.tsx b/src/features/posts/components/post-form/ImagePreview.tsx index a3f04f19..53811296 100644 --- a/src/features/posts/components/post-form/ImagePreview.tsx +++ b/src/features/posts/components/post-form/ImagePreview.tsx @@ -1,7 +1,7 @@ import DeleteIcon from '@/common/assets/images/write/delete-icon.svg'; interface ImagePreviewProps { - images: { id?: number; url: string }[]; + images: string[]; onDelete: (index: number) => () => void; } @@ -9,12 +9,9 @@ export function ImagePreview({ images, onDelete }: ImagePreviewProps) { return (
{images.map((image, index) => ( -
+
{'업로드 diff --git a/src/features/posts/components/post-form/PostForm.tsx b/src/features/posts/components/post-form/PostForm.tsx index 0e75977f..9228f21c 100644 --- a/src/features/posts/components/post-form/PostForm.tsx +++ b/src/features/posts/components/post-form/PostForm.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { DockedButton, EditButton } from '@/common/components/ui/buttons'; import { Select } from '@/common/components/ui/selections'; import { CATEGORY_OPTIONS, DEADLINE_OPTIONS } from '@/common/constants/options'; @@ -49,7 +50,7 @@ export function PostForm({ defaultValue={formData.title} /> image.url)} onDelete={handlers.imageDelete} /> void; onChange: (data: PostFormData) => void; }) { - const { user } = useUser(); const [formData, setFormData] = useState( initialData || { title: '', @@ -49,20 +45,10 @@ export function usePostForm({ }; const handleMainImageUpload = (file: File) => { - const reader = new FileReader(); - reader.onload = (e) => { - if (!e.target?.result || !user) { - return; - } - resizeImage(e.target.result.toString()).then(async (result) => { - const imgUrl = await uploadFirebase(user.id, result, 'posting'); - setFormData({ - ...formData, - images: [...formData.images, { url: imgUrl }], - }); - }); - }; - reader.readAsDataURL(file); + setFormData({ + ...formData, + images: [...formData.images, { file, url: URL.createObjectURL(file) }], + }); }; const handleTitleChange = (title: string) => { @@ -84,21 +70,15 @@ export function usePostForm({ }; const handleVoteOptionImageUpload = (index: number) => (file: File) => { - const reader = new FileReader(); - reader.onload = (e) => { - if (!e.target?.result || !user) { - return; - } - resizeImage(e.target.result.toString()).then(async (result) => { - const imgUrl = await uploadFirebase(user.id, result, 'posting'); - const newVoteOptions = formData?.voteOptions - ? [...formData.voteOptions] - : []; - newVoteOptions[index].image = imgUrl; - setFormData({ ...formData, voteOptions: newVoteOptions }); - }); - }; - reader.readAsDataURL(file); + const newVoteOptions = formData?.voteOptions + ? [...formData.voteOptions] + : []; + newVoteOptions[index].image = { file, url: URL.createObjectURL(file) }; + + setFormData({ + ...formData, + voteOptions: newVoteOptions, + }); }; const handleVoteOptionImageDelete = (index: number) => () => { diff --git a/src/features/posts/types/post-form.ts b/src/features/posts/types/post-form.ts index a147fd38..3e7fd014 100644 --- a/src/features/posts/types/post-form.ts +++ b/src/features/posts/types/post-form.ts @@ -3,12 +3,16 @@ export interface PostFormData { content: string; images: { id?: number; + file: File; url: string; }[]; categoryId?: number; deadline?: number; voteOptions: { label: string; - image: string | null; + image: { + file: File; + url: string; + } | null; }[]; } diff --git a/src/pages/write/WritePage.tsx b/src/pages/write/WritePage.tsx index f013b656..b5a69bdb 100644 --- a/src/pages/write/WritePage.tsx +++ b/src/pages/write/WritePage.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { TopAppBar } from '@/common/components/layout'; import { Popup } from '@/common/components/ui/modal'; +import { uploadFirebase } from '@/common/libs/firebase/firebaseManager'; import { scrollRestorationAtom } from '@/common/states/scroll-restoration'; import getFutureDateTime from '@/common/utils/getFutureDateTime'; import { PostForm } from '@/features/posts/components/post-form/PostForm'; @@ -17,6 +18,7 @@ export default function WritePage() { const navigate = useNavigate(); const { mutate: addPost, isLoading, isSuccess, data } = useAddPost(); const [hasChanges, setHasChanges] = useState(false); + const [uploadLoading, setUploadLoading] = useState(false); const { user } = useUser(); const handleChange = ({ @@ -48,10 +50,25 @@ export default function WritePage() { return; } + setUploadLoading(true); + const uploadPromises = [ + ...images.map((image) => uploadFirebase(user.id, image.file)), + ...voteOptions.map( + (option) => option.image && uploadFirebase(user.id, option.image.file), + ), + ]; + const result = (await Promise.all(uploadPromises)).filter( + (url) => !!url, + ) as string[]; + setUploadLoading(false); + const numOfMainImages = images.length; + addPost({ title: title.trim(), content: content.trim(), - files: images.map((image) => ({ url: image.url, contentType: 'image' })), + files: result + .slice(0, numOfMainImages) + .map((url) => ({ url, contentType: 'image' })), userId: user?.id, expirationTime: getFutureDateTime(deadline), choices: voteOptions @@ -59,7 +76,7 @@ export default function WritePage() { .map((option, i) => ({ sequenceNumber: i, label: option.label.trim(), - url: option.image || null, + url: result[numOfMainImages + i], })), worryCategoryId: categoryId, }); @@ -81,7 +98,7 @@ export default function WritePage() { } }, [isSuccess, data, navigate]); - if (isLoading) { + if (isLoading || uploadLoading) { return ; }