Skip to content

Commit bf00090

Browse files
authored
Merge pull request #81 from 9roomMoa/feat-23
Feat 23
2 parents 4d8e176 + 1355f67 commit bf00090

9 files changed

Lines changed: 229 additions & 12 deletions

File tree

src/controllers/noti-controller.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { StatusCodes } = require('http-status-codes');
2+
const notiService = require('../services/noti-service');
3+
4+
exports.getNotifications = async (req, res) => {
5+
try {
6+
const userId = req.user?.sub;
7+
8+
if (!userId) {
9+
return res.status(StatusCodes.BAD_REQUEST).json({
10+
success: false,
11+
message: 'User ID Omission',
12+
});
13+
}
14+
15+
const notifications = await notiService.getNotifications(userId);
16+
17+
return res.status(StatusCodes.OK).json({
18+
data: notifications,
19+
success: true,
20+
meessage: 'Notifications retrieved successfully',
21+
});
22+
} catch (err) {
23+
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
24+
success: false,
25+
message: 'Internal server error: ' + err.message,
26+
});
27+
}
28+
};

src/models/Notification.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const { required } = require('joi');
2+
const mongoose = require('mongoose');
3+
4+
const NotificationSchema = mongoose.Schema(
5+
{
6+
message: { type: String, required: true },
7+
sender: {
8+
type: mongoose.Schema.Types.ObjectId,
9+
ref: 'User',
10+
default: null,
11+
},
12+
receiver: {
13+
type: mongoose.Schema.Types.ObjectId,
14+
ref: 'User',
15+
required: true,
16+
},
17+
noticeType: {
18+
type: String,
19+
enum: [
20+
'task_assigned',
21+
'task_updated',
22+
'task_deadline',
23+
'comment_added',
24+
'document_uploaded',
25+
],
26+
},
27+
relatedTask: {
28+
type: mongoose.Schema.Types.ObjectId,
29+
ref: 'Task',
30+
default: null,
31+
},
32+
relatedComment: {
33+
type: mongoose.Schema.Types.ObjectId,
34+
ref: 'Comment',
35+
default: null,
36+
},
37+
relatedDocument: {
38+
type: mongoose.Schema.Types.ObjectId,
39+
default: null,
40+
},
41+
isRead: {
42+
type: Boolean,
43+
default: false,
44+
},
45+
},
46+
{ timestamps: true }
47+
);
48+
49+
module.exports = mongoose.model('Notification', NotificationSchema);

src/routes/project-route.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const router = express.Router();
33
const projectController = require('../controllers/project-controller');
44
const userController = require('../controllers/user-controller');
55
const authMiddleware = require('../middlewares/verify-token');
6+
const notiController = require('../controllers/noti-controller');
67

78
router.post('/', authMiddleware.verifyToken, projectController.createProject);
89

@@ -18,4 +19,10 @@ router.get(
1819
userController.getUserInfoByKeyword
1920
);
2021

22+
router.get(
23+
'/:pid/notifications',
24+
authMiddleware.verifyToken,
25+
notiController.getNotifications
26+
);
27+
2128
module.exports = router;

src/services/comment-service.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const Task = require('../models/Task');
22
const User = require('../models/User');
33
const Comment = require('../models/Comment');
44
const taskUtil = require('../utils/task-util');
5+
const User = require('../models/User');
6+
const Notification = require('../models/Notification');
57

