Notice
Recent Posts
Recent Comments
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

HYEWON JUNG의 개발일지

20240119 TIL 현재코드 정리 본문

개발일지

20240119 TIL 현재코드 정리

혜won 2024. 1. 22. 12:59

게시글 작성, 수정

유저 프로필 가져와서 setProfile해주기 WritePost

useEffect(() => {
    const fetchData = async () => {
      try {
        const {
          data: { user }
        } = await supabase.auth.getUser(); //auth에서 현재 로그인 중인 유저정보 가져오기
        setUserId(user!.id);

        const { data: profiles, error } = await supabase
          .from('user')
          .select('*')
          .eq('id', user!.id);  //유저의 id와 일치한 row가져와서 추가정보 가져오기

        if (error) {
          console.log(error);
        }

        if (profiles != null) {
          setProfile(profiles);
        }
      } catch (error: any) {
        console.log(error.message);
      }
    };

    fetchData();
  }, []);

WriteLayout으로 props 내려주기 WritePost

<WriteLayout
          profile={profile}
          isEdit={false}
          paramId=""
          setIsEditState={() => {}}
        />

위 코드에서 set해준 profile를 넘겨주고 isEdit은 false, paramId, setIsEditState 는 글 첫 작성과 글 수정 컴포넌트를 통합해서 detail에서 필요한 props이니 공란으로 넘긴다.

동일하게CommuDetail에서도

<WriteLayout
            profile={undefined}
            isEdit={true}
            paramId={param.id}
            setIsEditState={setIsEditState}
          />

props로 내려주면 된다.

WriteLaaout의 props 타입은

export type WriteLayoutProps = {
  profile: ProfileObject[] | undefined;
  isEdit: boolean;
  paramId: string | undefined;
  setIsEditState: (isEditState: boolean) => void;
};

이렇게 정의해주었다.

수정상태 detail에서는 isEditState으로 read 상태인지 Update 상태인지 구분하고 WriteLayout 에서는 isEdit으로 게시글 수정인지 게시글 첫 작성인지 구분한다.

글 작성 로직 설명 WriteLayout.tsx

storage upload & get (files , images)

파일 입력을 감지하는 함수다.

const handleFiles = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const fileList = e.target.files; //선택된 파일 목록
    if (fileList) {
      const filesArray = Array.from(fileList); //배열로 변환
      filesArray.forEach((file) => {  //각 파일을 전달
        handleFilesUpload(file);
      });
    }
  };

file 스토리지 upload & get

const handleFilesUpload = async (file: File) => {
    setFormValues((prevValues: any) => ({  //이전 선택 파일 지우기
      ...prevValues,
      files: [],
      uploadedFileUrl: []
    }));
    try {
      const newFileName = uuid();  //supabase가 한글이름인식 안함
      const { data, error } = await supabase.storage
        .from('files')
        .upload(`files/${newFileName}`, file);
      if (error) {
        console.log('파일 업로드 중 오류가 발생했습니다', error);
        return;
      }
      const res = supabase.storage.from('files').getPublicUrl(data.path);
      setFormValues((prevValues: any) => ({
        ...prevValues,
        files: [...prevValues.files, file], //가져오는 속도가 달라서 
        uploadedFileUrl: [...prevValues.uploadedFileUrl, res.data.publicUrl]
      }));
      console.log(res.data.publicUrl);
    } catch (error) {
      console.error('파일을 업로드하지 못했습니다:', error);
    }
  };

파일url과 파일은 스토리지에 넣고 가져오면서 같이 넣은 두 파일이 같이 오는 게 아니라 순차적으로 들어오기 때문에 원래 파일에 더해주는 식으로 진행했다

이미지 upload & get

