From d76609387241b9778ec87e97780a3a0844710241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Fri, 27 Mar 2026 23:00:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=A0=9C=EC=8A=A4?= =?UTF-8?q?=EC=B2=98=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Chat/ChatRoom.tsx | 4 +-- src/utils/hooks/useViewportHeightLock.ts | 46 ++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/pages/Chat/ChatRoom.tsx b/src/pages/Chat/ChatRoom.tsx index 9471ba4..e390578 100644 --- a/src/pages/Chat/ChatRoom.tsx +++ b/src/pages/Chat/ChatRoom.tsx @@ -84,8 +84,6 @@ function ChatRoom() { useChat(Number(chatRoomId)); const [value, setValue] = useState(''); - useViewportHeightLock(); - const textareaRef = useRef(null); const baseTextareaHeightRef = useRef(0); const { scrollContainerRef, topRef, scrollToBottom } = useChatRoomScroll({ @@ -97,6 +95,8 @@ function ChatRoom() { isFetchingNextPage, }); + useViewportHeightLock(scrollContainerRef); + const currentRoom = chatRoomList.rooms.find((room) => room.roomId === Number(chatRoomId)); const isGroup = currentRoom?.chatType === 'GROUP'; diff --git a/src/utils/hooks/useViewportHeightLock.ts b/src/utils/hooks/useViewportHeightLock.ts index c2fb462..2247ff0 100644 --- a/src/utils/hooks/useViewportHeightLock.ts +++ b/src/utils/hooks/useViewportHeightLock.ts @@ -1,9 +1,9 @@ -import { useLayoutEffect } from 'react'; +import { useLayoutEffect, type RefObject } from 'react'; import { isTextInputElement } from '@/utils/ts/dom'; const SCROLL_RESET_TIMEOUT_MS = 180; -function useViewportHeightLock() { +function useViewportHeightLock(scrollContainerRef?: RefObject) { useLayoutEffect(() => { const root = document.documentElement; const body = document.body; @@ -64,7 +64,43 @@ function useViewportHeightLock() { } }; + let lastTouchClientY = 0; + + const handleTouchStart = (event: TouchEvent) => { + if (event.touches.length !== 1) return; + + lastTouchClientY = event.touches[0].clientY; + }; + + const handleTouchMove = (event: TouchEvent) => { + if (event.touches.length !== 1) return; + + const scrollContainer = scrollContainerRef?.current; + const target = event.target; + + if (!scrollContainer) return; + + if (!(target instanceof Node) || !scrollContainer.contains(target)) { + event.preventDefault(); + return; + } + + const currentTouchClientY = event.touches[0].clientY; + const deltaY = currentTouchClientY - lastTouchClientY; + const canScroll = scrollContainer.scrollHeight > scrollContainer.clientHeight; + const isAtTop = scrollContainer.scrollTop <= 0; + const isAtBottom = scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 1; + + lastTouchClientY = currentTouchClientY; + + if (!canScroll || (deltaY > 0 && isAtTop) || (deltaY < 0 && isAtBottom)) { + event.preventDefault(); + } + }; + const handleWindowScroll = () => { + if (!isEditableFocused) return; + const currentScrollTop = Math.max( scrollingElement?.scrollTop ?? 0, root.scrollTop, @@ -88,6 +124,8 @@ function useViewportHeightLock() { window.addEventListener('focusin', handleFocusIn); window.addEventListener('focusout', handleFocusOut); window.addEventListener('scroll', handleWindowScroll, { passive: true }); + document.addEventListener('touchstart', handleTouchStart, { passive: true }); + document.addEventListener('touchmove', handleTouchMove, { passive: false }); window.visualViewport?.addEventListener('resize', handleViewportChange); window.visualViewport?.addEventListener('scroll', handleViewportChange); @@ -100,6 +138,8 @@ function useViewportHeightLock() { window.removeEventListener('focusin', handleFocusIn); window.removeEventListener('focusout', handleFocusOut); window.removeEventListener('scroll', handleWindowScroll); + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchmove', handleTouchMove); window.visualViewport?.removeEventListener('resize', handleViewportChange); window.visualViewport?.removeEventListener('scroll', handleViewportChange); @@ -109,7 +149,7 @@ function useViewportHeightLock() { body.style.height = prevBodyHeight; root.style.height = prevRootHeight; }; - }, []); + }, [scrollContainerRef]); } export default useViewportHeightLock; From c5ce304304b21686d2b29d682b6f8ebf14a95ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Fri, 27 Mar 2026 23:14:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=9E=85=EB=A0=A5=20=EC=9A=94?= =?UTF-8?q?=EC=86=8C=20=ED=84=B0=EC=B9=98=20=EB=8F=99=EC=9E=91=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/hooks/useViewportHeightLock.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/hooks/useViewportHeightLock.ts b/src/utils/hooks/useViewportHeightLock.ts index 2247ff0..230e2f4 100644 --- a/src/utils/hooks/useViewportHeightLock.ts +++ b/src/utils/hooks/useViewportHeightLock.ts @@ -74,11 +74,16 @@ function useViewportHeightLock(scrollContainerRef?: RefObject { if (event.touches.length !== 1) return; + if (!isEditableFocused) return; const scrollContainer = scrollContainerRef?.current; const target = event.target; + const targetElement = + target instanceof HTMLElement ? target : target instanceof Node ? target.parentElement : null; + const editableElement = targetElement?.closest('input, textarea, [contenteditable]'); if (!scrollContainer) return; + if (editableElement instanceof HTMLElement && isTextInputElement(editableElement)) return; if (!(target instanceof Node) || !scrollContainer.contains(target)) { event.preventDefault();