From cca29e99826c272eb21667c3fdad571e39ba9c67 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:27:56 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EB=9D=B1=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=B0=B0=EA=B2=BD=20=EC=9E=98=EB=A6=AC=EB=8A=94=ED=98=84?= =?UTF-8?q?=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/feed/FollowerListPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/feed/FollowerListPage.tsx b/src/pages/feed/FollowerListPage.tsx index 279a5dba..c6aa5497 100644 --- a/src/pages/feed/FollowerListPage.tsx +++ b/src/pages/feed/FollowerListPage.tsx @@ -149,6 +149,7 @@ const Wrapper = styled.div` align-items: center; min-width: 320px; max-width: 767px; + min-height: 100vh; padding: 0 20px; margin: 0 auto; background-color: var(--color-black-main); From 37c15ea67664a9d2c00242f223a98924976102c5 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Sun, 12 Oct 2025 13:31:19 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=97=A4=EB=8D=94=EC=9D=98=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=84=BC=ED=84=B0=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9C=A0=EB=AC=B4=20=EC=95=8C=EB=A0=A4=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/notifications/getNotificationExist.ts | 18 +++++++++++ src/assets/header/bell.svg | 6 ++-- src/assets/header/exist-bell.svg | 5 ++++ src/components/common/MainHeader.tsx | 30 ++++++++++++++++++- src/hooks/useSocialLoginToken.ts | 10 +++++-- src/stores/useAuthReadyStore.ts | 11 +++++++ 6 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/api/notifications/getNotificationExist.ts create mode 100644 src/assets/header/exist-bell.svg create mode 100644 src/stores/useAuthReadyStore.ts diff --git a/src/api/notifications/getNotificationExist.ts b/src/api/notifications/getNotificationExist.ts new file mode 100644 index 00000000..7bb7a573 --- /dev/null +++ b/src/api/notifications/getNotificationExist.ts @@ -0,0 +1,18 @@ +import { apiClient } from '../index'; + +export interface GetNotificationExistResponse { + isSuccess: boolean; + code: number; + message: string; + requestId: string; + data: { + exists: boolean; + }; +} + +export const getNotificationExist = async (): Promise => { + const response = await apiClient.get( + '/notifications/exists-unchecked', + ); + return response.data; +}; diff --git a/src/assets/header/bell.svg b/src/assets/header/bell.svg index b7d9d348..87d80beb 100644 --- a/src/assets/header/bell.svg +++ b/src/assets/header/bell.svg @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/src/assets/header/exist-bell.svg b/src/assets/header/exist-bell.svg new file mode 100644 index 00000000..3023a2d1 --- /dev/null +++ b/src/assets/header/exist-bell.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/components/common/MainHeader.tsx b/src/components/common/MainHeader.tsx index ad48f5da..a2aaf386 100644 --- a/src/components/common/MainHeader.tsx +++ b/src/components/common/MainHeader.tsx @@ -1,9 +1,13 @@ +import { useEffect, useState } from 'react'; import headerLogo from '../../assets/header/header-logo.svg'; import groupDoneLogo from '../../assets/header/group-done.svg'; import findUserLogo from '../../assets/header/findUser.svg'; import bellLogo from '../../assets/header/bell.svg'; +import bellExistLogo from '../../assets/header/exist-bell.svg'; import styled from '@emotion/styled'; import { IconButton } from './IconButton'; +import { getNotificationExist } from '@/api/notifications/getNotificationExist'; +import { useAuthReadyStore } from '@/stores/useAuthReadyStore'; interface MainHeaderProps { type: 'home' | 'group'; @@ -12,6 +16,26 @@ interface MainHeaderProps { } const MainHeader = ({ type, leftButtonClick, rightButtonClick }: MainHeaderProps) => { + const [hasUnchecked, setHasUnchecked] = useState(false); + const isAuthReady = useAuthReadyStore(s => s.isReady); + + useEffect(() => { + let mounted = true; + const fetchData = async () => { + if (!localStorage.getItem('authToken')) return; + try { + const res = await getNotificationExist(); + if (mounted && res.isSuccess) setHasUnchecked(res.data.exists); + } catch { + // ignore + } + }; + if (isAuthReady) void fetchData(); + return () => { + mounted = false; + }; + }, [isAuthReady]); + return ( @@ -21,7 +45,11 @@ const MainHeader = ({ type, leftButtonClick, rightButtonClick }: MainHeaderProps src={type === 'group' ? groupDoneLogo : findUserLogo} alt={type === 'group' ? '모임 완료 아이콘' : '사용자 찾기 아이콘'} /> - + ); diff --git a/src/hooks/useSocialLoginToken.ts b/src/hooks/useSocialLoginToken.ts index 0d02cbbc..9a6afaf1 100644 --- a/src/hooks/useSocialLoginToken.ts +++ b/src/hooks/useSocialLoginToken.ts @@ -1,12 +1,14 @@ import { useEffect, useRef, useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import { getToken } from '@/api/auth'; +import { useAuthReadyStore } from '@/stores/useAuthReadyStore'; export const useSocialLoginToken = () => { const location = useLocation(); // 토큰 발급 완료를 기다리는 Promise const tokenPromise = useRef | null>(null); + const setReady = useAuthReadyStore(s => s.setReady); useEffect(() => { const handleSocialLoginToken = async (): Promise => { @@ -50,6 +52,8 @@ export const useSocialLoginToken = () => { } catch (error) { console.error('💥 토큰 발급 중 오류 발생:', error); } + // 토큰 발급 시도 완료 시점에 ready true + setReady(true); }; // 소셜 로그인 후 리다이렉트된 경우에만 실행 @@ -57,10 +61,12 @@ export const useSocialLoginToken = () => { const isSocialLoginComplete = urlParams.get('loginTokenKey'); if (isSocialLoginComplete) { - // 토큰 발급 Promise를 저장 tokenPromise.current = handleSocialLoginToken(); + } else { + // 로그인 리다이렉트 경로가 아니어도 이미 토큰이 있을 수 있음 → 바로 ready + setReady(true); } - }, [location.pathname, location.search]); + }, [location.pathname, location.search, setReady]); // 토큰 발급 완료를 기다리는 함수 반환 const waitForToken = useCallback(async (): Promise => { diff --git a/src/stores/useAuthReadyStore.ts b/src/stores/useAuthReadyStore.ts new file mode 100644 index 00000000..bed230ff --- /dev/null +++ b/src/stores/useAuthReadyStore.ts @@ -0,0 +1,11 @@ +import { create } from 'zustand'; + +interface AuthReadyState { + isReady: boolean; + setReady: (ready: boolean) => void; +} + +export const useAuthReadyStore = create(set => ({ + isReady: false, + setReady: (ready: boolean) => set({ isReady: ready }), +})); From fbd62da5363ab1277dc68526865d549acc4222dd Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:36:59 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EB=9D=B1=EB=AA=A9=EB=A1=9D&=20?= =?UTF-8?q?=EB=82=B4=20=EB=9D=B1=EB=AA=A9=EB=A1=9D=20totalCount=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/users/getFollowerList.ts | 1 + src/api/users/getFollowingList.ts | 1 + src/pages/feed/FollowerListPage.tsx | 55 +++++++++++++++++++---------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/api/users/getFollowerList.ts b/src/api/users/getFollowerList.ts index 64dcb27f..32507a4e 100644 --- a/src/api/users/getFollowerList.ts +++ b/src/api/users/getFollowerList.ts @@ -9,6 +9,7 @@ export interface FollowerListResponse { followers: FollowData[]; nextCursor: string; isLast: boolean; + totalFollowerCount?: number; }; } diff --git a/src/api/users/getFollowingList.ts b/src/api/users/getFollowingList.ts index e6fd975d..e374b862 100644 --- a/src/api/users/getFollowingList.ts +++ b/src/api/users/getFollowingList.ts @@ -9,6 +9,7 @@ export interface FollowingListResponse { followings: FollowData[]; nextCursor: string; isLast: boolean; + totalFollowingCount?: number; }; } diff --git a/src/pages/feed/FollowerListPage.tsx b/src/pages/feed/FollowerListPage.tsx index c6aa5497..876c7d62 100644 --- a/src/pages/feed/FollowerListPage.tsx +++ b/src/pages/feed/FollowerListPage.tsx @@ -8,6 +8,7 @@ import type { UserProfileType } from '@/types/user'; import { getFollowerList } from '@/api/users/getFollowerList'; import { getFollowingList } from '@/api/users/getFollowingList'; import type { FollowData } from '@/types/follow'; +import LoadingSpinner from '@/components/common/LoadingSpinner'; const FollowerListPage = () => { const navigate = useNavigate(); @@ -16,6 +17,7 @@ const FollowerListPage = () => { // 상태 관리 const [userList, setUserList] = useState([]); + const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [retryCount, setRetryCount] = useState(0); @@ -77,6 +79,14 @@ const FollowerListPage = () => { setNextCursor(response.data.nextCursor); setIsLast(response.data.isLast); + // 총합 카운트 설정 (API별 키 분기) + if (type === 'followerlist') { + const total = (response.data as { totalFollowerCount?: number }).totalFollowerCount; + if (typeof total === 'number') setTotalCount(total); + } else { + const total = (response.data as { totalFollowingCount?: number }).totalFollowingCount; + if (typeof total === 'number') setTotalCount(total); + } // setTotalCount(prev => prev + userData.length); setRetryCount(0); } catch (error) { @@ -121,24 +131,33 @@ const FollowerListPage = () => { return ( } onLeftClick={handleBackClick} title={title} /> - 전체 {userList.length} - - {userList.map((user, index) => ( - - ))} - + 전체 {totalCount} + {loading && userList.length === 0 ? ( + + ) : ( + + {userList.map((user, index) => ( + + ))} + {loading && userList.length > 0 && ( +
+ +
+ )} +
+ )}
); }; From 953465a6a0739097b799cf37fe9c4a2dfadf4972 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:00:28 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EB=82=B4=20=EB=9D=B1=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D&=EB=9D=B1=20=EB=AA=A9=EB=A1=9D=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/feed/FollowerListPage.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pages/feed/FollowerListPage.tsx b/src/pages/feed/FollowerListPage.tsx index 876c7d62..ac641ac0 100644 --- a/src/pages/feed/FollowerListPage.tsx +++ b/src/pages/feed/FollowerListPage.tsx @@ -114,7 +114,7 @@ const FollowerListPage = () => { const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; - if (scrollTop + windowHeight >= documentHeight - 200) { + if (scrollTop + windowHeight >= documentHeight - 100) { loadUserList(nextCursor); } }; @@ -128,6 +128,15 @@ const FollowerListPage = () => { loadUserList(); }, [loadUserList]); + // 첫 페이지 높이가 뷰포트보다 작아 스크롤 이벤트가 발생하지 않는 경우 자동으로 다음 페이지를 프리페치 + useEffect(() => { + const doc = document.documentElement; + const needsMore = doc.scrollHeight <= window.innerHeight + 100; + if (!loading && !isLast && !!nextCursor && needsMore) { + loadUserList(nextCursor); + } + }, [userList, loading, isLast, nextCursor, loadUserList]); + return ( } onLeftClick={handleBackClick} title={title} /> @@ -192,9 +201,9 @@ const TotalBar = styled.div` const UserProfileList = styled.div` width: 100%; - height: 100vh; - /* min-width: 320px; - max-width: 540px; */ + /* 고정 헤더(TopBar) 영역을 제외한 최소 높이를 보장하여 하단 여백에도 배경이 비지 않도록 함 */ + min-height: 100vh; + background-color: var(--color-black-main); padding-top: 105px; padding-bottom: 20px; `;