const quillRef = useRef<ReactQuill | null>(null);

  const imageHandler = () => {
    try {
      const input = document.createElement('input'); 
			//input 엘리먼트 생성, 업로드 상자를 여는 컨트롤 역할
      input.setAttribute('type', 'file');
			//위 input의 타입 지정
      input.setAttribute('accept', 'image/*');
			//사용자가 선택할 수 있는 파일 종류 제한 (모든 이미지 가능)
      input.click();
      input.addEventListener('change', async () => {
        const file = input.files![0];
        const fileNewName = uuid();

        const { data, error } = await supabase.storage // 업로드
          .from('images')
          .upload(`quill_imgs/${fileNewName}.png`, file);
        if (error) {
          console.error('이미지 업로드 중 오류 발생:', error);
        } else {
          console.log('이미지가 성공적으로 업로드되었습니다:', data);
        }

        const response = supabase.storage  //겟
          .from('images')
          .getPublicUrl(`quill_imgs/${fileNewName}.png`);
        console.log(response);
        if (response.data) {
          const postImageUrl = response.data.publicUrl;
          const editor = quillRef.current?.getEditor(); //에디터 정보 가져오기
          const range = editor?.getSelection(true); //에디터 커서 위치
					//커서 위치에 이미지 넣기
          editor?.insertEmbed(range?.index || 0, 'image', postImageUrl);
          setFormValues((prevValues) => ({
            ...prevValues,
            mainImage: '이미지있음'  
					//아마 변경될 로직.. 중간때는 목록에 사진없이 진행할듯합니다.
          }));
          editor?.setSelection((range?.index || 0) + 1, 0);
					//이미지 다음칸으로 커서이동
					//다음줄로 이동했으면 해서 수정 예정
          console.log('가져왔다');
        } else {
          console.error('No public URL found in response data.');
        }
      });
    } catch (error) {
      console.log('error', error);
    }
  };

커뮤니티에선 wysiwyg 라이브러리인 react-quill을 사용해서 글작성을 하는데 이미지을 삽입하는핸들러가 기본적으로 있지만 src가 base 64문자열로 엄청 길기도 하고 db에 이미지를 바로 넣는 것은 용량문제가 생기기 때문에 이미지 핸들러를 새로 정의를 해줬다.

이미지 리사이징

import { ImageActions } from '@xeger/quill-image-actions';
import { ImageFormats } from '@xeger/quill-image-formats';

외부 모듈 사용

addPost 쿼리 설정

export const addPostMutation = async (insertData: InsertObject) => {
  try {
    const { data, error } = await supabase
      .from('community')
      .insert([insertData]);
    if (error) throw error;
  } catch (error) {
    console.error('Error adding post:', error);
  }
}
const addMutation = useMutation(addPostMutation, {
    onSuccess: (data) => {
      console.log(data);
      queryClient.invalidateQueries('posts');
      navigate('/community');
    }
  });

  const addPost = async () => {
    const insertData = {
      title: formValues.title,
      content: formValues.content,
      category: formValues.category,
      post_user: profile![0].id,
      nickname: profile![0].nickname
        ? profile![0].nickname
        : profile![0].username,
      files: formValues.files.map((file: File) => ({
        name: file.name,
        url: formValues.uploadedFileUrl
      })),
      main_image: formValues.mainImage,
      anon: formValues.anon
    };
    addMutation.mutate(insertData);
  };

아직 유효성 검사를 넣지 않았음, 오늘 내일로 추가예정

게시글 수정/ 업데이트

export const updatePostMutation = async (
  postData: UpdateObject | CommentUpload
) => {
  try {
    const { data, error } = await supabase
      .from('community')
      .update(postData.updateData)
      .eq('post_id', postData.paramId);

    if (error) {
      throw error;
    }
    return data;
  } catch (error) {
    console.error('Error update post:', error);
  }
};
const updatePost = () => {
    const fileArr = formValues.files.map((file: File) => ({
      name: file.name,
      url: formValues.uploadedFileUrl
    }));
    const postData = {
      updateData: {
        title: formValues.title,
        content: formValues.content,
        anon: formValues.anon,
        files: fileArr,
        main_image: formValues.mainImage,
        category: formValues.category
      },
      paramId
    };
    updateMutation.mutate(postData);
  };

