Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions backend/src/guards/getNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Elysia, { t } from "elysia";
import { isValidObjectId } from "mongoose";

import NotificationModel, { INotification } from "@back/models/notification";
import exit, { errorElysia } from "@back/utils/error";

const getNotification = new Elysia()
.use(NotificationModel)
.guard({
params: t.Object({
notification_id: t.String({ description: "알림 ID" }),
}),
response: {
...errorElysia(["NOTIFICATION_NOT_FOUND", "INVALID_ID_TYPE"]),
},
})
.resolve(async ({ params, notificationModel, error }): Promise<{ notification: INotification }> => {
const { notification_id } = params;

if (!isValidObjectId(notification_id)) return exit(error, "INVALID_ID_TYPE");

const found = await notificationModel.db.findById(notification_id);

if (!found) return exit(error, "NOTIFICATION_NOT_FOUND");

return { notification: found.toObject() };
})
.as("plugin");

export default getNotification;
4 changes: 4 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ if (Bun.env.NODE_ENV === "development") {
{
name: "Event",
description: "이벤트에 관련된 API입니다.",
},
{
name: "Notification",
description: "알림에 관련된 API입니다.",
}
]
}
Expand Down
109 changes: 109 additions & 0 deletions backend/src/models/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import dayjs from "dayjs";
import { Elysia } from "elysia";
import mongoose, { ObjectId } from "mongoose";

import { IDocument } from "@common/types/db";

export type NotificationType = "event_created";

interface DNotification {
userId: ObjectId;

senderType: "user" | "activity";
senderId: ObjectId;
senderName?: string;
senderImage?: string;

type: NotificationType;
message: string;
data?: Record<string, any>;
read: boolean;
createdAt: string;
}

export type INotification = IDocument<DNotification>;

const notificationSchema = new mongoose.Schema<INotification>({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
senderType: {
type: String,
enum: ["user", "activity"],
required: true,
},
senderId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
},
senderName: String,
senderImage: String,

type: {
type: String,
enum: ["event_created"],
required: true,
},
message: {
type: String,
required: true,
},
data: {
type: Object,
default: {},
},
read: {
type: Boolean,
default: false,
},
createdAt: {
type: String,
default: dayjs().format("YYYY-MM-DD HH:mm:ss"),
},
});

notificationSchema.index({ userId: 1, createdAt: -1 });

const NotificationDB = mongoose.model<INotification>("Notification", notificationSchema);

