-
Notifications
You must be signed in to change notification settings - Fork 11
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주차] 신동현 미션 제출합니다. #21
base: master
Are you sure you want to change the base?
Conversation
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": "react-jsx" | ||
}, |
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": "react-jsx" | |
}, | |
"jsx": "react-jsx", | |
"baseUrl": "src", | |
}, |
위와 같이 baseUrl 속성을 지정해주면, 컴포넌트나 메소드를 임포트할 때 src 이후의 절대경로로 불러올 수 있어요! 예를 들어,
import bigIcon from "../../assets/images/BigIcon.svg";
이렇게 임포트한 부분을
import bigIcon from "assets/images/BigIcon.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.
오 이런 방법이..!! 앞으로는 이런식으로 해봐야겠네요~! 감사합니다 😊
function getFilteredUsers(users: User[], searchTerm: string) { | ||
if (!searchTerm) return users; | ||
const filteredUsers = users.filter((user) => | ||
user.name.toLowerCase().includes(searchTerm.toLowerCase()) | ||
); | ||
return filteredUsers; | ||
} |
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.
같은(비슷한) 기능을 하는 함수는 따로 빼서 다른 파일에 정의해주는 것도 좋을 것 같아요!
최대한 추상화해서 다양한 상황에서 두루 쓰일 수 있게끔 하면 구현에도 편리하고 관심사분리도 잘 이뤄질 수
있을 것이라고 생각해요~~
filter를 사용해서 검색된 유저를 가져오고, lower case로 검사하는 디테일 좋은 것 같습니다!!
) as ChatData; | ||
|
||
// 모든 사용자의 마지막 메시지와 시간 가져오기 | ||
const lastMessage: Record<string, Message | 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.
Record 타입은 한 번도 사용해본 적 없는데 적절한 상황에서 잘 사용하신 것 같아요 저도 써보겠습니당~~
return { | ||
...user, | ||
lastMessageTimestamp: lastChat | ||
? new Date(lastChat.timestamp) // 마지막 메시지가 있으면 해당 시간을 가져옴 | ||
: new Date(0), // 마지막 timestamp 없으면 기본 시간으로 예외처리 | ||
}; |
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.
lastMessage를 따로 관리할 필요 없이 요 return 값 안에 각각의 lastMessage를 포함했어도 괜찮았을 것 같아요!
const lastMessageUnread: Record<string, Message | null> = {}; | ||
// 모든 사용자의 마지막 메시지에 대한 unread 상태 업데이트 : | ||
// 마지막 sender가 내(신동현)가 아닌 경우 unread 로 처리 | ||
userData.users.forEach((user) => { | ||
const lastMsg = lastMessage[user.id]; | ||
if (lastMsg) { | ||
lastMessageUnread[user.id] = { | ||
...lastMsg, | ||
unread: lastMsg.sender !== "신동현", | ||
}; | ||
} | ||
}); |
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 savedChats = JSON.parse( | ||
localStorage.getItem("chatMessages") || JSON.stringify(chatData) | ||
) as ChatData; | ||
|
||
savedChats[chatId] = updatedMessages; | ||
localStorage.setItem("chatMessages", JSON.stringify(savedChats)); |
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.
채팅방별로 데이터를 관리하는 것이라면 하나의 key value 쌍으로 localStorage에 저장하지 않고 각각 분리해서 저장했어도 괜찮았을 것 같아요! key를 chatMessages_${상대유저이름}와 같은 방식으로 지정하면 어렵지 않게 구현할 수 있을 것 같습니당
지금은 chatMessages안의 모든 data를 불러오고 새롭게 저장하는 과정이 있기 때문에 불필요한 데이터 로딩이 클 것 같아요!
물~론 저는 모든 메시지들을 한 번에 관리해서 지금 동현님 한 것 처럼 한 번에 불러오도록 하긴 했지만요ㅋㅋ.. 근데 채팅방 별로 관리하면 충분히 분리해서 저장할 수 있을 것 같습니다!
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 messageContainers = messages.map((message: Message, index) => { | ||
const isCurrentUser = message.sender === nowUser.name; | ||
|
||
return ( | ||
<Message | ||
key={message.id} | ||
sender={message.sender} | ||
content={message.content} | ||
nowUser={nowUser.name} | ||
showIcon={message.showIcon && !isCurrentUser} | ||
/> | ||
); | ||
}); |
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.
showIcon을 판단할 때, 위처럼 따로 로직을 만들 수도 있지만, map 안에서 index를 통해 다음 message의 sender와 비교하여 sender가 바뀌는 시점에 showIcon을 true로 설정해주는 방법을 사용해도 좋을 것 같아요!
const messageContainers = messages.map((message: Message, index) => { | |
const isCurrentUser = message.sender === nowUser.name; | |
return ( | |
<Message | |
key={message.id} | |
sender={message.sender} | |
content={message.content} | |
nowUser={nowUser.name} | |
showIcon={message.showIcon && !isCurrentUser} | |
/> | |
); | |
}); | |
const messageContainers = messages.map((message: Message, index) => { | |
const isCurrentUser = message.sender === nowUser.name; | |
return ( | |
<Message | |
key={message.id} | |
sender={message.sender} | |
content={message.content} | |
nowUser={nowUser.name} | |
showIcon={message.sender !== messages[index + 1].sender && !isCurrentUser} | |
/> | |
); | |
}); |
이런 식으로요!
<Search src={searchIcon} /> | ||
<Record src={recordIcon} /> |
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.
이런 컴포넌트 네이밍을 할 때 용도를 조금 더 드러내서 네이밍해도 괜찮을 것 같아요! Search나 Record만 이게 아이콘의 기능을 하는지, Search input의 기능을 하는지 버튼의 기능을하는지 알기 쉽지 않을 수 있거든요!
function formatTimestamp(isoString?: string) { | ||
if (!isoString) return "No last message"; | ||
|
||
const date = new Date(isoString); | ||
date.setHours(0, 0, 0, 0); | ||
|
||
const now = new Date(); | ||
now.setHours(0, 0, 0, 0); | ||
|
||
const diffTime = now.getTime() - date.getTime(); | ||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); | ||
|
||
// 날짜 차이에 따른 문자열 반환 | ||
//오늘은 시간 정보, 전날은 "어제", 그 전날은 '~일전'으로 반환해줌 | ||
if (diffDays === 0) { | ||
const date = new Date(isoString); | ||
let hours = date.getHours(); | ||
const minutes = date.getMinutes(); | ||
const ampm = hours >= 12 ? "PM" : "AM"; | ||
hours = hours % 12; | ||
hours = hours ? hours : 12; | ||
const strHours = hours < 10 ? `0${hours}` : `${hours}`; | ||
const strMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`; | ||
return `${strHours}:${strMinutes} ${ampm}`; | ||
} else if (diffDays === 1) { | ||
return "어제"; | ||
} else { | ||
return `${diffDays}일 전`; | ||
} | ||
} | ||
|
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.
긴 시험기간동안 고생 많으셨습니다!!
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.
사소하지만 lowerCamelCase를 지켜서 일관성 있게 네이밍하면 좋을거 같아요!
:hover { | ||
cursor: pointer; |
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.
페이지 전체적으로 curosr가 포인터로 되어있는 거 같아요. 필요한 요소에만 적용된다면 더 좋을거 같습니다!
@media (max-width: 850px) { | ||
// 화면 너비가 850px 이하인 경우 (ipad 환경) | ||
width: 100%; | ||
max-width: 20%; // item이 한줄에 5개씩 오도록 | ||
} | ||
|
||
@media (max-width: 480px) { | ||
// 화면 너비가 480px 이하인 경우 (모바일 환경) | ||
width: 100%; | ||
max-width: 33%; // item이 한줄에 3개씩 오도록 | ||
margin-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.
반응형으로 처리해준 디테일이 좋아요!
{isInputFocused ? ( | ||
//handleSendClick 작동?? click 작동 | ||
<Send onClick={handleSendClick} /> |
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.
이 부분에서 onClick 보다 onBlur가 우선순위를 가지면서 클릭되기 전에 <Send>
가 사라져 클릭으로 메세지가 안 보내지는 이슈가 있는거 같아요. 저 같은 경우에는 onMouseDown으로 해결했습니다! 한번 읽어보시면 좋을거 같아요 ㅎㅎ
{isInputFocused ? ( | |
//handleSendClick 작동?? click 작동 | |
<Send onClick={handleSendClick} /> | |
{isInputFocused ? ( | |
//handleSendClick 작동?? click 작동 | |
<Send onMouseDown={handleSendClick} /> |
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.
헉 그렇군요!!! 사실 계속 안돼서 구현을 못했던 부분인데, 참고해서 수정해야겠네요~!!! 알려주셔서 감사합니다~~🙇♀️ㅎㅎㅎ
userData.users.forEach((user) => { | ||
const lastMsg = lastMessage[user.id]; | ||
if (lastMsg) { | ||
lastMessageUnread[user.id] = { | ||
...lastMsg, | ||
unread: lastMsg.sender !== "신동현", | ||
}; | ||
} | ||
}); |
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.
�오 그게 더 좋은 플로우일 것 같네요~!! 시간이 되면 추가해보겠습니다 :) 감사합니당
배포링크
https://ceos-messenger.vercel.app/
디자인_피그마링크
<신경썼던 부분>
특히 친구 목록 페이지에서 어떻게 보여줄지 고민했는데, 모바일 & PC& 패드의 경우의 수 세가지로 나눠서 비율에 따라 친구를 보여줬습니다.
4주차 구현을 위해 추가적으로 메시지 전송 시간 정보를 추가하고, 기존 채팅방에 마지막 메시지에 아이콘 오류를 수정하였습니다.
사용자의 마지막 메시지를 저장하고 관리하는 법:
chatlist페이지에 채팅별 마지막 메시지를 가져오고, 업데이트 해야해서 이 부분에 대한 고민을 했습니다.
⇒ 마지막 메시지를 가져올때는 TS의 타입중 Record<K,T>를 사용해서 Record<string, Message | null>로 마지막 메시지의 시간, 메시지 content를 찾았습니다.
⇒ 예를 들어, 새 메시지가 도착하거나, 사용자가 특정 메시지를 읽었을 때 unread의 상태를 업데이트하여 해당 변화를 화면에 반영했습니다.
⇒ 이 마지막 메시지의 시간을 기준으로 가장 최근에 소통한 사람 리스트를 sorting 했습니다.
작동과정
1. profile 페이지
제일 먼저 뜨는 페이지로,
첫번째 아이템을 누르면 제 깃허브로 연동이 되고, 마지막 아이템을 누르면 채팅방 목록페이지 (2. chatlist 페이지) 으로 넘어갑니다.
2. chatlist 페이지
왼쪽이 답장 전, 오른쪽이 답장 보낸 후 화면입니다.chatlist 페이지는
각각의 채팅방을 누르면 chat 페이지 (지난과제에서 구현했었던) 으로 이동이 됩니다.
3. Friends 페이지
2페이지의 채팅방 목록의 상단에 있는 +Friends 요 버튼을 누르면 친구 목록 페이지로 넘어갑니다.
기능으로는 검색기능이 있습니다.채팅방에는 나를 포함한 유저 목록이 뜹니다.
고민했던 지점:
여러 채팅방의 데이터를 어떻게 관리할까 고민하다가 ChatData라는 인터페이스를 사용하였습니다. , 각 사용자 ID에 해당하는 메시지 배열을 관리하는 인터페이스로, 기존에 chat 페이지에서 사용한 Message를 상대방 userId: Message로 매핑한 구조를 만들어 local storage 에 저장하면서 사용했습니다.
interface ChatData {
[key: string]: Message[];
}
[Key Questions]
디자이너로부터 받은 QA 목록
[QA링크 (동현 & 예진)]
(https://www.notion.so/7647f04e81ea4b18a97f555e932670cd?pvs=21)
메세지 입력 시 메세지 입력창 변경
변경되어야할 점: 메세지 입력 시 Chat/Bottom Bar에서 Messaging 컴포넌트로 변경 부탁드립니다.
⇒ 메시지 입력시 input bar를 수정하였습니다. 입력할때는 오른쪽처럼 변경됩니다.
메세지 버블 정렬
마지막 메세지 버블들이 왼쪽으로 더 치우쳐 있습니다. 모든 버블들이 같은 레이아웃으로 정렬되도록 수정 부탁드립니다.
⇒ 마지막 메시지 버블이 치우친 문제도 margin 조정을 통하여 해결하였습니다. (아래는 수정한 사진 입니다!)
dhshin98@de442f5
dhshin98@fe2f338
Routing
어떤 네트워크 내에서 통신 데이터를 보낼 경로를 선택하는 일련의 과정, 경로를 정해주는 행위 자체와 그런 과정들을 다 포함하여 일컫는 말입니다.
SPA
SPA (Single Page Application)란 한 개(Single)의 Page로 구성된 Application이다. 현재의 페이지를 동적으로 다시 작성함으로써 사용자와 소통하는 웹 애플리케이션이나 웹사이트를 말한다. React는 SPA인데, 일반 웹 사이트처럼 URL에 따른 페이지 이동을 할 수 있게 해주는 기능이 React Router이다.
상태관리
상태관리는 useState와 useEffect 훅을 사용하여 상태 관리를 하였습니다. 기본적인 상태관리 함수를 사용하려고 했습니다. Recoil, Redux등의 전역상태관리를 해보고 싶었지만, 이번 과제에서는 진행하지 못한 것 또한 아쉬운 점인 것 같습니다.
느낀점
라우팅까지 구현하다보니까 재밌게 과제했습니다. 다만 코드의 재사용성에 대한 고민은 계속 해봐야할것 같습니다. 어찌저찌 구현을 한 뒤에도 코드가 안예쁜거 같아서 계속 더 고민하게 되는 것같습니다.
추가로, 디자이너 분께 필요하거나 궁금한 부분은 카톡으로 연락드렸고, 바로 답변주셔서 빠르게 소통이 가능했습니다.
추가 구현을 하게 된다면 상태메시지의 수정이 기능 같은 것이 있으면 좋겠다는 생각도 들었습니다.
계속 작업은 했는데 QA 수정을 하고 난 뒤로는 커밋을 틈틈히 못날려서 기능단위로 커밋을 못 날린게 아쉽습니다. 다음 과제부터는 꼭 바로바로 커밋을 날리겠습니다!!