업데이트와 추가는 거의 비슷하다. editValue를 따로 두지 않고 작업을 했기때문이다. 초기값을 편집 상태에 따라 달라지게 설정하였다.

file map부분이 같아서 따로 뺐다.

게시글 삭제 CommuDetail.tsx

export const deletePostMutation = async (postId: string | undefined) => {
  const { data, error } = await supabase
    .from('community')
    .delete()
    .eq('post_id', postId);

  if (error) {
    throw error;
  }
  return data;
};
const deleteMutation = useMutation(deletePostMutation, {
    onSuccess: () => {
      queryClient.invalidateQueries('posts');
    }
  });
  const deletePost = async () => {
    if (window.confirm(`정말로"${posts![0].title}" 글을 삭제하시겠습니까?`)) {
      deleteMutation.mutate(param.id);
      navigate('/community');
    }
  };

게시글 Read(트러블 슈팅에 들어갈 내용)

in CommunityList

const handleText = (content: string): string => {
    // 정규 표현식을 사용하여 태그를 제외한 텍스트만 추출
    const textOnly = content.replace(/<[^>]*>|&nbsp;/g, ' ');

    return textOnly;
  };

return (
<ContentArea>{handleText(post.content)}</ContentArea>
)

커뮤니티 목록에서는 콘텐츠의 텍스트만 나와야하는데 react-quill은 콘텐츠를 html 문자열로 주니가 파싱을 하고 조건부로 노출되게 해야하는데 문자열인점을 이용해 정규식으로 태그와  를 없앴다. 가끔 붙여넣기를 한 글에서 띄어쓰기가 된 부분이  로 그대로 나오는 경우가 있어서 추가해주었다.

in CommuDetail

import parse from 'html-react-parser';

return(
<Content>{parse(post.content)}</Content>
)

html-react-parser 라이브러리를 이용해서 html코드를 파싱해줬다.

in WriteLayout (isEdit)

content: isEdit
      ? posts![0].content.replace(/<p>(.*?)<\\/p>/g, ' <div>$1</div>')
      : '',

초기값에 정규식을 사용해서 <p></p>를 <div></div>로 변경해주었다.

그이유는 수정을 하기위해서 콘텐츠의 html문자열을 그대로 넣게되면

이랬던 글이

이렇게 되는 것이다.

개발자 도구에서 확인해보니

<p><img src=""/></p><p>텍스트</p>
//이게 
<p><img src=""/>텍스트</p>
//이렇게

quill에 들어가면서 html코드가 바뀌는 것을 확인했다. 여기저기 확인해보니 quill은 콘텐츠를 나타내기 위한 자체 delta형식을 사용한다는 것을 발견했다.

그래서 delta로 변환하는 것을 이것저것 다 깔았지만.. 모듈이 인식되지 않았다. 알고보니 react-quill에는 속해있지 않은 기능이라고..!

그래서 어떡해야하나 고민하던 중에 에디터 파싱 규칙이 뭔가 다를 수도 있겠다고 생각이 들어서 실험삼아 p태그를 div태그로 바꿔서 적용했더니.! 슈팅 성공 이었다.

file read(css 미완)

파일은 a태그를 이용해 읽어왔는데

<a
  key={index}
  href={file.url[index]}
  target="_blank"  //새창을 열어 파일을 보게함
  rel="noopener noreferrer" //보안 처리
 >

rel은 보안을 위해 사용했다.

noopener는 열린 새탭이 독립적인 프로세스에서 실행되서 원본페이지와의 연결이 끊어진다. 그래서 새탭에서 원본 페이지에 접근할 수 없어 보안에 좋다. 또한 새탭에 오류가 생겨도 원본페이지에는 영향이 없다고..!

noreferrer는 원본페이지의 정보가 새탭에 전달되지 않도록 하는 것이다. 프라이버시 보호차 많이 쓴다고 한다.