const createNotification = async ({
userId,
senderType,
senderId,
senderName,
senderImage,
type,
message,
data = {},
}: {
userId: ObjectId;
senderType: "user" | "activity";
senderId: ObjectId;
senderName?: string;
senderImage?: string;
type: NotificationType;
message: string;
data?: Record<string, any>;
}) => {
return NotificationDB.create({
userId,
senderType,
senderId,
senderName,
senderImage,
type,
message,
data,
read: false,
createdAt: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
};

const NotificationModel = new Elysia().decorate("notificationModel", {
db: NotificationDB,
create: createNotification,
});

export default NotificationModel;
39 changes: 36 additions & 3 deletions backend/src/routers/event/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import dayjs from "dayjs";
import Elysia, { t } from "elysia";

import timetableAuthorityService from "@back/guards/timetableAuthorityService";
import ActivityModel from "@back/models/activity";
import EventModel, { eventElysiaSchema } from "@back/models/event";
import JoinedActivityModel from "@back/models/joined_activity";
import NotificationModel from "@back/models/notification";
import TimetableModel from "@back/models/timetable";
import exit, { errorElysia } from "@back/utils/error";

const FORMAT = "YYYY-MM-DD HH:mm:ss";
Expand All @@ -23,9 +27,13 @@ const normalizeRepeat = (repeat: any) => {
const create = new Elysia()
.use(timetableAuthorityService)
.use(EventModel)
.use(TimetableModel)
.use(ActivityModel)
.use(JoinedActivityModel)
.use(NotificationModel)
.post(
"create",
async ({ body, eventModel, error }) => {
async ({ body, eventModel, timetableModel, notificationModel, activityModel, joinedActivityModel, user, error }) => {
try {
const repeat = normalizeRepeat(body.repeat);

Expand All @@ -38,6 +46,31 @@ const create = new Elysia()
repeat,
});

const timetable = await timetableModel.db.findById(body.timetable_id);

if (timetable && timetable.owner_type === "activity") {
const activity = await activityModel.db.findById(timetable.owner);
if (activity) {
const members = await joinedActivityModel.db.find({ activity_id: activity._id });

for (const member of members) {
await notificationModel.create({
userId: member.user_id,
senderType: "activity",
senderId: activity._id.toString(),
senderName: activity.name,
senderImage: activity.logo_url ?? "",
type: "event_created",
message: `${event.title}`,
data: {
eventId: event._id.toString(),
timetableId: timetable._id.toString(),
},
});
}
}
}

return {
success: true,
message: "이벤트 생성 성공",
Expand All @@ -64,10 +97,10 @@ const create = new Elysia()
},
detail: {
tags: ["Event"],
summary: "이벤트 생성",
summary: "이벤트 생성 + 알림",
description: "일반 또는 반복 이벤트를 생성합니다. `repeat` 옵션으로 고급 반복 설정도 가능합니다.",
},
}
);

export default create;
export default create;
4 changes: 3 additions & 1 deletion backend/src/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Elysia from "elysia";
import ActivityRouter from "./activity";
import AuthRouter from "./auth";
import EventRouter from "./event";
import NotificationRouter from "./notification";
import TimetableRouter from "./timetable";

const IndexRouter = new Elysia({
Expand All @@ -12,6 +13,7 @@ const IndexRouter = new Elysia({
.use(AuthRouter)
.use(ActivityRouter)
.use(TimetableRouter)
.use(EventRouter);
.use(EventRouter)
.use(NotificationRouter);

export default IndexRouter;
11 changes: 11 additions & 0 deletions backend/src/routers/notification/[id]/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Elysia from "elysia";

import read from "./read";

const EventIdRouter = new Elysia({
name: "Notification Router",
prefix: ":notification_id",
})
.use(read);

export default EventIdRouter;
36 changes: 36 additions & 0 deletions backend/src/routers/notification/[id]/read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Elysia, { t } from "elysia";

import getNotification from "@back/guards/getNotification";
import getUser from "@back/guards/getUser";
import NotificationModel from "@back/models/notification";

const read = new Elysia()
.use(getUser)
.use(getNotification)
.use(NotificationModel)
.patch(
"read",
async ({ notification, notificationModel }) => {
await notificationModel.db.updateOne(
{ _id: notification._id },
{ $set: { read: true } }
);

return { success: true, message: "알림을 읽음 처리했습니다." };
},
{
response: {
200: t.Object({
success: t.Boolean(),
message: t.String(),
}),
},
detail: {
tags: ["Notification"],
summary: "단일 알림 읽음 처리",
description: "특정 알림을 읽음 처리합니다.",
},
}
);

export default read;
15 changes: 15 additions & 0 deletions backend/src/routers/notification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Elysia from "elysia";

import NotificationIdRouter from "./[id]";
import list from "./list";
import read from "./read";

const NotificationRouter = new Elysia({
name: "Notification Router",
prefix: "notification",
})
.use(NotificationIdRouter)
.use(read)
.use(list);

export default NotificationRouter;
69 changes: 69 additions & 0 deletions backend/src/routers/notification/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Elysia, { t } from "elysia";

import getUser from "@back/guards/getUser";
import NotificationModel from "@back/models/notification";

const list = new Elysia()
.use(getUser)
.use(NotificationModel)
.get(
"list",
async ({ query, user, notificationModel }) => {
const { read } = query;

const filter: Record<string, any> = { userId: user._id };
if (read !== undefined) {
filter.read = read === "true";
}

const notifications = await notificationModel.db
.find(filter)
.sort({ createdAt: -1 })
.lean();

return {
success: true,
data: notifications.map((n) => ({
_id: n._id.toString(),
senderType: n.senderType,
senderId: n.senderId.toString(),
senderName: n.senderName,
senderImage: n.senderImage,
type: n.type,
message: n.message,
data: n.data,
read: n.read,
createdAt: n.createdAt,
})),
};
},
{
query: t.Object({
read: t.Optional(t.String({ description: "읽음 여부 필터: true / false" })),
}),
response: t.Object({
success: t.Boolean(),
data: t.Array(
t.Object({
_id: t.String(),
senderType: t.Enum({ user: "user", activity: "activity" }),
senderId: t.String(),
senderName: t.Optional(t.String()),
senderImage: t.Optional(t.String()),
type: t.String(),
message: t.String(),
data: t.Optional(t.Record(t.String(), t.Any())),
read: t.Boolean(),
createdAt: t.String(),
})
),
}),
detail: {
tags: ["Notification"],
summary: "알림 목록 조회",
description: "사용자의 알림 목록을 최신순으로 반환합니다. `read=true/false`로 필터링할 수 있습니다.",
},
}
);

export default list;
Loading