diff --git a/.gitignore b/.gitignore index 7ceb59f8..b5a8a278 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +mocks # Editor directories and files .vscode/* diff --git a/package-lock.json b/package-lock.json index 4a751573..48c536c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.1", + "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.1.8" }, "devDependencies": { @@ -4109,6 +4110,15 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tailwind-scrollbar-hide": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-4.0.0.tgz", + "integrity": "sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" + } + }, "node_modules/tailwindcss": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", diff --git a/package.json b/package.json index fa551def..d109a749 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.1", + "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.1.8" }, "devDependencies": { diff --git a/src/assets/icons/.gitkeep b/src/assets/icons/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/assets/icons/ic_close.svg b/src/assets/icons/ic_close.svg new file mode 100644 index 00000000..50f36dc1 --- /dev/null +++ b/src/assets/icons/ic_close.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/components/common/.gitkeep b/src/components/common/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/common/NotificationModal/NotificationCard.tsx b/src/components/common/NotificationModal/NotificationCard.tsx new file mode 100644 index 00000000..741a1c13 --- /dev/null +++ b/src/components/common/NotificationModal/NotificationCard.tsx @@ -0,0 +1,48 @@ +import calculateTimeDifference from '@/utils/calculateTimeDefference'; +import formatWorkTime from '@/utils/formatWorkTime'; + +interface NotificationCardProps { + status: 'accepted' | 'rejected'; // 공고 지원 상태 + restaurantName: string; // 음식점 이름 + startsAt: string; // 공고 시작 시간 (ISO 8601 문자열) + workHour: number; // 근무 시간 (시간 단위) + createdAt: string; // 알림 생성 시간 (ISO 8601 문자열) +} + +/* + * 사용자가 공고에 지원한 결과를 알리는 알림 카드 컴포넌트입니다. + * 상태에 따라 '승인' 혹은 '거절'로 표시되며, + * 가게 이름과 알바 시간, 생성 시간을 보여줍니다. + */ + +export default function NotificationCard({ + status, + restaurantName, + startsAt, + workHour, + createdAt, +}: NotificationCardProps) { + const formattedTime = formatWorkTime({ + startsAt, + workHour, + }); + const formattedCreatedAt = calculateTimeDifference(createdAt); + const formattedStatus = status === 'accepted' ? '승인' : '거절'; + const formattedStatusClass = + status === 'accepted' ? 'text-blue-20' : 'text-red-20'; + return ( +
+ {status === 'accepted' ? ( +
+ ) : ( +
+ )} +

+ {restaurantName} ({formattedTime}) 공고 지원이{' '} + {formattedStatus} + 되었어요. +

+

{formattedCreatedAt}

+
+ ); +} diff --git a/src/components/common/NotificationModal/NotificationModal.tsx b/src/components/common/NotificationModal/NotificationModal.tsx new file mode 100644 index 00000000..1f9508f8 --- /dev/null +++ b/src/components/common/NotificationModal/NotificationModal.tsx @@ -0,0 +1,57 @@ +import { useNavigate } from 'react-router-dom'; +import NotificationCard from './NotificationCard'; +import close from '@/assets/icons/ic_close.svg'; + +interface NotificationItem { + createdAt: string; // 생성 일시 (ISO 8601 문자열) + result: 'accepted' | 'rejected'; // 결과 상태 + read: boolean; // 읽음 여부 + shop: { + item: { + name: string; // 가게 이름 + }; + }; + notice: { + item: { + startsAt: string; // 근무 시작 시간 (ISO 8601 문자열) + workhour: number; // 근무 시간 (시간 단위) + }; + }; +} +interface NotificationModalProps { + data: NotificationItem[]; // 알림 데이터 배열 + count: number; // 알림 개수 +} + +/* + * 알림 모달 컴포넌트입니다. + * 알림 데이터들을 카드로 표시하며, 스크롤 가능합니다. + * PC/Tablet에서는 모달 형식으로, 바깥을 클릭하거나 esc를 통해 창을 닫을 수 있습니다. + * 모바일에서는 닫기 버튼으로 창을 닫을 수 있습니다. + */ + +export default function NotificationModal({ data, count }: NotificationModalProps) { + const navigate = useNavigate(); + return ( +
+
+

알림 {count}개

+ +
+
+ {data.map((data, index) => ( + + ))} +
+
+ ); +} diff --git a/src/index.css b/src/index.css index 7c5d0cb0..86ab5ca7 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,5 @@ @import 'tailwindcss'; +@import 'tailwind-scrollbar-hide/v4'; @font-face { font-family: 'SpoqaHanSansNeo-Regular'; @@ -96,5 +97,6 @@ --text-body1: 16px; --text-body2: 14px; --text-caption: 12px; + --shadow-custom: 0px 2px 8px 0px rgba(120, 116, 134, 0.25); --spacing: 0.0625rem; } diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/utils/calculateTimeDefference.ts b/src/utils/calculateTimeDefference.ts new file mode 100644 index 00000000..90901985 --- /dev/null +++ b/src/utils/calculateTimeDefference.ts @@ -0,0 +1,26 @@ +/* 주어진 날짜로부터 현재까지의 경과 시간을 사람이 읽기 좋은 문자열로 반환하는 함수 */ +export default function calculateTimeDifference(createdAt: string | number): string { + const currentDate = new Date(); + const createdDate = new Date(createdAt); + + const timeDifference = currentDate.getTime() - createdDate.getTime(); + const minutes = Math.floor(timeDifference / (1000 * 60)); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const months = Math.floor(days / 30); + const years = Math.floor(months / 12); + + if (minutes < 1) { + return '방금'; + } else if (minutes <= 59) { + return `${minutes}분 전`; + } else if (hours <= 23) { + return `${hours}시간 전`; + } else if (days <= 30) { + return `${days}일 전`; + } else if (months <= 11) { + return `${months}달 전`; + } else { + return `${years}년 전`; + } +} \ No newline at end of file diff --git a/src/utils/formatWorkTime.ts b/src/utils/formatWorkTime.ts new file mode 100644 index 00000000..92b4c5b7 --- /dev/null +++ b/src/utils/formatWorkTime.ts @@ -0,0 +1,29 @@ +interface workTime { + startsAt: string | number; // 근무 시작 시간 (ISO 8601 형식 문자열) + workHour: number; // 근무 시간 (시간 단위) +} + +/* 주어진 근무 시작 시간과 근무 시간을 기반으로 근무 시작~종료 시간을 "YYYY-MM-DD HH:mm~HH:mm" 형식의 문자열로 반환하는 함수 */ +export default function formatWorkTime({ + startsAt, + workHour, +}: workTime): string { + const date = new Date(startsAt); + + const kstDate = new Date(date.getTime() + 9 * 60 * 60 * 1000); + + /* 시각이 24시를 넘으면 다음날로 설정되어 자동으로 처리 */ + const endDate = new Date(kstDate); + endDate.setHours(endDate.getHours() + workHour); + + const year = kstDate.getFullYear(); + const month = String(kstDate.getMonth() + 1).padStart(2, '0'); + const day = String(kstDate.getDate()).padStart(2, '0'); + const startHours = String(kstDate.getHours()).padStart(2, '0'); + const startMinutes = String(kstDate.getMinutes()).padStart(2, '0'); + + const endHours = String(endDate.getHours()).padStart(2, '0'); + const endMinutes = String(endDate.getMinutes()).padStart(2, '0'); + + return `${year}-${month}-${day} ${startHours}:${startMinutes}~${endHours}:${endMinutes} `; +}