참고자료 https://velog.io/@parkseonup/noopener와-noreferrer

댓글기능

댓글 수정

버튼 클릭 시

const startEditComment = (index: number) => {
    setisEdit(true);  //수정상태로 변환
    setEditedCommentIndex(index);   // 해당 게시물의 인덱스 넣기
    setEditComment(comments[index].comment); // 해당 인덱스의 글 넣기
  };

업데이트 로직(editedCommentIndex 이거 안넣고 하는 로직 구상중..)

const updateCommentDetail = () => {
		//isEdit 상태가 true이고 editedCommentIndex가 null이 아닐 때
    if (isEdit && editedCommentIndex !== null) {
      // 수정 중인 댓글을 업데이트
      const updatedComments = comments.map((comment, index) =>
				//현재 선택된 댓글과 인덱스가 같은 경우에만 변경
        index === editedCommentIndex
          ? { ...comment, comment: editComment }
          : comment
      );
			// 변경된 내용 넣기
      setComments(updatedComments);
			//선택된 
      setEditedCommentIndex(null);

      const commentObject = {
        updateData: {
          comment: updatedComments
        },
        paramId
      };
      upsertMutation.mutate(commentObject);
    }
  };

삭제 로직

const deleteComment = (index: number) => {
    if (window.confirm('댓글을 삭제하시겠습니까?')) {
      const updatedComments = comments.filter((_, idx) => idx !== index);

      setComments(updatedComments);

      // 삭제된 댓글을 서버로 업데이트
      const commentObject = {
        updateData: {
          comment: updatedComments
        },
        paramId
      };
      upsertMutation.mutate(commentObject);
    }
  };

filter 인자 안에 _은 명시적으로 요소를 사용하지 않겠다는 의미라고 한다.

⇒ 하지만 알아먹기 힘들어서 그냥 comment로 쓸까.. 고민중

⇒ 하지만 유지보수에 좋다는데..? 아직 모르겟다

react-query 설정은 게시글 수정하는 것과 같은 mutation을 사용한다.

게시글 작성, 수정

유저 프로필 가져와서 setProfile해주기 WritePost

useEffect(() => {
    const fetchData = async () => {
      try {
        const {
          data: { user }
        } = await supabase.auth.getUser(); //auth에서 현재 로그인 중인 유저정보 가져오기
        setUserId(user!.id);

        const { data: profiles, error } = await supabase
          .from('user')
          .select('*')
          .eq('id', user!.id);  //유저의 id와 일치한 row가져와서 추가정보 가져오기

        if (error) {
          console.log(error);
        }

        if (profiles != null) {
          setProfile(profiles);
        }
      } catch (error: any) {
        console.log(error.message);
      }
    };

    fetchData();
  }, []);

WriteLayout으로 props 내려주기 WritePost

<WriteLayout
          profile={profile}
          isEdit={false}
          paramId=""
          setIsEditState={() => {}}
        />

위 코드에서 set해준 profile를 넘겨주고 isEdit은 false, paramId, setIsEditState 는 글 첫 작성과 글 수정 컴포넌트를 통합해서 detail에서 필요한 props이니 공란으로 넘긴다.

동일하게CommuDetail에서도

<WriteLayout
            profile={undefined}
            isEdit={true}
            paramId={param.id}
            setIsEditState={setIsEditState}
          />

props로 내려주면 된다.

WriteLaaout의 props 타입은

export type WriteLayoutProps = {
  profile: ProfileObject[] | undefined;
  isEdit: boolean;
  paramId: string | undefined;
  setIsEditState: (isEditState: boolean) => void;
};

이렇게 정의해주었다.

수정상태 detail에서는 isEditState으로 read 상태인지 Update 상태인지 구분하고 WriteLayout 에서는 isEdit으로 게시글 수정인지 게시글 첫 작성인지 구분한다.

글 작성 로직 설명 WriteLayout.tsx

