Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4주차] 지민재 미션 제출합니다. #14

Open
wants to merge 27 commits into
base: master
Choose a base branch
from

Conversation

mimizae
Copy link

@mimizae mimizae commented Nov 2, 2024

✨배포 링크
🎨 피그마 링크

image

😍 구현 기능

  • 채팅 목록 페이지, 친구 목록 페이지 (홈) 구현했습니다. 스토리 페이지는 디자이너 분께서 디자인하지 않으셔서 간단히 로딩 스피너 이용해서 서비스 준비 중이라고 띄워놓았습니다 :) 약간 다행인 부분... ㅎ..ㅎ

  • 친구 목록 페이지

    • 프로필 사진을 클릭하면 사이드바가 띄워지고, 검색창에 user 이름을 검색하면 필터링 되어 아래에 띄워지게 했습니다. 보여지는 user의 수를 계산해 리스트 위에 작게 표시되게 했습니다.
    • user을 클릭하면 user와 나(진나경)과의 채팅 페이지로 이동되며 mockChatData.json에 저장해 두었던 대화 내용이 보여집니다.
    • 부가적으로 전화 아이콘은 마우스 호버 시 약간 커지는 스타일을 주었습니다.
  • 채팅 목록 페이지

    • 친구 목록 페이지에서 쓴 헤더를 가져가 쓰고 (이때 헤더를 공통 컴포넌트화 했습니다.) 동일하게 이름을 검색하면 그 이름을 가진 user와 대화한 채팅이 보여집니다.
    • ActiveStatus 부분은 각 uesr의 프로필 사진에 z-index를 이용해 초록색 점을 사진 위에 띄워 현활 중임을 나타냈습니다. 이 ActiveStatus의 프로필 사진을 클릭했을 때 역시 그 user와의 채팅방으로 이동됩니다.
    • ChatList 컴포넌트에서는 로컬 스토리지에 저장된 사용자와의 마지막 채팅을 렌더링 하게 했습니다. (이때 recoil의 상태 관리와 useEffect 비동기와의 얽힘이 저를 힘들게 했습니다......) 또 가장 최근에 대화 나눈 user와의 채팅 리스트가 상단에 올라오도록 sorting 했습니다. 아직 안 읽은 메세지 표시는 구현하지 못했습니다... ㅠㅠ 실시간 채팅이 아닌 환경에서 안 읽은 것은 어떻게... 구현해야 할지 모르겠어서 혹시 이런 기능을 구현하신 분은 저에게 조언을 주세요... ㅜㅜ 😵‍💫
  • 채팅방

    • 이 채팅방 부분은 지난 과제였는데, 지난 과제 마감일이 촉박하기도 했고 축제와도 겹쳐있었어서 거의 80프로 하드코딩에... user와 Chat 데이터를 엉터리로... 저장했었어서 이것을 리팩토링해 다시 코드를 짜는 게 가장 힘들었습니다... 🫨🫨
    • 지난 과제에서는 유저가 뿐이었어서 그것을 바탕으로 코드를 짜뒀으니 아무리 새로운 유저를 생성해도 user1, 2로 하드코딩 되어있었기에 프로필 사진을 클릭하든 새로운 메세지를 보내든 버그 투성이었어서 해결하는 데 애를 먹었습니다 🥹
    • 지난번에는 감정 이모지를 다는 기능을 구현하지 못했는데 이번 과제에 구현했습니다. 로컬 스토리지에 이모지 상태를 저장해 새로 고침해도 데이터가 유지되도록 했습니다. 그냥 클릭 시 Emoji Picker가 나타나고 더블 클릭 시 이모지를 삭제하게 했습니다. 이때 두 클릭 이벤트가 겹치지 않도록 이벤트 전파를 막았습니다.
    • 첫 번째 메세지, 중간 메세지, 마지막 메세지를 props로 상태를 전달해 border-radius를 다르게 주어 인스타그램 DM처럼 구현했습니다.
    • 30분마다 채팅을 보내는 시각이 렌더링 되게 했습니다. 이때 메세지를 그룹화 해서 시간이 렌더링 되기 전, 렌더링 된 후를 구분하려 했는데… 실패했습니다… ㅜㅜ 메세지를 연달아 세 개를 보낼 때 만약 두 번째 메세지를 보내고 난 직후 시간이 렌더링 된다면 메세지의 순서가 초기화 되어야 하는데 이어져 border-radius가 의도한 대로 되진 않습니다… 😭
  • BottomNav 컴포넌트, Battery Bar

    • 친구, 채팅, 스토리 등 아이콘을 컴포넌트화 해서 색을 props로 전달해 조건부로 특정 페이지에서 색이 변하게 했습니다.
    • 디자이너 님께서 배터리 바나 아래의 작대기…?? 같은 건 구현하지 않아도 된다고 해주셔서 😍 손 놓고 있었는데 다른 팀원 분들의 디자인을 보니 이게 있는 게 훨씬 보기 좋아보여 ㅎㅎ 구현했습니다!!

