인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

TaeIl Lee님의 프로필 이미지
TaeIl Lee

작성한 질문수

[리뉴얼] React로 NodeBird SNS 만들기

express.static 미들웨어

이미지 첨부하고 글 작성시 500에러나는 현상

작성

·

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같은 툴을 사용해서요.

TaeIl Lee님의 프로필 이미지
TaeIl Lee
질문자

감사합니다 해결했습니다.

TaeIl Lee님의 프로필 이미지
TaeIl Lee

작성한 질문수

질문하기