68
exports.searchComments = async (keyword, taskId, userId) => {
79
try {
@@ -51,6 +53,23 @@ exports.createComment = async (commentData) => {
5153
const comment = new Comment(commentData);
5254
await comment.save();
5355

56+
const user = await User.findById(commentData.commenterId);
57+
const tasks = await Task.findById(commentData.taskId).select('assignedTo');
58+
59+
const notifications = tasks.assignedTo
60+
.filter((uid) => uid.toString() !== commentData.commenterId.toString())
61+
.map((uid) => ({
62+
message: `${user.name}님께서 코멘트를 남기셨습니다.`,
63+
receiver: uid,
64+
sender: commentData.commenterId,
65+
noticeType: 'comment_added',
66+
relatedComment: comment._id,
67+
}));
68+
69+
if (notifications.length > 0) {
70+
await Notification.insertMany(notifications);
71+
}
72+
5473
return comment;
5574
} catch (err) {
5675
console.error(err);

src/services/docs-service.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { GridFSBucket } = require('mongodb');
33
// const { ObjectId } = require('bson');
44
const taskUtil = require('../utils/task-util');
55
const Task = require('../models/Task');
6+
const Notification = require('../models/Notification');
67

78
let bucket;
89

@@ -37,12 +38,38 @@ exports.postDocument = async (file, taskId, userId) => {
3738

3839
uploadStream.end(file.buffer);
3940

40-
uploadStream.on('finish', () => {
41-
resolve({
42-
success: true,
43-
message: 'Document uploaded successfully',
44-
fileId: uploadStream.id,
45-
});
41+
uploadStream.on('finish', async () => {
42+
try {
43+
const task = await Task.findById(taskId).select('assignedTo title');
44+
45+
const notifications = task.assignedTo
46+
.filter((uid) => uid.toString() !== userId.toString())
47+
.map((uid) => ({
48+
message: `[${task.title}] 업무에 ${file.originalname} 문서가 업로드 되었습니다.`,
49+
receiver: uid,
50+
sender: userId,
51+
noticeType: 'document_uploaded',
52+
relatedTask: taskId,
53+
relatedDocument: uploadStream.id,
54+
}));
55+
56+
if (notifications.length > 0) {
57+
await Notification.insertMany(notifications);
58+
}
59+
60+
resolve({
61+
success: true,
62+
message: 'Document uploaded successfully',
63+
fileId: uploadStream.id,
64+
});
65+
} catch (err) {
66+
console.error(err.message);
67+
resolve({
68+
success: true,
69+
message: 'Document uploaded, but failed to notify users',
70+
fileId: uploadStream.id,
71+
});
72+
}
4673
});
4774

4875
uploadStream.on('error', (err) => {

src/services/noti-service.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const taskUtil = require('../utils/task-util');
2+
3+
exports.getNotifications = async (userId) => {
4+
try {
5+
const [notifications, unreadCount] = Promise.all([
6+
await Notification.find({ receiver: userId }).sort({ createdAt: -1 }),
7+
await Notification.countDocuments({ isRead: false, receiver: userId }),
8+
]);
9+
10+
return {
11+
notifications,
12+
unreadCount,
13+
};
14+
} catch (err) {
15+
console.error(err);
16+
throw err;
17+
}
18+
};

src/services/task-service.js

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const Project = require('../models/Project');
22
const Task = require('../models/Task');
33
const User = require('../models/User');
4+
const Notification = require('../models/Notification');
45
const taskUtil = require('../utils/task-util');
56

67
exports.createTask = async (taskData, userId) => {
@@ -19,6 +20,21 @@ exports.createTask = async (taskData, userId) => {
1920
const task = new Task({ ...taskData, createdBy: userId });
2021
await task.save();
2122

23+
if (Array.isArray(task.assignedTo) && task.assignedTo.length > 0) {
24+
const notifications = task.assignedTo
25+
.filter((uid) => uid.toString() !== userId.toString())
26+
.map((uid) => ({
27+
message: `${task.title} 업무가 할당되었습니다.`,
28+
receiver: uid,
29+
sender: userId,
30+
noticeType: 'task_assigned',
31+
relatedTask: task._id,
32+
}));
33+
34+
if (notifications.length > 0)
35+
await Notification.insertMany(notifications);
36+
}
37+
2238
return task;
2339
} catch (err) {
2440
console.error(err);
@@ -94,15 +110,31 @@ exports.updateTaskInfo = async (taskData, taskId, userId) => {
94110
throw new Error('date data are invalidate');
95111
}
96112

97-
const result = await Task.findByIdAndUpdate(
113+
const task = await Task.findByIdAndUpdate(
98114
taskId,
99115
{
100116
$set: taskData,
101117
},
102118
{ new: true, runValidators: true }
103119
);
104120

105-
return result;
121+
if (Array.isArray(task.assignedTo) && task.assignedTo.length > 0) {
122+
const notifications = task.assignedTo
123+
.filter((uid) => uid.toString !== userId.toString)
124+
.map((uid) => ({
125+
message: `${task.title} 업무가 업데이트 되었습니다.`,
126+
receiver: uid,
127+
sender: userId,
128+
noticeType: 'task_updated',
129+
relatedTask: task._id,
130+
}));
131+
132+
if (notifications.length > 0) {
133+
await Notification.insertMany(notifications);
134+
}
135+
}
136+
137+
return task;
106138
} catch (err) {
107139
console.error(err);
108140
throw new Error('Error occured during updating task');
@@ -171,14 +203,31 @@ exports.addManagers = async (taskId, userId, managerId) => {
171203
}
172204
await taskUtil.isExistingResource(User, managerId);
173205

174-
const updatedTask = Task.findByIdAndUpdate(
206+
const alreadyAssigned = task.assignedTo.map((uid) => uid.toString());
207+
208+
const updatedTask = await Task.findByIdAndUpdate(
175209
taskId,
176210
{
177211
$addToSet: { assignedTo: managerId },
178212
},
179213
{ new: true }
180214
);
181215

216+
if (
217+
!alreadyAssigned.includes(managerId.toString()) &&
218+
updatedTask.assignedTo.length > 0
219+
) {
220+
const notification = {
221+
message: `${updatedTask.title} 업무가 할당되었습니다.`,
222+
receiver: managerId,
223+
sender: userId,
224+
noticeType: 'task_assigned',
225+
relatedTask: updatedTask._id,
226+
};
227+
228+
await Notification.create(notification);
229+
}
230+
182231
return updatedTask;
183232
} catch (err) {
184233
console.error(err);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const Joi = require('joi');
2+
3+
exports.creatingNotificationSchema = Joi.object({
4+
message: Joi.string().required(),
5+
sender: Joi.string().required(),
6+
receiver: Joi.string().required(),
7+
noticeType: Joi.string().valid([
8+
'task_assigned',
9+
'task_updated',
10+
'task_deadline',
11+
'comment_added',
12+
'document_uploaded',
13+
'project_added',
14+
]),
15+
relatedTask: Joi.string().optional(),
16+
relatedComment: Joi.string().optional(),
17+
relatedComment: Joi.string().optional(),
18+
relatedDocument: Joi.string().optional(),
19+
isRead: Joi.string().required(),
20+
});

src/validation/task-validation.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ exports.creatingSchema = Joi.object({
77
status: Joi.string()
88
.valid('To Do', 'In Progress', 'Done', 'Issue', 'Hold')
99
.default('To Do'),
10-
priority: Joi.number().integer().min(0).max(4).default(0),
10+
priority: Joi.number().integer().min(1).max(5).default(1),
1111
project: Joi.string().required(), // 프로젝트 ID
1212
assignedTo: Joi.array().items(Joi.string()).optional(), // 사용자 ID
1313
// createdBy: Joi.string().required(), // 생성자 ID
1414
projectScope: Joi.string().valid('Public', 'Restricted').default('Public'),
1515
startDate: Joi.date(),
1616
dueDate: Joi.date(),
17-
isDaily: Joi.boolean().default('false'),
17+
isDaily: Joi.boolean().default(false),
1818
});
1919

2020
//상태, 공개여부, 일정, 우선선위
2121
exports.updatingSchema = Joi.object({
2222
status: Joi.string()
23-
.valid('To Do', 'In Progress', 'Done', 'Issue')
23+
.valid('To Do', 'In Progress', 'Done', 'Issue', 'Hold')
2424
.optional(),
2525
priority: Joi.number().integer().min(1).max(5).optional(),
2626
startDate: Joi.date().optional(),

0 commit comments

Comments
 (0)