• 카테고리

    질문 & 답변
  • 세부 분야

    풀스택

  • 해결 여부

    해결됨

좋아요 토글 버튼 구현시 TypeError: Cannot read property 'Likers' of undefined 에러 발생 문의 드립니다.

20.08.05 17:25 작성 조회수 417

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),
    ]);
};

답변 4

·

답변을 작성해보세요.

0

감사합니다.

오타 찾기가 쉽지않네요.

새로운걸 만드는것도 아닌데..

0

postId에서 p 소문자로 오타내셨네요. PostId입니다.

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부분 확인해보셔야 할 것 같습니다.