From dd5cd0304ef70bcc8f1d38def9446667dd59ef14 Mon Sep 17 00:00:00 2001 From: leemhoon00 Date: Wed, 3 Dec 2025 13:24:52 +0900 Subject: [PATCH 1/6] =?UTF-8?q?notification=20event=20payload=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/event-payload.types.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/infrastructure/event/event-payload.types.ts diff --git a/src/infrastructure/event/event-payload.types.ts b/src/infrastructure/event/event-payload.types.ts new file mode 100644 index 0000000..48c2dd1 --- /dev/null +++ b/src/infrastructure/event/event-payload.types.ts @@ -0,0 +1,28 @@ +export interface EventPayloadMap { + 'notification:FEED_LIKE': { feedId: string; likeCount: number }; + 'notification:FEED_MENTION': { + actorId: string; + feedId: string; + mentionedUserId?: string | null; + }; + 'notification:FEED_REPLY': { + actorId: string; + feedId: string; + parentId?: string | null; + }; + 'notification:FEED_COMMENT': { actorId: string; feedId: string }; + + 'notification:POST_MENTION': { + actorId: string; + postId: string; + mentionedUserId?: string | null; + }; + 'notification:POST_REPLY': { + actorId: string; + postId: string; + parentId?: string | null; + }; + 'notification:POST_COMMENT': { actorId: string; postId: string }; + + 'notification:FOLLOW': { actorId: string; userId: string }; +} From 27880914845d7eabcdb6656c6d25e63dfdf971d4 Mon Sep 17 00:00:00 2001 From: leemhoon00 Date: Wed, 3 Dec 2025 15:34:59 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20eventEmitter=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EB=8A=94=20=EC=AA=BD=20TypedEventEmitter?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 ++ src/infrastructure/event/event-payload.types.ts | 2 ++ src/infrastructure/event/event.module.ts | 9 +++++++++ src/infrastructure/event/typed-event-emitter.ts | 12 ++++++++++++ src/module/feed-comment/feed-comment.service.ts | 4 ++-- src/module/feed/feed.service.ts | 4 ++-- src/module/post-comment/post-comment.service.ts | 4 ++-- src/module/user/user.service.ts | 5 ++--- 8 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 src/infrastructure/event/event.module.ts create mode 100644 src/infrastructure/event/typed-event-emitter.ts diff --git a/src/app.module.ts b/src/app.module.ts index b455988..275bed4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -25,6 +25,7 @@ import { ChatModule } from './module/chat/chat.module'; import { AppController } from './app.controller'; import { ImageModule } from './module/image/image.module'; import { PushModule } from './module/push/push.module'; +import { EventModule } from './infrastructure/event/event.module'; @Module({ imports: [ @@ -56,6 +57,7 @@ import { PushModule } from './module/push/push.module'; delimiter: ':', wildcard: true, }), + EventModule, ClsModule.forRoot({ global: true, middleware: { mount: true }, diff --git a/src/infrastructure/event/event-payload.types.ts b/src/infrastructure/event/event-payload.types.ts index 48c2dd1..0464ee2 100644 --- a/src/infrastructure/event/event-payload.types.ts +++ b/src/infrastructure/event/event-payload.types.ts @@ -26,3 +26,5 @@ export interface EventPayloadMap { 'notification:FOLLOW': { actorId: string; userId: string }; } + +export type EventName = keyof EventPayloadMap; diff --git a/src/infrastructure/event/event.module.ts b/src/infrastructure/event/event.module.ts new file mode 100644 index 0000000..3a18f3c --- /dev/null +++ b/src/infrastructure/event/event.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { TypedEventEmitter } from './typed-event-emitter'; + +@Global() +@Module({ + providers: [TypedEventEmitter], + exports: [TypedEventEmitter], +}) +export class EventModule {} diff --git a/src/infrastructure/event/typed-event-emitter.ts b/src/infrastructure/event/typed-event-emitter.ts new file mode 100644 index 0000000..e05c996 --- /dev/null +++ b/src/infrastructure/event/typed-event-emitter.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { EventName, EventPayloadMap } from './event-payload.types'; + +@Injectable() +export class TypedEventEmitter { + constructor(private readonly eventEmitter: EventEmitter2) {} + + emit(event: K, payload: EventPayloadMap[K]) { + return this.eventEmitter.emit(event, payload); + } +} diff --git a/src/module/feed-comment/feed-comment.service.ts b/src/module/feed-comment/feed-comment.service.ts index a413c7a..24fdb8d 100644 --- a/src/module/feed-comment/feed-comment.service.ts +++ b/src/module/feed-comment/feed-comment.service.ts @@ -2,7 +2,7 @@ import { Injectable, HttpException } from '@nestjs/common'; import { FeedCommentReader } from './repository/feed-comment.reader'; import { FeedCommentWriter } from './repository/feed-comment.writer'; import { getImageUrl } from 'src/shared/util/get-image-url'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter'; import { Transactional } from '@nestjs-cls/transactional'; @Injectable() @@ -10,7 +10,7 @@ export class FeedCommentService { constructor( private readonly feedCommentReader: FeedCommentReader, private readonly feedCommentWriter: FeedCommentWriter, - private readonly eventEmitter: EventEmitter2, + private readonly eventEmitter: TypedEventEmitter, ) {} async create(userId: string, input: CreateFeedCommentInput) { diff --git a/src/module/feed/feed.service.ts b/src/module/feed/feed.service.ts index 3bb14c8..31e1834 100644 --- a/src/module/feed/feed.service.ts +++ b/src/module/feed/feed.service.ts @@ -5,7 +5,7 @@ import { SearchService } from 'src/database/search/search.service'; import { DdbService } from 'src/database/ddb/ddb.service'; import { RedisService } from 'src/database/redis/redis.service'; import { getImageUrl } from 'src/shared/util/get-image-url'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter'; import { Transactional } from '@nestjs-cls/transactional'; import { UserReader } from '../user/repository/user.reader'; @@ -17,7 +17,7 @@ export class FeedService { private ddb: DdbService, private redisService: RedisService, @Inject(SearchService) private searchService: SearchService, - private eventEmitter: EventEmitter2, + private eventEmitter: TypedEventEmitter, private userReader: UserReader, ) {} diff --git a/src/module/post-comment/post-comment.service.ts b/src/module/post-comment/post-comment.service.ts index 6f9a810..7c3124c 100644 --- a/src/module/post-comment/post-comment.service.ts +++ b/src/module/post-comment/post-comment.service.ts @@ -1,7 +1,7 @@ import { Injectable, HttpException } from '@nestjs/common'; import { PostCommentReader } from './repository/post-comment.reader'; import { PostCommentWriter } from './repository/post-comment.writer'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter'; import { Transactional } from '@nestjs-cls/transactional'; @Injectable() @@ -9,7 +9,7 @@ export class PostCommentService { constructor( private readonly postCommentReader: PostCommentReader, private readonly postCommentWriter: PostCommentWriter, - private eventEmitter: EventEmitter2, + private eventEmitter: TypedEventEmitter, ) {} async create(input: CreateInput) { diff --git a/src/module/user/user.service.ts b/src/module/user/user.service.ts index c2fbd1d..c5009ab 100644 --- a/src/module/user/user.service.ts +++ b/src/module/user/user.service.ts @@ -9,7 +9,7 @@ import { RedisService } from 'src/database/redis/redis.service'; import { getImageUrl } from 'src/shared/util/get-image-url'; import { removeHtml } from 'src/shared/util/remove-html'; import { AlbumReader } from '../album/repository/album.reader'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter'; import { Transactional } from '@nestjs-cls/transactional'; const linkSeparator = '|~|'; @@ -24,7 +24,7 @@ export class UserService { private postReader: PostReader, private redisService: RedisService, private albumReader: AlbumReader, - private eventEmitter: EventEmitter2, + private eventEmitter: TypedEventEmitter, ) {} async updateProfileImage(userId: string, imageName: string | null) { @@ -107,7 +107,6 @@ export class UserService { if (targetUser.subscription.includes('FOLLOW')) { this.eventEmitter.emit('notification:FOLLOW', { - type: 'FOLLOW', actorId: userId, userId: targetUserId, }); From 7cd5c5311d94f1a161e2d75ccdc31d3a929d1e88 Mon Sep 17 00:00:00 2001 From: leemhoon00 Date: Wed, 3 Dec 2025 17:25:01 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20notification.listener=EC=97=90?= =?UTF-8?q?=EC=84=9C=20payloadMapper=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/event-payload.types.ts | 8 ++-- .../notification/notification.listener.ts | 38 ++++++++++++---- src/module/notification/types/event.d.ts | 43 ------------------- 3 files changed, 33 insertions(+), 56 deletions(-) delete mode 100644 src/module/notification/types/event.d.ts diff --git a/src/infrastructure/event/event-payload.types.ts b/src/infrastructure/event/event-payload.types.ts index 0464ee2..6d7ff8d 100644 --- a/src/infrastructure/event/event-payload.types.ts +++ b/src/infrastructure/event/event-payload.types.ts @@ -3,24 +3,24 @@ export interface EventPayloadMap { 'notification:FEED_MENTION': { actorId: string; feedId: string; - mentionedUserId?: string | null; + mentionedUserId: string; }; 'notification:FEED_REPLY': { actorId: string; feedId: string; - parentId?: string | null; + parentId: string; }; 'notification:FEED_COMMENT': { actorId: string; feedId: string }; 'notification:POST_MENTION': { actorId: string; postId: string; - mentionedUserId?: string | null; + mentionedUserId: string; }; 'notification:POST_REPLY': { actorId: string; postId: string; - parentId?: string | null; + parentId: string; }; 'notification:POST_COMMENT': { actorId: string; postId: string }; diff --git a/src/module/notification/notification.listener.ts b/src/module/notification/notification.listener.ts index a17ddda..80b3fce 100644 --- a/src/module/notification/notification.listener.ts +++ b/src/module/notification/notification.listener.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import type * as Event from './types/event'; import { getImageUrl } from 'src/shared/util/get-image-url'; import { PrismaService } from 'src/database/prisma/prisma.service'; import { kyselyUuid } from 'src/shared/util/convert-uuid'; import { GlobalGateway } from '../websocket/global.gateway'; import { RedisService } from 'src/database/redis/redis.service'; import { PushService } from '../push/push.service'; +import { EventPayloadMap } from 'src/infrastructure/event/event-payload.types'; function getProfileLink(url: string) { return `${process.env.SERVICE_URL}/${url}`; @@ -30,7 +30,10 @@ export class NotificationListener { ) {} @OnEvent('notification:FOLLOW') - async handleFollowEvent({ actorId, userId }: Event.FollowEvent) { + async handleFollowEvent({ + actorId, + userId, + }: EventPayloadMap['notification:FOLLOW']) { const user = await this.prisma.user.findUnique({ where: { id: userId }, select: { subscription: true }, @@ -82,7 +85,10 @@ export class NotificationListener { } @OnEvent('notification:FEED_LIKE') - async handleFeedLikeEvent({ feedId, likeCount }: Event.FeedLikeEvent) { + async handleFeedLikeEvent({ + feedId, + likeCount, + }: EventPayloadMap['notification:FEED_LIKE']) { const [result] = await this.prisma.$kysely .selectFrom('Feed') .innerJoin('User', 'Feed.authorId', 'User.id') @@ -135,7 +141,10 @@ export class NotificationListener { } @OnEvent('notification:FEED_COMMENT') - async handleFeedComment({ feedId, actorId }: Event.FeedCommentEvent) { + async handleFeedComment({ + feedId, + actorId, + }: EventPayloadMap['notification:FEED_COMMENT']) { const [result] = await this.prisma.$kysely .selectFrom('Feed') .innerJoin('User', 'Feed.authorId', 'User.id') @@ -198,7 +207,11 @@ export class NotificationListener { } @OnEvent('notification:FEED_REPLY') - async handleFeedReply({ feedId, actorId, parentId }: Event.FeedReplyEvent) { + async handleFeedReply({ + feedId, + actorId, + parentId, + }: EventPayloadMap['notification:FEED_REPLY']) { const [result] = await this.prisma.$kysely .selectFrom('FeedComment') .innerJoin('User', 'FeedComment.writerId', 'User.id') @@ -266,7 +279,7 @@ export class NotificationListener { feedId, actorId, mentionedUserId, - }: Event.FeedMentionEvent) { + }: EventPayloadMap['notification:FEED_MENTION']) { const mentionedUser = await this.prisma.user.findUnique({ where: { id: mentionedUserId }, select: { id: true, subscription: true }, @@ -324,7 +337,10 @@ export class NotificationListener { } @OnEvent('notification:POST_COMMENT') - async handlePostComment({ postId, actorId }: Event.PostCommentEvent) { + async handlePostComment({ + postId, + actorId, + }: EventPayloadMap['notification:POST_COMMENT']) { const [author] = await this.prisma.$kysely .selectFrom('Post') .innerJoin('User', 'Post.authorId', 'User.id') @@ -388,7 +404,11 @@ export class NotificationListener { } @OnEvent('notification:POST_REPLY') - async handlePostReply({ postId, actorId, parentId }: Event.PostReplyEvent) { + async handlePostReply({ + postId, + actorId, + parentId, + }: EventPayloadMap['notification:POST_REPLY']) { const [result] = await this.prisma.$kysely .selectFrom('PostComment') .innerJoin('User', 'PostComment.writerId', 'User.id') @@ -461,7 +481,7 @@ export class NotificationListener { postId, actorId, mentionedUserId, - }: Event.PostMentionEvent) { + }: EventPayloadMap['notification:POST_MENTION']) { if (actorId === mentionedUserId) return; const mentionedUser = await this.prisma.user.findUnique({ diff --git a/src/module/notification/types/event.d.ts b/src/module/notification/types/event.d.ts deleted file mode 100644 index c511ba8..0000000 --- a/src/module/notification/types/event.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface FollowEvent { - actorId: string; - userId: string; -} - -export interface FeedLikeEvent { - feedId: string; - likeCount: number; -} - -export interface FeedCommentEvent { - feedId: string; - actorId: string; -} - -export interface FeedReplyEvent { - feedId: string; - actorId: string; - parentId: string; -} - -export interface FeedMentionEvent { - feedId: string; - actorId: string; - mentionedUserId: string; -} - -export interface PostCommentEvent { - postId: string; - actorId: string; -} - -export interface PostReplyEvent { - postId: string; - actorId: string; - parentId: string; -} - -export interface PostMentionEvent { - postId: string; - actorId: string; - mentionedUserId: string; -} From fc039909b154a78dd07f7c2a7fa87fc92757e497 Mon Sep 17 00:00:00 2001 From: leemhoon00 Date: Sun, 7 Dec 2025 18:17:41 +0900 Subject: [PATCH 4/6] dto 1.0.32 --- share/package.json | 2 +- src/module/feed/dto/feed.response.ts | 4 ++++ test/e2e/user/get-user-profile.e2e-spec.ts | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/share/package.json b/share/package.json index 376b4cf..e1dab4e 100644 --- a/share/package.json +++ b/share/package.json @@ -1,6 +1,6 @@ { "name": "@grimity/dto", - "version": "1.0.31", + "version": "1.0.32", "types": "dist/share/index.d.ts", "exports": { ".": { diff --git a/src/module/feed/dto/feed.response.ts b/src/module/feed/dto/feed.response.ts index 61742ca..a700db1 100644 --- a/src/module/feed/dto/feed.response.ts +++ b/src/module/feed/dto/feed.response.ts @@ -9,6 +9,7 @@ import { } from '../../../shared/response/cursor.response'; import { FeedCommentBaseResponse } from 'src/module/feed-comment/dto'; import { FeedBaseResponse, FeedResponse } from './feed.base.response'; +import { AlbumBaseResponse } from 'src/module/album/dto'; export class SearchedFeedResponse extends FeedBaseResponse { @ApiProperty({ type: UserBaseResponse }) @@ -77,6 +78,9 @@ export class FeedDetailResponse extends FeedResponse { @ApiProperty() commentCount: number; + @ApiProperty({ type: AlbumBaseResponse, nullable: true }) + album: AlbumBaseResponse | null; + @ApiProperty({ type: UserBaseWithBlockedResponse }) author: UserBaseWithBlockedResponse; } diff --git a/test/e2e/user/get-user-profile.e2e-spec.ts b/test/e2e/user/get-user-profile.e2e-spec.ts index 65682f6..325597a 100644 --- a/test/e2e/user/get-user-profile.e2e-spec.ts +++ b/test/e2e/user/get-user-profile.e2e-spec.ts @@ -93,7 +93,7 @@ describe('GET /users/:id - 유저 프로필 조회', () => { await prisma.block.createMany({ data: [ - { blockerId: me.id, blockingId: targetUser.id }, + // { blockerId: me.id, blockingId: targetUser.id }, { blockerId: targetUser.id, blockingId: me.id }, ], }); @@ -117,7 +117,7 @@ describe('GET /users/:id - 유저 프로필 조회', () => { feedCount: 0, postCount: 0, isFollowing: true, - isBlocking: true, + isBlocking: false, isBlocked: true, url: 'test2', albums: [], From cd98a9bb1ee16476a47314f163cd1ad5c74a33b3 Mon Sep 17 00:00:00 2001 From: leemhoon00 Date: Fri, 12 Dec 2025 16:02:02 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20push=20event=EB=A5=BC=20typedEv?= =?UTF-8?q?entEmitter=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/infrastructure/event/event-payload.types.ts | 13 +++++++++++++ src/module/chat/chat-message.service.ts | 9 ++++----- src/module/push/push.service.ts | 3 ++- src/shared/types/push-payload.d.ts | 10 ---------- test/e2e/chat-message/get-chat-messages.e2e-spec.ts | 1 + 5 files changed, 20 insertions(+), 16 deletions(-) delete mode 100644 src/shared/types/push-payload.d.ts diff --git a/src/infrastructure/event/event-payload.types.ts b/src/infrastructure/event/event-payload.types.ts index 6d7ff8d..e04c336 100644 --- a/src/infrastructure/event/event-payload.types.ts +++ b/src/infrastructure/event/event-payload.types.ts @@ -25,6 +25,19 @@ export interface EventPayloadMap { 'notification:POST_COMMENT': { actorId: string; postId: string }; 'notification:FOLLOW': { actorId: string; userId: string }; + + push: PushPayload; +} + +export interface PushPayload { + userId: string; + title: string; + text: string; + imageUrl?: string | null; + data?: Record; + silent?: boolean; + key?: string; + badge?: number; } export type EventName = keyof EventPayloadMap; diff --git a/src/module/chat/chat-message.service.ts b/src/module/chat/chat-message.service.ts index 9a73a8a..6f5c673 100644 --- a/src/module/chat/chat-message.service.ts +++ b/src/module/chat/chat-message.service.ts @@ -4,7 +4,7 @@ import { ChatReader } from './repository/chat.reader'; import { getImageUrl } from 'src/shared/util/get-image-url'; import { UserReader } from '../user/repository/user.reader'; import { RedisService } from 'src/database/redis/redis.service'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter'; @Injectable() export class ChatMessageService { @@ -14,7 +14,7 @@ export class ChatMessageService { private readonly chatReader: ChatReader, private readonly userReader: UserReader, private readonly redisService: RedisService, - private readonly eventEmitter: EventEmitter2, + private readonly eventEmitter: TypedEventEmitter, ) { this.redisClient = this.redisService.pubClient; } @@ -159,7 +159,7 @@ export class ChatMessageService { }); } if (!targetUserJoinedChat) { - const pushPayload: PushPayload = { + this.eventEmitter.emit(`push`, { userId: targetUserStatus.userId, title: `${myInfo.name}`, text: @@ -174,8 +174,7 @@ export class ChatMessageService { }, key: `chat-message-${chatId}`, badge: targetUserStatus.unreadCount + toCreateMessages.length, - }; - this.eventEmitter.emit(`push`, pushPayload); + }); } return; diff --git a/src/module/push/push.service.ts b/src/module/push/push.service.ts index c091762..1b2ddf0 100644 --- a/src/module/push/push.service.ts +++ b/src/module/push/push.service.ts @@ -3,6 +3,7 @@ import { PushRepository } from './push.repository'; import * as admin from 'firebase-admin'; import { OnEvent } from '@nestjs/event-emitter'; import { ConfigService } from '@nestjs/config'; +import { EventPayloadMap } from 'src/infrastructure/event/event-payload.types'; @Injectable() export class PushService implements OnModuleInit { @@ -37,7 +38,7 @@ export class PushService implements OnModuleInit { } @OnEvent('push') - async pushNotification({ userId, ...data }: PushPayload) { + async pushNotification({ userId, ...data }: EventPayloadMap['push']) { if (!this.app) { console.error('Firebase Admin SDK is not initialized'); return; diff --git a/src/shared/types/push-payload.d.ts b/src/shared/types/push-payload.d.ts deleted file mode 100644 index 81d1b0c..0000000 --- a/src/shared/types/push-payload.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface PushPayload { - userId: string; - title: string; - text: string; - imageUrl?: string | null; - data?: Record; - silent?: boolean; - key?: string; - badge?: number; -} diff --git a/test/e2e/chat-message/get-chat-messages.e2e-spec.ts b/test/e2e/chat-message/get-chat-messages.e2e-spec.ts index 96a4273..24de836 100644 --- a/test/e2e/chat-message/get-chat-messages.e2e-spec.ts +++ b/test/e2e/chat-message/get-chat-messages.e2e-spec.ts @@ -272,6 +272,7 @@ describe('GET /chat-messages?chatId - 채팅방 별 메세지 조회', () => { chatId: chat.id, userId: me.id, content: `test`, + createdAt: new Date(Date.now() - 1000), }, }); From 9b5b9cbe530c02b478f04539dc2de3b159275c51 Mon Sep 17 00:00:00 2001 From: leemhoon00 Date: Fri, 12 Dec 2025 16:38:24 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20redis=EC=9A=A9=20event=20pub/su?= =?UTF-8?q?b=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EB=94=B0=EB=A1=9C=20wrapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/database/redis/redis-event.types.ts | 43 +++++++++++++++++ src/database/redis/redis.module.ts | 5 +- src/database/redis/redis.service.ts | 11 +++-- src/database/redis/typed-redis-publisher.ts | 16 +++++++ .../event/event-payload.types.ts | 8 ++++ src/module/chat/chat-message.service.ts | 48 ++++++++++--------- src/module/chat/chat.listener.ts | 22 +++------ src/module/chat/chat.service.ts | 10 ++-- 8 files changed, 114 insertions(+), 49 deletions(-) create mode 100644 src/database/redis/redis-event.types.ts create mode 100644 src/database/redis/typed-redis-publisher.ts diff --git a/src/database/redis/redis-event.types.ts b/src/database/redis/redis-event.types.ts new file mode 100644 index 0000000..2c41046 --- /dev/null +++ b/src/database/redis/redis-event.types.ts @@ -0,0 +1,43 @@ +export interface RedisEventPayloadMap { + newChatMessage: { + chatId: string; + senderId: string; + chatUsers: { + id: string; + name: string; + image: string | null; + url: string; + unreadCount: number; + }[]; + messages: { + id: string; + content: string | null; + image: string | null; + createdAt: Date; + replyTo: { + id: string; + content: string | null; + image: string | null; + createdAt: Date; + } | null; + }[]; + }; + + likeChatMessage: { + messageId: string; + }; + + unlikeChatMessage: { + messageId: string; + }; + + deleteChat: { + chatIds: string[]; + }; +} + +export type RedisEventName = keyof RedisEventPayloadMap; + +// Redis -> EventEmitter로 전달될 때 targetUserId가 추가됨 +export type RedisToEventPayload = + RedisEventPayloadMap[K] & { targetUserId: string }; diff --git a/src/database/redis/redis.module.ts b/src/database/redis/redis.module.ts index 692af0d..a5af477 100644 --- a/src/database/redis/redis.module.ts +++ b/src/database/redis/redis.module.ts @@ -1,9 +1,10 @@ import { Module, Global } from '@nestjs/common'; import { RedisService } from 'src/database/redis/redis.service'; +import { TypedRedisPublisher } from './typed-redis-publisher'; @Global() @Module({ - providers: [RedisService], - exports: [RedisService], + providers: [RedisService, TypedRedisPublisher], + exports: [RedisService, TypedRedisPublisher], }) export class RedisModule {} diff --git a/src/database/redis/redis.service.ts b/src/database/redis/redis.service.ts index 3af7232..1dbd081 100644 --- a/src/database/redis/redis.service.ts +++ b/src/database/redis/redis.service.ts @@ -1,7 +1,8 @@ import { Injectable, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Redis from 'ioredis'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter'; +import { RedisEventName, RedisEventPayloadMap } from './redis-event.types'; @Injectable() export class RedisService implements OnModuleDestroy { @@ -10,7 +11,7 @@ export class RedisService implements OnModuleDestroy { constructor( private configService: ConfigService, - private eventEmitter: EventEmitter2, + private eventEmitter: TypedEventEmitter, ) { this.redis = new Redis({ host: this.configService.get('REDIS_HOST'), @@ -18,12 +19,14 @@ export class RedisService implements OnModuleDestroy { this.subRedis = this.redis.duplicate(); this.subRedis.on('message', (channel: string, message: string) => { - const { event, ...payload } = JSON.parse(message); + const { event, ...payload } = JSON.parse(message) as { + event: RedisEventName; + } & RedisEventPayloadMap[RedisEventName]; this.eventEmitter.emit(event, { ...payload, targetUserId: channel.split(':')[1], - }); + } as never); }); } diff --git a/src/database/redis/typed-redis-publisher.ts b/src/database/redis/typed-redis-publisher.ts new file mode 100644 index 0000000..1e54de4 --- /dev/null +++ b/src/database/redis/typed-redis-publisher.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { RedisService } from './redis.service'; +import { RedisEventName, RedisEventPayloadMap } from './redis-event.types'; + +@Injectable() +export class TypedRedisPublisher { + constructor(private readonly redisService: RedisService) {} + + async publish( + channel: string, + event: K, + payload: RedisEventPayloadMap[K], + ) { + await this.redisService.publish(channel, { event, ...payload }); + } +} diff --git a/src/infrastructure/event/event-payload.types.ts b/src/infrastructure/event/event-payload.types.ts index e04c336..221c473 100644 --- a/src/infrastructure/event/event-payload.types.ts +++ b/src/infrastructure/event/event-payload.types.ts @@ -1,3 +1,5 @@ +import type { RedisToEventPayload } from 'src/database/redis/redis-event.types'; + export interface EventPayloadMap { 'notification:FEED_LIKE': { feedId: string; likeCount: number }; 'notification:FEED_MENTION': { @@ -27,6 +29,12 @@ export interface EventPayloadMap { 'notification:FOLLOW': { actorId: string; userId: string }; push: PushPayload; + + // Redis Pub/Sub -> EventEmitter로 전달되는 채팅 이벤트 + newChatMessage: RedisToEventPayload<'newChatMessage'>; + likeChatMessage: RedisToEventPayload<'likeChatMessage'>; + unlikeChatMessage: RedisToEventPayload<'unlikeChatMessage'>; + deleteChat: RedisToEventPayload<'deleteChat'>; } export interface PushPayload { diff --git a/src/module/chat/chat-message.service.ts b/src/module/chat/chat-message.service.ts index 6f5c673..c0f3b6a 100644 --- a/src/module/chat/chat-message.service.ts +++ b/src/module/chat/chat-message.service.ts @@ -5,6 +5,7 @@ import { getImageUrl } from 'src/shared/util/get-image-url'; import { UserReader } from '../user/repository/user.reader'; import { RedisService } from 'src/database/redis/redis.service'; import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter'; +import { TypedRedisPublisher } from 'src/database/redis/typed-redis-publisher'; @Injectable() export class ChatMessageService { @@ -15,6 +16,7 @@ export class ChatMessageService { private readonly userReader: UserReader, private readonly redisService: RedisService, private readonly eventEmitter: TypedEventEmitter, + private readonly redisPublisher: TypedRedisPublisher, ) { this.redisClient = this.redisService.pubClient; } @@ -116,7 +118,7 @@ export class ChatMessageService { const chatUsers = await this.chatReader.findUsersByChatId(chatId); - const newMessageEvent = { + const newMessagePayload = { chatId, senderId: userId, chatUsers: chatUsers.map((user) => ({ @@ -144,19 +146,21 @@ export class ChatMessageService { })), }; - await this.redisService.publish(`user:${userId}`, { - ...newMessageEvent, - event: 'newChatMessage', - }); + await this.redisPublisher.publish( + `user:${userId}`, + 'newChatMessage', + newMessagePayload, + ); const myInfo = chatUsers.find((user) => user.id === userId); if (!myInfo) throw new HttpException('USER', 404); if (targetUserIsOnline) { - await this.redisService.publish(`user:${targetUserStatus.userId}`, { - ...newMessageEvent, - event: 'newChatMessage', - }); + await this.redisPublisher.publish( + `user:${targetUserStatus.userId}`, + 'newChatMessage', + newMessagePayload, + ); } if (!targetUserJoinedChat) { this.eventEmitter.emit(`push`, { @@ -170,7 +174,7 @@ export class ChatMessageService { data: { event: 'newChatMessage', deepLink: `/chats/${chatId}`, - data: JSON.stringify(newMessageEvent), + data: JSON.stringify(newMessagePayload), }, key: `chat-message-${chatId}`, badge: targetUserStatus.unreadCount + toCreateMessages.length, @@ -192,14 +196,14 @@ export class ChatMessageService { await this.chatWriter.updateMessageLike(messageId, true); - await this.redisService.publish(`user:${userId}`, { - messageId, - event: 'likeChatMessage', - }); - await this.redisService.publish(`user:${targetUser.id}`, { + await this.redisPublisher.publish(`user:${userId}`, 'likeChatMessage', { messageId, - event: 'likeChatMessage', }); + await this.redisPublisher.publish( + `user:${targetUser.id}`, + 'likeChatMessage', + { messageId }, + ); return; } @@ -214,14 +218,14 @@ export class ChatMessageService { await this.chatWriter.updateMessageLike(messageId, false); - await this.redisService.publish(`user:${userId}`, { + await this.redisPublisher.publish(`user:${userId}`, 'unlikeChatMessage', { messageId, - event: 'unlikeChatMessage', - }); - await this.redisService.publish(`user:${targetUser.id}`, { - messageId, - event: 'unlikeChatMessage', }); + await this.redisPublisher.publish( + `user:${targetUser.id}`, + 'unlikeChatMessage', + { messageId }, + ); return; } diff --git a/src/module/chat/chat.listener.ts b/src/module/chat/chat.listener.ts index c5e71f6..edb389b 100644 --- a/src/module/chat/chat.listener.ts +++ b/src/module/chat/chat.listener.ts @@ -1,40 +1,32 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { GlobalGateway } from '../websocket/global.gateway'; -import type { NewChatMessageEventResponse } from './dto'; +import type { EventPayloadMap } from 'src/infrastructure/event/event-payload.types'; @Injectable() export class ChatListener { constructor(private readonly gateway: GlobalGateway) {} @OnEvent('newChatMessage') - handleNewChatMessageEvent({ - targetUserId, - ...payload - }: NewChatMessageEventResponse & { targetUserId: string }) { - this.gateway.emitMessageEventToUser(targetUserId, payload); + handleNewChatMessageEvent(payload: EventPayloadMap['newChatMessage']) { + const { targetUserId, ...rest } = payload; + this.gateway.emitMessageEventToUser(targetUserId, rest); } @OnEvent('likeChatMessage') - handleLikeChatMessageEvent(payload: { - targetUserId: string; - messageId: string; - }) { + handleLikeChatMessageEvent(payload: EventPayloadMap['likeChatMessage']) { const { targetUserId, messageId } = payload; this.gateway.emitLikeChatMessageEventToUser(targetUserId, messageId); } @OnEvent('unlikeChatMessage') - handleUnlikeChatMessageEvent(payload: { - targetUserId: string; - messageId: string; - }) { + handleUnlikeChatMessageEvent(payload: EventPayloadMap['unlikeChatMessage']) { const { targetUserId, messageId } = payload; this.gateway.emitUnlikeChatMessageEventToUser(targetUserId, messageId); } @OnEvent('deleteChat') - handleDeleteChatEvent(payload: { targetUserId: string; chatIds: string[] }) { + handleDeleteChatEvent(payload: EventPayloadMap['deleteChat']) { const { targetUserId, chatIds } = payload; this.gateway.emitDeleteChatEventToUser(targetUserId, chatIds); } diff --git a/src/module/chat/chat.service.ts b/src/module/chat/chat.service.ts index 630c929..692828c 100644 --- a/src/module/chat/chat.service.ts +++ b/src/module/chat/chat.service.ts @@ -3,6 +3,7 @@ import { ChatReader } from './repository/chat.reader'; import { ChatWriter } from './repository/chat.writer'; import { UserReader } from '../user/repository/user.reader'; import { RedisService } from 'src/database/redis/redis.service'; +import { TypedRedisPublisher } from 'src/database/redis/typed-redis-publisher'; import { getImageUrl } from 'src/shared/util/get-image-url'; @Injectable() @@ -14,6 +15,7 @@ export class ChatService { private readonly chatWriter: ChatWriter, private readonly userReader: UserReader, private readonly redisService: RedisService, + private readonly redisPublisher: TypedRedisPublisher, ) { this.redisClient = this.redisService.pubClient; } @@ -119,18 +121,14 @@ export class ChatService { }); } - this.redisService.publish(`user:${userId}`, { - event: 'deleteChat', + this.redisPublisher.publish(`user:${userId}`, 'deleteChat', { chatIds: [chatId], }); } async deleteChats(userId: string, chatIds: string[]) { await this.chatWriter.exitManyChats(userId, chatIds); - this.redisService.publish(`user:${userId}`, { - event: 'deleteChat', - chatIds, - }); + this.redisPublisher.publish(`user:${userId}`, 'deleteChat', { chatIds }); return; }