2024.04.04 TIL - 최종프로젝트 10일차 (기존 데이터 불러와서 수정 update -다중 이미지 포함 )

Innes·2024년 4월 4일
0

TIL(Today I Learned)

목록 보기
107/147
post-thumbnail

개인 액션 수정 페이지

기존에 작성되어 있던 데이터 불러오기

  • 페이지에서 params로 action_id를 받아오고, 해당 action_id와 일치하는 id column의 데이터를 individual_green_actions 테이블에서 가져온다.
  • 이때, 외래키로 연결해놓은 green_action_images 테이블에서 action_id column에 일치하는 action_id의 img url들을 함께 받아온다!
  • 기존 값을 'value'속성에 넣으니까 'onChange'써야한다는 warning도 뜨고, 텍스트가 변경되지도 않았다. 그래서 'defaultValue'속성으로 적용하니까 텍스트 수정도 되고 내가 원하던 대로 구현되었다!
export const getActionForEdit = async (action_id: string) => {
  try {
    const { data, error } = await supabase
      .from("individual_green_actions")
      // ⭐️ 외래키 연결된 테이블에서 url 가져오기
      .select(`*, green_action_images(img_url)`)
      .eq("id", action_id);

    if (error) {
      throw error;
    }
    return data[0];
  } catch (error) {
    console.error(error);
    throw error;
  }
};

수정 update 로직 고민

  • 이제 가져온 original data를 수정하려면.... 액션 등록 페이지 했을 때처럼 formData로 전달해주면 끝인가..? 메서드만 update로 바꾸고..?
    근데 수정된 값이 있는지, 없는지를 따져서 해야될 것 같은데... 스토리지에도 똑같은 파일이 있나 없나도 따져아하지 않나? 그럼 그 파일이름은 어떻게 알수있지? 파일이름 'file.name + uuid'로 만들어놨는데 ㅠ
    텍스트도 변경된게 없으면 그냥 그대로 두고, 변경된 부분만 update하고싶은데...

수정된 내용 update하기

1. 텍스트 수정사항 있는 경우

로직 고민

  • 액션 등록 할때처럼 formData보낸다음 get해서 그걸 update하면 될 것 같은데,
  • 수정사항이 있는 경우에만 해당 값을 update해주는 식으로 하고싶은데...
  • 해당 action_id 열을 삭제해버린다음 그냥 싹 다 다시 insert해버려?!
    아냐... 비효율적이고 맞지도 않아 update메서드를 왜 쓰겠어...
  • 결론 : 테이블에 있는 내용 불러와서, 새 내용과 덮어쓰기 한 후, 덮어쓰기한 최신 내용을 테이블에 다시 update해주기
// edit-api.ts

// 1. 텍스트 formData 업데이트 함수
export const updateActionTextForm = async ({
  action_id,
  formData,
}: {
  action_id: string;
  formData: FormData;
}) => {
  try {
    // 업데이트할 텍스트 데이터
    const nextData: FormDataTypeEdit = {
      title: String(formData.get("activityTitle")),
      content: String(formData.get("activityDescription")),
      start_date: String(formData.get("startDate")),
      end_date: String(formData.get("endDate")),
      location: String(formData.get("activityLocation")),
      recruit_number: Number(formData.get("maxParticipants")),
      kakao_link: String(formData.get("openKakaoLink")),
    };

    // ⭐️ supabase에서 해당 action_id의 데이터 가져오기
    const { data: existingData, error } = await supabase
      .from("individual_green_actions")
      .select()
      .eq("id", action_id)
      .single();

    if (error) {
      throw error;
    }

    // ⭐️ 기존 데이터와 새 데이터 비교하여 변경된 부분 업데이트
    // 이렇게 하면 덮어쓰기가 된다는걸 처음 알았다.
    const updatedData = { ...existingData, ...nextData };

    // ⭐️ supabase에서 업데이트
    // 수정된 객체를 바로 업데이트로 넣어주기
    const { error: updateError } = await supabase
      .from("individual_green_actions")
      .update(updatedData)
      .eq("id", action_id);

    if (updateError) {
      throw updateError;
    }

    return action_id;
  } catch (error) {
    console.error("Error updating data:", error);
    throw error;
  }
};

