React

 

 

 

John Ahn

 

인프런   ==>     라하며 배우는 노드, 리액트 시리즈 - 유튜브 사이트 만들기

 

 

유튜브 강의 목록 :   https://www.youtube.com/playlist?list=PL9a7QRYt5fqnlSRu--re7N_1Ean5jFsh3

 

강의 파일 : https://cdn.jsdelivr.net/gh/braverokmc79/react-youtube-clone@v1.0.0/Youtube%20Clone%20(Ko).pdf

 

Boiler Plate  소스 https://github.com/braverokmc79/ReactYoutubeCloneSeries

완성본 소스 (John Ahn:  https://github.com/jaewonhimnae/react-youtube-clone

소스 https://github.com/braverokmc79/react-youtube-clone

 

 

강의 목록

1.유튜브 사이트 만들기
2.전체적인 틀 만들고 MongoDB 연결
3.비디오 업로드 FORM 만들기1
4.비디오 업로드 FORM 만들기2
5.Multer로 노드 서버에 비디오 저장하기
6. ffmpeg로 비디오 썸네일 생성하기
7.비디오 업로드 하기
8.랜딩 페이지에 비디오들 나타나게 하기
9.비디오 디테일 페이지 만들기
10.디테일 비디오 페이지에 Side 비디오 생성
11.구독 기능 1
12.구독 기능 2
13.구독 비디오 페이지
14.댓글 기능 생성 1 구조 설명
15.댓글 기능 생성 2 Comment js
16.댓글 기능 생성 3 SingleComment
17.댓글 기능 생성 4 ReplyComment
18.좋아요 싫어요 기능1 구조 설명
19.좋아요 싫어요 기능2 템플릿, 데이터 가져오기
20.좋아요 싫어요 기능 3 클릭시 기능들
 

 

 

 

 

11.구독 기능 1

 

 

 

리액트

components/views/VideoDetailPage/Sections/Subscribe.js

import React, { useEffect } from 'react'
import Axios from 'axios';
import { useState } from 'react';

function Subscribe(props) {

    const [SubscribeNumber, setSubscribeNumber] = useState(0);
    const [Subscribed, setSubscribed] = useState(false);

    useEffect(() => {
        const variable = { userTo: props.userTo }
        Axios.post("/api/subscribe/subscribeNumber", variable)
            .then(res => {
                if (res.data.success) {
                    setSubscribeNumber(res.data.subscribeNumber);
                } else {
                    alert('구독자 수 정보를 받아오지 못했습니다.');
                }

            });


        const subscribeVariable = { userTo: props.userTo, userFrom: localStorage.getItem("userId") };

        Axios.post("/api/subscribe/subscribed", subscribeVariable)
            .then(res => {
                if (res.data.success) {
                    setSubscribed(res.data.subscribed);
                } else {
                    alert("정보를 받아오지 못했습니다.");
                }
            });


    }, []);




    return (
        
{SubscribeNumber} {Subscribed ? 'Subscribed' : 'Subscribe'}

) } export default Subscribe

 

 

 

Node.js

1) models/ Subscriber.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const subscriberSchema = Schema({
    userTo: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    },
    userFrom: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    }

}, { timestamps: true });


const Subscriber = mongoose.model('Subscriber', subscriberSchema);

module.exports = { Subscriber }

 

2)routes/ subscribe.js

const express = require('express');
const router = express.Router();
const { Subscriber } = require('../models/Subscriber');


//======================================
//      Subscribe
//======================================

//구독자 수 가져오기
router.post("/subscribeNumber", (req, res) => {

    Subscriber.find({ 'userTo': req.body.userTo })
        .exec((err, subscribe) => {
            if (err) return res.status(400).send(err);
            return res.status(200).json({ success: true, subscribeNumber: subscribe.length })
        });

});


//접속유저 구독상태 가져오기
router.post("/subscribed", (req, res) => {
    Subscriber.find({ 'userTo': req.body.userTo, 'userFrom': req.body.userFrom })
        .exec((err, subscribe) => {
            if (err) return res.status(400).send(err);
            console.log("접속유저 구독상태 가져오기");
            let result = false;
            if (subscribe.length !== 0) {
                result = true;
            }
            res.status(200).json({ success: true, subscribed: result });
        });

});


module.exports = router;

 

 

 

 

 

 

 

 

 

12.구독 기능 2

 

 

 

리액트

components/views/VideoDetailPage/Sections/Subscribe.js

