From 288756bdd5227f164e0cc77a2c31f3385cfedd18 Mon Sep 17 00:00:00 2001 From: Kled <1017yu@daum.net> Date: Tue, 2 Apr 2024 17:12:01 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/notification/api/getSubscribe.ts | 22 ++++++++++++++++++++++ src/app/sign-in/kakao/page.tsx | 4 +++- src/constants/api/end-point.ts | 6 ++++-- src/types/notification/index.ts | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/app/notification/api/getSubscribe.ts diff --git a/src/app/notification/api/getSubscribe.ts b/src/app/notification/api/getSubscribe.ts new file mode 100644 index 00000000..f49658c2 --- /dev/null +++ b/src/app/notification/api/getSubscribe.ts @@ -0,0 +1,22 @@ +import { END_POINT } from '@/constants/api/end-point'; +import CustomError from '@/error/CustomError'; +import { axiosInstance } from '@/lib/axios/axios-instance'; +import { TResponse } from '@/types/common/response'; +import { SubscribeResponse } from '@/types/notification'; + +export const getSubscribe = async () => { + const { main, getSubscribe } = END_POINT.notificationController; + + try { + const response = await axiosInstance>( + main + getSubscribe, + ); + + return response; + } catch (error: unknown) { + if (error instanceof CustomError) { + throw new Error(error.message); + } + throw new Error(String(error)); + } +}; diff --git a/src/app/sign-in/kakao/page.tsx b/src/app/sign-in/kakao/page.tsx index e850f173..581f9c77 100644 --- a/src/app/sign-in/kakao/page.tsx +++ b/src/app/sign-in/kakao/page.tsx @@ -12,9 +12,10 @@ import { encrypt } from '@/utils/crypto'; import { useRouter } from 'next/navigation'; import { getEmail } from '@/utils/getEmail'; import { useToast } from '@/app/_components/ui/use-toast'; +import { SIGN_UP_ERROR_MESSAGES } from '@/constants/sign-in'; import { setTokensInCookies } from '@/utils/setTokensInCookies'; +import { getSubscribe } from '@/app/notification/api/getSubscribe'; import { ERROR_CODE, STATUS_CODE } from '@/constants/api/statusCode'; -import { SIGN_UP_ERROR_MESSAGES } from '@/constants/sign-in'; // 카카오 소셜 로그인 REDIRECT URI PAGE function Page() { @@ -43,6 +44,7 @@ function Page() { case STATUS_CODE.ok: router.push(service); setId(response.data.id); + await getSubscribe(); break; case invalidEmail: diff --git a/src/constants/api/end-point.ts b/src/constants/api/end-point.ts index 9e4debfd..267723b3 100644 --- a/src/constants/api/end-point.ts +++ b/src/constants/api/end-point.ts @@ -161,14 +161,16 @@ export const END_POINT = { notificationController: { // DEFAULT - default: '/notification', + main: '/notification', getNotification: (page: number) => { const url = new URLSearchParams(); url.append('page', String(page)); - return `${END_POINT.notificationController.default}?${url.toString()}`; + return `${END_POINT.notificationController.main}?${url.toString()}`; }, + + getSubscribe: '/subscribe', }, }; diff --git a/src/types/notification/index.ts b/src/types/notification/index.ts index 809a473b..04e7d53d 100644 --- a/src/types/notification/index.ts +++ b/src/types/notification/index.ts @@ -15,3 +15,9 @@ export interface NotificationProps { export interface NotificationData extends Pagination { content: NotificationProps[]; } + +export interface SubscribeResponse { + id: string; + event: string; + data: string; +} From 333bd4c5621e1ae67f28fb695626ec58f06a0473 Mon Sep 17 00:00:00 2001 From: Kled <1017yu@daum.net> Date: Tue, 2 Apr 2024 17:13:50 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20eslint=20=EC=9E=AC=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.json | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 39f3f467..eff4c942 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,7 @@ "es2021": true, "jest": true }, - "extends": ["next/core-web-vitals", "airbnb", "airbnb-typescript"], + "extends": ["next/core-web-vitals"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { @@ -17,33 +17,8 @@ "plugins": ["react", "@typescript-eslint"], "rules": { "react-hooks/exhaustive-deps": "off", - "react/jsx-props-no-spreading": "off", - "linebreak-style": 0, - "import/no-extraneous-dependencies": "off", "import/prefer-default-export": ["off", { "target": "single" }], - "object-curly-newline": "off", - "arrow-parens": "off", - "arrow-body-style": "off", - "@typescript-eslint/no-shadow": "off", - "operator-linebreak": "off", - "react/require-default-props": "off", - "implicit-arrow-linebreak": "off", - "@typescript-eslint/naming-convention": "off", - "consistent-return": "off", - "@typescript-eslint/indent": "off", - "jsx-a11y/control-has-associated-label": "off", - "react/jsx-wrap-multilines": "off", - "react/jsx-indent": "off", - "react/no-invalid-html-attribute": "off", - "eslint-plugin-import/no-cycle": "off", - "import/no-cycle":"off", - "no-param-reassign":"off", - "jsx-a11y/label-has-associated-control": [ - 2, - { - "labelAttributes": ["htmlFor"] - } - ] + "react/display-name": "off" }, "globals": { "React": "writable" From 89854cded52fe093a38eebe459ca792d826bc735 Mon Sep 17 00:00:00 2001 From: Kled <1017yu@daum.net> Date: Tue, 2 Apr 2024 17:14:17 +0900 Subject: [PATCH 3/6] =?UTF-8?q?chore:=20localhost=20https=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- init-https.sh | 10 ++++++++++ package.json | 1 + server.js | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 init-https.sh create mode 100644 server.js diff --git a/init-https.sh b/init-https.sh new file mode 100644 index 00000000..5ddb3505 --- /dev/null +++ b/init-https.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +MKCERT_INSTALLED=$(which mkcert) + +if [ -z $MKCERT_INSTALLED ];then + brew install mkcert +fi + +mkcert -install +mkcert localhost \ No newline at end of file diff --git a/package.json b/package.json index 79d5d15a..ff960de3 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "init-https": "sh init-https.sh", "dev": "next dev", "build": "next build", "build:dev": "env-cmd -f .env.development next build", diff --git a/server.js b/server.js new file mode 100644 index 00000000..bf220d60 --- /dev/null +++ b/server.js @@ -0,0 +1,40 @@ +const http = require('http'); +const { parse } = require('url'); +const next = require('next'); + +const https = require('https'); +const fs = require('fs'); + +const dev = process.env.NODE_ENV !== 'production'; +const app = next({ dev }); +const handle = app.getRequestHandler(); + +const PORT = 3000; + +const httpsOptions = { + key: fs.readFileSync('./key.pem'), + cert: fs.readFileSync('./cert.pem'), +}; + +app.prepare().then(() => { + http + .createServer((req, res) => { + const parsedUrl = parse(req.url, true); + handle(req, res, parsedUrl); + }) + .listen(PORT, err => { + if (err) throw err; + console.log(`> Ready on http://localhost:${PORT}`); + }); + + // https 서버 추가 + https + .createServer(httpsOptions, (req, res) => { + const parsedUrl = parse(req.url, true); + handle(req, res, parsedUrl); + }) + .listen(PORT + 1, err => { + if (err) throw err; + console.log(`> HTTPS: Ready on https://localhost:${PORT + 1}`); + }); +}); From 42a5ea1c3b2f236b0a7565ed318e007eba004f88 Mon Sep 17 00:00:00 2001 From: Kled <1017yu@daum.net> Date: Tue, 9 Apr 2024 10:27:53 +0900 Subject: [PATCH 4/6] =?UTF-8?q?chore:=20SSE=EB=A5=BC=20=EC=9C=84=ED=95=9C?= =?UTF-8?q?=20polyfill=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 13 +++++++++++++ package.json | 2 ++ 2 files changed, 15 insertions(+) diff --git a/package-lock.json b/package-lock.json index b3fa2b11..85ef3a98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "dayjs": "^1.11.10", "embla-carousel-autoplay": "^8.0.0-rc22", "embla-carousel-react": "^8.0.0-rc17", + "event-source-polyfill": "^1.0.31", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.299.0", "next": "14.0.3", @@ -68,6 +69,7 @@ "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "@types/crypto-js": "^4.2.1", + "@types/event-source-polyfill": "^1.0.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.10.4", @@ -4708,6 +4710,12 @@ "@types/estree": "*" } }, + "node_modules/@types/event-source-polyfill": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz", + "integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==", + "dev": true + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -8565,6 +8573,11 @@ "node": ">=0.10.0" } }, + "node_modules/event-source-polyfill": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz", + "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==" + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", diff --git a/package.json b/package.json index ff960de3..ee5444c8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dayjs": "^1.11.10", "embla-carousel-autoplay": "^8.0.0-rc22", "embla-carousel-react": "^8.0.0-rc17", + "event-source-polyfill": "^1.0.31", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.299.0", "next": "14.0.3", @@ -74,6 +75,7 @@ "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "@types/crypto-js": "^4.2.1", + "@types/event-source-polyfill": "^1.0.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.10.4", From 4f7ccfa2ef2fa87237b442311878a11a72aa1c42 Mon Sep 17 00:00:00 2001 From: Kled <1017yu@daum.net> Date: Tue, 9 Apr 2024 10:28:28 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/notification/_components/Card.tsx | 36 +++++++++++++++----- src/app/notification/api/readNotification.ts | 15 ++++++++ src/constants/api/end-point.ts | 4 +++ 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 src/app/notification/api/readNotification.ts diff --git a/src/app/notification/_components/Card.tsx b/src/app/notification/_components/Card.tsx index ef7f8554..4ea8b2ca 100644 --- a/src/app/notification/_components/Card.tsx +++ b/src/app/notification/_components/Card.tsx @@ -2,17 +2,37 @@ import Link from 'next/link'; import Image from 'next/image'; import { Megaphone } from 'lucide-react'; -import type { NotificationProps } from '@/types/notification'; import { COLOR } from '@/styles/color'; +import type { NotificationProps } from '@/types/notification'; +import getTimeDiff from '@/utils/getTimeDiff'; +import { useRouter } from 'next/navigation'; +import readNotification from '../api/readNotification'; +import clsx from 'clsx'; // is_read 읽음 여부에 따른 디자인 변화 필요 function Card({ ...props }: NotificationProps) { - const { url, title, content, created_at, image } = props; + const { url, title, content, created_at, image, id, is_read } = props; + const router = useRouter(); + const timeDiff = getTimeDiff(created_at); + const buttonClassName = (isRead?: boolean) => + clsx( + 'w-[calc(100%-32px)] bg-white rounded-[10px] border my-2 mx-4 flex px-[10px] py-[14px] gap-3', + { + 'border-purple-200 bg-[#F2EDFF]': !isRead, + 'border-grey-200 bg-white': isRead, + }, + ); + + const handleClick = (id: number) => { + router.push(url); + readNotification(id); + }; return ( - handleClick(id)} + className={buttonClassName(is_read)} > {image ? ( )} -
+
{title} {content} - {created_at} + {timeDiff}
- + ); } diff --git a/src/app/notification/api/readNotification.ts b/src/app/notification/api/readNotification.ts new file mode 100644 index 00000000..3e30ec72 --- /dev/null +++ b/src/app/notification/api/readNotification.ts @@ -0,0 +1,15 @@ +import CustomError from '@/error/CustomError'; +import { axiosInstance } from '@/lib/axios/axios-instance'; +import { END_POINT } from '@/constants/api/end-point'; + +const readNotification = async (id: number) => { + try { + await axiosInstance.patch(END_POINT.notificationController.read(id)); + } catch (error: unknown) { + if (error instanceof CustomError) { + throw new Error(error.message); + } + } +}; + +export default readNotification; diff --git a/src/constants/api/end-point.ts b/src/constants/api/end-point.ts index 267723b3..7d637a32 100644 --- a/src/constants/api/end-point.ts +++ b/src/constants/api/end-point.ts @@ -171,6 +171,10 @@ export const END_POINT = { return `${END_POINT.notificationController.main}?${url.toString()}`; }, + read: (notificationId: number) => { + return `${END_POINT.notificationController.main}/${notificationId}/read`; + }, + getSubscribe: '/subscribe', }, }; From f27152d6505f5d3ab1e885249c0be27c03013655 Mon Sep 17 00:00:00 2001 From: Kled <1017yu@daum.net> Date: Tue, 9 Apr 2024 10:49:04 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20sse=201=EC=B0=A8=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/notification/api/fetchSse.ts | 47 ++++++++++++++++++++++++ src/app/notification/api/getSubscribe.ts | 22 ----------- src/app/service/page.tsx | 2 +- src/app/sign-in/kakao/page.tsx | 4 +- 4 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 src/app/notification/api/fetchSse.ts delete mode 100644 src/app/notification/api/getSubscribe.ts diff --git a/src/app/notification/api/fetchSse.ts b/src/app/notification/api/fetchSse.ts new file mode 100644 index 00000000..4ceb04c7 --- /dev/null +++ b/src/app/notification/api/fetchSse.ts @@ -0,0 +1,47 @@ +import { BACK_URL } from '@/constants/api'; +import { END_POINT } from '@/constants/api/end-point'; +import CustomError from '@/error/CustomError'; +import { getCookie } from '@/lib/cookie'; +import { decrypt } from '@/utils/crypto'; +import { EventSourcePolyfill } from 'event-source-polyfill'; + +export const fetchSse = async () => { + const { main, getSubscribe } = END_POINT.notificationController; + const accessToken = decrypt(getCookie({ name: 'access_token' })); + + try { + const eventSource = new EventSourcePolyfill( + BACK_URL + main + getSubscribe, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + eventSource.onopen = () => { + console.log('Connection established.'); + }; + + // 서버로부터 빈 메시지가 수신되면 이를 무시합니다. + eventSource.onmessage = event => { + if (event.data.trim() === '') { + return; + } + const eventData = JSON.parse(event.data); + console.log('Received message:', eventData); + // 여기서 이벤트 데이터를 처리하거나 화면에 표시하는 등의 작업을 수행합니다. + }; + + // 오류 처리 로직을 추가합니다. + eventSource.onerror = error => { + console.error('EventSource error:', error); + eventSource.close(); + }; + } catch (error: unknown) { + if (error instanceof CustomError) { + throw new Error(error.message); + } + throw new Error(String(error)); + } +}; diff --git a/src/app/notification/api/getSubscribe.ts b/src/app/notification/api/getSubscribe.ts deleted file mode 100644 index f49658c2..00000000 --- a/src/app/notification/api/getSubscribe.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { END_POINT } from '@/constants/api/end-point'; -import CustomError from '@/error/CustomError'; -import { axiosInstance } from '@/lib/axios/axios-instance'; -import { TResponse } from '@/types/common/response'; -import { SubscribeResponse } from '@/types/notification'; - -export const getSubscribe = async () => { - const { main, getSubscribe } = END_POINT.notificationController; - - try { - const response = await axiosInstance>( - main + getSubscribe, - ); - - return response; - } catch (error: unknown) { - if (error instanceof CustomError) { - throw new Error(error.message); - } - throw new Error(String(error)); - } -}; diff --git a/src/app/service/page.tsx b/src/app/service/page.tsx index 32f0e2bf..01e41b79 100644 --- a/src/app/service/page.tsx +++ b/src/app/service/page.tsx @@ -14,7 +14,7 @@ function Page() {
<Link href={CALLBACK_URL.notification}> - <Bell strokeWidth={2.5} /> + <Bell strokeWidth={2} /> </Link> </section> <Notice /> diff --git a/src/app/sign-in/kakao/page.tsx b/src/app/sign-in/kakao/page.tsx index 581f9c77..5fa38836 100644 --- a/src/app/sign-in/kakao/page.tsx +++ b/src/app/sign-in/kakao/page.tsx @@ -14,7 +14,7 @@ import { getEmail } from '@/utils/getEmail'; import { useToast } from '@/app/_components/ui/use-toast'; import { SIGN_UP_ERROR_MESSAGES } from '@/constants/sign-in'; import { setTokensInCookies } from '@/utils/setTokensInCookies'; -import { getSubscribe } from '@/app/notification/api/getSubscribe'; +import { fetchSse } from '@/app/notification/api/fetchSse'; import { ERROR_CODE, STATUS_CODE } from '@/constants/api/statusCode'; // 카카오 소셜 로그인 REDIRECT URI PAGE @@ -44,7 +44,7 @@ function Page() { case STATUS_CODE.ok: router.push(service); setId(response.data.id); - await getSubscribe(); + await fetchSse(); break; case invalidEmail: