Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 5 additions & 28 deletions src/page/main-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import BottomSheet from '@widgets/main/bottom-sheet/bottom-sheet';
import { BottomSheetLocationSearch } from '@widgets/main/bottom-sheet/contents/bottom-sheet-location-search';
import { RadioContent } from '@widgets/main/bottom-sheet/contents/radio/radio-content';
import { Card } from '@widgets/main/card/card';
import { NotificationPanel } from '@widgets/main/notification/notificationPanel';
import { NotificationPopover } from '@widgets/main/notification/notification-popover';
import { useNavigate } from 'react-router-dom';
import { FloatingActionButton } from '@shared/ui/floatingActionButton';
import PlusIcon from '@shared/assets/icon/plus.svg?react';
Expand All @@ -17,19 +17,6 @@ import { formatDate } from '@shared/utils/date';
export type SortType = 'latest' | 'near';
type SheetType = 'location' | 'sort' | null;

const mockNotifications = [
{
value:
'“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.',
time: '방금 전',
},
{
value:
'“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.',
time: '10분 전',
},
];

const MainPage = () => {
const navigate = useNavigate();
const [location, setLocation] = useState('');
Expand All @@ -46,20 +33,10 @@ const MainPage = () => {
onLeftClick={() => navigate('/my')}
onRightClick={() => setIsNotificationOpen((prev) => !prev)}
/>
{isNotificationOpen && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setIsNotificationOpen(false)}
/>
<div
className="absolute right-[2.4rem] top-[5.6rem] z-50"
onClick={(e) => e.stopPropagation()}
>
<NotificationPanel notifications={mockNotifications} />
</div>
</>
)}
<NotificationPopover
isOpen={isNotificationOpen}
onClose={() => setIsNotificationOpen(false)}
/>
<div className="flex flex-col gap-[0.8rem] px-[2.4rem] pb-[0.4rem]">
<span className="text-gray-600 typo-h3 h-[2.2rem]">
오늘도 안전한 하루 되세요!
Expand Down
33 changes: 5 additions & 28 deletions src/page/my/my-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,9 @@ import BellIcon from '@shared/assets/icon/bell.svg?react';
import ArchiveIcon from '@shared/assets/icon/archive.svg?react';
import EditIcon from '@shared/assets/icon/edit.svg?react';
import { useNavigate } from 'react-router-dom';
import { NotificationPanel } from '@widgets/main/notification/notificationPanel';
import { NotificationPopover } from '@widgets/main/notification/notification-popover';
import { useState } from 'react';
import { MyButton } from '@widgets/my/my-button';

const mockNotifications = [
{
value:
'“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.',
time: '방금 전',
},
{
value:
'“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.',
time: '10분 전',
},
];
const MyPage = () => {
const navigate = useNavigate();
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
Expand All @@ -32,20 +19,10 @@ const MyPage = () => {
onLeftClick={() => navigate(-1)}
onRightClick={() => setIsNotificationOpen((prev) => !prev)}
/>
{isNotificationOpen && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setIsNotificationOpen(false)}
/>
<div
className="absolute right-[2.4rem] top-[5.6rem] z-50"
onClick={(e) => e.stopPropagation()}
>
<NotificationPanel notifications={mockNotifications} />
</div>
</>
)}
<NotificationPopover
isOpen={isNotificationOpen}
onClose={() => setIsNotificationOpen(false)}
/>
<p className="typo-h1 px-[2.4rem]">마이페이지</p>
<div className="flex items-center gap-[1.2rem] h-[11.2rem] px-[2.4rem] py-[2rem] border-b">
<img className="w-[7.2rem] h-[7.2rem] rounded-full border" />
Expand Down
33 changes: 5 additions & 28 deletions src/page/my/nickName-change.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,10 @@ import ArrowLeftIcon from '@shared/assets/icon/arrow-left.svg?react';
import BellIcon from '@shared/assets/icon/bell.svg?react';
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
import { NotificationPanel } from '@widgets/main/notification/notificationPanel';
import { NotificationPopover } from '@widgets/main/notification/notification-popover';
import Input from '@shared/ui/input';
import { Button } from '@shared/ui/button';

const mockNotifications = [
{
value:
'“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.',
time: '방금 전',
},
{
value:
'“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.',
time: '10분 전',
},
];

const NickNameChage = () => {
const navigate = useNavigate();
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
Expand All @@ -31,20 +18,10 @@ const NickNameChage = () => {
onLeftClick={() => navigate(-1)}
onRightClick={() => setIsNotificationOpen((prev) => !prev)}
/>
{isNotificationOpen && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setIsNotificationOpen(false)}
/>
<div
className="absolute right-[2.4rem] top-[5.6rem] z-50"
onClick={(e) => e.stopPropagation()}
>
<NotificationPanel notifications={mockNotifications} />
</div>
</>
)}
<NotificationPopover
isOpen={isNotificationOpen}
onClose={() => setIsNotificationOpen(false)}
/>
<p className="typo-h1 h-[6.9rem] px-[2.4rem]">닉네임 변경</p>
<div className="flex flex-col gap-[2rem] px-[2.4rem]">
<div className="flex flex-col gap-[0.8rem]">
Expand Down
30 changes: 30 additions & 0 deletions src/shared/api/domain/notifications/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { mutationOptions, queryOptions } from '@tanstack/react-query';
import { api } from '@shared/api/config/instance';
import { END_POINT } from '@shared/api/end-point';
import { NOTIFICATION_QUERY_KEY } from '@shared/api/query-key';
import type { GetNotificationsResponse } from '@shared/types/notifications/type';

const getNotifications = async (): Promise<GetNotificationsResponse> => {
return api
.get(END_POINT.NOTIFICATION.LIST)
.json<GetNotificationsResponse>();
};

