Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Notification` MODIFY `type` ENUM('FOLLOW', 'NEW_PROMPT', 'INQUIRY', 'ANNOUNCEMENT', 'REPORT', 'ADMIN_MESSAGE') NOT NULL;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ enum NotificationType {
INQUIRY
ANNOUNCEMENT
REPORT
ADMIN_MESSAGE
}

enum Payment_provider {
Expand Down
4 changes: 4 additions & 0 deletions src/messages/services/message.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Service } from "typedi";
import { CreateMessageDto } from "../dtos/message.dto";
import { MessageRepository } from "../repositories/message.repository";
import { AppError } from "../../errors/AppError";
import eventBus from "../../config/eventBus";

@Service()
export class MessageService {
Expand Down Expand Up @@ -114,6 +115,9 @@ async sendMessage(currentUserId: number, data: CreateMessageDto) {

const message = await this.messageRepository.createMessage(data);

// 새 관리자 메세지 알림 이벤트 발생
eventBus.emit('adminMessage.created', data.sender_id, data.receiver_id, data.body);

return {
message: "메시지 전송 성공",
message_id: message.message_id,
Expand Down
14 changes: 13 additions & 1 deletion src/notifications/listeners/notification.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createFollowNotification,
createInquiryNotification,
createPromptNotification,
createAdminMessageNtification
} from "../services/notification.service";


Expand Down Expand Up @@ -52,4 +53,15 @@ eventBus.on('prompt.created', async (prompterId: number, promptId: number) => {
} catch (err) {
console.error("[알림 리스너 오류]: 새로운 프롬프트 업로드 알림 생성 실패", err);
}
});
});


// 관리자 메세지 알림 리스너
eventBus.on('adminMessage.created', async( adminId: number, userId: number, content: string) => {
try {
await createAdminMessageNtification(adminId, userId, content);
} catch (err) {
console.error("[알림 리스너 오류]: 관리자 메세지 알림 생성 실패", err);
}

})
7 changes: 3 additions & 4 deletions src/notifications/repositories/notification.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ export const createNotification = async ({
});
};

// ==========알림 목록 조회==========
// ==========알림 목록 조회==========
export const findNotificationsByUserId = async (
userId: number,
Expand Down Expand Up @@ -131,9 +130,9 @@ export const findNotificationsByUserId = async (
}),
});

// 2. FOLLOW / NEW_PROMPT 알림에 대해 profileImage 조회
// profileImage 조회
const actorIdsToFetch = notifications
.filter(n => (n.type === 'FOLLOW' || n.type === 'NEW_PROMPT') && n.actor)
.filter(n => (n.type === 'FOLLOW' || n.type === 'NEW_PROMPT' || n.type === 'ADMIN_MESSAGE') && n.actor)
.map(n => n.actor!.user_id);

let actorProfilesMap: Record<number, { url: string } | null> = {};
Expand All @@ -160,7 +159,7 @@ export const findNotificationsByUserId = async (
? {
...n.actor,
profileImage:
n.type === 'FOLLOW' || n.type === 'NEW_PROMPT'
n.type === 'FOLLOW' || n.type === 'NEW_PROMPT' || n.type === 'ADMIN_MESSAGE'
? actorProfilesMap[n.actor.user_id] ?? null
: null,
}
Expand Down
53 changes: 36 additions & 17 deletions src/notifications/routes/notification.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,25 @@ router.get('/me', authenticateJwt, getNotificationList); // 알림 목록 조회
* description: |
* - 커서 기반 페이지네이션(cursor-based-pagination) 사용.
* - `cursor`는 이전 요청에서 받은 마지막 데이터의 ID를 의미하며, 이를 기준으로 이후 데이터를 조회.
*
* - 첫 요청 시에는 `cursor`를 생략하여 최신 데이터부터 조회.
*
* - `has_more` 속성으로 더 불러올 데이터가 있는지 미리 확인 가능.
*
* - type 종류:
* - FOLLOW: 누가 나를 팔로우 했을 때
* - NEW_PROMPT: 알림 설정한 프롬프터가 새 프롬프트를 올렸을 때
* - INQUIRY: 나에게 문의사항이 도착했을 때
* - ANNOUNCEMENT: 공지사항이 등록되었을 때
* - REPORT: 내 신고가 접수되었을 때
* - `FOLLOW`: 누가 나를 팔로우 했을 때
*
* - `NEW_PROMPT`: 알림 설정한 프롬프터가 새 프롬프트를 올렸을 때
*
* - `INQUIRY`: 나에게 문의사항이 도착했을 때
*
* - `ANNOUNCEMENT`: 공지사항이 등록되었을 때
*
* - actor 필드는 알림을 유발한 사용자를 뜻하며, 타입이 REPORT, ANNOUNCEMENT일 때에만 null입니다.
* - profile_image 필드는 타입이 FOLLOW, NEW_PROMPT일 경우에만 반환됩니다.
* - `REPORT`: 내 신고가 접수되었을 때
*
* - `ADMIN_MESSAGE`: 관리자 메시지가 도착했을 때
*
* - `actor` 필드는 알림을 유발한 사용자를 뜻하며, 타입이 `REPORT`, `ANNOUNCEMENT`일 때에는 null입니다.

* tags: [Notifications]
* security:
Expand Down Expand Up @@ -117,28 +124,31 @@ router.get('/me', authenticateJwt, getNotificationList); // 알림 목록 조회
* has_more: false
* notifications:
* - notification_id: 600
* content: 신고가 접수되었습니다.
* content: "신고가 접수되었습니다."
* type: REPORT
* created_at: "2025-10-26T16:58:29.743Z"
* link_url: null
* actor: null
* - notification_id: 599
* content: 신고가 접수되었습니다.
* type: REPORT
* content: "`홍길동`님이 새 프롬프트를 업로드하셨습니다."
* type: NEW_PROMPT
* created_at: "2025-10-26T16:57:04.162Z"
* link_url: null
* actor: null
* link_url: /profile/10
* actor:
* user_id: 10
* nickname: "홍길동"
* profile_image: "https://promptplace-s3.s3.ap-northeast-2.amazonaws.com/profile-images/1a2b3c4d-5678-90ab-cdef-1234567890ab_1755892991870.png"
* - notification_id: 598
* content: 신고가 접수되었습니다.
* type: REPORT
* content: "새로운 공지사항이 등록되었습니다."
* type: ANNOUNCEMENT
* created_at: "2025-10-26T13:38:26.906Z"
* link_url: null
* actor: null
* - notification_id: 489
* content: "‘또도도잉’님이 회원님을 팔로우합니다."
* type: FOLLOW
* content: "프롬프트에 새로운 문의가 도착했습니다."
* type: INQUIRY
* created_at: "2025-08-21T12:26:45.288Z"
* link_url: "/profile/33"
* link_url: "/inquiries/2"
* actor:
* user_id: 33
* nickname: "또도도잉"
Expand All @@ -152,6 +162,15 @@ router.get('/me', authenticateJwt, getNotificationList); // 알림 목록 조회
* user_id: 33
* nickname: "또도도잉"
* profile_image: "https://promptplace-s3.s3.ap-northeast-2.amazonaws.com/profile-images/3b137096-7915-408d-ad94-b70e5aa53107_1755892991870.png"
* - notification_id: 487
* content: "안녕하세요. 프롬프트 플레이스 관리자입니다. 회원님의 원활한 서비스 이용을 위해 공지사항을 확인해주세요."
* type: ADMIN_MESSAGE
* created_at: "2025-08-21T12:26:42.522Z"
* link_url: null
* actor:
* user_id: 45
* nickname: "관리자"
* profile_image: "https://promptplace-s3.s3.ap-northeast-2.amazonaws.com/profile-images/3b137096-7915-408d-ad94-b70e5aa53107_1755892991870.png"
* statusCode: 200
* 401:
* description: 인증 실패
Expand Down
14 changes: 13 additions & 1 deletion src/notifications/services/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,16 @@ export const getNotificationHasNewStatusService = async (
const hasNew = latestNotificationTime > lastNotificationCheckTime;

return { hasNew };
};
};

// 관리자 메세지 알림
export const createAdminMessageNtification = async( adminId: number, userId: number, content: string) => {

return createNotificationService({
userId,
type: NotificationType.ADMIN_MESSAGE,
content: content,
linkUrl: null,
actorId: adminId,
});
}