storage upload & get (files , images)

파일 입력을 감지하는 함수다.

const handleFiles = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const fileList = e.target.files; //선택된 파일 목록
    if (fileList) {
      const filesArray = Array.from(fileList); //배열로 변환
      filesArray.forEach((file) => {  //각 파일을 전달
        handleFilesUpload(file);
      });
    }
  };

file 스토리지 upload & get

const handleFilesUpload = async (file: File) => {
    setFormValues((prevValues: any) => ({  //이전 선택 파일 지우기
      ...prevValues,
      files: [],
      uploadedFileUrl: []
    }));
    try {
      const newFileName = uuid();  //supabase가 한글이름인식 안함
      const { data, error } = await supabase.storage
        .from('files')
        .upload(`files/${newFileName}`, file);
      if (error) {
        console.log('파일 업로드 중 오류가 발생했습니다', error);
        return;
      }
      const res = supabase.storage.from('files').getPublicUrl(data.path);
      setFormValues((prevValues: any) => ({
        ...prevValues,
        files: [...prevValues.files, file], //가져오는 속도가 달라서 
        uploadedFileUrl: [...prevValues.uploadedFileUrl, res.data.publicUrl]
      }));
      console.log(res.data.publicUrl);
    } catch (error) {
      console.error('파일을 업로드하지 못했습니다:', error);
    }
  };

파일url과 파일은 스토리지에 넣고 가져오면서 같이 넣은 두 파일이 같이 오는 게 아니라 순차적으로 들어오기 때문에 원래 파일에 더해주는 식으로 진행했다

이미지 upload & get

const quillRef = useRef<ReactQuill | null>(null);

  const imageHandler = () => {
    try {
      const input = document.createElement('input'); 
			//input 엘리먼트 생성, 업로드 상자를 여는 컨트롤 역할
      input.setAttribute('type', 'file');
			//위 input의 타입 지정
      input.setAttribute('accept', 'image/*');
			//사용자가 선택할 수 있는 파일 종류 제한 (모든 이미지 가능)
      input.click();
      input.addEventListener('change', async () => {
        const file = input.files![0];
        const fileNewName = uuid();

        const { data, error } = await supabase.storage // 업로드
          .from('images')
          .upload(`quill_imgs/${fileNewName}.png`, file);
        if (error) {
          console.error('이미지 업로드 중 오류 발생:', error);
        } else {
          console.log('이미지가 성공적으로 업로드되었습니다:', data);
        }

        const response = supabase.storage  //겟
          .from('images')
          .getPublicUrl(`quill_imgs/${fileNewName}.png`);
        console.log(response);
        if (response.data) {
          const postImageUrl = response.data.publicUrl;
          const editor = quillRef.current?.getEditor(); //에디터 정보 가져오기
          const range = editor?.getSelection(true); //에디터 커서 위치
					//커서 위치에 이미지 넣기
          editor?.insertEmbed(range?.index || 0, 'image', postImageUrl);
          setFormValues((prevValues) => ({
            ...prevValues,
            mainImage: '이미지있음'  
					//아마 변경될 로직.. 중간때는 목록에 사진없이 진행할듯합니다.
          }));
          editor?.setSelection((range?.index || 0) + 1, 0);
					//이미지 다음칸으로 커서이동
					//다음줄로 이동했으면 해서 수정 예정
          console.log('가져왔다');
        } else {
          console.error('No public URL found in response data.');
        }
      });
    } catch (error) {
      console.log('error', error);
    }
  };

커뮤니티에선 wysiwyg 라이브러리인 react-quill을 사용해서 글작성을 하는데 이미지을 삽입하는핸들러가 기본적으로 있지만 src가 base 64문자열로 엄청 길기도 하고 db에 이미지를 바로 넣는 것은 용량문제가 생기기 때문에 이미지 핸들러를 새로 정의를 해줬다.

이미지 리사이징

