월 17,600원
5개월 할부 시다른 수강생들이 자주 물어보는 질문이 궁금하신가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
오류가 많은거 같습니다ㅠㅠ
이게 뭔가요...ㅠ첨보는건데 js파일 분할하면서 꼬인건지 설치가 잘못된건지 모르겠어요 PostCardContent.js 파일에 postData?.split ~ 하면 아래 에러는 없어지는데 위에 오류 뜹니다reducers/index.jsimport {HYDRATE} from 'next-redux-wrapper'; import {combineReducers} from 'redux'; import user from './user'; import post from './post'; // (이전상태, 액션) => 다음상태 const rootReducer = combineReducers({ index: (state = {}, action) => { switch (action.type) { case HYDRATE: console.log('HYDRATE', action); return {...state, ...action.payload}; default: return state; } }, user, post }); export default rootReducer; reducers/post.jsimport shortId from 'shortid' import produce from 'immer'; import faker from 'faker'; export const initialState = { mainPosts: [{ id: 1, User: { id: 1, nickname: 'z', }, content: '첫 번째 게시글 #해시태그 #익스프레스', Images: [{ id: shortId.generate(), src: 'https://bookthumb-phinf.pstatic.net/cover/137/995/13799585.jpg?udate=20180726', }, { id: shortId.generate(), src: 'https://gimg.gilbut.co.kr/book/BN001998/rn_view_BN001998.jpg', }, { id: shortId.generate(), src: 'https://gimg.gilbut.co.kr/book/BN001998/rn_view_BN001998.jpg', }], Comments: [{ id: shortId.generate(), User: { id: shortId.generate(), nickname: 'nero', }, content: '우와 개정판이 나왔군요~', }, { id: shortId.generate(), User: { id: shortId.generate(), nickname: 'hero', }, content: '얼른 사고싶어요~', }], }], imagePaths: [], addPostLoading: false, addPostDone: false, addPostError: null, removePostLoading: false, removePostDone: false, removePostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null, }; initialState.mainPosts = initialState.mainPosts.concat( Array(20).fill().map(() => ({ id: shortId.generate(), User: { id: shortId.generate(), nickname: faker.name.findName(), }, Content: faker.lorem.paragraph, Images: [{ src: faker.image.imageUrl(), }], Comments: [{ User: { id: shortId.generate(), nickname: faker.name.findName(), }, content: faker.lorem.sentence(), }], })), ); export const ADD_POST_REQUEST = 'ADD_POSTS_REQUEST'; export const ADD_POST_SUCCESS = 'ADD_POSTS_SUCCESS'; export const ADD_POST_FAILURE = 'ADD_POSTS_FAILURE'; export const REMOVE_POST_REQUEST = 'REMOVE_POST_REQUEST'; export const REMOVE_POST_SUCCESS = 'REMOVE_POST_SUCCESS'; export const REMOVE_POST_FAILURE = 'REMOVE_POST_FAILURE'; export const ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST'; export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS'; export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE'; export const addPost = (data) => ({ type: ADD_POST_REQUEST, data }); export const addComment = (data) => ({ type: ADD_COMMENT_REQUEST, data }); const dummyPost = (data) => ({ id: data.id, content: data.content, User: { id: 1, nickname: 'Z0', }, Images: [], Comments: [], }); const dummyComment = (data) => ({ id: shortId.generate(), content: data, User: { id: 1, nickname: 'Z1', }, }); // 이전 상태를 액션을 통해 다음 상태로 만들어내는 함수(불변성은 지키면서) const reducer = (state = initialState, action) => produce(state, (draft) => { switch (action.type) { 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 = (dummyPost(action.data)); break; case ADD_POST_FAILURE: draft.addPostLoading = true; 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 = state.mainPosts.filter((v) => v.id !== action.data); 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(dummyComment(action.data.content)); draft.addCommentLoading = false; draft.addCommentDone = true; break; // const postIndex = state.mainPosts.findIndex((v) => v.id === action.data.postId); // const post = {...state.mainPosts[postIndex]}; // post.Comments = [dummyComment(action.data.content), ...post.Comments]; // const mainPosts = [...state.mainPosts]; // mainPosts[postIndex] = post; // return { // ...state, // mainPosts, // addCommentLoading: false, // addCommentDone: true // }; } case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; default: break; } }); export default reducer; reducers/user.jsimport produce from 'immer'; export const initialState = { logInLoading: false, // 로그인 시도중 logInDone: false, logInError: null, logOutLoading: false, // 로그아웃 시도중 logOutDone: false, logOutError: null, signUpLoading: false, // 회원가입 시도중 signUpDone: false, signUpError: false, changeNicknameLoading: false, // 닉네임 변경 시도중 changeNicknameDone: false, changeNicknameError: false, me: null, signUpData: {}, loginData: {} }; export const LOG_IN_REQUEST = 'LOG_IN_REQUEST'; export const LOG_IN_SUCCESS = 'LOG_IN_SUCCESS'; export const LOG_IN_FAILURE = 'LOG_IN_FAILURE'; export const LOG_OUT_REQUEST = 'LOG_OUT_REQUEST'; export const LOG_OUT_SUCCESS = 'LOG_OUT_SUCCESS'; export const LOG_OUT_FAILURE = 'LOG_OUT_FAILURE'; export const SIGN_UP_REQUEST = 'SIGN_UP_REQUEST'; export const SIGN_UP_SUCCESS = 'SIGN_UP_SUCCESS'; export const SIGN_UP_FAILURE = 'SIGN_UP_FAILURE'; export const CHANGE_NICKNAME_REQUEST = 'CHANGE_NICKNAME_REQUEST'; export const CHANGE_NICKNAME_SUCCESS = 'CHANGE_NICKNAME_SUCCESS'; export const CHANGE_NICKNAME_FAILURE = 'CHANGE_NICKNAME_FAILURE'; export const FOLLOW_REQUEST = 'FOLLOW_REQUEST'; export const FOLLOW_SUCCESS = 'FOLLOW_SUCCESS'; export const FOLLOW_FAILURE = 'FOLLOW_FAILURE'; export const UNFOLLOW_REQUEST = 'UNFOLLOW_REQUEST'; export const UNFOLLOW_SUCCESS = 'UNFOLLOW_SUCCESS'; export const UNFOLLOW_FAILURE = 'UNFOLLOW_FAILURE'; export const ADD_POST_TO_ME = 'ADD_POST_TO_ME'; export const REMOVE_POST_OF_ME = 'REMOVE_POST_OF_ME'; const dummyUser = (data) => ({ ...data, nickname: 'zo', id: 1, Posts: [{id: 1}], Followings: [{nickname: 'Boo'}, {nickname: 'B'}, {nickname: 'C'}], Followers: [{nickname: 'ZZ'}, {nickname: 'BB'}, {nickname: 'CC'}] }); export const loginRequestAction = (data) => { return { type: LOG_IN_REQUEST, data, } } export const logoutRequestAction = () => { return { type: LOG_OUT_REQUEST, } } const reducer = (state = initialState, action) => produce(state, (draft) => { switch (action.type) { case LOG_IN_REQUEST: draft.logInLoading = true; draft.logInError = null; draft.logInDone = false; break; case LOG_IN_SUCCESS: draft.logInLoading = false; draft.me = dummyUser(action.data); draft.logInDone = true; break; case LOG_IN_FAILURE: draft.logInLoading = false; draft.logInError = action.error; break; case LOG_OUT_REQUEST: draft.logOutLoading = true; draft.logOutError = null; draft.logOutDone = false; break; case LOG_OUT_SUCCESS: draft.logOutLoading = false; draft.logOutDone = true; draft.me = null; break; case LOG_OUT_FAILURE: draft.logOutLoading = false; draft.logOutError = action.error; break; case SIGN_UP_REQUEST: draft.signUpLoading = true; draft.signUpError = null; draft.signUpDone = false; break; case SIGN_UP_SUCCESS: draft.signUpLoading = false; draft.signUpDone = true; break; case SIGN_UP_FAILURE: draft.signUpLoading = false; draft.signUpError = action.error; break; case CHANGE_NICKNAME_REQUEST: draft.changeNicknameLoading = true; draft.changeNicknameError = null; draft.changeNicknameDone = false; break; case CHANGE_NICKNAME_SUCCESS: draft.changeNicknameLoading = false; draft.changeNicknameDone = true; break; case CHANGE_NICKNAME_FAILURE: draft.changeNicknameLoading = false; draft.changeNicknameError = action.error; break; // 게시글 등록 case ADD_POST_TO_ME: draft.me.Posts.unshift({id: action.data}); break; // return { // ...state, // me: { // ...state.me, // Posts: [{ id: action.data }, ..state.me.Posts], // }, // }; // 게시글 삭제 case REMOVE_POST_OF_ME: draft.me.Posts = draft.me.Posts.filter((v) => v.id !== action.data); break; // return { // ...state, // me: { // ...state.me, // Posts: state.me.Posts.filter((v) => v.id !== action.data) // }, // }; default: break; } }); export default reducer; pages/profile.jsimport Head from 'next/head'; import React, {useEffect} from 'react'; import Router from 'next/router'; import AppLayout from '../components/AppLayout'; import NicknameEditForm from '../components/NicknameEditFrom'; import FollowList from '../components/FollowList'; import { useSelector } from 'react-redux'; const Profile = () => { const {me} = useSelector((state) => state.user); useEffect(() => { if (!(me && me.id)) { Router.push('/'); } }, [me && me.id]); if(!me) { return null; } return ( <AppLayout> <Head> <title>내 프로필 | NodeBird</title> </Head> <NicknameEditForm /> <FollowList header="팔로잉" data={me.Followings} /> <FollowList header="팔로워" data={me.Followers} /> </AppLayout> ) } export default Profile; 로그인하고 게시글 쓰면 profile.js에 useEffect쪽 '/' 루트로 가지는거 같습니다빈 창 떠요
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요.
제로초님 안녕하세요. 강의는 다 들었는데 추가로 공부를 하던 도중 학습 방향을 못잡고 있어서 질문 드립니다. 다른 강의에서 react-query와 redux-tookit 이야기를 해주셔서 공식문서와 노드버드 깃헙을 예제삼아 공부를 하고 있었습니다.제가 보기에는 react-query는 강의에서 swr과 비슷한것 같고 결국 데이터를 가지고 관리하려면 redux에 해당하는 redux-tookit을 사용해야할거 같은데 맞나요? 그리고 다른 답변글을 보니 rtk-query + redux-tookit으로 프로젝트 구조를 잡고 해야할거 같은데 맞는지 궁금합니다.또한 react-query 예제 같은 경우 각 페이지나 데이터가 필요한 부분에서 useQuery 같은 훅을 통해 데이터를 가져와서 즉시 보여주는 식으로 돼 있던데 여기에 redux-tookit 개념이 들어가면 데이터 흐름을 어떤식으로 이뤄져야 하는지도 궁금합니다.마지막으로 관련된 강의는 없겠죠??ㅠㅠ 양심 없게도 이번에도 현영님 강의 통해서 편하게 학습하려 했는데 강의는 따로 없는거 같아서 질문드립니다. 감사합니다!!
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
정적빌드시 라우팅 질문
안녕하세요 문의사항있어서 글을 남겨봅니다 강의를 듣고 프로젝트를 진행중인데 정적배포를 위해 빌드와 동시에 export를 진행하니 out폴더에 page별로 html이 생성되어추출이 됩니다. 그러다보니 out폴더 파일을 그대로 s3에 배포해보니 link라우팅이 처음에는 되지만 예를 들어 기존 시작파일이 inde.html => Router.push('/test');를 이용하면 test로 이동되지만 제가 직업 url에 url:3060/test를 입력시 페이지를 찾지못하고 url:3060/test.html로 이동해야 이동이 가능합니다 해당부분어떤 것 때문에 정적빌드시 라우팅이 되지 않는 것일까요?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
src참조 오류 관련 질문드리겠습니다.
ADD_POSTING_SEUCCESS후에 다음과 같이 정상적으로 post가 등록됬습니다.근데 아래와 같이 클라이언트에서 포스트의 이미지 주소를 참조하지 못하고 있다고 에러가 발생해서 질문드립니다.express static에서 문제가 발생한것같은데 혹시 해당 오류 원인에 대해서 알 수 있을까요?postcard.js const PostCard = ({ post }) => { const dispatch = useDispatch(); const id = useSelector((state) => state.user.me && state.user.me.id); const liked = post.Likers.find((v) => v.id === id); return ( <article> <CardWrapper bodyStyle={{height: '120px', overflow: 'hidden'}} hoverable cover={ <CardImageWrapper> <ImageWrapper alt="post image" src={`http://localhost3065/${post.Images[0].src}`} onClick={showPostModal} /> </CardImageWrapper> } : : export default PostCard;postingform.jsimport React, { useCallback } from 'react'; import { Button, Form, Input, Upload } from 'antd'; import { UploadOutlined } from '@ant-design/icons'; import { useDispatch, useSelector } from 'react-redux'; import Router from 'next/router'; import { PostingFormWrapper, FormWrapper, FormHeader, HeaderText, HeaderBtn, HeaderDiviver, ImageUploaderWrapper, ContentFormWrapper, TagsInputWrapper } from './styles'; import { ADD_POST_REQUEST, UPLOAD_IMAGES_REQUEST } from '../../reducers/post'; const PostingForm = () => { const { imagePaths, addPostLoading } = useSelector((state) => state.post); const dispatch = useDispatch(); const onSubmitForm = useCallback((value) => { console.log(value); dispatch({ type: ADD_POST_REQUEST, data: { imagePaths, content: value, } }) }, [imagePaths]); const normFile = useCallback((e) => { console.log(`normFile : ${e}`); if (Array.isArray(e)) { return e; } return e?.fileList; }, []); const onChangeImages = useCallback((e) => { console.log(`onchange : ${e}`); const imageFormData = new FormData(); e.fileList.forEach((f) => { imageFormData.append('image', f.originFileObj); }); dispatch({ type: UPLOAD_IMAGES_REQUEST, data: imageFormData, }); }, []); // const onRemoveImages = useCallback((e) => { // console.log(`onRemove : ${e.name}`); // }, []); const onBeforeUpload = useCallback((file, fileList) => { // Return False So That Antd Doesn't Upload The Picture Right Away return false }, []); return ( <section> <PostingFormWrapper name="posting" onFinish={onSubmitForm} scrollToFirstError encType='multipart/form-data' > <FormWrapper style={{marginBottom: '1em'}}> <FormHeader> <HeaderText>Post Writing</HeaderText> <div> <HeaderBtn type='primary' size='large' htmlType="submit" loading={addPostLoading} > 등록 </HeaderBtn> <Button size='large'>취소</Button> </div> </FormHeader> <HeaderDiviver /> </FormWrapper> <FormWrapper name="title" rules={[ { type: 'text', }, { required: true, message: '포스팅 제목을 입력하세요.', }, ]} hasFeedback > <Input placeholder='제목을 입력해 주세요.' allowClear="true" size='large' /> </FormWrapper> <FormWrapper name="desc" rules={[ { type: 'text', }, ]} > <Input placeholder='포스팅의 간략한 설명을 입력해 주세요.' allowClear="true" size='large' /> </FormWrapper> <ImageUploaderWrapper name="images" rules={[ { required: true, message: '조리사진을 첨부하세요.', }, ]} valuePropName="fileList" getValueFromEvent={normFile} > {/* action: 파일을 업로드할 실제 URL -> localhost3065/images */} <Upload.Dragger name="image" multiple // action="http://localhost:3065" listType="picture" onChange={onChangeImages} // onRemove={onRemoveImages} beforeUpload={onBeforeUpload} > <p style={{marginBottom: '0.5em'}}>Drag files here OR</p> <Button type='primary' size='large' icon={<UploadOutlined />}>Upload</Button> </Upload.Dragger> </ImageUploaderWrapper> <ContentFormWrapper name="ingredient" rules={[ { type: 'text', }, { required: true, message: '재료를 입력하세요.', }, ]} hasFeedback > <Input.TextArea placeholder='재료를 입력하세요.' size='large' showCount maxLength={100} rows={5} /> </ContentFormWrapper> <ContentFormWrapper name="recipes" rules={[ { type: 'text', }, { required: true, message: '요리방법을 입력하세요.', }, ]} hasFeedback > <Input.TextArea placeholder='요리방법을 입력하세요.' size='large' showCount maxLength={1000} rows={20} /> </ContentFormWrapper> <ContentFormWrapper name="tips" rules={[ { type: 'text', }, ]} > <Input.TextArea placeholder='Tip을 입력하세요.' size='large' showCount maxLength={200} rows={8} /> </ContentFormWrapper> <Form.Item name="tags" rules={[ { type: 'text', }, ]} > <TagsInputWrapper placeholder='태그를 입력해 주세요.' size='large' /> </Form.Item> </PostingFormWrapper> </section> ) }; export default PostingForm;reducerconst reducer = (state = initialState, action) => { return produce(state, (draft) => { switch (action.type) { 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 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; } }); }; export default reducer;sagafunction uploadImagesAPI(data) { console.log('사가의 데이터', 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) { yield put({ type: UPLOAD_IMAGES_FAILURE, data: err.response.data }) } } function addPostAPI(data) { return axios.post('/post', data); } function* addPost(action) { try { const result = yield call(addPostAPI, action.data); console.log(result.data); yield put({ type: ADD_POST_SUCCESS, data: result.data, }) yield put({ type: BOARD_ADD_POST_TO_ME, data: result.data, }) } catch(err) { yield put({ type: ADD_POST_FAILURE, data: err.response.data }) } } back/app.jsconst express = require('express'); const cors = require('cors'); const passport = require('passport'); const session = require('express-session'); const cookieParser = require('cookie-parser'); const dotenv = require('dotenv'); const morgan = require('morgan'); const path = require('path'); const postsRouter = require('./routes/posts'); const postRouter = require('./routes/post'); const userRouter = require('./routes/user'); const db = require('./models') const app = express(); const passportConfig = require('./passport'); dotenv.config(); db.sequelize.sync() .then(() => { console.log('db 연결 성공'); }) .catch(console.error); passportConfig(); app.use(morgan('dev')); app.use(cors({ origin: 'http://localhost:3060', credentials: true, })); app.use('/', express.static(path.join(__dirname, 'uploads'))); app.use(express.json({ limit: '100mb' })); app.use(express.urlencoded({ limit: '100mb', 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.use('/posts', postsRouter); app.use('/post', postRouter); app.use('/user', userRouter); app.listen(3065, () => { console.log('서버 실행 중'); });back/post.jsconst express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { Post, Comment, Image, User } = 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); const basename = path.basename(file.originalname, ext); done(null, basename + '_' + new Date().getTime() + ext); }, }), limits: { fileSize: 20 * 1024 * 1024 }, }); // data: { // imagePaths, // content: value, // } router.post('/', isLoggedIn, async (req, res, next) => { // addPostAPI / POST /post try { const post = await Post.create({ UserId: req.user.id, title: req.body.content.title, desc: req.body.content.desc, ingredient: req.body.content.ingredient, recipes: req.body.content.recipes, tips: req.body.content.tips, tags: req.body.content.tags, }); if (req.body.imagePaths) { if (Array.isArray(req.body.imagePaths)) { const images = await Promise.all(req.body.imagePaths.map((image) => Image.create({ src: image }))); await post.addImages(images); } else { const image = await Image.create({ src: req.body.imagePaths }); 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', attributes: ['id'], }] }); console.log(fullPost); res.status(201).json(fullPost); } catch (error) { console.error(error); next(error); } }); router.post('/images', isLoggedIn, upload.array('image'), async (req, res, next) => { try { console.log('라우터', req.files); res.json(req.files.map((v) => v.filename)); } catch (error) { console.error(error); next(error); } }); module.exports = router;
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
TypeError: Cannot destructure property 'mainPosts' of 오류 어디가 문제일까요...
찾다가 몰라서 오류 질문이요찾아도 안보입니다 ㅠㅠ어딘가 빼먹은건가요??연동이 안된건가요??코드 이중에 하나인가요? pages폴더 - index.jsimport React from 'react'; import { useSelector } from 'react-redux'; import AppLayout from '../components/AppLayout'; import PostCard from '../components/PostCard'; import PostForm from '../components/PostForm'; const Home = () => { const {me} = useSelector((state) => state.user); const {mainPosts} = useSelector((state) => state.post); return ( <AppLayout> {me && <PostForm />} {mainPosts.map((post) => <PostCard key={post.id} post={post} /> )} </AppLayout> ); }; export default Home;store 폴더 - configureStore.jsimport {createWrapper} from 'next-redux-wrapper'; import {createStore, applyMiddleware, compose} from 'redux'; import {composeWithDevTools} from 'redux-devtools-extension'; import createSagaMiddleware from 'redux-saga'; import reducer from '../reducers'; import rootSaga from '../sagas'; const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => { console.log(action); return next(action); }; const configureStore = () => { const sagaMiddleware = createSagaMiddleware(); const middlewares = [sagaMiddleware, loggerMiddleware]; const enhancer = process.env.NODE_ENV === 'production' ? compose(applyMiddleware(...middlewares)) : composeWithDevTools(applyMiddleware(...middlewares)) const store = createStore(reducer, enhancer); store.sagaTask = sagaMiddleware.run(rootSaga) return store; } const wrapper = createWrapper(configureStore, { debug: process.env.NODE_ENV === 'development', }); export default wrapper;package.json"dependencies": { "@ant-design/icons": "^4.7.0", "antd": "^4.23.1", "axios": "^0.27.2", "next": "^12.3.0", "next-redux-wrapper": "^6.0.2", "prop-types": "^15.8.1", "react-redux": "^8.0.2", "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.5", "update": "^0.7.4" }, "devDependencies": { "babel-eslint": "^10.1.0", "eslint": "^8.23.1", "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" } } 강의는 redux-saga 연동하기 부분이에요
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
업로드시 POST http://localhost:3065/ 404 (Not Found) 에러질문입니다.
antd upload컴포넌트를 사용해서 해당 강의를 학습하고 있습니다.근데 이미지 업로드시 POST http://localhost:3065/ 404 (Not Found) 에러가 발생하고 있습니다.redux툴을 확인해보니 아래와 같이 upload_images_success는 실행됬는데 redux의 imagepaths에는 파일명이 없습니다.확실하지는 않지만 antd upload에 action속성이 잘못되어서 오류가 발생하는것같은데 에러 원인에 대해서 알 수 있을까요?참고 코드도 첨부하겠습니다. uploadform const normFile = useCallback((e) => { console.log('Upload event:', e); if (Array.isArray(e)) { return e; } return e?.fileList; }, []); const onChangeImages = useCallback((e) => { console.log('images', e.fileList); const imageFormData = new FormData(); [].forEach.call(e.fileList, (f) => { imageFormData.append('image', f); }); dispatch({ type: UPLOAD_IMAGES_REQUEST, data: imageFormData, }) }, []); <ImageUploaderWrapper name="images" rules={[ { required: true, message: '조리사진을 첨부하세요.', }, ]} valuePropName="fileList" getValueFromEvent={normFile} > {/* action: 파일을 업로드할 실제 URL -> localhost3065/images */} <Upload.Dragger name="image" multiple action="http://localhost:3065" listType="picture" onChange={onChangeImages} > <p style={{marginBottom: '0.5em'}}>Drag files here OR</p> <Button type='primary' size='large' icon={<UploadOutlined />}>Upload</Button> </Upload.Dragger> </ImageUploaderWrapper> reduxexport const initialState = { mainPosts: [], imagePaths: [], uploadImagesLoading: false, // 이미지 업로드 uploadImagesDone: false, uploadImagesError: null, }; export const UPLOAD_IMAGES_REQUEST = 'UPLOAD_IMAGES_REQUEST'; export const UPLOAD_IMAGES_SUCCESS = 'UPLOAD_IMAGES_SUCCESS'; export const UPLOAD_IMAGES_FAILURE = 'UPLOAD_IMAGES_FAILURE'; const reducer = (state = initialState, action) => { return produce(state, (draft) => { switch (action.type) { 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; default: break; } }); }; export default reducer; sagafunction 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) { yield put({ type: UPLOAD_IMAGES_FAILURE, data: err.response.data }) } } function* watchUploadImages() { yield takeLatest(UPLOAD_IMAGES_REQUEST, uploadImages); } export default function* postSaga() { yield all([ fork(watchUploadImages), ]); } routertry { 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); const basename = path.basename(); done(null, basename + '_' + new Date().getTime() + ext); }, }), limits: { fileSize: 20 * 1024 * 1024 }, }); router.post('/images', isLoggedIn, upload.array('image'), async (req, res, next) => { try { console.log(req.files); res.json(req.files.map((v) => v.filename)); } catch (error) { console.error(error); next(error); } }); app.jsapp.use('/', express.static(path.join(__dirname, 'uploads')));
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
다이나믹 라우터 애러 도와주시면 감사하겠습니다.
1 of 1 unhandled errorServer ErrorTypeError: Cannot read properties of null (reading 'Likers')This error happened while generating the page. Any console logs will be displayed in the terminal window.Sourcecomponents\PostCard.js (18:23) @ PostCard 16 | const dispatch = useDispatch(); 17 | const id = useSelector((state) => state.user.me?.id) > 18 | const liked = post.Likers.find((item) => item.id === id) | ^ 19 | 20 | const onToggleComment = useCallback(() => { 21 | setCommentFormOpened((prev) => !prev)Call StackFunction.getInitialPropspages\_document.tsx (91:33)Show collapsed frames선생님도 같은 오류 나왔었던거 있어서 그대로 오류 해결하는데도 이 오류가 안없어지네요...//서버 메세지 GET /posts?lastId=0 200 12.677 ms - 2197 GET /post/60 200 15.146 ms - 216서버에서는 요청 잘 받는것 같은데 Likers가 계속 비어있는거 같습니다... 메인 화면에서는 작동이제 다 잘되는데 다이나믹 라우트 post/ 로 넘어가기만 하면 오류 발생하네요... 하루종일 매달리고 있는데... 선생님 깃허브 코드랑 강의 코드가 달라서 조금 헷갈려서요... 깃허브 주소 남기겠습니다 한번 봐주시면 감사하겠습니다. ㅠㅠhttps://github.com/wihyanghoon/react-nodebird// [id].js import React from 'react'; import { useSelector } from 'react-redux'; import { useRouter } from 'next/router'; import { END } from 'redux-saga'; import axios from 'axios'; import { LOAD_POST_REQUEST } from '../../reducers/post'; import wrapper from '../../store/configureStore'; import PostCard from '../../components/PostCard'; import AppLayout from '../../components/AppLayout'; import { LOAD_MYINFO_REQUEST } from '../../reducers/user'; const Post = () => { const { singlePost } = useSelector((state) => state.post); const router = useRouter(); const { id } = router.query; // if (router.isFallback) { // return <div>Loading...</div> // } return ( <AppLayout> <PostCard post={ singlePost } /> </AppLayout> ); }; export const getServerSideProps = wrapper.getServerSideProps(async (context) => { const cookie = context.req ? context.req.headers.cookie : ''; console.log(context); axios.defaults.headers.Cookie = ''; if (context.req && cookie) { axios.defaults.headers.Cookie = cookie; } context.store.dispatch({ type: LOAD_MYINFO_REQUEST, }); context.store.dispatch({ type: LOAD_POST_REQUEST, data: context.params.id, }); context.store.dispatch(END); await context.store.sagaTask.toPromise(); }); export default Post;import { delay, all, fork, takeLatest, put, throttle, call } from "redux-saga/effects"; import { ADD_POST_REQUEST, ADD_POST_SUCCESS, ADD_POST_FAILURE, REMOVE_POST_REQUEST, REMOVE_POST_SUCCESS, REMOVE_POST_FAILURE, ADD_COMMENT_REQUEST, ADD_COMMENT_SUCCESS, ADD_COMMENT_FAILURE, LOAD_POSTS_REQUEST, LOAD_POSTS_SUCCESS, LOAD_POSTS_FAILURE, LIKE_POST_REQUEST, LIKE_POST_SUCCESS, LIKE_POST_FAILURE, UNLIKE_POST_REQUEST, UNLIKE_POST_SUCCESS, UNLIKE_POST_FAILURE, UPLOAD_IMAGES_REQUEST, UPLOAD_IMAGES_SUCCESS, UPLOAD_IMAGES_FAILURE, RETWEET_REQUEST,RETWEET_SUCCESS,RETWEET_FAILURE, LOAD_POST_REQUEST, LOAD_POST_SUCCESS, LOAD_POST_FAILURE } from '../reducers/post' import { ADD_POST_TO_ME, REMOVE_POST_TO_ME } from "../reducers/user"; import axios from "axios"; import shortId from 'shortid'; function addPostAPI(data) { return axios.post('/post', data); } function* addPost(action) { try { const result = yield call(addPostAPI, action.data); yield console.log(result) 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) { console.log(action.data) try { const result = yield call(removePostAPI, action.data) yield console.log(typeof result.data.PostId) yield put({ type: REMOVE_POST_SUCCESS, data: result.data }) yield put({ type: REMOVE_POST_TO_ME, data: result.data.PostId }) } catch (err) { yield put({ type: REMOVE_POST_FAILURE, error: err.response.data }); } } function addCommentAPI(data) { return axios.post(`/post/${data.postId}/comment`, data) } function* addComment(action) { try { const result = yield call(addCommentAPI, action.data) yield put({ type: ADD_COMMENT_SUCCESS, data: result.data }); } catch (err) { yield put({ type: ADD_COMMENT_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, data: err.response.data, }); } } function loadPostAPI(data) { return axios.get(`/post/${data}`); } function* loadPost(action) { try { const result = yield call(loadPostAPI, action.data); yield put({ type: LOAD_POST_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: LOAD_POST_FAILURE, error: err.response.data, }); } } function likePostAPI(data) { return axios.patch(`/post/${data}/like `, data); } 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, data: err.response.data, }); } } function UnLikePostAPI(data) { return axios.delete(`/post/${data}/like`); } function* UnLikePost(action) { try { const result = yield call(UnLikePostAPI, action.data); yield put({ type: UNLIKE_POST_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: UNLIKE_POST_FAILURE, data: 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, data: err.response.data, }); } } 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, err: err.response.data, }); } } function* watchLoadPosts() { yield throttle(5000, LOAD_POSTS_REQUEST, loadPosts); } function* watchLoadPost() { yield takeLatest(LOAD_POST_REQUEST, loadPost); } function* watchAddPost() { yield takeLatest(ADD_POST_REQUEST, addPost) } function* watchRemovePost() { yield takeLatest(REMOVE_POST_REQUEST, removePost) } function* watchCommentPost() { yield takeLatest(ADD_COMMENT_REQUEST, addComment) } function* watchLikePost() { yield takeLatest(LIKE_POST_REQUEST, likePost) } function* watchUnLiketPost() { yield takeLatest(UNLIKE_POST_REQUEST, UnLikePost) } function* watchUpLoadImages() { yield takeLatest(UPLOAD_IMAGES_REQUEST, upLoadImages) } function* watchRetweet() { yield takeLatest(RETWEET_REQUEST, retweet) } export default function* postSaga() { yield all([ fork(watchAddPost), fork(watchCommentPost), fork(watchRemovePost), fork(watchLoadPosts), fork(watchLoadPost), fork(watchLikePost), fork(watchUnLiketPost), fork(watchUpLoadImages), fork(watchRetweet), ]); }import shortId from 'shortid'; import produce from 'immer'; import faker from 'faker'; import { LIKE_FAILURE, LIKE_REQUEST, LIKE_SUCCESS } from './user'; export const initialState = { mainPosts: [], imagePath: [], hasMorePosts: true, loadPostsLoading: false, loadPostsDone: false, loadPostsError: null, loadPostLoading: false, loadPostDone: false, loadPostError: null, likeLoading: false, likeDone: false, likeError: null, unLikeLoading: false, unLikeDone: false, unLikeError: null, addPostLoadding: false, addPostDone: false, addPostErr: null, removePostLoadding: false, removePostDone: false, removePostErr: null, addCommentLoadding: false, addCommentDone: false, addCommentErr: null, upLoadImagesLoadding: false, upLoadImagesDone: false, upLoadImagesErr: null, retweetLoadding: false, retweetDone: false, retweetErr: null, singlePost: null, } // export const getDemmuyPost = (number) => Array(number).fill().map(() => ({ // id: shortId.generate(), // User: { // id: shortId.generate(), // nickname: faker.name.findName(), // }, // content: faker.lorem.paragraph(), // Images: [{ // src: faker.image.image(), // }], // Comments: [{ // User: { // id: shortId.generate(), // nickname: faker.name.findName(), // }, // content: faker.lorem.sentence(), // }], // })) export const REMOVE_IMAGES_SUCSESS = 'REMOVE_IMAGES_SUCSESS'; export const LOAD_POST_REQUEST = 'LOAD_POSTS_REQUEST'; export const LOAD_POST_SUCCESS = 'LOAD_POSTS_SUCCESS'; export const LOAD_POST_FAILURE = 'LOAD_POSTS_FAILURE'; export const LOAD_POSTS_REQUEST = 'LOAD_POSTS_REQUEST'; export const LOAD_POSTS_SUCCESS = 'LOAD_POSTS_SUCCESS'; export const LOAD_POSTS_FAILURE = 'LOAD_POSTS_FAILURE'; export const ADD_POST_REQUEST = 'ADD_POST_REQUEST' export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS' export const ADD_POST_FAILURE = 'ADD_POST_FAILURE' export const REMOVE_POST_REQUEST = 'REMOVE_POST_REQUEST' export const REMOVE_POST_SUCCESS = 'REMOVE_POST_SUCCESS' export const REMOVE_POST_FAILURE = 'REMOVE_POST_FAILURE' export const ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST' export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS' export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE' export const LIKE_POST_REQUEST = 'LIKE_POST_REQUEST'; export const LIKE_POST_SUCCESS = 'LIKE_POST_SUCCESS'; export const LIKE_POST_FAILURE = 'LIKE_POST_FAILURE'; export const UPLOAD_IMAGES_REQUEST = 'UPLOAD_IMAGES_REQUEST'; export const UPLOAD_IMAGES_SUCCESS = 'UPLOAD_IMAGES_SUCCESS'; export const UPLOAD_IMAGES_FAILURE = 'UPLOAD_IMAGES_FAILURE'; export const UNLIKE_POST_REQUEST = 'UNLIKE_POST_REQUEST'; export const UNLIKE_POST_SUCCESS = 'UNLIKE_POST_SUCCESS'; export const UNLIKE_POST_FAILURE = 'UNLIKE_POST_FAILURE'; export const RETWEET_REQUEST = 'RETWEET_REQUEST' export const RETWEET_SUCCESS = 'RETWEET_SUCCESS' export const RETWEET_FAILURE = 'RETWEET_FAILURE' export const addPostAction = (data) => { return { type: ADD_POST_REQUEST, data } } export const addCommentAction = (data) => { return { type: ADD_COMMENT_REQUEST, data } } const reducer = (state = initialState, action) => { return produce(state, (draft) => { switch (action.type) { case RETWEET_REQUEST: draft.retweetLoadding = true draft.retweetDone = false draft.retweetErr = null break; case RETWEET_SUCCESS: draft.retweetLoadding = false draft.retweetDone = true draft.mainPosts.unshift(action.data) break; case RETWEET_FAILURE: draft.retweetLoadding = false draft.retweetErr = action.err break; case REMOVE_IMAGES_SUCSESS: console.log(action.data) draft.imagePath = draft.imagePath.filter((item, index) => index !== action.data) 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 = draft.mainPosts.length === 10; break; case LOAD_POSTS_FAILURE: draft.loadPostsLoading = false; draft.loadPostsError = action.error; break; case LOAD_POST_REQUEST: draft.loadPostLoading = true; draft.loadPostDone = false; draft.loadPostError = null; break; case LOAD_POST_SUCCESS: draft.loadPostLoading = false; draft.loadPostDone = true; draft.singlePost = action.data; break; case LOAD_POST_FAILURE: draft.loadPostLoading = false; draft.loadPostError = action.error; break; case ADD_POST_REQUEST: draft.addPostLoadding = true draft.addPostDone = false draft.addPostErr = null break; case ADD_POST_SUCCESS: draft.addPostLoadding = false draft.addPostDone = true draft.mainPosts.unshift(action.data) draft.imagePath = [] break; case ADD_POST_FAILURE: draft.addPostLoadding = false draft.addPostErr = action.err break; case REMOVE_POST_REQUEST: draft.removePostLoadding = true draft.removePostDone = false draft.removePostErr = null break; case REMOVE_POST_SUCCESS: draft.removePostLoadding = false draft.removePostDone = true draft.mainPosts = state.mainPosts.filter((item) => item.id !== action.data.PostId) break; case REMOVE_POST_FAILURE: draft.removePostLoadding = false draft.removePostErr = action.err break; case ADD_COMMENT_REQUEST: draft.addCommentLoadding = true draft.addCommentDone = false draft.addCommentErr = null break; case ADD_COMMENT_SUCCESS: const post = draft.mainPosts.find((item) => { return item.id === action.data.PostId }) post.Comments.unshift(action.data) draft.addCommentLoadding = false draft.addCommentDone = true break; case ADD_COMMENT_FAILURE: draft.addCommentLoadding = false draft.addCommentErr = action.error break; case LIKE_POST_REQUEST: draft.likeLoading = true draft.likeDone = false draft.likeError = null break; case LIKE_POST_SUCCESS: { draft.likeLoading = false draft.likeDone = true const post = draft.mainPosts.find((item) => item.id === action.data.PostId) post.Likers.push({ id: action.data.UserId }) break; } case LIKE_POST_FAILURE: draft.unLikeLoading = false draft.unLikeError = true break; case UNLIKE_POST_REQUEST: draft.unLikeLoading = true draft.unLikeDone = false draft.unLikeError = null break; case UNLIKE_POST_SUCCESS: { draft.unLikeLoading = false draft.unLikeDone = true const post = draft.mainPosts.find((v) => v.id === action.data.PostId); post.Likers = post.Likers.filter((v) => v.id !== action.data.UserId); break; } case UNLIKE_POST_FAILURE: draft.unLikeLoading = false draft.unLikeDone = true break; case UPLOAD_IMAGES_REQUEST: draft.upLoadImagesLoadding = true draft.upLoadImagesDone = false draft.upLoadImagesErr = null break; case UPLOAD_IMAGES_SUCCESS: draft.upLoadImagesLoadding = true draft.upLoadImagesDone = false draft.imagePath = action.data break; case UPLOAD_IMAGES_FAILURE: draft.upLoadImagesLoadding = false draft.upLoadImagesErr = action.error break; default: return state } }) } export default reducerconst express = require('express') const multer = require('multer') const path = require('path') const fs = require('fs') const { Post, Comment, Image, User, Hashtag } = require('../models') const { isLoggedIn } = require('./middlewares') const user = require('../models/user') const router = express.Router(); try { fs.accessSync('uploads') } catch (error) { console.log('폴더가 없으므로 생성합니다.') fs.mkdirSync('uploads') } const upload = multer({ storage: multer.diskStorage({ destination(req, file, done) { done(null, 'uploads'); }, filename(req, file, done) { // 제로초.png const ext = path.extname(file.originalname); // 확장자 추출(.png) const basename = path.basename(file.originalname, ext); // 제로초 done(null, basename + '_' + new Date().getTime() + ext); // 제로초15184712891.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() }, }))); // [[노드, true], [리액트, true]] await post.addHashtags(result.map((v) => v[0])); } if (req.body.image) { if (Array.isArray(req.body.image)) { // 이미지를 여러 개 올리면 image: [제로초.png, 부기초.png] const images = await Promise.all(req.body.image.map((image) => Image.create({ src: image }))); await post.addImages(images); } else { // 이미지를 하나만 올리면 image: 제로초.png 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', attributes: ['id'], }] }) res.status(201).json(fullPost) } catch (error) { console.error(error) next(error) } }) router.delete('/:PostId', isLoggedIn, async (req, res, next) => { try { await Post.destroy({ where: { id: req.params.PostId, UserId: req.user.id }, }) res.status(201).json({ PostId: parseInt(req.params.PostId, 10) }) } catch (error) { console.error(error) next(error) } }) router.post('/:postId/comment', isLoggedIn, async (req, res) => { try { const post = await Post.findOne({ where: { id: req.params.postId } }) if (!post) { return res.status(403).send('존재하지 않는 게시글 입니다.') } const commnet = await Comment.create({ content: req.body.content, PostId: Number(req.params.postId), UserId: req.user.id, }) const fullComment = await Comment.findOne({ where: { id: commnet.id }, include: [{ model: User, attributes: ['id', 'nickname'] }] }) res.status(201).json(fullComment) } catch (error) { console.error(error) next(error) } }) router.get('/:postId', async (req, res, next) => { try { const post = await Post.findOne({ where: { id: req.params.postId }, }) if (!post) { return res.status(404).send('존재하지않는 게시글입니다..') } const fullPost = await Post.findOne({ where: { id: post.id }, include: [{ model: Post, as: 'Retweet', include: [{ model: User, attributes: ['id', 'nickname'] }, { model: Image, }] }, { model: User, attributes: ['id', 'nickname'] }, { model: User, as: 'Likers', attributes: ['id', 'nickname'] }, { model: Image, }, { model: Comment, include: [{ model: User, attributes: ['id', 'nickname'] }] }] }) res.status(200).json(fullPost) } catch (err) { console.error(err) next(err) } }); 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/like', (req, res, next) => { }) router.post('/images', isLoggedIn, upload.array('image'), (req, res, next) => { res.json(req.files.map((item) => item.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: User, // 좋아요 누른 사람 as: 'Likers', attributes: ['id'], }, { model: Image, }, { model: Comment, include: [{ model: User, attributes: ['id', 'nickname'], }], }], }) res.status(201).json(retweetWithPrevPost); } catch (error) { console.error(error); next(error); } }); module.exports = router;
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
서버 사이드랜더링 후 오류 관련 질문 드립니다.
Server ErrorError: Error serializing .initialState.user.loadMyInfoErr returned from getServerSideProps in "/". Reason: undefined cannot be serialized as JSON. Please use null or omit this value all together.선생님 해당 애러 한시간 넘게 지금 찾고있는데 일단 번역해보거나 지금 다른분이 올리셨던 질문이 똑같은게 있었는데 도움이 되질않았습니다. 보니 직렬화 문제? 그리고 initialState.user.loadMyInfoErr이쪽이 문제였던것같아 리듀서에 loadMyInfoerr를 그냥 없애보니 화면랜더링은 되는데 로그인이 안되는 게 발생했습니다. 일단 선생님께 질문드리고 난뒤에도 계속 찾아보겠습니다.//index.js import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { END } from 'redux-saga' import AppLayout from '../components/AppLayout' import PostForm from '../components/PostForm' import PostCard from '../components/PostCard' import { LOAD_POSTS_REQUEST } from '../reducers/post' import { LOAD_MYINFO_REQUEST } from '../reducers/user' import wrapper from '../store/configureStore' import axios from 'axios' const Home = () => { const dispatch = useDispatch(); const { me, logInErr } = useSelector((state) => state.user) const { mainPosts, hasMorePosts, loadPostsLoading, retweetErr } = useSelector((state) => state.post) useEffect(() => { if (retweetErr) { alert(retweetErr) } }, [retweetErr]) useEffect(() => { function onScroll() { if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) { if (hasMorePosts && !loadPostsLoading) { const lastId = mainPosts[mainPosts.length - 1]?.id; dispatch({ type: LOAD_POSTS_REQUEST, lastId, }); } } } window.addEventListener('scroll', onScroll); return () => { window.removeEventListener('scroll', onScroll); }; }, [hasMorePosts, loadPostsLoading, mainPosts]) return ( <AppLayout> {me && <PostForm />} {mainPosts.map((item) => <PostCard key={item.id} post={item} />)} </AppLayout> ) } export const getServerSideProps = wrapper.getServerSideProps( async (context) => { context.store.dispatch({ type: LOAD_MYINFO_REQUEST, }); context.store.dispatch({ type: LOAD_POSTS_REQUEST, }); context.store.dispatch(END); await context.store.sagaTask.toPromise(); }) export default Homeimport { delay, all, fork, put, takeLatest, call } from "redux-saga/effects"; import axios from 'axios' import { LOG_IN_REQUEST, LOG_IN_SUCCESS, LOG_IN_FAILURE, LOG_OUT_REQUEST, LOG_OUT_SUCCESS, LOG_OUT_FAILURE, SIGN_UP_REQUEST, SIGN_UP_SUCCESS, SIGN_UP_FAILURE, FOLLOW_REQUEST, FOLLOW_SUCCESS, FOLLOW_FAILURE, UNFOLLOW_REQUEST, UNFOLLOW_SUCCESS, UNFOLLOW_FAILURE, LOAD_USER_REQUEST, LOAD_USER_SUCCESSS, LOAD_USER_FAILURE, CHANGE_NICK_REQUEST, CHANGE_NICK_SUCCESS, CHANGE_NICK_FAILURE, LOAD_FOLLOWER_REQUEST, LOAD_FOLLOWER_SUCCESS, LOAD_FOLLOWER_FAILURE, LOAD_FOLLWING_REQUEST, LOAD_FOLLWING_SUCESSS, LOAD_FOLLWING_FAILURE, REMOVE_FOLLOWER_REQUEST, REMOVE_FOLLOWER_SUCCESS, REMOVE_FOLLOWER_FAILURE, LOAD_MYINFO_REQUEST, LOAD_MYINFO_SUCCESSS, LOAD_MYINFO_FAILURE } from '../reducers/user' function loadMyInfoAPI() { return axios.get('/user') } function* loadMyInfo() { try { const result = yield call(loadMyInfoAPI) yield put({ type: LOAD_MYINFO_SUCCESSS, data: result.data, }); } catch (err) { console.log(err); yield put({ type: LOAD_MYINFO_FAILURE, error: err.response.data }); } } function getUserAPI(data) { return axios.get(`/user/${data}`) } function* getUser(action) { try { const result = yield call(getUserAPI, action.data) yield put({ type: LOAD_USER_SUCCESSS, data: result.data, }); } catch (err) { yield put({ type: LOAD_USER_FAILURE, error: err.response.data }); } } function getFollwerAPI(data) { return axios.get('/user/follower', data) } function* getFollwer(action) { try { const result = yield call(getFollwerAPI, action.data) yield put({ type: LOAD_FOLLOWER_SUCCESS, data: result.data, }); } catch (err) { yield put({ type: LOAD_FOLLOWER_FAILURE, error: err.response.data }); } } function getFollowingAPI(data) { return axios.get('/user/following', data) } function* getFollowing(action) { try { const result = yield call(getFollowingAPI, action.data) yield put({ type: LOAD_FOLLWING_SUCESSS, data: result.data, }); } catch (err) { yield put({ type: LOAD_FOLLWING_FAILURE, error: err.response.data }); } } function logInAPI(data) { return axios.post('/user/login', data) } function* logIn(action) { try { const result = yield call(logInAPI, action.data) yield put({ type: LOG_IN_SUCCESS, data: result.data, }); } catch (err) { yield put({ type: LOG_IN_FAILURE, error: err.response.data }); } } function logOutAPI() { return axios.post('/user/logout'); } function* logOut() { try { yield call(logOutAPI); yield put({ type: LOG_OUT_SUCCESS, }); } catch (err) { console.error(err); yield put({ type: LOG_OUT_FAILURE, error: err.response.data, }); } } function signUpAPI(data) { return axios.post('/user', data) } function* signUp(action) { try { const result = yield call(signUpAPI, action.data); console.log(result); yield put({ type: SIGN_UP_SUCCESS, }); } catch (err) { console.error(err); yield put({ type: SIGN_UP_FAILURE, error: err.response.data, }); } } function followAPI(data) { return axios.patch(`/user/${data}/follow`) } function* follow(action) { try { const result = yield call(followAPI , action.data) yield put({ type: FOLLOW_SUCCESS, data: result.data }); } catch (err) { yield put({ type: FOLLOW_FAILURE, error: err.response.data }); } } function unFollowAPI(data) { return axios.delete(`/user/${data}/follow`) } function* unFollow(action) { try { const result = yield call(unFollowAPI, action.data) yield console.log(result) yield put({ type: UNFOLLOW_SUCCESS, data: result.data }); } catch (err) { yield put({ type: UNFOLLOW_FAILURE, error: err.response.data }); } } function chanegeNickAPI(data) { return axios.patch('/user/nickname', { nickname : data }) } function* chanegeNick(action) { try { const result = yield call(chanegeNickAPI, action.data) yield console.log(result) yield put({ type: CHANGE_NICK_SUCCESS, data: result.data, }); } catch (err) { yield put({ type: CHANGE_NICK_FAILURE, error: err.response.data }); } } function removeFollowerAPI(data) { return axios.delete(`/user/${data}/following`) } function* removeFollower(action) { try { const result = yield call(removeFollowerAPI, action.data) console.log(result) yield console.log(result) yield put({ type: REMOVE_FOLLOWER_SUCCESS, data: result.data, }); } catch (err) { yield put({ type: REMOVE_FOLLOWER_FAILURE, error: err.response.data }); } } function* watchLogIn() { yield takeLatest(LOG_IN_REQUEST, logIn) } function* watchLogOut() { yield takeLatest(LOG_OUT_REQUEST, logOut) } function* watchSignUp() { yield takeLatest(SIGN_UP_REQUEST, signUp) } function* watchFollow() { yield takeLatest(FOLLOW_REQUEST, follow) } function* watchUnFollow() { yield takeLatest(UNFOLLOW_REQUEST, unFollow) } function* watchGetUser() { yield takeLatest(LOAD_USER_REQUEST, getUser) } function* watchGetFollow() { yield takeLatest(LOAD_FOLLOWER_REQUEST, getFollwer) } function* watchGetFollowing() { yield takeLatest(LOAD_FOLLWING_REQUEST, getFollowing) } function* watchChanegeNick() { yield takeLatest(CHANGE_NICK_REQUEST, chanegeNick) } function* watchRemoveFollower(){ yield takeLatest(REMOVE_FOLLOWER_REQUEST, removeFollower) } function* watchLoadMyInfo(){ yield takeLatest(LOAD_MYINFO_REQUEST, loadMyInfo) } export default function* userSaga() { yield all([ fork(watchLogIn), fork(watchLoadMyInfo), fork(watchLogOut), fork(watchLogOut), fork(watchSignUp), fork(watchFollow), fork(watchUnFollow), fork(watchRemoveFollower), fork(watchGetUser), fork(watchChanegeNick), fork(watchGetFollow), fork(watchGetFollowing), ]) }const express = require('express') const bcrypt = require('bcrypt') const { User, Post, Image, Comment } = require('../models') const { isLoggedIn, isNotLoggedIn } = require('./middlewares') const passport = require('passport'); const db = require('../models'); const router = express.Router(); router.get('/', async (req, res, next) => { // GET /user try { if (req.user) { const fullUserWithoutPassword = await User.findOne({ where: { id: req.user.id }, attributes: { exclude: ['password'] }, include: [{ model: Post, attributes: ['id'], }, { model: User, as: 'Followings', attributes: ['id'], }, { model: User, as: 'Followers', attributes: ['id'], }] }) res.status(200).json(fullUserWithoutPassword); } else { res.status(200).json(null); } } catch (error) { console.error(error); next(error); } }); router.post('/login', isNotLoggedIn, (req, res, next) => { passport.authenticate('local', (err, user, info) => { if (err) { console.error(err) return next(err) } if (info) { return res.status(401).send(info.reason); } return req.login(user, async (loginErr) => { if (loginErr) { console.error(loginErr) return next(loginErr) } const fullUserWithoutPassword = await User.findOne({ where: { id: user.id }, attributes: { exclude: ['password'] }, include: [{ model: Post, }, { model: User, as: 'Followings', }, { model: User, as: 'Followers', }] }) return res.status(200).json(fullUserWithoutPassword) }) })(req, res, next) }) router.post('/logout', isLoggedIn, (req, res) => { req.logout(); req.session.destroy(); res.send('OK') }) router.post('/', isNotLoggedIn, async (req, res, next) => { try { const exUser = await User.findOne({ where: { email: req.body.email, } }); if (exUser) { return res.status(403).send('이미 사용 중인 아이디입니다.'); } const hashedPassword = await bcrypt.hash(req.body.password, 12); await User.create({ email: req.body.email, nickname: req.body.nick, password: hashedPassword, }); res.status(201).send('ok'); } catch (error) { console.error(error); next(error); // status 500 } }) router.patch('/nickname', isLoggedIn, async (req, res, next) => { try { console.log(req.body) await User.update({ nickname: req.body.nickname }, { where: { id: req.user.id } }) res.status(200).json({ nickname: req.body.nickname }) } catch (error) { console.log(error) next(error) } }) router.patch('/:userId/follow', isLoggedIn, async (req, res, next) => { // 유저 팔로우 try { const user = await User.findOne({ where: { id: req.params.userId } }) // 게시글 작성자 1번 if (!user) { res.status(403).send('없는 유저입니다.') } await user.addFollowers(req.user.id); // 게시글 작성자를 팔로우한다 / 내 계정 정보를 넘김 // 팔로워 아이디 2번 res.status(200).json({ userId: parseInt(req.params.userId, 10) }) } catch (error) { console.log(error) next(error) } }) router.delete('/:userId/follow', isLoggedIn, async (req, res, next) => { // 유저 팔로우 취소 try { const user = await User.findOne({ where: { id: req.params.userId } }) if (!user) { res.status(403).send('없는 유저입니다.') } await user.removeFollowers(req.user.id); res.status(200).json({ userId: parseInt(req.params.userId, 10) }) } catch (error) { console.log(error) next(error) } }) router.delete('/:userId/following', isLoggedIn, async (req, res, next) => { // 유저 팔로우 취소 try { const user = await User.findOne({ where: { id: req.params.userId } }) if (!user) { res.status(403).send('없는 유저입니다.') } await user.removeFollowings(req.user.id); res.status(200).json({ userId: parseInt(req.params.userId, 10) }) } catch (error) { console.log(error) next(error) } }) router.get('/follower', isLoggedIn, async (req, res, next) => { try { const user = await User.findOne({ where: { id: req.user.id } }) if (!user) { res.status(403).send('없는 유저입니다.') } const followers = await user.getFollowers(); res.status(200).json(followers) } catch (error) { console.log(error) next(error) } }) router.get('/following', isLoggedIn, async (req, res, next) => { try { const user = await User.findOne({ where: { id: req.user.id } }) if (!user) { res.status(403).send('없는 유저입니다.') } const followings = await user.getFollowings(); res.status(200).json(followings) } catch (error) { console.log(error) next(error) } }) module.exports = router;
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
TypeError: Cannot read properties of undefined (reading 'user')
마지막에 npm run dev 하고 페이지를 열면 이런 에러가 뜹니다.AppLayout.js이쪽 user부분에 문제가 있는건가요? 어느부분에서 오류가 났는지 소스코드도 안나와 있고,user 부분에 무슨 문제가 있는건지 도무지 알수가 없네요 ㅠㅠ
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
테이블 관계, 관계 메서드에 대해서 질문드리겠습니다.
DB는 처음다뤄봐서인지 이전 테이블관계 강의와 해당 강의를 몇번을 회독했는데도 이해가 가지않거나, 궁금한점이 있어서 질문드립니다. 테이블끼리 관계를 설정하고, 관계 데이터를 사용하는 이유가 무엇인가요? include만 사용하는 것과 어떤 차이가 있는지 궁금해서 질문드립니다. 강의에서 관계를 아래와 같이 설정한 뒤 User는 'Likers', Post는 'Liked'라는 별칭을 지정하셨는데, 별칭의 사용이유가 다른 관계와의 구분외에 또 있을까요? 뭔가 이 부분의 사용 이유가 정확하게 이해가 안되서 질문드립니다 ㅜㅜdb.Post.belongsToMany(db.User, { through: 'Like', as: 'Likers' }) db.User.belongsToMany(db.Post, { through: 'Like', as: 'Liked' }) 아래 식은 Likers(User)테이블에서 유저ID(req.user.id)를 찾아 해당 데이터를 Post 테이블에 추가한다는 뜻을 이해했는데 맞을까요?await post.addLikers(req.user.id); 좋아요 기능을 구현한 뒤 Post와 User의 관계에서 생성된 Like 매핑테이블을 확인해보니 다음과 같이 데이터가 추가되었던데 혹시 어느 부분에서 매핑테이블에 데이터가 추가된건가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
backend 쪽 친구의 springboot 과 연결하고싶습니다.
@RequestMapping("/v1/users") @RestController public class UserController { private final UserService userService; private final KakaoApiService kakaoApiService; private final PostService postService; @GetMapping("/email") public ResponseEntity<Boolean> isEmailDuplicated(@RequestParam String email) { return ResponseEntity.ok(userService.isEmailDuplicated(email)); } @PostMapping("/signup") public UserResponse join(@RequestBody @Valid UserCreateRequest request) { UserDto userDto = userService.createUser(request.toDto()); return UserResponse.from(userDto); } @GetMapping public List<UserResponse> getUsers() { List<UserDto> userDtos = userService.getAllUsers(); return userDtos.stream().map(UserResponse::from).collect(Collectors.toList()); } @GetMapping("/my-info") public ResponseEntity<UserProfileResponse> showMyInfo(@LoginUser SessionUser sessionUser) { UserDto userDto = userService.getUser(sessionUser.getUserId()); List<PostDto> postDtos = postService.getPostsByUser(sessionUser.getUserId()); return ResponseEntity.ok(UserProfileResponse.fromDto(userDto, postDtos)); } @PutMapping public ResponseEntity<UserResponse> modifyUser(@LoginUser SessionUser sessionUser, @RequestBody UserModifyRequest updateRequest) { UserDto userDto = userService.modifyUser(sessionUser.getUserId(), updateRequest.toDto()); return ResponseEntity.ok(UserResponse.from(userDto)); } @PutMapping("/photo") public ResponseEntity<String> modifyUserProfileImg(@LoginUser SessionUser sessionUser, @RequestPart MultipartFile photo) { return ResponseEntity.ok(userService.modifyUserProfileImg(sessionUser.getUserId(), photo)); } @GetMapping("/kakao/friends") public ResponseEntity<KakaoFriendsResponse> getKakaoFriends(@LoginUser SessionUser sessionUser) { if (Objects.nonNull(sessionUser.getAccessToken())) { throw new KakaoNotAuthenticationExcpetion("카카오 계정 인증이 필요합니다.", ErrorCode.KAKAO_NOT_AUTHENTICATION); } return ResponseEntity.ok(kakaoApiService.getKakaoFriends(sessionUser.getAccessToken())); }이게 친구 login쪽인데 제가 saga에서 저걸 받아오려면 이런식으로 받아오는게 맞을까요? function signUpAPI(data) { return axios.post('/v1/users/signup', data) };
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
reducer, saga의 실행결과 콘솔 출력 질문드립니다.
안녕하세요 제로초님 강의 잘듣고있습니다.해당 강의 18:36초에 출력된 reducer, action 내용 콘솔은 혹시 어디에서 어떤 내용을 출력해서 나온 결과일까요?{type: "LOG_OUT_SUCCESS", @@redux-saga/SAGA_ACTION: true}
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
next와 spring
안녕하세요 . 제로초님 . 제가 친구랑 토이프로젝트 중인데 친구는 spring으로 백을 구현했는데 제가 back에서 데이터를 가져와 ui를 구성하려면 어떤식으로 코드를 짜야하나요
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
사가 부분 계속 확인했는데도 로그인이 안됩니다. ㅠ
```sagas/user.ts import axios from "axios"; import { all, delay, fork, put, takeLatest } from "redux-saga/effects"; import { LOG_IN_FAILURE, LOG_IN_REQUEST, LOG_IN_SUCCESS, LOG_OUT_FAILURE, LOG_OUT_REQUEST, LOG_OUT_SUCCESS, SIGN_UP_FAILURE, SIGN_UP_REQUEST, SIGN_UP_SUCCESS, } from "../reducers/user"; function loginAPI(data: any) { return axios.post("/api/login", data); } function* logIn(action: any) { try { yield delay(1000); yield put({ type: LOG_IN_SUCCESS, data: action.data, }); } catch (error: any) { yield put({ type: LOG_IN_FAILURE, error: error.response.data, }); } } function logoutAPI() { return axios.post("/api/login"); } function* logOut() { try { yield delay(1000); yield put({ type: LOG_OUT_SUCCESS, }); } catch (error: any) { yield put({ type: LOG_OUT_FAILURE, error: error.response.data, }); } } function signUpAPI() { return axios.post("/api/signUp"); } function* signUp() { try { yield delay(1000); yield put({ type: SIGN_UP_SUCCESS, }); } catch (error: any) { yield put({ type: SIGN_UP_FAILURE, error: error.response.data, }); } } function* watchLogin() { console.log("사가로그인 리스너 "); yield takeLatest(LOG_IN_REQUEST, logIn); } function* watchLogOut() { yield takeLatest(LOG_OUT_REQUEST, logOut); } function* watchSignUp() { yield takeLatest(SIGN_UP_REQUEST, signUp); } export default function* userSaga() { yield all([fork(watchLogin), fork(watchLogOut), fork(watchSignUp)]); } ``` ```sagas/index.ts import { all, fork } from "redux-saga/effects"; import postSaga from "./post"; import userSaga from "./user"; export default function* rootSaga() { yield all([fork(postSaga), fork(userSaga)]); } ``` ```reducers/user.ts export const initialState = { logInLoading: false, logInDone: false, logInError: null, logOutLoading: false, logOutDone: false, logOutError: null, signUpLoading: false, signUpDone: false, signUpError: null, changeNicknameLoading: false, changeNicknameDone: false, changeNicknameError: null, me: null, signUpData: {}, loginData: {}, }; export const LOG_IN_REQUEST = "LOG_IN_REQUEST"; export const LOG_IN_SUCCESS = "LOG_IN_SUCCESS"; export const LOG_IN_FAILURE = "LOG_IN_FAILURE"; export const LOG_OUT_REQUEST = "LOG_OUT_REQUEST"; export const LOG_OUT_SUCCESS = "LOG_OUT_SUCCESS"; export const LOG_OUT_FAILURE = "LOG_OUT_FAILURE"; export const SIGN_UP_REQUEST = "SIGN_UP_REQUEST"; export const SIGN_UP_SUCCESS = "SIGN_UP_SUCCESS"; export const SIGN_UP_FAILURE = "SIGN_UP_FAILURE"; export const CHANGE_NICKNAME_REQUEST = "CHANGE_NICKNAME_REQUEST"; export const CHANGE_NICKNAME_SUCCESS = "CHANGE_NICKNAME_SUCCESS"; export const CHANGE_NICKNAME_FAILURE = "CHANGE_NICKNAME_FAILURE"; export const FOLLOW_REQUEST = "FOLLOW_REQUEST"; export const FOLLOW_SUCCESS = "FOLLOW_SUCCESS"; export const FOLLOW_FAILURE = "FOLLOW_FAILURE"; export const UNFOLLOW_REQUEST = "UNFOLLOW_REQUEST"; export const UNFOLLOW_SUCCESS = "UNFOLLOW_SUCCESS"; export const UNFOLLOW_FAILURE = "UNFOLLOW_FAILURE"; const dummyUser = (data: any) => ({ ...data, nickname: "GIYEON", id: 1, Posts: [], Followings: [], Followers: [], }); //action export const loginRequestAction = (data: any) => { return { type: LOG_IN_REQUEST, data, }; }; export const logoutRequestAction = () => { return { type: LOG_OUT_REQUEST, }; }; const reducer = (state: any = initialState, action: any) => { switch (action.type) { case LOG_IN_REQUEST: return { ...state, logInLoading: true, logInError: null, logInDone: false, }; case LOG_IN_SUCCESS: return { ...state, logInLoading: false, logInDone: true, me: dummyUser(action.data), }; case LOG_IN_FAILURE: return { ...state, logInLoading: false, logInError: action.error, }; case LOG_OUT_REQUEST: return { ...state, logOutLoading: true, logOutDone: false, logOutError: null, }; case LOG_OUT_SUCCESS: return { ...state, logOutLoading: false, logOutDone: true, me: null, }; case LOG_OUT_FAILURE: return { ...state, logOutLoading: false, logOutError: action.error, }; case SIGN_UP_REQUEST: return { ...state, signUpLoading: true, signUpDone: false, signUpError: null, }; case SIGN_UP_SUCCESS: return { ...state, signUpLoading: false, signUpDone: true, }; case SIGN_UP_FAILURE: return { ...state, signUpLoading: false, signUpError: action.error, }; case CHANGE_NICKNAME_REQUEST: return { ...state, changeNicknameLoading: true, changeNicknameDone: false, changeNicknameError: null, }; case CHANGE_NICKNAME_SUCCESS: return { ...state, changeNicknameLoading: false, changeNicknameDone: true, }; case CHANGE_NICKNAME_FAILURE: return { ...state, changeNicknameLoading: false, changeNicknameError: action.error, }; default: return state; } }; export default reducer; ``` logInLoading true까지 되고 로그인 무한 로딩이 걸립니다.뭐가 문제일까요 ..
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
게시물 작성 후 닉네임 변경
선생님 게시물을 작성하고 난 다음에 닉네임을 변경을 하면 새롭게 변경된 닉네임을 가진 게시물이 새로생기는데 이건 왜그런걸까요 ...
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
toolkit action.payload 질문 있습니다.
zerocho님 github /react-nodebird/toolkit/front/reducers/post.js 에서 likePost.fulfilled 질문이 있습니다!. addCase(likePost.fulfilled, (state, action) => { const post = _find(state.mainPosts, { id: action.payload.PostId }); state.likePostLoading = false; state.likePostDone = true; post.Likers.push({ id: action.payload.UserId });여기 코드에서 action.payload.postId는 어디서 전달해주는 값인지 알 수 있을까요?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
동적라우팅 seo 질문!
안녕하세요 제로초님ssr에서 동적라우팅으로 생성된 페이지들은 검색엔진이 어떻게 캐치하는 것인가요?getServerSideProps로 ssr 하는 경우 빌드 시점이 아닌 해당 페이지를 요청할때 데이터를 포함시켜서 문서를 만들텐데 검색엔진에 어떻게 노출되는지 궁금합니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요 제로초님 redux-persist 질문 드립니다.
안녕하세요 제로초님redux-toolkit에 next-redux-wrapper와 redux-persist 연결하는데 질문드립니다.import userReducer from "@slice/userSlice"; import commonSlice from "@slice/commonSlice"; import localSlice from "@slice/locals"; export const persistConfig = { key: "root", version: 0, whitelist: ["locals"], storage, }; export const rootReducer = (state, action) => { if (action.type === HYDRATE) { return { ...state, ...action.payload, }; } return combineReducers({ user: userReducer, common: commonSlice, locals: localSlice, })(state, action); }; export const reducer = persistReducer(persistConfig, rootReducer); 이렇게 초기 셋팅을 해주었고, export const store = configureStore({ reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], }, }).concat(logger), devTools: process.env.NEXT_PUBLIC_NODE_ENV !== "production", }); export const persistor = persistStore(store); const setupStore = (context: any): EnhancedStore => store; const makeStore: MakeStore<any> = (context: any) => setupStore(context); export const wrapper = createWrapper<Store>(makeStore); export default wrapper;이렇게 스토어를 만들었습니다.그런데 문제가.. persist에서 rehydrate에서는 저장된 값이 유지 되어있으나.next-redux-wrapper의 hydrate에서 초기 스테이트값을 다시 저장해버리는 문제가 있습니다.둘중 하나를 버려야 할까요? 아니면 셋팅이 잘못된걸까요?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
saga의 loadPostsAPI 함수 실행 질문드립니다.
안녕하세요 제로초님 강의잘듣고있습니다.게시글을 불러올 때 saga에서 다음과 같은 에러가 발생하면서 loadPostsAPI 함수가 실행이 안되서 질문드립니다.콘솔의 오류를 확인해보니 data속성을 읽을 수 없어서 LOAD_POSTS_FAILURE에서 error를 출력합니다.강의내용의 코드와 다른점이 없는것같은데 혹시 이문제에 대해 알 수 있을까요?참코 코드도 같이 첨부하겠습니다.reducers/post.jsconst reducer = (state = initialState, action) => { return produce(state, (draft) => { switch (action.type) { 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.firstPageLoad = false; draft.mainPosts = draft.mainPosts.concat(action.data); draft.hasMorePosts = draft.mainPosts.length < 30; break; case LOAD_POSTS_FAILURE: draft.loadPostsLoading = false; draft.loadPostsError = action.error; break;sagas/post.jsimport { all, fork, delay, put, takeLatest, call } from 'redux-saga/effects'; import shortId from 'shortid'; import axios from 'axios'; import { LOAD_POSTS_REQUEST, LOAD_POSTS_SUCCESS, LOAD_POSTS_FAILURE, } from '../reducers/post'; function loadPostsAPI() { console.log('사가 실행2'); return axios.get('/posts'); } function* loadPosts() { try { console.log('사가 실행1'); const result = yield call(loadPostsAPI, action.data); yield put({ type: LOAD_POSTS_SUCCESS, data: result.data, }) } catch(err) { yield put({ type: LOAD_POSTS_FAILURE, data: err.response.data }) } } function* watchLoadPosts() { yield takeLatest(LOAD_POSTS_REQUEST, loadPosts); } export default function* postSaga() { yield all([ fork(watchLoadPosts), ]); } routes/posts.jsconst express = require('express'); const { Post, User, Image, Comment } = require('../models'); const router = express.Router(); router.get('/', async (req, res, next) => { try { const posts = await Post.findAll({ limit: 10, order: [ ['createdAt', 'DESC'], [Comment, 'createdAt', 'DESC'], ], include: [{ model: User, attributes: ['id', 'nickname'], }, { model: Image, }, { model: Comment, include: [{ model: User, attributes: ['id', 'nickname'], }], }], }); console.log(posts); res.status(200).json(posts); } catch (error) { console.error(error); next(error); } }); module.exports = router;
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
혹시 강의에서 Next Image를 안쓰는 이유가 있을까요 ??
혹시 강의에서 Next Image를 안쓰는 이유가 있을까요 ??