2. 이미지 수정사항 있는 경우

치열했던 로직 고민들...

로직 고민 1차

  • 스토리지에 이미지파일 업로드
    (action_id 해당하는 폴더 찾고, 거기에 해당 파일 있으면 냅두고, 수정할때 새로운거 update뿐만 아니라 파일을 삭제했을수도 있으니까 삭제한게 있는지도 체크해야하고... 삭제했으면 파일도 지우고 새로운 파일은 업로드하고 이런식으로 가야하지 않나)
  • 수정된 파일의 url 반환하기
  • 삭제된 파일 있으면 green_action_images 테이블에서도 삭제해야함 ㅠㅠ


    🧡 아무리 Gpt가 발전했다지만, 내가 원하는 로직을 완벽하게 구현해주진 못하고, 아무리 내가 원하는 기능들을 충분히 설명하더라도 완벽한 코드로 뱉어주지 않기에 결국 내가 원하는 로직을 제대로 생각하고, 가장 효율적이고 효과적인 방법을 혼자 많이 고민해야한다는 점에서 결국 gpt가 있어도 스스로 사고하는 힘이 개발자에게는 필수라는 생각이 든다.

로직 고민 2차 - 튜터님께 sos

  • 스토리지는 새로운 파일 생겼을때 업데이트만 하고, 굳이 삭제까지 열심히 할 필요는 없다는 것이 튜터님 의견
  1. newFiles 배열을 들고 와서 스토리지에 업데이트(덮어쓰기) 하고, newUrls배열 반환
  2. newUrls배열 가지고 imges테이블로 가서, action_id의 urls에 덮어쓰기(update)
    ?? 잠만.. 근데 그러면 만약 이런 상황은??
    • 스토리지 : 1 2 3 이미지가 있음
    • 이미지 테이블 : 1 2 3 url이 있음
    • 내가 페이지에서 1 2 4 로 이미지를 업데이트함 (3삭제, 4추가)
    • 🤍 1 2 4의 파일명 알아놓기
    • 🤍 스토리지에 1 2 4를 덮어쓰기
    • 스토리지에는 4가 추가돼서 1 2 3 4가 됨
    • 🤍 스토리지에서 반환하는 url은 1 2 4만 반환하도록 하기
      (알아놓은 파일명 가지고 url 반환해오면 됨 getPublicUrl)
      (유저가 업데이트한 파일배열대로 url을 반환해오면 되는것임)
    • 🤍 1 2 4 의 url을 들고 테이블로 가서 비교 후 업데이트
      (텍스트 formData처럼 덮어쓰기로 update도 괜찮을듯)
    • 테이블엔 1 2 3이 있으니, 3을 4로 업데이트

로직 고민 3차

  • 새롭게 들어온 이미지 파일 스토리지에 업로드
  • 폴더 안에 있는 이미지 url 그냥 전부다 가져오자
  • images 테이블에 action_id에 해당하는 이미지들 url 따져보고, 없는거 업데이트 하자
    (근데 그럼 삭제한건?ㅠ 또 도돌이표네)
    (렌더링할때 파일들을 들고 들어와서 set하려했는데 스토리지에 있는 이미지들을 가져올게 아니라 테이블의 url과 일치하는 파일을 가져와야할거아냐)
    (그럼 스토리지에 이미지가 쌓인다면 수정하면서 삭제한 이미지 처리는 어떻게 해야하는거지?)

대망의 해결책

진짜... 이거 하는데 하루가 걸렸다... 로직이 진짜 어려웠다. 내가 복잡하게 생각해서 그런건지...?ㅠㅠ


⭐️ 아무튼 핵심은 deletedIds를 state로 따로 관리하는거였다.
이미지 삭제 눌렀을때, 기존에 테이블에 있던 이미지를 가져온거라면 img_id가 있을테니 그걸 state배열에 저장. 기존에 테이블에 있던 이미지가 아니면 img_id가 null이니까 id없는 경우는 배열 저장하지 않음.
그리고 '수정완료' 클릭시 deletedIds를 가지고 이미지 테이블의 id에 해당하는 id가 있으면 그 열은 삭제.