import { ImageActions } from '@xeger/quill-image-actions';
import { ImageFormats } from '@xeger/quill-image-formats';

외부 모듈 사용

addPost 쿼리 설정

export const addPostMutation = async (insertData: InsertObject) => {
  try {
    const { data, error } = await supabase
      .from('community')
      .insert([insertData]);
    if (error) throw error;
  } catch (error) {
    console.error('Error adding post:', error);
  }
}
const addMutation = useMutation(addPostMutation, {
    onSuccess: (data) => {
      console.log(data);
      queryClient.invalidateQueries('posts');
      navigate('/community');
    }
  });

  const addPost = async () => {
    const insertData = {
      title: formValues.title,
      content: formValues.content,
      category: formValues.category,
      post_user: profile![0].id,
      nickname: profile![0].nickname
        ? profile![0].nickname
        : profile![0].username,
      files: formValues.files.map((file: File) => ({
        name: file.name,
        url: formValues.uploadedFileUrl
      })),
      main_image: formValues.mainImage,
      anon: formValues.anon
    };
    addMutation.mutate(insertData);
  };

아직 유효성 검사를 넣지 않았음, 오늘 내일로 추가예정

게시글 수정/ 업데이트

export const updatePostMutation = async (
  postData: UpdateObject | CommentUpload
) => {
  try {
    const { data, error } = await supabase
      .from('community')
      .update(postData.updateData)
      .eq('post_id', postData.paramId);

    if (error) {
      throw error;
    }
    return data;
  } catch (error) {
    console.error('Error update post:', error);
  }
};
const updatePost = () => {
    const fileArr = formValues.files.map((file: File) => ({
      name: file.name,
      url: formValues.uploadedFileUrl
    }));
    const postData = {
      updateData: {
        title: formValues.title,
        content: formValues.content,
        anon: formValues.anon,
        files: fileArr,
        main_image: formValues.mainImage,
        category: formValues.category
      },
      paramId
    };
    updateMutation.mutate(postData);
  };

업데이트와 추가는 거의 비슷하다. editValue를 따로 두지 않고 작업을 했기때문이다. 초기값을 편집 상태에 따라 달라지게 설정하였다.

file map부분이 같아서 따로 뺐다.

게시글 삭제 CommuDetail.tsx

export const deletePostMutation = async (postId: string | undefined) => {
  const { data, error } = await supabase
    .from('community')
    .delete()
    .eq('post_id', postId);

  if (error) {
    throw error;
  }
  return data;
};
const deleteMutation = useMutation(deletePostMutation, {
    onSuccess: () => {
      queryClient.invalidateQueries('posts');
    }
  });
  const deletePost = async () => {
    if (window.confirm(`정말로"${posts![0].title}" 글을 삭제하시겠습니까?`)) {
      deleteMutation.mutate(param.id);
      navigate('/community');
    }
  };

게시글 Read(트러블 슈팅에 들어갈 내용)

in CommunityList

const handleText = (content: string): string => {
    // 정규 표현식을 사용하여 태그를 제외한 텍스트만 추출
    const textOnly = content.replace(/<[^>]*>|&nbsp;/g, ' ');

    return textOnly;
  };

return (
<ContentArea>{handleText(post.content)}</ContentArea>
)

커뮤니티 목록에서는 콘텐츠의 텍스트만 나와야하는데 react-quill은 콘텐츠를 html 문자열로 주니가 파싱을 하고 조건부로 노출되게 해야하는데 문자열인점을 이용해 정규식으로 태그와  를 없앴다. 가끔 붙여넣기를 한 글에서 띄어쓰기가 된 부분이  로 그대로 나오는 경우가 있어서 추가해주었다.

in CommuDetail

import parse from 'html-react-parser';

return(
<Content>{parse(post.content)}</Content>
)

html-react-parser 라이브러리를 이용해서 html코드를 파싱해줬다.

in WriteLayout (isEdit)

