Resolved
Written on
·
499
0
로그인후
좋아요 버튼이 1회만 작동하고 상태값이 바뀌지 않습니다.
에러 메세지에서는
TypeError: Cannot read property 'Likers' of undefined 라고 나오는데..
PostCard.js
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Card, Popover, Button, Avatar, List, Comment } from 'antd';
import { RetweetOutlined, HeartOutlined, HeartTwoTone, MessageOutlined, EllipsisOutlined } from '@ant-design/icons';
import { useSelector, useDispatch } from 'react-redux';
import PostImages from './PostImages';
import CommentForm from './CommentForm';
import PostCardContent from './PostCardContent';
import { REMOVE_POST_REQUEST, LIKE_POST_REQUEST, UNLIKE_POST_REQUEST } from '../reducers/post';
import FollowButton from './FollowButton';
const PostCard = ({ post }) => {
// state.user.me && state.user.me.id => state.user.me?.id
const dispatch = useDispatch();
const { removePostLoding } = useSelector((state) => state.post);
// const [liked, setLiked] = useState(false);
const [commentFormOpened, setCommentFormOpened] = useState(false);
const onLike = useCallback(() => {
if (!id) {
return alert('로그인이 필요합니다.');
}
return dispatch({
type: LIKE_POST_REQUEST,
data: post.id,
})
}, [id]);
const onUnLike = useCallback(() => {
if (!id) {
return alert('로그인이 필요합니다.');
}
return dispatch({
type: UNLIKE_POST_REQUEST,
data: post.id,
})
}, [id]);
const onToggleComment = useCallback(() => {
setCommentFormOpened((prev) => !prev);
}, []);
const onRemovePost = useCallback(() => {
dispatch({
type: REMOVE_POST_REQUEST,
data: post.id,
});
}, [])
const id = useSelector((state) => state.user.me?.id);
const liked = post.Likers.find((v) => v.id === id);
return (
<div style={{ marginBottom: 20 }}>
<Card
cover={post.Images[0] && <PostImages images={post.Images} />}
actions={[
<RetweetOutlined key="retweet" />,
liked
? <HeartTwoTone twoToneColor="#eb2f96" key="heart" onClick={onUnLike} />
: <HeartOutlined key="heart" onClick={onLike} />,
<MessageOutlined key="comment" onClick={onToggleComment} />,
<Popover key="more" content={(
<Button.Group>
{id && post.User.id === id
? (
<>
<Button>수정</Button>
<Button type="danger" loading={removePostLoding} onClick={onRemovePost}>삭제</Button>
</>
) : <Button>신고</Button>}
</Button.Group>
)}>
<EllipsisOutlined />
</Popover>,
]}
extra={ id && <FollowButton post={post} />}
>
<Card.Meta
avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
title={post.User.nickname}
description={<PostCardContent postData={post.content} />}
/>
</Card>
{commentFormOpened && (
<div>
<CommentForm post={post} />
<List
header={`${post.Comments.length}개의 댓글`}
itemLayout="horizontal"
dataSource={post.Comments}
renderItem={(item) => (
<li>
<Comment
author={item.User.nickname}
avatar={<Avatar>{item.User.nickname[0]}</Avatar>}
content={item.content}
/>
</li>
)}
/>
</div>
)}
</div>
);
};
PostCard.propTypes = {
post: PropTypes.shape({
id: PropTypes.number,
User: PropTypes.object,
content: PropTypes.string,
createdAt:PropTypes.string,
Comments: PropTypes.arrayOf(PropTypes.object),
Images: PropTypes.arrayOf(PropTypes.object),
Likers: PropTypes.arrayOf(PropTypes.object),
}).isRequired,
};
export default PostCard;
reducers/post.js
import produce from 'immer';
import {
REMOVE_POST_OF_ME
} from './user';
export const initialState = {
mainPosts: [],
imagePaths: [], // 이미지를 업로드 할때 이미지 경로
postAdded: false,
hasMorePosts: true,
likePostLoading: false,
likePostDone: false, // 추가
likePostError: null, // 추가
unlikePostLoading: false,
unlikePostDone: false, // 추가
unlikePostError: null, // 추가
loadPostsLoading: false,
loadPostsDone: false, // 추가
loadPostsError: null, // 추가
// 게시글 추가가 완료되었을때 true 로 변한다. postAdded: false, => addPostLoading: false,
addPostLoading: false,
addPostDone: false, // 추가
addPostError: null, // 추가
addCommentLoading: false, // 댓글 추가가 완료되었을때 true 로 변한다.
addCommentDone: false, // 추가
addCommentError: null, // 추가
removePostLoading: false,
removePostDone: false, // 추가
removePostError: null, // 추가
};
export const LOAD_POSTS_REQUEST = 'LOAD_POSTS_REQUEST';
export const LOAD_POSTS_SUCCESS = 'LOAD_POSTS_SUCCESS';
export const LOAD_POSTS_FAILURE = 'LOAD_POSTS_FAILURE';
export const LIKE_POST_REQUEST = 'LIKE_POST_REQUEST';
export const LIKE_POST_SUCCESS = 'LIKE_POST_SUCCESS';
export const LIKE_POST_FAILURE = 'LIKE_POST_FAILURE';
export const UNLIKE_POST_REQUEST = 'UNLIKE_POST_REQUEST';
export const UNLIKE_POST_SUCCESS = 'UNLIKE_POST_SUCCESS';
export const UNLIKE_POST_FAILURE = 'UNLIKE_POST_FAILURE';
export const ADD_POST_REQUEST = 'ADD_POST_REQUEST';
export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS';
export const ADD_POST_FAILURE = 'ADD_POST_FAILURE';
export const REMOVE_POST_REQUEST = 'REMOVE_POST_REQUEST';
export const REMOVE_POST_SUCCESS = 'REMOVE_POST_SUCCESS';
export const REMOVE_POST_FAILURE = 'REMOVE_POST_FAILURE';
export const ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST';
export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS';
export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE';
export const addComment = (data) => ({
type: ADD_COMMENT_REQUEST,
data,
});
export const addPost = (data) => ({
type: ADD_POST_REQUEST,
data,
});
export const removePost = (data) => ({
type: REMOVE_POST_OF_ME,
data,
});
const reducer = (state = initialState, action) => produce(state, (draft) => {
switch (action.type) {
case LIKE_POST_REQUEST:
draft.likePostLoading = true;
draft.likePostDone = false;
draft.likePostError = null;
break;
case LIKE_POST_SUCCESS: {
const post = draft.mainPosts.find((v) => v.id === action.data.PostId);
post.Likers.push({ id: action.data.UserId });
draft.likePostLoading = false;
draft.likePostDone = true;
break;
}
case LIKE_POST_FAILURE:
draft.likePostLoading = false;
draft.likePostError = action.error;
break;
case UNLIKE_POST_REQUEST:
draft.unlikePostLoading = true;
draft.unlikePostDone = false;
draft.unlikePostError = null;
break;
case UNLIKE_POST_SUCCESS: {
const post = draft.mainPosts.find((v) => v.id === action.data.PostId);
post.Likers = post.Likers.filter((v) => v.id !== action.data.UserId); // 제거
draft.unlikePostLoading = false;
draft.unlikePostDone = true;
break;
}
case UNLIKE_POST_FAILURE:
draft.unlikePostLoading = false;
draft.unlikePostError = action.error;
break;
case LOAD_POSTS_REQUEST:
draft.loadPostsLoading = true;
draft.loadPostsDone = false;
draft.loadPostsError = null;
break;
case LOAD_POSTS_SUCCESS:
draft.loadPostsLoading = false;
draft.loadPostsDone = true;
draft.mainPosts = action.data.concat(draft.mainPosts);
draft.hasMorePosts = draft.mainPosts.length < 50;
break;
case LOAD_POSTS_FAILURE:
draft.loadPostsLoading = false;
draft.loadPostsError = action.error;
break;
case ADD_POST_REQUEST:
draft.addPostLoading = true;
draft.addPostDone = false;
draft.addPostError = null;
break;
case ADD_POST_SUCCESS:
draft.addPostLoading = false;
draft.addPostDone = true;
draft.mainPosts.unshift(action.data);
break;
case ADD_POST_FAILURE:
draft.addPostLoading = true;
draft.addPostError = action.error;
break;
case ADD_COMMENT_REQUEST:
draft.addCommentLoading = true;
draft.addCommentDone = false;
draft.addCommentError = null;
break;
case ADD_COMMENT_SUCCESS:
const post = draft.mainPosts.find((v) => v.id === action.data.PostId);
post.Comments.unshift(action.data);
draft.addCommentLoading = false;
draft.addCommentDone = true;
break;
case ADD_COMMENT_FAILURE:
draft.addCommentLoading = false;
draft.addCommentError = action.error;
break;
case REMOVE_POST_REQUEST:
draft.removePostLoading = true;
draft.removePostDone = false;
draft.removePostError = null;
break;
case REMOVE_POST_SUCCESS:
draft.removePostLoading = false;
draft.removePostDone = true;
draft.mainPosts = draft.mainPosts.filter((v) => v.id !== action.data);
break;
case REMOVE_POST_FAILURE:
draft.removePostLoading = false;
draft.removePostError = action.error;
break;
default:
break;
}
});
export default reducer;
sagas/post.js
import { all, fork, put, takeLatest, delay, throttle, call } from 'redux-saga/effects';
import axios from 'axios';
import {
LOAD_POSTS_REQUEST,
LOAD_POSTS_SUCCESS,
LOAD_POSTS_FAILURE,
LIKE_POST_REQUEST,
LIKE_POST_SUCCESS,
LIKE_POST_FAILURE,
UNLIKE_POST_REQUEST,
UNLIKE_POST_SUCCESS,
UNLIKE_POST_FAILURE,
ADD_POST_REQUEST,
ADD_POST_SUCCESS,
ADD_POST_FAILURE,
ADD_COMMENT_REQUEST,
ADD_COMMENT_SUCCESS,
ADD_COMMENT_FAILURE,
REMOVE_POST_REQUEST,
REMOVE_POST_SUCCESS,
REMOVE_POST_FAILURE,
} from '../reducers/post';
import {
ADD_POST_TO_ME,
REMOVE_POST_OF_ME,
} from '../reducers/user';
function likePostAPI(data) {
return axios.patch(`/post/${data}/like`);
};
function* likePost(action) {
try {
const result = yield call(likePostAPI, action.data);
yield put({
type: LIKE_POST_SUCCESS,
data: result.data,
});
} catch (err) {
console.error(err);
yield put({ // put => dispatch 다.
type: LIKE_POST_FAILURE,
data: err.response.data,
});
}
}
function unlikePostAPI(data) {
return axios.delete(`/post/${data}/like`);
};
function* unlikePost(action) {
try {
const result = yield call(unlikePostAPI, action.data);
yield put({
type: UNLIKE_POST_SUCCESS,
data: result.data,
});
} catch (err) {
console.error(err);
yield put({ // put => dispatch 다.
type: UNLIKE_POST_FAILURE,
data: err.response.data,
});
}
}
function loadPostsAPI(data) {
return axios.get('/posts', data);
};
function* loadPosts(action) {
try {
const result = yield call(loadPostsAPI, action.data);
yield put({
type: LOAD_POSTS_SUCCESS,
data: result.data,
});
} catch (err) {
console.error(err);
yield put({ // put => dispatch 다.
type: LOAD_POSTS_FAILURE,
data: err.response.data,
});
}
}
function addCommentAPI(data) {
return axios.post(`/post/${data.postId}/comment`, data); // POST /post/1/comment
};
function* addComment(action) {
try {
const result = yield call(addCommentAPI, action.data);
// yield delay(1000);
yield put({
type: ADD_COMMENT_SUCCESS,
data: result.data,
});
} catch (err) {
console.error(err);
yield put({ // put => dispatch 다.
type: ADD_COMMENT_FAILURE,
data: err.response.data,
});
};
};
function addPostAPI(data) {
return axios.post('/post', { content: data });
};
function* addPost(action) {
try {
const result = yield call(addPostAPI, action.data);
yield put({
type: ADD_POST_SUCCESS,
// data: action.data,
data: result.data
});
yield put({
type: ADD_POST_TO_ME,
data: result.data.id,
});
} catch (err) {
console.error(err);
yield put({ // put => dispatch 다.
type: ADD_POST_FAILURE,
data: err.response.data,
});
};
};
function removePostAPI(data) {
return axios.delete(`/api/post/${data.postId}/comment`, data);
};
function* removePost(action) {
try {
// yield put({
// type: 'REMOVE_POST_REQUEST',
// });
// const result = yield call(removePostAPI);
yield delay(1000);
yield put({
type: REMOVE_POST_SUCCESS,
data: action.data,
});
console.log('removePost');
yield put({
type: REMOVE_POST_OF_ME,
data: action.data,
});
} catch (err) {
console.error(err);
yield put({ // put => dispatch 다.
type: REMOVE_POST_FAILURE,
data: err.response.data,
});
};
};
function* watchLoadPosts() {
// 5초에 한번 게사글이 로드 된다.
yield throttle(5000, LOAD_POSTS_REQUEST, loadPosts);
};
function* watchAddPost() {
yield takeLatest(ADD_POST_REQUEST, addPost);
};
function* watchAddComment() {
yield takeLatest(ADD_COMMENT_REQUEST, addComment);
};
function* watchRemovePost() {
yield takeLatest(REMOVE_POST_REQUEST, removePost);
};
function* watchLikePost() {
yield takeLatest(LIKE_POST_REQUEST, likePost);
};
function* watchUnLikePost() {
yield takeLatest(UNLIKE_POST_REQUEST, unlikePost);
};
export default function* postSaga() {
yield all([
fork(watchLikePost),
fork(watchUnLikePost),
fork(watchAddPost),
fork(watchLoadPosts),
fork(watchRemovePost),
fork(watchAddComment),
]);
};
Answer 4
0
0
0
로그에는 정상적으로 찍혀서 나옵니다.
첫번째 클릭은 정상적으로 DB 에 입력이 되지만 브라우저에 리덕스 데브툴즈에 Likers 값이 바뀌지 않습니다.
0
const post = draft.mainPosts.find((v) => v.id === action.data.PostId);
post.Likers = post.Likers.filter((v) => v.id !== action.data.UserId); // 제거
에서 post가 undefined일 것 같은 느낌이 드는데요.
saga의 post.js에서 put({ type: UNLIKE_POST_SUCCESS 할 때 넣어주는 data에 PostId가 제대로 들어있나요?
backend/routes/post.js에서 unlike하는 라우터 res.json부분 확인해보셔야 할 것 같습니다.