import React, { useEffect } from 'react'
import Axios from 'axios';
import { useState } from 'react';

function Subscribe(props) {

    const [SubscribeNumber, setSubscribeNumber] = useState(0);
    const [Subscribed, setSubscribed] = useState(false);

    const getSubscribeNumber = () => {
        const variable = { userTo: props.userTo }
        Axios.post("/api/subscribe/subscribeNumber", variable)
            .then(res => {
                if (res.data.success) {
                    setSubscribeNumber(res.data.subscribeNumber);
                } else {
                    alert('구독자 수 정보를 받아오지 못했습니다.');
                }
            });
    }

    useEffect(() => {

        getSubscribeNumber();

        const subscribeVariable = { userTo: props.userTo, userFrom: localStorage.getItem("userId") };

        Axios.post("/api/subscribe/subscribed", subscribeVariable)
            .then(res => {
                if (res.data.success) {
                    setSubscribed(res.data.subscribed);
                } else {
                    alert("정보를 받아오지 못했습니다.");
                }
            });


    }, []);


    const onSubscribe = () => {

        let subscribeVariable = {
            userTo: props.userTo,
            userFrom: props.userFrom
        }


        if (Subscribed) {
            //이미 구독 중이라면
            Axios.post("/api/subscribe/unSubscribe", subscribeVariable)
                .then(res => {
                    if (res.data.success) {
                        getSubscribeNumber();
                        setSubscribed(!Subscribed);
                    } else {
                        alert("구독 취소하는데 실패 했습니다.");
                    }
                });

        } else {
            Axios.post("/api/subscribe/subscribe", subscribeVariable)
                .then(res => {
                    if (res.data.success) {
                        getSubscribeNumber();
                        setSubscribed(!Subscribed);
                    } else {
                        alert("구독 하는데 실패 했습니다.");
                    }
                });

        }
    }



    return (
        
{SubscribeNumber} {Subscribed ? 'Subscribed' : 'Subscribe'}

) } export default Subscribe

 

 

Node.js

routes/subscribe.js

~


//구독하기
router.post("/subscribe", (req, res) => {

    const subscribe = new Subscriber(req.body);
    subscribe.save((err, doc) => {
        if (err) return res.json({ success: false, err });
        res.status(200).json({ success: true });
    });
});


//구독취소하기
router.post("/unSubscribe", (req, res) => {
    Subscriber.findOneAndDelete({ userTo: req.body.userTo, userFrom: req.body.userFrom })
        .exec((err, doc) => {
            if (err) return res.status(400).json({ success: false, err });
            res.status(200).json({ success: true, doc })
        })
});


~

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

13.구독 비디오 페이지

 


 

 

리액트

components/views/SubscriptionPage/SubscriptionPage.js

import React, { useEffect } from 'react'
import { Row, Col, Avatar } from 'antd';
import Title from 'antd/lib/typography/Title';
import Meta from 'antd/lib/card/Meta';
import Axios from 'axios';
import { useState } from 'react';
import moment from 'moment';
import { Link } from 'react-router-dom';

function SubscriptionPage() {

    const [Videos, setVideos] = useState([]);

    useEffect(() => {
        const subscriptionVariables = {
            userFrom: localStorage.getItem("userId")
        }

        Axios.post("/api/subscribe/getSubscriptionVideos", subscriptionVariables)
            .then(res => {
                if (res.data.success) {
                    setVideos(res.data.videos);
                } else {
                    alert("비디오 가져오기를 실패 했습니다.");
                }
            });

    }, []);


    const renderCards = Videos.map((video, index) => {

        var minutes = Math.floor(video.duration / 60);
        var seconds = Math.floor(video.duration - minutes * 60);

        return 
            
 

 

{minutes} : {seconds}


} title={video.title} /> {video.writer.name}
{video.views} - {moment(video.createdAt).format("MMM Do YY")} }) return ( <>

 

구독 비디오 {renderCards}

) } export default SubscriptionPage

 

 

Node.js

routes/subscribe.js

//구독 페이지
router.post("/getSubscriptionVideos", (req, res) => {

    //1.자신의 아이디를 가지고 구독하는 사람들을 찾는다.
    Subscriber.find({ userFrom: req.body.userFrom })
        .exec((err, subscriberInfo) => {
            if (err) return res.status(400).json({ success: false, err });


            let subscribedUser = [];

            subscriberInfo.map((subscriber, i) => {
                subscribedUser.push(subscriber.userTo);
            });


            //2.찾은 사람들의 비디오를 가지고 온다.
            Video.find({ writer: { $in: subscribedUser } })
                .populate('writer')
                .exec((err, videos) => {
                    if (err) return res.status(400).send(err);
                    res.status(200).json({ success: true, videos });
                })

        });

});

 

 

 

 

 

 

 

 

 

