Skip to content

Commit fdc9aaf

Browse files
committed
Update: report feature
1 parent eaeb171 commit fdc9aaf

17 files changed

Lines changed: 223 additions & 16 deletions

File tree

ch4/front/pages/_app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ NodeBird.propTypes = {
1919
Component: PropTypes.elementType.isRequired,
2020
};
2121

22+
export function reportWebVitals(metric) {
23+
console.log(metric);
24+
}
25+
2226
export default wrapper.withRedux(withReduxSaga(NodeBird));

ch7/front/pages/_app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ NodeBird.propTypes = {
1919
Component: PropTypes.elementType.isRequired,
2020
};
2121

22+
export function reportWebVitals(metric) {
23+
console.log(metric);
24+
}
25+
2226
export default wrapper.withRedux(NodeBird);

https/back/models/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const hashtag = require('./hashtag');
44
const image = require('./image');
55
const post = require('./post');
66
const user = require('./user');
7+
const report = require('./report');
78

89
const env = process.env.NODE_ENV || 'development';
910
const config = require('../config/config')[env];
@@ -16,6 +17,7 @@ db.Hashtag = hashtag;
1617
db.Image = image;
1718
db.Post = post;
1819
db.User = user;
20+
db.Report = report;
1921

2022
Object.keys(db).forEach(modelName => {
2123
db[modelName].init(sequelize);

https/back/models/post.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = class Post extends Model {
2121
static associate(db) {
2222
db.Post.belongsTo(db.User); // post.addUser, post.getUser, post.setUser
2323
db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' }); // post.addHashtags
24+
db.Post.hasMany(db.Report);
2425
db.Post.hasMany(db.Comment); // post.addComments, post.getComments
2526
db.Post.hasMany(db.Image); // post.addImages, post.getImages
2627
db.Post.belongsToMany(db.User, { through: 'Like', as: 'Likers' }) // post.addLikers, post.removeLikers

https/back/models/report.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const DataTypes = require('sequelize');
2+
const { Model } = DataTypes;
3+
4+
module.exports = class Report extends Model {
5+
static init(sequelize) {
6+
return super.init({
7+
// id가 기본적으로 들어있다.
8+
content: {
9+
type: DataTypes.TEXT,
10+
allowNull: false,
11+
},
12+
}, {
13+
modelName: 'Report',
14+
tableName: 'reports',
15+
charset: 'utf8mb4',
16+
collate: 'utf8mb4_general_ci', // 이모티콘 저장
17+
sequelize,
18+
});
19+
}
20+
static associate(db) {
21+
db.Report.belongsTo(db.User);
22+
db.Report.belongsTo(db.Post);
23+
}
24+
};

https/back/models/user.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module.exports = class Post extends Model {
2828
}
2929
static associate(db) {
3030
db.User.hasMany(db.Post);
31+
db.User.hasMany(db.Report);
3132
db.User.hasMany(db.Comment);
3233
db.User.belongsToMany(db.Post, { through: 'Like', as: 'Liked' })
3334
db.User.belongsToMany(db.User, { through: 'Follow', as: 'Followers', foreignKey: 'FollowingId' });

https/back/package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

https/back/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"multer": "^1.4.2",
2525
"multer-s3": "^2.9.0",
2626
"mysql2": "^2.2.5",
27+
"nodemailer": "^6.4.13",
2728
"passport": "^0.4.1",
2829
"passport-local": "^1.0.0",
2930
"pm2": "^4.5.0",

https/back/routes/post.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ const path = require('path');
44
const fs = require('fs');
55
const multerS3 = require('multer-s3');
66
const AWS = require('aws-sdk');
7+
const nodemailer = require('nodemailer');
78

8-
const { Post, Image, Comment, User, Hashtag } = require('../models');
9+
const { Post, Image, Comment, User, Hashtag, Report } = require('../models');
910
const { isLoggedIn } = require('./middlewares');
1011

12+
const prod = process.env.NODE_ENV === 'production';
1113
const router = express.Router();
1214

1315
try {
@@ -220,6 +222,58 @@ router.post('/:postId/comment', isLoggedIn, async (req, res, next) => { // POST
220222
}
221223
});
222224

225+
router.post('/:postId/report', isLoggedIn, async (req, res, next) => { // POST /post/1/comment
226+
try {
227+
const post = await Post.findOne({
228+
where: { id: req.params.postId },
229+
});
230+
if (!post) {
231+
return res.status(403).send('존재하지 않는 게시글입니다.');
232+
}
233+
const exReport = await Report.findOne({
234+
where: {
235+
PostId: parseInt(req.params.postId, 10),
236+
UserId: req.user.id,
237+
}
238+
});
239+
if (exReport) {
240+
return res.status(403).send('이미 신고하셨습니다.');
241+
}
242+
await Report.create({
243+
content: req.body.content,
244+
PostId: parseInt(req.params.postId, 10),
245+
UserId: req.user.id,
246+
});
247+
const transporter = nodemailer.createTransport({
248+
host: 'smtp.gmail.com',
249+
port: 465,
250+
secure: true,
251+
auth: {
252+
user: 'zerohch0@gmail.com',
253+
pass: process.env.GMAIL_PASSWORD,
254+
},
255+
});
256+
await transporter.verify();
257+
await transporter.sendMail({
258+
from: '"NodeBird 신고내역" <report@nodebird.com>',
259+
to: '"NodeBird 관리자" <zerohch0@gmail.com>',
260+
subject: 'NodeBird - 신고 발생',
261+
// nodebird.com/user/emailAuth/:token
262+
html: `
263+
<div>
264+
<a href="${prod ? 'https://nodebird.com' : 'http://localhost:3060'}/post/${req.params.postId}">신고가 접수되었습니다.</a>
265+
<p>${req.body.content}</p>
266+
</div>
267+
`,
268+
});
269+
console.log('Mail sent');
270+
res.status(201).send('ok');
271+
} catch (error) {
272+
console.error(error);
273+
next(error);
274+
}
275+
});
276+
223277
router.patch('/:postId/like', isLoggedIn, async (req, res, next) => { // PATCH /post/1/like
224278
try {
225279
const post = await Post.findOne({ where: { id: req.params.postId }});

https/front/components/PostCard.js

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { useState, useCallback } from 'react';
1+
import React, { useState, useCallback, useEffect } from 'react';
22
import PropTypes from 'prop-types';
33
import { useSelector, useDispatch } from 'react-redux';
4-
import { Card, Popover, Button, Avatar, List, Comment } from 'antd';
4+
import { message, Card, Popover, Button, Avatar, List, Comment, Modal, Input } from 'antd';
55
import {
66
RetweetOutlined, HeartOutlined, MessageOutlined, EllipsisOutlined, HeartTwoTone,
77
} from '@ant-design/icons';
@@ -16,18 +16,25 @@ import {
1616
REMOVE_POST_REQUEST,
1717
UNLIKE_POST_REQUEST,
1818
RETWEET_REQUEST,
19-
UPDATE_POST_REQUEST,
19+
UPDATE_POST_REQUEST, REPORT_POST_REQUEST,
2020
} from '../reducers/post';
2121
import FollowButton from './FollowButton';
22+
import useInput from '../hooks/useInput';
2223

24+
const { TextArea } = Input;
2325
moment.locale('ko');
2426

2527
const PostCard = ({ post }) => {
2628
const dispatch = useDispatch();
27-
const { removePostLoading } = useSelector((state) => state.post);
2829
const [commentFormOpened, setCommentFormOpened] = useState(false);
29-
const id = useSelector((state) => state.user.me?.id);
30+
const [modalVisible, setModalVisible] = useState(false);
31+
const [reportText, onChangeReportText] = useInput('');
3032
const [editMode, setEditMode] = useState(false);
33+
const removePostLoading = useSelector((state) => state.post.removePostLoading);
34+
const reportPostLoading = useSelector((state) => state.post.reportPostLoading);
35+
const reportPostDone = useSelector((state) => state.post.reportPostDone);
36+
const reportPostError = useSelector((state) => state.post.reportPostError);
37+
const id = useSelector((state) => state.user.me?.id);
3138

3239
const onClickUpdate = useCallback(() => {
3340
setEditMode(true);
@@ -89,6 +96,35 @@ const PostCard = ({ post }) => {
8996
});
9097
}, [id]);
9198

99+
const onClickReport = useCallback(() => {
100+
console.log('신고', post.id);
101+
setModalVisible(true);
102+
});
103+
104+
const onCloseModal = useCallback(() => {
105+
setModalVisible(false);
106+
}, []);
107+
108+
const onSubmitReport = useCallback(() => {
109+
console.log(id, post.id, reportText);
110+
dispatch({
111+
type: REPORT_POST_REQUEST,
112+
data: {
113+
postId: post.id,
114+
content: reportText,
115+
},
116+
});
117+
}, [reportText]);
118+
119+
useEffect(() => {
120+
if (reportPostDone) {
121+
setModalVisible(false);
122+
}
123+
if (reportPostError) {
124+
setModalVisible(false);
125+
}
126+
}, [reportPostDone, reportPostError]);
127+
92128
const liked = post.Likers.find((v) => v.id === id);
93129
return (
94130
<div style={{ marginBottom: 20 }}>
@@ -111,7 +147,7 @@ const PostCard = ({ post }) => {
111147
<Button type="danger" loading={removePostLoading} onClick={onRemovePost}>삭제</Button>
112148
</>
113149
)
114-
: <Button>신고</Button>}
150+
: <Button onClick={onClickReport}>신고</Button>}
115151
</Button.Group>
116152
)}
117153
>
@@ -121,6 +157,17 @@ const PostCard = ({ post }) => {
121157
title={post.RetweetId ? `${post.User.nickname}님이 리트윗하셨습니다.` : null}
122158
extra={id && <FollowButton post={post} />}
123159
>
160+
<Modal
161+
title="신고하기"
162+
visible={modalVisible}
163+
onOk={onSubmitReport}
164+
confirmLoading={reportPostLoading}
165+
onCancel={onCloseModal}
166+
>
167+
<form>
168+
<TextArea value={reportText} onChange={onChangeReportText} />
169+
</form>
170+
</Modal>
124171
{post.RetweetId && post.Retweet
125172
? (
126173
<Card

0 commit comments

Comments
 (0)