"포스트 생성에 실패했습니다" 실패 문구와 함께 에러가 발생합니다ㅠㅠ
해결됨
한 입 크기로 잘라먹는 React.js 실전 프로젝트 - SNS 편
안녕하세요 정환님! 정환님의 강의를 들으면서 코드를 따라 작성해보다가 포스트와 이미지를 최종적으로 업로드 하는 과정에서 아래의 캡쳐 사진과 같이 포스트 생성에 실패했다는 에러 메시지가 뜨게됩니다. 네트워크 탭에 보면, 포스트 부분은 잘 전달이 되는거같은데 이미지 생성에 관한 결과가 보이지 않는 것으로 보아 이미지 생성 관련 코드에서 문제가 생긴것같은데 무슨 문제일까요??..ㅠ 아래는 해당 코드들입니다. 이미지 생성 관련 코드를 가져왔는데 혹여나 다른 부분의 코드가 필요하면 추가 업로드 드리겠습니다. api/post.ts import type { PostEntity } from './../types'; import supabase from '@/lib/supabase'; export async function createPost(content: string) { const { data, error } = await supabase .from('post') .insert({ content }) .select() .single() if (error) throw error return data } export async function createPostWithImages({ content, images, userId }: { content: string images: File[] userId: string }) { // 1. 새로운 포스트 생성 (맨 위에서 생성한 포스트 비동기 함수 가져오기) const post = await createPost(content) if (images.length === 0) return post // 2. 이미지 업로드 try { const imageUrls = await Promise.all( images.map((image) => { const fileExtension = image.name.split(".").pop() || "webp" const fileName = `${Date.now()}-${crypto.randomUUID()}.${fileExtension}` const filePath = `${userId}/${post.id}/${fileName}` return uploadImage({ file: image, filePath }) }) ) // 3. 포스트 테이블 업데이트 const updatedPost = await updatePost({ id: post.id, image_urls: imageUrls }) return updatedPost } catch(error) { await deletePost(post.id) throw error } } export async function updatePost(post: Partial<PostEntity> & { id: number }) { const { data, error } = await supabase .from("post") .update(post) .eq("id", post.id) .select() .single() if (error) throw error return data } export async function deletePost(id: number) { const { data, error } = await supabase .from("post") .delete() .eq("id", id) .select() .single() if (error) throw error return data } hooks/mutations/post/use-create-post.ts import type { UseMutationCallback } from './../../../types'; import { createPost, createPostWithImages } from '@/api/post'; import { useMutation } from '@tanstack/react-query'; export function useCreatePost(callbacks?:UseMutationCallback) { return useMutation({ mutationFn: createPostWithImages, onSuccess: () => { if(callbacks?.onSuccess) callbacks.onSuccess() }, onError: (error) => { if (callbacks?.onError) callbacks.onError(error) } }) } components/modal/post-editor-modal.tsx import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' import { useCreatePost } from '@/hooks/mutations/post/use-create-post' import { usePostEditorModal } from '@/store/post-editor-modal' import { useSession } from '@/store/session' import { ImageIcon, XIcon } from 'lucide-react' import { useRef, useState, useEffect, ChangeEvent } from 'react' import { toast } from "sonner" import { Carousel, CarouselContent, CarouselItem } from '../ui/carousel' type Image = { file: File, previewUrl: string } export default function PostEditorModal() { const session = useSession() const { isOpen, close } = usePostEditorModal() const { mutate: createPost, isPending: isCreatePostPending } = useCreatePost({ onSuccess: () => { close() }, onError: (error) => { toast.error("포스트 생성에 실패했습니다", { position: "top-center" }) } }) const [content, setContent] = useState("") const [images, setImages] = useState<Image[]>([]) const textareaRef = useRef<HTMLTextAreaElement>(null) const fileInputRef = useRef<HTMLInputElement>(null) const handleCloseModal = () => { close() } const handleCreatePostClick = () => { if (content.trim() === "") return createPost({ content, images: images.map((image) => image.file), userId: session!.user.id }) } const handleSelectImages = (e:ChangeEvent<HTMLInputElement>) => { if (e.target.files) { const files = Array.from(e.target.files) files.forEach((file) => { setImages((prev) => [ ...prev, { file, previewUrl: URL.createObjectURL(file) }, ]) }) } e.target.value = "" } const handleDeleteImage = (image:Image) => { setImages((prevImages) => prevImages.filter((item) => item.previewUrl !== image.previewUrl)) } useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto" textareaRef.current.style.height = textareaRef.current.scrollHeight + "px" } }, [content]) useEffect(() => { if (!isOpen) return textareaRef.current?.focus() setContent("") setImages([]) }, [isOpen]) return ( <Dialog open={isOpen} onOpenChange={handleCloseModal}> <DialogContent className="max-h-[90vh]"> <DialogTitle>포스트 작성</DialogTitle> <textarea disabled={isCreatePostPending} ref={textareaRef} value={content} onChange={(e) => setContent(e.target.value)} className="max-h-125 min-h-25 focus:outline-none" placeholder="무슨 일이 있었나요?" /> <input onChange={handleSelectImages} ref={fileInputRef} type="file" accept="image/*" multiple className="hidden" /> {images.length > 0 && <Carousel> <CarouselContent> {images.map((image) => <CarouselItem className="basis-2/5" key={image.previewUrl}> <div className="relative"> <img src={image.previewUrl} className="h-full w-full rounded-sm object-cover" /> <div onClick={() => handleDeleteImage(image)} className="absolute top-0 right-0 m-1 cursor-pointer rounded-full bg-black/30 p-1" > <XIcon className="h-4 w-4 text-white"/> </div> </div> </CarouselItem>)} </CarouselContent> </Carousel>} <Button onClick={() => { fileInputRef.current?.click() }} variant={"outline"} className="cursor=pointer" > <ImageIcon/> 이미지 추가</Button> <Button disabled={isCreatePostPending} onClick={handleCreatePostClick} className="cursor=pointer"> 저장 </Button> </DialogContent> </Dialog> ) }
- react
- typescript
- react-query
- supabase
- zustand