14.댓글 기능 생성 1 구조 설명

 

 

 

Node.js

models/Comment.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const commentSchema = Schema({
    writer: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    },
    postId: {
        type: Schema.Types.ObjectId,
        ref: 'Video'
    },
    responseTo: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    },
    content: {
        type: String
    }

}, { timestamps: true });


const Comment = mongoose.model('Comment', commentSchema);

module.exports = { Comment }

 

 

리액트

components/views/VideoDetailPage/Sections/Comment.js

import React from 'react'

function Comment() {
    return (
        
Comment

) } export default Comment

 

 

 

 

 

 

 

 

 

 

 

15.댓글 기능 생성 2 Comment js

 

 

 

 

 

1) Node.js

routes/comment.js

const express = require('express');
const router = express.Router();
const { Comment } = require('../models/Comment');


//======================================
//      Comment
//======================================

router.post("/saveComment", (req, res) => {
    const comment = new Comment(req.body);
    comment.save((err, doc) => {
        if (err) return res.json({ success: false, err });

        Comment.find({ '_id': doc._id }) //방금 등록한 글 반환처리
            //.populate("writer")
            .exec((err, result) => {
                if (err) return result.json({ success: false, err });
                res.status(200).json({ success: true, result });
            })


    });

});



module.exports = router;

 

 

 

 

2) 리액트

components/views/VideoDetailPage/Sections/Comment.js

import React from 'react'
import { useState } from 'react';
import { Button } from 'antd';
import Axios from 'axios';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';

function Comment(props) {

    const user = useSelector(state => state.user);
    const [commentValue, setCommentValue] = useState("");
    const params = useParams();


    const handleClick = (event) => {
        setCommentValue(event.currentTarget.value);
    }

    const onSubmit = (event) => {
        event.preventDefault();

        const variable = {
            content: commentValue,
            writer: user.userData._id,
            postId: params.videoId,
        }

        Axios.post('/api/comment/saveComment', variable)
            .then(res => {
                if (res.data.success) {
                    setCommentValue("");
                    console.log("res.data : ", res.data.result);
                } else {
                    alert("커맨트를 저장하지 못했습니다.");
                }
            });
    }


    return (
        <div>
            <br />
            <p>댓글</p>
            <hr />

            {/* Comment Lists */}

            {/* Root Comment Form */}

            <form style={{ display: 'flex' }} >
                <textarea style={{ width: "100%", borderRadius: '5px' }}
                    onChange={handleClick}
                    value={commentValue}
                    placeholder='코멘트를 작성해 주세요.'
                >
                </textarea>
                <br />
                <Button style={{ width: '20%', height: '52px' }}
                    type="primary"
                    onClick={onSubmit}>댓글작성</Button>
            </form>
        </div>
    )


}

export default Comment;

 

 

 

 

 

 

 

 

 

 

16.댓글 기능 생성 3 SingleComment

 

 

 

 

 

 

 

1) Node.js

routes/comment.js

//댓글 목록 가져오기
router.post("/getComments", (req, res) => {

    Comment.find({ 'postId': req.body.videoId })
        .populate("writer")
        .exec((err, comments) => {
            if (err) return result.json({ success: false, err });
            res.status(200).json({ success: true, comments });
        })

});

 

 

 

2) 리액트

 

components/views/VideoDetailPage/VideoDetailPage.js

import React, { useEffect } from 'react';
import { Row, Col, List, Avatar } from 'antd';
import { useParams } from 'react-router-dom';
import Axios from 'axios';
import { useState } from 'react';
import SideVideo from './Sections/SideVideo';
import Subscribe from './Sections/Subscribe';
import { useSelector } from 'react-redux';
import Comment from './Sections/Comment';


