From a7a9b0de7668ad297f77cadf801735129613ba41 Mon Sep 17 00:00:00 2001 From: LeeSoonGu Date: Thu, 13 Jun 2024 12:05:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20header=EB=B6=80=EB=B6=84=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=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 --- .../NotificationModal.module.scss | 73 +++++++++++++++++++ .../components/Notification.module.scss | 44 +++++++++++ .../components/Notification.tsx | 49 +++++++++++++ .../NotificationModal/hooks/useUserQuery.ts | 6 ++ .../NotificationModal/hooks/userRequest.ts | 40 ++++++++++ components/NotificationModal/index.tsx | 46 ++++++++++++ components/NotificationModal/types/types.ts | 26 +++++++ .../utils/extractNotificationInfo.ts | 22 ++++++ .../utils/formatTimeRange.ts | 35 +++++++++ .../NotificationModal/utils/getElapsedTime.ts | 18 +++++ .../Header/NotiButton/NotiButtons.module.scss | 30 ++++---- components/layout/Header/NotiButton/index.tsx | 33 ++++++--- components/layout/Header/Uignb/index.tsx | 5 +- public/image/close_icon.svg | 10 +++ 14 files changed, 410 insertions(+), 27 deletions(-) create mode 100644 components/NotificationModal/NotificationModal.module.scss create mode 100644 components/NotificationModal/components/Notification.module.scss create mode 100644 components/NotificationModal/components/Notification.tsx create mode 100644 components/NotificationModal/hooks/useUserQuery.ts create mode 100644 components/NotificationModal/hooks/userRequest.ts create mode 100644 components/NotificationModal/index.tsx create mode 100644 components/NotificationModal/types/types.ts create mode 100644 components/NotificationModal/utils/extractNotificationInfo.ts create mode 100644 components/NotificationModal/utils/formatTimeRange.ts create mode 100644 components/NotificationModal/utils/getElapsedTime.ts create mode 100644 public/image/close_icon.svg diff --git a/components/NotificationModal/NotificationModal.module.scss b/components/NotificationModal/NotificationModal.module.scss new file mode 100644 index 0000000..74ba313 --- /dev/null +++ b/components/NotificationModal/NotificationModal.module.scss @@ -0,0 +1,73 @@ +// NotificationModal.module.scss + +.wrapper { + position: absolute; + top: 35px; + right: 0px; + + display: flex; + padding: 14px 20px; + flex-direction: column; + align-items: flex-start; + width: 368px; + gap: 16px; + + border-radius: 10px; + border: 1px solid var(--gray30); + background-color: #f9f9fb; + box-shadow: 0px 2px 8px 0px rgba(60, 59, 62, 0.26); + + z-index: 10; + + @media only screen and (max-width: 768px) { + top: 0px; + position: fixed; + box-shadow: none; + width: 100%; + height: 100vh; + border: none; + border-radius: 0; + } +} + +.title { + color: var(--black); + font-family: "Spoqa Han Sans Neo"; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.alert-div { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 300px; + font-family: "Spoqa Han Sans Neo"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 22px; + + @media only screen and (max-width: 768px) { + height: 100%; + } +} + +.styled-close-icon { + display: block; + + @media (max-width: 375px) { + cursor: pointer; + display: block; + } +} + +.container { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/components/NotificationModal/components/Notification.module.scss b/components/NotificationModal/components/Notification.module.scss new file mode 100644 index 0000000..a15f7bc --- /dev/null +++ b/components/NotificationModal/components/Notification.module.scss @@ -0,0 +1,44 @@ +.wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + width: 100%; + padding: 10px 12px; + border-radius: 5px; + border: 1px solid var(--gray20); + background: var(--white); + + .container { + display: flex; + align-items: flex-start; + gap: 4px; + } + + .circle { + display: block; + min-width: 4px; + height: 4px; + border-radius: 50%; + margin-top: 10px; + } + + .result { + font-weight: 700; + } + + .notifi-text { + color: var(---black); + text-align: left; + font-family: "Spoqa Han Sans Neo"; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 20px; + } + + .elapsed-time { + color: var(--gray40); + font-size: 12px; + } +} diff --git a/components/NotificationModal/components/Notification.tsx b/components/NotificationModal/components/Notification.tsx new file mode 100644 index 0000000..76483f4 --- /dev/null +++ b/components/NotificationModal/components/Notification.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import styles from "./Notification.module.scss"; +import type { Notification } from "../types/types"; +import useCookie from "@/hooks/useCookies"; +import { useClearNotification } from "../hooks/useUserQuery"; + +export default function Notification({ + alertId, + name, + result, + elapsedTime, + formattedTime, +}: Notification) { + const { id, jwt } = useCookie(); + const mutation = useClearNotification(id, alertId, jwt); + + const handleDeleteNotification = () => { + mutation.mutate(); + }; + + return ( +
+
+
+
+ {name} ({formattedTime}) 공고 지원이{" "} + + {result === "accepted" ? "승인" : "거절"} + {" "} + 되었어요. +
+
+ {elapsedTime} +
+ ); +} diff --git a/components/NotificationModal/hooks/useUserQuery.ts b/components/NotificationModal/hooks/useUserQuery.ts new file mode 100644 index 0000000..22500ab --- /dev/null +++ b/components/NotificationModal/hooks/useUserQuery.ts @@ -0,0 +1,6 @@ +import { useMutation } from "react-query"; +import { ClearNotification } from "./userRequest"; + +export function useClearNotification(id: string, alertId: string, jwt: string) { + return useMutation(() => ClearNotification(id, alertId, jwt)); +} diff --git a/components/NotificationModal/hooks/userRequest.ts b/components/NotificationModal/hooks/userRequest.ts new file mode 100644 index 0000000..e09539a --- /dev/null +++ b/components/NotificationModal/hooks/userRequest.ts @@ -0,0 +1,40 @@ +import axios from "axios"; +import { BASE_URL } from "@/constants/constants"; + +export const ClearNotification = async ( + id: string, + alertId: string, + jwt: string +) => { + try { + await axios.put( + `${BASE_URL}/users/${id}/alerts/${alertId}`, + {}, + { + headers: { + Authorization: `${jwt}`, + }, + } + ); + + return { success: true }; + } catch (error) { + if (axios.isAxiosError(error)) { + const status = error.response ? error.response.status : null; + switch (status) { + case 400: + throw new Error("잘못된 요청입니다. 요청 형식을 확인해 주세요."); + case 403: + throw new Error("접근이 거부되었습니다. 권한을 확인해 주세요."); + case 404: + throw new Error( + "요청한 리소스를 찾을 수 없습니다. URL을 확인해 주세요." + ); + default: + throw new Error("알 수 없는 에러가 발생했습니다."); + } + } else { + throw new Error("네트워크 오류가 발생했습니다."); + } + } +}; diff --git a/components/NotificationModal/index.tsx b/components/NotificationModal/index.tsx new file mode 100644 index 0000000..204a8d0 --- /dev/null +++ b/components/NotificationModal/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import styles from "./NotificationModal.module.scss"; +import Image from "next/image"; +import Notification from "./components/Notification"; +import extractNotificationInfo from "./utils/extractNotificationInfo"; +import { NotificationItem } from "./types/types"; +import closeIcon from "@/public/image/close_icon.svg"; + +interface NotificationModalProps { + handleClickNoti: () => void; + isModalOpen: boolean; + notificationList: NotificationItem[]; +} + +export default function NotificationModal({ + handleClickNoti, + isModalOpen, + notificationList, +}: NotificationModalProps) { + const notifications = extractNotificationInfo(notificationList); + + return ( + isModalOpen && ( +
+
+ {`알림 ${notificationList.length}개`} + close_icon +
+ {notificationList.length ? ( + notifications.map((notification, index) => ( + + )) + ) : ( +
알림이 없습니다.
+ )} +
+ ) + ); +} diff --git a/components/NotificationModal/types/types.ts b/components/NotificationModal/types/types.ts new file mode 100644 index 0000000..658b6a1 --- /dev/null +++ b/components/NotificationModal/types/types.ts @@ -0,0 +1,26 @@ +export interface Notification { + alertId: string; + name: string; + result: string; + elapsedTime: string; + formattedTime: string; +} + +export interface NotificationItem { + item: { + id: string; + createdAt: string; + result: string; + shop: { + item: { + name: string; + }; + }; + notice: { + item: { + startsAt: string; + workhour: number; + }; + }; + }; +} diff --git a/components/NotificationModal/utils/extractNotificationInfo.ts b/components/NotificationModal/utils/extractNotificationInfo.ts new file mode 100644 index 0000000..9e51e13 --- /dev/null +++ b/components/NotificationModal/utils/extractNotificationInfo.ts @@ -0,0 +1,22 @@ +import getElapsedTime from "./getElapsedTime"; +import { NotificationItem } from "../types/types"; +import formatTimeRange from "./formatTimeRange"; + +export default function extractNotificationInfo( + notificationList: NotificationItem[] +) { + const notifications = notificationList.map(({ item }) => { + const { id, createdAt, result, shop, notice } = item; + const { name } = shop.item; + const { startsAt, workhour } = notice.item; + + const elapsedTime = getElapsedTime(createdAt); + const formattedTime = formatTimeRange(startsAt, workhour); + + const alertId = id; + + return { alertId, name, result, elapsedTime, formattedTime }; + }); + + return notifications; +} diff --git a/components/NotificationModal/utils/formatTimeRange.ts b/components/NotificationModal/utils/formatTimeRange.ts new file mode 100644 index 0000000..4f745d6 --- /dev/null +++ b/components/NotificationModal/utils/formatTimeRange.ts @@ -0,0 +1,35 @@ +export default function formatTimeRange(startsAt: string, workhour: number) { + const startsTime = new Date(startsAt); + const endsTime = new Date(startsTime.getTime() + workhour * 60 * 60 * 1000); + + const formattedStartsAt = formatDate(startsTime, true); + const formattedEndsAt = formatDate(endsTime, false); + + return `${formattedStartsAt} ~ ${formattedEndsAt}`; +} + +function formatDate(date: Date, isStartTime: boolean): string { + const newDate = date.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" }); + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + + return isStartTime + ? `${year}-${month}-${day} ${hours}:${minutes}` + : `${hours}:${minutes}`; +} + +export const utilFormatDuration = (duration: string, workhour: number) => { + const date = duration.slice(0, 10).replace(/-/g, "."); + const hours = parseInt(duration.slice(11, 13)); + const minutes = duration.slice(14, 16); + + let endHours = hours + workhour; + + const startTime = `${hours.toString().padStart(2, "0")}:${minutes}`; + const endTime = `${endHours}:${minutes}`; + + return `${date} ${startTime}~${endTime}`; +}; diff --git a/components/NotificationModal/utils/getElapsedTime.ts b/components/NotificationModal/utils/getElapsedTime.ts new file mode 100644 index 0000000..1b5878c --- /dev/null +++ b/components/NotificationModal/utils/getElapsedTime.ts @@ -0,0 +1,18 @@ +export default function getElapsedTime(timeString: string) { + const currentTime = new Date(); + const givenTime = new Date(timeString); + const elapsedMilliseconds = currentTime.getTime() - givenTime.getTime(); + const elapsedMinutes = Math.floor(elapsedMilliseconds / (1000 * 60)); + + if (elapsedMinutes < 1) { + return "방금 전"; + } else if (elapsedMinutes < 60) { + return `${elapsedMinutes}분 전`; + } else if (elapsedMinutes < 60 * 24) { + const elapsedHours = Math.floor(elapsedMinutes / 60); + return `${elapsedHours}시간 전`; + } else { + const elapsedDays = Math.floor(elapsedMinutes / (60 * 24)); + return `${elapsedDays}일 전`; + } +} diff --git a/components/layout/Header/NotiButton/NotiButtons.module.scss b/components/layout/Header/NotiButton/NotiButtons.module.scss index ea76b38..c6b7916 100644 --- a/components/layout/Header/NotiButton/NotiButtons.module.scss +++ b/components/layout/Header/NotiButton/NotiButtons.module.scss @@ -1,18 +1,18 @@ -// .notiButton { - +// .modalContainer { +// position: absolute; +// z-index: 1000; +// top: +50px; +// right: 350px; +// height: 200px; +// width: 368px; +// padding: 0 20px 24px; +// overflow-y: auto; +// border: 1px solid var(--gray30); +// border-radius: 10px; +// background-color: #c1bce7; +// box-shadow: 0 2px 8px 0 #c1b7ec40; // } -.modalContainer { - position: absolute; - z-index: 1000; - top: +50px; - right: 350px; - height: 200px; - width: 368px; - padding: 0 20px 24px; - overflow-y: auto; - border: 1px solid var(--gray30); - border-radius: 10px; - background-color: #c1bce7; - box-shadow: 0 2px 8px 0 #c1b7ec40; +.button { + position: relative; } diff --git a/components/layout/Header/NotiButton/index.tsx b/components/layout/Header/NotiButton/index.tsx index 5fdd035..784922c 100644 --- a/components/layout/Header/NotiButton/index.tsx +++ b/components/layout/Header/NotiButton/index.tsx @@ -1,34 +1,45 @@ import React, { useState } from "react"; +import NotificationModal from "@/components/NotificationModal"; +import useCookie from "@/hooks/useCookies"; import Image from "next/image"; +import { useNoticesData } from "@/hooks/useUserQuery"; import styles from "./NotiButtons.module.scss"; -import useCookie from "@/hooks/useCookies"; -import { useNoticesData } from "../../../../hooks/useUserQuery"; export default function NotiButton() { const [isModalOpen, setIsModalOpen] = useState(false); const { jwt, id } = useCookie(); - const result = useNoticesData(id, jwt); - const activeStatus = result?.data?.count ? "active" : "inactive"; + const response = useNoticesData(id, jwt); + + const responseList = response?.data?.items ?? []; + + const notificationList = responseList.filter( + (item: { item: { read: boolean } }) => item.item.read === false + ); + + const activeStatus = notificationList.length > 0 ? "active" : "inactive"; const handleClickNoti = () => { - if (isModalOpen) { - setIsModalOpen(false); - return; - } - setIsModalOpen(true); + setIsModalOpen(!isModalOpen); }; return ( <> - - {isModalOpen &&
} ); } diff --git a/components/layout/Header/Uignb/index.tsx b/components/layout/Header/Uignb/index.tsx index 6f30df6..1a9e59a 100644 --- a/components/layout/Header/Uignb/index.tsx +++ b/components/layout/Header/Uignb/index.tsx @@ -13,7 +13,10 @@ export default function UiGnb({ userType, handleClickMovePage }: GnbProps) { 더줄게 - + ); } diff --git a/public/image/close_icon.svg b/public/image/close_icon.svg new file mode 100644 index 0000000..45c5f3b --- /dev/null +++ b/public/image/close_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file