⭐️ 결국 이미지 테이블을 제대로 관리하는게 가장 중요했다. 스토리지는 그냥 업데이트보다는 이미지 계속 업로드만 한다고 생각해야되고, 삭제까지는 아직 어려운 단계라고 한다.
그리고 결국 화면에서 보여주는 부분을 이미지 테이블에서 url을 가져와서 보여주기 때문에 테이블에서 새로운거 추가 및 삭제를 제대로 연결시켜놓는게 가장 중요한 포인트였다.

// individualAction / edit / [id] / page.tsx

// 페이지 접근시 action_id의 기존 데이터 가져오기
  const {
    data: originalActionData,
    isLoading: isOriginalDataLoading,
    isError: isOriginalDataError,
  } = useQuery({
    queryKey: [QUERY_KEY_INDIVIDUALACTION_FOR_EDIT],
    // 데이터 가져오면서 동시에 {id: 이미지id, img_url: 이미지url} 객체 배열을 uploadedFileUrls에 set
    // (이미지 삭제 시 이미지id가 필요하기 때문)
    queryFn: async () => {
      try {
        // getActionForEdit 함수 호출하여 데이터 가져오기
        const data = await getActionForEdit(action_id);

        // uploadedFileUrls에 {id: img_id, img_url: img_url} 객체 배열을 set하기
        const idsAndUrlsObjArray = [...data.green_action_images];
        setUploadedFileUrls(idsAndUrlsObjArray);

        // 가져온 데이터 반환
        return data;
      } catch (error) {
        throw new Error("Error fetching data");
      }
    },
  });

  if (isOriginalDataLoading) {
    return <div>Loading...</div>;
  }
  if (isOriginalDataError) {
    return <div>Error</div>;
  }

// individualAction / edit / [id] / page.tsx

 // '수정완료' 클릭시
  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    const formData = new FormData(event.currentTarget);

    try {
      // 확인창 표시
      const isConfirmed = window.confirm("수정하시겠습니까?");
      if (isConfirmed) {
        // 1. action_id와 텍스트 formData 보내서 update
        await updateActionTextForm({
          action_id,
          formData,
        });

        // 2. 이미지 스토리지에 저장하기 + 이미지 url 배열 반환받기
        // 기존에 있던 이미지 외에, 새롭게 업로드한 이미지들이 file저장 + url반환됨
        const imgUrlsArray = await uploadFilesAndGetUrls({ files, action_id });

        // 3. 반환받은 url배열을 테이블에 insert
        await insertImgUrls({ action_id, imgUrlsArray });

        // 4. deleteFileIds 참고, 테이블에 해당 id 있으면 행 삭제
        // 기존에 스토리지에 있던 이미지를 삭제 요청한 경우 테이블에서 삭제해주는 로직
        await deleteImagesByIds(deleteFileIds);

        // 입력값 초기화
        const target = event.target as HTMLFormElement;
        target.reset();

        // 확인을 클릭하면 action_id의 상세페이지로 이동
        router.push(`/individualAction/detail/${action_id}`);
      }
    } catch (error) {
      console.error("Error inserting data:", error);
    }
  };
// ImgEdit.tsx

  // 이미지 미리보기 띄우기
  const handleShowPreview = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) {
      return;
    }
    const imageUrl = URL.createObjectURL(file);
    setUploadedFileUrls((prev) => [...prev, { id: "", img_url: imageUrl }]);
    setFiles((prev) => [...prev, file]);
  };

  // 미리보기 이미지 삭제
  const handleDeleteImage = (index: number, deletedId: string) => {
    // deletedId가 null이 아닌 경우 이미지id를 배열에 추가
    // (이 이미지id 배열을 이용해서, 테이블에서 해당id가 있으면 행을 삭제할 예정)
    if (deletedId) {
      setDeleteFileIds((prev) => {
        return [...prev, deletedId];
      });
    }
    setUploadedFileUrls((prev) => {
      const updatedUrls = [...prev];
      updatedUrls.splice(index, 1);
      return updatedUrls;
    });
    setFiles((prev) => {
      const updateFiles = [...prev];
      updateFiles.splice(index, 1);
      return updateFiles;
    });
  };
// edit-api.ts