function VideoDetailPage() {

    const user = useSelector(state => state.user);

    const { videoId } = useParams();
    const [VideoDetail, setVideoDetail] = useState([]);
    const variable = { videoId: videoId };
    const [Comments, setComments] = useState("");

    useEffect(() => {
        //console.log("비디오 디테일");
        Axios.post(`/api/video/getVideoDetail`, variable)
            .then(res => {
                if (res.data.success) {
                    setVideoDetail(res.data.videoDetail);
                } else {
                    alert("비디오 정보를 가져오는데 실패했습니다.");
                }

            });

        //댓글 목록 가져오기
        Axios.post("/api/comment/getComments", variable)
            .then(res => {
                if (res.data.success) {
                    console.log("res.data.comments   :", res.data.comments);
                    setComments(res.data.comments);
                } else {
                    alert("댓글 목록을 가져오는데 실패 하였습니다.");
                }
            });

    }, []);

    const movePage = (id) => {
        //console.log("페이지 이동 :" + id);
        Axios.post(`/api/video/getVideoDetail`, { videoId: id })
            .then(res => {
                if (res.data.success) {
                    // console.log("res.data.videoDetail : ", res.data.videoDetail);
                    setVideoDetail(res.data.videoDetail);
                } else {
                    alert("비디오 정보를 가져오는데 실패했습니다.");
                }

            });

        //댓글 목록 가져오기
        Axios.post("/api/comment/getComments", { videoId: id })
            .then(res => {
                if (res.data.success) {
                    console.log("res.data.comments   :", res.data.comments);
                    setComments(res.data.comments);
                } else {
                    alert("댓글 목록을 가져오는데 실패 하였습니다.");
                }
            });
    }

    const refreshFunction = (newComment) => {
        setComments(Comments.concat(newComment));
    }


    if (VideoDetail.writer) {

        const subscribeButton = user.userData.isAuth && VideoDetail.writer._id !== localStorage.getItem("userId") && <Subscribe userTo={VideoDetail.writer._id} userFrom={localStorage.getItem("userId")} />;


        return (
            <Row gutter={[16, 16]}>
                <Col lg={15} md={13} xs={24}>
                    <div style={{ width: '100%', padding: '3rem 4rem' }}>
                        <video style={{ width: "100%", maxHeight: 600 }} src={`http://localhost:5000/${VideoDetail.filePath}`} controls />


                        <List.Item
                            actions={[subscribeButton]}
                        >
                            <List.Item.Meta
                                avatar={<Avatar src={VideoDetail.writer && VideoDetail.writer.image} />}
                                title={VideoDetail.writer.name}
                                description={VideoDetail.description}
                            />
                        </List.Item>

                        {/* comments */}
                        <Comment commentLists={Comments} refreshFunction={refreshFunction} />
                    </div>
                </Col >

                <Col lg={9} md={11} xs={24} style={{ marginTop: 50 }}>
                    <SideVideo movePage={movePage} />
                </Col>

            </Row >
        )

    } else {
        return (
            <div>..Loading</div>
        )
    }
}

export default VideoDetailPage

 

 

 

 

components/views/VideoDetailPage/Sections/Comment.js

import React from 'react'
import { useState } from 'react';
import { Button } from 'antd';
import Axios from 'axios';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import SingleComment from './SingleComment';

function Comment(props) {

    const user = useSelector(state => state.user);
    const [commentValue, setCommentValue] = useState("");
    const params = useParams();


    const handleClick = (event) => {
        setCommentValue(event.currentTarget.value);
    }

    const onSubmit = (event) => {
        event.preventDefault();

        const variable = {
            content: commentValue,
            writer: user.userData._id,
            postId: params.videoId,
        }

        Axios.post('/api/comment/saveComment', variable)
            .then(res => {
                if (res.data.success) {
                    setCommentValue("");
                    props.refreshFunction(res.data.result);
                } else {
                    alert("커맨트를 저장하지 못했습니다.");
                }
            });
    }



    return (
        <div>
            <br />
            <p>댓글</p>
            <hr />

            {/* Comment Lists */}
            {props.commentLists && props.commentLists.map((comment, index) => (
                (!comment.responseTo &&
                    <SingleComment comment={comment} refreshFunction={props.refreshFunction} />
                )
            ))}


            {/* Root Comment Form */}

            <form style={{ display: 'flex' }} >
                <textarea style={{ width: "100%", borderRadius: '5px' }}
                    onChange={handleClick}
                    value={commentValue}
                    placeholder='코멘트를 작성해 주세요.'
                >
                </textarea>
                <br />
                <Button style={{ width: '20%', height: '52px' }}
                    onClick={onSubmit}>댓글작성하기</Button>
            </form>
        </div>
    )


}

export default Comment;

 

 

 

 

components/views/VideoDetailPage/Sections/SingleComment.js

import React from 'react'
import { Comment, Avatar, Button, Input } from 'antd';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import Axios from 'axios';
import { useSelector } from 'react-redux';

