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
99 changes: 99 additions & 0 deletions public/assets/icon-bell.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
@tailwind utilities;

:root {

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
Expand Down
53 changes: 53 additions & 0 deletions src/app/mobile/admin/notification/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import MobileLayout from '@/components/mobile/layout';
import NotificationItem from '@/components/mobile/NotificationItem';
import Header from '@/components/mobile/Header';
import { elapsedTime } from '@/utils/elapsedTime';
import { NotificationProps } from '@/types/notificationType';

const NotificationDetail: AdminNotificationType[] = [
{
notificationId: 0,
message: '메시지 1입니다',
link: '/desktop/login',
isRead: false,
status: 'ADMIN_RENTAL_APPLY',
createdAt: '2025-02-16T08:44:45.476Z',
},
{
notificationId: 1,
message: '메시지 2입니다',
link: '/mobile',
isRead: true,
status: 'ADMIN_RENTAL_CANCEL',
createdAt: '2025-02-16T06:44:45.476Z',
},
{
notificationId: 2,
message:
'메시지 두 줄 테스트입니다 두 줄 테스트 두 줄 테스트 두 줄 테스트 두 줄 테스트',
link: '/mobile',
isRead: true,
status: 'ADMIN_RETURN_APPLY',
createdAt: '2025-02-12T05:44:45.476Z',
},
];

type AdminNotificationType = NotificationProps;

export default function Notification() {
return (
<MobileLayout>
<Header title="관리자 알림" />
{NotificationDetail.map((item) => (
<NotificationItem
key={item.notificationId}
message={item.message}
link={item.link}
isRead={item.isRead}
status={item.status}
createdAt={elapsedTime(item.createdAt)}
/>
))}
</MobileLayout>
);
}
53 changes: 53 additions & 0 deletions src/app/mobile/notification/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import MobileLayout from '@/components/mobile/layout';
import NotificationItem from '@/components/mobile/NotificationItem';
import Header from '@/components/mobile/Header';
import { elapsedTime } from '@/utils/elapsedTime';
import { NotificationProps } from '@/types/notificationType';

const NotificationDetail: UserNotificationType[] = [
{
notificationId: 0,
message: '메시지 1입니다',
link: '/desktop/login',
isRead: false,
status: 'USER_RENTAL_APPLY',
createdAt: '2025-02-16T08:44:45.476Z',
},
{
notificationId: 1,
message: '메시지 2입니다',
link: '/mobile',
isRead: true,
status: 'USER_RENTAL_REJECTED',
createdAt: '2025-02-16T06:44:45.476Z',
},
{
notificationId: 2,
message:
'메시지 두 줄 테스트입니다 두 줄 테스트 두 줄 테스트 두 줄 테스트 두 줄 테스트',
link: '/mobile',
isRead: true,
status: 'USER_RETURN_APPLY',
createdAt: '2025-02-12T05:44:45.476Z',
},
];

type UserNotificationType = NotificationProps;

export default function Notification() {
return (
<MobileLayout>
<Header title="알림" />
{NotificationDetail.map((item) => (
<NotificationItem
key={item.notificationId}
message={item.message}
link={item.link}
isRead={item.isRead}
status={item.status}
createdAt={elapsedTime(item.createdAt)}
/>
))}
</MobileLayout>
);
}
4 changes: 2 additions & 2 deletions src/components/mobile/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ export default function Header({ title, menu = false }: HeaderProps) {
const router = useRouter();

return (
<section className="flex h-8 w-full items-center justify-between px-4 py-1.5">
<section className="h-8.5 flex w-full items-center justify-between px-4 py-1.5">
<button
className="h-6 w-6 items-center justify-center"
type="button"
onClick={() => router.back()}
>
<IconArrow />
</button>
<div className="font-medium text-black-primary">{title}</div>
<div className="font-medium leading-6 text-black-primary">{title}</div>
{menu ? (
<button
className="h-6 w-6 items-center justify-center"
Expand Down
58 changes: 58 additions & 0 deletions src/components/mobile/NotificationItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import Link from 'next/link';
import { cn } from '@/lib/utils';
import {
AdminNotificationText,
AdminNotificationTypes,
UserNotificationText,
} from '@/constants/notificationStatus';
import { NotificationProps } from '@/types/notificationType';
import IconBell from 'public/assets/icon-bell.svg';

export default function NotificationItem({
message,
link,
isRead,
status,
createdAt,
}: NotificationProps) {
const isAdminStatus = (
notificationStatus: string,
): notificationStatus is AdminNotificationTypes =>
notificationStatus.startsWith('ADMIN');

return (
<Link className="w-full" href={link}>
<section
className={cn(
'flex w-full gap-2.5 p-5',
isRead ? '' : 'bg-main-tertiary',
)}
>
<div className="flex h-4 w-4">
<IconBell />
</div>

<section className="flex w-full flex-col items-start gap-2.5 text-sm font-medium">
<section className="flex w-full justify-between text-xs">
<div
className={cn(
'font-medium',
/REJECTED|CANCEL/.test(status)
? 'text-return-red'
: 'text-return-blue',
)}
>
{isAdminStatus(status)
? AdminNotificationText[status]
: UserNotificationText[status]}
</div>
<div className="font-medium text-gray-secondary">{createdAt}</div>
</section>
{message}
</section>
</section>
</Link>
);
}
38 changes: 38 additions & 0 deletions src/constants/notificationStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const ADMIN_NOTIFICATION_STATUS = {
ADMIN_RENTAL_APPLY: 'ADMIN_RENTAL_APPLY',
ADMIN_RENTAL_CANCEL: 'ADMIN_RENTAL_CANCEL',
ADMIN_RETURN_APPLY: 'ADMIN_RETURN_APPLY',
ADMIN_RETURN_CANCEL: 'ADMIN_RETURN_CANCEL',
} as const;

export const USER_NOTIFICATION_STATUS = {
USER_RENTAL_APPLY: 'USER_RENTAL_APPLY',
USER_RENTAL_APPROVED: 'USER_RENTAL_APPROVED',
USER_RENTAL_REJECTED: 'USER_RENTAL_REJECTED',
USER_RETURN_APPLY: 'USER_RETURN_APPLY',
USER_RETURN_COMPLETED: 'USER_RETURN_COMPLETED',
} as const;

export type AdminNotificationStatus =
(typeof ADMIN_NOTIFICATION_STATUS)[keyof typeof ADMIN_NOTIFICATION_STATUS];

export type UserNotificationStatus =
(typeof USER_NOTIFICATION_STATUS)[keyof typeof USER_NOTIFICATION_STATUS];

export const AdminNotificationText: Record<AdminNotificationStatus, string> = {
[ADMIN_NOTIFICATION_STATUS.ADMIN_RENTAL_APPLY]: '대여 신청',
[ADMIN_NOTIFICATION_STATUS.ADMIN_RENTAL_CANCEL]: '대여 취소',
[ADMIN_NOTIFICATION_STATUS.ADMIN_RETURN_APPLY]: '반납 신청',
[ADMIN_NOTIFICATION_STATUS.ADMIN_RETURN_CANCEL]: '반납 취소',
} as const;

export const UserNotificationText: Record<UserNotificationStatus, string> = {
[USER_NOTIFICATION_STATUS.USER_RENTAL_APPLY]: '대여 신청',
[USER_NOTIFICATION_STATUS.USER_RENTAL_APPROVED]: '대여 승인',
[USER_NOTIFICATION_STATUS.USER_RENTAL_REJECTED]: '대여 반려',
[USER_NOTIFICATION_STATUS.USER_RETURN_APPLY]: '반납 신청',
[USER_NOTIFICATION_STATUS.USER_RETURN_COMPLETED]: '반납 완료',
} as const;

export type AdminNotificationTypes = keyof typeof AdminNotificationText;
export type UserNotificationTypes = keyof typeof UserNotificationText;
Comment on lines +37 to +38
Copy link
Member

@hyeonjin6530 hyeonjin6530 Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 AdminNotificationStatus 타입이 AdminNotificationText의 키값과 동일해서 아래와 같이 적어도 되지 않을까용? (user도!)

export type AdminNotificationTypes = AdminNotificationStatus;
export type UserNotificationTypes = UserNotificationStatus;

Copy link
Member Author

@sinji2102 sinji2102 Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

허거덩 가독성 측면에서는 이렇게 수정하는 게 더 좋을 수도 있겠네요!!

다만 제가 걱정하는 부분은 수민 오빠가 물론 잘 처리해 주었겠지만... 만약 새로운 상태가 추가된다면

export type AdminNotificationTypes = AdminNotificationStatus;
export type UserNotificationTypes = UserNotificationStatus;

방식으로 처리한다면 존재하지 않는 키가 타입으로 포함될 위험이 있다고 생각했어요! 때문에 keyof typeof를 사용해서 실제로 존재하는 키만 정의하고 혹여나 추가되거나 삭제될 때 자동으로 타입을 맞췄으면... 해서 저렇게 정의했습니당!

그리구 AdminNotificationText는 텍스트 매핑에 사용되는 키 값들의 타입이구 AdminNotificationStatus는 알림의 상태값 자체라서 지금대로 유지하는 게 개인적으로 더 명시적이라구 생각해요!

쪼금 뜨거운 감자...... 가 될 수 잇는 여지가 잇긴 하네용... 생각해보니까 굳이? 인 거 같기도 하고... 넘 어렵따 좋은 의견 있으면 남겨주세요!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그렇군용😮

저는 상태가 정해져있고, AdminNotificationStatus 타입을 기반으로 AdminNotificationText가 만들어 졌기 때문에 keyof typeof 연산을 추가로 하지 않아도 되지 않을까하는 생각이었답니다 히히

근데 새로운 상태가 추가 될 수 있고 그에 따른 위험이 생길 수 있다는 점을 생각하면 기존의 코드대로 진행하는 것이 좋을 거 같습니다!!

신지 완전 야무진 머쨍이 개발자😎👍

13 changes: 13 additions & 0 deletions src/types/notificationType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
AdminNotificationStatus,
UserNotificationStatus,
} from '@/constants/notificationStatus';

export interface NotificationProps {
notificationId?: number;
message: string;
link: string;
isRead: boolean;
status: AdminNotificationStatus | UserNotificationStatus;
createdAt: string;
}
18 changes: 18 additions & 0 deletions src/utils/elapsedTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const elapsedTime = (date: string): string => {
const start = new Date(date);
const end = new Date();

const seconds = Math.floor((end.getTime() - start.getTime()) / 1000);
if (seconds < 60) return '방금 전';

const minutes = seconds / 60;
if (minutes < 60) return `${Math.floor(minutes)}분 전`;

const hours = minutes / 60;
if (hours < 24) return `${Math.floor(hours)}시간 전`;

const days = hours / 24;
if (days < 7) return `${Math.floor(days)}일 전`;

return start.toLocaleDateString(); // 날짜를 'yyyy-MM-dd' 형식으로 반환
};
6 changes: 3 additions & 3 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export default {
'white-primary': '#FAFAFA',
'return-red': '#CC2E2E',
'return-blue': '#5294FF',
'main-primary': '#2E5ACC',
'main-secondary': '#899FD8',
'main-tertiary': '#C0C9E0',
'main-primary': '#899FD8',
'main-secondary': '#C0C9E0',
'main-tertiary': '#ECF4FF',
'gray-primary': '#37393C',
'gray-secondary': '#898C8E',
'gray-tertiary': '#F2F1F1',
Expand Down
Loading