diff --git a/app/assets/icon/icon-bell.svg b/app/assets/icon/icon-bell.svg
new file mode 100644
index 00000000..f4140fd8
--- /dev/null
+++ b/app/assets/icon/icon-bell.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/assets/icon/icon-notification-matching-gray.svg b/app/assets/icon/icon-notification-matching-gray.svg
new file mode 100644
index 00000000..c4c2ac5e
--- /dev/null
+++ b/app/assets/icon/icon-notification-matching-gray.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/assets/icon/icon-notification-matching.svg b/app/assets/icon/icon-notification-matching.svg
new file mode 100644
index 00000000..acad5896
--- /dev/null
+++ b/app/assets/icon/icon-notification-matching.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/assets/icon/icon-notification-proposal-gray.svg b/app/assets/icon/icon-notification-proposal-gray.svg
new file mode 100644
index 00000000..ba8bc93b
--- /dev/null
+++ b/app/assets/icon/icon-notification-proposal-gray.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/assets/icon/icon-notification-proposal.svg b/app/assets/icon/icon-notification-proposal.svg
new file mode 100644
index 00000000..a0346807
--- /dev/null
+++ b/app/assets/icon/icon-notification-proposal.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/assets/icon/red-dot.svg b/app/assets/icon/red-dot.svg
new file mode 100644
index 00000000..916cd55e
--- /dev/null
+++ b/app/assets/icon/red-dot.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/components/common/NavigateHeader.tsx b/app/components/common/NavigateHeader.tsx
index 68828b87..a9f70db8 100644
--- a/app/components/common/NavigateHeader.tsx
+++ b/app/components/common/NavigateHeader.tsx
@@ -41,9 +41,7 @@ export default function NavigationHeader({
-
diff --git a/app/components/layout/RealmatchHeader.tsx b/app/components/layout/RealmatchHeader.tsx
index 5e0e37ad..45c72728 100644
--- a/app/components/layout/RealmatchHeader.tsx
+++ b/app/components/layout/RealmatchHeader.tsx
@@ -1,22 +1,25 @@
import { useNavigate } from "react-router";
import RealMatchLogo from "../../assets/logo/realmatch-logo-line.png"
+import bellIcon from "../../assets/icon/icon-bell.svg";
+import redDot from "../../assets/icon/red-dot.svg";
type RealMatchHeaderProps = {
/** 뒤로가기 버튼 노출 여부 */
title?: string;
showBack?: boolean;
onBack?: () => void;
+ hasNotification?: boolean;
};
export default function RealMatchHeader({
showBack = true,
onBack,
+ hasNotification = true,
}: RealMatchHeaderProps) {
const navigate = useNavigate();
const handleBack = () => {
if (onBack) return onBack();
-
// 브라우저 히스토리 뒤로가기
navigate(-1);
};
@@ -53,18 +56,42 @@ export default function RealMatchHeader({
) : null}
- {/* Center: Logo + Text (정중앙 고정) */}
-
+ {/* Center: Logo + Text */}
+
+
+
+
+
+
+
);
diff --git a/app/routes/_main.tsx b/app/routes/_main.tsx
index 480140b4..453c58b2 100644
--- a/app/routes/_main.tsx
+++ b/app/routes/_main.tsx
@@ -3,6 +3,8 @@ import { useState, useEffect, useRef } from "react";
import BottomTab from "../components/layout/BottomTab";
import { LayoutContext } from "./layout-context";
import Logo from "../assets/logo/realmatch-logo-line.png";
+import bellIcon from "../assets/icon/icon-bell.svg";
+import redDot from "../assets/icon/red-dot.svg";
import { tokenStorage } from "../lib/token";
export default function MainLayout() {
@@ -61,7 +63,25 @@ export default function MainLayout() {
>
-
+
+
+
)}
diff --git a/app/routes/notification/components/NotificationItem.tsx b/app/routes/notification/components/NotificationItem.tsx
new file mode 100644
index 00000000..b52772d2
--- /dev/null
+++ b/app/routes/notification/components/NotificationItem.tsx
@@ -0,0 +1,59 @@
+// components/common/NotificationItem.tsx
+import proposalIcon from "../../../assets/icon/icon-notification-proposal.svg";
+import proposalGrayIcon from "../../../assets/icon/icon-notification-proposal-gray.svg";
+import matchingIcon from "../../../assets/icon/icon-notification-matching.svg";
+import matchingGrayIcon from "../../../assets/icon/icon-notification-matching-gray.svg";
+import { type NotificationItem as NotificationType } from "../api/notification";
+
+interface Props {
+ item: NotificationType;
+ onClick: (id: string, isRead: boolean) => void;
+}
+
+export default function NotificationItem({ item, onClick }: Props) {
+ const getIcon = () => {
+ const isMatching = item.category === "MATCHING" || item.iconType === "MATCHING";
+
+ if (item.isRead) {
+ // 읽은 상태
+ return isMatching ? matchingGrayIcon : proposalGrayIcon;
+ }
+
+ // 안 읽은 상태
+ return isMatching ? matchingIcon : proposalIcon;
+ };
+
+ // 시간 포맷 로직
+ const formatTime = (dateString: string) => {
+ const date = new Date(dateString);
+ const ampm = date.getHours() >= 12 ? '오후' : '오전';
+ let hours = date.getHours() % 12;
+ hours = hours ? hours : 12;
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return `${ampm} ${hours}시 ${minutes}분`;
+ };
+
+ return (
+ onClick(item.id, item.isRead)}
+ className={`px-4 py-4 flex gap-3 cursor-pointer transition-colors active:bg-gray-50 ${
+ !item.isRead ? "bg-[#F5F6FF]" : "bg-[#F5F6FF]"
+ }`}
+ >
+
+
})
+ {!item.isRead && (
+
+ )}
+
+
+
+ {item.body}
+
+
+ {formatTime(item.createdAt)}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/notification/notification-content.tsx b/app/routes/notification/notification-content.tsx
index 15f64258..4c014a1a 100644
--- a/app/routes/notification/notification-content.tsx
+++ b/app/routes/notification/notification-content.tsx
@@ -1,32 +1,27 @@
import { useState, useEffect } from "react";
-import { fetchNotifications, readNotification, type NotificationItem } from "./api/notification";
+import { fetchNotifications, readNotification, readAllNotifications, type NotificationItem as NotificationType } from "./api/notification";
import { useHideHeader } from "../../hooks/useHideHeader";
import NavigationHeader from "../../components/common/NavigateHeader";
import emptyImg from "./../../assets/empty.png";
import { useNavigate } from "react-router-dom";
+import NotificationItem from "./components/NotificationItem";
-function TabButton({ label, active, onClick, count }: { label: string; active: boolean; onClick: () => void; count?: number }) {
+function TabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; count?: number }) {
return (
);
}
export default function NotificationContent() {
- const [notifications, setNotifications] = useState([]);
+ const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<"ALL" | "PROPOSAL" | "MATCHING">("ALL");
@@ -53,19 +48,19 @@ export default function NotificationContent() {
fetchNotificationsData();
}, [activeTab]);
- /*const handleReadAll = async () => {
+ // 전체 읽기 핸들러
+ const handleReadAll = async () => {
+ if (unreadCount === 0) return;
try {
const data = await readAllNotifications();
if (data.isSuccess) {
- setNotifications((prev) =>
- prev.map((n) => ({ ...n, isRead: true }))
- );
+ setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })));
setUnreadCount(0);
}
} catch (error) {
console.error("전체 읽기 처리 실패:", error);
}
- };*/
+ };
const handleReadNotification = async (id: string, isRead: boolean) => {
if (isRead) return;
@@ -85,16 +80,20 @@ export default function NotificationContent() {
}
};
+ const groupedNotifications = notifications.reduce((acc: { [key: string]: NotificationType[] }, item) => {
+ const date = item.createdAt.split('T')[0];
+ if (!acc[date]) acc[date] = [];
+ acc[date].push(item);
+ return acc;
+ }, {});
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- return date.toLocaleDateString('ko-KR', {
- year: '2-digit',
- month: '2-digit',
- day: '2-digit'
- }).replace(/ /g, '').slice(0, -1);
- };
+ const sortedDates = Object.keys(groupedNotifications).sort((a, b) => b.localeCompare(a));
+ const formatGroupDate = (dateStr: string) => {
+ const date = new Date(dateStr);
+ const days = ['일', '월', '화', '수', '목', '금', '토'];
+ return `${date.getFullYear().toString().slice(2)}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getDate().toString().padStart(2, '0')} (${days[date.getDay()]})`;
+ };
return (
@@ -107,7 +106,6 @@ export default function NotificationContent() {
label="전체"
active={activeTab === "ALL"}
onClick={() => setActiveTab("ALL")}
- count={activeTab === "ALL" ? unreadCount : undefined}
/>
-
+
{loading ? (
-
로딩 중...
- ) : notifications.length > 0 ? (
+
로딩 중...
+ ) : sortedDates.length > 0 ? (
- {notifications.map((item) => (
-
handleReadNotification(item.id, item.isRead)}
- className={`p-4 border-b border-gray-50 flex flex-col gap-1 transition-colors active:bg-gray-50 ${!item.isRead ? "bg-blue-50/50" : "bg-white"
- }`}
- >
-
-
- {formatDate(item.createdAt)}
+ {sortedDates.map((date, index) => (
+
+
+
+ {formatGroupDate(date)}
- {!item.isRead && (
-
+ {/* 첫 번째 날짜 그룹(최신)에만 '전체 읽기' 노출 */}
+ {index === 0 && (
+
)}
-
- {item.body}
-
+ {groupedNotifications[date].map((item) => (
+
+ ))}
))}