From 05354dacf46dfb854452c1e54d5f552d1d6bf706 Mon Sep 17 00:00:00 2001 From: hyojin Date: Thu, 16 Apr 2026 16:56:47 +0900 Subject: [PATCH 01/11] =?UTF-8?q?(feat/#235):=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/taskmate/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/taskmate/page.tsx b/src/app/taskmate/page.tsx index 5f35aaa2..de7a98cf 100644 --- a/src/app/taskmate/page.tsx +++ b/src/app/taskmate/page.tsx @@ -8,7 +8,7 @@ import WelcomeBanner from "@/components/home/WelcomeBanner"; export default function TaskmatePage() { return ( -
+
error
}> From 968865228aa2123baa3485e51a8010c87e76d7fe Mon Sep 17 00:00:00 2001 From: hyojin Date: Thu, 16 Apr 2026 18:16:25 +0900 Subject: [PATCH 02/11] =?UTF-8?q?(feat/#235):=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94-=20=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=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 --- .../NotificationPopover/NotificatioPanel.tsx | 2 +- .../NotificationPopover.tsx | 69 ++++++++++++++++--- src/components/NavigationBar/index.tsx | 5 +- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/components/NavigationBar/NotificationPopover/NotificatioPanel.tsx b/src/components/NavigationBar/NotificationPopover/NotificatioPanel.tsx index cf0a95ef..8d138027 100644 --- a/src/components/NavigationBar/NotificationPopover/NotificatioPanel.tsx +++ b/src/components/NavigationBar/NotificationPopover/NotificatioPanel.tsx @@ -67,7 +67,7 @@ const NotificationPanel = ({ onClose }: Props) => { }; return ( -
+
알림 diff --git a/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx b/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx index c000a737..72d31464 100644 --- a/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx +++ b/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx @@ -1,29 +1,82 @@ "use client"; -import React, { useState } from "react"; -import { Icon } from "@/components/common/Icon/index"; +import { cva } from "class-variance-authority"; +import { useEffect, useRef, useState } from "react"; + +import { Icon } from "@/components/common/Icon"; +import { cn } from "@/utils/utils"; import NotificationPanel from "./NotificatioPanel"; -const NotificationPopover = () => { +const buttonVariants = cva( + "cursor-pointer bg-inverse-normal flex shrink-0 items-center justify-center rounded-[99px]", + { + variants: { + placement: { + default: "size-16 ring-1 ring-gray-300 ring-inset", + aside: "size-[30px]", // bell(30)와 동일 + }, + }, + defaultVariants: { + placement: "default", + }, + }, +); + +interface NotificationPopoverProps { + placement?: "aside"; +} + +const NotificationPopover = ({ placement }: NotificationPopoverProps) => { const [open, setOpen] = useState(false); + const popoverRef = useRef(null); const handleClick = () => setOpen((prev) => !prev); + useEffect(() => { + if (!open) return; + + const handlePointerDownOutside = (event: PointerEvent) => { + const target = event.target as Node; + if (!popoverRef.current?.contains(target)) { + setOpen(false); + } + }; + + document.addEventListener("pointerdown", handlePointerDownOutside); + return () => + document.removeEventListener("pointerdown", handlePointerDownOutside); + }, [open]); + return ( -
+
- {open && setOpen(false)} />} + {open && ( +
e.stopPropagation()} + > + setOpen(false)} /> +
+ )}
); }; diff --git a/src/components/NavigationBar/index.tsx b/src/components/NavigationBar/index.tsx index 772117cb..2fdbb1b8 100644 --- a/src/components/NavigationBar/index.tsx +++ b/src/components/NavigationBar/index.tsx @@ -34,7 +34,8 @@ const navigationBarAsideVariants = cva( variants: { open: { true: "h-screen overflow-y-scroll mobile:w-[360px] mobile:p-8", - false: "h-[56px] mobile:w-[60px] mobile:px-3 mobile:py-8", + false: + "h-[56px] mobile:w-[60px] mobile:px-3 mobile:py-8 gap-6 items-center", }, }, defaultVariants: { open: false }, @@ -53,7 +54,7 @@ export const NavigationBar = () => { style={{ willChange: "width, height", zIndex: NAVIGATION_BAR_ZINDEX }} >
- {/* */} + {!isOpen && } Date: Thu, 16 Apr 2026 18:22:17 +0900 Subject: [PATCH 03/11] =?UTF-8?q?(feat/#235):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationPopover/NotificationPopover.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx b/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx index 72d31464..21feb54e 100644 --- a/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx +++ b/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx @@ -1,9 +1,11 @@ "use client"; +import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; import { cva } from "class-variance-authority"; import { useEffect, useRef, useState } from "react"; import { Icon } from "@/components/common/Icon"; +import { notificationInfiniteQueries } from "@/features/notification/query/notificationInfiniteQueries"; import { cn } from "@/utils/utils"; import NotificationPanel from "./NotificatioPanel"; @@ -48,6 +50,16 @@ const NotificationPopover = ({ placement }: NotificationPopoverProps) => { document.removeEventListener("pointerdown", handlePointerDownOutside); }, [open]); + // 알림 표시 + const { data: notificationData } = useSuspenseInfiniteQuery( + notificationInfiniteQueries.notificationsInfinite(), + ); + + const hasUnread = + notificationData?.pages.some((page) => + page.items.some((item) => item.isRead === false), + ) ?? false; + return (
{ size={placement === "aside" ? 30 : 24} className="text-gray-500" /> + + {hasUnread && ( + + )} {open && ( From c77d16f2283ddb8e11a31a7db940babc45cc6a09 Mon Sep 17 00:00:00 2001 From: hyojin Date: Thu, 16 Apr 2026 18:31:16 +0900 Subject: [PATCH 04/11] =?UTF-8?q?(feat/#235):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20bottom/left=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NavigationBar/NotificationPopover/NotificationPopover.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx b/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx index 21feb54e..13c47212 100644 --- a/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx +++ b/src/components/NavigationBar/NotificationPopover/NotificationPopover.tsx @@ -84,9 +84,7 @@ const NotificationPopover = ({ placement }: NotificationPopoverProps) => {
e.stopPropagation()} > From b1142f313ff850b17411879fd5e8ddde37c9072e Mon Sep 17 00:00:00 2001 From: hyojin Date: Thu, 16 Apr 2026 21:07:27 +0900 Subject: [PATCH 05/11] =?UTF-8?q?(feat/#235):=20=EC=A6=90=EA=B2=A8?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/FavoriteGoalsItem.tsx | 6 ++++-- src/components/home/FavoriteGoalsSection.tsx | 16 ++++++++++++++++ src/features/goal/api.ts | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/components/home/FavoriteGoalsItem.tsx b/src/components/home/FavoriteGoalsItem.tsx index 8b76044f..c04d28f3 100644 --- a/src/components/home/FavoriteGoalsItem.tsx +++ b/src/components/home/FavoriteGoalsItem.tsx @@ -8,6 +8,8 @@ export interface FavoriteGoalsItemProps { goal: { goalId: number; goalName: string; + progressPercent: number; + isFavorite: boolean; }; } @@ -25,9 +27,9 @@ export function FavoriteGoalsItem({ teamId={String(teamId)} goalId={goal.goalId} title={goal.goalName} - progress={0} + progress={goal.progressPercent} color="green" - isFavorite={false} + isFavorite={goal.isFavorite} className="w-full shrink-0 bg-green-100" />
diff --git a/src/components/home/FavoriteGoalsSection.tsx b/src/components/home/FavoriteGoalsSection.tsx index 8bd01383..0868927c 100644 --- a/src/components/home/FavoriteGoalsSection.tsx +++ b/src/components/home/FavoriteGoalsSection.tsx @@ -1,5 +1,6 @@ "use client"; +import { useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; @@ -13,6 +14,7 @@ import { useInfiniteScroll } from "@/hooks/useInfiniteScroll/useInfiniteScroll"; export function FavoriteGoalsSection() { const router = useRouter(); + const queryClient = useQueryClient(); const { ref, data, isFetchingNextPage } = useInfiniteScroll( mainInfiniteQueries.favoriteGoalsInfinite(), @@ -74,6 +76,20 @@ export function FavoriteGoalsSection() { return () => container.removeEventListener("scroll", updateScrollState); }, []); + useEffect(() => { + const handleFavoriteToggled = () => { + queryClient.invalidateQueries({ queryKey: ["favoriteGoals"] }); + }; + + window.addEventListener("goal-favorite-toggled", handleFavoriteToggled); + return () => { + window.removeEventListener( + "goal-favorite-toggled", + handleFavoriteToggled, + ); + }; + }, [queryClient]); + if (items.length === 0) { return (
diff --git a/src/features/goal/api.ts b/src/features/goal/api.ts index be37a79e..02d3afe3 100644 --- a/src/features/goal/api.ts +++ b/src/features/goal/api.ts @@ -25,8 +25,19 @@ export const goalApi = { params: { sort }, }), - toggleFavorite: (goalId: number) => - apiClient.post<{ success: boolean }>(`/api/goals/${goalId}/favorite`), + toggleFavorite: async (goalId: number) => { + const result = await apiClient.post<{ success: boolean }>( + `/api/goals/${goalId}/favorite`, + ); + + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("goal-favorite-toggled", { detail: { goalId } }), + ); + } + + return result; + }, getSummary: (goalId: string) => apiClient.get(`/api/goals/${goalId}/summary`), From a4f00f97b07e9dac3b46978320dc17c194fd679d Mon Sep 17 00:00:00 2001 From: hyojin Date: Thu, 16 Apr 2026 21:44:20 +0900 Subject: [PATCH 06/11] =?UTF-8?q?(feat/#235):=20=ED=8C=80=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../team/[teamId]/management/page.tsx | 47 +++++++++++++------ src/components/management/DeleteModal.tsx | 30 +++++++----- src/components/management/ErrorModal.tsx | 4 +- src/components/management/MemberList.tsx | 39 ++++++++++++--- 4 files changed, 86 insertions(+), 34 deletions(-) diff --git a/src/app/taskmate/team/[teamId]/management/page.tsx b/src/app/taskmate/team/[teamId]/management/page.tsx index e8f94b96..2e6859d7 100644 --- a/src/app/taskmate/team/[teamId]/management/page.tsx +++ b/src/app/taskmate/team/[teamId]/management/page.tsx @@ -1,10 +1,11 @@ "use client"; import { useParams, useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import TextButton from "@/components/common/TextButton/TextButton"; import DeleteModal from "@/components/management/DeleteModal"; +import ErrorModal from "@/components/management/ErrorModal"; import InviteModal from "@/components/management/InviteModal"; import MemberList from "@/components/management/MemberList"; import TeamNameEditor from "@/components/management/TeamNameEditor"; @@ -17,6 +18,8 @@ const TeamManagement = () => { const router = useRouter(); const params = useParams<{ teamId: string }>(); const teamId = params.teamId; + const [errorMessage, setErrorMessage] = useState(""); + const [errorModalOpen, setErrorModalOpen] = useState(false); const handleOpenInvite = () => { open( @@ -40,6 +43,10 @@ const TeamManagement = () => { close(); router.replace("/taskmate"); }} + onError={(message) => { + setErrorMessage(message); + setErrorModalOpen(true); + }} />, ); }; @@ -66,21 +73,31 @@ const TeamManagement = () => { }, [teamId, router]); return ( -
-
-

팀 정보 수정

-
- - - - 팀 삭제하기 - +
+
+
+

+ 팀 정보 수정 +

+
+ + + + 팀 삭제하기 + +
-
-
+ + + setErrorModalOpen(false)} + /> + ); }; diff --git a/src/components/management/DeleteModal.tsx b/src/components/management/DeleteModal.tsx index 2725cf80..e5cf050d 100644 --- a/src/components/management/DeleteModal.tsx +++ b/src/components/management/DeleteModal.tsx @@ -1,30 +1,38 @@ "use client"; -import { useRouter } from "next/navigation"; - import Button from "@/components/common/Button/Button"; import TextButton from "@/components/common/TextButton/TextButton"; interface DeleteModalProps { onClose: () => void; onSubmitDelete: () => Promise; + onError: (message: string) => void; } // @TODO: onSubmitDelete 함수를 Page에서 받아오는 방식 제거 ( Page가 갖는 책임 아님 ) -const DeleteModal = ({ onClose, onSubmitDelete }: DeleteModalProps) => { - const router = useRouter(); - +const DeleteModal = ({ + onClose, + onSubmitDelete, + onError, +}: DeleteModalProps) => { const handleSubmit = async (e: React.SyntheticEvent) => { e.preventDefault(); // @TODO: useMutation 으로 리팩토링 - // @TODO: console 제거 try { await onSubmitDelete(); onClose(); - router.replace("/taskmate"); - } catch (error) { - console.log("팀 삭제 실패", error); + } catch (error: unknown) { + onClose(); + const message = + typeof error === "object" && + error !== null && + "data" in error && + typeof (error as { data?: { message?: unknown } }).data?.message === + "string" + ? ((error as { data: { message: string } }).data.message ?? "") + : "팀 삭제에 실패했습니다."; + onError(message); } }; @@ -37,8 +45,8 @@ const DeleteModal = ({ onClose, onSubmitDelete }: DeleteModalProps) => {

팀 페이지와 팀 정보를 삭제합니다.

-

- 삭제된 팀 페이지는 복구할 수 없어요 +

+ 삭제된 팀 페이지는 복구할 수 없어요.

{ - {message} + {message} diff --git a/src/components/management/MemberList.tsx b/src/components/management/MemberList.tsx index e13681b4..d4d24e36 100644 --- a/src/components/management/MemberList.tsx +++ b/src/components/management/MemberList.tsx @@ -44,6 +44,29 @@ const MemberList = ({ onInviteClick }: MemberListProps) => { const [errorMessage, setErrorMessage] = useState(""); const [errorModalOpen, setErrorModalOpen] = useState(false); + const getApiErrorMessage = (error: unknown, fallback: string) => { + if (error && typeof error === "object") { + const data = (error as { data?: unknown }).data; + if ( + data && + typeof data === "object" && + "message" in data && + typeof (data as { message?: unknown }).message === "string" + ) { + return (data as { message: string }).message; + } + + if ( + "message" in error && + typeof (error as { message?: unknown }).message === "string" + ) { + return (error as { message: string }).message; + } + } + + return fallback; + }; + // @TODO: myUserId를 가져오는 Hooks 로 분리 const { data: me } = useQuery({ ...userQueries.myInfo(), @@ -86,8 +109,10 @@ const MemberList = ({ onInviteClick }: MemberListProps) => { if (myUserId === memberUserId && pending.role !== "ADMIN") { router.push("/taskmate"); } - } catch { - setErrorMessage("유효하지 않은 권한 설정 입니다."); + } catch (error) { + setErrorMessage( + getApiErrorMessage(error, "유효하지 않은 권한 설정 입니다."), + ); setErrorModalOpen(true); } finally { setRoleChangeModalOpen(false); @@ -97,7 +122,7 @@ const MemberList = ({ onInviteClick }: MemberListProps) => { /* @TODO: useOverlay 공통 hooks 로 적용 */ const openMemberDeleteModal = (memberId: number) => { setPending({ memberId }); - setConfirmMessage("팀원의 권한을 삭제 하시겠습니까?"); + setConfirmMessage("팀원을 삭제 하시겠습니까?"); setMemberDeleteModalOpen(true); }; @@ -110,8 +135,10 @@ const MemberList = ({ onInviteClick }: MemberListProps) => { setMembers((prev) => prev.filter((member) => member.id !== pending.memberId), ); - } catch { - setErrorMessage("관리자는 본인을 팀에서 삭제할 수 없습니다."); + } catch (error) { + setErrorMessage( + getApiErrorMessage(error, "관리자는 본인을 팀에서 삭제할 수 없습니다."), + ); setErrorModalOpen(true); } finally { setMemberDeleteModalOpen(false); @@ -218,7 +245,7 @@ const MemberList = ({ onInviteClick }: MemberListProps) => { {/* 팀원 삭제 모달 */} {/* @TODO: useOverlay 공통 hooks 로 적용 */} setMemberDeleteModalOpen(false)} onConfirm={handleDeleteMember} From 0122752692864b4aebaebc9f58939a44c14181ec Mon Sep 17 00:00:00 2001 From: hyojin Date: Thu, 16 Apr 2026 22:35:56 +0900 Subject: [PATCH 07/11] =?UTF-8?q?(feat/#235):=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 21 +++++++++---------- .../team/[teamId]/management/page.tsx | 6 +++--- src/hooks/useDropdown/Dropdown.tsx | 10 +++++++-- src/hooks/useDropdown/index.ts | 2 +- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9a0d6e0c..879cf034 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,11 +6,10 @@ import { SpeedInsights } from "@vercel/speed-insights/next"; import ToastProvider from "@/components/common/Toast"; import Overlay from "@/hooks/useOverlay/Overlay"; import { pretendard } from "@/lib/fonts"; -import { initMocks } from "@/mocks"; -import MSWInitializer from "@/mocks/MSWInitializer"; +// import { initMocks } from "@/mocks"; import { ReactQueryClientProvider } from "@/providers/ReactQueryProvider"; -initMocks(); +// initMocks(); export default function RootLayout({ children, @@ -23,14 +22,14 @@ export default function RootLayout({ className={pretendard.variable} > - - - - - {children} - - - + {/* */} + + + + {children} + + + {/* */} diff --git a/src/app/taskmate/team/[teamId]/management/page.tsx b/src/app/taskmate/team/[teamId]/management/page.tsx index 2e6859d7..c15d2a2b 100644 --- a/src/app/taskmate/team/[teamId]/management/page.tsx +++ b/src/app/taskmate/team/[teamId]/management/page.tsx @@ -74,12 +74,12 @@ const TeamManagement = () => { return (
-
-
+
+

팀 정보 수정

-
+
({ isOpen, selected: current, toggle, + close, selectItem, containerRef, } = useDropdown([...options], selected); @@ -52,12 +53,17 @@ export const Dropdown = ({ // TODO: 삭제 예정 // 기존 코드의 동작을 이해해야 함 const handleSelect = async (value: T) => { + if (!onSelect) { + selectItem(value); + return; + } + try { await onSelect?.(value); - // API 성공 시에만 UI 선택값 변경 - selectItem(value); } catch (error) { console.error("dropdown select error", error); + } finally { + close(); } }; diff --git a/src/hooks/useDropdown/index.ts b/src/hooks/useDropdown/index.ts index 957cf882..5e9b9ff1 100644 --- a/src/hooks/useDropdown/index.ts +++ b/src/hooks/useDropdown/index.ts @@ -43,5 +43,5 @@ export function useDropdown(options: string[], initialSelected?: string) { return () => document.removeEventListener("mousedown", handleClickOutside); }, []); - return { isOpen, selected, toggle, selectItem, containerRef }; + return { isOpen, selected, toggle, close, selectItem, containerRef }; } From fc586abf4872b85d92efb8dc1edaea88c8b995b1 Mon Sep 17 00:00:00 2001 From: hyojin Date: Fri, 17 Apr 2026 09:00:57 +0900 Subject: [PATCH 08/11] =?UTF-8?q?(feat/#235):=20msw=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 879cf034..9a0d6e0c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,10 +6,11 @@ import { SpeedInsights } from "@vercel/speed-insights/next"; import ToastProvider from "@/components/common/Toast"; import Overlay from "@/hooks/useOverlay/Overlay"; import { pretendard } from "@/lib/fonts"; -// import { initMocks } from "@/mocks"; +import { initMocks } from "@/mocks"; +import MSWInitializer from "@/mocks/MSWInitializer"; import { ReactQueryClientProvider } from "@/providers/ReactQueryProvider"; -// initMocks(); +initMocks(); export default function RootLayout({ children, @@ -22,14 +23,14 @@ export default function RootLayout({ className={pretendard.variable} > - {/* */} - - - - {children} - - - {/* */} + + + + + {children} + + + From b827fddd7d2f4ea42aeaa086206bbbe52d7eadea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A2=85=EC=A7=84=28SugarSyrup=29?= Date: Sun, 19 Apr 2026 22:15:41 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat(#235):=20test=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NavigationBar/index.test.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/NavigationBar/index.test.tsx b/src/components/NavigationBar/index.test.tsx index 85d82b99..5668d92d 100644 --- a/src/components/NavigationBar/index.test.tsx +++ b/src/components/NavigationBar/index.test.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen } from "@testing-library/react"; import { useRouter } from "next/navigation"; import type { ComponentProps, ReactElement } from "react"; @@ -35,6 +36,11 @@ jest.mock("@/components/common/LogoutButton", () => ({ default: () => null, })); +jest.mock("@/components/NavigationBar/NotificationPopover", () => ({ + __esModule: true, + default: () => null, +})); + jest.mock("@/components/common/Icon", () => ({ Icon: ({ name }: { name: string }) => , })); @@ -47,6 +53,7 @@ function renderWithNavContext( ui: ReactElement, value: Partial = {}, ) { + const queryClient = new QueryClient(); const defaults: ContextValue = { isOpen: true, open: jest.fn(), @@ -57,9 +64,11 @@ function renderWithNavContext( }; return render( - - {ui} - , + + + {ui} + + , ); } From 2c4d9ade990a70f1b741938393ea5530149f87fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A2=85=EC=A7=84=28SugarSyrup=29?= Date: Sun, 19 Apr 2026 22:48:01 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat(#235):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B0=A8?= =?UTF-8?q?=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/taskmate/layout.tsx | 1 - src/components/NavigationBar/index.tsx | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/taskmate/layout.tsx b/src/app/taskmate/layout.tsx index 587057a5..d658ce71 100644 --- a/src/app/taskmate/layout.tsx +++ b/src/app/taskmate/layout.tsx @@ -1,6 +1,5 @@ import { NavigationBar } from "@/components/NavigationBar"; import NavigationBarProvider from "@/components/NavigationBar/provider"; - import NotificationSubscriber from "@/features/notification/NotificationSubscriber"; export default function TaskmateLayout({ diff --git a/src/components/NavigationBar/index.tsx b/src/components/NavigationBar/index.tsx index 2fdbb1b8..052caca9 100644 --- a/src/components/NavigationBar/index.tsx +++ b/src/components/NavigationBar/index.tsx @@ -112,7 +112,9 @@ export const NavigationBar = () => { - + + +
From 705f0a43f275638dbd8a84b338dc6b620c23940b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A2=85=EC=A7=84=28SugarSyrup=29?= Date: Mon, 20 Apr 2026 00:15:48 +0900 Subject: [PATCH 11/11] =?UTF-8?q?feat(#235):=20SSE=20Token=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=20=EC=9A=94=EC=B2=AD=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/notification/useNotificationSSE.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/features/notification/useNotificationSSE.ts b/src/features/notification/useNotificationSSE.ts index 208b9401..e7369c76 100644 --- a/src/features/notification/useNotificationSSE.ts +++ b/src/features/notification/useNotificationSSE.ts @@ -32,6 +32,8 @@ export function useNotificationSSE() { if (!isLogin) return; let unmounted = false; + let connectAttempts = 0; + const MAX_CONNECT_ATTEMPTS = 3; const clear = () => { if (timerRef.current !== null) { @@ -44,6 +46,8 @@ export function useNotificationSSE() { const connect = async () => { if (unmounted) return; + if (connectAttempts >= MAX_CONNECT_ATTEMPTS) return; + connectAttempts += 1; try { const tokenRes = await NotificationApi.issueSseToken(); @@ -59,6 +63,7 @@ export function useNotificationSSE() { es.onerror = async () => { clear(); + if (connectAttempts >= MAX_CONNECT_ATTEMPTS) return; try { // 401 등 인증 문제 시 BFF 재발급 트리거 await queryClient.fetchQuery(userQueries.myInfo());