Skip to content

Commit

Permalink
Refactor/#87: 댓글 및 답글 스크롤 포커싱 문제, 프로필 라우팅 문제 수정 (#89)
Browse files Browse the repository at this point in the history
* feat: 브라우저 resize 이벤트로 디바이스 키보드 높이값 계산하여 Context로 할당

* refactor: MobileViewContext 변경사항 적용

* refactor: 각 OS 및 디바이스에 따라 스크롤 로직 분기 적용 및 보완

* feat: 댓글이 하나도 없을 경우 Empty Section 표시

* fix: 무한 스크롤 intersectionRef div 위치 조정

* feat: 댓글 및 답글 프로필 클릭 시 해당 프로필 페이지로 네비게이션

* feat: 게시글 상세 profile 영역 클릭 시 해당 프로필 페이지로 네비게이션

* refactor: 불필요한 Promise 대신 setTimeout 적용

* refactor: Empty Section 에서 댓글 반영이 되지 않던 오류 수정

* refactor: 세부 스킬 영역이 길 경우 오작동 우려가 있어 프로필 이미지 클릭시 프로필 라우팅하도록 변경
  • Loading branch information
semnil5202 authored Apr 4, 2024
1 parent b14a9f0 commit 222a625
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 39 deletions.
9 changes: 7 additions & 2 deletions src/components/ProfileInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { Text, TextDivider, Box, Flex, ImageView, PNGDefaultProfileInfo36 } from 'concept-be-design-system';
import { Fragment } from 'react';

import useNavigatePage from '../pages/hooks/useNavigatePage';

interface Props {
memberId: number;
imageUrl: string;
nickname: string;
skillList: string[];
}

// TODO: 프로필 이미지 사진 오류 시 보여줄 기본 프로필 이미지 사진 URL

const ProfileInfo = ({ imageUrl, nickname, skillList }: Props) => {
const ProfileInfo = ({ memberId, imageUrl, nickname, skillList }: Props) => {
const { goProfilePage } = useNavigatePage();

return (
<Flex alignItems="center" gap={10}>
<Flex alignItems="center" gap={10} onClick={() => goProfilePage(memberId)} cursor="pointer">
<Box width={36} height={36} overflow="hidden" borderRadius="0 150px 150px 0">
<ImageView src={imageUrl} alt="프로필" defaultSrc={PNGDefaultProfileInfo36} />
</Box>
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useInitScrollPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const setPageScrollPosition = (pageName: string, position: number) => {
};

const useInitScrollPosition = (pageName: string) => {
const mobileViewRef = useMobileViewRefContext();
const { mobileViewRef } = useMobileViewRefContext();
const { hasMatched } = useRouteMatched();

useLayoutEffect(() => {
Expand Down
33 changes: 28 additions & 5 deletions src/layouts/MobileView.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
import styled from '@emotion/styled';
import { useRef } from 'react';
import { MutableRefObject, useEffect, useRef } from 'react';
import { Outlet } from 'react-router-dom';

import { MobileViewRefContext } from './contexts/MobileViewContext';
import Navbar from './Navbar';

const MobileView = () => {
// TODO: Header 도메인 얽힘 문제 해결 확인 시 사용 예정
// const isMatchedHeader = hasMatched('/feed', '/feed/:id', '/profile', '/profile/:id', '/profile/:id/more');
const innerHeight = window.innerHeight;

const calculateKeyboardHeight = (keyboardHeightRef: MutableRefObject<number>) => {
const visualViewHeight = window.visualViewport?.height;

if (visualViewHeight && keyboardHeightRef.current === 0) {
keyboardHeightRef.current = innerHeight - visualViewHeight;
}
};

const MobileView = () => {
const mobileViewRef = useRef<HTMLElement | null>(null);
const keyboardHeightRef = useRef<number>(0);

useEffect(() => {
if (!window.visualViewport) return;
const windowVisualViewPort = window.visualViewport;

const onResizeViewPortHeight = () => {
calculateKeyboardHeight(keyboardHeightRef);
};

windowVisualViewPort.addEventListener('resize', onResizeViewPortHeight);

return () => {
windowVisualViewPort.removeEventListener('resize', onResizeViewPortHeight);
};
}, []);

return (
<MobileViewRefContext.Provider value={{ mobileViewRef }}>
<MobileViewRefContext.Provider value={{ mobileViewRef, keyboardHeightRef }}>
<Wrapper ref={mobileViewRef}>
<Outlet />
<Navbar />
Expand Down
5 changes: 3 additions & 2 deletions src/layouts/contexts/MobileViewContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MutableRefObject, createContext, useContext } from 'react';

interface MobileViewMainContextProps {
mobileViewRef: MutableRefObject<HTMLElement | null>;
keyboardHeightRef: MutableRefObject<number>;
}

export const MobileViewRefContext = createContext<MobileViewMainContextProps | null>(null);
Expand All @@ -17,7 +18,7 @@ const useMobileViewContext = () => {

export const useMobileViewRefContext = () => {
const context = useMobileViewContext();
const { mobileViewRef } = context;
const { mobileViewRef, keyboardHeightRef } = context;

return mobileViewRef;
return { mobileViewRef, keyboardHeightRef };
};
3 changes: 2 additions & 1 deletion src/pages/FeedDetail/FeedDetail.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const FeedDetailPage = () => {
const navigate = useNavigate();
const { id: feedId } = useParams() as { id: string };
const {
memberId,
imageUrl,
nickname,
skillList,
Expand Down Expand Up @@ -59,7 +60,7 @@ const FeedDetailPage = () => {
</Header>

<Box padding="30px 22px 30px 22px" marginTop={48}>
<ProfileInfo imageUrl={imageUrl} nickname={nickname} skillList={skillList} />
<ProfileInfo memberId={memberId} imageUrl={imageUrl} nickname={nickname} skillList={skillList} />
<Spacer size={20} />
<Box>
<div>
Expand Down
2 changes: 2 additions & 0 deletions src/pages/FeedDetail/components/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const Comment = ({
myImageUrl,
myNickname,
comment: {
memberId,
parentCommentId,
nickname,
profileImageUrl,
Expand Down Expand Up @@ -86,6 +87,7 @@ const Comment = ({
<Box margin="20px 0">
<Flex justifyContent="space-between">
<CommentProfileInfo
memberId={memberId}
imageUrl={profileImageUrl}
nickname={nickname}
skillList={memberSkills}
Expand Down
14 changes: 12 additions & 2 deletions src/pages/FeedDetail/components/CommentProfileInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@ import { Box, Flex, ImageView, PNGDefaultProfileInfo36, Text, TextDivider } from
import { Fragment } from 'react';

import { formatCommentDate } from '../../Feed/utils/formatCommentDate';
import useNavigatePage from '../../hooks/useNavigatePage';

interface Props {
memberId: number;
imageUrl: string;
nickname: string;
skillList: string[];
createdAt: string;
}

const CommentProfileInfo = ({ imageUrl, nickname, skillList, createdAt }: Props) => {
const CommentProfileInfo = ({ memberId, imageUrl, nickname, skillList, createdAt }: Props) => {
const { goProfilePage } = useNavigatePage();
return (
<Flex gap={10}>
<Box width={36} height={36} overflow="hidden" borderRadius="0 150px 150px 0">
<Box
width={36}
height={36}
overflow="hidden"
borderRadius="0 150px 150px 0"
onClick={() => goProfilePage(memberId)}
cursor="pointer"
>
<ImageView src={imageUrl} alt="프로필" defaultSrc={PNGDefaultProfileInfo36} />
</Box>
<Flex paddingTop={2} direction="column" gap={4}>
Expand Down
37 changes: 23 additions & 14 deletions src/pages/FeedDetail/components/Comments.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Box, Divider } from 'concept-be-design-system';
import { Box, Divider, SVGProfileMessageDots } from 'concept-be-design-system';
import { Fragment, useRef } from 'react';

import Comment from './Comment';
import WriteComment from './WriteComment';
import EmptyTabContentSection from '../../Profile/components/EmptyTabContentSection';
import { useMemberInfoQuery } from '../../Profile/hooks/queries/useMemberInfoQuery';
import { getUserId } from '../../Profile/utils/getUserId';
import useCommentsQuery from '../hooks/queries/useCommentsQuery';
Expand All @@ -22,19 +23,27 @@ const Comments = ({ feedId }: Props) => {
return (
<Box padding="20px 22px">
<WriteComment feedId={feedId} myImageUrl={myImageUrl} myNickname={myNickname} />
{comments.map((comment, idx) => (
<Fragment key={comment.parentCommentId}>
<Comment
comment={comment}
feedId={feedId}
myImageUrl={myImageUrl}
myNickname={myNickname}
mySkillList={mySkillList}
/>
{idx !== comments.length - 1 ? <Divider color="l3" /> : <></>}
</Fragment>
))}
<div ref={intersectionRef}></div>
{comments.length > 0 ? (
comments.map((comment, idx) => (
<Fragment key={comment.parentCommentId}>
<Comment
comment={comment}
feedId={feedId}
myImageUrl={myImageUrl}
myNickname={myNickname}
mySkillList={mySkillList}
/>
{idx !== comments.length - 1 ? <Divider color="l3" /> : <></>}
</Fragment>
))
) : (
<>
<div ref={intersectionRef}></div>
<EmptyTabContentSection svg={SVGProfileMessageDots} textList={['', '아직 작성된 댓글이 없어요.']} />
</>
)}
{/* 무한 스크롤이 간헐적으로 되지 않는 문제 때문에 생긴 중복 분기 처리 로직입니다. 문제 해결시 리팩토링 예정 */}
{comments.length > 0 && <div ref={intersectionRef}></div>}
</Box>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/pages/FeedDetail/components/Recomment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const Recomment = ({
myImageUrl,
myNickname,
recomment: {
memberId,
childCommentId,
profileImageUrl,
nickname,
Expand Down Expand Up @@ -73,6 +74,7 @@ const Recomment = ({
</div>
<Box margin="0 0 20px 0">
<CommentProfileInfo
memberId={memberId}
imageUrl={profileImageUrl}
nickname={nickname}
skillList={memberSkills}
Expand Down
88 changes: 76 additions & 12 deletions src/pages/FeedDetail/contexts/CommentFocusContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useContext, useState, createContext, MutableRefObject, useRef, ReactNode } from 'react';

import { useMobileViewRefContext } from '../../../layouts/contexts/MobileViewContext';

interface CommentFocusContextType {
isFocusComment: boolean;
openCommentTextarea: () => void;
Expand All @@ -17,49 +19,111 @@ interface Props {
children: ReactNode;
}

const CommentFocusContext = createContext<CommentFocusContextType | null>(null);
interface FocusUsingKeyboardHeightProps {
textareaRef: MutableRefObject<HTMLTextAreaElement | null>;
mobileViewRef: MutableRefObject<HTMLElement | null>;
keyboardCurrent: number;
isComment?: boolean;
}

const focusTextareaRef = (textareaRef: MutableRefObject<HTMLTextAreaElement | null>) => {
if (!textareaRef.current) return;
const CommentFocusContext = createContext<CommentFocusContextType | null>(null);

textareaRef.current.focus();
textareaRef.current.scrollIntoView({ block: 'center', behavior: 'smooth' });
const isIphone = /ip/i.test(navigator.userAgent.toLowerCase());
const INIT_KEYBOARD_HEIGHT = 280;
const FOCUSING_DIFFERENCE = 1.4;

const focusUsingKeyboardHeight = ({
textareaRef,
mobileViewRef,
keyboardCurrent,
isComment,
}: FocusUsingKeyboardHeightProps) => {
if (!textareaRef.current || !mobileViewRef.current) return;

const textareaRefCurrent = textareaRef.current;
const mobileViewRefCurrent = mobileViewRef.current;
const textareaRect = textareaRefCurrent.getBoundingClientRect();
const innerHeight = window.innerHeight;
const keyboardHeight = keyboardCurrent || INIT_KEYBOARD_HEIGHT;
const elementAbsolutePosition = mobileViewRefCurrent.scrollTop + textareaRect.top;

textareaRefCurrent.focus();

// focus() 동작 완료 이후 포커싱 로직 동작토록 의도적으로 비동기 상황으로 수행
const timerId = setTimeout(() => {
clearTimeout(timerId);

// 가상 키보드 내에 댓글 및 답글 입력창이 가려질 가능성이 있는 경우에만 포커싱 로직 동작
if (innerHeight - textareaRect.top >= keyboardHeight) return;

// 댓글 입력창 및 IOS 디바이스는 아래 포커싱 로직으로 동작
if (isIphone && isComment) {
mobileViewRefCurrent.scroll({
top: elementAbsolutePosition - innerHeight + keyboardHeight * FOCUSING_DIFFERENCE,
behavior: 'smooth',
});
return;
}

// 답글 입력창 및 IOS 외 모든 디바이스는 아래 포커싱 로직으로 동작
textareaRefCurrent.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}, 0);
};

const initTextareaRef = (textareaRef: MutableRefObject<HTMLTextAreaElement | null>) => {
const resetTextareaFocus = (textareaRef: MutableRefObject<HTMLTextAreaElement | null>) => {
textareaRef.current = null;
};

export const CommentFocusProvider = ({ children }: Props) => {
const { mobileViewRef, keyboardHeightRef } = useMobileViewRefContext();
const [isFocusComment, setIsFocusComment] = useState<boolean>(false);
const commentTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const recommentTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const editCommentTextareaRef = useRef<HTMLTextAreaElement | null>(null);

/* --- 댓글 입력창 포커싱 및 초기화 --- */
const openCommentTextarea = () => {
focusTextareaRef(commentTextareaRef);

setIsFocusComment(true);

focusUsingKeyboardHeight({
textareaRef: commentTextareaRef,
mobileViewRef,
keyboardCurrent: keyboardHeightRef.current,
isComment: true,
});
return;
};

const closeCommentTextarea = () => {
setIsFocusComment(false);
};

/* --- 대댓글 입력창 포커싱 및 초기화 --- */
const focusRecommentTextarea = () => {
focusTextareaRef(recommentTextareaRef);
focusUsingKeyboardHeight({
textareaRef: recommentTextareaRef,
mobileViewRef,
keyboardCurrent: keyboardHeightRef.current,
isComment: false,
});
};

const initRecommentTextarea = () => {
initTextareaRef(recommentTextareaRef);
resetTextareaFocus(recommentTextareaRef);
};

/* --- 댓글 및 대댓글 수정 입력창 포커싱 및 초기화 --- */
const focusEditCommentTextarea = () => {
focusTextareaRef(editCommentTextareaRef);
focusUsingKeyboardHeight({
textareaRef: editCommentTextareaRef,
mobileViewRef,
keyboardCurrent: keyboardHeightRef.current,
isComment: false,
});
};

const initEditCommentTextarea = () => {
initTextareaRef(editCommentTextareaRef);
resetTextareaFocus(editCommentTextareaRef);
};

return (
Expand Down
3 changes: 3 additions & 0 deletions src/pages/FeedDetail/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface FeedDetailResponse {
memberId: number;
imageUrl: string;
nickname: string;
skillList: string[];
Expand All @@ -20,6 +21,7 @@ export interface FeedDetailResponse {
}

export interface CommentParentResponse {
memberId: number;
parentCommentId: string;
nickname: string;
profileImageUrl: string;
Expand All @@ -35,6 +37,7 @@ export interface CommentParentResponse {
}

export interface CommentChildResponse {
memberId: number;
childCommentId: string;
nickname: string;
profileImageUrl: string;
Expand Down

0 comments on commit 222a625

Please sign in to comment.