From 08ab188098819cb1f02c33a57e1ac8452c5a523e Mon Sep 17 00:00:00 2001 From: kim-hyunjoo <78135416+kim-hyunjoo@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:01:30 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC,=20=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EB=AA=A8=EB=8B=AC=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=ED=95=91=20=EC=8B=9C=20=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20?= =?UTF-8?q?=EC=9E=90=EC=97=B0=EC=8A=A4=EB=9F=BD=EA=B2=8C=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor: UserCard 글자 색상 수정 및 주석 삭제 * Fix: UserCard 클릭 시 가끔 씹히던 현상 해결 * Feat: 본인 헤더에도 스켈레톤 적용 * Refactor: isClickedUserCard 전역 상태로 관리 * Refactor: 메세지 관련 react-query 커스텀 훅 refetchInterval 설정 및 리팩토링 * Refactor: 코드 정리 및 배포 전 QA 수정사항 반영 - isFetchedAfterMount 옵션 이용하여 처음에만 스켈레톤 길게 적용, 이후 짧게 적용 - 실시간 대화가 가능하도록 refetchInterval 적용 - 채팅방이 켜져있고 message가 refetch되어 변경되는 경우 알림을 읽도록 적용 - 글쓰기 버튼 hover시 배경 생기는 현상 삭제 - isClickedUserCard 전역 스토어로 분리 - 그 외 자식에게 넘겨준 Props들 전역 스토어에서 바로 사용하는 방식으로 변경 * Feat: useIsTyping 커스텀 훅 분리 * Refactor: 기존에 타자치던 도중에 끊기던 스켈레톤을 자연스럽게 변경, 메세지 모달에서 유저 검색 시 Admin 안나오도록 필터 추가 * Feat: 로그아웃 시 MessageReceiver 관련 스토어 전부 날리기 --- .../Common/Header/HeaderTab/index.tsx | 5 +++ .../DirectMessage/MessageModal/index.tsx | 12 +++---- src/Components/FollowModal/index.tsx | 18 +++++----- src/Hooks/Api/Search/index.ts | 6 +++- src/Hooks/useDebouncedSearch/index.ts | 25 ++++--------- src/Hooks/useDebouncedSearch/type.ts | 1 - src/Hooks/useIsTyping/index.ts | 35 +++++++++++++++++++ 7 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 src/Hooks/useIsTyping/index.ts diff --git a/src/Components/Common/Header/HeaderTab/index.tsx b/src/Components/Common/Header/HeaderTab/index.tsx index 6a9a1a00..3f19b2a7 100644 --- a/src/Components/Common/Header/HeaderTab/index.tsx +++ b/src/Components/Common/Header/HeaderTab/index.tsx @@ -21,6 +21,7 @@ import useAuthUserStore from '@/Stores/AuthUser'; import Badge from '@/Components/Base/Badge'; import filterNotificationLength from './filterNotificationLength'; import DropDownOnlyOption from '../../DropDownOnlyOption'; +import useMessageReceiver from '@/Stores/MessageReceiver'; const HeaderTab = () => { const navigate = useNavigate(); @@ -86,6 +87,8 @@ const HeaderTab = () => { const { setAuthUser } = useAuthUserStore(); + const { setReceiver, setIsClickedUserCard } = useMessageReceiver(); + // useMutation으로 로그아웃 처리 const { mutate } = useMutation({ mutationFn: logout, @@ -96,6 +99,8 @@ const HeaderTab = () => { navigate('/'); setTab('home'); setAuthUser({}); + setReceiver(null); + setIsClickedUserCard(false); }, }); diff --git a/src/Components/DirectMessage/MessageModal/index.tsx b/src/Components/DirectMessage/MessageModal/index.tsx index 242151e5..9331dd0c 100644 --- a/src/Components/DirectMessage/MessageModal/index.tsx +++ b/src/Components/DirectMessage/MessageModal/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { useTheme } from 'styled-components'; import Modal from '@/Components/Common/Modal'; import { StyledBody, StyledContainer, StyledHeader } from './style'; @@ -12,6 +12,7 @@ import DirectMessageSkeleton from '../Skeleton'; import { useSearchUsers } from '@/Hooks/Api/Search'; import useDebouncedSearch from '@/Hooks/useDebouncedSearch'; import useMessageReceiver from '@/Stores/MessageReceiver'; +import useIsTyping from '@/Hooks/useIsTyping'; const MessageModal = ({ setIsModalOpen, @@ -19,9 +20,8 @@ const MessageModal = ({ isMobileSize = false, }: MessageModalProps) => { const { colors } = useTheme(); - const inputRef = useRef(null); + const { inputRef, isTyping } = useIsTyping(); - const [isTyping, setIsTyping] = useState(false); const [selected, setSelected] = useState(null); const [searchQuery, setSearchQuery] = useState(''); @@ -36,16 +36,14 @@ const MessageModal = ({ const debouncedSearch = useDebouncedSearch({ inputRef, callback: setSearchQuery, - setIsTyping, }); - const handleInputChange = async () => { + const handleInputChange = () => { setSelected(null); - setIsTyping(true); debouncedSearch(); }; - const handleClickButton = async () => { + const handleClickButton = () => { if (!selected) { return; } diff --git a/src/Components/FollowModal/index.tsx b/src/Components/FollowModal/index.tsx index 667ef488..e2940a77 100644 --- a/src/Components/FollowModal/index.tsx +++ b/src/Components/FollowModal/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-nested-ternary */ /* eslint-disable no-underscore-dangle */ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import Input from '../Base/Input'; import Modal from '../Common/Modal'; @@ -14,6 +14,7 @@ import { getUser } from '@/Services/User'; import { sendNotifications } from '@/Services/Notification'; import useResize from '@/Hooks/useResize'; import useDebouncedSearch from '@/Hooks/useDebouncedSearch'; +import useIsTyping from '@/Hooks/useIsTyping'; /** * @param userData 해당 유저의 UserType 데이터 @@ -27,12 +28,13 @@ const FollowModal = ({ onChangeOpen, }: FollowModalProps) => { const navigator = useNavigate(); - const inputRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); - const [isTyping, setIsTyping] = useState(false); const [follows, setFollows] = useState([]); const [searchFollows, setSearchFollows] = useState(follows); + const { isMobileSize } = useResize(); + const { inputRef, isTyping } = useIsTyping(); const search = (query: string, fetchedFollows: UserType[]) => { // 검색 중인 단어가 없다면 전체 팔로우 목록을 보여준다. @@ -51,7 +53,6 @@ const FollowModal = ({ inputRef, follows, callback: search, - setIsTyping, }); const fetchFollowData = useCallback(async () => { @@ -85,17 +86,14 @@ const FollowModal = ({ } search(inputRef.current.value, fetchedFollows); - setTimeout(() => { - setIsLoading(false); - }, 100); - }, [mode, userData]); + setIsLoading(false); + }, [mode, userData, inputRef]); useEffect(() => { fetchFollowData(); }, [userData, mode, fetchFollowData]); const handleInputChange = () => { - setIsTyping(true); debouncedSearch(); }; @@ -144,7 +142,7 @@ const FollowModal = ({ return ( diff --git a/src/Hooks/Api/Search/index.ts b/src/Hooks/Api/Search/index.ts index 9007fd7e..092ce462 100644 --- a/src/Hooks/Api/Search/index.ts +++ b/src/Hooks/Api/Search/index.ts @@ -11,7 +11,11 @@ export const useSearchUsers = (query: string, myId: string) => { return []; // query가 비어있는 경우 빈 배열 반환 } const users = await searchUsers(query); - return users ? users.filter((user) => user._id !== myId) : []; + return users + ? users.filter( + (user) => user._id !== myId && user.role !== 'SuperAdmin', + ) + : []; }, }); diff --git a/src/Hooks/useDebouncedSearch/index.ts b/src/Hooks/useDebouncedSearch/index.ts index f93e139f..cba6149b 100644 --- a/src/Hooks/useDebouncedSearch/index.ts +++ b/src/Hooks/useDebouncedSearch/index.ts @@ -1,31 +1,20 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { debounce } from 'lodash'; -import { useMemo } from 'react'; import { Props } from './type'; const useDebouncedSearch = ({ inputRef, follows = [], callback, - setIsTyping, delay = 200, }: Props) => { - const debouncedSearch = useMemo( - () => - debounce(() => { - if (!inputRef || !inputRef.current) { - return; - } - const query = inputRef.current.value.trim(); - callback(query, follows); - - setTimeout(() => { - setIsTyping(false); - }, 400); - }, delay), - [], - ); - + const debouncedSearch = debounce(() => { + if (!inputRef || !inputRef.current) { + return; + } + const query = inputRef.current.value.trim(); + callback(query, follows); + }, delay); return debouncedSearch; }; diff --git a/src/Hooks/useDebouncedSearch/type.ts b/src/Hooks/useDebouncedSearch/type.ts index 75d8c5bd..227f3b03 100644 --- a/src/Hooks/useDebouncedSearch/type.ts +++ b/src/Hooks/useDebouncedSearch/type.ts @@ -4,6 +4,5 @@ export interface Props { inputRef: React.RefObject; follows?: UserType[]; callback: (query: string, follows: UserType[]) => void; - setIsTyping: (state: boolean) => void; delay?: number; } diff --git a/src/Hooks/useIsTyping/index.ts b/src/Hooks/useIsTyping/index.ts new file mode 100644 index 00000000..0f3e53cb --- /dev/null +++ b/src/Hooks/useIsTyping/index.ts @@ -0,0 +1,35 @@ +/* eslint-disable consistent-return */ +import { useEffect, useRef, useState } from 'react'; + +const useIsTyping = () => { + const inputRef = useRef(null); + const [isTyping, setIsTyping] = useState(false); + + useEffect(() => { + const element = inputRef.current as HTMLInputElement | null; + if (!element) return; + + let typingTimer: NodeJS.Timeout; + + const handleTyping = () => { + clearTimeout(typingTimer); + setIsTyping(true); + + typingTimer = setTimeout(() => { + setIsTyping(false); + }, 1000); // 타자 입력이 멈추고 1초 후에 setIsTyping(false) 호출 + }; + + element.addEventListener('input', handleTyping); + + // 컴포넌트 언마운트 시 이벤트 핸들러 제거 + return () => { + element.removeEventListener('input', handleTyping); + clearTimeout(typingTimer); + }; + }, []); + + return { inputRef, isTyping, setIsTyping }; +}; + +export default useIsTyping;