Skip to content

Commit

Permalink
feat: header부분 알림 기능 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
LeeSG98 committed Jun 13, 2024
1 parent 3378f7e commit a7a9b0d
Show file tree
Hide file tree
Showing 14 changed files with 410 additions and 27 deletions.
73 changes: 73 additions & 0 deletions components/NotificationModal/NotificationModal.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
44 changes: 44 additions & 0 deletions components/NotificationModal/components/Notification.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
49 changes: 49 additions & 0 deletions components/NotificationModal/components/Notification.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.wrapper} onClick={handleDeleteNotification}>
<div className={styles.container}>
<div
className={styles.circle}
style={{
background:
result === "accepted"
? "var(--blue20, #0080FF)"
: "var(--red30, #EC5A46)",
}}
></div>
<div className={styles.notifiText}>
{name} ({formattedTime}) 공고 지원이{" "}
<span
className={styles.result}
style={{
color: result === "accepted" ? "var(--blue20)" : "var(--red30)",
}}
>
{result === "accepted" ? "승인" : "거절"}
</span>{" "}
되었어요.
</div>
</div>
<span className={styles.elapsedTime}>{elapsedTime}</span>
</div>
);
}
6 changes: 6 additions & 0 deletions components/NotificationModal/hooks/useUserQuery.ts
Original file line number Diff line number Diff line change
@@ -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));
}
40 changes: 40 additions & 0 deletions components/NotificationModal/hooks/userRequest.ts
Original file line number Diff line number Diff line change
@@ -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("네트워크 오류가 발생했습니다.");
}
}
};
46 changes: 46 additions & 0 deletions components/NotificationModal/index.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<div className={styles.wrapper}>
<div className={styles.container}>
<span
className={styles.title}
>{`알림 ${notificationList.length}개`}</span>
<Image
className={styles.styledCloseIcon}
src={closeIcon}
alt="close_icon"
onClick={handleClickNoti}
/>
</div>
{notificationList.length ? (
notifications.map((notification, index) => (
<Notification key={index} {...notification} />
))
) : (
<div className={styles.alertDiv}>알림이 없습니다.</div>
)}
</div>
)
);
}
26 changes: 26 additions & 0 deletions components/NotificationModal/types/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
};
};
}
22 changes: 22 additions & 0 deletions components/NotificationModal/utils/extractNotificationInfo.ts
Original file line number Diff line number Diff line change
@@ -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;
}
35 changes: 35 additions & 0 deletions components/NotificationModal/utils/formatTimeRange.ts
Original file line number Diff line number Diff line change
@@ -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}`;
};
18 changes: 18 additions & 0 deletions components/NotificationModal/utils/getElapsedTime.ts
Original file line number Diff line number Diff line change
@@ -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}일 전`;
}
}
Loading

0 comments on commit a7a9b0d

Please sign in to comment.