월 17,600원
5개월 할부 시다른 수강생들이 자주 물어보는 질문이 궁금하신가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Global CSS cannot be imported from files
Global CSS cannot be imported from files other than your Custom <App>. Please move all global CSS imports to pages\_app.js. import 'antd/dist/antd.css'; _app.js 에 css임포트 하였는데 이러한 에러가 뜨는 이유가 있을까요 ㅠ
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
connect ECONNREFUSED 127.0.0.1:3333
고생 많으십니다. 아래처럼 npx sequelize db:create 입력시 connect ECONNREFUSED 127.0.0.1:3333으로 표시되고 있습니다. 연결되지 않았다는 뜻 같은데 해당 포트 주소가 DB 주소로 설정한 상태입니다. 워크벤치를 통해서도 해당 주소로 잘 접속되고 있고 터미널을 통해서도 mysql을 켜둔 상태입니다. config.js에도 잘 입력하여 back 서버에 git pull로 잘 적용한 상태입니다. 왜 이런걸까요??
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
map 내에서 고차함수 사용 질문 드립니다.
1) const temp ⇒ (data) => { //로직 }; <Button onClick={() => temp(data)}>버튼</Button> 2) const temp = (data) ⇒ () ⇒ { //로직 }; <Button onClick={temp(data)}>버튼</Button> 1번처럼 사용해도 기능에 문제는 없는데 2번처럼 사용하는 경우가 성능상의 문제 때문일까요? 명확한 답을 알고싶은데 구글링을 해봐도 원하는 답을 찾기가 어려워 질문 남겨봅니다... 😥
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
pages폴더의 파일을 통해 components폴더의 컴포넌트들로 이동 할 경우
안녕하세요 제로초님 제로초님 영상을 보며 조금씩 응용해가면서 개인 사이드프로젝트를 진행하고 있습니다! 하단 로그인 navbar 를 클릭하여 pages의 login.js을 거쳐 LoginForm이라는 컴포넌트로 이동을 하고 있습니다! (마이페이지, 새글작성도 마찬가지) 제로초님의 방식은 AppLayout에서 컴포넌트에 바로 setIsLoggedIn props를 전달하는 방식인데 저는 AppLayout에 컴포넌트가 아닌 각각의 <Link>로 걸어놔서 props를 어떻게 효율적으로 전달해줘야할지 고민이 됩니다 ,,! 효율적인 해결방법이 생각나지않아서 const [isLoggedIn, setIsLoggedIn] = useState(false); 을 props 전달이 필요한 각 pages 마다 정의했고, login페이지에선 로그인이 되어있지 않으면 LoginForm 컴포넌트, 되어있으면 MainPage로 이동하게끔 return 부분의 컴포넌트를 이런식으로 전달을 해줬습니다 <AppLayout> { isLoggedIn ? <MainPage setIsLoggedIn={setIsLoggedIn}/> : <LoginForm setIsLoggedIn={setIsLoggedIn}/>} </AppLayout> 로그인 상태를 다른 페이지로 이동하면 유지하지 못함 (이건 서버가 없어서 그런건가 싶기도 해요..!) const [isLoggedIn, setIsLoggedIn] = useState(false);이 로그인, 마이페이지, 새글작성에 모두 중복되서 정의되어있음 (AppLayout컴포넌트에는 정의되어있지않음) 이런 문제점이 존재해서 혼자 해결해보려고 했지만 적당한 방법을 못찾아서 문의글을 남깁니다 ㅠㅠ AppLayout.js import React, { useState } from "react"; import PropTypes from "prop-types"; import Link from "next/link"; import { Row, Col } from "antd"; import styles from '../styles/styles.module.css'; import LoginForm from './LoginForm'; import MyPage from './MyPage'; const AppLayout = ({ children }) => { return ( <> <Link href="/"> <a><h1 className={styles.logo}>놀멍쉬멍 <img src="logoIcon.png" alt="logoImage" style={{width: '24px'}} /></h1> </a> </Link> <div>{children}</div> <Row className={styles.bottomNav}> <Col xs={4}><Link href="/writePost"><a>새글작성</a></Link></Col> <Col xs={4}><Link href="/community"><a>커뮤니티</a></Link></Col> <Col xs={8}> <Link href="/"> <a><img className={styles.centerNav} src='../icon.png'/></a> </Link> </Col> <Col xs={4}><Link href="/myPage"><a>마이페이지</a></Link></Col> <Col xs={4}><Link href="/login"><a>로그인</a></Link></Col> </Row> </> ); }; AppLayout.propTypes = { children: PropTypes.node.isRequired, }; export default AppLayout; pages/login.js import React, {useState} from "react"; import AppLayout from "../components/AppLayout"; import Head from 'next/head'; import MainPage from '../components/MainPage'; import LoginForm from '../components/LoginForm'; const login = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); return ( <> <Head> <meta charSet="utf-8" /> <title>로그인 | 놀멍쉬멍</title> </Head> <AppLayout> { isLoggedIn ? <MainPage setIsLoggedIn={setIsLoggedIn}/> : <LoginForm setIsLoggedIn={setIsLoggedIn}/>} </AppLayout> </> ); }; export default login; components/LoginForm.js import React, { useCallback, useState } from 'react'; import {Form, Input, Button} from 'antd'; import Link from 'next/link'; import styles from '../styles/login.module.css'; import styled from 'styled-components'; const LoginBtn = styled(Button)` background-color: white; border: 1px solid #857171; &:hover { background-color: #857171; border: 1px solid #857171; color: white; } `; const LoginForm = ({setIsLoggedIn}) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const onChangeEmail = useCallback((e) => { setEmail(e.target.value); },[]); const onChangePassword = useCallback((e) => { setPassword(e.target.value); },[]); const onSubmitBtn = useCallback(() => { setIsLoggedIn(true); console.log(email, password); }, [email, password]); return ( <> <Form className={styles.LoginForm} onFinish={onSubmitBtn}> <div> <label htmlFor="user-email">이메일</label> <br /> <Input name="user-email" value={email} onChange={onChangeEmail} required/> </div> <div> <label htmlFor="user-password">비밀번호</label> <br /> <Input name="user-password" type="password" value={password} onChange={onChangePassword} required /> </div> <div className={styles.buttonWrapper}> <LoginBtn htmlType="submit" loading={false}>로그인</LoginBtn> < br/> <span>놀멍쉬멍이 처음이신가요?</span> <Link href="/signup"><a>회원가입</a></Link> </div> </Form> </> ); }; export default LoginForm; 폴더구조
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Link 와 a 태그 사용처
안녕하세요 열심히 수강하고있는 입문자입니다. 메뉴쪽은 링크로 href로 했고, <Link href=""><a>메뉴</a></Link> 마지막 사이트 링크 걸었을 때는 a태그를 사용했는데, <a href="">내용</a> 두 링크의 사용처에 대해 알 수 있을까요? 외부사이트 이동을 Link로 사용해도 상관 없는 부분이겠지요?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
갑자기 실행 오류가 뜨네요..
❯ npm run dev > nodebird@1.0.0 dev /Users/hangyujin/Desktop/JBBP/tutorial/nodebird/front > next ready - started server on 0.0.0.0:3000, url: http://localhost:3000 info - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5 error - ../../../../../node_modules/react-redux/es/components/Context.js:1:0 Module not found: Can't resolve 'react' Import trace for requested module: ./../../../../../node_modules/react-redux/es/exports.js ./../../../../../node_modules/react-redux/es/index.js ./node_modules/next-redux-wrapper/es6/index.js ./store/configureStore.js ./pages/_app.js https://nextjs.org/docs/messages/module-not-found 왜 그럴까요,,, nodemodules과 package-lock.json 삭제했다가 다시 npm install하고 실행해도 똑같습니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
slick 이 작동을 안합니다 !
현재 화면의 상태입니다. 제가 강의를 보면서 작성한 코드가 작동을 하지 않아서 제공되는 코드를 git에서 긁어서 넣어 봤는데도 똑같은 현상이 일어납니다. \ ImagesZoom 에서 index.js와 styles.js를 만들고 그것을 zerocho teacher가 만든 코드를 넣엇습니다. 그런데도 클릭을 하면 배경이 검은색이 되고 넘길수 있어야 하는데 그런것은 없이 맨 상단의 사진처럼 그냥 맨 위에 상세 이미지와 x버튼만 나오는 것을 확인할 수 있습니다. import React, { useState } from 'react'; import PropTypes from 'prop-types'; import Slick from 'react-slick'; import { Overlay, Header, CloseBtn, SlickWrapper, ImgWrapper, Indicator, Global } from './styles'; const ImagesZoom = ({ images, onClose }) => { const [currentSlide, setCurrentSlide] = useState(0); return ( <Overlay> <Global /> <Header> <h1>상세 이미지</h1> <CloseBtn onClick={onClose} /> </Header> <SlickWrapper> <div> <Slick initialSlide={0} beforeChange={(slide, newSlide) => setCurrentSlide(newSlide)} infinite arrows={false} slidesToShow={1} slidesToScroll={1} > {images.map((v) => ( <ImgWrapper key={v.src}> <img src={v.src} alt={v.src} /> </ImgWrapper> ))} </Slick> <Indicator> <div> {currentSlide + 1} {' '} / {images.length} </div> </Indicator> </div> </SlickWrapper> </Overlay> ); }; ImagesZoom.propTypes = { images: PropTypes.arrayOf(PropTypes.shape({ src: PropTypes.string, })).isRequired, onClose: PropTypes.func.isRequired, }; export default ImagesZoom; 위는 index.js이고 밑은 style.js입니다. import styled, { createGlobalStyle } from 'styled-components'; import { CloseOutlined } from '@ant-design/icons'; export const Global = createGlobalStyle` .slick-slide { display: inline-block; } .ant-card-cover { transform: none !important; } ` export const Overlay = styled.div` position: fixed; z-index: 5000; top: 0; left: 0; right: 0; bottom: 0; `; export const Header = styled.header` height: 44px; background: white; position: relative; padding: 0; text-align: center; & h1 { margin: 0; font-size: 17px; color: #333; line-height: 44px; } `; export const SlickWrapper = styled.div` height: calc(100% - 44px); background: #090909; `; export const CloseBtn = styled(CloseOutlined)` position: absolute; right: 0; top: 0; padding: 15px; line-height: 14px; cursor: pointer; `; export const Indicator = styled.div` text-align: center; & > div { width: 75px; height: 30px; line-height: 30px; border-radius: 15px; background: #313131; display: inline-block; text-align: center; color: white; font-size: 15px; } `; export const ImgWrapper = styled.div` padding: 32px; text-align: center; & img { margin: 0 auto; max-height: 750px; } `; 도대체 무엇을 잘못한걸까요? import를 잘못햇으면 뭐가 선언이 되지 않앗다 오류가 날텐데 이 경우에는 직접 작성한 것이 작동하지 않아 심지어 그대로 긁어온것인데 말이죠 ㅠㅠ
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
패스포트 로그인하기에서 ./passport를 찾을 수 없다고 합니다
지금 백엔드 노드 서버로 구축하기에서 패스포트로 로그인하기, 쿠키/세션 전체 로그인 흐름 이쪽 강의를 듣고 있는데 서버를 실행 시키면 ./passport 를 찾을 수 없다고 뜹니다 npm uninstall passport passport-local npm i passport passport-local 이렇게 재설치 해도 해당 모듈을 찾을 수 없다고 뜹니다 app.js passport/index.js passport/local.js package.json front/package.json { "name": "react-nordbird-front", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "next -p 3060", "build": "build next" }, "author": "heymi", "license": "ISC", "dependencies": { "@ant-design/icons": "^4.6.2", "antd": "^4.16.11", "axios": "^0.21.4", "bcrypt": "^5.0.1", "eslint-plugin-jsx-a11y": "^6.4.1", "faker": "^5.5.3", "immer": "^9.0.6", "next": "^9.5.5", "next-redux-wrapper": "^7.0.2", "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.4", "react-slick": "^0.28.1", "redux": "^4.1.1", "redux-devtools-extension": "^2.13.9", "redux-saga": "^1.1.3", "saga": "^4.0.0-alpha", "shortid": "^2.2.16", "styled-components": "^5.3.0", "update": "^0.7.4" }, "devDependencies": { "babel-eslint": "^10.1.0", "eslint": "^7.31.0", "eslint-config-airbnb": "^18.2.1", "eslint-plugin-import": "^2.24.2", "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^4.2.0" } }
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
useSelector 사용시 undefined 문제
안녕하세요, AppLayout.js 에서 const isLoggedIn = useSelector((state) => state.user.isLoggedIn); 이부분 state의 user가 undefined가 뜹니다... 어느부분을 더 확인해봐야 할까요? TypeError: Cannot read property 'user' of undefined at C:\Users\oht36\react-projects\react-nodebird\prepare\front\.next\server\pages\index.js:152:101 at useSelectorWithStoreAndSubscription (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\react-redux\lib\hooks\useSelector.js:39:30) at useSelector (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\react-redux\lib\hooks\useSelector.js:139:25) at AppLayout (C:\Users\oht36\react-projects\react-nodebird\prepare\front\.next\server\pages\index.js:152:85) at processChild (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3353:14) at resolve (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3270:5) at ReactDOMServerRenderer.render (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3753:22) at ReactDOMServerRenderer.read (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3690:29) at renderToString (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:4298:27) at Object.renderPage (C:\Users\oht36\react-projects\react-nodebird\prepare\front\node_modules\next\dist\next-server\server\render.js:50:851)
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요~
안녕하세요 강좌를 마치고 홀로 하드코딩을해 react + nodejs 게시판을 만들고 배포까지 완료하였습니다. 그중에 로그인 문제가 발생하여 질문드립니다. 3일간의 검색결과 passport사용시 secure : false 속성을 사용해야된다는 것을 알아 냈는데요 이 경우 https 두개의 서버를 각각 플론트와 백엔드로해 로그인을 하면 쿠키 전송이 되지 않거나 true로하면 passport가 동작하지 못하는 현상이 발생한것으로 추측합니다. 해결 방법을 찾지 못해 질문드립니다. 배포 사이트 : https://font-techblog.herokuapp.com/ 깃헙주소 : https://github.com/ITwoo/portfolio-react-node-blog (깃헙은 배포중인 사이트와 살짝 다를수 있습니다.) id : 1@1 password 1 로 설정해 두었습니다. 감사합니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Unhandled Runtime Error
아이디랑 비밀번호 입력하고 로그인 누르면 다음과 같은 에러가 표시 되네요 ㅜㅜ LoginForm.js UserProfile.js
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
넥스트를 좀더 알고싶어서 찾아봤습니다
넥스트를 알고싶어서 좀더 알아봤는데 파일이 스타일 파일이 생기더라구요 HTML 이랑 CSS가 익숙해서 꾸미려고하는데 꾸미는게 너무 익숙지않아서 그러는데 저가 styles 파일을 만들고 그안에 css를 만들어서 예시) import layoutStyle from '../styles/layout.css' 이렇게해서 컴포넌트안에 Applayout에 스타일을 적용을 따로 하려고하는데 안되더라구요 에러 메세지가 구글 폰트도 사용해서 넣으려고하니까 안되서 방법을 찾는게 어렵습니다 도와주세요 쌤 Global CSS cannot be imported from files other than your Custom <App>. Please move all global CSS imports to pages\_app.js. Or convert the import to Component-Level CSS (CSS Modules). Read more: https://err.sh/next.js/css-global 이렇게 뜨더라구요 어떻게 CSS를 해야할지 잘 모르겠습니다
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
'onFinish' ignored 에러 관련 질문 있습니다.
우선, 이번 강좌와 다른 내용의 질문을 드려 죄송합니다. 아래와 같이 에러 메시지가 나왔습니다. 코드는 문제가 없어 보이는데, 로그인 버튼을 클릭해도 아무런 반응이 없는 건, 아래와 같은 onFinish문제 때문일까요? antd를 다시 설치해봐도 해결이 되지 않는데 어떻게 해야 하나요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
MenuItem should not leave undefined `key`.
브라우저 콘솔 창에서 다음과 같은 에러가 뜨네요. 아마도 antd에서 제공하는 Menu.Item에 key를 넣으라는 소리 같은데.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
sequelize 랑 sequlize-cl
저 두개는 버전 6이여도 상관없나요 ? 쌤은 버전 5이시길레 여쭈어봅니다
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
제가 latest next js 버전 11에서 하고 있는데 PropTypes를 찾지 못한다고 나와요
[제로초 강좌 질문 필독 사항입니다]질문에는 여러분에게 도움이 되는 질문과 도움이 되지 않는 질문이 있습니다.도움이 되는 질문을 하는 방법을 알려드립니다.https://www.youtube.com/watch?v=PUKOWrOuC0c0. 숫자 0부터 시작한 이유는 1보다 더 중요한 것이기 때문입니다. 에러가 났을 때 해결을 하는 게 중요한 게 아닙니다. 왜 여러분은 해결을 못 하고 저는 해결을 하는지, 어디서 힌트를 얻은 것이고 어떻게 해결한 건지 그걸 알아가셔야 합니다. 그렇지 못한 질문은 무의미한 질문입니다.1. 에러 메시지를 올리기 전에 반드시 스스로 번역을 해야 합니다. 번역기 요즘 잘 되어 있습니다. 에러 메시지가 에러 해결 단서의 90%를 차지합니다. 한글로 번역만 해도 대부분 풀립니다. 그냥 에러메시지를 올리고(심지어 안 올리는 분도 있습니다. 저는 독심술사가 아닙니다) 해결해달라고 하시면 아무런 도움이 안 됩니다.2. 에러 메시지를 잘라서 올리지 않아야 합니다. 입문자일수록 에러메시지에서 어떤 부분이 가장 중요한 부분인지 모르실 겁니다. 그러니 통째로 올리셔야 합니다.3. 코드도 같이 올려주세요. 다만 코드 전체를 다 올리거나, 깃헙 주소만 띡 던지지는 마세요. 여러분이 "가장" 의심스럽다고 생각하는 코드를 올려주세요. 4. 이 강좌를 바탕으로 여러분이 응용을 해보다가 막히는 부분, 여러 개의 선택지 중에서 조언이 필요한 부분, 제 경험이 궁금한 부분에 대한 질문은 대환영입니다. 다만 여러분의 회사 일은 질문하지 마세요.5. 강좌 하나 끝날 때마다 남의 질문들을 읽어보세요. 여러분이 곧 만나게 될 에러들입니다.6. 위에 적은 내용을 명심하지 않으시면 백날 강좌를 봐도(제 강좌가 아니더라도) 실력이 늘지 않고 그냥 코딩쇼 관람 및 한컴타자연습을 한 셈이 될 겁니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Cannot read property 'Likers' of null
고생이 많으십니다. 위 에러가 났는데 찾기가 너무 어려워서 올렸습니다. 찾는다고 찾았는데 두개의 이메일로 각각 게시글을 10개씩 썼는데도 막상 해당 post 주소로 들어가게 되면 이렇게 존재하지 않는 게시글(404)이라고 표시되고 있습니다. 그리고 콘솔창에 계속해서 Internal Server Error 500 에러 렌더링이 반복되고 있습니다. 코드는 routes/ post.js 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) { // 제로초.png const ext = path.extname(file.originalname) // 확장자 추출(.png) const basename = path.basename(file.originalname, ext) // 제로초 done(null, basename + '_' + new Date().getTime() + ext) // 제로초15134314.png } }), limits: { fileSize: 20 * 1024 * 1024 } // 20mb }) router.post('/', isLoggedIn, upload.none(), async (req, res) => { // 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.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/comment', isLoggedIn, async (req, res, next) => { // POST /post/1/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: 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.get('/:postId', async (req, res, next) => { // GET /post/1 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: User, attributes: ['id', 'nickname'] }, { model: Image }, { model: Comment, include: [{ model: User, attributes: ['id', 'nickname'] }] }, { model: User, as: 'Likers', attributes: ['id'] }, { model: Post, as: 'Retweet', include: [{ model: User, attributes: ['id', 'nickname'] }, { model: Image }] }] }) res.status(200).json(fullPost) } catch (error) { console.error(error) next(error) } }) 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: User, attributes: ['id', 'nickname'] }, { model: Image }, { model: Comment, include: [{ model: User, attributes: ['id', 'nickname'] }] }, { model: User, as: 'Likers', attributes: ['id'] }, { model: Post, as: 'Retweet', include: [{ model: User, attributes: ['id', 'nickname'] }, { model: Image }] }] }) res.status(201).json(retweetWithPrevPost) } 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.json({ PostId: parseInt(req.params.postId, 10) }) } catch (error) { console.error(error) next(error) } }) module.exports = router reducers/ post.js import produce from 'immer' export const initialState = { mainPosts: [], singlePost: null, imagePaths: [], hasMorePosts: true, likePostLoading: false, likePostDone: false, likePostError: null, unlikePostLoading: false, unlikePostDone: false, unlikePostError: null, loadPostLoading: false, loadPostDone: false, loadPostError: 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' export const UPLOAD_IMAGES_SUCCESS = 'UPLOAD_IMAGES_SUCCESS' export const UPLOAD_IMAGES_FAILURE = 'UPLOAD_IMAGES_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 UNLIKE_POST_REQUEST = 'UNLIKE_POST_REQUEST' export const UNLIKE_POST_SUCCESS = 'UNLIKE_POST_SUCCESS' export const UNLIKE_POST_FAILURE = 'UNLIKE_POST_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 LOAD_POST_REQUEST = 'LOAD_POST_REQUEST' export const LOAD_POST_SUCCESS = 'LOAD_POST_SUCCESS' export const LOAD_POST_FAILURE = 'LOAD_POST_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 RETWEET_REQUEST = 'RETWEET_REQUEST' export const RETWEET_SUCCESS = 'RETWEET_SUCCESS' export const RETWEET_FAILURE = 'RETWEET_FAILURE' export const REMOVE_IMAGE = 'REMOVE_IMAGE' export const addPost = (data) => ({ type: ADD_POST_REQUEST, data }) export const addComment = (data) => ({ type: ADD_COMMENT_REQUEST, data }) const reducer = (state = initialState, action) => { return produce(state, (draft) => { switch (action.type) { case REMOVE_IMAGE: draft.imagePaths = draft.imagePaths.filter((v, i) => i !== action.data) break case RETWEET_REQUEST: draft.retweetLoading = true draft.retweetDone = false draft.retweetError = null break case RETWEET_SUCCESS: { draft.retweetLoading = false draft.retweetDone = true draft.mainPosts.unshift(action.data) break } case RETWEET_FAILURE: draft.retweetLoading = false draft.retweetError = action.error 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.likePostLoading = false draft.likePostDone = true break } case UNLIKE_POST_FAILURE: draft.unlikePostLoading = false draft.unlikePostError = action.error break case LOAD_POST_REQUEST: draft.loadPostLoading = true draft.loadPostDone = false draft.loadPostError = null break case LOAD_POST_SUCCESS: draft.singlePost = action.data draft.loadPostLoading = false draft.loadPostDone = true break case LOAD_POST_FAILURE: draft.loadPostLoading = false draft.loadPostError = action.error break case LOAD_POSTS_REQUEST: draft.loadPostsLoading = true draft.loadPostsDone = false draft.loadPostsError = null break case LOAD_POSTS_SUCCESS: draft.mainPosts = draft.mainPosts.concat(action.data) draft.loadPostsLoading = false draft.loadPostsDone = true 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.mainPosts.unshift(action.data) draft.addPostLoading = false draft.addPostDone = true 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 default: break } }) } export default reducer sagas/ post.js import axios from 'axios' import { all, delay, put, takeLatest, fork, throttle, call } from "redux-saga/effects"; import shortId from 'shortid'; import { ADD_POST_FAILURE, ADD_POST_SUCCESS, ADD_COMMENT_SUCCESS, ADD_COMMENT_FAILURE, ADD_POST_REQUEST, ADD_COMMENT_REQUEST, REMOVE_POST_FAILURE, REMOVE_POST_REQUEST, REMOVE_POST_SUCCESS, LOAD_POSTS_REQUEST, LOAD_POSTS_SUCCESS, LOAD_POSTS_FAILURE, LIKE_POST_REQUEST, UNLIKE_POST_REQUEST, LIKE_POST_SUCCESS, LIKE_POST_FAILURE, 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_OF_ME } from '../reducers/user'; function addPostAPI(data) { return axios.post('/post', data) } function* addPost(action) { try { const result = yield call(addPostAPI, action.data) const id = shortId.generate() yield put({ type: ADD_POST_SUCCESS, data: result.data }) yield put({ type: ADD_POST_TO_ME, data: result.data.id }) } catch (err) { yield put({ type: ADD_POST_FAILURE, error: err.response.data }) } } function retweetAPI(data) { return axios.post(`/post/${data}/retweet`, data) } function* retweet(action) { try { const result = yield call(retweetAPI, action.data) yield put({ type: RETWEET_SUCCESS, data: result.data }) } catch (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) { 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) { 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) { 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) { yield put({ type: LOAD_POSTS_FAILURE, error: 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) { yield put({ type: LOAD_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) { 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(error) 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* watchAddPost() { yield takeLatest(ADD_POST_REQUEST, addPost) } function* watchLoadPosts() { yield throttle(5000, LOAD_POSTS_REQUEST, loadPosts) } function* watchLoadPost() { yield takeLatest(LOAD_POST_REQUEST, loadPost) } 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(watchLoadPost), fork(watchRemovePost), fork(watchAddComment) ]) } pages/post/ [id].js import axios from "axios" import { useRouter } from "next/router" import { useSelector } from "react-redux" import { END } from 'redux-saga' import AppLayout from "../../components/AppLayout" import PostCard from "../../components/PostCard" import { LOAD_POST_REQUEST } from "../../reducers/post" import { LOAD_MY_INFO_REQUEST } from "../../reducers/user" import wrapper from "../../store/configureStore" const Post = () => { const router = useRouter() const { id } = router.query const { singlePost } = useSelector((state) => state.post) 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_MY_INFO_REQUEST }) context.store.dispatch({ type: LOAD_POST_REQUEST, data: context.params.id }) context.store.dispatch(END) await context.store.sagaTask.toPromise() }) export default Post 입니다
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
commenttForm submit 버튼 작동 에러
import { Form, Input, Button } from "antd"; import React, { useCallback, useState } from "react"; import useInput from "../hooks/useInput"; import PropTypes from "prop-types"; import { useSelector } from "react-redux"; const CommentForm = ({ post }) => { const id = useSelector((state) => state.user.me?.id); const [commentText, onChangeCommentText] = useInput(""); const onSubmitComment = useCallback(() => { console.log(post.id, commentText); }, [commentText]); return ( <Form onFinish={onSubmitComment}> <Form.Item style={{ position: "relative", margin: 0 }}> <Input.TextArea value={commentText} onChange={onChangeCommentText} rows={4} /> <Button type="primary" htmlType="submit" style={{ position: "absolute", right: 0 }} > 삐약 </Button> </Form.Item> </Form> ); }; CommentForm.propTypes = { post: PropTypes.object.isRequired, }; export default CommentForm; 문제 button에 style객체 추가 이후 console.log가 찍히지 않는 현상 style={{ position: "absolute", right: 0 }} 여기까지 쳤을 때는 작동을 하였으나 style={{ position: "absolute", right: 0, bottom: -40 }} bottom -40 을 추가하면 작동 안함 bottom 0 은 작동이 잘되는 것을 보니 마이너스 추가시 문제가 방생하는 것 같음 왜 그럴까요?ㅠㅠ
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
http-proxy-middleware 관련해서 질문 있습니다 !
cors 모듈 이용해서 access-control-allow-credentials access-control-allow-origin 을 설정해야하는건 알겠습니다. 만약에 client에 proxy -> http-proxy-middleware를 이용해서 세팅하면 cors는 잡히는데 credentials는 따로 셋팅할 필요가 없나요 ?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
42초에서 말씀하신 서버사이드 렌더링에 대해 질문
0:38 초가량 부근에서 서버사이드렌더링은 defaultandserver?에서 html을 데이터와 합쳐서 그려준다고 그랬죠? 라고 하신 부분이 이해가 잘되지 않아서요 styledcomponent는 style을 컴포넌트로 꾸며주는 라이브러리고 렌더링할떄 작동하므로 서버에서 렌더링 하고 내려오는 SSR의 동작원리상 서버쪽에서 적용이 안됐다고 보면 되는거 같은데요 (맞나요?) 렌더링이라는거 자체가 브라우저에서 렌더링 엔진이 코드를 읽고 DOM, CSSOM하고 렌더트리로 합친다음에 리플로우 리페인트하는 과정이라고 알고 있습니다. 그럼 무조건 호스트쪽 컴퓨터의 브라우저에서 렌더링하는게 아닌가 싶은데 서버쪽에서 '렌더링'을 미리 해준다는 것이 무슨 말인지 이해가 잘되지 않습니다. 말씀하시는 '렌더링'은 화면을 구성할 코드를 만들어줬다는 말인가요?