Conversation
… 30분 간격 그리고 날짜가 바뀔 때 시간 렌더링)
|
제가 시간이 없어서 프로젝트 코드는 못 볼거 같은데, key question이 너무 잘 작성된 것 같아요! 이번 과제도 너무 수고 많으셨습니다. |
westofsky
left a comment
There was a problem hiding this comment.
컴포넌트 분리를 잘 하시는 것 같습니다!!
내부 로직도 더 분리해서 가독성을 더 챙겨도 좋을 것 같아요
| {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]; // 현재 메시지의 이모지 가져오기 |
| const TopNavBar: React.FC<{ opponentUserId: number, currentUserId: number }> = ({ opponentUserId, currentUserId }) => { | ||
| const navigate = useNavigate(); | ||
| const userData = useRecoilValue(userDataState); // atom에서 사용자 데이터 가져오기 | ||
| const [, setCurrentUserId] = useRecoilState(currentUserIdState); |
There was a problem hiding this comment.
| const [, setCurrentUserId] = useRecoilState(currentUserIdState); | |
| const setCurrentUserId = useSetRecoilState(currentUserIdState); |
recoil에 이런 setter도 있습니다~
| 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); |
There was a problem hiding this comment.
이 함수도 handleSendMessage 밖으로 로직을 분리해도 좋을 것 같아요
s-uxun
left a comment
There was a problem hiding this comment.
안녕하세요 민재님!
이번 코드리뷰를 맡은 송유선입니다. 🤍
민재님 코드리뷰를 이전에도 한 적이 있는데, 그 때도 느꼈었지만 매번 열심히 공부하면서 코드를 작성해 주시는 것 같아요. 주석도 꼼꼼하게 달아주셔서 확인하기 편했습니다! 채팅방도 지난 번 과제보다 더욱 발전해서 인상깊었어요~~ 이모지 반응, 검색, 정렬 등 많은 기능을 시도하신 점이 좋았습니다👍🏻 시험 기간동안 과제하시느라 너무 고생 많으셨고 저희 다음 과제도 함께 파이팅해요!
| 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> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
현재 path에 따라 색상이 변하는 하단 메뉴 바를 구현하셨네요! 사용자가 있는 페이지 위치에 따라 활성화된 색상이 변하는 기능을 잘 구현해주신 것 같다는 생각이 듭니다. 다만 중복된 부분이 꽤 많이 보이는 것 같아요. 아래와 같이 map을 활용해서 더 간결하게 작성해보면 어떨까요? :)
| 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"; | |||
There was a problem hiding this comment.
민재님께서 이번에 해주신 과제에서는 전부 index.tsx와 style.tsx를 나눠서 작성해주셨더라구요! 파일을 나누신 이유가 있을까요? 물론 코딩 스타일은 개인적인 취향이 반영될 수 있지만, 저는 개인적으로 CSS-in-JS 방식의 가장 큰 장점이 스타일과 로직을 한 파일에서 관리할 수 있다는 점이라고 생각해요. 파일을 분리하면 스타일을 수정할 때 두 파일을 오가야 하기에 번거로울 수 있지 않을까 싶은 생각이 들었습니다. 만약 styled-components를 사용하신다면, 컴포넌트 파일 하단에 스타일을 정의해 보시는 것도 추천드려요!
또 한 가지, 현재 폴더 구분을 깔끔하게 잘 해주셨는데, 파일명이 전부 index.tsx와 style.tsx로 작성되어 있어서 유지보수 시 어떤 컴포넌트를 다루는지 파일명만으로 파악하기 어려울 수 있을 것 같아요. 파일 이름에 해당 컴포넌트의 역할을 나타내 주시면, 나중에 코드를 다시 볼 때 훨씬 이해하기 쉬울 것 같습니다!
| // 검색어에 따라 채팅 목록 필터링 | ||
| const filteredChatRooms = Object.keys(chatRooms).filter((chatId) => { | ||
| const chat = chatRooms[chatId]; | ||
| return chat.users.some((user) => user.name.toLowerCase().includes(searchTerm.toLowerCase())); | ||
| }); |
There was a problem hiding this comment.
검색 기능 구현하셨네요! 이름에 따라 잘 검색됩니다 짱! 👍🏻
이건 사실 중요한 건 아니긴 한데, 개인적으로 채팅방 목록 위에 검색창이 있으니까 뭔가... 내용도 검색되는 것처럼 느껴지더라구요..! 그런데 이 검색창은 이름만 필터링해주니까 사용자 관점에서 봤을 때 placeholder로 '이름으로 검색' 이런 거 넣어주면 더욱 좋을 것 같아욥
| 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; // 내림차순으로 정렬 | ||
| }); |
| 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); // 이모지 제거 처리 |
There was a problem hiding this comment.
이모지로 반응을 달 수 있도록 하신 부분이 너무 귀엽네요!! 다양한 경우를 고려하려고 노력하신 것 같아요 👍🏻
근데 이 파일은 Chat의 전반적인 내용을 담고 있는데 이모지 부분이 너무 많은 비중을 차지하고 있는 것 같아요! 다른 파일에서 정의한 다음에 컴포넌트로 불러오시는 건 어떨까요?
| position: absolute; // 부모 요소에 상대적으로 위치 고정 | ||
| bottom: 0; // 부모 요소의 하단에 고정 | ||
| left: 0; // 왼쪽도 고정 | ||
| right: 0; // 오른쪽도 고정 |
There was a problem hiding this comment.
position - absolute를 사용해서 인풋창을 채팅방 하단에 고정시켜 주셨네요! 우선 인풋창은 하단에 잘 고정되었는데, 아무래도 부모 요소에 위치를 고정시킨 것이다보니 아래 사진과 같은 문제가 있어요~
이 인풋창이 chat 컴포넌트 위에 있는데다가, 아이콘을 제외한 배경이 투명이라 저렇게 된 듯 합니다~~ 개인적인 생각으로는, 이미 chat 스타일에서 height: 645px;, overflow-y: auto;를 정의해주셨기 때문에 이 인풋을 굳이 absolute 포지션으로 작성하지 않고 삭제하셔도 좋을 것 같아요 ㅎㅎ 문제 없이 하단에 위치하면서도 chat을 input창 위까지로 지정해주기 때문에 input창을 넘어가는 일도, 자동 스크롤에 문제가 생길 일도 크게 없을 것 같습니다~!
| 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'; |
There was a problem hiding this comment.
폴더가 많이 중첩되어 있어서 상대경로가 복잡하네요..! 다음엔 절대 경로를 사용해보시는 것도 좋을 것 같아요~~ 저도 파일이 많아지면서 절대 경로의 필요성을 좀 느꼈답니다,,, (근데 저도 후반에 바꾸기엔 너무 복잡해져서... 다음을 기약했..습니다...)
yyj0917
left a comment
There was a problem hiding this comment.
코드에서 로직이 정말 많고, 디자인 하는 부분들이 디테일한 부분이 많아 구현하기 어려우셨을텐데 잘 구현되어있는 것 같아요! 정말 고생많으셨을 것 같습니다! 폴더와 파일이 많아지면서 index.tsx, style.tsx로 구분하는 게 조금은 가독성이 떨어지는 부분도 있는 것 같아서 프로젝트의 복잡도가 올라갔을 때 다른 방법도 함께 적용되면 더 좋은 코드가 될 것 같습니다! 고생많으셨습니다. 많이 배워갑니다!
There was a problem hiding this comment.
index.tsx, style.tsx로 컴포넌트를 구분했을 때 장단점이 있는 걸로 알고 있습니다! 어떤 장점을 부각시키기 위해 사용하셨는지 궁금해요:)
| 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; // 내림차순으로 정렬 | ||
| }); |
There was a problem hiding this comment.
메세지가 입력하는 순대로 기록되게 했을 때 보여주는 로직을 1번부터 보여주게 하는 것과 정렬하는 방식에 어떤 차이가 있는지 궁금합니다.
| 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); // 상대방 정보 찾기 |
There was a problem hiding this comment.
읽음, 안 읽음 기능을 생각해봤을 때 저도 적용은 안 시켜봤지만, chatroom기준으로 채팅방에 입장했을 때를 기록해주는 boolean value가 있고, 이 값으로 처리를 해주는 방법도 생각해봤습니다!
| }; | ||
|
|
||
| // 클릭과 더블 클릭을 구분하기 위한 타이머 | ||
| let clickTimeout: NodeJS.Timeout | null = null; |
| (previousTime && (msg.userId === messages[index - 1].userId && | ||
| currentTime.getTime() - previousTime.getTime() > 10 * 1000)); | ||
|
|
||
| // 메시지가 그룹의 첫 번째 메시지인지 확인 |
There was a problem hiding this comment.
이 부분에서 메시지의 그룹을 나누고, 각 메시지의 위치를 파악하는 이유가 무엇인지 궁금합니다!
| <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> |
There was a problem hiding this comment.
이부분에서는 style-component가 아닌 일반 태그를 사용해주셨는데 다른 이유가 있나요?! 이모지와 메세지 시간을 설정하는 부분에서 고생이 많으셨을 것 같습니다!
| 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'; // 기본 값 |
There was a problem hiding this comment.
border-radius 구분하시는 로직에 박수를 보내드리고 싶습니다...
| onKeyDown={(e) => { | ||
| if (e.key === 'Enter') { | ||
| handleSendMessage(); // 엔터 키를 눌렀을 때 메시지 전송 | ||
| } | ||
| }} |
There was a problem hiding this comment.
마지막 글자가 같이 쳐지는 상황이 발생합니다! 이 부분은 KeyDown을 사용해서인데 KeyUp으로 오류를 해결할 수 있습니다. 톡방에 성준님께서 올려주신 포스트 한번 참고해보시는 것도 좋을 것 같아요