const deleteNotifications = async () => {
await api.delete(END_POINT.NOTIFICATION.LIST);
};

export const NOTIFICATION_QUERY_OPTIONS = {
LIST: () =>
queryOptions({
queryKey: NOTIFICATION_QUERY_KEY.LIST(),
queryFn: getNotifications,
}),
};

export const NOTIFICATION_MUTATION_OPTIONS = {
DELETE_ALL: () =>
mutationOptions({
mutationFn: deleteNotifications,
}),
};
3 changes: 3 additions & 0 deletions src/shared/api/end-point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ export const END_POINT = {
S3: {
PRESIGNED_UPLOAD: 'api/s3/presigned-upload',
},
NOTIFICATION: {
LIST: 'api/notifications',
},
} as const;
4 changes: 4 additions & 0 deletions src/shared/api/query-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ export const FEED_QUERY_KEY = {
LIST: () => ['feeds'] as const,
DETAIL: (feedId: number) => ['feed', feedId] as const,
};

export const NOTIFICATION_QUERY_KEY = {
LIST: () => ['notifications'] as const,
};
5 changes: 5 additions & 0 deletions src/shared/types/notifications/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { paths } from '@shared/types/schema';

export type GetNotificationsResponse =
paths['/api/notifications']['get']['responses']['200']['content']['*/*'];

22 changes: 22 additions & 0 deletions src/shared/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,25 @@ export const formatDate = (date: string) =>
minute: '2-digit',
hour12: true,
});

export const formatRelativeTime = (date: string) => {
const diffMs = new Date(date).getTime() - Date.now();
const rtf = new Intl.RelativeTimeFormat('ko', { numeric: 'auto' });
const diffMinutes = Math.round(diffMs / (1000 * 60));

if (Math.abs(diffMinutes) < 60) {
return rtf.format(diffMinutes, 'minute');
}

const diffHours = Math.round(diffMinutes / 60);
if (Math.abs(diffHours) < 24) {
return rtf.format(diffHours, 'hour');
}

const diffDays = Math.round(diffHours / 24);
if (Math.abs(diffDays) < 7) {
return rtf.format(diffDays, 'day');
}

return formatDate(date);
};
1 change: 1 addition & 0 deletions src/widgets/main/notification/notification-card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ChevronRightIcon from '@shared/assets/icon/chevron-right.svg?react';
export interface NotificationCardProps {
id: number;
value: string;
time: string;
}
Expand Down
60 changes: 60 additions & 0 deletions src/widgets/main/notification/notification-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
NOTIFICATION_MUTATION_OPTIONS,
NOTIFICATION_QUERY_OPTIONS,
} from '@shared/api/domain/notifications/query';
import { formatRelativeTime } from '@shared/utils/date';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { NotificationPanel } from '@widgets/main/notification/notificationPanel';

interface NotificationPopoverProps {
isOpen: boolean;
onClose: () => void;
}

export function NotificationPopover({
isOpen,
onClose,
}: NotificationPopoverProps) {
const queryClient = useQueryClient();
const { data: notifications = [] } = useQuery({
...NOTIFICATION_QUERY_OPTIONS.LIST(),
enabled: isOpen,
});
const { mutate: deleteAllNotifications, isPending } = useMutation({
...NOTIFICATION_MUTATION_OPTIONS.DELETE_ALL(),
onSuccess: () => {
queryClient.setQueryData(
NOTIFICATION_QUERY_OPTIONS.LIST().queryKey,
[],
);
},
});

if (!isOpen) {
return null;
}

const mappedNotifications = notifications.map((notification) => ({
id: notification.notificationId ?? 0,
value: notification.message ?? '',
time: notification.createdAt
? formatRelativeTime(notification.createdAt)
: '',
}));

return (
<>
<div className="fixed inset-0 z-40" onClick={onClose} />
<div
className="absolute right-[2.4rem] top-[5.6rem] z-50"
onClick={(e) => e.stopPropagation()}
>
<NotificationPanel
notifications={mappedNotifications}
isDeleting={isPending}
onDeleteAll={() => deleteAllNotifications()}
/>
</div>
</>
);
}
31 changes: 26 additions & 5 deletions src/widgets/main/notification/notificationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,44 @@ import {

interface NotificationPanelProps {
notifications: NotificationCardProps[];
isDeleting?: boolean;
onDeleteAll?: () => void;
}

export function NotificationPanel({ notifications }: NotificationPanelProps) {
export function NotificationPanel({
notifications,
isDeleting = false,
onDeleteAll,
}: NotificationPanelProps) {
return (
<div className="w-[30rem] px-[1.2rem] h-[30.6rem] bg-white border border-gray-200 rounded-[12px]">
<div className="flex h-[5.4rem] justify-between">
<span className="w-[6.4rem] h-[2.2rem] pt-[1.2rem] typo-h3">
알림 내역
</span>
<button className="w-[4.1rem] h-[1.8rem] pt-[1.4rem] typo-cation text-gray-300">
<button
className="w-[4.1rem] h-[1.8rem] pt-[1.4rem] typo-cation text-gray-300 disabled:opacity-50"
onClick={onDeleteAll}
disabled={notifications.length === 0 || isDeleting}
>
전체삭제
</button>
</div>
<div className="flex flex-col gap-[1.2rem]">
{notifications.map((item, index) => (
<NotificationCard key={index} value={item.value} time={item.time} />
))}
{notifications.length === 0 ? (
<div className="flex h-[22rem] items-center justify-center typo-body2 text-gray-300">
도착한 알림이 없어요.
</div>
) : (
notifications.map((item) => (
<NotificationCard
key={item.id}
id={item.id}
value={item.value}
time={item.time}
/>
))
)}
</div>
</div>
);
Expand Down
Loading