[ react ] 게시글 수정하기 (이미지포함)

Suji Kang·2023년 11월 27일
1
post-custom-banner

수정하기는 어떻게 하지 ❓ 생각해보면,

수정하기 버튼을 누르면,


이렇게 뜨면되잖아?

🔎 게시글 만들기 페이지를 사용해보자

activityWrite.js

import ActivityCU from "../../components/activity/activityCU";
import DashboardLayout from "../../components/common/layout";
import { useAuth } from "../../components/hooks/hooks";


const ActivityWritePage = () => {
  useAuth();
  return (
    <DashboardLayout target="활동게시판">
      <ActivityCU isUpdate={false} />
    </DashboardLayout>
  );
}

export default ActivityWritePage;

activityWrite에서 props를 받아온다

activityCU.js

const ActivityCU = (props) => {
    // 작성 페이지에서 사용되면 props.isEdit에 false가 들어있음
    // 수정 페이지에서 사용되면 props.isEdit에 true가 들어있음
     return (
        <section>
            <h1>{props.isEdit ? `${props.activityId}번 게시글 수정하기` : '새 게시글 작성하기'}</h1>
         );
      }

activityUpdate.js

const ActivityUpdatePage = () => {
 useAuth();
 const { accessToken } = useContext(UserContext);
 const [activity, setActivity] = useState(null);
 
  //게시글 조회하기 
 useEffect(() => {
        const tmp = async () => {
            if (accessToken === null) return;
            try {
                let res = await axios.get(`/api/activities/${params.id}`, { //🌟(몇번?)게시물 가져와줘 라고 하는것
                    headers: { Authorization: `Bearer ${accessToken}` }
                });
                if (res.data.owner === false) {
                    alert('잘못된 접근입니다~! 사유:작성자가 아님');
                    navigate('/activity', { replace: true });
                    return;
                }
                setActivity(res.data); //🌟그결과는 activity에 설정

            } catch (err) {
                console.log(err);
                alert('오류가 발생했어요');
            }
        }

        tmp();
    }, [accessToken]);
  
  
    return (
        <DashboardLayout target="활동게시판">
            {activity ? <ActivityCU activity={activity} isEdit={true} activityId={params.id} />
                : <h1>로딩중</h1>}
        </DashboardLayout>
    );

activityCU한테 activity를 넘겨준다.


console.log(props.activity)

이 객체를 activityCU한테 준것임.


activityCU.js

 const [formInfo, setFormInfo] = useState({
        values: {
            title: props.isEdit ? props.activity?.title : '',
            content: props.isEdit ? props.activity?.content : '',
        },
        errors: {
        title: props.isEdit ? '' : '제목은 필수 입력 값입니다.',
        content: props.isEdit ? '' : '내용은 필수 입력 값입니다.'
        },
        touched: { title: false, content: false },
    });

activityCU에 있는것을 -> activityWritePage 로 옮겨준다

activityCU.js

return(
 <ActivityInputWrap>
         <label htmlFor="writerEmail">작성자</label>
         <input disabled value={props.isEdit? props.activity?.writer_email : props.userEmail} id="writerEmail" />
 </ActivityInputWrap>

<button>{props.isEdit ? '수정' : '작성'}하기</button>
  );

그다음은, null 이 아니고 새로운 id : 23인 activity새로운 객체가 생긴다.
화면을 다시 그리는데 , 문제는 리랜더링 될때 useState변수도 또 실행이 되나? ->아니. 안된다.
리렌더더링 되기 이전에것을 기억하고있어야한다.
그래서 title,content의 undefiend를 기억하고있기 때문에 문제가 생긴다.

문제를 해결하기위해서, 삼항연산자를 사용해서

  return (
        <DashboardLayout target="활동게시판">
            {activity ? <ActivityCU activity={activity} isEdit={true} activityId={params.id} />
                : <h1>로딩중</h1>}
        </DashboardLayout>
    );

(35-38)


업로드된 이미지 가져오기 ❗️

이미지 url 을 가지고 와서
이모양의 객체를 만들어줄거다.

    const [imgList, setImgList] = useState(props.isEdit ? 
        props.activity.img_url.map((el)=>{return {id:el, previewUrl:el, origin:null}})
        : []);

업로드가 되어있는 이미지 한개를

🔎 경우의수가 이렇게나 많다.. 차근차근 살펴보자 ❗️

📌 1. 기존 이미 업로드 완료된 이미지를 삭제하는경우 --> images폴더에서 파일을 찾아 삭제
📌 2. 기존 이미 업로드 완료된 이미지를 aaa이미지로 변경 --> images폴더에서 파일을 삭제, 새로운 aaa를 업로드
📌 3. 이미지 건드리지않음 --> 이미지 할거 없음
📌 4. 기존 이미 업로드 완료된 이미지를 그대로 냅두고, 새로운 이미지를 추가하는 경우 --> 새로운 이미지만 새롭게 업로드

삭제버튼이 클릭되면 실행될코드

const [deleteImg, setDeleteImg] = useState([]); //삭제할 이미지들
const onImgDelete = (id) => {
        setImgList(imgList.filter((e) => e.id !== id));
        setDeleteImg([...deleteImg, id]);  //원래 기존의 삭제할 이미지들에다가 새로운 id를 추가해준다.
    }

그런데 문제는 ❗️

새롭게 올린 id인지 , 업로드 이미 되어있는 id인지를 알수가 없다. (그냥 id로만은.)
그래서 객체(img를) 그자체를 받아와야한다.

const [deleteImg, setDeleteImg] = useState([]); //삭제할 이미지들
const onImgDelete = (img) => {
        setImgList(imgList.filter((e) => e.id !== img.id)); //객체로 받은 img안에 있는 id
        if (img.originFile === null) //실제로 들어간 이미지 
            setDeleteImg([...deleteImg, img.id]);  //원래 기존의 삭제할 이미지들에다가 새로운 id를 추가해준다 .
    }  
   // console.log(deleteImg)
 return(
     <button
        style={{ position: 'absolute', top: '0', right: '0' }}
        onClick={() => { onImgDelete(img) }}
        type='button'>삭제하기
     </button>
    );

console.log(deleteImg)
onImgDelete를 클릭하면, 업로드가 되있었던 이미지를 삭제했을때는 console.log에 찍히는것을 볼수있다.
그런데 아직 업로드가 안됬던 이미지는❓ 삭제를 눌러도 deleteImg에 들어가지 않는다. 👉 그래서 업로드가 안된 이미지는 삭제를 해도 의미가 없다. 어차피 deleteImg에 없으니깐.


originFile이 있는지 없는지 에 따라 구분을 하고있기 때문이다.


setDeleteImg([...deleteImg, img.id]);
원래업로드되어있던 이미지를 새로운 이미지로 바꿈.
근데 아직 서버에는 올라가지 않아서, originFile이 남아있다.
그런데, 이상태에서 또 맘에들지 않아 이미지를 변경했다.
그러면? 이미지가 또 추가가 된다. 같은게 (전혀 변경이 되지 않았다는뜻)

그래서, 이렇게 하면 (!deleteImg.includes(img.id) && img.originFile === null)

 const onImgChanged = (img, e) => { //🌟img를 받아온다. 
        //이미 이미지가 업로드 되었던것을 다시 선택해서 실행될때는
        //기존의 배열에서 기존의 선택된 이미지를 지우고 
        //기존의 배열에서 id 가  동일한 요소 찾기
        //기존 imgList 라는 state변수(배열)은 변하면 안되기 때문에 똑같은 요소를 갖고있는 복제본 생성
        // let cpy = [...imgList];  //배열 복제 //새로운배열 생성. 객체가 들어있다. 
        //여기는 문제가 생긴다. int, string 타입이 아닌 객체가 들어있기 때문에 사용할수없음. 하지만 int, string 타입이라면 사용가능.  
        if (props.isEdit) {  //🌟 edit 일때만 delete실행
            if (!deleteImg.includes(img.id) && img.originFile === null) { // deleteImg에 img.id가 없고, img.originFile이 null이라면
                setDeleteImg([...deleteImg, img.id]); //원래 요소에다가 삭제할 이미지 배열에 추가해준다.
            }
        }
     }
 return(   
	<input
   	 onChange={(e) => onImgChanged(img.id, e)} //img.id를 넘겨줘야함
   	 accept="image/*"
     type="file" />
	)

이제 삭제하고 추가해도 마지막 추가한것만 남는다. 👍 (20-23)


  const onImgDelete = (img) => {
        setImgList(imgList.filter((e) => e.id !== img.id));
        if (img.originFile === null && props.isEdit) 
          //실제로 들어간 이미지  &&  edit일때
            setDeleteImg([...deleteImg, img.id]);
    }

 🌟 // fd 안에 images 에는 이미지 파일들이 들어있음
      if(props.isEdit){ //수정사항이라면?
        // deleteImg.forEach((imgUrl)=> fd.append('deleteImg', imgUrl));
        fd.append('deleteImg', JSON.stringify(deleteImg)); //🌟문자열로 바꿔서준다 
        // fd.append('deleteImg' , deleteImg);
        fd.append('id', props.activityId )

        try{
          let res = await axios.put('/api/activities', fd, {headers:{Authorization:`Bearer ${accessToken}`}});
          alert('수정 완료!');
          navigate(`/activity/${res.data.id}`, {replace:true});
        }catch(err){
          console.log(err);
          alert('오류발생');
        }
    🌟  }else{ // 수정사항이 아니라면?
        try{
          let res = await axios.post('/api/activities', fd , {headers:{Authorization : `Bearer ${accessToken}`}});
          //res.data.id // 방금 추가된 게시글의 id가 들어잇다
          console.log(res.data.id);
          alert('추가완료!');
          navigate(`/activity/${res.data.id}`, {replace:true});
  
        }catch(err){
          console.log(err);
          alert('err');
        }
      }  
    }

