From 8f1ff88bd4f8cb83a18bb39667c16f2fd7e6ade5 Mon Sep 17 00:00:00 2001 From: jeschun Date: Tue, 21 Oct 2025 00:34:31 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0,=20notificationOn=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/header/nav.tsx | 149 +++++++++++++++++---------- src/pages/my-profile/index.tsx | 2 +- 2 files changed, 96 insertions(+), 55 deletions(-) diff --git a/src/components/layout/header/nav.tsx b/src/components/layout/header/nav.tsx index d67102d..597d7ad 100644 --- a/src/components/layout/header/nav.tsx +++ b/src/components/layout/header/nav.tsx @@ -1,3 +1,4 @@ +import { getUserAlerts, markAlertRead } from '@/api/alerts'; import { Icon } from '@/components/ui'; import Notification, { type Alert } from '@/components/ui/modal/notification/Notification'; import { useUserApplications } from '@/context/userApplicationsProvider'; @@ -5,7 +6,7 @@ import useAuth from '@/hooks/useAuth'; import { cn } from '@/lib/utils/cn'; import { UserRole } from '@/types/user'; import Link from 'next/link'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; interface NavItems { href: string; @@ -22,43 +23,80 @@ const NAV_ITEMS: Record = { }; const Nav = () => { - const { role, isLogin, logout } = useAuth(); + const { role, isLogin, logout, user } = useAuth(); const { applications } = useUserApplications(); const [open, setOpen] = useState(false); - // 읽음 처리한 알림 ID들 (간단 로컬 상태) const [readIds, setReadIds] = useState>(new Set()); + const [apiAlerts, setApiAlerts] = useState([]); - // 알바님 알림: 승인/거절만 표시 - const alerts: Alert[] = useMemo(() => { + // 1) 서버 알림 불러오기 (사장님/알바 공통) + useEffect(() => { + if (!isLogin || !user?.id) { + setApiAlerts([]); + return; + } + (async () => { + try { + const res = await getUserAlerts(user.id, { offset: 0, limit: 50 }); + const mapped: Alert[] = (res.items ?? []).map(({ item }) => ({ + id: item.id, + createdAt: item.createdAt, + result: item.result, + read: item.read, + shop: { item: item.shop.item }, + notice: { item: item.notice.item }, + })); + setApiAlerts(mapped); + } catch { + setApiAlerts([]); // 실패해도 UI는 동작(직원 fallback) + } + })(); + }, [isLogin, user?.id]); + + // 2) (직원 전용) 지원내역 기반 fallback 알림 + const fallbackAlertsForEmployee: Alert[] = useMemo(() => { + if (role !== 'employee') return []; return applications .filter(a => a.item.status !== 'pending') .map(a => ({ id: a.item.id, createdAt: a.item.createdAt ?? new Date().toISOString(), result: a.item.status === 'accepted' ? 'accepted' : 'rejected', - // ▶ 읽음: 사용자가 메시지를 클릭했을 때만 true read: readIds.has(a.item.id), shop: { item: a.item.shop.item, href: `/shops/${a.item.shop.item.id}` }, notice: { item: a.item.notice.item, href: `/notices/${a.item.notice.item.id}` }, })); - }, [applications, readIds]); - - const handleRead = (id: string) => { - setReadIds(prev => { - const next = new Set(prev); - next.add(id); - return next; - }); - }; + }, [applications, role, readIds]); - // role이 초기 undefined일 수 있어 방어 - const currentRole: UserRole = (role ?? 'guest') as UserRole; + // 3) 실제 표시할 알림: 서버 결과가 있으면 우선, 없으면(특히 직원) fallback + const alerts: Alert[] = useMemo(() => { + const base = apiAlerts.length > 0 ? apiAlerts : fallbackAlertsForEmployee; + // 로컬 읽음 세트 반영(서버 알림에도 적용) + return base.map(a => (readIds.has(a.id) ? { ...a, read: true } : a)); + }, [apiAlerts, fallbackAlertsForEmployee, readIds]); - // 아이콘은 "패널 열림 상태"로만 토글 + const unreadCount = alerts.filter(a => !a.read).length; const bellIcon: 'notificationOn' | 'notificationOff' = open ? 'notificationOn' : 'notificationOff'; + const bellColor = open || unreadCount > 0 ? 'bg-red-400' : 'bg-black'; + + // 알림 읽음 처리(서버 + 로컬 동기화) + const handleRead = async (id: string) => { + try { + if (user?.id) await markAlertRead(user.id, id); + } finally { + setReadIds(prev => { + const next = new Set(prev); + next.add(id); + return next; + }); + setApiAlerts(prev => prev.map(a => (a.id === id ? { ...a, read: true } : a))); + } + }; + + const currentRole: UserRole = (role ?? 'guest') as UserRole; return ( ); diff --git a/src/pages/my-profile/index.tsx b/src/pages/my-profile/index.tsx index 7986733..b323d44 100644 --- a/src/pages/my-profile/index.tsx +++ b/src/pages/my-profile/index.tsx @@ -187,7 +187,7 @@ export default function MyProfileDetailPage() { title='신청 내역' content='마음에 드는 공고를 찾아 지원해 보세요.' buttonText='공고 보러가기' - href='/notices' + href='/' /> ) : ( From fe29f78119ba5c28f2938b5521e1d566f61918ce Mon Sep 17 00:00:00 2001 From: jeschun Date: Tue, 21 Oct 2025 01:37:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=92=84=20style:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20=EC=A4=91=EB=B3=B5=20=EC=9B=90=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/header/nav.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/layout/header/nav.tsx b/src/components/layout/header/nav.tsx index 597d7ad..3148d81 100644 --- a/src/components/layout/header/nav.tsx +++ b/src/components/layout/header/nav.tsx @@ -77,9 +77,8 @@ const Nav = () => { }, [apiAlerts, fallbackAlertsForEmployee, readIds]); const unreadCount = alerts.filter(a => !a.read).length; - const bellIcon: 'notificationOn' | 'notificationOff' = open - ? 'notificationOn' - : 'notificationOff'; + const bellIcon: 'notificationOn' | 'notificationOff' = + open || unreadCount > 0 ? 'notificationOn' : 'notificationOff'; const bellColor = open || unreadCount > 0 ? 'bg-red-400' : 'bg-black'; // 알림 읽음 처리(서버 + 로컬 동기화) @@ -136,9 +135,6 @@ const Nav = () => { ariaLabel='알림' className={bellColor} /> - {unreadCount > 0 && ( - - )} {open && ( From 010eaa751d1d304b04369df90ab0d5596f783847 Mon Sep 17 00:00:00 2001 From: jeschun Date: Tue, 21 Oct 2025 01:57:10 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=A7=81=EC=9B=90?= =?UTF-8?q?=EC=97=90=EA=B2=8C=EB=A7=8C=20=EC=95=84=EC=9D=B4=EC=BD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/header/nav.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/layout/header/nav.tsx b/src/components/layout/header/nav.tsx index 3148d81..2a4b60f 100644 --- a/src/components/layout/header/nav.tsx +++ b/src/components/layout/header/nav.tsx @@ -32,7 +32,7 @@ const Nav = () => { // 1) 서버 알림 불러오기 (사장님/알바 공통) useEffect(() => { - if (!isLogin || !user?.id) { + if (!isLogin || !user?.id || role !== 'employer') { setApiAlerts([]); return; } @@ -105,7 +105,7 @@ const Nav = () => { ))} - {isLogin && ( + {isLogin && role === 'employee' && ( <> + )} + + {/* ✅ 사장님(employer)에게만 알림 버튼 숨김 */} + {isLogin && role !== 'employer' && ( +
- {/* 로그인 사용자는 누구나 알림 버튼 노출 (사장님 포함) */} -
- - - {open && ( - setOpen(false)} - /> - )} -
- + {open && ( + setOpen(false)} + /> + )} +
)} );