-
Notifications
You must be signed in to change notification settings - Fork 4
[✨feat] 알림 모달 생성 #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
[✨feat] 알림 모달 생성 #32
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
ac93e52
♻️ refactor: UserType 적용 #20 #6
gummmmmy0v0 aeba6a8
✨feat: 알림 모달 공통 컴포넌트 생성 #20
gummmmmy0v0 c84fa1d
✨feat: 알림 모달 컴포넌트 생성 #20
gummmmmy0v0 3b19fb1
🍱 asset: 승인/거절 뱃지 아이콘 이미지 추가 #20
gummmmmy0v0 9b0d0f5
♻️ refactor: 아이콘 2개 -> 동일 1개로 변경, 지원 결과 뱃지 함수 추가 #20
gummmmmy0v0 0fc08a3
♻️ refactor: getTime 함수 DATE/TIME 분리, 알림 메시지 적용 #20
gummmmmy0v0 092f626
♻️ refactor: 지원 공고 기간 DATE_RANGE 추가 #20
gummmmmy0v0 c4c50be
✨feat: timeAgo 함수 생성 및 적용 #20
gummmmmy0v0 3472bdc
♻️ refactor: 알림 스타일 변경 #20
gummmmmy0v0 da53541
🔧chore: 오류 수정 중 패키지 파일 원상복구 #20
gummmmmy0v0 e713228
🔧chore: 패키지 파일 원상복구
gummmmmy0v0 ccc950e
♻️ refactor: Modal 폴더 내 index.ts 디폴트 내보내기 수정 #20
gummmmmy0v0 80d95e7
♻️ refactor: {' '} 띄어쓰기 부분 수정 #20
gummmmmy0v0 c7e52e2
♻️ refactor: getTime SNAKE_CASE -> camelCase 변경 및 관련 문서 수정 #20
gummmmmy0v0 4290ecf
♻️ refactor: alert 중첩 구조 분해 할당 #20
gummmmmy0v0 70acdc7
♻️ refactor: SORTED_ALERTS 최신순 수정, 적용 #20
gummmmmy0v0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| export { Notification } from '@/components/ui/modal/notification'; | ||
| export { Table } from '@/components/ui/table'; | ||
| export { Dropdown } from './dropdown'; | ||
| export { Icon } from './icon'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { Notification } from '@/components/ui/modal/notification'; |
140 changes: 140 additions & 0 deletions
140
src/components/ui/modal/notification/Notification.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { Meta, StoryFn } from '@storybook/nextjs'; | ||
| import Notification, { Alert } from './Notification'; | ||
|
|
||
| /* eslint-disable no-console */ | ||
|
|
||
| const meta: Meta<typeof Notification> = { | ||
| title: 'Components/Notification', | ||
| component: Notification, | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| const Template: StoryFn<typeof Notification> = args => <Notification {...args} />; | ||
|
|
||
| export const Default = Template.bind({}); | ||
| Default.args = { | ||
| alerts: [ | ||
| { | ||
| id: '1', | ||
| read: false, | ||
| createdAt: '2025-10-03T14:14:00Z', | ||
| result: 'accepted', | ||
| shop: { | ||
| item: { | ||
| id: 'shop1', | ||
| name: '맛집 A', | ||
| category: '음식점', | ||
| address1: '서울 강남구', | ||
| address2: '역삼동 123-45', | ||
| description: '맛있는 음식점', | ||
| imageUrl: 'https://via.placeholder.com/150', | ||
| originalHourlyPay: 15000, | ||
| }, | ||
| href: '/shop/shop1', | ||
| }, | ||
| notice: { | ||
| item: { | ||
| id: 'notice1', | ||
| hourlyPay: 15000, | ||
| description: '맛집 알바', | ||
| startsAt: '2025-10-01T09:00:00Z', | ||
| workhour: 8, | ||
| closed: false, | ||
| }, | ||
| href: '/notice/notice1', | ||
| }, | ||
| }, | ||
| { | ||
| id: '2', | ||
| read: false, | ||
| createdAt: '2025-10-02T10:50:00Z', | ||
| result: 'rejected', | ||
| shop: { | ||
| item: { | ||
| id: 'shop2', | ||
| name: '카페 B', | ||
| category: '카페', | ||
| address1: '서울 서초구', | ||
| address2: '서초동 678-90', | ||
| description: '커피 맛집', | ||
| imageUrl: 'https://via.placeholder.com/150', | ||
| originalHourlyPay: 12000, | ||
| }, | ||
| href: '/shop/shop2', | ||
| }, | ||
| notice: { | ||
| item: { | ||
| id: 'notice2', | ||
| hourlyPay: 12000, | ||
| description: '카페 알바', | ||
| startsAt: '2025-10-02T10:00:00Z', | ||
| workhour: 6, | ||
| closed: false, | ||
| }, | ||
| href: '/notice/notice2', | ||
| }, | ||
| }, | ||
| { | ||
| id: '3', | ||
| read: true, | ||
| createdAt: '2025-10-02T08:20:00Z', | ||
| result: 'accepted', | ||
| shop: { | ||
| item: { | ||
| id: 'shop3', | ||
| name: '도서관 C', | ||
| category: '도서관', | ||
| address1: '서울 마포구', | ||
| address2: '상암동 456-78', | ||
| description: '조용한 도서관', | ||
| imageUrl: 'https://via.placeholder.com/150', | ||
| originalHourlyPay: 10000, | ||
| }, | ||
| href: '/shop/shop3', | ||
| }, | ||
| notice: { | ||
| item: { | ||
| id: 'notice3', | ||
| hourlyPay: 10000, | ||
| description: '도서관 알바', | ||
| startsAt: '2025-10-03T11:00:00Z', | ||
| workhour: 4, | ||
| closed: false, | ||
| }, | ||
| href: '/notice/notice3', | ||
| }, | ||
| }, | ||
| { | ||
| id: '4', | ||
| read: true, | ||
| createdAt: '2025-10-01T11:20:00Z', | ||
| result: 'rejected', | ||
| shop: { | ||
| item: { | ||
| id: 'shop4', | ||
| name: '헬스장 D', | ||
| category: '헬스장', | ||
| address1: '서울 송파구', | ||
| address2: '잠실동 789-01', | ||
| description: '피트니스 센터', | ||
| imageUrl: 'https://via.placeholder.com/150', | ||
| originalHourlyPay: 18000, | ||
| }, | ||
| href: '/shop/shop4', | ||
| }, | ||
| notice: { | ||
| item: { | ||
| id: 'notice4', | ||
| hourlyPay: 18000, | ||
| description: '헬스장 알바', | ||
| startsAt: '2025-10-04T09:00:00Z', | ||
| workhour: 5, | ||
| closed: false, | ||
| }, | ||
| href: '/notice/notice4', | ||
| }, | ||
| }, | ||
| ] as Alert[], | ||
| onRead: (id: string) => console.log('Read notification', id), | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import Icon from '@/components/ui/icon/icon'; | ||
| import { Notice } from '@/types/notice'; | ||
| import { Shop } from '@/types/shop'; | ||
| import { useState } from 'react'; | ||
| import NotificationMessage from './NotificationMessage'; | ||
|
|
||
| export interface Alert { | ||
| id: string; | ||
| createdAt: string; | ||
| result: 'accepted' | 'rejected'; | ||
| read: boolean; | ||
| shop: { item: Shop; href?: string }; | ||
| notice: { item: Notice; href?: string }; | ||
| } | ||
|
|
||
| interface NotificationProps { | ||
| alerts: Alert[]; | ||
| onRead: (id: string) => void; | ||
| isOpen?: boolean; | ||
| onClose?: () => void; | ||
| } | ||
|
|
||
| export default function Notification({ alerts, onRead }: NotificationProps) { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const notificationCount = alerts.filter(alert => !alert.read).length; | ||
| const SORTED_ALERTS = [...alerts].sort( | ||
| (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() | ||
| ); | ||
|
|
||
| return ( | ||
| <> | ||
| <div className='relative flex justify-end'> | ||
| <button | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className={`${isOpen ? 'hidden' : 'block'} relative md:block`} | ||
| > | ||
| <Icon iconName='notificationOn' iconSize='sm' ariaLabel='알림' /> | ||
| </button> | ||
| </div> | ||
| {isOpen && ( | ||
| <div className='flex min-h-screen flex-col gap-4 bg-red-100 px-5 py-10'> | ||
| <div className='flex justify-between'> | ||
| <div className='text-[20px] font-bold'>알림 {notificationCount}개</div> | ||
| <div> | ||
| <button onClick={() => setIsOpen(false)}> | ||
| <Icon iconName='close' iconSize='lg' ariaLabel='닫기' /> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <div></div> | ||
| {SORTED_ALERTS.length === 0 ? ( | ||
| <div className='flex flex-1 items-center justify-center'> | ||
| <p>알림이 없습니다.</p> | ||
| </div> | ||
| ) : ( | ||
| <div className='flex flex-col items-center gap-4'> | ||
| {SORTED_ALERTS.map(alert => ( | ||
| <NotificationMessage key={alert.id} alert={alert} onRead={onRead} /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| )} | ||
| </> | ||
| ); | ||
| } |
61 changes: 61 additions & 0 deletions
61
src/components/ui/modal/notification/NotificationMessage.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { getTime } from '@/lib/utils/getTime'; | ||
| import { timeAgo } from '@/lib/utils/timeAgo'; | ||
| import { clsx } from 'clsx'; | ||
| import { Alert } from './Notification'; | ||
| import ResultBadge from './ResultBadge'; | ||
|
|
||
| export default function NotificationMessage({ | ||
| alert, | ||
| onRead, | ||
| }: { | ||
| alert: Alert; | ||
| onRead: (id: string) => void; | ||
| }) { | ||
| const { | ||
| id, | ||
| result, | ||
| read, | ||
| createdAt, | ||
| shop: { | ||
| item: { name: shopName }, | ||
| }, | ||
| notice: { | ||
| item: { startsAt, workhour }, | ||
| }, | ||
| } = alert; | ||
|
|
||
| const RESULT_TEXT = result === 'accepted' ? '승인' : '거절'; | ||
| const DATE_RANGE = getTime(startsAt, workhour); | ||
| const NOTIFICATION_MESSAGE_CONTAINER = clsx( | ||
| 'w-full gap-2 break-words rounded border border-gray-200 bg-white px-3 py-4' | ||
| ); | ||
|
|
||
| return ( | ||
| <div className={NOTIFICATION_MESSAGE_CONTAINER}> | ||
| <button onClick={() => onRead(id)} className='w-full text-left'> | ||
| <div className='flex flex-col gap-2'> | ||
| <ResultBadge result={result} /> | ||
| <p | ||
| className={clsx('text-sm', { | ||
| 'text-gray-400': read, | ||
| })} | ||
| > | ||
| {`${shopName} (${DATE_RANGE.date} ${DATE_RANGE.startTime} ~ | ||
| ${DATE_RANGE.endTime}) 공고 지원이 `} | ||
| <span | ||
| className={clsx({ | ||
| 'text-gray-500': read, | ||
| 'text-blue-200': !read && result === 'accepted', | ||
| 'text-red-400': !read && result === 'rejected', | ||
| })} | ||
| > | ||
| {RESULT_TEXT} | ||
| </span> | ||
| 되었어요. | ||
| </p> | ||
| <span className='text-xs text-gray-400'>{timeAgo(createdAt)}</span> | ||
| </div> | ||
| </button> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import Icon from '@/components/ui/icon/icon'; | ||
|
|
||
| export interface ResultBadgeProps { | ||
| result: 'accepted' | 'rejected'; | ||
| } | ||
| const ICON_COLORS: Record<ResultBadgeProps['result'], string> = { | ||
| accepted: 'bg-blue-200', | ||
| rejected: 'bg-red-400', | ||
| }; | ||
|
|
||
| export default function ResultBadge({ result }: ResultBadgeProps) { | ||
| return ( | ||
| <Icon | ||
| iconName='resultBadge' | ||
| className={`${ICON_COLORS[result]} h-[5px] w-[5px]`} | ||
| ariaLabel={result === 'accepted' ? '승인 상태' : '거절 상태'} | ||
| /> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default as Notification } from '@/components/ui/modal/notification/Notification'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.