🥹 이번 미션을 수행하며 느낀 점

뭐든지 미리미리 해놔야한다는 걸 매 미션 때마다 느끼면서... 매 미션 때마다 미룬이가 되는 걸 반복하고 있습니다...
또 지난번 미션이 아무리 촉박했다 한들 조금 더 신경 써서 구현해 놨었더라면 지금이 더 편하지 않았을까 하는 아쉬움도 있습니다. 최대한 피그마와 동일하게 하려 했지만… 안 읽은 메세지 같은 요소를 구현하지 못하고 코딩 컨벤션도 급한 마음에 잘 못 지킨 게 아쉬움이 남습니다 ㅜㅜ 변수명도 급하다 보니 마음대로 막 적은 것 같네용...
그래도 이번 미션에서 전역 상태 관리를 제대로 쓰고 이해한 것 같아 뿌듯한 마음이 듭니다 ㅎㅎ 전역 상태 관리로 사용해봤던 recoil 말고 다른 걸 써볼까 하다가 지금 recoil도 얕게 이해하고 있다는 걸 깨달아 이것부터 제대로 공부하고 넘어가자는 마음에 recoil을 사용했습니다. userData와 ChatData를 일부러 미션 마지막 즈음까지 useState만을 써서 컴포넌트별로 독립적으로 fetch해서 사용했었는데 이렇게 하다보니 상태를 중앙에서 한 번에 관리하는 것의 편안함을 그 어느 때보다 뼈져리게 느꼈고 지역 상태 관리와 전역 상태 관리의 차이점도 보다 더 잘 이해할 수 있어 좋았습니다. 🥰
이번 미션으로 채팅 미션은 끝날 것 같지만 좀 더 살피며 계속해서 유지보수 하고 싶은 마음이 듭니다.
미친듯이 날카로운 피드백 부탁드립니다 👍🏻🔥

🔎 Key Question

노션에 정리해 두었습니다! ✨

다들 이번 과제도 수고 많으셨습니다!! 🩷🩷

@Programming-Seungwan
Copy link

제가 시간이 없어서 프로젝트 코드는 못 볼거 같은데, key question이 너무 잘 작성된 것 같아요!
작성된 내용이 react에서 어떠한 문제를 해결하기 위해 나온 것이고 추후에 사용할 nextJS에서는 어떻게 적용되는지 공부를 킾 고잉하셔두 좋을 것 같아요.
우선, react에서는 민재님께서 잘 아시는 것처럼 react-router-dom 라이브러리를 통해 정적, 동적 라우팅을 진행합니다. 하지만 nextJS에서는 이것을 디렉터리 구조를 통해서 내부적으로 알아서 구현하기 때문에 개발자의 편의성이 더 좋아요. 또한 좋은 UX / UI를 위한 방법으로 제시해주신 바와 같이 lazy loading을 위한 code spliting 역시 알아서 지원해주고요. 여기에 CDN과 SSG, SSR 같은 방식까지 설명을 덧붙여 주셨는데, 이것이 배포와 어떤 연관이 있는지까지 살펴보시는 것을 추천합니다. reactJS는 하나의 파일만을 빌드하고 사용자에게 바로 빈 html 파일과 js를 전송하죠? 그래서 리액트는 S3라는 aws의 스토리지 서비스에 배포해도 되지만 nextJS는 백엔드 기능까지 지원하므로 단순 스토리지가 아닌 "서버 인스턴스(그냥 컴퓨터라고 보시면 됩니다)"를 통해 배포하기 때문에 완전히 달라진다고 말씀드리고 싶어요.
마지막으로 상태 관리 역시 너무 잘 작성해주신 것 같아요. 민재님께서도 useReducer() 훅을 사용해 보셨을텐데, 어찌보면 상태를 선언해주는 로직과 이를 변경해주는 코드가 분리되어 있다는 느낌이 드셨을 것 같은데요 저는 redux에 이게 잘 드러나있다고 생각합니다(사실 redux라는 이름이 reduce + flux 의 줄임말입니다 🙃). 이러한 상태 관리 라이브러리도 nextJS 같은 서버 사이드 렌더링을 지원하는 기술에서는 사용 양상이 다소 달라지구요. 이 과정에서 네트워크 관련 지식이 중요하게 개입되니 관련 공부도 잘 해나가시면 좋겠습니다(CS는 디비 + 컴네 + 운체 라는 말을 사람들이 괜히 하는게 아니야!🤣).