content: isEdit
      ? posts![0].content.replace(/<p>(.*?)<\\/p>/g, ' <div>$1</div>')
      : '',

초기값에 정규식을 사용해서 <p></p>를 <div></div>로 변경해주었다.

그이유는 수정을 하기위해서 콘텐츠의 html문자열을 그대로 넣게되면

이랬던 글이

이렇게 되는 것이다.

개발자 도구에서 확인해보니

<p><img src=""/></p><p>텍스트</p>
//이게 
<p><img src=""/>텍스트</p>
//이렇게

quill에 들어가면서 html코드가 바뀌는 것을 확인했다. 여기저기 확인해보니 quill은 콘텐츠를 나타내기 위한 자체 delta형식을 사용한다는 것을 발견했다.

그래서 delta로 변환하는 것을 이것저것 다 깔았지만.. 모듈이 인식되지 않았다. 알고보니 react-quill에는 속해있지 않은 기능이라고..!

그래서 어떡해야하나 고민하던 중에 에디터 파싱 규칙이 뭔가 다를 수도 있겠다고 생각이 들어서 실험삼아 p태그를 div태그로 바꿔서 적용했더니.! 슈팅 성공 이었다.

file read(css 미완)

파일은 a태그를 이용해 읽어왔는데

<a
  key={index}
  href={file.url[index]}
  target="_blank"  //새창을 열어 파일을 보게함
  rel="noopener noreferrer" //보안 처리
 >

rel은 보안을 위해 사용했다.

noopener는 열린 새탭이 독립적인 프로세스에서 실행되서 원본페이지와의 연결이 끊어진다. 그래서 새탭에서 원본 페이지에 접근할 수 없어 보안에 좋다. 또한 새탭에 오류가 생겨도 원본페이지에는 영향이 없다고..!

noreferrer는 원본페이지의 정보가 새탭에 전달되지 않도록 하는 것이다. 프라이버시 보호차 많이 쓴다고 한다.

참고자료 https://velog.io/@parkseonup/noopener와-noreferrer

댓글기능

댓글 수정

버튼 클릭 시

const startEditComment = (index: number) => {
    setisEdit(true);  //수정상태로 변환
    setEditedCommentIndex(index);   // 해당 게시물의 인덱스 넣기
    setEditComment(comments[index].comment); // 해당 인덱스의 글 넣기
  };

업데이트 로직(editedCommentIndex 이거 안넣고 하는 로직 구상중..)

const updateCommentDetail = () => {
		//isEdit 상태가 true이고 editedCommentIndex가 null이 아닐 때
    if (isEdit && editedCommentIndex !== null) {
      // 수정 중인 댓글을 업데이트
      const updatedComments = comments.map((comment, index) =>
				//현재 선택된 댓글과 인덱스가 같은 경우에만 변경
        index === editedCommentIndex
          ? { ...comment, comment: editComment }
          : comment
      );
			// 변경된 내용 넣기
      setComments(updatedComments);
			//선택된 
      setEditedCommentIndex(null);

      const commentObject = {
        updateData: {
          comment: updatedComments
        },
        paramId
      };
      upsertMutation.mutate(commentObject);
    }
  };

삭제 로직

const deleteComment = (index: number) => {
    if (window.confirm('댓글을 삭제하시겠습니까?')) {
      const updatedComments = comments.filter((_, idx) => idx !== index);

      setComments(updatedComments);

      // 삭제된 댓글을 서버로 업데이트
      const commentObject = {
        updateData: {
          comment: updatedComments
        },
        paramId
      };
      upsertMutation.mutate(commentObject);
    }
  };

filter 인자 안에 _은 명시적으로 요소를 사용하지 않겠다는 의미라고 한다.

⇒ 하지만 알아먹기 힘들어서 그냥 comment로 쓸까.. 고민중

⇒ 하지만 유지보수에 좋다는데..? 아직 모르겟다

react-query 설정은 게시글 수정하는 것과 같은 mutation을 사용한다.