12-35


13-20

 fd.append('deleteImg' , deleteImg);

이미지가 한개만 있을때는 문제가 생길수있다.

그래서, 문자열로 바꿔서준다.

 if (props.isEdit) {
                formData.append('deleteImg', JSON.stringify(deleteImg));

express에서는 문자열을 배열로 바꿔준다.

 const deleteImg = JSON.parse(req.body.deleteImg);
  try {
    for (let i = 0; i < deleteImg.length; i++) {
      console.log(deleteImg[i]);
      await pool.query(sql, [deleteImg[i]]); // 테이블에서 이미지 삭제
      fs.unlinkSync(`../public${deleteImg[i]}`);// 폴더에서 이미지 삭제
    }

근데 여기서, 또 ❗️sql 은 삭제가 되었지만, 폴더에는 아직 남아있다😭

📝 라이브러리를 하나 사용해야한다.

fs 👉 이미설치가 되어있어서 따로 설치할것은 없다.

const fs = require('fs')
fs.unlinkSync('파일경로');

이렇게하면 sql에서도 삭제가되고, 폴더에서도 삭제된것을 확인할수있다.

아직도.. 이게 끝은 아니다 😭


수정한거는 새로운 이미지로 바뀌어야한다.
새로 추가한 이미지도 추가해주어야한다.

끝도없이 나오는 경우의수.. 😇 😲 😆
사진을수정 한후 새롭게 추가시킬 (기존꺼이미 업로드한거삭제후 새로운걸로 추가)
app.js

 sql = `
        insert into tbl_activity_img
        values(?, ?)
      `;
        for (let i = 0; i < req.files.length; i++) {
            await pool.query(sql, [Number(req.body.id), `/images/${req.files[i].filename}`]);
            //req.body.id --> 수정할 게시물의 id 
            //req.files --> 수정할 이미지 파일들
            //반복문을 써서 req.files에 있는 이미지들을 mysql에 추가
        }
        res.json({ id: Number(req.body.id) });//6번이 수정되면 수정된 6번을 리액트로 보내줌
        }

activityCU.js

try {
    let res = await axios.put('/api/activities', formData, {
        headers: { Authorization: `Bearer ${accessToken}` },
    });
    alert('수정 완료');
    navigate(`/activity/${res.data.id}`, { replace: true });
    //수정후 수정된 id 게시글로 이동
} catch (err) {
    console.log(err);
    alert('수정 실패');
}

이렇게 하면 수정 삭제가 완료❗️❗️


 const handleSubmit = async (e)=>{
    e.preventDefault();
    let cpy = JSON.parse(JSON.stringify(formInfo));
    cpy.touched.title = true;
    cpy.touched.content = true;
    setFormInfo(cpy);

    // 모든 제목하고, 내용이 정상적으로 입력 되었다면 ??
    // 제목 에러메시지가 '' 이고 내용 에러메시지가 '' 라면 정상적으로 모든 값이입력됨
    if(formInfo.errors.title === '' && formInfo.errors.content === ''){
      // alert('정상적인 값 입력됨 서버로 전송!');
      const fd = new FormData();
      imgList.forEach((img)=>{
        if(img.origin!== null){
          fd.append('images', img.origin)
        }
      });
      fd.append('title', formInfo.values.title);
      fd.append('content', formInfo.values.content);
     🌟 // fd 안에 images 에는 이미지 파일들이 들어있음
      if(props.isEdit){
        // deleteImg.forEach((imgUrl)=> fd.append('deleteImg', imgUrl));
        fd.append('deleteImg', JSON.stringify(deleteImg)); //🌟문자열로 바꿔서준다 
        // fd.append('deleteImg' , deleteImg);
        fd.append('id', props.activityId )

        try{
          let res = await axios.put('/api/activities', fd, {headers:{Authorization:`Bearer ${accessToken}`}});
          alert('수정 완료!');
          navigate(`/activity/${res.data.id}`, {replace:true});
        }catch(err){
          console.log(err);
          alert('오류발생');
        }
    🌟  }else{
        try{
          let res = await axios.post('/api/activities', fd , {headers:{Authorization : `Bearer ${accessToken}`}});
          //res.data.id // 방금 추가된 게시글의 id가 들어잇다
          console.log(res.data.id);
          alert('추가완료!');
          navigate(`/activity/${res.data.id}`, {replace:true});
  
        }catch(err){
          console.log(err);
          alert('err');
        }
      }  
    }
  }

app.js

app.put('/api/activities', upload.array('images'), async (req, res) => {
  // console.log(req.files)
  // console.log(req.body);
  // console.log(req.headers.authorization);
  let sql = `
    delete from tbl_activity_img
    where img_url = ?;
  `
  console.log(req.body.deleteImg);
  console.log(req.body);
  const deleteImg = JSON.parse(req.body.deleteImg);
  try {
    for (let i = 0; i < deleteImg.length; i++) {
      console.log(deleteImg[i]);
      await pool.query(sql, [deleteImg[i]]); // 테이블에서 이미지 삭제
      fs.unlinkSync(`../public${deleteImg[i]}`);// 폴더에서 이미지 삭제
    }

    sql = `
      update tbl_activities
      set title=?, content=?, updated_date=now()
      where id = ?
    `;
    await pool.query(sql, [req.body.title, req.body.content, req.body.id]);

    sql = `
      insert into tbl_activity_img
      values(?, ?)
    `;
    for (let i = 0; i < req.files.length; i++) {
      await pool.query(sql, [Number(req.body.id), `/images/${req.files[i].filename}`]);
    }

    res.json({ id: Number(req.body.id) });

  } catch (err) {
    console.log(err);
    res.status(500).json('실패');
  }
})
profile
나를위한 노트필기 📒🔎📝
post-custom-banner

0개의 댓글