이번 과제도 너무 수고 많으셨습니다.

Copy link

@westofsky westofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트 분리를 잘 하시는 것 같습니다!!
내부 로직도 더 분리해서 가독성을 더 챙겨도 좋을 것 같아요

Comment on lines +114 to +140
{messages.map((msg, index) => {
const isMyMessage = msg.userId === currentUserId; // 메시지가 현재 사용자의 것인지 확인

const currentTime = new Date(msg.time); // 현재 메시지의 시간
const previousTime = index > 0 ? new Date(messages[index - 1].time) : null; // 이전 메시지의 시간

// 메시지의 시간 표시 여부 결정
const shouldShowTime = index === 0 ||
(previousTime && currentTime.toLocaleDateString() !== previousTime.toLocaleDateString()) ||
(previousTime && (currentTime.getTime() - previousTime.getTime() > 1800000)) ||
(previousTime && (msg.userId === messages[index - 1].userId &&
currentTime.getTime() - previousTime.getTime() > 10 * 1000));

// 메시지가 그룹의 첫 번째 메시지인지 확인
const isFirstMessage = shouldShowTime || index === 0 || messages[index - 1]?.userId !== msg.userId;

const isLastBeforeTimeMessage = shouldShowTime && index > 0 && messages[index - 1]?.userId === msg.userId;

const isLastOtherMessage =
!isMyMessage &&
(isLastBeforeTimeMessage || index === messages.length - 1 || messages[index + 1]?.userId === currentUserId);

const isGroupEnd = isLastBeforeTimeMessage || index === messages.length - 1 || messages[index + 1]?.userId !== msg.userId;

const isMiddleMessage = !isFirstMessage && !isGroupEnd; // 중간 메시지인지 확인

const emoji = selectedEmoji[index]; // 현재 메시지의 이모지 가져오기

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsx내 복잡한 로직은 따로 함수로 분리하면 좋을 것 같아요

const TopNavBar: React.FC<{ opponentUserId: number, currentUserId: number }> = ({ opponentUserId, currentUserId }) => {
const navigate = useNavigate();
const userData = useRecoilValue(userDataState); // atom에서 사용자 데이터 가져오기
const [, setCurrentUserId] = useRecoilState(currentUserIdState);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [, setCurrentUserId] = useRecoilState(currentUserIdState);
const setCurrentUserId = useSetRecoilState(currentUserIdState);

recoil에 이런 setter도 있습니다~

Comment on lines +80 to +110
const timeoutId = setTimeout(() => {
const receivedMessage1: MessageProps = { userId: opponentUserId, content: "세오스 20기", time: new Date().toISOString() };
const updatedMessagesWithFirstResponse: MessageProps[] = [...updatedMessages, receivedMessage1];
setMessages(updatedMessagesWithFirstResponse);

// 로컬 스토리지와 Recoil 상태에 저장
localStorage.setItem(`chatMessages-${chatId}`, JSON.stringify(updatedMessagesWithFirstResponse));
setChatData((prevChatData) => ({
...prevChatData,
[chatId!]: {
...prevChatData[chatId!],
messages: updatedMessagesWithFirstResponse,
},
}));

setTimeout(() => {
const receivedMessage2:MessageProps = { userId: opponentUserId, content: "FE 파이팅 🩷🩷", time: new Date().toISOString() };
const updatedMessagesWithSecondResponse: MessageProps[] = [...updatedMessagesWithFirstResponse, receivedMessage2];
setMessages(updatedMessagesWithSecondResponse);

// 로컬 스토리지와 Recoil 상태에 최종 메시지 저장
localStorage.setItem(`chatMessages-${chatId}`, JSON.stringify(updatedMessagesWithSecondResponse));
setChatData((prevChatData) => ({
...prevChatData,
[chatId!]: {
...prevChatData[chatId!],
messages: updatedMessagesWithSecondResponse,
},
}));
}, 2000);
}, 2000);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 함수도 handleSendMessage 밖으로 로직을 분리해도 좋을 것 같아요

Copy link

@s-uxun s-uxun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 민재님!
이번 코드리뷰를 맡은 송유선입니다. 🤍

민재님 코드리뷰를 이전에도 한 적이 있는데, 그 때도 느꼈었지만 매번 열심히 공부하면서 코드를 작성해 주시는 것 같아요. 주석도 꼼꼼하게 달아주셔서 확인하기 편했습니다! 채팅방도 지난 번 과제보다 더욱 발전해서 인상깊었어요~~ 이모지 반응, 검색, 정렬 등 많은 기능을 시도하신 점이 좋았습니다👍🏻 시험 기간동안 과제하시느라 너무 고생 많으셨고 저희 다음 과제도 함께 파이팅해요! ☺️

Comment on lines +9 to +31
const BottomNav: React.FC = () => {
const location = useLocation();

return (
<BottomNavContainer>
<MenuLayout>
<NavItem as={Link} to="/" $active={location.pathname === "/"} >
<FriendIcon color={location.pathname === "/" ? "#1675FF" : "#72787F"} /> {/* 경로에 따라 색상 변경 */}
<Menu color={location.pathname === "/" ? "#1675FF" : "#72787F"}>친구</Menu>
</NavItem>
<NavItem as={Link} to="/chat" $active={location.pathname === "/chat"}>
<ChatIcon color={location.pathname === "/chat" ? "#1675FF" : "#72787F"} /> {/* 경로에 따라 색상 변경 */}
<Menu color={location.pathname === "/chat" ? "#1675FF" : "#72787F"}>채팅</Menu>
</NavItem>
<NavItem as={Link} to="/story" $active={location.pathname === "/story"}>
<StroyIcon color={location.pathname === "/story" ? "#1675FF" : "#72787F"} /> {/* 스토리 아이콘은 항상 회색으로 설정 */}
<Menu color={location.pathname === "/story" ? "#1675FF" : "#72787F"} >스토리</Menu>
</NavItem>
</MenuLayout>
<HomeIndicator/>
</BottomNavContainer>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 path에 따라 색상이 변하는 하단 메뉴 바를 구현하셨네요! 사용자가 있는 페이지 위치에 따라 활성화된 색상이 변하는 기능을 잘 구현해주신 것 같다는 생각이 듭니다. 다만 중복된 부분이 꽤 많이 보이는 것 같아요. 아래와 같이 map을 활용해서 더 간결하게 작성해보면 어떨까요? :)

Suggested change
const BottomNav: React.FC = () => {
const location = useLocation();
return (
<BottomNavContainer>
<MenuLayout>
<NavItem as={Link} to="/" $active={location.pathname === "/"} >
<FriendIcon color={location.pathname === "/" ? "#1675FF" : "#72787F"} /> {/* 경로에 따라 색상 변경 */}
<Menu color={location.pathname === "/" ? "#1675FF" : "#72787F"}>친구</Menu>
</NavItem>
<NavItem as={Link} to="/chat" $active={location.pathname === "/chat"}>
<ChatIcon color={location.pathname === "/chat" ? "#1675FF" : "#72787F"} /> {/* 경로에 따라 색상 변경 */}
<Menu color={location.pathname === "/chat" ? "#1675FF" : "#72787F"}>채팅</Menu>
</NavItem>
<NavItem as={Link} to="/story" $active={location.pathname === "/story"}>
<StroyIcon color={location.pathname === "/story" ? "#1675FF" : "#72787F"} /> {/* 스토리 아이콘은 항상 회색으로 설정 */}
<Menu color={location.pathname === "/story" ? "#1675FF" : "#72787F"} >스토리</Menu>
</NavItem>
</MenuLayout>
<HomeIndicator/>
</BottomNavContainer>
);
};
const navItems = [
{ path: "/", label: "친구", Icon: FriendIcon },
{ path: "/chat", label: "채팅", Icon: ChatIcon },
{ path: "/story", label: "스토리", Icon: StoryIcon },
];
// 우선 이렇게 주요 아이템들을 정의합니다.
const BottomNav: React.FC = () => {
const location = useLocation();
return (
<BottomNavContainer>
<MenuLayout>
{navItems.map(({ path, label, Icon }) => {
const isActive = location.pathname === path;
const color = isActive ? "#1675FF" : "#72787F";
// 위에서 정의한 아이템들을 map을 활용해 순회하면서 현재 path에 해당하는 걸 찾도록 합니다. 맞는 건 active 상태로 정의하고 색상을 할당해요.
return (
<NavItem as={Link} to={path} $active={isActive} key={path}>
<Icon color={color} />
<Menu color={color}>{label}</Menu>
</NavItem>
// 그럼 이렇게 가독성 좋은 코드로 간단하게 쓸 수 있습니당!
);
})}
</MenuLayout>
<HomeIndicator/>
</BottomNavContainer>
);
};

@@ -0,0 +1,33 @@
import React from "react";
import { ActiveStatusLayout, StatusWrapper, Photo, Name, StatusContainer, StatusDot } from "./style";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

민재님께서 이번에 해주신 과제에서는 전부 index.tsx와 style.tsx를 나눠서 작성해주셨더라구요! 파일을 나누신 이유가 있을까요? 물론 코딩 스타일은 개인적인 취향이 반영될 수 있지만, 저는 개인적으로 CSS-in-JS 방식의 가장 큰 장점이 스타일과 로직을 한 파일에서 관리할 수 있다는 점이라고 생각해요. 파일을 분리하면 스타일을 수정할 때 두 파일을 오가야 하기에 번거로울 수 있지 않을까 싶은 생각이 들었습니다. 만약 styled-components를 사용하신다면, 컴포넌트 파일 하단에 스타일을 정의해 보시는 것도 추천드려요!

또 한 가지, 현재 폴더 구분을 깔끔하게 잘 해주셨는데, 파일명이 전부 index.tsx와 style.tsx로 작성되어 있어서 유지보수 시 어떤 컴포넌트를 다루는지 파일명만으로 파악하기 어려울 수 있을 것 같아요. 파일 이름에 해당 컴포넌트의 역할을 나타내 주시면, 나중에 코드를 다시 볼 때 훨씬 이해하기 쉬울 것 같습니다!

Comment on lines +20 to +24
// 검색어에 따라 채팅 목록 필터링
const filteredChatRooms = Object.keys(chatRooms).filter((chatId) => {
const chat = chatRooms[chatId];
return chat.users.some((user) => user.name.toLowerCase().includes(searchTerm.toLowerCase()));
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검색 기능 구현하셨네요! 이름에 따라 잘 검색됩니다 짱! 👍🏻

이건 사실 중요한 건 아니긴 한데, 개인적으로 채팅방 목록 위에 검색창이 있으니까 뭔가... 내용도 검색되는 것처럼 느껴지더라구요..! 그런데 이 검색창은 이름만 필터링해주니까 사용자 관점에서 봤을 때 placeholder로 '이름으로 검색' 이런 거 넣어주면 더욱 좋을 것 같아욥

Comment on lines +27 to +36
const sortedChatRooms = filteredChatRooms.sort((a, b) => {
const lastMessageA = chatRooms[a].messages[chatRooms[a].messages.length - 1];
const lastMessageB = chatRooms[b].messages[chatRooms[b].messages.length - 1];

// 메시지가 없을 경우 처리
const timeA = lastMessageA ? new Date(lastMessageA.time).getTime() : 0;
const timeB = lastMessageB ? new Date(lastMessageB.time).getTime() : 0;

return timeB - timeA; // 내림차순으로 정렬
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마지막 멘트 시점을 기준으로 정렬해주시는 부분 좋아요~~!

Comment on lines +27 to +107
const emojiList = ['👍🏻', '🩷', '😍', '😄', '😯', '😢', '😡'];

// Chats 컴포넌트 정의
const Chats = forwardRef<HTMLDivElement, ChatProps>(({ currentUserId, opponentUserId, messages, getProfileImage }, ref) => {
const [selectedEmoji, setSelectedEmoji] = useState<{ [key: number]: string }>({}); // 선택된 이모지 상태
const [visibleEmojiPicker, setVisibleEmojiPicker] = useState<{ [key: number]: boolean }>({}); // 이모지 피커의 가시성 상태

// 컴포넌트가 마운트될 때 로컬 스토리지에서 이모지 가져오기
useEffect(() => {
const storedEmojis = localStorage.getItem('selectedEmojis');
if (storedEmojis) {
setSelectedEmoji(JSON.parse(storedEmojis)); // 저장된 이모지 상태 업데이트
}
}, []);

// 선택된 이모지가 변경될 때 로컬 스토리지 업데이트
useEffect(() => {
if (Object.keys(selectedEmoji).length > 0) {
localStorage.setItem('selectedEmojis', JSON.stringify(selectedEmoji)); // 로컬 스토리지에 저장
}
}, [selectedEmoji]);

// 이모지 클릭 핸들러
const handleEmojiClick = (index: number, emoji: string) => {
setSelectedEmoji((prev) => ({ ...prev, [index]: emoji })); // 선택된 이모지 상태 업데이트
setVisibleEmojiPicker((prev) => ({ ...prev, [index]: false })); // 이모지 피커 숨기기
};

// 이모지 피커 토글 핸들러
const toggleEmojiPicker = (index: number) => {
setVisibleEmojiPicker((prev) => ({ ...prev, [index]: !prev[index] })); // 해당 인덱스의 이모지 피커 가시성 전환
};

// 이모지를 제거하는 함수
const handleEmojiDoubleClick = (index: number) => {
setSelectedEmoji((prev) => {
const newSelectedEmoji = { ...prev };
if (newSelectedEmoji[index]) { // 해당 인덱스의 이모지가 존재하는 경우
delete newSelectedEmoji[index]; // 이모지 제거
}
// 로컬 스토리지 업데이트
localStorage.setItem('selectedEmojis', JSON.stringify(newSelectedEmoji));
return newSelectedEmoji; // 새로운 상태 반환
});
};

// 이모지를 메시지 내용에 추가하는 함수
const updateMessageWithEmoji = (index: number, content: string, emoji?: string) => {
if (emoji) {
return `${content} ${emoji}`; // 이모지를 추가
}
return content; // 이모지가 없으면 원래 내용 반환
};

// 클릭과 더블 클릭을 구분하기 위한 타이머
let clickTimeout: NodeJS.Timeout | null = null;

// 메시지를 클릭할 때 이모지 피커를 토글하는 핸들러
const handleMessageClick = (event: React.MouseEvent<HTMLDivElement>, index: number) => {
event.preventDefault(); // 기본 클릭 동작 방지

// 클릭 이벤트가 발생했을 때 타이머 설정
if (clickTimeout) {
clearTimeout(clickTimeout); // 기존 타이머를 초기화
}

// 더블 클릭이 아니라면 이모지 피커를 토글
clickTimeout = setTimeout(() => {
toggleEmojiPicker(index);
}, 250); // 250ms 이내에 더블 클릭이 발생하지 않으면 클릭으로 간주
};
// 메시지를 더블 클릭할 때 이모지를 제거하는 이벤트 핸들러
const handleMessageDoubleClick = (index: number, event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation(); // 이벤트 전파를 막아 다른 클릭 이벤트가 실행되지 않게 함

if (clickTimeout) {
clearTimeout(clickTimeout); // 더블 클릭 시 클릭 타이머 초기화
clickTimeout = null; // 타이머를 null로 설정
}

handleEmojiDoubleClick(index); // 이모지 제거 처리
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이모지로 반응을 달 수 있도록 하신 부분이 너무 귀엽네요!! 다양한 경우를 고려하려고 노력하신 것 같아요 👍🏻

근데 이 파일은 Chat의 전반적인 내용을 담고 있는데 이모지 부분이 너무 많은 비중을 차지하고 있는 것 같아요! 다른 파일에서 정의한 다음에 컴포넌트로 불러오시는 건 어떨까요?

Comment on lines +8 to +11
position: absolute; // 부모 요소에 상대적으로 위치 고정
bottom: 0; // 부모 요소의 하단에 고정
left: 0; // 왼쪽도 고정
right: 0; // 오른쪽도 고정
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

position - absolute를 사용해서 인풋창을 채팅방 하단에 고정시켜 주셨네요! 우선 인풋창은 하단에 잘 고정되었는데, 아무래도 부모 요소에 위치를 고정시킨 것이다보니 아래 사진과 같은 문제가 있어요~

image

이 인풋창이 chat 컴포넌트 위에 있는데다가, 아이콘을 제외한 배경이 투명이라 저렇게 된 듯 합니다~~ 개인적인 생각으로는, 이미 chat 스타일에서 height: 645px;, overflow-y: auto;를 정의해주셨기 때문에 이 인풋을 굳이 absolute 포지션으로 작성하지 않고 삭제하셔도 좋을 것 같아요 ㅎㅎ 문제 없이 하단에 위치하면서도 chat을 input창 위까지로 지정해주기 때문에 input창을 넘어가는 일도, 자동 스크롤에 문제가 생길 일도 크게 없을 것 같습니다~!

Suggested change
position: absolute; // 부모 요소에 상대적으로 위치 고정
bottom: 0; // 부모 요소의 하단에 고정
left: 0; // 왼쪽도 고정
right: 0; // 오른쪽도 고정

import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom';
import phone from '../../../../assets/ChatRoom/phone.svg';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

폴더가 많이 중첩되어 있어서 상대경로가 복잡하네요..! 다음엔 절대 경로를 사용해보시는 것도 좋을 것 같아요~~ 저도 파일이 많아지면서 절대 경로의 필요성을 좀 느꼈답니다,,, (근데 저도 후반에 바꾸기엔 너무 복잡해져서... 다음을 기약했..습니다...)

Copy link

@yyj0917 yyj0917 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드에서 로직이 정말 많고, 디자인 하는 부분들이 디테일한 부분이 많아 구현하기 어려우셨을텐데 잘 구현되어있는 것 같아요! 정말 고생많으셨을 것 같습니다! 폴더와 파일이 많아지면서 index.tsx, style.tsx로 구분하는 게 조금은 가독성이 떨어지는 부분도 있는 것 같아서 프로젝트의 복잡도가 올라갔을 때 다른 방법도 함께 적용되면 더 좋은 코드가 될 것 같습니다! 고생많으셨습니다. 많이 배워갑니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index.tsx, style.tsx로 컴포넌트를 구분했을 때 장단점이 있는 걸로 알고 있습니다! 어떤 장점을 부각시키기 위해 사용하셨는지 궁금해요:)

Comment on lines +27 to +36
const sortedChatRooms = filteredChatRooms.sort((a, b) => {
const lastMessageA = chatRooms[a].messages[chatRooms[a].messages.length - 1];
const lastMessageB = chatRooms[b].messages[chatRooms[b].messages.length - 1];

// 메시지가 없을 경우 처리
const timeA = lastMessageA ? new Date(lastMessageA.time).getTime() : 0;
const timeB = lastMessageB ? new Date(lastMessageB.time).getTime() : 0;

return timeB - timeA; // 내림차순으로 정렬
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메세지가 입력하는 순대로 기록되게 했을 때 보여주는 로직을 1번부터 보여주게 하는 것과 정렬하는 방식에 어떤 차이가 있는지 궁금합니다.

Comment on lines +45 to +47
const lastMessage = chat.messages[chat.messages.length - 1]; // 마지막 메시지
const opponentId = chat.users.find((user) => user.id !== chat.users[0].id)?.id; // 상대방 ID
const opponentData = users.find((user) => user.id === opponentId); // 상대방 정보 찾기
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

읽음, 안 읽음 기능을 생각해봤을 때 저도 적용은 안 시켜봤지만, chatroom기준으로 채팅방에 입장했을 때를 기록해주는 boolean value가 있고, 이 값으로 처리를 해주는 방법도 생각해봤습니다!

};

// 클릭과 더블 클릭을 구분하기 위한 타이머
let clickTimeout: NodeJS.Timeout | null = null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 기능이 있는지 처음 알았습니다! 배워가겠습니다.

(previousTime && (msg.userId === messages[index - 1].userId &&
currentTime.getTime() - previousTime.getTime() > 10 * 1000));

// 메시지가 그룹의 첫 번째 메시지인지 확인
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에서 메시지의 그룹을 나누고, 각 메시지의 위치를 파악하는 이유가 무엇인지 궁금합니다!

Comment on lines +143 to +189
<div key={index} style={{ display: 'flex', flexDirection: 'column', alignItems: isMyMessage ? 'flex-end' : 'flex-start' }}>
{shouldShowTime && (
<MessageTime>
{`${getDayLabel(currentTime)} ${currentTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`} {/* 메시지 시간 표시 */}
</MessageTime>
)}
<div onClick={(event) => handleMessageClick(event, index)} style={{ cursor: 'pointer', position: 'relative' }}>
{isMyMessage ? ( // 내 메시지일 경우
<MyMessage
$isFirstMessage={isFirstMessage}
$isGroupEnd={isGroupEnd}
$isMiddleMessage={isMiddleMessage}
$hasEmoji={!!emoji} // 이모지가 있는지 확인하여 전달
onDoubleClick={(event) => handleMessageDoubleClick(index, event)} // 더블 클릭 시 이모지 제거
>
{updateMessageWithEmoji(index, msg.content)}
{emoji && <MymessageEmoji >{emoji}</MymessageEmoji>} {/* 메시지 위에 이모지 표시 */}
</MyMessage>
) : ( // 다른 사용자의 메시지일 경우
<OtherMessageContainer
$hasProfileImg={isLastOtherMessage}
$isGroupEnd={isGroupEnd}
>
{getProfileImage(isLastOtherMessage ? index : index - 1)}
<OtherMessage
$isFirstMessage={isFirstMessage}
$isMiddleMessage={isMiddleMessage}
$isGroupEnd={isGroupEnd}
$hasEmoji={!!emoji} // 이모지가 있는지 확인하여 전달
onDoubleClick={(event) => handleMessageDoubleClick(index, event)} // 더블 클릭 시 이모지 제거
>
{updateMessageWithEmoji(index, msg.content)}
{emoji && <OtherMessageEmoji >{emoji}</OtherMessageEmoji>} {/* 메시지 위에 이모지 표시 */}
</OtherMessage>
</OtherMessageContainer>
)}
</div>
{visibleEmojiPicker[index] && ( // 이모지 피커가 보이는 경우
<EmojiPicker>
{emojiList.map((emoji, emojiIndex) => (
<Emoji key={emojiIndex} onClick={() => handleEmojiClick(index, emoji)}> {/* 이모지 클릭 시 추가 */}
{emoji}
</Emoji>
))}
</EmojiPicker>
)}
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분에서는 style-component가 아닌 일반 태그를 사용해주셨는데 다른 이유가 있나요?! 이모지와 메세지 시간을 설정하는 부분에서 고생이 많으셨을 것 같습니다!

Comment on lines +24 to +28
border-radius: ${({ $isFirstMessage, $isGroupEnd, $isMiddleMessage }) => {
if ($isFirstMessage) return '16px 16px 4px 16px'; // 첫 번째 메시지
if ($isGroupEnd) return '16px 4px 16px 16px'; // 마지막 메시지
if ($isMiddleMessage) return '16px 4px 4px 16px'; // 중간 메시지
return '16px'; // 기본 값
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

border-radius 구분하시는 로직에 박수를 보내드리고 싶습니다...

Comment on lines +36 to +40
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSendMessage(); // 엔터 키를 눌렀을 때 메시지 전송
}
}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마지막 글자가 같이 쳐지는 상황이 발생합니다! 이 부분은 KeyDown을 사용해서인데 KeyUp으로 오류를 해결할 수 있습니다. 톡방에 성준님께서 올려주신 포스트 한번 참고해보시는 것도 좋을 것 같아요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants