From 2d98adf41519d351df97da9f7bc403a878b63b10 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 09:19:46 +0900 Subject: [PATCH 01/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20onDeleteAnswerNotifi?= =?UTF-8?q?cation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/_service/notification/notification.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/_service/notification/notification.service.ts b/src/app/api/_service/notification/notification.service.ts index 8fe24e3..67041cd 100644 --- a/src/app/api/_service/notification/notification.service.ts +++ b/src/app/api/_service/notification/notification.service.ts @@ -22,9 +22,9 @@ export class NotificationService { this.queueService = QueueService.get(); this.redisPubSub = RedisPubSubService.getInstance(); this.prisma = GetPrismaClient.getClient(); - this.DeleteAnswerNotification = this.DeleteAnswerNotification.bind(this); + this.onDeleteAnswerNotification = this.onDeleteAnswerNotification.bind(this); this.redisPubSub.sub('answer-deleted-event', (data) => { - this.DeleteAnswerNotification(data); + this.onDeleteAnswerNotification(data); }); } public static getInstance() { @@ -66,7 +66,7 @@ export class NotificationService { }); } - public async DeleteAnswerNotification(data: AnswerDeletedEvPayload) { + private async onDeleteAnswerNotification(data: AnswerDeletedEvPayload) { const key = `answer:${data.deleted_id}`; try { await this.prisma.notification.delete({ where: { notiKey: key } }); From 415ca791a8b33a0ba704cc9c52e492c0a35154ec Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 09:31:46 +0900 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=90=9B=20=EB=8B=B5=EB=B3=80=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=EC=82=AD=EC=A0=9C=EB=95=8C=EB=8F=84=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=EC=82=AD=EC=A0=9C=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/_service/queue/workers/AccountClean.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/api/_service/queue/workers/AccountClean.ts b/src/app/api/_service/queue/workers/AccountClean.ts index 9065b23..c3f19e3 100644 --- a/src/app/api/_service/queue/workers/AccountClean.ts +++ b/src/app/api/_service/queue/workers/AccountClean.ts @@ -1,3 +1,5 @@ +import { AnswerDeletedEvPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; +import { RedisPubSubService } from '@/app/api/_service/redis-pubsub/redis-event.service'; import { GetPrismaClient } from '@/app/api/_utils/getPrismaClient/get-prisma-client'; import { Logger } from '@/utils/logger/Logger'; import { Job, Queue, Worker } from 'bullmq'; @@ -11,12 +13,14 @@ const logger = new Logger('AccountCleanWork'); export class AccountCleanJob { private cleanQueue; private cleanWorker; + private redisPubsub: RedisPubSubService; constructor(connection: Redis) { + this.redisPubsub = RedisPubSubService.getInstance(); this.cleanQueue = new Queue(accountClean, { connection, }); - this.cleanWorker = new Worker(accountClean, this.process, { + this.cleanWorker = new Worker(accountClean, this.process.bind(this), { connection, concurrency: 10, removeOnComplete: { @@ -56,6 +60,9 @@ export class AccountCleanJob { } for (const a of parts) { await prisma.answer.delete({ where: { id: a.id } }); + await this.redisPubsub.pub('answer-deleted-event', { + deleted_id: a.id, + }); } counter += parts.length; logger.debug(`답변 ${counter} 개 삭제됨`); From b0726d94be35479a415c1832a9b9a1abf90e8d2f Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 10:00:38 +0900 Subject: [PATCH 03/12] =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/notification.service.ts | 25 +++++++++++++++++++ src/app/api/user/notification/route.ts | 5 ++++ 2 files changed, 30 insertions(+) diff --git a/src/app/api/_service/notification/notification.service.ts b/src/app/api/_service/notification/notification.service.ts index 67041cd..d3cd699 100644 --- a/src/app/api/_service/notification/notification.service.ts +++ b/src/app/api/_service/notification/notification.service.ts @@ -131,4 +131,29 @@ export class NotificationService { return sendApiError(500, 'readAllNotificationsApi FAIL!'); } } + + @Auth() + @RateLimit({ bucket_time: 60, req_limit: 10 }, 'user') + public async deleteAllNotificationApi( + _req: NextRequest, + @JwtPayload tokenPayload?: jwtPayloadType, + ): Promise { + const handle = tokenPayload?.handle; + if (!handle) { + return sendApiError(401, 'Unauthorized'); + } + try { + const notifications = await this.prisma.notification.findMany({ + where: { userHandle: handle }, + select: { id: true }, + }); + const deleted = await this.prisma.notification.deleteMany({ where: { userHandle: handle } }); + for (const n of notifications) { + this.redisPubSub.pub('answer-deleted-event', { deleted_id: n.id }); + } + return NextResponse.json({ message: `OK! ${deleted.count} notifications Deleted` }); + } catch (err) { + return sendApiError(500, JSON.stringify(err)); + } + } } diff --git a/src/app/api/user/notification/route.ts b/src/app/api/user/notification/route.ts index 5152015..9f2451d 100644 --- a/src/app/api/user/notification/route.ts +++ b/src/app/api/user/notification/route.ts @@ -5,3 +5,8 @@ export async function GET(req: NextRequest) { const notificationService = NotificationService.getInstance(); return await notificationService.getMyNotificationsApi(req); } + +export async function DELETE(req: NextRequest) { + const notificationService = NotificationService.getInstance(); + return await notificationService.deleteAllNotificationApi(req); +} \ No newline at end of file From 041a5462319aa29aeb85220a7efe5be949c1beb7 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 10:47:08 +0900 Subject: [PATCH 04/12] =?UTF-8?q?=E2=9C=A8=20=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=ED=95=A8=20=EB=B9=84=EC=9A=B0=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main/settings/page.tsx | 39 +++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/app/main/settings/page.tsx b/src/app/main/settings/page.tsx index 5f4e430..1b2b206 100644 --- a/src/app/main/settings/page.tsx +++ b/src/app/main/settings/page.tsx @@ -67,6 +67,7 @@ export default function Settings() { const accountCleanModalRef = useRef(null); const importBlockModalRef = useRef(null); const deleteAllQuestionsModalRef = useRef(null); + const deleteAllNotificationsModalRef = useRef(null); const { register, @@ -181,6 +182,17 @@ export default function Settings() { } }; + const onDeleteAllNotifications = async () => { + setButtonClicked(true); + const res = await fetch('/api/user/notification', { + method: 'DELETE', + }); + setButtonClicked(false); + if (!res.ok) { + throw new Error('알림을 삭제하는데 실패했어요!'); + } + }; + return (
{userInfo === undefined ? ( @@ -306,8 +318,25 @@ export default function Settings() {
-
+ +
+ + 알림함 비우기 +
+
+ 알림함의 모든 알림을 지워요. 지워진 알림은 되돌릴 수 없으니 주의하세요. +
+ +
차단 목록 가져오기 @@ -376,6 +405,14 @@ export default function Settings() { ref={logoutAllModalRef} onClick={onLogoutAll} /> + Date: Thu, 19 Dec 2024 11:22:11 +0900 Subject: [PATCH 05/12] =?UTF-8?q?Fix:=20=EC=95=8C=EB=A6=BC=ED=95=A8=20?= =?UTF-8?q?=EB=B9=84=EC=9B=80=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/_dto/notification/notification.dto.ts | 1 + .../_service/notification/notification.service.ts | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/_dto/notification/notification.dto.ts b/src/app/_dto/notification/notification.dto.ts index ff62ec7..950153e 100644 --- a/src/app/_dto/notification/notification.dto.ts +++ b/src/app/_dto/notification/notification.dto.ts @@ -3,6 +3,7 @@ import { AnswerWithProfileDto } from '@/app/_dto/answers/Answers.dto'; export type NotificationPayloadTypes = [ { notification_name: 'answer_on_my_question'; data: AnswerWithProfileDto; target: string }, { notification_name: 'read_all_notifications'; data: null; target: string }, + { notification_name: 'delete_all_notifications'; data: null; target: string }, ][number]; export class NotificationDto { diff --git a/src/app/api/_service/notification/notification.service.ts b/src/app/api/_service/notification/notification.service.ts index d3cd699..2aaab75 100644 --- a/src/app/api/_service/notification/notification.service.ts +++ b/src/app/api/_service/notification/notification.service.ts @@ -143,14 +143,12 @@ export class NotificationService { return sendApiError(401, 'Unauthorized'); } try { - const notifications = await this.prisma.notification.findMany({ - where: { userHandle: handle }, - select: { id: true }, - }); const deleted = await this.prisma.notification.deleteMany({ where: { userHandle: handle } }); - for (const n of notifications) { - this.redisPubSub.pub('answer-deleted-event', { deleted_id: n.id }); - } + this.redisPubSub.pub('websocket-notification-event', { + notification_name: 'delete_all_notifications', + data: null, + target: handle, + }); return NextResponse.json({ message: `OK! ${deleted.count} notifications Deleted` }); } catch (err) { return sendApiError(500, JSON.stringify(err)); From f343016de86eb79ea64a35b8f1bde06afbf294a0 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 11:36:25 +0900 Subject: [PATCH 06/12] =?UTF-8?q?=E2=9C=A8=20=EB=8B=B5=EB=B3=80=EC=9D=B4?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=EB=90=98=EA=B1=B0=EB=82=98=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=ED=95=A8=EC=9D=B4=20=EB=B9=84=EC=9B=8C=EC=A7=88?= =?UTF-8?q?=EB=95=8C=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main/_events.ts | 35 +++++++++++++++++++++++++---------- src/app/main/_header.tsx | 8 +++++++- src/app/main/layout.tsx | 26 +++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/app/main/_events.ts b/src/app/main/_events.ts index 273075b..e8a0cb0 100644 --- a/src/app/main/_events.ts +++ b/src/app/main/_events.ts @@ -1,6 +1,6 @@ import { Logger } from '@/utils/logger/Logger'; import { questionDto } from '../_dto/questions/question.dto'; -import { QuestionDeletedPayload } from '../_dto/websocket-event/websocket-event.dto'; +import { AnswerDeletedEvPayload, QuestionDeletedPayload } from '../_dto/websocket-event/websocket-event.dto'; import { AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; import { NotificationPayloadTypes } from '../_dto/notification/notification.dto'; @@ -46,7 +46,8 @@ export class MyQuestionEv { } const FetchMoreAnswerRequestEvent = 'FetchMoreAnswerRequestEvent'; -const WebSocketAnswerEvent = 'WebSocketAnswerEvent'; +const WebSocketAnswerCreatedEvent = 'WebSocketAnswerCreatedEvent'; +const WebSocketAnswerDeletedEvent = 'WebSocketAnswerDeletedEvent'; export class AnswerEv { private static logger = new Logger('AnswerEv', { noColor: true }); static addFetchMoreRequestEventListener(onEvent: (ev: CustomEvent) => void) { @@ -63,21 +64,35 @@ export class AnswerEv { window.dispatchEvent(ev); } - static addCreatedAnswerEventListener(onEvent: (ev: CustomEvent) => void) { - AnswerEv.logger.debug('Added WebSocket Answer EventListener'); - window.addEventListener(WebSocketAnswerEvent, onEvent as EventListener); + static addAnswerCreatedEventListener(onEvent: (ev: CustomEvent) => void) { + AnswerEv.logger.debug('Added WebSocket AnswerCreated EventListener'); + window.addEventListener(WebSocketAnswerCreatedEvent, onEvent as EventListener); } - static removeCreatedAnswerEventListener(onEvent: (ev: CustomEvent) => void) { - AnswerEv.logger.debug('Removed WebSocket Answer EventListener'); - window.removeEventListener(WebSocketAnswerEvent, onEvent as EventListener); + static removeAnswerCreatedEventListener(onEvent: (ev: CustomEvent) => void) { + AnswerEv.logger.debug('Removed WebSocket AnswerCreated EventListener'); + window.removeEventListener(WebSocketAnswerCreatedEvent, onEvent as EventListener); } - static sendCreatedAnswerEvent(data: AnswerWithProfileDto) { - const ev = new CustomEvent(WebSocketAnswerEvent, { detail: data }); + static sendAnswerCreatedEvent(data: AnswerWithProfileDto) { + const ev = new CustomEvent(WebSocketAnswerCreatedEvent, { detail: data }); window.dispatchEvent(ev); AnswerEv.logger.debug('New Answer Created', data); } + + static addAnswerDeletedEventListener(onEvent: (ev: CustomEvent) => void) { + AnswerEv.logger.debug('Added WebSocket AnswerDeleted EventListener'); + window.addEventListener(WebSocketAnswerDeletedEvent, onEvent as EventListener); + } + static removeAnswerDeletedEventListener(onEvent: (ev: CustomEvent) => void) { + AnswerEv.logger.debug('Removed WebSocket AnswerDeleted EventListener'); + window.removeEventListener(WebSocketAnswerDeletedEvent, onEvent as EventListener); + } + static sendAnswerDeletedEvent(data: AnswerDeletedEvPayload) { + const ev = new CustomEvent(WebSocketAnswerDeletedEvent, { detail: data }); + AnswerEv.logger.debug('Answer Deleted', data); + window.dispatchEvent(ev); + } } const NotificationEvent = 'NotificationEvent'; diff --git a/src/app/main/_header.tsx b/src/app/main/_header.tsx index 7a20dcb..41d29e3 100644 --- a/src/app/main/_header.tsx +++ b/src/app/main/_header.tsx @@ -88,12 +88,13 @@ export default function MainHeader({ setUserProfile }: headerProps) { } case 'answer-created-event': { const data = ws_data as WebsocketAnswerCreatedEvent; - AnswerEv.sendCreatedAnswerEvent(data.data); + AnswerEv.sendAnswerCreatedEvent(data.data); console.debug('WS: 새로운 답변이 생겼어요!', data.data); break; } case 'answer-deleted-event': { const data = ws_data as WebsocketAnswerDeletedEvent; + AnswerEv.sendAnswerDeletedEvent(data.data); console.debug('WS: 답변이 삭제되었어요!', data.data); break; } @@ -110,6 +111,11 @@ export default function MainHeader({ setUserProfile }: headerProps) { NotificationEv.sendNotificationEvent(data.data); break; } + case 'delete_all_notifications': { + console.debug('WS: 모든 알림이 삭제되었어요!', data.data); + NotificationEv.sendNotificationEvent(data.data); + break; + } default: { break; } diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index 2b7c89a..da694bd 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -8,6 +8,7 @@ import { FetchAllAnswersReqDto } from '../_dto/answers/fetch-all-answers.dto'; import { AnswerListWithProfileDto, AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; import { AnswerEv, NotificationEv } from './_events'; import { NotificationDto, NotificationPayloadTypes } from '../_dto/notification/notification.dto'; +import { AnswerDeletedEvPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; type MainPageContextType = { answers: AnswerWithProfileDto[] | null; @@ -54,7 +55,14 @@ export default function MainLayout({ modal, children }: { children: React.ReactN unread_count: 0, }; }); + break; + } + case 'delete_all_notifications': { + setNoti({ notifications: [], unread_count: 0 }); + break; } + default: + break; } }; @@ -82,16 +90,28 @@ export default function MainLayout({ modal, children }: { children: React.ReactN }); }; - const onWebSocketEv = (ev: CustomEvent) => { + const onAnswerCreated = (ev: CustomEvent) => { setAnswers((prevAnswer) => (prevAnswer ? [ev.detail, ...prevAnswer] : [])); }; + const onAnswerDeleted = (ev: CustomEvent) => { + setAnswers((prev) => { + if (prev) { + console.log('Answer삭제!', ev.detail, prev); + return prev.filter((v) => v.id !== ev.detail.deleted_id); + } else { + return prev; + } + }); + }; AnswerEv.addFetchMoreRequestEventListener(onEv); - AnswerEv.addCreatedAnswerEventListener(onWebSocketEv); + AnswerEv.addAnswerCreatedEventListener(onAnswerCreated); + AnswerEv.addAnswerDeletedEventListener(onAnswerDeleted); NotificationEv.addNotificationEventListener(onNotiEv); return () => { AnswerEv.removeFetchMoreRequestEventListener(onEv); - AnswerEv.removeCreatedAnswerEventListener(onWebSocketEv); + AnswerEv.removeAnswerCreatedEventListener(onAnswerCreated); + AnswerEv.removeAnswerDeletedEventListener(onAnswerDeleted); NotificationEv.removeNotificationEventListener(onNotiEv); }; }, []); From 1e031143f41c64c6f245878a11f843cb2d3f261d Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 11:47:41 +0900 Subject: [PATCH 07/12] =?UTF-8?q?=E2=9C=A8=20=EB=8B=B5=EB=B3=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=9C=20=EA=B2=BD=EC=9A=B0=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=ED=95=A8=EC=97=90=EC=84=9C=EB=8F=84=20=EC=A6=89?= =?UTF-8?q?=EC=8B=9C=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main/layout.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index da694bd..d99d653 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -96,12 +96,21 @@ export default function MainLayout({ modal, children }: { children: React.ReactN const onAnswerDeleted = (ev: CustomEvent) => { setAnswers((prev) => { if (prev) { - console.log('Answer삭제!', ev.detail, prev); return prev.filter((v) => v.id !== ev.detail.deleted_id); } else { return prev; } }); + setNoti((prev) => { + if (prev) { + return { + notifications: prev.notifications.filter((v) => { + return v.notification_name !== 'answer_on_my_question' || v.data.id !== ev.detail.deleted_id; + }), + unread_count: prev.unread_count - 1, + }; + } + }); }; AnswerEv.addFetchMoreRequestEventListener(onEv); From 997227deb8c91f11f6f6ebcffcede0c31adc7fb9 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 12:11:29 +0900 Subject: [PATCH 08/12] =?UTF-8?q?=E2=9C=A8=20=EB=8B=B5=EB=B3=80=20?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A6=AC=EB=B0=8D=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EB=8F=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main/user/[handle]/_answers.tsx | 35 +++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/app/main/user/[handle]/_answers.tsx b/src/app/main/user/[handle]/_answers.tsx index 883688d..a2a0db5 100644 --- a/src/app/main/user/[handle]/_answers.tsx +++ b/src/app/main/user/[handle]/_answers.tsx @@ -4,9 +4,11 @@ import { useParams } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; import Answer from '@/app/_components/answer'; import { userProfileDto } from '@/app/_dto/fetch-profile/Profile.dto'; -import { AnswerDto } from '@/app/_dto/answers/Answers.dto'; +import { AnswerDto, AnswerWithProfileDto } from '@/app/_dto/answers/Answers.dto'; import { FetchUserAnswersDto } from '@/app/_dto/answers/fetch-user-answers.dto'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; +import { AnswerDeletedEvPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; +import { AnswerEv } from '@/app/main/_events'; type ResponseType = { answers: AnswerDto[]; @@ -73,7 +75,6 @@ export default function UserPage() { if (answers && count) { const filteredAnswer = answers.filter((el) => el.id !== id); setAnswers(filteredAnswer); - setCount((prevCount) => (prevCount ? prevCount - 1 : null)); } }; @@ -129,6 +130,36 @@ export default function UserPage() { }; }, [mounted, untilId]); + useEffect(() => { + const onAnswerCreated = (ev: CustomEvent) => { + setAnswers((prev) => { + if (prev && ev.detail.answeredPersonHandle === profileHandle) { + return [ev.detail, ...prev]; + } else { + return prev; + } + }); + setCount((prev) => (prev ? prev + 1 : null)); + }; + const onAnswerDeleted = (ev: CustomEvent) => { + setAnswers((prev) => { + if (prev) { + return prev.filter((v) => v.id !== ev.detail.deleted_id); + } else { + return prev; + } + }); + setCount((prev) => (prev ? prev - 1 : null)); + }; + + AnswerEv.addAnswerCreatedEventListener(onAnswerCreated); + AnswerEv.addAnswerDeletedEventListener(onAnswerDeleted); + return () => { + AnswerEv.removeAnswerCreatedEventListener(onAnswerCreated); + AnswerEv.removeAnswerDeletedEventListener(onAnswerDeleted); + }; + }, []); + return (
{userProfile !== null && ( From bd61f2f727e1a71d115324e5ed283daf32e5eee3 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 15:11:13 +0900 Subject: [PATCH 09/12] =?UTF-8?q?=F0=9F=8E=A8=20Header=EC=97=90=EC=84=9C?= =?UTF-8?q?=20WebsocketManager=EB=A5=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main/_header.tsx | 115 ++++-------------------------- src/app/main/_websocketManager.ts | 103 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 100 deletions(-) create mode 100644 src/app/main/_websocketManager.ts diff --git a/src/app/main/_header.tsx b/src/app/main/_header.tsx index 41d29e3..88c884f 100644 --- a/src/app/main/_header.tsx +++ b/src/app/main/_header.tsx @@ -10,19 +10,10 @@ import { logout } from '@/utils/logout/logout'; import { MyProfileContext, MyProfileEv } from '@/app/main/_profileContext'; import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; import { Logger } from '@/utils/logger/Logger'; -import { - WebsocketAnswerCreatedEvent, - WebsocketAnswerDeletedEvent, - WebsocketEvent, - WebsocketNotificationEvent, - WebsocketPayloadTypes, - WebsocketQuestionCreatedEvent, - WebsocketQuestionDeletedEvent, -} from '@/app/_dto/websocket-event/websocket-event.dto'; import { FaXmark } from 'react-icons/fa6'; -import { AnswerEv, MyQuestionEv, NotificationEv } from './_events'; import WebSocketState from '../_components/webSocketState'; import { NotificationContext } from './layout'; +import { webSocketManager } from '@/app/main/_websocketManager'; type headerProps = { setUserProfile: Dispatch>; @@ -33,7 +24,7 @@ export default function MainHeader({ setUserProfile }: headerProps) { const forcedLogoutModalRef = useRef(null); const [questionsNum, setQuestions_num] = useState(0); const [questionsToastMenu, setQuestionsToastMenu] = useState(false); - const websocket = useRef(null); + const websocketRef = useRef(null); const [wsState, setWsState] = useState(); const ws_retry_counter = useRef(0); const [loginChecked, setLoginChecked] = useState(false); @@ -59,89 +50,6 @@ export default function MainHeader({ setUserProfile }: headerProps) { return data; } }; - const webSocketManager = () => { - if (websocket.current) { - websocket.current.close(); - } - websocket.current = new WebSocket('/api/websocket'); - websocket.current.onmessage = (ws_event: MessageEvent) => { - const ws_data = JSON.parse(ws_event.data) as WebsocketEvent; - switch (ws_data.ev_name) { - case 'question-created-event': { - const data = ws_data as WebsocketQuestionCreatedEvent; - console.debug('WS: 새로운 질문이 생겼어요!,', data.data); - MyProfileEv.SendUpdateReq({ questions: data.data.question_numbers }); - MyQuestionEv.SendUpdateReq(data.data); - toastTimeout.current = setTimeout(() => { - setQuestionsToastMenu(false); - }, 8000); - setQuestionsToastMenu(true); - break; - } - case 'question-deleted-event': { - const data = ws_data as WebsocketQuestionDeletedEvent; - console.debug('WS: 질문이 삭제되었어요!', data.data); - MyProfileEv.SendUpdateReq({ questions: data.data.question_numbers }); - MyQuestionEv.SendDeleteReq(data.data); - setQuestionsToastMenu(false); - break; - } - case 'answer-created-event': { - const data = ws_data as WebsocketAnswerCreatedEvent; - AnswerEv.sendAnswerCreatedEvent(data.data); - console.debug('WS: 새로운 답변이 생겼어요!', data.data); - break; - } - case 'answer-deleted-event': { - const data = ws_data as WebsocketAnswerDeletedEvent; - AnswerEv.sendAnswerDeletedEvent(data.data); - console.debug('WS: 답변이 삭제되었어요!', data.data); - break; - } - case 'websocket-notification-event': { - const data = ws_data as WebsocketNotificationEvent; - switch (data.data.notification_name) { - case 'answer_on_my_question': { - console.debug('WS: 내 질문에 답변이 등록되었어요!', data.data.data); - NotificationEv.sendNotificationEvent(data.data); - break; - } - case 'read_all_notifications': { - console.debug('WS: 모든 알림이 읽음처리 되었어요!', data.data); - NotificationEv.sendNotificationEvent(data.data); - break; - } - case 'delete_all_notifications': { - console.debug('WS: 모든 알림이 삭제되었어요!', data.data); - NotificationEv.sendNotificationEvent(data.data); - break; - } - default: { - break; - } - } - break; - } - case 'keep-alive': { - break; - } - } - }; - - websocket.current.onopen = () => { - console.debug('웹소켓이 열렸어요!'); - ws_retry_counter.current = 0; - setWsState(websocket.current?.readyState); - }; - websocket.current.onclose = (ev: CloseEvent) => { - console.debug('웹소켓이 닫혔어요!', ev); - setWsState(websocket.current?.readyState); - }; - websocket.current.onerror = (ev: Event) => { - console.log(`웹소켓 에러`, ev); - setWsState(websocket.current?.readyState); - }; - }; const onProfileUpdateEvent = (ev: CustomEvent>) => { const logger = new Logger('onProfileUpdateEvent', { noColor: true }); @@ -168,33 +76,40 @@ export default function MainHeader({ setUserProfile }: headerProps) { } const webSocketRetryInterval = setInterval( () => { - if (websocket.current === null || websocket.current?.readyState === 3) { + if (websocketRef.current === null || websocketRef.current?.readyState === 3) { if (ws_retry_counter.current < 5) { ws_retry_counter.current += 1; console.log('웹소켓 연결 재시도...', ws_retry_counter.current); - webSocketManager(); + webSocketManager({ websocketRef, toastTimeout, setWsState, setQuestionsToastMenu }); } else { console.log('웹소켓 연결 최대 재시도 횟수를 초과했어요!'); clearInterval(webSocketRetryInterval); return; } } else { - websocket.current.send(`mua: ${Date.now()}`); + websocketRef.current.send(`mua: ${Date.now()}`); } }, 5000 + ws_retry_counter.current * 2000, ); - webSocketManager(); + webSocketManager({ websocketRef, toastTimeout, setWsState, setQuestionsToastMenu }); return () => { clearTimeout(toastTimeout.current); clearInterval(webSocketRetryInterval); - if (websocket.current?.readyState === 1) { - websocket.current.close(); + if (websocketRef.current?.readyState === 1) { + websocketRef.current.close(); } }; }, [loginChecked]); + useEffect(() => { + if (websocketRef.current && websocketRef.current.readyState === 1) { + console.debug('Websocket 연결 재시도 카운터 초기화!'); + ws_retry_counter.current = 0; + } + }, [websocketRef.current?.readyState]); + useEffect(() => { if (!notificationContext) return; setNotiNum(notificationContext.unread_count); diff --git a/src/app/main/_websocketManager.ts b/src/app/main/_websocketManager.ts new file mode 100644 index 0000000..e190615 --- /dev/null +++ b/src/app/main/_websocketManager.ts @@ -0,0 +1,103 @@ +import { + WebsocketAnswerCreatedEvent, + WebsocketAnswerDeletedEvent, + WebsocketEvent, + WebsocketNotificationEvent, + WebsocketPayloadTypes, + WebsocketQuestionCreatedEvent, + WebsocketQuestionDeletedEvent, +} from '@/app/_dto/websocket-event/websocket-event.dto'; +import { MyQuestionEv, AnswerEv, NotificationEv } from '@/app/main/_events'; +import { MyProfileEv } from '@/app/main/_profileContext'; +import { Dispatch, MutableRefObject, SetStateAction } from 'react'; + +type managerPropsType = { + websocketRef: MutableRefObject; + toastTimeout: MutableRefObject; + setWsState: Dispatch>; + setQuestionsToastMenu: Dispatch>; +}; + +export const webSocketManager = (props: managerPropsType) => { + const { websocketRef: websocket, toastTimeout, setWsState, setQuestionsToastMenu } = props; + if (websocket.current) { + websocket.current.close(); + } + websocket.current = new WebSocket('/api/websocket'); + websocket.current.onmessage = (ws_event: MessageEvent) => { + const ws_data = JSON.parse(ws_event.data) as WebsocketEvent; + switch (ws_data.ev_name) { + case 'question-created-event': { + const data = ws_data as WebsocketQuestionCreatedEvent; + console.debug('WS: 새로운 질문이 생겼어요!,', data.data); + MyProfileEv.SendUpdateReq({ questions: data.data.question_numbers }); + MyQuestionEv.SendUpdateReq(data.data); + toastTimeout.current = setTimeout(() => { + setQuestionsToastMenu(false); + }, 8000); + setQuestionsToastMenu(true); + break; + } + case 'question-deleted-event': { + const data = ws_data as WebsocketQuestionDeletedEvent; + console.debug('WS: 질문이 삭제되었어요!', data.data); + MyProfileEv.SendUpdateReq({ questions: data.data.question_numbers }); + MyQuestionEv.SendDeleteReq(data.data); + setQuestionsToastMenu(false); + break; + } + case 'answer-created-event': { + const data = ws_data as WebsocketAnswerCreatedEvent; + AnswerEv.sendAnswerCreatedEvent(data.data); + console.debug('WS: 새로운 답변이 생겼어요!', data.data); + break; + } + case 'answer-deleted-event': { + const data = ws_data as WebsocketAnswerDeletedEvent; + AnswerEv.sendAnswerDeletedEvent(data.data); + console.debug('WS: 답변이 삭제되었어요!', data.data); + break; + } + case 'websocket-notification-event': { + const data = ws_data as WebsocketNotificationEvent; + switch (data.data.notification_name) { + case 'answer_on_my_question': { + console.debug('WS: 내 질문에 답변이 등록되었어요!', data.data.data); + NotificationEv.sendNotificationEvent(data.data); + break; + } + case 'read_all_notifications': { + console.debug('WS: 모든 알림이 읽음처리 되었어요!', data.data); + NotificationEv.sendNotificationEvent(data.data); + break; + } + case 'delete_all_notifications': { + console.debug('WS: 모든 알림이 삭제되었어요!', data.data); + NotificationEv.sendNotificationEvent(data.data); + break; + } + default: { + break; + } + } + break; + } + case 'keep-alive': { + break; + } + } + }; + + websocket.current.onopen = () => { + console.debug('웹소켓이 열렸어요!'); + setWsState(websocket.current?.readyState); + }; + websocket.current.onclose = (ev: CloseEvent) => { + console.debug('웹소켓이 닫혔어요!', ev); + setWsState(websocket.current?.readyState); + }; + websocket.current.onerror = (ev: Event) => { + console.log(`웹소켓 에러`, ev); + setWsState(websocket.current?.readyState); + }; +}; From 9c8475fb6a61aa1e6b478651ba6c41829e62aca9 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 16:01:14 +0900 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=8E=A8=20fetchMyProfile=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main/_header.tsx | 24 +++++------------------- src/app/main/layout.tsx | 8 ++++---- src/utils/profile/fetchMyProfile.ts | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 src/utils/profile/fetchMyProfile.ts diff --git a/src/app/main/_header.tsx b/src/app/main/_header.tsx index 88c884f..027544a 100644 --- a/src/app/main/_header.tsx +++ b/src/app/main/_header.tsx @@ -14,6 +14,7 @@ import { FaXmark } from 'react-icons/fa6'; import WebSocketState from '../_components/webSocketState'; import { NotificationContext } from './layout'; import { webSocketManager } from '@/app/main/_websocketManager'; +import { fetchMyProfile } from '@/utils/profile/fetchMyProfile'; type headerProps = { setUserProfile: Dispatch>; @@ -33,24 +34,6 @@ export default function MainHeader({ setUserProfile }: headerProps) { const notificationContext = useContext(NotificationContext); - const fetchMyProfile = async (): Promise => { - const user_handle = localStorage.getItem('user_handle'); - - if (user_handle) { - const res = await fetch('/api/db/fetch-my-profile', { - method: 'GET', - }); - if (!res.ok) { - if (res.status === 401) { - forcedLogoutModalRef.current?.showModal(); - } - return; - } - const data = await res.json(); - return data; - } - }; - const onProfileUpdateEvent = (ev: CustomEvent>) => { const logger = new Logger('onProfileUpdateEvent', { noColor: true }); setUserProfile((prev) => { @@ -116,8 +99,11 @@ export default function MainHeader({ setUserProfile }: headerProps) { }, [notificationContext]); useEffect(() => { + const onResNotOk = () => { + forcedLogoutModalRef.current?.showModal(); + }; if (setUserProfile) { - fetchMyProfile().then((r) => { + fetchMyProfile(onResNotOk).then((r) => { setUserProfile(r); setQuestions_num(r?.questions ?? 0); setLoginChecked(true); diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index d99d653..2444f2e 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -38,7 +38,7 @@ export default function MainLayout({ modal, children }: { children: React.ReactN const onNotiEv = (ev: CustomEvent) => { const notiData = ev.detail; - switch (ev.detail.notification_name) { + switch (notiData.notification_name) { case 'answer_on_my_question': { setNoti((prev) => { return { @@ -78,7 +78,7 @@ export default function MainLayout({ modal, children }: { children: React.ReactN }); fetchNoti(); - const onEv = (ev: CustomEvent) => { + const onFetchMoreEv = (ev: CustomEvent) => { const id = ev.detail; fetchAllAnswers({ sort: 'DESC', limit: 25, untilId: id }).then((r) => { if (r.length === 0) { @@ -113,12 +113,12 @@ export default function MainLayout({ modal, children }: { children: React.ReactN }); }; - AnswerEv.addFetchMoreRequestEventListener(onEv); + AnswerEv.addFetchMoreRequestEventListener(onFetchMoreEv); AnswerEv.addAnswerCreatedEventListener(onAnswerCreated); AnswerEv.addAnswerDeletedEventListener(onAnswerDeleted); NotificationEv.addNotificationEventListener(onNotiEv); return () => { - AnswerEv.removeFetchMoreRequestEventListener(onEv); + AnswerEv.removeFetchMoreRequestEventListener(onFetchMoreEv); AnswerEv.removeAnswerCreatedEventListener(onAnswerCreated); AnswerEv.removeAnswerDeletedEventListener(onAnswerDeleted); NotificationEv.removeNotificationEventListener(onNotiEv); diff --git a/src/utils/profile/fetchMyProfile.ts b/src/utils/profile/fetchMyProfile.ts new file mode 100644 index 0000000..bac041b --- /dev/null +++ b/src/utils/profile/fetchMyProfile.ts @@ -0,0 +1,19 @@ +import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; + +export async function fetchMyProfile(onResNotOk?: (code: number) => void): Promise { + const user_handle = localStorage.getItem('user_handle'); + + if (user_handle) { + const res = await fetch('/api/db/fetch-my-profile', { + method: 'GET', + }); + if (!res.ok) { + if (onResNotOk) { + onResNotOk(res.status); + } + return; + } + const data = await res.json(); + return data; + } +} From d36ca0c8f24dbe49fc7195bb57596cfe509ffbc5 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 17:06:04 +0900 Subject: [PATCH 11/12] =?UTF-8?q?=F0=9F=8E=A8=20main=20header,=20layout=20?= =?UTF-8?q?refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MainHeader 에서 fetch하는 코드들 제거 - 초기 fetch들은 main layout으로 통일 - 각각의 fetch함수들은 파일 분리 --- src/app/main/_header.tsx | 56 +------- src/app/main/layout.tsx | 186 +++++++++++++++------------ src/utils/answers/fetchAllAnswers.ts | 32 +++++ src/utils/notification/fetchNoti.ts | 21 +++ src/utils/profile/fetchMyProfile.ts | 6 +- 5 files changed, 163 insertions(+), 138 deletions(-) create mode 100644 src/utils/answers/fetchAllAnswers.ts create mode 100644 src/utils/notification/fetchNoti.ts diff --git a/src/app/main/_header.tsx b/src/app/main/_header.tsx index 027544a..e7925cd 100644 --- a/src/app/main/_header.tsx +++ b/src/app/main/_header.tsx @@ -1,51 +1,33 @@ 'use client'; import Link from 'next/link'; -import { Dispatch, SetStateAction, useContext, useEffect, useRef, useState } from 'react'; +import { useContext, useEffect, useRef, useState } from 'react'; import { FaInfoCircle, FaUser } from 'react-icons/fa'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; -import DialogModalOneButton from '@/app/_components/modalOneButton'; import { refreshJwt } from '@/utils/refreshJwt/refresh-jwt-token'; import { logout } from '@/utils/logout/logout'; -import { MyProfileContext, MyProfileEv } from '@/app/main/_profileContext'; -import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; -import { Logger } from '@/utils/logger/Logger'; +import { MyProfileContext } from '@/app/main/_profileContext'; import { FaXmark } from 'react-icons/fa6'; import WebSocketState from '../_components/webSocketState'; import { NotificationContext } from './layout'; import { webSocketManager } from '@/app/main/_websocketManager'; -import { fetchMyProfile } from '@/utils/profile/fetchMyProfile'; type headerProps = { - setUserProfile: Dispatch>; + questionsNum: number; + loginChecked: boolean; }; -export default function MainHeader({ setUserProfile }: headerProps) { +export default function MainHeader({ questionsNum, loginChecked }: headerProps) { const profile = useContext(MyProfileContext); const logoutModalRef = useRef(null); - const forcedLogoutModalRef = useRef(null); - const [questionsNum, setQuestions_num] = useState(0); const [questionsToastMenu, setQuestionsToastMenu] = useState(false); const websocketRef = useRef(null); const [wsState, setWsState] = useState(); const ws_retry_counter = useRef(0); - const [loginChecked, setLoginChecked] = useState(false); const toastTimeout = useRef(); const [notiNum, setNotiNum] = useState(0); const notificationContext = useContext(NotificationContext); - const onProfileUpdateEvent = (ev: CustomEvent>) => { - const logger = new Logger('onProfileUpdateEvent', { noColor: true }); - setUserProfile((prev) => { - if (prev) { - const newData = { ...prev, ...ev.detail }; - logger.log('My Profile Context Update With: ', ev.detail); - return newData; - } - }); - setQuestions_num((prev) => ev.detail.questions ?? prev); - }; - const menuClose = () => { const el = document.activeElement as HTMLLIElement; if (el) { @@ -98,27 +80,6 @@ export default function MainHeader({ setUserProfile }: headerProps) { setNotiNum(notificationContext.unread_count); }, [notificationContext]); - useEffect(() => { - const onResNotOk = () => { - forcedLogoutModalRef.current?.showModal(); - }; - if (setUserProfile) { - fetchMyProfile(onResNotOk).then((r) => { - setUserProfile(r); - setQuestions_num(r?.questions ?? 0); - setLoginChecked(true); - }); - } - }, [setUserProfile]); - - useEffect(() => { - MyProfileEv.addEventListener(onProfileUpdateEvent); - - return () => { - MyProfileEv.removeEventListener(onProfileUpdateEvent); - }; - }, []); - useEffect(() => { const fn = async () => { const now = Math.ceil(Date.now() / 1000); @@ -226,13 +187,6 @@ export default function MainHeader({ setUserProfile }: headerProps) { ref={logoutModalRef} onClick={logout} /> -
diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index 2444f2e..89a1f62 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -2,13 +2,18 @@ import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; import MainHeader from '@/app/main/_header'; -import { createContext, useCallback, useEffect, useState } from 'react'; -import { MyProfileContext } from '@/app/main/_profileContext'; -import { FetchAllAnswersReqDto } from '../_dto/answers/fetch-all-answers.dto'; -import { AnswerListWithProfileDto, AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; +import { createContext, useEffect, useRef, useState } from 'react'; +import { MyProfileContext, MyProfileEv } from '@/app/main/_profileContext'; +import { AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; import { AnswerEv, NotificationEv } from './_events'; import { NotificationDto, NotificationPayloadTypes } from '../_dto/notification/notification.dto'; import { AnswerDeletedEvPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; +import { Logger } from '@/utils/logger/Logger'; +import { fetchMyProfile } from '@/utils/profile/fetchMyProfile'; +import DialogModalOneButton from '@/app/_components/modalOneButton'; +import { logout } from '@/utils/logout/logout'; +import { fetchAllAnswers } from '@/utils/answers/fetchAllAnswers'; +import { fetchNoti } from '@/utils/notification/fetchNoti'; type MainPageContextType = { answers: AnswerWithProfileDto[] | null; @@ -24,18 +29,55 @@ export default function MainLayout({ modal, children }: { children: React.ReactN const [loading, setLoading] = useState(true); const [untilId, setUntilId] = useState(undefined); const [noti, setNoti] = useState(); + const [questionsNum, setQuestions_num] = useState(0); + const [loginChecked, setLoginChecked] = useState(false); + const forcedLogoutModalRef = useRef(null); - const fetchNoti = useCallback(async () => { - const handle = localStorage.getItem('user_handle'); - if (!handle) { - return; + const onResNotOk = async (code: number, res: Response) => { + if (code === 401) { + forcedLogoutModalRef.current?.showModal(); + } else { + alert(await res.text()); } - const res = await fetch('/api/user/notification'); - if (!res.ok) alert(await res.text()); - const data = (await res.json()) as NotificationDto; - setNoti(data); + }; + // ------------ Initial Fetch ----------------------------- + useEffect(() => { + fetchMyProfile(onResNotOk).then((r) => { + setUserProfileData(r); + setQuestions_num(r?.questions ?? 0); + setLoginChecked(true); + }); + fetchAllAnswers({ sort: 'DESC', limit: 25 }, onResNotOk).then((r) => { + if (r.length === 0) { + setLoading(false); + setAnswers([]); + return; + } + setAnswers(r); + setUntilId(r[r.length - 1].id); + }); + fetchNoti(onResNotOk).then((v) => { + setNoti(v); + }); + }, []); + + // ------------- add Event callbacks -------------------- + useEffect(() => { + MyProfileEv.addEventListener(onProfileUpdateEvent); + AnswerEv.addFetchMoreRequestEventListener(onFetchMoreEv); + AnswerEv.addAnswerCreatedEventListener(onAnswerCreated); + AnswerEv.addAnswerDeletedEventListener(onAnswerDeleted); + NotificationEv.addNotificationEventListener(onNotiEv); + return () => { + MyProfileEv.removeEventListener(onProfileUpdateEvent); + AnswerEv.removeFetchMoreRequestEventListener(onFetchMoreEv); + AnswerEv.removeAnswerCreatedEventListener(onAnswerCreated); + AnswerEv.removeAnswerDeletedEventListener(onAnswerDeleted); + NotificationEv.removeNotificationEventListener(onNotiEv); + }; }, []); + // ---------------- event callback functions ------------------- const onNotiEv = (ev: CustomEvent) => { const notiData = ev.detail; switch (notiData.notification_name) { @@ -66,64 +108,53 @@ export default function MainLayout({ modal, children }: { children: React.ReactN } }; - useEffect(() => { - fetchAllAnswers({ sort: 'DESC', limit: 25 }).then((r) => { + const onProfileUpdateEvent = (ev: CustomEvent>) => { + const logger = new Logger('onProfileUpdateEvent', { noColor: true }); + setUserProfileData((prev) => { + if (prev) { + const newData = { ...prev, ...ev.detail }; + logger.log('My Profile Context Update With: ', ev.detail); + return newData; + } + }); + setQuestions_num((prev) => ev.detail.questions ?? prev); + }; + + const onFetchMoreEv = (ev: CustomEvent) => { + const id = ev.detail; + fetchAllAnswers({ sort: 'DESC', limit: 25, untilId: id }).then((r) => { if (r.length === 0) { setLoading(false); - setAnswers([]); return; } - setAnswers(r); + setAnswers((prev_answers) => (prev_answers ? [...prev_answers, ...r] : null)); setUntilId(r[r.length - 1].id); }); - fetchNoti(); - - const onFetchMoreEv = (ev: CustomEvent) => { - const id = ev.detail; - fetchAllAnswers({ sort: 'DESC', limit: 25, untilId: id }).then((r) => { - if (r.length === 0) { - setLoading(false); - return; - } - setAnswers((prev_answers) => (prev_answers ? [...prev_answers, ...r] : null)); - setUntilId(r[r.length - 1].id); - }); - }; + }; - const onAnswerCreated = (ev: CustomEvent) => { - setAnswers((prevAnswer) => (prevAnswer ? [ev.detail, ...prevAnswer] : [])); - }; - const onAnswerDeleted = (ev: CustomEvent) => { - setAnswers((prev) => { - if (prev) { - return prev.filter((v) => v.id !== ev.detail.deleted_id); - } else { - return prev; - } - }); - setNoti((prev) => { - if (prev) { - return { - notifications: prev.notifications.filter((v) => { - return v.notification_name !== 'answer_on_my_question' || v.data.id !== ev.detail.deleted_id; - }), - unread_count: prev.unread_count - 1, - }; - } - }); - }; + const onAnswerCreated = (ev: CustomEvent) => { + setAnswers((prevAnswer) => (prevAnswer ? [ev.detail, ...prevAnswer] : [])); + }; - AnswerEv.addFetchMoreRequestEventListener(onFetchMoreEv); - AnswerEv.addAnswerCreatedEventListener(onAnswerCreated); - AnswerEv.addAnswerDeletedEventListener(onAnswerDeleted); - NotificationEv.addNotificationEventListener(onNotiEv); - return () => { - AnswerEv.removeFetchMoreRequestEventListener(onFetchMoreEv); - AnswerEv.removeAnswerCreatedEventListener(onAnswerCreated); - AnswerEv.removeAnswerDeletedEventListener(onAnswerDeleted); - NotificationEv.removeNotificationEventListener(onNotiEv); - }; - }, []); + const onAnswerDeleted = (ev: CustomEvent) => { + setAnswers((prev) => { + if (prev) { + return prev.filter((v) => v.id !== ev.detail.deleted_id); + } else { + return prev; + } + }); + setNoti((prev) => { + if (prev) { + return { + notifications: prev.notifications.filter((v) => { + return v.notification_name !== 'answer_on_my_question' || v.data.id !== ev.detail.deleted_id; + }), + unread_count: prev.unread_count - 1, + }; + } + }); + }; return (
@@ -132,35 +163,20 @@ export default function MainLayout({ modal, children }: { children: React.ReactN {modal}
- +
{children}
+
); } -async function fetchAllAnswers(req: FetchAllAnswersReqDto) { - const query: string[] = []; - for (const [key, value] of Object.entries(req)) { - query.push(`${key}=${value}`); - } - const url = `/api/db/answers?${query.join('&')}`; - const res = await fetch(url, { - method: 'GET', - cache: 'no-cache', - }); - try { - if (res.ok) { - const answers = ((await res.json()) as AnswerListWithProfileDto).answersList; - return answers; - } else { - throw new Error(`답변을 불러오는데 실패했어요!: ${await res.text()}`); - } - } catch (err) { - alert(err); - throw err; - } -} diff --git a/src/utils/answers/fetchAllAnswers.ts b/src/utils/answers/fetchAllAnswers.ts new file mode 100644 index 0000000..b481bcb --- /dev/null +++ b/src/utils/answers/fetchAllAnswers.ts @@ -0,0 +1,32 @@ +import { AnswerWithProfileDto, AnswerListWithProfileDto } from '@/app/_dto/answers/Answers.dto'; +import { FetchAllAnswersReqDto } from '@/app/_dto/answers/fetch-all-answers.dto'; + +export async function fetchAllAnswers( + req: FetchAllAnswersReqDto, + onResNotOk?: (code: number, res: Response) => void, +): Promise { + const query: string[] = []; + for (const [key, value] of Object.entries(req)) { + query.push(`${key}=${value}`); + } + const url = `/api/db/answers?${query.join('&')}`; + const res = await fetch(url, { + method: 'GET', + cache: 'no-cache', + }); + try { + if (res.ok) { + const answers = ((await res.json()) as AnswerListWithProfileDto).answersList; + return answers; + } else { + if (onResNotOk) { + onResNotOk(res.status, res); + return []; + } + throw new Error(`답변을 불러오는데 실패했어요!: ${await res.text()}`); + } + } catch (err) { + alert(err); + throw err; + } +} diff --git a/src/utils/notification/fetchNoti.ts b/src/utils/notification/fetchNoti.ts new file mode 100644 index 0000000..ece53bf --- /dev/null +++ b/src/utils/notification/fetchNoti.ts @@ -0,0 +1,21 @@ +import { NotificationDto } from '@/app/_dto/notification/notification.dto'; + +export async function fetchNoti( + onResNotOk?: (code: number, res: Response) => void, +): Promise { + const handle = localStorage.getItem('user_handle'); + if (!handle) { + return; + } + const res = await fetch('/api/user/notification'); + if (!res.ok) { + if (onResNotOk) { + onResNotOk(res.status, res); + return; + } else { + throw new Error(`Fail to fatch notifications! ${await res.text()}`); + } + } + const data = (await res.json()) as NotificationDto; + return data; +} diff --git a/src/utils/profile/fetchMyProfile.ts b/src/utils/profile/fetchMyProfile.ts index bac041b..26625aa 100644 --- a/src/utils/profile/fetchMyProfile.ts +++ b/src/utils/profile/fetchMyProfile.ts @@ -1,6 +1,8 @@ import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; -export async function fetchMyProfile(onResNotOk?: (code: number) => void): Promise { +export async function fetchMyProfile( + onResNotOk?: (code: number, res: Response) => void, +): Promise { const user_handle = localStorage.getItem('user_handle'); if (user_handle) { @@ -9,7 +11,7 @@ export async function fetchMyProfile(onResNotOk?: (code: number) => void): Promi }); if (!res.ok) { if (onResNotOk) { - onResNotOk(res.status); + onResNotOk(res.status, res); } return; } From 13e3bbd5c66b9e176a593c72d56080dd18610f00 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 17:15:50 +0900 Subject: [PATCH 12/12] =?UTF-8?q?=F0=9F=8E=A8=20=5FprofileContext=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이벤트는 _events.ts로 - MyProfileContext는 layout.tsx 로 이동 --- src/app/main/_events.ts | 28 +++++++++++++++++++++++++ src/app/main/_header.tsx | 3 +-- src/app/main/_profileContext.ts | 34 ------------------------------- src/app/main/_websocketManager.ts | 3 +-- src/app/main/layout.tsx | 4 ++-- src/app/main/questions/page.tsx | 2 +- src/app/main/settings/page.tsx | 3 ++- src/app/main/social/page.tsx | 2 +- 8 files changed, 36 insertions(+), 43 deletions(-) delete mode 100644 src/app/main/_profileContext.ts diff --git a/src/app/main/_events.ts b/src/app/main/_events.ts index e8a0cb0..29aa331 100644 --- a/src/app/main/_events.ts +++ b/src/app/main/_events.ts @@ -3,6 +3,7 @@ import { questionDto } from '../_dto/questions/question.dto'; import { AnswerDeletedEvPayload, QuestionDeletedPayload } from '../_dto/websocket-event/websocket-event.dto'; import { AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; import { NotificationPayloadTypes } from '../_dto/notification/notification.dto'; +import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; const QuestionCreateEvent = 'QuestionCreateEvent'; const QuestionDeleteEvent = 'QuestionDeleteEvent'; @@ -114,3 +115,30 @@ export class NotificationEv { NotificationEv.logger.debug('Notification Event Sent', data); } } + +const ProfileUpdateReqEvent = 'ProfileUpdateReqEvent'; +type ProfileUpdateReqEvent = typeof ProfileUpdateReqEvent; +type ProfileUpdateReqData = Partial; +/** + * MyProfileContext 의 Update요청 Event들 + */ +export class MyProfileEv { + private constructor() {} + private static logger = new Logger('UpdateMyProfileContext', { noColor: true }); + static async SendUpdateReq(data: Partial) { + const ev = new CustomEvent(ProfileUpdateReqEvent, { bubbles: true, detail: data }); + window.dispatchEvent(ev); + MyProfileEv.logger.debug('Send My Profile Update Request Event...'); + } + + static addEventListener(onEvent: (ev: CustomEvent) => void) { + MyProfileEv.logger.debug('add Profile Update EventListener'); + window.addEventListener(ProfileUpdateReqEvent, onEvent as EventListener); + } + + static removeEventListener(onEvent: (ev: CustomEvent) => void) { + MyProfileEv.logger.debug('Remove Profile Update Req EventListener'); + window.removeEventListener(ProfileUpdateReqEvent, onEvent as EventListener); + } +} + diff --git a/src/app/main/_header.tsx b/src/app/main/_header.tsx index e7925cd..69e74c6 100644 --- a/src/app/main/_header.tsx +++ b/src/app/main/_header.tsx @@ -6,10 +6,9 @@ import { FaInfoCircle, FaUser } from 'react-icons/fa'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; import { refreshJwt } from '@/utils/refreshJwt/refresh-jwt-token'; import { logout } from '@/utils/logout/logout'; -import { MyProfileContext } from '@/app/main/_profileContext'; import { FaXmark } from 'react-icons/fa6'; import WebSocketState from '../_components/webSocketState'; -import { NotificationContext } from './layout'; +import { MyProfileContext, NotificationContext } from './layout'; import { webSocketManager } from '@/app/main/_websocketManager'; type headerProps = { diff --git a/src/app/main/_profileContext.ts b/src/app/main/_profileContext.ts deleted file mode 100644 index 5309dac..0000000 --- a/src/app/main/_profileContext.ts +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { createContext } from 'react'; -import { userProfileMeDto } from '@/app//_dto/fetch-profile/Profile.dto'; -import { Logger } from '@/utils/logger/Logger'; - -export const MyProfileContext = createContext(undefined); - -const ProfileUpdateReqEvent = 'ProfileUpdateReqEvent'; -type ProfileUpdateReqEvent = typeof ProfileUpdateReqEvent; -type ProfileUpdateReqData = Partial; - -/** - * MyProfileContext 의 Update요청 Event들 - */ -export class MyProfileEv { - private constructor() {} - private static logger = new Logger('UpdateMyProfileContext', { noColor: true }); - static async SendUpdateReq(data: Partial) { - const ev = new CustomEvent(ProfileUpdateReqEvent, { bubbles: true, detail: data }); - window.dispatchEvent(ev); - MyProfileEv.logger.debug('Send My Profile Update Request Event...'); - } - - static addEventListener(onEvent: (ev: CustomEvent) => void) { - MyProfileEv.logger.debug('add Profile Update EventListener'); - window.addEventListener(ProfileUpdateReqEvent, onEvent as EventListener); - } - - static removeEventListener(onEvent: (ev: CustomEvent) => void) { - MyProfileEv.logger.debug('Remove Profile Update Req EventListener'); - window.removeEventListener(ProfileUpdateReqEvent, onEvent as EventListener); - } -} diff --git a/src/app/main/_websocketManager.ts b/src/app/main/_websocketManager.ts index e190615..38c3bf2 100644 --- a/src/app/main/_websocketManager.ts +++ b/src/app/main/_websocketManager.ts @@ -7,8 +7,7 @@ import { WebsocketQuestionCreatedEvent, WebsocketQuestionDeletedEvent, } from '@/app/_dto/websocket-event/websocket-event.dto'; -import { MyQuestionEv, AnswerEv, NotificationEv } from '@/app/main/_events'; -import { MyProfileEv } from '@/app/main/_profileContext'; +import { MyQuestionEv, AnswerEv, NotificationEv, MyProfileEv } from '@/app/main/_events'; import { Dispatch, MutableRefObject, SetStateAction } from 'react'; type managerPropsType = { diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index 89a1f62..94dbd24 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -3,9 +3,8 @@ import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; import MainHeader from '@/app/main/_header'; import { createContext, useEffect, useRef, useState } from 'react'; -import { MyProfileContext, MyProfileEv } from '@/app/main/_profileContext'; import { AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; -import { AnswerEv, NotificationEv } from './_events'; +import { AnswerEv, MyProfileEv, NotificationEv } from './_events'; import { NotificationDto, NotificationPayloadTypes } from '../_dto/notification/notification.dto'; import { AnswerDeletedEvPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; import { Logger } from '@/utils/logger/Logger'; @@ -22,6 +21,7 @@ type MainPageContextType = { }; export const AnswersContext = createContext(undefined); export const NotificationContext = createContext(undefined); +export const MyProfileContext = createContext(undefined); export default function MainLayout({ modal, children }: { children: React.ReactNode; modal: React.ReactNode }) { const [userProfileData, setUserProfileData] = useState(); diff --git a/src/app/main/questions/page.tsx b/src/app/main/questions/page.tsx index 6647d93..54a6a59 100644 --- a/src/app/main/questions/page.tsx +++ b/src/app/main/questions/page.tsx @@ -4,11 +4,11 @@ import Question from '@/app/_components/question'; import { useContext, useEffect, useRef, useState } from 'react'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; import DialogModalLoadingOneButton from '@/app/_components/modalLoadingOneButton'; -import { MyProfileContext } from '@/app/main/_profileContext'; import { questionDto } from '@/app/_dto/questions/question.dto'; import { MyQuestionEv } from '../_events'; import { Logger } from '@/utils/logger/Logger'; import { QuestionDeletedPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; +import { MyProfileContext } from '@/app/main/layout'; const fetchQuestions = async (): Promise => { const res = await fetch('/api/db/questions'); diff --git a/src/app/main/settings/page.tsx b/src/app/main/settings/page.tsx index 1b2b206..bd47f6d 100644 --- a/src/app/main/settings/page.tsx +++ b/src/app/main/settings/page.tsx @@ -6,13 +6,14 @@ import { useContext, useEffect, useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; import { UserSettingsUpdateDto } from '@/app/_dto/settings/settings.dto'; import { $Enums } from '@prisma/client'; -import { MyProfileEv, MyProfileContext } from '@/app/main/_profileContext'; import BlockList from '@/app/main/settings/_table'; import CollapseMenu from '@/app/_components/collapseMenu'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; import { AccountCleanReqDto } from '@/app/_dto/account-clean/account-clean.dto'; import { FaLock, FaUserLargeSlash } from 'react-icons/fa6'; import { MdDeleteSweep, MdOutlineCleaningServices } from 'react-icons/md'; +import { MyProfileContext } from '@/app/main/layout'; +import { MyProfileEv } from '@/app/main/_events'; export type FormValue = { stopAnonQuestion: boolean; diff --git a/src/app/main/social/page.tsx b/src/app/main/social/page.tsx index 73a400e..18f3b87 100644 --- a/src/app/main/social/page.tsx +++ b/src/app/main/social/page.tsx @@ -2,8 +2,8 @@ import UsernameAndProfile from '@/app/_components/userProfile'; import { FollowingListResDto } from '@/app/_dto/following/following.dto'; +import { MyProfileContext } from '@/app/main/layout'; import { useContext, useEffect, useState } from 'react'; -import { MyProfileContext } from '../_profileContext'; export default function Social() { const [following, setFollowing] = useState();