이미지 첨부하고 글 작성시 500에러나는 현상
미해결
[리뉴얼] React로 NodeBird SNS 만들기
에러메세지 /post/images /post SequelizeValidationError: notNull Violation: Image.content cannot be null at InstanceValidator._validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:50:13) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async InstanceValidator._validateAndRunHooks (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:60:7) at async InstanceValidator.validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:54:12) at async model.save (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:2368:7) at async Function.create (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:1344:12) at async Promise.all (index 0) at async C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\routes\post.js:65:24 /post 에러메세지 (post.js를 고쳐봐도 해결이 되지 않습니다) (읽기 쉽게 주석을 지우고 올려서 post.js:65번 줄이 아니라 if(req.body.image)부분 봐주시면 될 것 같습니다.) const express = require("express"); // const multer = require("multer"); const path = require("path"); // const fs = require("fs"); const { Post, Image, Comment, User, Hashtag } = require("../models"); const { isLoggedIn } = require("./middlewares"); const router = express.Router(); try { fs.accessSync("uploads"); } catch (error) { console.log("uploads 폴더가 없으므로 생성합니다."); fs.mkdirSync("uploads"); } const upload = multer({ storage: multer.diskStorage({ destination(req, file, done) { done(null, "uploads"); }, filename(req, file, done) { const ext = path.extname(file.originalname); // 확장자 추출(.png) const basename = path.basename(file.originalname, ext); // 사용자 done(null, basename + "_" + new Date().getTime() + ext); // 사용자14512512.png }, }), limits: { fileSize: 20 * 1024 * 1024 }, // 20MB }); router.post("/", isLoggedIn, upload.none(), async (req, res, next) => { // POST / post try { const hashtags = req.body.content.match(/#[^\s#]+/g); const post = await Post.create({ content: req.body.content, UserId: req.user.id, }); if (hashtags) { const result = await Promise.all( hashtags.map((tag) => Hashtag.findOrCreate({ where: { name: tag.slice(1).toLowerCase() }, }) ) ); await post.addHashtags(result.map((v) => v[0])); } if (req.body.image) { if (Array.isArray(req.body.image)) { const images = await Promise.all(req.body.image.map((image) => Image.create({ src: image }))); console.log(images); await post.addImages(images); } else { const image = await Image.create({ src: req.body.image }); await post.addImages(image); } } const fullPost = await Post.findOne({ where: { id: post.id }, include: [ { model: Image, }, { model: Comment, include: [ { model: User, // attributes: ["id", "nickname"], }, ], }, { model: User, // 게시글 작성자 attributes: ["id", "nickname"], }, { model: User, // 좋아요 누른사람 // 위랑 구분하기 위해 as Likers가져옴 as: "Likers", attributes: ["id"], }, ], }); res.status(201).json(fullPost); } catch (error) { console.error(error); next(error); } }); router.post("/images", isLoggedIn, upload.array("image"), (req, res, next) => { // POST /post/images console.log(req.files); res.json(req.files.map((v) => v.filename)); }); router.post("/:postId/retweet", isLoggedIn, async (req, res, next) => { // POST /post/1/retweet try { const post = await Post.findOne({ where: { id: req.params.postId }, include: [ { model: Post, as: "Retweet", }, ], }); if (!post) { return res.status(403).send("존재하지 않는 게시글입니다."); } if (req.user.id === post.UserId || (post.Retweet && post.Retweet.UserId === req.user.id)) { return res.status(403).send("자신의 글은 리트윗할 수 없습니다."); } // 리트윗한 게시글인지 const retweetTargetId = post.RetweetId || post.id; const exPost = await Post.findOne({ where: { UserId: req.user.id, RetweetId: retweetTargetId, }, }); if (exPost) { return res.status(403).send("이미 리트윗한 게시글입니다."); } const retweet = await Post.create({ UserId: req.user.id, RetweetId: retweetTargetId, content: "retweet", }); const retweetWithPrevPost = await Post.findOne({ where: { id: retweet.id }, include: [ { model: Post, as: "Retweet", include: [ { model: User, attributes: ["id", "nickname"], }, { model: Image, }, ], }, { model: User, attributes: ["id", "nickname"], }, { model: Image, }, { model: Comment, include: [ { model: User, attributes: ["id", "nickname"], }, ], }, ], }); res.status(201).json(retweetWithPrevPost); } catch (error) { console.error(error); next(error); } }); router.post("/:postId/comment", isLoggedIn, async (req, res, next) => { // POST / post/comment try { const post = await Post.findOne({ where: { id: req.params.postId }, }); if (!post) { return res.status(403).send("존재하지 않는 게시글입니다."); } const comment = await Comment.create({ content: req.body.content, // :postId에 들어감 (문자열로) PostId: parseInt(req.params.postId, 10), UserId: req.user.id, }); const fullComment = await Comment.findOne({ where: { id: comment.id }, include: [ { model: User, attributes: ["id", "nickname"], }, ], }); res.status(201).json(fullComment); } catch (error) { console.error(error); next(error); } }); router.patch("/:postId/like", isLoggedIn, async (req, res, next) => { // PATCH /post/1/like try { const post = await Post.findOne({ where: { id: req.params.postId } }); if (!post) { return res.status(403).send("게시글이 존재하지 않습니다."); } await post.addLikers(req.user.id); res.json({ PostId: post.id, UserId: req.user.id }); } catch (error) { console.error(error); next(error); } }); router.delete("/:postId/like", isLoggedIn, async (req, res, next) => { // DELETE /post/1/like try { const post = await Post.findOne({ where: { id: req.params.postId } }); if (!post) { return res.status(403).send("게시글이 존재하지 않습니다."); } await post.removeLikers(req.user.id); res.json({ PostId: post.id, UserId: req.user.id }); } catch (error) { console.error(error); next(error); } }); router.delete("/:postId", isLoggedIn, async (req, res, next) => { // DELETE / post/10 try { await Post.destroy({ where: { id: req.params.postId, UserId: req.user.id }, }); res.status(200).json({ PostId: parseInt(req.params.postId, 10) }); } catch (error) { console.error(error); next(error); } }); module.exports = router; router/post.js const express = require("express"); const cors = require("cors"); const session = require("express-session"); const cookieParser = require("cookie-parser"); const passport = require("passport"); const dotenv = require("dotenv"); const morgan = require("morgan"); const path = require("path"); const postRouter = require("./routes/post"); const postsRouter = require("./routes/posts"); const userRouter = require("./routes/user"); const db = require("./models"); const passportConfig = require("./passport"); dotenv.config(); const app = express(); db.sequelize .sync() .then(() => { console.log("db 연결 성공"); }) .catch(console.error); passportConfig(); app.use(morgan("dev")); // cors에러 해결 app.use( cors({ // https://localhost:3060에서 온 요청만 허용 origin: "http://localhost:3060", credentials: true, }) ); app.use("/", express.static(path.join(__dirname, "uploads"))); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser(process.env.COOKIE_SECRET)); app.use( session({ saveUninitialized: false, resave: false, secret: process.env.COOKIE_SECRET, }) ); app.use(passport.initialize()); app.use(passport.session()); app.get("/", (req, res) => { res.send("Hello express"); }); app.use("/post", postRouter); app.use("/posts", postsRouter); app.use("/user", userRouter); app.listen(3065, () => { console.log("서버 실행중"); }); app.js module.exports = (sequelize, DataTypes) => { const Post = sequelize.define( "Post", { content: { type: DataTypes.TEXT, allowNull: false, }, }, { charser: "utf8mb4", collate: "utf8mb4_general_ci", } ); Post.associate = (db) => { db.Post.belongsTo(db.User); db.Post.belongsToMany(db.Hashtag, { through: "PostHashtag" }); db.Post.hasMany(db.Comment); db.Post.hasMany(db.Image); db.Post.belongsToMany(db.User, { through: "Like", as: "Likers" }); db.Post.belongsTo(db.Post, { as: "Retweet" }); }; return Post; }; models/post.js ValidationError [SequelizeValidationError]: notNull Violation: Image.content cannot be null at InstanceValidator._validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:50:13) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async InstanceValidator._validateAndRunHooks (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:60:7) at async InstanceValidator.validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:54:12) at async model.save (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:2368:7) at async Function.create (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:1344:12) at async C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\routes\post.js:70:23 { errors: [ ValidationErrorItem { message: 'Image.content cannot be null', type: 'notNull Violation', path: 'content', value: null, origin: 'CORE', instance: [Image], validatorKey: 'is_null', validatorName: null, validatorArgs: [] } ] } SequelizeValidationError: notNull Violation: Image.content cannot be null at InstanceValidator._validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:50:13) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async InstanceValidator._validateAndRunHooks (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:60:7) at async InstanceValidator.validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:54:12) at async model.save (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:2368:7) at async Function.create (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:1344:12) at async C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\routes\post.js:70:23 POST /post 500 26.161 ms - 1182 터미널 에러 import produce from 'immer'; export type mainPost = { mainPosts: any, imagePaths: object[], likePostLoading: boolean, likePostDone: boolean, likePostError: boolean, unlikePostLoading: boolean, unlikePostDone: boolean, unlikePostError: boolean, addPostLoading: boolean, addPostDone: boolean, addPostError: boolean, addCommentLoading: boolean, addCommentDone: boolean, addCommentError: boolean, hasMorePosts: boolean, loadPostsLoading: boolean, loadPostsDone: boolean, loadPostsError: boolean, removePostLoading: boolean, removePostDone: boolean, removePostError: boolean, uploadImagesLoading: boolean, uploadImagesDone: boolean, uploadImagesError: boolean, retweetLoading: boolean, retweetDone: boolean, retweetError: boolean, } export const initialState: mainPost = { mainPosts: [], imagePaths: [], hasMorePosts: true, // infinite scroll likePostLoading: false, likePostDone: false, likePostError: null, unlikePostLoading: false, unlikePostDone: false, unlikePostError: null, loadPostsLoading: false, loadPostsDone: false, loadPostsError: null, addPostLoading: false, addPostDone: false, addPostError: null, removePostLoading: false, removePostDone: false, removePostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null, uploadImagesLoading: false, uploadImagesDone: false, uploadImagesError: null, retweetLoading: false, retweetDone: false, retweetError: null, } export const UPLOAD_IMAGES_REQUEST = 'UPLOAD_IMAGES_REQUEST' as const; export const UPLOAD_IMAGES_SUCCESS = 'UPLOAD_IMAGES_SUCCESS' as const; export const UPLOAD_IMAGES_FAILURE = 'UPLOAD_IMAGES_FAILURE' as const; export const LIKE_POST_REQUEST = 'LIKE_POST_REQUEST' as const; export const LIKE_POST_SUCCESS = 'LIKE_POST_SUCCESS' as const; export const LIKE_POST_FAILURE = 'LIKE_POST_FAILURE' as const; export const UNLIKE_POST_REQUEST = 'UNLIKE_POST_REQUEST' as const; export const UNLIKE_POST_SUCCESS = 'UNLIKE_POST_SUCCESS' as const; export const UNLIKE_POST_FAILURE = 'UNLIKE_POST_FAILURE' as const; export const LOAD_POSTS_REQUEST = 'LOAD_POSTS_REQUEST' as const; export const LOAD_POSTS_SUCCESS = 'LOAD_POSTS_SUCCESS' as const; export const LOAD_POSTS_FAILURE = 'LOAD_POSTS_FAILURE' as const; export const ADD_POST_REQUEST = 'ADD_POST_REQUEST' as const; export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS' as const; export const ADD_POST_FAILURE = 'ADD_POST_FAILURE' as const; export const REMOVE_POST_REQUEST = 'REMOVE_POST_REQUEST' as const; export const REMOVE_POST_SUCCESS = 'REMOVE_POST_SUCCESS' as const; export const REMOVE_POST_FAILURE = 'REMOVE_POST_FAILURE' as const; export const ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST' as const; export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS' as const; export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE' as const; export const RETWEET_REQUEST = 'RETWEET_REQUEST' as const; export const RETWEET_SUCCESS = 'RETWEET_SUCCESS' as const; export const RETWEET_FAILURE = 'RETWEET_FAILURE' as const; export const REMOVE_IMAGE = 'REMOVE_IMAGE' as const; export const addPost = (data) => ({ type: ADD_POST_REQUEST, data, }) export const addComment = (data) => ({ type: ADD_COMMENT_REQUEST, data, }) const reducer = (state: mainPost = initialState, action: any) => { return produce(state, (draft) => { switch (action.type) { case RETWEET_REQUEST: draft.retweetLoading = true; draft.retweetDone = false; draft.retweetError = null; break; case RETWEET_SUCCESS: draft.retweetLoading = false; draft.retweetDone = true; break; case RETWEET_FAILURE: draft.retweetLoading = false; draft.retweetError = action.error; break; default: break; // 서버에서 이미지를 지우고 싶으면 비동기로 만들어줘야 한다. // 서버에서 이미지를 안지우는 이유는 머신러닝 등을 위해 데이터 수집을 할 수도 있어서 case REMOVE_IMAGE: draft.imagePaths = draft.imagePaths.filter((v, i) => i !== action.data) break; case UPLOAD_IMAGES_REQUEST: draft.uploadImagesLoading = true; draft.uploadImagesDone = false; draft.uploadImagesError = null; break; case UPLOAD_IMAGES_SUCCESS: { // 데이터들 여기 저장 draft.imagePaths = action.data; draft.uploadImagesLoading = false; draft.uploadImagesDone = true; break; } case UPLOAD_IMAGES_FAILURE: draft.uploadImagesLoading = false; draft.uploadImagesError = action.error; break; 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 = draft.mainPosts.concat(action.data); draft.hasMorePosts = action.data.length === 10; 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); draft.imagePaths = []; break; case ADD_POST_FAILURE: draft.addPostLoading = false; draft.addPostError = 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.PostId); break; case REMOVE_POST_FAILURE: draft.removePostLoading = false; draft.removePostError = 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; } }); } export default reducer; reducer/post import { all, fork, delay, put, takeEvery, takeLatest, throttle, call } from 'redux-saga/effects'; import shortId from 'shortid'; import axios from 'axios'; import { ADD_COMMENT_FAILURE, ADD_COMMENT_REQUEST, ADD_COMMENT_SUCCESS, ADD_POST_FAILURE, ADD_POST_REQUEST, ADD_POST_SUCCESS, LIKE_POST_FAILURE, LIKE_POST_REQUEST, LIKE_POST_SUCCESS, LOAD_POSTS_FAILURE, LOAD_POSTS_REQUEST, LOAD_POSTS_SUCCESS, REMOVE_POST_FAILURE, REMOVE_POST_REQUEST, REMOVE_POST_SUCCESS, RETWEET_FAILURE, RETWEET_REQUEST, RETWEET_SUCCESS, UNLIKE_POST_FAILURE, UNLIKE_POST_REQUEST, UNLIKE_POST_SUCCESS, UPLOAD_IMAGES_FAILURE, UPLOAD_IMAGES_REQUEST, UPLOAD_IMAGES_SUCCESS, } from '../reducers/post'; import { ADD_POST_TO_ME, REMOVE_POST_OF_ME } from '../reducers/user'; function retweetAPI(data) { return axios.post(`/post/${data}/retweet`); } function* retweet(action) { try { const result = yield call(retweetAPI, action.data); yield put({ type: RETWEET_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: RETWEET_FAILURE, error: err.response.data, }); } } function uploadImagesAPI(data) { return axios.post('/post/images', data); } function* uploadImages(action) { try { const result = yield call(uploadImagesAPI, action.data); yield put({ type: UPLOAD_IMAGES_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: UPLOAD_IMAGES_FAILURE, error: err.response.data, }); } } 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({ type: LIKE_POST_FAILURE, error: 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({ type: UNLIKE_POST_FAILURE, error: err.response.data, }); } } function loadPostsAPI(lastId) { return axios.get(`/posts?lastId=${lastId || 0}`) } function* loadPosts(action) { try { const result = yield call(loadPostsAPI, action.lastId) yield put({ type: LOAD_POSTS_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: LOAD_POSTS_FAILURE, error: err.response.data, }) } } function addPostAPI(data) { return axios.post('/post', data) } function* addPost(action) { try { const result = yield call(addPostAPI, action.data) yield put({ type: ADD_POST_SUCCESS, data: result.data, }); yield put({ type: ADD_POST_TO_ME, data: result.data.id, }) } catch (err) { console.error(err); yield put({ type: ADD_POST_FAILURE, error: err.response.data, }) } } function removePostAPI(data) { return axios.delete(`/post/${data}`); } function* removePost(action) { try { const result = yield call(removePostAPI, action.data) yield put({ type: REMOVE_POST_SUCCESS, data: result.data, }); yield put({ type: REMOVE_POST_OF_ME, data: action.data, }) } catch (err) { console.error(err); yield put({ type: REMOVE_POST_FAILURE, error: 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 put({ type: ADD_COMMENT_SUCCESS, data: result.data }) } catch (err) { console.error(err); yield put({ type: ADD_COMMENT_FAILURE, error: err.response.data, }) } } function* watchRetweet() { yield takeLatest(RETWEET_REQUEST, retweet); } function* watchUploadImages() { yield takeLatest(UPLOAD_IMAGES_REQUEST, uploadImages); } function* watchLikePost() { yield takeLatest(LIKE_POST_REQUEST, likePost); } function* watchUnlikePost() { yield takeLatest(UNLIKE_POST_REQUEST, unlikePost); } function* watchLoadPosts() { yield throttle(2000, LOAD_POSTS_REQUEST, loadPosts); } function* watchAddPost() { yield takeLatest(ADD_POST_REQUEST, addPost); } function* watchRemovePost() { yield takeLatest(REMOVE_POST_REQUEST, removePost); } function* watchAddComment() { yield takeLatest(ADD_COMMENT_REQUEST, addComment); } export default function* postSaga() { yield all([ fork(watchRetweet), fork(watchUploadImages), fork(watchLikePost), fork(watchUnlikePost), fork(watchAddPost), fork(watchLoadPosts), fork(watchRemovePost), fork(watchAddComment), ]) } sagas/post import { EllipsisOutlined, HeartOutlined, HeartTwoTone, MessageOutlined, RetweetOutlined } from '@ant-design/icons'; import { Avatar, Button, Card, List, Popover, Comment } from 'antd'; import { useDispatch, useSelector } from 'react-redux'; import PostImages from './PostImages'; import PropTypes from 'prop-types'; import { useCallback, useState } from 'react'; import CommentForm from './CommentForm'; import PostCardContent from './PostCardContent'; import { LIKE_POST_REQUEST, REMOVE_POST_REQUEST, UNLIKE_POST_REQUEST, RETWEET_REQUEST } from '../reducers/post'; import FollowButton from './FollowButton'; const PostCard = ({ post }) => { const dispatch = useDispatch(); const { removePostLoading } = useSelector((state: any) => state.post); const [commentFormOpened, setCommentFormOpened] = useState<boolean>(false); const id = useSelector((state: any) => state.user.me?.id) 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(() => { if (!id) { return alert('로그인이 필요합니다.') } setCommentFormOpened((prev) => !prev) }, [id]) const onRemovePost = useCallback(() => { if (!id) { return alert('로그인이 필요합니다.') } return dispatch({ type: REMOVE_POST_REQUEST, data: post.id, }) }, [id]) const onRetweet = useCallback(() => { if (!id) { return alert('로그인이 필요합니다.') } return dispatch({ type: RETWEET_REQUEST, data: post.id, }) }, [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" onClick={onRetweet} />, liked ? <HeartTwoTone twoToneColor="#eb2f96" key="heart" onClick={onUnLike} /> : <HeartOutlined key="heart" onClick={onLike} />, <MessageOutlined key="message" onClick={onToggleComment} />, <Popover key="ellipsis" content={( <Button.Group> {id && post.UserId === id ? ( <> <Button>수정</Button> <Button type="danger" loading={removePostLoading} onClick={onRemovePost}>삭제</Button> </> ) : <Button>신고</Button>} </Button.Group> )} > <EllipsisOutlined /> </Popover>, ]} // 누가 리트윗 했는지 title={post.RetweetId ? `${post.User.nickname}님이 리트윗하셨습니다.` : null} extra={id && <FollowButton post={post} />} > {post.RetweetId && post.Retweet ? (<Card cover={post.Retweet.Images[0] && <PostImages images={post.Retweet.Images} />} > <Card.Meta avatar={<Avatar>{post.Retweet.User.nickname[0]}</Avatar>} title={post.Retweet.User.nickname} description={<PostCardContent postData={post.Retweet.content} />} /> </Card>) : ( <Card.Meta avatar={<Avatar>{post.User.nickname[0]}</Avatar>} title={post.User.nickname} description={<PostCardContent postData={post.content} />} /> )} </Card> {commentFormOpened && ( <div> <List header={`${post.Comments.length}개의 댓글`} itemLayout="horizontal" dataSource={post.Comments} renderItem={(item: any) => ( <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, UserId: PropTypes.number, content: PropTypes.string, createdAt: PropTypes.string, Comments: PropTypes.arrayOf(PropTypes.any), Images: PropTypes.arrayOf(PropTypes.any), Likers: PropTypes.arrayOf(PropTypes.object), RetweetId: PropTypes.number, Retweet: PropTypes.objectOf(PropTypes.any), }).isRequired, }; export default PostCard; PostCard import { Form, Input, Button } from 'antd'; import { useCallback, useRef, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import useInput from '../hooks/useInput'; import { ADD_POST_REQUEST, REMOVE_IMAGE, UPLOAD_IMAGES_REQUEST } from '../reducers/post'; const PostForm = () => { const { imagePaths, addPostDone } = useSelector((state: any) => state.post); const dispatch = useDispatch() const imageInput = useRef<any>(); const [text, onChangeText, setText] = useInput('') useEffect(() => { if (addPostDone) { setText('') } }, [addPostDone]) const onSubmit = useCallback(() => { if (!text || !text.trim()) { return alert('게시글을 작성하세요.'); } const formData = new FormData(); imagePaths.forEach((p: string) => { formData.append('image', p); }) formData.append('content', text); return dispatch({ type: ADD_POST_REQUEST, data: formData, }); }, [text, imagePaths]) const onClickImageUpload = useCallback(() => { imageInput.current.click(); }, [imageInput.current]) const onChangeImages = useCallback((e) => { console.log('images', e.target.files); const imageFormData = new FormData(); [].forEach.call(e.target.files, (f) => { imageFormData.append('image', f); }); dispatch({ type: UPLOAD_IMAGES_REQUEST, data: imageFormData, }) }, []) const onRemoveImage = useCallback((index: number) => () => { dispatch({ type: REMOVE_IMAGE, data: index, }) }, []) return ( <Form style={{ margin: '10px 0 20px' }} encType='multipart/form-data' onFinish={onSubmit}> <Input.TextArea value={text} onChange={onChangeText} maxLength={140} placeholder="어떤 신기한 일이 있었나요?" /> <div> <input type="file" name="image" multiple hidden ref={imageInput} onChange={onChangeImages} /> <Button onClick={onClickImageUpload}>이미지 업로드</Button> <Button type="primary" style={{ float: 'right' }} htmlType="submit">짹짹</Button> </div> <div> {imagePaths.map((v: string, i: number) => ( <div key={v} style={{ display: 'inline-block' }}> <img src={`http://localhost:3065/${v}`} style={{ width: '200px' }} alt={v} /> <div> <Button onClick={onRemoveImage(i)}>제거</Button> </div> </div> ))} </div> </Form> ) } export default PostForm; PostForm POST http://localhost:3065/post 500 (Internal Server Error) dispatchXhrRequest @ xhr.js:247 xhr @ xhr.js:49 dispatchRequest @ dispatchRequest.js:51 request @ Axios.js:142 httpMethod @ Axios.js:181 wrap @ bind.js:5 addPostAPI @ post.tsx:127 runCallEffect @ redux-saga-core.esm.js:524 runEffect @ redux-saga-core.esm.js:1204 digestEffect @ redux-saga-core.esm.js:1271 next @ redux-saga-core.esm.js:1161 proc @ redux-saga-core.esm.js:1108 (익명) @ redux-saga-core.esm.js:585 immediately @ redux-saga-core.esm.js:56 runForkEffect @ redux-saga-core.esm.js:584 runEffect @ redux-saga-core.esm.js:1204 digestEffect @ redux-saga-core.esm.js:1271 next @ redux-saga-core.esm.js:1161 currCb @ redux-saga-core.esm.js:1251 takeCb @ redux-saga-core.esm.js:503 put @ redux-saga-core.esm.js:339 (익명) @ redux-saga-core.esm.js:376 exec @ redux-saga-core.esm.js:31 flush @ redux-saga-core.esm.js:87 asap @ redux-saga-core.esm.js:46 chan.put @ redux-saga-core.esm.js:375 (익명) @ redux-saga-core.esm.js:1412 dispatch @ VM285:3665 (익명) @ PostForm.tsx:33 onFinish @ Form.js:68 (익명) @ useForm.js:761 Promise.then(비동기) FormStore.submit @ useForm.js:757 onSubmit @ Form.js:119 callCallback @ react-dom.development.js:188 invokeGuardedCallbackDev @ react-dom.development.js:237 invokeGuardedCallback @ react-dom.development.js:292 invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:306 executeDispatch @ react-dom.development.js:389 executeDispatchesInOrder @ react-dom.development.js:414 executeDispatchesAndRelease @ react-dom.development.js:3278 executeDispatchesAndReleaseTopLevel @ react-dom.development.js:3287 forEachAccumulated @ react-dom.development.js:3259 runEventsInBatch @ react-dom.development.js:3304 runExtractedPluginEventsInBatch @ react-dom.development.js:3514 handleTopLevel @ react-dom.development.js:3558 batchedEventUpdates$1 @ react-dom.development.js:21871 batchedEventUpdates @ react-dom.development.js:795 dispatchEventForLegacyPluginEventSystem @ react-dom.development.js:3568 attemptToDispatchEvent @ react-dom.development.js:4267 dispatchEvent @ react-dom.development.js:4189 unstable_runWithPriority @ scheduler.development.js:653 runWithPriority$1 @ react-dom.development.js:11039 discreteUpdates$1 @ react-dom.development.js:21887 discreteUpdates @ react-dom.development.js:806 dispatchDiscreteEvent @ react-dom.development.js:4168 500 에러메세지 연거 Image.content cannot be null이라는게 왜 나오는지 모르겠습니다.. req.body.image에 도달하기 이전 어딘가에서 문제가 터지는거 같기는 한데 콘솔도 찍고 확인해봐도 어디 어디서 꼬인건지 못찾겠습니다..
- express
- redux
- react
- nodejs
- Next.js





