월 17,600원
5개월 할부 시다른 수강생들이 자주 물어보는 질문이 궁금하신가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Browserslist에러 질문
Browserslist: caniuse-lite is outdated. Please run:npx browserslist@latest --update-dbready - started server on http://localhost:3000info - automatically enabled Fast Refresh for 1 custom loaderC:\Users\내컴\Desktop\fashionary\front\node_modules\next\dist\build\webpack-config.js:38const isLocal=request.startsWith('.')||// Always check for unix-style path, as webpack sometimes 안녕하세요 선생님. 프론트단을 실행하려고 보니까 위와 같은 메시지가 떠서 에러메시지에 나온대로 터미널에 npx browserslist@latest --update-db를 입력했는데도 같은 에러가 뜨는건 어디를 봐야 하는 건가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
데이터 보안에 대해 질문드립니다.
강의에서 웬만하면 정보를 최소화해서 보내라고 말씀하셨는데 그 이유가 redux devtools에 기록이 되기 때문인가요? ajax를 사용하면 따로 attribute로 안 거르고 다 보내도 괜찮을까요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
로그아웃 질문 있습니다
로그 아웃을 할 때 분명 Passport 에서 로그아웃 처리는 되는거 같은데 크롬 개발자 도구에서는 쿠키가 남아있습니다 혹시 완전하게 쿠키를 제거하는 방법을 알 수 있을지 해서 질문 드렸습니다 로그아웃 api 부분 코드입니다
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Post.findOne을 미들웨어로 분리했을 때 객체 전달 방법이 궁금합니다.
const post = await Post.findOne({ where: { id: req.params.postId, }, }); if (!post) return req.status(403).send('존재하지 않는 게시글');id에 맞는 게시물을 찾는 코드가 생각보다 중복이 생기는 것 같아서 미들웨어로 분리 하려고 합니다.이 때, 미들웨어로 분리하게 된다면const findPost = async (req, res, next) => { const post = await Post.findOne({ where: { id: req.params.postId, }, }); if (!post) return req.status(403).send('존재하지 않는 게시글'); req.post = post; next(); };해당 방식으로 미들웨어로 분리를 하고, post 객체를 req에 담아서 보내도록 했는데요.혹시 다음 미들웨어에서만 사용가능하게 데이터를 전달할 수 있는 방식이나 req에 담아서 보내지 않는 방식이 있는지 궁금합니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
toolkit reducer 관련 질문드립니다.
강사님 github의 toolkit패키지 reducer/user.js 파일입니다.궁금한것은 reducers에 쓰여지는 내용과 extraReducers의 차이점이 궁금합니다. 강의에는 없는 내용이고 나중에 추가해주신부분이라 구글링을 해보았지만 단순 정의만으로는 이해하기가 좀 어려웠습니다.제가 지금 이해한 차이점으로는 extraReducers에서는 pending, fulfilled, rejected 상태에 따라 state를 컨트롤 할 수 있다 정도입니다.그렇다면 reducers에 작성하신 addPostToMe와 removePostToMe는 pending, fulfilled, rejected 상태에 따른 state컨트롤이 필요없어서 reducers에 작성하신건지, 아니면 addPostToMe와 removePostToMe가 post.js 관련이지만 user.js와도 뭔가 살짝? 관련이 있어서 user.js의 reducers에 넣으신건지 이해가 잘안됩니다. 혹시 질문에 부족한점 있었다면 알려주시면 더 보충해서 다시 질문드리도록 하겠습니다. 감사합니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
해결했습니다
구글링끝에 결국 해결했습니다!제로초님도 같이 찾아주시려고 해주셔서 감사드려요~!
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
로그아웃 버튼 위치가 왜 이럴까요?
두번째 돌려보는 중입니다. 처음에는 못찾는 오타가 있는 줄 알았는데 두번 보니 오타는 아닌것 같고 antd 버전에서 뭐가 달라진 걸까요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
react-query, RTK query 선택관련
안녕하세요. 제로초님 강의를 듣고 개인프로젝트를 만들다가 궁금한게 있어 질문드립니다.기존에는 redux toolkit을 사용하여 진행을 했습니다. 그러다가 Boilerplate 코드문제로 react-query나 rtk query를 redux toolkit과 같이 사용을 하려고 하는데 제로초님께서는 redux toolkit과 같이 사용한다는 가정 하에 둘 중 어떤 걸 더 추천하시나요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
프로젝트 구조 질문드립니다.
nodejs는 route, controller, service, repository, entity로 하면 중복 없이 잘 짤 수 있다고 조언하신 글을 봤습니다.nextjs는 프로젝트 구조를 어떻게 짜야 하나요? 구글링하니까 정말 다양한 방법이 많이 있던데 제로초님은 실무에서 어떻게 짜시는지 궁금합니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
프론트 npm run build가 안됩니다
ubuntu@ip-172-31-37-255:~/react-nodebird/front$ npm run build > react-nodebird-front@1.0.0 build /home/ubuntu/react-nodebird/front > cross-env ANALYZE=true NODE_ENV=production next build Browserslist: caniuse-lite is outdated. Please run: npx browserslist@latest --update-db info - Using external babel configuration from /home/ubuntu/react-nodebird/front/.babelrc Webpack Bundle Analyzer saved report to /home/ubuntu/react-nodebird/front/.next/server/analyze/client.html Webpack Bundle Analyzer saved report to /home/ubuntu/react-nodebird/front/.next/analyze/client.html info - Creating an optimized production build info - Compiled successfully > Build error occurred Error: Build optimization failed: found page without a React Component as default export in pages/about See https://err.sh/vercel/next.js/page-without-valid-component for more info. at build (/home/ubuntu/react-nodebird/front/node_modules/next/dist/build/index.js:21:115) info - Collecting page data .npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! react-nodebird-front@1.0.0 build: cross-env ANALYZE=true NODE_ENV=production next build npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the react-nodebird-front@1.0.0 build script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /home/ubuntu/.npm/_logs/2022-12-03T04_13_49_262Z-debug.log 백엔드에서 npm run dev하면 use 80 무슨 오류 떠서 포트번호 85번으로 바꿧어요백엔드쪽 app.js 85번으로 바꾸고vim app.js에서 85번 바꾸고프론트쪽 package.json에서 -p 85로 바꿧어요git status해도 아무것도 안나오고 백엔드 프론트 서버키고 npm run build 하면 오류뜹니다 app.jsfront / package.js
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
불변성 push 사용 (immer쓰지않고)
immer를 쓰지않았을 때 위에 코드를 어떻게 작성해야되는지 알고싶습니다.아래와 같이 해봤는데 에러가 나서요.혹시 push를 쓰지않고 배열로 해야되나요? ex- [{ id: action.data }, ...state.me.Followings]
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
다른 라이브러리 적용 질문
안녕하세요 선생님. 달력 컴포넌트를 ant말고 react FullCalendar로 쓰고 싶어서 설치해서 적용하려고 하니까 자꾸 에러가 나서 질문 드립니다. ./node_modules/@fullcalendar/common/main.css Global CSS cannot be imported from within node_modules. Read more: https://err.sh/next.js/css-npm Location: node_modules\@fullcalendar\common\main.js 이런 에러가 떠서 단순히 fullcalendar 폴더의 위치를 node_modules바깥으로 빼서 import하면 되겠거니 했는데 그렇게 했더니 fullcalendar파일 설정에서 충돌한다고 뜨는게 많아서 이런 방법은 불가능할것 같고...어떻게 다른 라이브러리를 적용해야 할지 몰라서 질문 드립니다ㅠㅠ
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
설정을 잘못한건가요??
start 추가하고 back에서 git pull 했습니다 이렇게 뜨는데 이게 뭔가여..mysql -uroot -p 로 mysql 연결까지 됩니다 vim .env 했어요 처음에 프로젝트 만들때 설정을 잘못한건가요??
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
이미지 첨부하고 글 작성시 500에러나는 현상
에러메세지 /post/images/postSequelizeValidationError: 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.jsmodule.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/postimport { 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/postimport { 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에 도달하기 이전 어딘가에서 문제가 터지는거 같기는 한데 콘솔도 찍고 확인해봐도 어디 어디서 꼬인건지 못찾겠습니다..
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
antd Menu 참조 오류
antd와 icon을 설치하고 메뉴를 적용하려고 하는데 Server ErrorReferenceError: Meun is not definedThis error happened while generating the page. Any console logs will be displayed in the terminal window.이렇게 오류가 떴습니다.오류가 뜬 위치는 appLayout의 8번째 줄 Menu입니다.처음에는 제가 설치한 게 5 버전이라서 그런가 싶어 4.3.0 버전으로 다시 설치했는데도 똑같은 오류가 납니다.현재 버전은 "@ant-design/icons": "^4.8.0", "antd" : "^4.3.0"입니다.아래는 제 코드입니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
getServerSideProps 질문입니다.
안녕하세요 ?강의를 듣고 redux 없이 getServerSideProps 를 사용해보려고 공식문서에 나온대로 이렇게 사용해보았는데요.console.log 로 값을 찍어보니 이런식으로 한글이 깨지더라구요..fetch로 데이터를 가져와봤더니 잘 가져와지구요..또 getServerSideProps 가 아닌 컴포넌트 내 useEffect 에서 axios.get 을 해도 데이터가 잘 들어옵니다. 왜 getServerSideProps에서 axios만 한글이 깨질까요 ?참고로 저 로컬 주소는 json-server 로 띄워둔 데이터 입니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
sequelize include 질문입니다.
post 테이블을 조회할때 include 를 사용하여 postimage 테이블의 'imagePath' 컬럼을 포함 시켰더니 실제 데이터의 형식이...PostImage: [{"imagePath": "aaaa"}, {"imagePath": "bbbb"}] 로 추가 되더라구요. 이것을 productImage:["aaaa", "bbbb'] 의 형태로 받을 수 있는 방법은 없을까요 ?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
sequelize 모델 관계성, 함수 질문입니다.
안녕하세요 선생님. 게시글 등록 routes부분 만드는 중에 모르는 부분이 생겨서 질문 드립니다.sequelize의 모델간 관계성에서 hasOne으로 설정하면, belongsTo로 설정한 테이블 명을 apple이라고 했을 때 addApple()이라는 함수가 없는 건가요?hasMany로 했을 땐 잘 작동하는데 hasOne으로 하니까 addApple()이라는 함수가 없다고 해서요..이와 관련된 공식문서나 글을 보고 이해하고 싶은데 공식문서를 봐도 이해가 안돼서 여쭤봅니다ㅠㅠㅠ
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
빌드할때 오류
ubuntu@ip-172-31-37-255:~/react-nodebird/front$ npm run build > react-nodebird-front@1.0.0 build /home/ubuntu/react-nodebird/front > cross-env ANALYZE=true NODE_ENV=production next build Browserslist: caniuse-lite is outdated. Please run: npx browserslist@latest --update-db info - Using external babel configuration from /home/ubuntu/react-nodebird/front/.babelrc info - Creating an optimized production build Failed to compile. ModuleNotFoundError: Module not found: Error: Can't resolve './ImagesZoom/index.' in '/home/ubuntu/react-nodebird/front/components' > Build error occurred Error: > Build failed because of webpack errors at build (/home/ubuntu/react-nodebird/front/node_modules/next/dist/build/index.js:15:918) at runMicrotasks (<anonymous>) at processTicksAndRejections (internal/process/task_queues.js:95:5) npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! react-nodebird-front@1.0.0 build: cross-env ANALYZE=true NODE_ENV=production next build npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the react-nodebird-front@1.0.0 build script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /home/ubuntu/.npm/_logs/2022-11-23T12_19_20_531Z-debug.log packge.json"name": "react-nodebird-front", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "next", "build": "cross-env ANALYZE=true NODE_ENV=production next build" }, "author": "SHH", "license": "ISC", "dependencies": { "@ant-design/icons": "^4.7.0", "@next/bundle-analyzer": "^13.0.4", "antd": "^4.23.3", "axios": "^1.1.2", "babel-plugin-styled-components": "^2.0.7", "cross-env": "^7.0.3", "immer": "^9.0.15", "moment": "^2.29.4", "next": "^9.5.5", "next-redux-wrapper": "^6.0.2", "prop-types": "^15.8.1", "react": "^16.14.0", "react-dom": "^16.14.0", "react-redux": "^8.0.4", "react-slick": "^0.29.0", "redux": "^4.2.0", "redux-devtools-extension": "^2.13.9", "redux-saga": "^1.2.1", "shortid": "^2.2.16", "styled-components": "^5.3.6", "swr": "^1.3.0" }, "devDependencies": { "@faker-js/faker": "^5.5.3", "babel-eslint": "^10.1.0", "eslint": "^8.24.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", "faker": "^5.5.3"이런 에러인데 /ImagesZoom/index.js 이쪽에서 잘못된 건가요?? 기능은 정상적입니다
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
아이디,비번 작성후 로그인이되지 않습니다.
리덕스 적용 후 아이디,비번값 넣고 로그인버튼 누르면 로그인된 화면으로 넘어가질 않습니다. 버튼이 동작하지 않습니다 ㅠㅠ 콘솔과 코드 보여드릴게요ㅠㅠ콘솔LoginForm.jsimport React, { useCallback, useState } from "react"; import Link from "next/link"; import { Form, Input, Button } from "antd"; import styled from "styled-components"; import { useDispatch } from "react-redux"; import useInput from "../hooks/useInput"; import { loginAction } from "../reducers"; const LoginForm = () => { const dispatch = useDispatch(); const [id, onChangeId] = useInput(""); const [password, setPassword] = useState(""); const FormWrapper = styled(Form)` padding: 10px !important; `; const onChangePassword = useCallback((e) => { setPassword(e.target.value); }, []); const onSubmitForm = useCallback(() => { dispatch(loginAction({ id, password })); }, [id, password]); return ( <FormWrapper onFinish={onSubmitForm}> <div> <label htmlFor="user-id">아이디</label> <Input name="user-id" value={id} onChange={onChangeId} required></Input> </div> <div> <label htmlFor="user-password">비밀번호</label> <Input name="user-password" value={password} onChange={onChangePassword} required ></Input> </div> <div style={{ maringTop: "10px" }}> <Button type="primary" htmlType="submit" loading={false}> 로그인 </Button> <Link href="/signup"> <a>회원가입</a> </Link> </div> </FormWrapper> ); }; export default LoginForm; AppLayout.jsimport React from "react"; import PropTypes from "prop-types"; import Link from "next/link"; import { Menu, Input, Row, Col } from "antd"; import UserProfile from "../components/UserProfile"; import LoginForm from "../components/LoginForm"; import { useSelector } from "react-redux"; const AppLayout = ({ children }) => { const isLoggedIn = useSelector((state) => { state.user.isLoggedIn; }); return ( <div> <Menu mode="horizontal"> <Menu.Item key="/"> <Link href="/"> <a>메인</a> </Link> </Menu.Item> <Menu.Item key="/profile"> <Link href="/profile"> <a>프로필</a> </Link> </Menu.Item> <Menu.Item key="mail"> <Input.Search enterButton style={{ verticalAlign: "middle" }} /> </Menu.Item> <Menu.Item key="/signup"> <Link href="/signup"> <a>회원가입</a> </Link> </Menu.Item> </Menu> <Row gutter={8}> <Col xs={24} md={6}> {isLoggedIn ? <UserProfile /> : <LoginForm />} </Col> <Col xs={24} md={12}> {children} </Col> <Col xs={24} md={6}> <a href="https://github.com/wejunguk" target="_blank" rel="noreferrer noopener" > github by </a> </Col> </Row> </div> ); }; AppLayout.propTypes = { children: PropTypes.node.isRequired, }; export default AppLayout; index.jsimport { HYDRATE } from "next-redux-wrapper"; const initialState = { user: { isLoggedIn: false, user: null, signUpData: {}, loginData: {}, }, post: { mainPosts: [], }, }; export const loginAction = (data) => { return { type: "LOG_IN", data, }; }; export const logoutAction = () => { return { type: "LOG_OUT", }; }; const rootReducer = (state = initialState, action) => { switch (action.type) { case HYDRATE: console.log("HYDRATE", action); return { ...state, ...action.payload, }; case "LOG_IN": return { ...state, user: { ...state.user, isLoggedIn: true, user: action.data, }, }; case "LOG_OUT": return { ...state, user: { ...state.user, isLoggedIn: false, user: null, }, }; default: return state; } }; export default rootReducer; configureStore.jsimport { createWrapper } from "next-redux-wrapper"; import { createStore } from "redux"; import reducer from "../reducers"; const configureStore = () => { const store = createStore(reducer); return store; }; const wrapper = createWrapper(configureStore, { debug: process.env.NODE_ENV === "development", }); export default wrapper;