✨배포 링크
🎨 피그마 링크
😍 구현 기능
채팅 목록 페이지, 친구 목록 페이지 (홈) 구현했습니다. 스토리 페이지는 디자이너 분께서 디자인하지 않으셔서 간단히 로딩 스피너 이용해서 서비스 준비 중이라고 띄워놓았습니다 :) 약간 다행인 부분... ㅎ..ㅎ
친구 목록 페이지
채팅 목록 페이지
채팅방
BottomNav 컴포넌트, Battery Bar
🥹 이번 미션을 수행하며 느낀 점
뭐든지 미리미리 해놔야한다는 걸 매 미션 때마다 느끼면서... 매 미션 때마다 미룬이가 되는 걸 반복하고 있습니다...
또 지난번 미션이 아무리 촉박했다 한들 조금 더 신경 써서 구현해 놨었더라면 지금이 더 편하지 않았을까 하는 아쉬움도 있습니다. 최대한 피그마와 동일하게 하려 했지만… 안 읽은 메세지 같은 요소를 구현하지 못하고 코딩 컨벤션도 급한 마음에 잘 못 지킨 게 아쉬움이 남습니다 ㅜㅜ 변수명도 급하다 보니 마음대로 막 적은 것 같네용...
그래도 이번 미션에서 전역 상태 관리를 제대로 쓰고 이해한 것 같아 뿌듯한 마음이 듭니다 ㅎㅎ 전역 상태 관리로 사용해봤던 recoil 말고 다른 걸 써볼까 하다가 지금 recoil도 얕게 이해하고 있다는 걸 깨달아 이것부터 제대로 공부하고 넘어가자는 마음에 recoil을 사용했습니다. userData와 ChatData를 일부러 미션 마지막 즈음까지 useState만을 써서 컴포넌트별로 독립적으로 fetch해서 사용했었는데 이렇게 하다보니 상태를 중앙에서 한 번에 관리하는 것의 편안함을 그 어느 때보다 뼈져리게 느꼈고 지역 상태 관리와 전역 상태 관리의 차이점도 보다 더 잘 이해할 수 있어 좋았습니다. 🥰
이번 미션으로 채팅 미션은 끝날 것 같지만 좀 더 살피며 계속해서 유지보수 하고 싶은 마음이 듭니다.
미친듯이 날카로운 피드백 부탁드립니다 👍🏻🔥
🔎 Key Question
노션에 정리해 두었습니다! ✨
다들 이번 과제도 수고 많으셨습니다!! 🩷🩷