• 카테고리

    질문 & 답변
  • 세부 분야

    풀스택

  • 해결 여부

    미해결

안녕하세요 제로초님 이미지 업로드 관련 질문이 있습니다.

23.11.04 22:28 작성 조회수 269

0

이미지 업로드를 위한 multer 강의까지 수강을 하였습니다.

이미지 업로드를 할때 uploads 폴더에도 이미지가 잘 들어가고

UPLOAD_IMAGES_SUCCESS도 잘 나오는 상황입니다.

하지만 제로초님의 화면은 제거 버튼과 비록 깨지지만 이미지가 올라간 화면이 보이는데 저는 그 부분이 나오지 않아서 이 점이 궁금하여 질문 드립니다.

import React, { useState } from "react";
import { Button, Card, Popover, Avatar, Image, List, Comment } from "antd";
import {
    RetweetOutlined,
    HeartOutlined,
    MessageOutlined,
    EllipsisOutlined,
    HeartTwoTone,
} from "@ant-design/icons";
import { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import PropTypes from "prop-types";
import PostImages from "./PostImages";
import CommentForm from "./CommentForm";
import PostCardContent from "./PostCardContent";
import {
    REMOVE_POST_REQUEST,
    LIKE_POST_REQUEST,
    UNLIKE_POST_REQUEST,
} from "../reducers/post";
import FollowButton from "./FollowButton";

const PostCard = ({ post }) => {
    //pages/index.js에서 mainPosts에서 하나씩 뜯어서 보내줌
    const dispatch = useDispatch();

    const [commentFormOpened, setCommentFormOpened] = useState(false);
    //댓글창 열지 말지

    const onLike = useCallback(() => {
        dispatch({
            type: LIKE_POST_REQUEST,
            data: post.id,
        });
    }, []); //좋아요

    const onUnlike = useCallback(() => {
        dispatch({
            type: UNLIKE_POST_REQUEST,
            data: post.id,
        });
    }, []); //좋아요 취소

    const onToggleComment = useCallback(() => {
        setCommentFormOpened((prev) => !prev);
    }, []); //폼 버튼 한번 더 누르면 폼 닫기

    const onRemovePost = useCallback(() => {
        return dispatch({
            type: REMOVE_POST_REQUEST,
            data: post.id,
        });
    }, [id]);

    const id = useSelector((state) => state.user.me?.id);
    const { removePostloading } = useSelector((state) => state.post);
    const liked = post.Likers.find((v) => v.id === id); //게시글 좋아요 누른 사람 중에 내가 있는지.

    return (
        <div style={{ marginBottom: 20 }}>
            <Card
                cover={post.Images?.[0] && <PostImages images={post.Images} />}
                //이미지가 존재한다면 PostImages를 출력
                actions={[
                    //카드 아래에 존재하는 것들
                    <RetweetOutlined key="retweet" />,
                    liked ? (
                        <HeartTwoTone twoToneColor="red" onClick={onUnlike} />
                    ) : (
                        <HeartOutlined key="heart" onClick={onLike} />
                    ),
                    <MessageOutlined onClick={onToggleComment} key="comment" />,
                    <Popover //더보기 같은 역할
                        key="more"
                        content={
                            <Button.Group>
                                {id && post.User.id === id ? (
                                    <>
                                        {/* 내가 쓴 글이면 수정, 삭제 */}
                                        <Button>수정</Button>
                                        <Button
                                            type="danger"
                                            onClick={onRemovePost}
                                            loading={removePostloading}
                                        >
                                            삭제
                                        </Button>
                                    </>
                                ) : (
                                    // 내가 쓴 글이 아니라면
                                    <Button>신고</Button>
                                )}
                            </Button.Group>
                        }
                    >
                        <EllipsisOutlined />
                    </Popover>,
                ]}
                extra={id && <FollowButton post={post} />}
            >
                <Card.Meta //프로필과 내용 등
                    avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
                    title={post.User.nickname}
                    description={<PostCardContent postData={post.content} />}
                />
            </Card>
            {commentFormOpened && (
                //commentFormOpened가 true이면 열어라
                <div>
                    {/* 어떤 게시글에 댓글을 남기는지.. */}
                    <CommentForm post={post} />
                    <List
                        header={`${post.Comments.length}개의 댓글`}
                        itemLayout="horizontal"
                        dataSource={post.Comments} //데이터는 여기서 가져와서
                        renderItem={(
                            item //이런식으로 출력한다
                        ) => (
                            <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,
        content: PropTypes.string,
        createdAt: PropTypes.string,
        Comment: PropTypes.arrayOf(PropTypes.object),
        Images: PropTypes.arrayOf(PropTypes.object),
        Likers: PropTypes.arrayOf(PropTypes.object),
    }).isRequired,
};

export default PostCard;

저의 PostCard 코드입니다 이 코드에서 cover={post.Images?.[0] && <PostImages images={post.Images} />}
이 부분이 이미지들을 출력해주는 부분이 아닌가요?? 저의 화면에는 아래처럼 나오지 않습니다.

 

답변 1

답변을 작성해보세요.

0

mainPosts 안의 post에 images 가 어떻게 되어있는지 확인해보세요.

윤채현님의 프로필

윤채현

질문자

2023.11.05

이런식으로 imagePaths안에 잘 들어있긴합니다..

imagePaths는 아무 관련이 없습니다. 현재 post는 mainPosts의 값을 반복문을 돌아 렌더링하는 겁니다. Images 안에가 빈 배열이기 때문에 이미지가 없는 겁니다. Images 안에를 채우세요.

서버에서 데이터 불러올 때 Images 까지 include 해서 쓰시면 됩니다.

윤채현님의 프로필

윤채현

질문자

2023.11.05

도저히 어디를 수정할지 모르겠네요..

혹시 힌트를 주실 수 있을까요

 

서버쪽에서 게시글 등록하는 쪽, 게시글 불러오는 쪽 수정하세요

윤채현님의 프로필

윤채현

질문자

2023.11.05

1) 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");
}

router.post("/", isLoggedIn, async (req, res, next) => {
    // 보기에는 "/"로 되어있지만 실제로는 "/post"로 되어있다.
    try {
        const post = await Post.create({
            content: req.body.content,
            UserId: req.user.id,
        });
        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("/:postId/comment", isLoggedIn, async (req, res, next) => {
    //:postId는 동적으로 바뀐다.
    //POST /post/comment
    // 보기에는 "/"로 되어있지만 실제로는 "/post"로 되어있다.
    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), //문자열로 넘어가기 때문에 int형으로 바꿔줘야한다.
            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);
    }
});

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(
    "/images",
    isLoggedIn,
    upload.array("image"), //PostForm.js에서input에 올린 이미지가 배열로 들어감 (이미지를 여러장 올릴 수 있게 하기 위해서)
    (req, res, next) => {
        // POST /post/images/
        //이곳은 이미지 업로드 후 실행되는 부분, 업로드는 위에 upload에서 이미 다 올라감
        console.log(req.files);
        res.json(req.files.map((v) => v.filename)); //프론트로 보내줌
    }
);


module.exports = router;
//node에서는 import와 export defau lt를 사용하지 않고 require를 사용한다.

2) back/app.js

const express = require("express");
const postRouter = require("./routes/post");
const postsRouter = require("./routes/posts");

const userRouter = require("./routes/user");
const cors = require("cors");
const session = require("express-session");
const morgan = require("morgan");
const passport = require("passport");
const cookieParser = require("cookie-parser");
const dotenv = require("dotenv");
const path = require("path");

const app = express(); //익스프레스 서버
const db = require("./models");
const passportConfig = require("./passport");

db.sequelize
    .sync()
    .then(() => {
        console.log("db 연결 성공");
    })
    .catch(console.error);

dotenv.config();
passportConfig();

app.use(morgan("dev"));
app.use(
    cors({
        origin: "http://localhost:3060",
        credentials: true, //도메인이 달라도 보낼수 있어짐 3060 -> 3065로 쿠키를 보내준다.
    })
); //cors설정
//res.setHeader("Access-Control-Allow-Origin", "http://localhost:3060");
//CORS해결법 *은 모든 주소에 대해서 라는 뜻
//localhost 3060에서 온 것은 허용해주게싸
app.use(express.json());
//익스프레스서버에 뭔가를 장착하겠다.
//프론트에서 Json 형식으로 받은 것을 req.body에 넣어준다.
app.use(express.urlencoded({ extended: true }));
//form에서 제출한 것을 넘겨준다.
//front에서 보낸 action.data를 req.body에 넣어주느 역할

app.use("/", express.static(path.join(__dirname, "uploads")));
app.use(
    session({
        saveUninitialized: false,
        resave: false,
        secret: process.env.COOKIE_SECRET,
    }) //실제적인 정보를 프론트로 넘기면 안되기때문에, 랜덤한 문자열을 넘겨서 백서버와 프론트서버에 동일한 정보를 가질 수 있게 만들어줌
);
app.use(passport.initialize());
app.use(passport.session());
app.use(cookieParser(process.env.COOKIE_SECRET));
app.get("/", (req, res) => {
    res.send("hello express");
});
app.get("/api", (req, res) => {
    res.send("hello api");
});

app.use("/posts", postsRouter); //"/post"가 중복되므로 앞으로 뽑아줄 수 있다.

app.use("/post", postRouter); //"/post"가 중복되므로 앞으로 뽑아줄 수 있다.
app.use("/user", userRouter); //"/post"가 중복되므로 앞으로 뽑아줄 수 있다.

app.listen(3065, () => {
    console.log("서버 실행 중");
});

 

이게 제 코드입니다..

수정해보려고 노력중인데 도저히 어느 부분이 틀린지 모르겠네요ㅠ

지금 게시글 등록할 때 이미지를 같이 등록하지 않고 계십니다.

https://github.com/ZeroCho/react-nodebird/blob/master/ch5/back/routes/post.js#L44

강의 코드가 많이 누락되었는데 다 따라하신 것 맞나요?

윤채현님의 프로필

윤채현

질문자

2023.11.18

어.. 지금 이미지 업로드를 위한 multer강의까지 들었는데 저 링크로 주신 부분은 어느 강의에 나오는 부분인가요...? 제가 놓친건가요?

 

제가 강의에서 아직 이미지 등록 개발을 안 했는데 이미지 등록을 시도하시는 거 아닌가요? 강의를 더 보시면 나옵니다.

윤채현님의 프로필

윤채현

질문자

2023.11.18

그 이미지 등록하면 아래에 이미지가 나오는건 아니지만 깨진 이미지라도 나오는 부분이 있어서 어느정도 구현이 된건줄 알고 질문을 남겼습니다!