클라이언트
와 서버
에 Dependencies
다운받기
npm install
Server
은 Root 경로, Client
는 client폴더 경로server/config/dev.js
파일 설정
MongoDB
로그인dev.js
파일에 넣는다.⭐ server/config/dev.js
module.exports = {
mongoURI:
'mongodb+srv://devPark:<password>@react-boiler-plate.ovbtd.mongodb.net/react-youtube-clone?retryWrites=true&w=majority'
}
MongoDB | RDBMS |
---|---|
Database | Database |
Collections | Tables |
Documents | Rows |
Fields | Columns |
⭐ server/models/Video.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const videoSchema = mongoose.Schema({
writer: {
type: Schema.Types.ObjectId,
ref: 'User'
},
title: {
type: String,
maxlength: 50,
},
description: {
type: String,
},
privacy: {
type: Number,
},
filePath: {
type: String,
},
category: {
type: String,
},
views: {
type: Number,
default: 0
},
duration: {
type: String
},
thumbnail: {
type: String
}
}, { timestamps: true })
const Video = mongoose.model('Video', videoSchema)
module.exports = { Video }
multer,
ffmpeg
$ brew install ffmpeg
설치방법$ npm install fluent-ffmpeg --save
const express = require('express');
const router = express.Router();
const multer = require('multer');
var ffmpeg = require('fluent-ffmpeg');
const { Video } = require("../models/Video");
const { Subscriber } = require("../models/Subscriber");
const { auth } = require("../middleware/auth");
var storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/')
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}_${file.originalname}`)
},
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname)
if (ext !== '.mp4') {
return cb(res.status(400).end('only jpg, png, mp4 is allowed'), false);
}
cb(null, true)
}
})
var upload = multer({ storage: storage }).single("file")
//=================================
// User
//=================================
⭐ server/routes/video.js
router.post('/uploadfiles', (req, res) => {
upload(req, res, err => {
if (err) {
return res.json({ success: false, err })
}
return res.json({ success: true, filePath: res.req.file.path, fileName: res.req.file.filename })
})
});
router.post('/thumbnail', (req, res) => {
// 썸네일 생성하고 비디오 러닝타임도 가져오기
let thumbsFilePath = ''
let fileDuration = ''
// 비디오 러닝타임 가져오기
ffmpeg.ffprobe(req.body.filePath, function(err, metadata){
console.dir(metadata) // all metadata
console.log(metadata.format.duration)
fileDuration = metadata.format.duration
})
// 썸네일 생성
ffmpeg(req.body.filePath)
// 썸네일의 filename을 생성
.on('filenames', function (filenames) {
console.log('Will generate ' + filenames.join(', '))
console.log(filenames)
thumbsFilePath = 'uploads/thumbnails/' + filenames[0]
})
// 썸네일 생성 후 처리
.on('end', function () {
console.log('Screenshots taken')
return res.json({ success: true, thumbsFilePath: thumbsFilePath, fileDuration: fileDuration })
})
// 에러처리
.on('error', function(err) {
console.error(err)
return res.json({ success: false, err })
})
// 옵션
.screenshots({
// Will take screens at 20%, 40%, 60% and 80% of the video
count: 3,
folder: 'uploads/thumbnails/',
size:'320x240',
// %b input basename ( filename w/o extension )
filename:'thumbnail-%b.png'
})
})
router.post('/uploadVideo', (req, res) => {
// 비디오 정보를 mongoDB에 저장한다
const video = new Video(req.body)
video.save((err, doc) => {
if(err) return res.status(400).json({ success: false, err })
return res.status(200).json({
success: true
})
})
})
⭐ VideoUploadPage.js
import React, { useState } from 'react'
import { Typography, Form, Input, Button, Icon} from 'antd'
import Dropzone from 'react-dropzone'
import axios from 'axios'
import { useSelector } from 'react-redux'
const { Title } = Typography
const { TextArea } = Input
const PrivateOptions = [
{ value: 0, label: 'Private' },
{ value: 1, label: 'Public' }
]
const CatogoryOptions = [
{ value: 0, label: 'Film & Animation' },
{ value: 1, label: 'Autos & Vehicles' },
{ value: 2, label: 'Music' },
{ value: 3, label: 'Pets & Animals' },
{ value: 4, label: 'Sports' }
]
function VideoUploadPage(props) {
// 리덕스의 state 스토어에 가서 user 정보를 선택
const user = useSelector(state => state.user)
const [VideoTitle, setVideoTitle] = useState('')
const [Description, setDescription] = useState('')
const [Privacy, setPrivacy] = useState(0)
const [Category, setCategory] = useState('Film & Animation')
// 비디오썸네일 저장 데이터
const [FilePath, setFilePath] = useState('')
const [Duration, setDuration] = useState('')
const [ThumbnailPath, setThumbnailPath] = useState('')
const onTitleChange = (e) => {
setVideoTitle(e.currentTarget.value)
}
const onDescriptionChange = (e) => {
setDescription(e.currentTarget.value)
}
const onPrivateChange = (e) => {
setPrivacy(e.currentTarget.value)
}
const onCategoryChange = (e) => {
setCategory(e.currentTarget.value)
}
const onDrop = (files) => {
let formData = new FormData();
const config = {
header: { 'content-type': 'multipart/form-data' }
}
formData.append("file", files[0])
// 비디오 업로드
axios.post('/api/video/uploadfiles', formData, config)
.then(response => {
if (response.data.success) {
let variable = {
filePath: response.data.filePath,
fileName: response.data.fileName
}
setFilePath(response.data.filePath)
// filepath를 이용해 썸네일 만들기
axios.post('/api/video/thumbnail', variable)
.then(response => {
if (response.data.success) {
setDuration(response.data.fileDuration)
setThumbnailPath(response.data.thumbsFilePath)
} else {
alert('썸네일 생성에 실패했습니다.');
}
})
} else {
alert('비디오 업로드를 실패했습니다.')
}
})
}
const onSubmit = (e) => {
e.preventDefault()
// Video Collention에 모두 넣기 위해
const variables = {
// 리덕스의 State에서 확인 가능 (user.userData)
// react-redux의 useSelector를 이용
writer: user.userData._id,
title: VideoTitle,
description: Description,
privacy: Privacy,
filePath: FilePath,
category: Category,
duration: Duration,
thumbnail: ThumbnailPath
}
axios.post('/api/video/uploadVideo', variables)
.then(response => {
if (response.data.success) {
alert('성공적으로 업로드를 했습니다.')
setTimeout(() => {
props.history.push('/')
}, 3000)
} else {
alert('비디오 업로드에 실패했습니다.')
}
})
}
return (
<div style={{ maxWidth: '700px', margin: '2rem auto' }}>
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<Title level={2} > Upload Video</Title>
</div>
<Form onSubmit={onSubmit}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{/* Drop Zone */}
<Dropzone
onDrop={onDrop}
multiple={false}
maxSize={800000000}>
{({ getRootProps, getInputProps }) => (
<div style={{ width: '300px', height: '240px', border: '1px solid lightgray', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
{...getRootProps()}
>
<input {...getInputProps()} />
<Icon type="plus" style={{ fontSize: '3rem' }} />
</div>
)}
</Dropzone>
{/* Thumbnail */}
{ThumbnailPath &&
<div>
<img src={`http://localhost:5000/${ThumbnailPath}`} alt="thumbnail" />
</div>
}
</div>
<br />
<br />
<label>Title</label>
<Input
onChange={onTitleChange}
value={VideoTitle}
/>
<br />
<br />
<label>Description</label>
<TextArea
onChange={onDescriptionChange}
value={Description}
/>
<br />
<br />
<select onChange={onPrivateChange}>
{PrivateOptions.map((item, index) => (
<option key={index} value={item.value}>{item.label}</option>
))}
</select>
<br />
<br />
<select onChange={onCategoryChange}>
{CatogoryOptions.map((item, index) => (
<option key={index} value={item.value}>{item.label}</option>
))}
</select>
<br />
<br />
<Button type="primary" size="large" onClick={onSubmit}>
Submit
</Button>
</Form>
</div>
)
}
export default VideoUploadPage
⭐ server/routes/video.js
router.get('/getVideos', (req, res) => {
Video.find()
/* server/models/Video.js
writer: {
type: Schema.Types.ObjectId,
ref: 'User'
} */
.populate('writer')
.exec((err, videos) => {
if(err) return res.status(400).send(err)
return res.status(200).json({ success: true, videos })
})
})
⭐ LandingPage.js
import React, { useEffect, useState } from 'react'
import axios from 'axios'
import moment from 'moment'
import { Card, Avatar, Col, Typography, Row } from 'antd'
const { Title } = Typography
const { Meta } = Card
function LandingPage() {
const [Video, setVideo] = useState([])
useEffect(() => {
axios.get('/api/video/getVideos')
.then(response => {
if (response.data.success) {
setVideo(response.data.videos)
} else {
alert('비디오를 가져오는데 실패했습니다.')
}
})
}, [])
const renderCards = Video.map((video, index) => {
var minutes = Math.floor(video.duration / 60);
var seconds = Math.floor(video.duration - minutes * 60);
return <Col key={index} lg={6} md={8} xs={24}>
<a href={`/video/${video._id}`} >
<div style={{ position: 'relative' }}>
<img style={{ width: '100%' }} src={`http://localhost:5000/${video.thumbnail}`} />
<div className="duration">
<span>{minutes} : {seconds}</span>
</div>
</div>
</a>
<br />
<Meta
avatar={
<Avatar src={video.writer.image} />
}
title={video.title}
description
/>
<span>{video.writer.name} </span><br />
<span style={{ marginLeft: '3rem' }}> {video.views} views</span>
- <span> {moment(video.createdAt).format("MMM Do YY")} </span>
</Col>
})
return (
<div style={{ width: '85%', margin: '3rem auto' }}>
<Title level={2} > Recommended </Title>
<hr />
<Row gutter={[32, 16]}>
{renderCards}
</Row>
</div>
)
}
export default LandingPage
⭐ client/src/index.css
// 비디오 시간에 관한 CSS
.duration {
bottom: 0;
right: 0;
position: absolute;
margin: 4px;
color: #fff;
background-color: rgba(17, 17, 17, 0.8);
opacity: 0.8;
padding: 2px 4px;
border-radius: 2px;
letter-spacing: 0.5px;
font-size: 12px;
font-weight: 500;
line-height: 12px;
}
⭐ server/routes/video.js
router.post('/getVideoDetail', (req, res) => {
Video.findOne({ "_id" : req.body.videoId })
.populate('writer')
.exec((err, videoDetail) => {
if(err) return res.status(400).send(err)
return res.status(200).json({ success: true, videoDetail })
})
})
⭐// App.js
import VideoDetailPage from './views/VideoDetailPage/VideoDetailPage'
function App() {
return (
<Switch>
<Route
exact
path="/video/:videoId"
component={Auth(VideoDetailPage, null)}
/>
</Switch>
)
}
⭐ VideoDetailPage.js
import React, { useEffect, useState } from 'react'
import { List, Avatar, Row, Col } from 'antd'
import axios from 'axios'
import SideVideo from './Sections/SideVideo'
import Subscriber from './Sections/Subscriber'
import Comment from './Sections/Comment'
import LikeDislikes from './Sections/LikeDislikes'
function VideoDetailPage(props) {
const videoId = props.match.params.videoId
const variable = { videoId: videoId }
const [VideoDetail, setVideoDetail] = useState([])
const [Comments, setComments] = useState([])
useEffect(() => {
axios.post('/api/video/getVideoDetail', variable)
.then(response => {
if(response.data.success) {
setVideoDetail(response.data.videoDetail)
} else {
alert('비디오 정보를 가져오길 실패했습니다.')
}
})
axios.post('/api/comment/getComments', variable)
.then(response => {
if(response.data.success) {
setComments(response.data.comments)
console.log(response.data.comments)
} else {
alert('코멘트 정보를 가져오길 실패했습니다.')
}
})
}, [])
const refreshFunction = (newComment) => {
setComments(Comments.concat(newComment))
}
if(VideoDetail.writer) {
const subscribeButton = VideoDetail.writer._id !== localStorage.getItem('userId')
&& <Subscriber
userTo={VideoDetail.writer._id}
// userFrom : 개발자도구 - Application - Local Storage - userId
userFrom={localStorage.getItem('userId')} />
return (
<Row gutter={[16, 16]}>
<Col lg={18} xs={24}>
<div style={{ width: '100%', padding: '3rem 4em' }}>
<video style={{ width: '100%' }}
src={`http://localhost:5000/${VideoDetail.filePath}`}
controls />
<List.Item
actions={[ <LikeDislikes vdieo
userId={localStorage.getItem('userId')}
videoId={videoId}/>,
subscribeButton ]}
>
<List.Item.Meta
avatar={<Avatar src={VideoDetail.writer.image} />}
title={VideoDetail.writer.name}
description={VideoDetail.description}
/>
</List.Item>
{/* Comments */}
<Comment postId={videoId} refreshFunction={refreshFunction} commentLists={Comments}/>
</div>
</Col>
<Col lg={6} xs={24}>
<SideVideo />
</Col>
</Row>
)
} else {
return (
<div>...loading</div>
)
}
}
export default VideoDetailPage
⭐ VideoDetailPage/Sections/SideVideo.js
import React, {useEffect, useState} from 'react'
import axios from 'axios'
function SideVideo() {
const [SideVideos, setSideVideos] = useState([])
useEffect(() => {
axios.get('/api/video/getVideos')
.then(response => {
if (response.data.success) {
setSideVideos(response.data.videos)
} else {
alert('비디오를 가져오는데 실패했습니다.')
}
})
}, [])
const renderSideVideo = SideVideos.map(( video, index) => {
var minutes = Math.floor(video.duration / 60)
var seconds = Math.floor(video.duration - minutes * 60)
return <div key={index} style={{ display: 'flex', marginTop: '1rem', padding: '0 2rem' }}>
<div style={{ width:'40%', marginRight:'1rem' }}>
<a href>
<img
style={{ width: '100%' }}
src={`http://localhost:5000/${video.thumbnail}`}
alt="thumbnail" />
</a>
</div>
<div style={{ width:'50%' }}>
<a href style={{ color:'gray' }}>
<span style={{ fontSize: '1rem', color: 'black' }}>{video.title} </span><br />
<span> {video.writer.name} </span><br />
<span> {video.views} views </span><br />
<span> {minutes} : {seconds} </span><br />
</a>
</div>
</div>
})
return (
<React.Fragment>
<div style={{ marginTop:'3rem' }}></div>
{renderSideVideo}
</React.Fragment>
)
}
export default SideVideo
userTo
, userFrom
⭐// server/models/Subscriber.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const subscriberSchema = mongoose.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 }
⭐ server/routes/video.js
const { Subscriber } = require('../models/Subscriber')
router.post('/getSubscriptionVideos', (req, res) => {
// 1. 자신의 아이디를 가지고 Subscriber Collectionp에서 구독한 사람들을 찾는다.
Subscriber.find({ 'userFrom': req.body.userFrom })
.exec(( err, subscriberInfo )=> {
if(err) return res.status(400).send(err)
// userTo의 정보를 모두 넣어준다.
let subscribedUser = []
subscriberInfo.map((subscriber, i)=> {
subscribedUser.push(subscriber.userTo)
})
// 2. 찾은 사람들의 비디오를 가지고 온다.
// 정보가 2개 이상일 때는 { writer : req.body.id } 가 사용되지 못한다.
Video.find({ writer: { $in: subscribedUser }})
.populate('writer')
.exec((err, videos) => {
if(err) return res.status(400).send(err)
return res.status(200).json({ success: true, videos })
})
})
})
⭐ server/routes/subscribe.js
const express = require('express');
const router = express.Router();
const { Subscriber } = require('../models/Subscriber')
const { auth } = require("../middleware/auth")
//=================================
// 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)
let result = false;
if(subscribe.length !== 0) {
result = true
}
return res.status(200).json({ success: true, subcribed: result })
})
})
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 })
return res.status(200).json({ success: true, doc })
})
})
router.post('/subscribe', (req, res) => {
const subscribe = new Subscriber(req.body)
subscribe.save((err, doc) => {
if(err) return res.json({ success: false, err })
return res.status(200).json({ success: true })
})
})
module.exports = router
⭐ SubscriptionPage.js
import React, { useEffect, useState } from 'react'
import { Card, Avatar, Col, Typography, Row } from 'antd'
import axios from 'axios'
import moment from 'moment'
const { Title } = Typography
const { Meta } = Card
function SubscriptionPage() {
const [Videos, setVideos] = useState([])
useEffect(() => {
const subscriptionVariables = {
userFrom: localStorage.getItem('userId')
}
axios.post('/api/video/getSubscriptionVideos', subscriptionVariables)
.then(response => {
if (response.data.success) {
setVideos(response.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 <Col key={index} lg={6} md={8} xs={24}>
<a href={`/video/${video._id}`} >
<div style={{ position: 'relative' }}>
<img style={{ width: '100%' }} src={`http://localhost:5000/${video.thumbnail}`} />
<div className="duration">
<span>{minutes} : {seconds}</span>
</div>
</div>
</a>
<br />
<Meta
avatar={
<Avatar src={video.writer.image} />
}
title={video.title}
description
/>
<span>{video.writer.name} </span><br />
<span style={{ marginLeft: '3rem' }}> {video.views} views</span>
- <span> {moment(video.createdAt).format("MMM Do YY")} </span>
</Col>
})
return (
<div style={{ width: '85%', margin: '3rem auto' }}>
<Title level={2} > Recommended </Title>
<hr />
<Row gutter={[32, 16]}>
{renderCards}
</Row>
</div>
)
}
export default SubscriptionPage
⭐ VideoDetailPage/Sections/Subscriber.js
import React, { useEffect, useState } from 'react'
import axios from 'axios'
function Subscriber(props) {
const [SubscribeNumber, setSubscribeNumber] = useState(0)
const [Subscribed, setSubscribed] = useState(false)
useEffect(() => {
// userTo : VideoDetailPage.js의 userTo={VideoDetail.writer._id}
const variable = { userTo: props.userTo }
axios.post('/api/subscribe/subscribeNumber', variable)
.then(response => {
if (response.data.success) {
setSubscribeNumber(response.data.subscribeNumber)
} else {
alert('구독자 수 정보를 가져오지 못했습니다.')
}
})
let subscribedVariable = { userTo: props.userTo, userFrom: props.userFrom }
// 잘못된 코드 : let subscribedVariable = { userTo: props.userTo, userFrom: localStorage.getItem('userId')}
// 다른 방법 :
// const userTo = props.userTo
// const userFrom = props.userFrom
// let subscribedVariable = { userTo: userTo, userFrom: userFrom }
axios.post('/api/subscribe/subscribed', subscribedVariable)
.then(response => {
if (response.data.success) {
setSubscribed(response.data.subcribed)
} else {
alert('구독 정보를 가져오지 못했습니다.')
}
})
}, [])
const onSubscribe = ( ) => {
let subscribeVariables = {
userTo : props.userTo,
userFrom : props.userFrom
}
// 이미 구독 중이라면
if(Subscribed) {
axios.post('/api/subscribe/unSubscribe', subscribeVariables)
.then(response => {
if(response.data.success){
setSubscribeNumber(SubscribeNumber - 1)
setSubscribed(!Subscribed)
} else {
alert('구독을 취소하는데 실패했습니다.')
}
})
// 구독중이지 않다면
} else {
axios.post('/api/subscribe/subscribe', subscribeVariables)
.then(response => {
if(response.data.success) {
setSubscribeNumber(SubscribeNumber + 1)
setSubscribed(!Subscribed)
} else {
alert('구독하는데 실패했습니다.')
}
})
}
}
return (
<div>
<button
style={{
backgroundColor: `${Subscribed ? '#AAAAAA' : '#CC0000'}`, borderRadius: '4px',
color: 'white', padding: '10px 16px', fontWeight: '500',
fontSize: '1rem', textTransform: 'uppercase'
}}
onClick={onSubscribe}
>
{SubscribeNumber} {Subscribed ? 'Subscribed' : 'Subscribe'}
</button>
</div>
)
}
export default Subscriber
writer
, postId
, responseTo
, content
⭐ server/models/Comment.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const commentSchema = mongoose.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 }
props.refreshFunction
props.refreshFunction
loadComments
⭐ server/routes/comment.js
const express = require('express')
const router = express.Router()
const { Comment } = require('../models/Comment')
//=================================
// Subscribe
//=================================
router.post('/saveComment', (req, res) => {
const comment = new Comment(req.body)
comment.save((err, comment) => {
if (err) return res.json({ success: false, err })
Comment.find({ '_id': comment._id })
.populate('writer')
.exec((err, result) => {
if (err) return res.json({ success: false, err })
return res.status(200).json({ success: true, result })
})
})
})
module.exports = router
⭐// VideoDetailPage/Sections/Comment.js
import React, { useState } from 'react'
import axios from 'axios'
import { useSelector } from 'react-redux'
function Comment(props) {
const videoId = props.postId
// useSelector를 사용하여 writer state를 리덕스에서 가져오기
const user = useSelector(state => state.user)
const [commentValue, setcommentValue] = useState('')
const handleClick = (event) => {
setcommentValue(event.currentTarget.value)
}
const onSubmit = (event) => {
event.preventDefault()
const variables = {
content: commentValue,
// useSelector를 사용하여 writer state를 리덕스에서 가져오기
writer: user.userData._id,
postId: props.videoId
}
axios.post('/api/comment/saveComment', variables)
.then(response => {
if (response.data.success) {
props.refreshFunction(response.data.result)
} else {
alert('코멘트를 저장하지 못했습니다.')
}
})
}
return (
<div>
<br />
<p> Replies </p>
<hr />
{/* Root Comment Form */}
<form style={{ display: 'flex' }} onSubmit={onSubmit}>
<textarea
style={{ width: '100%', borderRadius: '5px' }}
onChange={handleClick}
value={commentValue}
placeholder="코멘트를 작성해 주세요"
/>
<br />
<button style={{ width: '20%', height: '52px' }}
onClick={onSubmit}>Submit</button>
</form>
</div>
)
}
export default Comment
저장된 댓글을 부모컴포넌트에 업데이트
⭐// VideoDetailPage/Sections/Comment.js
function Comment(props) {
...
return (
{/* Comment Lists */}
{props.commentLists && props.commentLists.map((comment, index) => (
(!comment.responseTo &&
<SingleComment
postId={videoId}
refreshFunction={props.refreshFunction}
comment={comment} />
)
))}
)
}
⭐// VideoDetailPage/Sections/SingleComment.js
import React, { useState } from 'react'
import { Comment, Avatar, Button, Input } from 'antd'
import axios from 'axios'
import { useSelector } from 'react-redux'
const { TextArea } = Input
function SingleComment(props) {
// useSelector를 사용하여 writer state를 리덕스에서 가져오기
const user = useSelector(state => state.user)
const [OpenReply, setOpenReply] = useState(false)
const [CommentValue, setCommentValue] = useState('')
const onClickReplyOpen = () => {
setOpenReply(!OpenReply)
}
const onHandleChange = (event) => {
setCommentValue(event.currentTarget.value)
}
const onSubmit = (event) => {
event.preventDefault()
const variables = {
content: CommentValue,
// useSelector를 사용하여 writer state를 리덕스에서 가져오기
writer: user.userData._id,
postId: props.postId,
responseTo : props.comment._id
}
axios.post('/api/comment/saveComment', variables)
.then(response => {
if (response.data.success) {
props.refreshFunction(response.data.result)
} else {
alert('코멘트를 저장하지 못했습니다.')
}
})
}
const actions = [
<span onClick={onClickReplyOpen} key="comment-basic-reply-to"> Reply to </span>
]
return (
<div>
<Comment
actions={actions}
author={props.comment.writer.name}
avatar={<Avatar src={props.comment.writer.image} alt />}
content={<p> {props.comment.content} </p>}
/>
{OpenReply &&
<form style={{ display: 'flex' }} onSubmit={onSubmit}>
<textarea
style={{ width: '100%', borderRadius: '5px' }}
onChange={onHandleChange}
value={CommentValue}
placeholder="코멘트를 작성해 주세요"
/>
<br />
<button style={{ width: '20%', height: '52px' }}
onClick={onSubmit}>Submit</button>
</form>
}
</div>
)
}
export default SingleComment
⭐// server/routes/comment.js
router.post('/getComments', (req, res) => {
Comment.find({ "postId": req.body.videoId })
.populate('writer')
.exec((err, comments) => {
if (err) return res.status(400).send(err)
return res.status(200).json({ success: true, comments })
})
})
⭐// VideoDetailPage/Sections/Comment.js
function Comment(props) {
...
return (
{/* Comment Lists */}
{props.commentLists && props.commentLists.map((comment, index) => (
(!comment.responseTo &&
<React.Fragment>
<SingleComment postId={videoId}
refreshFunction={props.refreshFunction}
comment={comment} />
<ReplyComment postId={videoId}
refreshFunction={props.refreshFunction}
parentCommentId={comment._id}
commentLists={props.commentLists}/>
</React.Fragment>
)
))}
)
}
⭐// VideoDetailPage/Sections/ReplyComment.js
import React, { useEffect, useState } from 'react'
import SingleComment from '../Sections/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 ++
}
})
setChildCommentNumber(ChildCommentNumber)
}, [props.commentLists])
const renderReplyComment = (parentCommentId) =>
props.commentLists.map((comment, index) => (
<React.Fragment>
{
comment.responseTo === parentCommentId &&
<div style={{ width: '80%', marginLeft: '40px' }}>
<SingleComment postId={props.videoId}
refreshFunction={props.refreshFunction}
comment={comment} />
<ReplyComment postId={props.videoId}
refreshFunction={props.refreshFunction}
parentCommentId={comment._id}
commentLists={props.commentLists}/>
</div>
}
</React.Fragment>
))
const onHandleChange = () => {
setOpenReplyComments(!OpenReplyComments)
}
return (
<div>
{ChildCommentNumber > 0 &&
<p style={{ fontSize: '14px', margin: 0, color: 'gray' }} onClick={onHandleChange}>
View 1 more comment(s)
</p>
}
{OpenReplyComments &&
renderReplyComment(props.parentCommentId)
}
</div>
)
}
export default ReplyComment
⭐// VideoDetailPage/Sections/SingleComment.js
function SingleComment(props) {
const onSubmit = (event) => {
event.preventDefault()
const variables = {
content: CommentValue,
// useSelector를 사용하여 writer state를 리덕스에서 가져오기
writer: user.userData._id,
postId: props.postId,
responseTo : props.comment._id
}
axios.post('/api/comment/saveComment', variables)
.then(response => {
if (response.data.success) {
setCommentValue('')
setOpenReply(false)
props.refreshFunction(response.data.result)
} else {
alert('코멘트를 저장하지 못했습니다.')
}
})
}
// server/models/Like.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const likeSchema = mongoose.Schema(
{
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
},
commentId: {
type: Schema.Types.ObjectId,
ref: 'Comment',
},
videoId: {
type: Schema.Types.ObjectId,
ref: 'Video',
},
},
{ timestamps: true }
)
const Like = mongoose.model('Like', likeSchema)
module.exports = { Like }
// server/models/DisLike.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const dislikeSchema = mongoose.Schema(
{
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
},
commentId: {
type: Schema.Types.ObjectId,
ref: 'Comment',
},
videoId: {
type: Schema.Types.ObjectId,
ref: 'Video',
},
},
{ timestamps: true }
)
const Dislike = mongoose.model('Dislike', dislikeSchema)
module.exports = { Dislike }
// VideoDetailPage/Sections/SingleComment.js
import LikeDislikes from './LikeDislikes'
function SingleComment(props) {
const actions = [
<LikeDislikes
userId={localStorage.getItem('userId')}
commentId={props.comment._id}/>
,<span onClick={onClickReplyOpen} key="comment-basic-reply-to"> Reply to </span>
]
return (
)
}
// VideoDetailPage/Sections/LikeDislike.js
import React, { useEffect, useState } from 'react'
import { Tooltip, Icon } from 'antd'
import axios from 'axios'
function LikeDislikes(props) {
const [Likes, setLikes] = useState(0)
const [Dislikes, setDislikes] = useState(0)
const [LikeAction, setLikeAction] = useState(null)
const [DislikeAction, setDislikeAction] = useState(null)
let variable = {}
if (props.video) {
variable = { videoId: props.videoId, userId: props.userId }
} else {
variable = { commentId: props.commentId, userId: props.userId }
}
useEffect(() => {
axios.post('/api/like/getLikes', variable)
.then(response => {
if (response.data.success) {
// 얼마나 많은 좋아요를 받았는지
setLikes(response.data.likes.length)
// 내가 이미 그 좋아요를 눌렀는지
response.data.likes.map(like => {
if (like.userId === props.userId) {
setLikeAction('liked')
}
})
} else {
alert('Likes 정보를 가져오는데 실패했습니다.')
}
})
axios.post('/api/like/getDislikes', variable)
.then(response => {
if (response.data.success) {
// 얼마나 많은 싫어요를 받았는지
setDislikes(response.data.dislikes.length)
// 내가 이미 그 싫어요를 눌렀는지
response.data.dislikes.map(dislike => {
if (dislike.userId === props.userId) {
setDislikeAction('disliked')
}
})
} else {
alert('DisLikes 정보를 가져오는데 실패했습니다')
}
})
}, [])
const onLike = () => {
// Like이 클릭이 되어있지 않았을 때
if (LikeAction === null) {
axios.post('/api/like/upLike', variable)
.then(response => {
if (response.data.success) {
setLikes(Likes + 1)
setLikeAction('liked')
// 만약 dislike가 이미 클릭되어 있으면
if (DislikeAction !== null) {
setDislikeAction(null)
setDislikes(Dislikes - 1)
}
} else {
alert('Like를 올리지 못하였습니다.')
}
})
} else {
// Like이 클릭이 되어있을 때
axios.post('/api/like/unLike', variable)
.then(response => {
if (response.data.success) {
setLikes(Likes - 1)
setLikeAction(null)
} else {
alert('Like를 내리지 못하였습니다.')
}
})
}
}
const onDisLike = () => {
// Disklike가 클릭이 되어있을 때
if (DislikeAction !== null) {
axios.post('/api/like/unDisLike', variable)
.then(response => {
if (response.data.success) {
setDislikes(Dislikes - 1)
setDislikeAction(null)
} else {
alert('Dislike를 내리는데 실패했습니다.')
}
})
// Dislike가 클릭되어 있지 않을때
} else {
axios.post('/api/like/upDisLike', variable)
.then(response => {
if (response.data.success) {
setDislikes(Dislikes + 1)
setDislikeAction('disliked')
// 만약 dislike가 이미 클릭되어 있으면
if(LikeAction !== null ) {
setLikeAction(null)
setLikes(Likes - 1)
}
} else {
alert('Dislike를 올리는데 실패했습니다.')
}
})
}
}
return (
<React.Fragment>
<span key="comment-basic-like">
<Tooltip title="Like">
<Icon type="like"
theme={LikeAction === 'liked' ? 'filled' : 'outlined'}
onClick={onLike} />
</Tooltip>
<span style={{ paddingLeft: '8px', cursor: 'auto' }}>{Likes}</span>
</span>
<span key="comment-basic-dislike">
<Tooltip title="Dislike">
<Icon
type="dislike"
theme={DislikeAction === 'disliked' ? 'filled' : 'outlined'}
onClick={onDisLike}
/>
</Tooltip>
<span style={{ paddingLeft: '8px', cursor: 'auto' }}>{Dislikes}</span>
</span>
</React.Fragment>
)
}
export default LikeDislikes
// server/routes/like.js
const express = require('express')
const router = express.Router()
const { Like } = require('../models/Like')
const { Dislike } = require('../models/Dislike')
//=================================
// Likes DisLikes
//=================================
router.post('/getLikes', (req, res) => {
let variable = {}
if (req.body.videoId) {
variable = { videoId: req.body.videoId }
} else {
variable = { commentId: req.body.commentId }
}
Like.find(variable)
.exec((err, likes) => {
if (err) return res.status(400).send(err)
res.status(200).json({ success: true, likes })
})
})
router.post('/getDislikes', (req, res) => {
let variable = {}
if (req.body.videoId) {
variable = { videoId: req.body.videoId }
} else {
variable = { commentId: req.body.commentId }
}
Dislike.find(variable)
.exec((err, dislikes) => {
if (err) return res.status(400).send(err)
res.status(200).json({ success: true, dislikes })
})
})
router.post('/upLike', (req, res) => {
let variable = {}
if (req.body.videoId) {
variable = { videoId: req.body.videoId, userId: req.body.userId }
} else {
variable = { commentId: req.body.commentId , userId: req.body.userId }
}
// Like Collection에 클릭 정보를 넣어준다.
const like = new Like(variable)
like.save((err, likeResult) => {
if (err) return res.json({ success: false, err })
// 만약 Dislike이 이미 클릭이 되어 있다면, Dislike를 1 줄여준다
Dislike.findOneAndDelete(variable)
.exec((err, disLikeResult) => {
if (err) return res.status(400).json({ success: false, err })
res.status(200).json({ success: true })
})
})
})
router.post('/unLike', (req, res) => {
let variable = {}
if (req.body.videoId) {
variable = { videoId: req.body.videoId, userId: req.body.userId }
} else {
variable = { commentId: req.body.commentId , userId: req.body.userId }
}
Like.findOneAndDelete(variable)
.exec((err, result) => {
if (err) return res.status(400).json({ success: false, err })
res.status(200).json({ success: true })
})
})
router.post('/unDisLike', (req, res) => {
let variable = {}
if (req.body.videoId) {
variable = { videoId: req.body.videoId, userId: req.body.userId }
} else {
variable = { commentId: req.body.commentId , userId: req.body.userId }
}
Dislike.findOneAndDelete(variable)
.exec((err, result) => {
if (err) return res.status(400).json({ success: false, err })
res.status(200).json({ success: true })
})
})
router.post('/upDisLike', (req, res) => {
let variable = {}
if (req.body.videoId) {
variable = { videoId: req.body.videoId, userId: req.body.userId }
} else {
variable = { commentId: req.body.commentId , userId: req.body.userId }
}
// Dislike Collection에 클릭 정보를 넣어준다.
const disLike = new Dislike(variable)
disLike.save((err, dislikeResult) => {
if (err) return res.json({ success: false, err })
// 만약 Like이 이미 클릭이 되어 있다면, Like를 1 줄여준다
Like.findOneAndDelete(variable)
.exec((err, likeResult) => {
if (err) return res.status(400).json({ success: false, err })
res.status(200).json({ success: true })
})
})
})
module.exports = router