묻고 답해요
164만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결
게정을 인증하고 싶어요..
제가 계정을 인증할려고 해요.핸드폰이 없는 것은 아니에요.그런데 제가 핸드폰이 와이파이가 없어서(기능이) 카카오톡은 사용을 못해요.메세지로 인증할수 있다고 하는데, 메세지가 안오네요.
-
미해결Vue.js 완벽 가이드 - 실습과 리팩토링으로 배우는 실전 개념
import구문
import { fetchNewsList} from '../api/index.js'왜 import 구문에서 API 함수를 가져오는데 { } 객체처럼 가져오나요??
-
미해결[리뉴얼] React로 NodeBird SNS 만들기
이미지 첨부하고 글 작성시 500에러나는 현상
에러메세지 /post/images/postSequelizeValidationError: notNull Violation: Image.content cannot be null at InstanceValidator._validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:50:13) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async InstanceValidator._validateAndRunHooks (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:60:7) at async InstanceValidator.validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:54:12) at async model.save (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:2368:7) at async Function.create (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:1344:12) at async Promise.all (index 0) at async C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\routes\post.js:65:24/post 에러메세지 (post.js를 고쳐봐도 해결이 되지 않습니다)(읽기 쉽게 주석을 지우고 올려서 post.js:65번 줄이 아니라 if(req.body.image)부분 봐주시면 될 것 같습니다.)const express = require("express"); // const multer = require("multer"); const path = require("path"); // const fs = require("fs"); const { Post, Image, Comment, User, Hashtag } = require("../models"); const { isLoggedIn } = require("./middlewares"); const router = express.Router(); try { fs.accessSync("uploads"); } catch (error) { console.log("uploads 폴더가 없으므로 생성합니다."); fs.mkdirSync("uploads"); } const upload = multer({ storage: multer.diskStorage({ destination(req, file, done) { done(null, "uploads"); }, filename(req, file, done) { const ext = path.extname(file.originalname); // 확장자 추출(.png) const basename = path.basename(file.originalname, ext); // 사용자 done(null, basename + "_" + new Date().getTime() + ext); // 사용자14512512.png }, }), limits: { fileSize: 20 * 1024 * 1024 }, // 20MB }); router.post("/", isLoggedIn, upload.none(), async (req, res, next) => { // POST / post try { const hashtags = req.body.content.match(/#[^\s#]+/g); const post = await Post.create({ content: req.body.content, UserId: req.user.id, }); if (hashtags) { const result = await Promise.all( hashtags.map((tag) => Hashtag.findOrCreate({ where: { name: tag.slice(1).toLowerCase() }, }) ) ); await post.addHashtags(result.map((v) => v[0])); } if (req.body.image) { if (Array.isArray(req.body.image)) { const images = await Promise.all(req.body.image.map((image) => Image.create({ src: image }))); console.log(images); await post.addImages(images); } else { const image = await Image.create({ src: req.body.image }); await post.addImages(image); } } const fullPost = await Post.findOne({ where: { id: post.id }, include: [ { model: Image, }, { model: Comment, include: [ { model: User, // attributes: ["id", "nickname"], }, ], }, { model: User, // 게시글 작성자 attributes: ["id", "nickname"], }, { model: User, // 좋아요 누른사람 // 위랑 구분하기 위해 as Likers가져옴 as: "Likers", attributes: ["id"], }, ], }); res.status(201).json(fullPost); } catch (error) { console.error(error); next(error); } }); router.post("/images", isLoggedIn, upload.array("image"), (req, res, next) => { // POST /post/images console.log(req.files); res.json(req.files.map((v) => v.filename)); }); router.post("/:postId/retweet", isLoggedIn, async (req, res, next) => { // POST /post/1/retweet try { const post = await Post.findOne({ where: { id: req.params.postId }, include: [ { model: Post, as: "Retweet", }, ], }); if (!post) { return res.status(403).send("존재하지 않는 게시글입니다."); } if (req.user.id === post.UserId || (post.Retweet && post.Retweet.UserId === req.user.id)) { return res.status(403).send("자신의 글은 리트윗할 수 없습니다."); } // 리트윗한 게시글인지 const retweetTargetId = post.RetweetId || post.id; const exPost = await Post.findOne({ where: { UserId: req.user.id, RetweetId: retweetTargetId, }, }); if (exPost) { return res.status(403).send("이미 리트윗한 게시글입니다."); } const retweet = await Post.create({ UserId: req.user.id, RetweetId: retweetTargetId, content: "retweet", }); const retweetWithPrevPost = await Post.findOne({ where: { id: retweet.id }, include: [ { model: Post, as: "Retweet", include: [ { model: User, attributes: ["id", "nickname"], }, { model: Image, }, ], }, { model: User, attributes: ["id", "nickname"], }, { model: Image, }, { model: Comment, include: [ { model: User, attributes: ["id", "nickname"], }, ], }, ], }); res.status(201).json(retweetWithPrevPost); } catch (error) { console.error(error); next(error); } }); router.post("/:postId/comment", isLoggedIn, async (req, res, next) => { // POST / post/comment try { const post = await Post.findOne({ where: { id: req.params.postId }, }); if (!post) { return res.status(403).send("존재하지 않는 게시글입니다."); } const comment = await Comment.create({ content: req.body.content, // :postId에 들어감 (문자열로) PostId: parseInt(req.params.postId, 10), UserId: req.user.id, }); const fullComment = await Comment.findOne({ where: { id: comment.id }, include: [ { model: User, attributes: ["id", "nickname"], }, ], }); res.status(201).json(fullComment); } catch (error) { console.error(error); next(error); } }); router.patch("/:postId/like", isLoggedIn, async (req, res, next) => { // PATCH /post/1/like try { const post = await Post.findOne({ where: { id: req.params.postId } }); if (!post) { return res.status(403).send("게시글이 존재하지 않습니다."); } await post.addLikers(req.user.id); res.json({ PostId: post.id, UserId: req.user.id }); } catch (error) { console.error(error); next(error); } }); router.delete("/:postId/like", isLoggedIn, async (req, res, next) => { // DELETE /post/1/like try { const post = await Post.findOne({ where: { id: req.params.postId } }); if (!post) { return res.status(403).send("게시글이 존재하지 않습니다."); } await post.removeLikers(req.user.id); res.json({ PostId: post.id, UserId: req.user.id }); } catch (error) { console.error(error); next(error); } }); router.delete("/:postId", isLoggedIn, async (req, res, next) => { // DELETE / post/10 try { await Post.destroy({ where: { id: req.params.postId, UserId: req.user.id }, }); res.status(200).json({ PostId: parseInt(req.params.postId, 10) }); } catch (error) { console.error(error); next(error); } }); module.exports = router;router/post.js const express = require("express"); const cors = require("cors"); const session = require("express-session"); const cookieParser = require("cookie-parser"); const passport = require("passport"); const dotenv = require("dotenv"); const morgan = require("morgan"); const path = require("path"); const postRouter = require("./routes/post"); const postsRouter = require("./routes/posts"); const userRouter = require("./routes/user"); const db = require("./models"); const passportConfig = require("./passport"); dotenv.config(); const app = express(); db.sequelize .sync() .then(() => { console.log("db 연결 성공"); }) .catch(console.error); passportConfig(); app.use(morgan("dev")); // cors에러 해결 app.use( cors({ // https://localhost:3060에서 온 요청만 허용 origin: "http://localhost:3060", credentials: true, }) ); app.use("/", express.static(path.join(__dirname, "uploads"))); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser(process.env.COOKIE_SECRET)); app.use( session({ saveUninitialized: false, resave: false, secret: process.env.COOKIE_SECRET, }) ); app.use(passport.initialize()); app.use(passport.session()); app.get("/", (req, res) => { res.send("Hello express"); }); app.use("/post", postRouter); app.use("/posts", postsRouter); app.use("/user", userRouter); app.listen(3065, () => { console.log("서버 실행중"); }); app.jsmodule.exports = (sequelize, DataTypes) => { const Post = sequelize.define( "Post", { content: { type: DataTypes.TEXT, allowNull: false, }, }, { charser: "utf8mb4", collate: "utf8mb4_general_ci", } ); Post.associate = (db) => { db.Post.belongsTo(db.User); db.Post.belongsToMany(db.Hashtag, { through: "PostHashtag" }); db.Post.hasMany(db.Comment); db.Post.hasMany(db.Image); db.Post.belongsToMany(db.User, { through: "Like", as: "Likers" }); db.Post.belongsTo(db.Post, { as: "Retweet" }); }; return Post; };models/post.js ValidationError [SequelizeValidationError]: notNull Violation: Image.content cannot be null at InstanceValidator._validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:50:13) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async InstanceValidator._validateAndRunHooks (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:60:7) at async InstanceValidator.validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:54:12) at async model.save (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:2368:7) at async Function.create (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:1344:12) at async C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\routes\post.js:70:23 { errors: [ ValidationErrorItem { message: 'Image.content cannot be null', type: 'notNull Violation', path: 'content', value: null, origin: 'CORE', instance: [Image], validatorKey: 'is_null', validatorName: null, validatorArgs: [] } ] } SequelizeValidationError: notNull Violation: Image.content cannot be null at InstanceValidator._validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:50:13) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async InstanceValidator._validateAndRunHooks (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:60:7) at async InstanceValidator.validate (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\instance-validator.js:54:12) at async model.save (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:2368:7) at async Function.create (C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\node_modules\sequelize\lib\model.js:1344:12) at async C:\Users\TaeIl\Desktop\frontStudy\React-Nodebird\backend\routes\post.js:70:23 POST /post 500 26.161 ms - 1182터미널 에러 import produce from 'immer'; export type mainPost = { mainPosts: any, imagePaths: object[], likePostLoading: boolean, likePostDone: boolean, likePostError: boolean, unlikePostLoading: boolean, unlikePostDone: boolean, unlikePostError: boolean, addPostLoading: boolean, addPostDone: boolean, addPostError: boolean, addCommentLoading: boolean, addCommentDone: boolean, addCommentError: boolean, hasMorePosts: boolean, loadPostsLoading: boolean, loadPostsDone: boolean, loadPostsError: boolean, removePostLoading: boolean, removePostDone: boolean, removePostError: boolean, uploadImagesLoading: boolean, uploadImagesDone: boolean, uploadImagesError: boolean, retweetLoading: boolean, retweetDone: boolean, retweetError: boolean, } export const initialState: mainPost = { mainPosts: [], imagePaths: [], hasMorePosts: true, // infinite scroll likePostLoading: false, likePostDone: false, likePostError: null, unlikePostLoading: false, unlikePostDone: false, unlikePostError: null, loadPostsLoading: false, loadPostsDone: false, loadPostsError: null, addPostLoading: false, addPostDone: false, addPostError: null, removePostLoading: false, removePostDone: false, removePostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null, uploadImagesLoading: false, uploadImagesDone: false, uploadImagesError: null, retweetLoading: false, retweetDone: false, retweetError: null, } export const UPLOAD_IMAGES_REQUEST = 'UPLOAD_IMAGES_REQUEST' as const; export const UPLOAD_IMAGES_SUCCESS = 'UPLOAD_IMAGES_SUCCESS' as const; export const UPLOAD_IMAGES_FAILURE = 'UPLOAD_IMAGES_FAILURE' as const; export const LIKE_POST_REQUEST = 'LIKE_POST_REQUEST' as const; export const LIKE_POST_SUCCESS = 'LIKE_POST_SUCCESS' as const; export const LIKE_POST_FAILURE = 'LIKE_POST_FAILURE' as const; export const UNLIKE_POST_REQUEST = 'UNLIKE_POST_REQUEST' as const; export const UNLIKE_POST_SUCCESS = 'UNLIKE_POST_SUCCESS' as const; export const UNLIKE_POST_FAILURE = 'UNLIKE_POST_FAILURE' as const; export const LOAD_POSTS_REQUEST = 'LOAD_POSTS_REQUEST' as const; export const LOAD_POSTS_SUCCESS = 'LOAD_POSTS_SUCCESS' as const; export const LOAD_POSTS_FAILURE = 'LOAD_POSTS_FAILURE' as const; export const ADD_POST_REQUEST = 'ADD_POST_REQUEST' as const; export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS' as const; export const ADD_POST_FAILURE = 'ADD_POST_FAILURE' as const; export const REMOVE_POST_REQUEST = 'REMOVE_POST_REQUEST' as const; export const REMOVE_POST_SUCCESS = 'REMOVE_POST_SUCCESS' as const; export const REMOVE_POST_FAILURE = 'REMOVE_POST_FAILURE' as const; export const ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST' as const; export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS' as const; export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE' as const; export const RETWEET_REQUEST = 'RETWEET_REQUEST' as const; export const RETWEET_SUCCESS = 'RETWEET_SUCCESS' as const; export const RETWEET_FAILURE = 'RETWEET_FAILURE' as const; export const REMOVE_IMAGE = 'REMOVE_IMAGE' as const; export const addPost = (data) => ({ type: ADD_POST_REQUEST, data, }) export const addComment = (data) => ({ type: ADD_COMMENT_REQUEST, data, }) const reducer = (state: mainPost = initialState, action: any) => { return produce(state, (draft) => { switch (action.type) { case RETWEET_REQUEST: draft.retweetLoading = true; draft.retweetDone = false; draft.retweetError = null; break; case RETWEET_SUCCESS: draft.retweetLoading = false; draft.retweetDone = true; break; case RETWEET_FAILURE: draft.retweetLoading = false; draft.retweetError = action.error; break; default: break; // 서버에서 이미지를 지우고 싶으면 비동기로 만들어줘야 한다. // 서버에서 이미지를 안지우는 이유는 머신러닝 등을 위해 데이터 수집을 할 수도 있어서 case REMOVE_IMAGE: draft.imagePaths = draft.imagePaths.filter((v, i) => i !== action.data) break; case UPLOAD_IMAGES_REQUEST: draft.uploadImagesLoading = true; draft.uploadImagesDone = false; draft.uploadImagesError = null; break; case UPLOAD_IMAGES_SUCCESS: { // 데이터들 여기 저장 draft.imagePaths = action.data; draft.uploadImagesLoading = false; draft.uploadImagesDone = true; break; } case UPLOAD_IMAGES_FAILURE: draft.uploadImagesLoading = false; draft.uploadImagesError = action.error; break; case LIKE_POST_REQUEST: draft.likePostLoading = true; draft.likePostDone = false; draft.likePostError = null; break; case LIKE_POST_SUCCESS: { const post = draft.mainPosts.find((v) => v.id === action.data.PostId); post.Likers.push({ id: action.data.UserId }); draft.likePostLoading = false; draft.likePostDone = true; break; } case LIKE_POST_FAILURE: draft.likePostLoading = false; draft.likePostError = action.error; break; case UNLIKE_POST_REQUEST: draft.unlikePostLoading = true; draft.unlikePostDone = false; draft.unlikePostError = null; break; case UNLIKE_POST_SUCCESS: { const post = draft.mainPosts.find((v) => v.id === action.data.PostId); post.Likers = post.Likers.filter((v) => v.id !== action.data.UserId); draft.unlikePostLoading = false; draft.unlikePostDone = true; break; } case UNLIKE_POST_FAILURE: draft.unlikePostLoading = false; draft.unlikePostError = action.error; break; case LOAD_POSTS_REQUEST: draft.loadPostsLoading = true; draft.loadPostsDone = false; draft.loadPostsError = null; break; case LOAD_POSTS_SUCCESS: draft.loadPostsLoading = false; draft.loadPostsDone = true; draft.mainPosts = draft.mainPosts.concat(action.data); draft.hasMorePosts = action.data.length === 10; break; case LOAD_POSTS_FAILURE: draft.loadPostsLoading = false; draft.loadPostsError = action.error; break; case ADD_POST_REQUEST: draft.addPostLoading = true; draft.addPostDone = false; draft.addPostError = null; break; case ADD_POST_SUCCESS: draft.addPostLoading = false; draft.addPostDone = true; draft.mainPosts.unshift(action.data); draft.imagePaths = []; break; case ADD_POST_FAILURE: draft.addPostLoading = false; draft.addPostError = action.error; break; case REMOVE_POST_REQUEST: draft.removePostLoading = true; draft.removePostDone = false; draft.removePostError = null; break; case REMOVE_POST_SUCCESS: draft.removePostLoading = false; draft.removePostDone = true; draft.mainPosts = draft.mainPosts.filter((v) => v.id !== action.data.PostId); break; case REMOVE_POST_FAILURE: draft.removePostLoading = false; draft.removePostError = action.error; break; case ADD_COMMENT_REQUEST: draft.addCommentLoading = true; draft.addCommentDone = false; draft.addCommentError = null; break; case ADD_COMMENT_SUCCESS: { const post = draft.mainPosts.find((v) => v.id === action.data.PostId); post.Comments.unshift(action.data); draft.addCommentLoading = false; draft.addCommentDone = true; break; } case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; } }); } export default reducer;reducer/postimport { all, fork, delay, put, takeEvery, takeLatest, throttle, call } from 'redux-saga/effects'; import shortId from 'shortid'; import axios from 'axios'; import { ADD_COMMENT_FAILURE, ADD_COMMENT_REQUEST, ADD_COMMENT_SUCCESS, ADD_POST_FAILURE, ADD_POST_REQUEST, ADD_POST_SUCCESS, LIKE_POST_FAILURE, LIKE_POST_REQUEST, LIKE_POST_SUCCESS, LOAD_POSTS_FAILURE, LOAD_POSTS_REQUEST, LOAD_POSTS_SUCCESS, REMOVE_POST_FAILURE, REMOVE_POST_REQUEST, REMOVE_POST_SUCCESS, RETWEET_FAILURE, RETWEET_REQUEST, RETWEET_SUCCESS, UNLIKE_POST_FAILURE, UNLIKE_POST_REQUEST, UNLIKE_POST_SUCCESS, UPLOAD_IMAGES_FAILURE, UPLOAD_IMAGES_REQUEST, UPLOAD_IMAGES_SUCCESS, } from '../reducers/post'; import { ADD_POST_TO_ME, REMOVE_POST_OF_ME } from '../reducers/user'; function retweetAPI(data) { return axios.post(`/post/${data}/retweet`); } function* retweet(action) { try { const result = yield call(retweetAPI, action.data); yield put({ type: RETWEET_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: RETWEET_FAILURE, error: err.response.data, }); } } function uploadImagesAPI(data) { return axios.post('/post/images', data); } function* uploadImages(action) { try { const result = yield call(uploadImagesAPI, action.data); yield put({ type: UPLOAD_IMAGES_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: UPLOAD_IMAGES_FAILURE, error: err.response.data, }); } } function likePostAPI(data) { return axios.patch(`/post/${data}/like`); } function* likePost(action) { try { const result = yield call(likePostAPI, action.data); yield put({ type: LIKE_POST_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: LIKE_POST_FAILURE, error: err.response.data, }); } } function unlikePostAPI(data) { return axios.delete(`/post/${data}/like`); } function* unlikePost(action) { try { const result = yield call(unlikePostAPI, action.data); yield put({ type: UNLIKE_POST_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: UNLIKE_POST_FAILURE, error: err.response.data, }); } } function loadPostsAPI(lastId) { return axios.get(`/posts?lastId=${lastId || 0}`) } function* loadPosts(action) { try { const result = yield call(loadPostsAPI, action.lastId) yield put({ type: LOAD_POSTS_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: LOAD_POSTS_FAILURE, error: err.response.data, }) } } function addPostAPI(data) { return axios.post('/post', data) } function* addPost(action) { try { const result = yield call(addPostAPI, action.data) yield put({ type: ADD_POST_SUCCESS, data: result.data, }); yield put({ type: ADD_POST_TO_ME, data: result.data.id, }) } catch (err) { console.error(err); yield put({ type: ADD_POST_FAILURE, error: err.response.data, }) } } function removePostAPI(data) { return axios.delete(`/post/${data}`); } function* removePost(action) { try { const result = yield call(removePostAPI, action.data) yield put({ type: REMOVE_POST_SUCCESS, data: result.data, }); yield put({ type: REMOVE_POST_OF_ME, data: action.data, }) } catch (err) { console.error(err); yield put({ type: REMOVE_POST_FAILURE, error: err.response.data, }) } } function addCommentAPI(data) { return axios.post(`/post/${data.postId}/comment`, data) // POST /post/1/comment } function* addComment(action) { try { const result = yield call(addCommentAPI, action.data) yield put({ type: ADD_COMMENT_SUCCESS, data: result.data }) } catch (err) { console.error(err); yield put({ type: ADD_COMMENT_FAILURE, error: err.response.data, }) } } function* watchRetweet() { yield takeLatest(RETWEET_REQUEST, retweet); } function* watchUploadImages() { yield takeLatest(UPLOAD_IMAGES_REQUEST, uploadImages); } function* watchLikePost() { yield takeLatest(LIKE_POST_REQUEST, likePost); } function* watchUnlikePost() { yield takeLatest(UNLIKE_POST_REQUEST, unlikePost); } function* watchLoadPosts() { yield throttle(2000, LOAD_POSTS_REQUEST, loadPosts); } function* watchAddPost() { yield takeLatest(ADD_POST_REQUEST, addPost); } function* watchRemovePost() { yield takeLatest(REMOVE_POST_REQUEST, removePost); } function* watchAddComment() { yield takeLatest(ADD_COMMENT_REQUEST, addComment); } export default function* postSaga() { yield all([ fork(watchRetweet), fork(watchUploadImages), fork(watchLikePost), fork(watchUnlikePost), fork(watchAddPost), fork(watchLoadPosts), fork(watchRemovePost), fork(watchAddComment), ]) } sagas/postimport { EllipsisOutlined, HeartOutlined, HeartTwoTone, MessageOutlined, RetweetOutlined } from '@ant-design/icons'; import { Avatar, Button, Card, List, Popover, Comment } from 'antd'; import { useDispatch, useSelector } from 'react-redux'; import PostImages from './PostImages'; import PropTypes from 'prop-types'; import { useCallback, useState } from 'react'; import CommentForm from './CommentForm'; import PostCardContent from './PostCardContent'; import { LIKE_POST_REQUEST, REMOVE_POST_REQUEST, UNLIKE_POST_REQUEST, RETWEET_REQUEST } from '../reducers/post'; import FollowButton from './FollowButton'; const PostCard = ({ post }) => { const dispatch = useDispatch(); const { removePostLoading } = useSelector((state: any) => state.post); const [commentFormOpened, setCommentFormOpened] = useState<boolean>(false); const id = useSelector((state: any) => state.user.me?.id) const onLike = useCallback(() => { if (!id) { return alert('로그인이 필요합니다.') } return dispatch({ type: LIKE_POST_REQUEST, data: post.id, }) }, [id]) const onUnLike = useCallback(() => { if (!id) { return alert('로그인이 필요합니다.') } return dispatch({ type: UNLIKE_POST_REQUEST, data: post.id, }) }, [id]) const onToggleComment = useCallback(() => { if (!id) { return alert('로그인이 필요합니다.') } setCommentFormOpened((prev) => !prev) }, [id]) const onRemovePost = useCallback(() => { if (!id) { return alert('로그인이 필요합니다.') } return dispatch({ type: REMOVE_POST_REQUEST, data: post.id, }) }, [id]) const onRetweet = useCallback(() => { if (!id) { return alert('로그인이 필요합니다.') } return dispatch({ type: RETWEET_REQUEST, data: post.id, }) }, [id]) const liked = post.Likers.find((v) => v.id === id); return ( <div style={{ marginBottom: 20 }}> <Card cover={post.Images[0] && <PostImages images={post.Images} />} actions={[ <RetweetOutlined key="retweet" onClick={onRetweet} />, liked ? <HeartTwoTone twoToneColor="#eb2f96" key="heart" onClick={onUnLike} /> : <HeartOutlined key="heart" onClick={onLike} />, <MessageOutlined key="message" onClick={onToggleComment} />, <Popover key="ellipsis" content={( <Button.Group> {id && post.UserId === id ? ( <> <Button>수정</Button> <Button type="danger" loading={removePostLoading} onClick={onRemovePost}>삭제</Button> </> ) : <Button>신고</Button>} </Button.Group> )} > <EllipsisOutlined /> </Popover>, ]} // 누가 리트윗 했는지 title={post.RetweetId ? `${post.User.nickname}님이 리트윗하셨습니다.` : null} extra={id && <FollowButton post={post} />} > {post.RetweetId && post.Retweet ? (<Card cover={post.Retweet.Images[0] && <PostImages images={post.Retweet.Images} />} > <Card.Meta avatar={<Avatar>{post.Retweet.User.nickname[0]}</Avatar>} title={post.Retweet.User.nickname} description={<PostCardContent postData={post.Retweet.content} />} /> </Card>) : ( <Card.Meta avatar={<Avatar>{post.User.nickname[0]}</Avatar>} title={post.User.nickname} description={<PostCardContent postData={post.content} />} /> )} </Card> {commentFormOpened && ( <div> <List header={`${post.Comments.length}개의 댓글`} itemLayout="horizontal" dataSource={post.Comments} renderItem={(item: any) => ( <li> <Comment author={item.User.nickname} avatar={<Avatar>{item.User.nickname[0]}</Avatar>} content={item.content} /> </li> )} /> </div> )} </div > ) } PostCard.propTypes = { post: PropTypes.shape({ id: PropTypes.number, User: PropTypes.object, UserId: PropTypes.number, content: PropTypes.string, createdAt: PropTypes.string, Comments: PropTypes.arrayOf(PropTypes.any), Images: PropTypes.arrayOf(PropTypes.any), Likers: PropTypes.arrayOf(PropTypes.object), RetweetId: PropTypes.number, Retweet: PropTypes.objectOf(PropTypes.any), }).isRequired, }; export default PostCard;PostCard import { Form, Input, Button } from 'antd'; import { useCallback, useRef, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import useInput from '../hooks/useInput'; import { ADD_POST_REQUEST, REMOVE_IMAGE, UPLOAD_IMAGES_REQUEST } from '../reducers/post'; const PostForm = () => { const { imagePaths, addPostDone } = useSelector((state: any) => state.post); const dispatch = useDispatch() const imageInput = useRef<any>(); const [text, onChangeText, setText] = useInput('') useEffect(() => { if (addPostDone) { setText('') } }, [addPostDone]) const onSubmit = useCallback(() => { if (!text || !text.trim()) { return alert('게시글을 작성하세요.'); } const formData = new FormData(); imagePaths.forEach((p: string) => { formData.append('image', p); }) formData.append('content', text); return dispatch({ type: ADD_POST_REQUEST, data: formData, }); }, [text, imagePaths]) const onClickImageUpload = useCallback(() => { imageInput.current.click(); }, [imageInput.current]) const onChangeImages = useCallback((e) => { console.log('images', e.target.files); const imageFormData = new FormData(); [].forEach.call(e.target.files, (f) => { imageFormData.append('image', f); }); dispatch({ type: UPLOAD_IMAGES_REQUEST, data: imageFormData, }) }, []) const onRemoveImage = useCallback((index: number) => () => { dispatch({ type: REMOVE_IMAGE, data: index, }) }, []) return ( <Form style={{ margin: '10px 0 20px' }} encType='multipart/form-data' onFinish={onSubmit}> <Input.TextArea value={text} onChange={onChangeText} maxLength={140} placeholder="어떤 신기한 일이 있었나요?" /> <div> <input type="file" name="image" multiple hidden ref={imageInput} onChange={onChangeImages} /> <Button onClick={onClickImageUpload}>이미지 업로드</Button> <Button type="primary" style={{ float: 'right' }} htmlType="submit">짹짹</Button> </div> <div> {imagePaths.map((v: string, i: number) => ( <div key={v} style={{ display: 'inline-block' }}> <img src={`http://localhost:3065/${v}`} style={{ width: '200px' }} alt={v} /> <div> <Button onClick={onRemoveImage(i)}>제거</Button> </div> </div> ))} </div> </Form> ) } export default PostForm;PostForm POST http://localhost:3065/post 500 (Internal Server Error) dispatchXhrRequest @ xhr.js:247 xhr @ xhr.js:49 dispatchRequest @ dispatchRequest.js:51 request @ Axios.js:142 httpMethod @ Axios.js:181 wrap @ bind.js:5 addPostAPI @ post.tsx:127 runCallEffect @ redux-saga-core.esm.js:524 runEffect @ redux-saga-core.esm.js:1204 digestEffect @ redux-saga-core.esm.js:1271 next @ redux-saga-core.esm.js:1161 proc @ redux-saga-core.esm.js:1108 (익명) @ redux-saga-core.esm.js:585 immediately @ redux-saga-core.esm.js:56 runForkEffect @ redux-saga-core.esm.js:584 runEffect @ redux-saga-core.esm.js:1204 digestEffect @ redux-saga-core.esm.js:1271 next @ redux-saga-core.esm.js:1161 currCb @ redux-saga-core.esm.js:1251 takeCb @ redux-saga-core.esm.js:503 put @ redux-saga-core.esm.js:339 (익명) @ redux-saga-core.esm.js:376 exec @ redux-saga-core.esm.js:31 flush @ redux-saga-core.esm.js:87 asap @ redux-saga-core.esm.js:46 chan.put @ redux-saga-core.esm.js:375 (익명) @ redux-saga-core.esm.js:1412 dispatch @ VM285:3665 (익명) @ PostForm.tsx:33 onFinish @ Form.js:68 (익명) @ useForm.js:761 Promise.then(비동기) FormStore.submit @ useForm.js:757 onSubmit @ Form.js:119 callCallback @ react-dom.development.js:188 invokeGuardedCallbackDev @ react-dom.development.js:237 invokeGuardedCallback @ react-dom.development.js:292 invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:306 executeDispatch @ react-dom.development.js:389 executeDispatchesInOrder @ react-dom.development.js:414 executeDispatchesAndRelease @ react-dom.development.js:3278 executeDispatchesAndReleaseTopLevel @ react-dom.development.js:3287 forEachAccumulated @ react-dom.development.js:3259 runEventsInBatch @ react-dom.development.js:3304 runExtractedPluginEventsInBatch @ react-dom.development.js:3514 handleTopLevel @ react-dom.development.js:3558 batchedEventUpdates$1 @ react-dom.development.js:21871 batchedEventUpdates @ react-dom.development.js:795 dispatchEventForLegacyPluginEventSystem @ react-dom.development.js:3568 attemptToDispatchEvent @ react-dom.development.js:4267 dispatchEvent @ react-dom.development.js:4189 unstable_runWithPriority @ scheduler.development.js:653 runWithPriority$1 @ react-dom.development.js:11039 discreteUpdates$1 @ react-dom.development.js:21887 discreteUpdates @ react-dom.development.js:806 dispatchDiscreteEvent @ react-dom.development.js:4168 500 에러메세지 연거Image.content cannot be null이라는게 왜 나오는지 모르겠습니다..req.body.image에 도달하기 이전 어딘가에서 문제가 터지는거 같기는 한데 콘솔도 찍고 확인해봐도 어디 어디서 꼬인건지 못찾겠습니다..
-
미해결
Pdf 파일로도 받아볼수있나요?
전자책 기능이 불편해서요. 오늘 결제 하였습니다
-
미해결스프링 DB 2편 - 데이터 접근 활용 기술
jar 실행시 에러가 나서 질문 드립니다.
MyBatis 버전을 빌드하고java -jar Item.jar 로 실행 하면 에러가 나는데 혹시 해결 방안이 있을까요?IDE에선 잘돌아 갑니다.구글링을 해봐도 딱히 안보여 질문 남깁니다.
-
미해결냉동코더의 알기 쉬운 Modern Android Development 입문
Room과 Flow의 관계
책을 즐겨찾기를 추가하고 즐겨찾기 탭으로 갔을때 즐겨찾기 했던 책이 자동으로 Recyclerview에 추가되어있는 모습을 볼 수 있는데 이는 Flow, 즉 데이터 스트림을 사용했기 때문인거죠?? 즉 FavoriteFragment의 onViewCreated에 있는 함수가 collect 트리거를 작동시키면 자동으로 Room에 추가된 데이터가 RecyclerView에 추가되는 형식이라고 이해했습니다.근데 앱을 조금 변경하여 특정버튼을 눌러야 DB에서 데이터를 가져오는 로직으로 (즉 버튼을 눌러야 ViewModel의 getFavoriteBook을 호출하는 형식) 변경하였는데 버튼을 누르지 않아도 알아서 ViewModel에서 즐겨찾기 데이터가 갱신이 되더라고요...ㅇㅅㅇ 분명 collect 트리거를 작동시키지 않았는데요
-
미해결CSS에 날개를 달아주는 Sass (SCSS)
명령 프롬포트
여기서 도저히 다음으로 넘어가지질 않는데요 ㅠㅠ 저도 쌤이랑 동일하게 바탕화면에 sasstest 폴더를 만들었는데 바탕화면으로 나가지질 않습니다...
-
미해결빅데이터분석기사 실기대비 (R 활용)
작업형 2유형 질문
- 학습 관련 질문을 남겨주세요. 상세히 작성하면 더 좋아요! - 먼저 유사한 질문이 있었는지 검색해보세요. - 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요. 안녕하세요 선생님의 강의로 준비를 열심히하고있는 학생입니다나름 이제 방법을 터득해 1유형은 걍 데이터마님 캐글 등의 사이트 문제 반복 풀이, 2유형은 선생님의 말씀대로Str, summary 를 통한 데이터 확인 후 결측치 있을시 삭제 혹은 평균화, 팩터형으로 바꾸기도하고, level 이 너오무 많을시 팩터형 삭제 등 까지만 하고 바로 datapartion에 들어갑니다그런데 다른분들을 보니 위의 과정을 거친 후 scale도 하시고 주성분 분석 등 다양한 방법 후 데이터를 학습시키는거같은데..여기서 질문입니다. 선생님 말씀대로 위의 과정을 간단히 (왜냐하면 변수에 대해 사전지식이 없기때문) 전처리하고 데이터를 학습시키기만 하더라도 완전 고득점까진아니더라도 반타작이상은 하지않을까 싶어서요… 선생님 고견은 어떠신지 여쭙니다
-
미해결[백문이불여일타] 데이터 분석을 위한 고급 SQL
(LeetCode.627) IF문 보다 CASE문의 실행시간이 왜 더 빠른가요?
- 학습 관련 질문을 남겨주세요. 상세히 작성하면 더 좋아요! - 먼저 유사한 질문이 있었는지 검색해보세요. - 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요.CASE문으로 실행했을 때는 241ms, IF문으로 실행했을 때 280ms로, CASE문이 더 빨랐습니다.저는 CASE문의 구조가 더욱 복잡해 시간이 더 오래 걸릴 것이라 생각하고 IF문으로 돌려봤는데 오히려 이게 더 느리네요. 그 이유를 알 수 있을까요?
-
미해결마케터를 위한 구글 애널리틱스 실무
강의북 pdf 자료 요청드립니다.
안녕하세요 좋은 강연 감사합니다.원활한 학습을 위해 강의북 PDF 자료를 요청드립니다.yrjung@woowahan.com
-
미해결예제로 배우는 스프링 입문 (개정판)
예제가 이전과 다르네요
쿼리부분에서 fetch부분이 빠지고 다른식으로 검색필터를 적용하는 데 똑같이 복붙하고 firstName만 바꿨는데 쿼리쪽에서 오류나니 어떻게 바꿔야할지.. collection도 없어요ㅠ
-
미해결따라하며 배우는 리액트 테스트 [2023.11 업데이트]
es6 jest 미지원 오류 문의
안녕하세요 강의 잘 듣고 있습니다. 강의를 듣다가 axios를 설치하고 import 하는 과정에서 다음과 같은 문제가 발생했습니다. 구글링을 해보니 jest가 es6를 지원하지 않아서 발생하는 문제라고 하던데 구글링해서 찾아본 방법들은 해결이 되지 않아 문의 드립니다. FAIL src/pages/OrderPage/tests/Type.test.js ● Test suite failed to run Jest encountered an unexpected token Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax. Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration. By default "node_modules" folder is ignored by transformers. Here's what you can do: • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it. • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config. • If you need a custom transformation specify a "transform" option in your config. • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option. You'll find more details and examples of these config options in the docs: https://jestjs.io/docs/configuration For information about custom transformations, see: https://jestjs.io/docs/code-transformation Details: C:\Users\multicampus\Desktop\projects\react-test-app\react-shop-test\node_modules\axios\index.js:1 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import axios from './lib/axios.js'; ^^^^^^ SyntaxError: Cannot use import statement outside a module > 1 | import axios from 'axios'; | ^ 2 | import React, { useEffect, useState } from 'react' 3 | import { Products } from './Products'; 4 | at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1728:14) at Object.<anonymous> (src/pages/OrderPage/Type.js:1:1) at Object.<anonymous> (src/pages/OrderPage/tests/Type.test.js:2:1) at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13) at runJest (node_modules/@jest/core/build/runJest.js:404:19)
-
미해결[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
게임 서버
안녕하세요 루키스님! 어느덧 루키스님의 강의를 들은지 1년이 돼가는데요제가 이번에 졸업작품으로 2D 멀티 생존형 배틀로얄 게임을 제작해보려고 합니다!1년짜리 프로젝트구요 질문을 드린 이유는강의 처럼 서버를 직접 설계하는 것이 나을까 아니면 포톤 같은 엔진을 써서 쉽게 개발을 할까를 여쭤보고 싶었습니다!실력 향상 면에서는 서버를 직접 설계해보는 것이 낫겠지만 실무에서 원하는 것은 빠른 대처일테니까요큰 게임 회사에 취직하기 위해서 어떤 서버를 쓰는 것이 포트폴리오로 좋을까요?? 현직자의 입장이 궁금합니다!+ 아직 클라이언트 개발자로 갈지 서버로 갈지 정하지를 못한 상황입니다. 이런 상황에서 클라 서버 둘다 해봐도 괜찮은걸까요? 지금까지 개발해본 경험은 대부분 클라이언트고 서버 개발이라곤 루키스님의 강의가 전부였습니다 ㅠㅠ
-
미해결입문자를 위한, ES6+ 최신 자바스크립트 입문
삭제를 다 하고 싶은데요,,,
앞전 질문에서 앞에 추가하기도 X 버튼이 뜨게 잘 되었습니다. 감사드려요...복사하지 않고 입력을 해본다는게 그만 오타를 못찾는 바람에 안되었습니다.한가지 더 추가로 해보려고 하는데, 타깃제거를 클릭하면 추가한 목록들을 다 지우게 하고 싶어서 수정을 해보았는데,,,,추가버튼을 클릭할때 추가된 li에 item 이라는 클래스를 다 만들게 했구요 li.classList.add('item');그랬더니 추가항목에 클래스가 잘 추가된거까지는 확인을 했습니다.그런다음,,,removeTargetBtn.addEventListener('click', function(){ let targetList = document.querySelectorAll('.item'); targetList.remove(); });querrySelectorAll 로 추가된 클래스 item 를 모두 가져와서 remove를 했는데,,,,,문법상으로 틀린게 있는거 같은데....ㅠㅠ
-
해결됨갖고노는 MySQL 데이터베이스 by 얄코
강의페이지에 이메일이 어디잇나요
🛑 이곳에 질문하지 마세요!!!질문은 강의페이지에 안내드린 방식으로 이메일로 보내주세요!강의페이지에 질문은 어떻게 해야하는지 어디에 나와있는지 도저히 찾을 수가 없습니다. 급하게 배워야 하는데 시간이 너무 촉박하기도 한데 express 연동부터가 안되네요.. 터미널에서 자꾸error 라고 뜨는데 질문할 곳도 없고 ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ
-
미해결Vue.js 끝장내기 - 실무에 필요한 모든 것
안녕하세요 해당 프로젝트의 watch $route 관련 질문 드립니다.
안녕하세요 캡틴판교님스터디를 하다가 궁금한 점이 있어 글을 남깁니다. [그림-1]그림1과 같이 App.vue 내에 watch $route를 통해 페이지 라우팅의 변화를 감지하는 테스트 코드를 넣었는데 예상대로 잘 동작합니다. 문제는 다른 컴포넌트 혹은 page에서 위와 같은 코드를 작성하여도 동작하지 않는다는 것입니다. [그림-2]그림2와 같이 SignupForm.vue 컴포넌트 내에서 watch $route를 코드를 작성후 테스트 하였는데 동작하지 않더라구요... 다른 컴포넌트들에서도 같은 코드를 넣었는데 콘솔에 찍히지 않았습니다. 궁금해서 여쭤봅니다 감사합니다.
-
미해결한 입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지
안녕하세요 LocalStorage 데이터베이스 부분 오류 질문입니다.
안녕하세요 제가 이제 LocalStorage를 이용해서 하는 챕터까지 진행했는데요전부 다 코딩치고 여러가지 테스트를 해보다가 일기를 전부 삭제를 했더니 오류가 나옵니다..갑자기 화면은 아무것도 안뜨고 콘솔창에서는 Uncaught TypeError: Cannot read properties of undefined (reading 'id') 이란 에러가 뜹니다.그리고 에러위치는 App:52:1 로 dataId.current = parseInt(diaryList[0].id) + 1; 요 부분에서 에러가 나는거 같습니다.... 제가 어느부분을 잘못한걸까요??
-
미해결실전! 웹사이트제작! Step by Step! (한국소비자원 소비자시대- 레이아웃제작 기초)
CSS 맨 마지막 top 클래스 부분 질문입니다
안녕하세요.CSS 맨 마지막 top 클래스 부분 질문입니다 아래와 같이 강의에서 알려주신 대로 하면 잘 됩니다..top { float: right; margin-right: 30px; margin-bottom: 30px; width: 80px; height: 80px; border-radius: 50%; background: #454545; background-image: url(../img/arrow.JPG); background-repeat: no-repeat; background-size: 25px; background-position: top; position: relative; } .top a {display: block; width: 100%; height: 100%;} .top p { font-size: 16px; font-weight: bold; color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } 그런데, 아래와 같이 하면 안 됩니다. (.top a 부분을 따로 작성하지 않고 .top 안에서 설정함).top { float: right; margin-right: 30px; margin-bottom: 30px; width: 80px; height: 80px; border-radius: 50%; background: #454545; background-image: url(../img/arrow.JPG); background-repeat: no-repeat; background-size: 25px; background-position: top; position: relative; display: block; width: 100%; height: 100%; } .top p { font-size: 16px; font-weight: bold; color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } 아래와 같이 해도 안 됩니다.(.top p 를 .top a p 로 함).top { float: right; margin-right: 30px; margin-bottom: 30px; width: 80px; height: 80px; border-radius: 50%; background: #454545; background-image: url(../img/arrow.JPG); background-repeat: no-repeat; background-size: 25px; background-position: top; position: relative; } .top a {display: block; width: 100%; height: 100%;} .top a p { font-size: 16px; font-weight: bold; color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } 이 세 가지의 차이점을 정확하게 설명해 주시면 감사하겠습니다.
-
해결됨[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part1: C++ 프로그래밍 입문
list #2 2:40~ 임의접근
안녕하십니까! 강의 정말 잘 듣고 있습니다!다름이 아니라 list의 임의 접근 문법이 막힌 이유에서 속도차이에 대해서 뭔가 팍!하고 와닿지 않아 질문드립니다.vector같은 경우 '연속된'주소에 데이터가 저장되기 때문에 n번째 데이터는 ++연산으로 '바로 옆주소' 로 이동하여 찾는다. 그렇기 때문에 비교적 빠르게 찾을 수 있다.하지만 list의 경우 연속되지 않는 다른 어딘가에 다음 값을 저장하고 그 주소로 가는 주소값을 들고 있는데 vector와는 다르게 일일이 '주소값을 타고 워프'를 해줘야하기 때문에 시간이 많이 걸린다. 라고 이해를 했지만 위 두 경우의 속도 차이가 문법을 막을 만큼 차이가 어마어마하게 나는 건가요?!
-
미해결스프링 핵심 원리 - 기본편
findAllBean 질문입니다.
<조회한 빈이 모두 필요할 때, List, Map> 강의 4분 경입니다. <옵션 처리> 강의에서, '@Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다'고 하였는데본 강의에서는 컴파일, 실행이 모두 잘 되며policyMap = {}policies = []라고 뜹니다. 스프링 빈에 DiscountService만 등록되어 policyMap, policies에 자동 주입할 것이 없는데 왜 오류가 발생하지 않는 건가요?OrderServiceImpl에 (스프링 빈이 아닌)Member을 넣어 보니 정상적으로 오류가 발생하는데, 본 테스트에서는 왜 오류가 나지 않는지 궁금합니다.