작성
·
443
0
에러메세지
/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에 도달하기 이전 어딘가에서 문제가 터지는거 같기는 한데 콘솔도 찍고 확인해봐도 어디 어디서 꼬인건지 못찾겠습니다..
답변 1
0
db가 잘못 만들어진 것 같은데요. Image 테이블에는 content 컬럼이 없어야 하는데 있는 것 같습니다. Image 테이블을 지웠다 다시 만드세요. workbench같은 툴을 사용해서요.
감사합니다 해결했습니다.