// 1. 텍스트 formData 업데이트 함수
type FormDataWithoutUid = Omit<FormDataType, "user_uid">;
export const updateActionTextForm = async ({
  action_id,
  formData,
}: {
  action_id: string;
  formData: FormData;
}) => {
  try {
    // 업데이트할 텍스트 데이터
    const nextData: FormDataWithoutUid = {
      title: String(formData.get("activityTitle")),
      content: String(formData.get("activityDescription")),
      start_date: String(formData.get("startDate")),
      end_date: String(formData.get("endDate")),
      location: String(formData.get("activityLocation")),
      recruit_number: Number(formData.get("maxParticipants")),
      kakao_link: String(formData.get("openKakaoLink")),
    };

    // supabase에서 해당 action_id의 데이터 가져오기
    const { data: existingData, error } = await supabase
      .from("individual_green_actions")
      .select()
      .eq("id", action_id)
      .single();

    if (error) {
      throw error;
    }

    // 기존 데이터와 새 데이터 비교하여 변경된 부분 업데이트 (덮어쓰기)
    const updatedData = { ...existingData, ...nextData };

    // supabase에서 업데이트
    const { error: updateError } = await supabase
      .from("individual_green_actions")
      .update(updatedData)
      .eq("id", action_id);

    if (updateError) {
      throw updateError;
    }

    return action_id;
  } catch (error) {
    console.error("Error updating data:", error);
    throw error;
  }
};

// 2. 스토리지의 action_id 폴더에 이미지 추가 + 방금 추가한 이미지들 url배열 반환
// (기존에 없던 새롭게 추가한 이미지들이 스토리지에 저장됨)
export const uploadFilesAndGetUrls = async ({
  files,
  action_id,
}: FileUpload) => {
  try {
    const imgUrlsArray = await Promise.all(
      // map으로 (파일 스토리지에 업로드 + url 반환) 반복
      files.map(async (file) => {
        if (file) {
          console.log("file.name", file.name);
          const fileName = `${(file.name, crypto.randomUUID())}`;
          // 'action_id' 폴더 생성, 파일이름은 uuid
          const filePath = `${action_id}/${fileName}`;
          const { error } = await supabase.storage
            // 'green_action' 버켓에 이미지 업로드
            .from("green_action")
            .upload(filePath, file, {
              cacheControl: "3600",
              upsert: true,
            });

          if (error) {
            console.error("Error uploading file:", error);
            return null;
          }

          // url 가져오기
          const imgUrl = await supabase.storage
            .from("green_action")
            .getPublicUrl(`${action_id}/${fileName}`);

          if (!imgUrl || !imgUrl.data) {
            console.error("Error getting public URL for file:", imgUrl);
            throw new Error("Error getting public URL for file");
          }

          return imgUrl.data.publicUrl;
        }
      }),
    );
    // null 또는 undefined 값 제거 후 string 배열로 변환
    const TypeFilteredUrls = imgUrlsArray.filter(
      (url) => url !== null && url !== undefined,
    ) as string[];

    return TypeFilteredUrls;
  } catch (error) {
    console.error("Error uploading files and getting URLs:", error);
    return [];
  }
};

// 3. 이미지url들 table에 넣기
export const insertImgUrls = async ({
  action_id,
  imgUrlsArray,
}: InsertImgUrls) => {
  try {
    const response = await Promise.all(
      imgUrlsArray.map(async (url: string) => {
        const { data, error } = await supabase
          .from("green_action_images")
          .insert({
            action_id,
            img_url: url,
          });

        if (error) {
          throw error;
        }
        return data;
      }),
    );
    return response;
  } catch (error) {
    console.log("error", error);
    throw error;
  }
};

// 4. deleteFileIds 배열에 있는 id가 테이블에 있으면 해당 행 삭제
export const deleteImagesByIds = async (deleteFileIds: string[]) => {
  try {
    // deleteFileIds에 포함된 각 id에 대한 삭제 요청
    await Promise.all(
      deleteFileIds.map(async (id) => {
        const { error } = await supabase
          .from("green_action_images")
          .delete()
          .eq("id", id);
        if (error) {
          throw error;
        }
      }),
    );
    console.log("Images deleted successfully.");
  } catch (error) {
    console.error("Error deleting images:", error);
    throw new Error("Error deleting images");
  }
};
profile
무서운 속도로 흡수하는 스폰지 개발자 🧽

0개의 댓글