-
Notifications
You must be signed in to change notification settings - Fork 10
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
base: master
Are you sure you want to change the base?
Conversation
… 30분 간격 그리고 날짜가 바뀔 때 시간 렌더링)
제가 시간이 없어서 프로젝트 코드는 못 볼거 같은데, key question이 너무 잘 작성된 것 같아요! 이번 과제도 너무 수고 많으셨습니다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
컴포넌트 분리를 잘 하시는 것 같습니다!!
내부 로직도 더 분리해서 가독성을 더 챙겨도 좋을 것 같아요
{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]; // 현재 메시지의 이모지 가져오기 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 함수도 handleSendMessage 밖으로 로직을 분리해도 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 민재님!
이번 코드리뷰를 맡은 송유선입니다. 🤍
민재님 코드리뷰를 이전에도 한 적이 있는데, 그 때도 느꼈었지만 매번 열심히 공부하면서 코드를 작성해 주시는 것 같아요. 주석도 꼼꼼하게 달아주셔서 확인하기 편했습니다! 채팅방도 지난 번 과제보다 더욱 발전해서 인상깊었어요~~ 이모지 반응, 검색, 정렬 등 많은 기능을 시도하신 점이 좋았습니다👍🏻 시험 기간동안 과제하시느라 너무 고생 많으셨고 저희 다음 과제도 함께 파이팅해요!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 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.
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로 작성되어 있어서 유지보수 시 어떤 컴포넌트를 다루는지 파일명만으로 파악하기 어려울 수 있을 것 같아요. 파일 이름에 해당 컴포넌트의 역할을 나타내 주시면, 나중에 코드를 다시 볼 때 훨씬 이해하기 쉬울 것 같습니다!
// 검색어에 따라 채팅 목록 필터링 | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
검색 기능 구현하셨네요! 이름에 따라 잘 검색됩니다 짱! 👍🏻
이건 사실 중요한 건 아니긴 한데, 개인적으로 채팅방 목록 위에 검색창이 있으니까 뭔가... 내용도 검색되는 것처럼 느껴지더라구요..! 그런데 이 검색창은 이름만 필터링해주니까 사용자 관점에서 봤을 때 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; // 내림차순으로 정렬 | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마지막 멘트 시점을 기준으로 정렬해주시는 부분 좋아요~~!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이모지로 반응을 달 수 있도록 하신 부분이 너무 귀엽네요!! 다양한 경우를 고려하려고 노력하신 것 같아요 👍🏻
근데 이 파일은 Chat의 전반적인 내용을 담고 있는데 이모지 부분이 너무 많은 비중을 차지하고 있는 것 같아요! 다른 파일에서 정의한 다음에 컴포넌트로 불러오시는 건 어떨까요?
position: absolute; // 부모 요소에 상대적으로 위치 고정 | ||
bottom: 0; // 부모 요소의 하단에 고정 | ||
left: 0; // 왼쪽도 고정 | ||
right: 0; // 오른쪽도 고정 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
폴더가 많이 중첩되어 있어서 상대경로가 복잡하네요..! 다음엔 절대 경로를 사용해보시는 것도 좋을 것 같아요~~ 저도 파일이 많아지면서 절대 경로의 필요성을 좀 느꼈답니다,,, (근데 저도 후반에 바꾸기엔 너무 복잡해져서... 다음을 기약했..습니다...)
There was a problem hiding this 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로 구분하는 게 조금은 가독성이 떨어지는 부분도 있는 것 같아서 프로젝트의 복잡도가 올라갔을 때 다른 방법도 함께 적용되면 더 좋은 코드가 될 것 같습니다! 고생많으셨습니다. 많이 배워갑니다!
There was a problem hiding this 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로 컴포넌트를 구분했을 때 장단점이 있는 걸로 알고 있습니다! 어떤 장점을 부각시키기 위해 사용하셨는지 궁금해요:)
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메세지가 입력하는 순대로 기록되게 했을 때 보여주는 로직을 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.
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; |
There was a problem hiding this comment.
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)); | ||
|
||
// 메시지가 그룹의 첫 번째 메시지인지 확인 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분에서 메시지의 그룹을 나누고, 각 메시지의 위치를 파악하는 이유가 무엇인지 궁금합니다!
<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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이부분에서는 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
border-radius 구분하시는 로직에 박수를 보내드리고 싶습니다...
onKeyDown={(e) => { | ||
if (e.key === 'Enter') { | ||
handleSendMessage(); // 엔터 키를 눌렀을 때 메시지 전송 | ||
} | ||
}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마지막 글자가 같이 쳐지는 상황이 발생합니다! 이 부분은 KeyDown을 사용해서인데 KeyUp으로 오류를 해결할 수 있습니다. 톡방에 성준님께서 올려주신 포스트 한번 참고해보시는 것도 좋을 것 같아요
✨배포 링크
🎨 피그마 링크
😍 구현 기능
채팅 목록 페이지, 친구 목록 페이지 (홈) 구현했습니다. 스토리 페이지는 디자이너 분께서 디자인하지 않으셔서 간단히 로딩 스피너 이용해서 서비스 준비 중이라고 띄워놓았습니다 :) 약간 다행인 부분... ㅎ..ㅎ
친구 목록 페이지
채팅 목록 페이지
채팅방
BottomNav 컴포넌트, Battery Bar
🥹 이번 미션을 수행하며 느낀 점
뭐든지 미리미리 해놔야한다는 걸 매 미션 때마다 느끼면서... 매 미션 때마다 미룬이가 되는 걸 반복하고 있습니다...
또 지난번 미션이 아무리 촉박했다 한들 조금 더 신경 써서 구현해 놨었더라면 지금이 더 편하지 않았을까 하는 아쉬움도 있습니다. 최대한 피그마와 동일하게 하려 했지만… 안 읽은 메세지 같은 요소를 구현하지 못하고 코딩 컨벤션도 급한 마음에 잘 못 지킨 게 아쉬움이 남습니다 ㅜㅜ 변수명도 급하다 보니 마음대로 막 적은 것 같네용...
그래도 이번 미션에서 전역 상태 관리를 제대로 쓰고 이해한 것 같아 뿌듯한 마음이 듭니다 ㅎㅎ 전역 상태 관리로 사용해봤던 recoil 말고 다른 걸 써볼까 하다가 지금 recoil도 얕게 이해하고 있다는 걸 깨달아 이것부터 제대로 공부하고 넘어가자는 마음에 recoil을 사용했습니다. userData와 ChatData를 일부러 미션 마지막 즈음까지 useState만을 써서 컴포넌트별로 독립적으로 fetch해서 사용했었는데 이렇게 하다보니 상태를 중앙에서 한 번에 관리하는 것의 편안함을 그 어느 때보다 뼈져리게 느꼈고 지역 상태 관리와 전역 상태 관리의 차이점도 보다 더 잘 이해할 수 있어 좋았습니다. 🥰
이번 미션으로 채팅 미션은 끝날 것 같지만 좀 더 살피며 계속해서 유지보수 하고 싶은 마음이 듭니다.
미친듯이 날카로운 피드백 부탁드립니다 👍🏻🔥
🔎 Key Question
노션에 정리해 두었습니다! ✨
다들 이번 과제도 수고 많으셨습니다!! 🩷🩷