수정하기 버튼을 누르면,
이렇게 뜨면되잖아?
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('실패');
}
})