function SingleComment(props) {

    const [OpenReply, setOpenReply] = useState(false);
    const [CommentValue, setCommentValue] = useState("");
    const user = useSelector(state => state.user);
    const params = useParams();

    const onClickRplyOpen = () => {
        setOpenReply(!OpenReply);
    }

    const actions = [
        <span onClick={onClickRplyOpen} key="comment-basic-reply-to">Reply to</span>
    ]

    const onHandleChange = (event) => {
        setCommentValue(event.currentTarget.value);
    }

    const onSubmit = (event) => {
        event.preventDefault();

        const variable = {
            content: CommentValue,
            writer: user.userData._id,
            postId: params.videoId,
            responseTo: props.comment._id
        }

        Axios.post('/api/comment/saveComment', variable)
            .then(res => {
                if (res.data.success) {
                    setCommentValue("");
                    props.refreshFunction(res.data.result);
                } else {
                    alert("커맨트를 저장하지 못했습니다.");
                }
            });
    }

    return (
        <div>
            <Comment
                actions={actions}
                author={props.comment.writer.name}
                avatar={<Avatar src={props.comment.writer.image} alt={props.comment.writer.name} />}
                content={<p>{props.comment.content}</p>}
            />

            {OpenReply &&
                <form style={{ display: 'flex' }} onSubmit={onSubmit} >
                    <textarea style={{ width: "100%", borderRadius: '5px' }}
                        onChange={onHandleChange}
                        value={CommentValue}
                        placeholder='코멘트를 작성해 주세요.'
                    >
                    </textarea>
                    <br />
                    <Button style={{ width: '20%', height: '52px' }} onClick={onSubmit}
                    >댓글작성하기</Button>
                </form>
            }

        </div>
    )
}

export default SingleComment

 

 

 

 

 

 

 

 

 

 

17.댓글 기능 생성 4 ReplyComment

 

 

 

 

리액트

components/views/VideoDetailPage/Sections/Comment.js

   {/* Comment Lists */}
            {props.commentLists && props.commentLists.map((comment, index) => (
                (!comment.responseTo &&

                    <React.Fragment key={index}>
                        <SingleComment refreshFunction={props.refreshFunction} comment={comment} />
                        <ReplyComment parentCommentId={comment._id} commentLists={props.commentLists} refreshFunction={props.refreshFunction} />
                    </React.Fragment>

                )
            ))}

 

 

 

 

components/views/VideoDetailPage/Sections/ReplyComment.js

import React from 'react'
import { useState, useEffect } from 'react';
import SingleComment from './SingleComment';

function ReplyComment(props) {

    const [ChildCommentNumber, setChildCommentNumber] = useState(0);
    const [OpenReplyComments, setOpenReplyComments] = useState(false);


    useEffect(() => {
        let commentNumber = 0;

        props.commentLists.map((comment) => {
            if (comment.responseTo === props.parentCommentId) {
                commentNumber++;
            }
            return comment;
        });

        setChildCommentNumber(commentNumber);

    }, [props.commentLists]);


    const renderReplyCommnet = (parentCommentId) => {
        return props.commentLists.map((comment, index) => (
            <React.Fragment key={index} >
                {

                    comment.responseTo === parentCommentId &&
                    <div style={{ width: "80%", marginLeft: '40px', padding: "10px" }}>
                        <SingleComment refreshFunction={props.refreshFunction} comment={comment} />
                        {/* 댓글이 존재하면 ReplyComment 가 계속 반복 호출 처리 되어 진다. */}
                        <ReplyComment parentCommentId={comment._id} commentLists={props.commentLists} refreshFunction={props.refreshFunction} />
                    </div>

                }
            </React.Fragment >

        ));

    }

    const onHandleChange = () => {
        setOpenReplyComments(!OpenReplyComments);
    }

    return (

        <div>
            {ChildCommentNumber > 0 &&
                <p style={{ fontSize: '14px', margin: 0, color: 'gray', cursor: "pointer" }} onClick={onHandleChange}>
                    view {ChildCommentNumber} Moer comment(s)
                </p>
            }


            {OpenReplyComments &&
                renderReplyCommnet(props.parentCommentId)
            }

        </div >
    )
}

export default ReplyComment

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

구더기 무서워 장 못 담글까 , 다소의 장애가 있더라도, 해야 할 일이나 하고 싶은 일은 하게 마련이라는 뜻.

댓글 ( 4)

댓글 남기기

작성