-
Notifications
You must be signed in to change notification settings - Fork 10
[4주차] 손주완 과제 제출합니다. #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
base: main
Are you sure you want to change the base?
Conversation
only1Ksy
left a comment
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 DateSeparator = ({ date, isFirstMessage = false }: DateSeparatorProps) => { | ||
| return ( | ||
| <div className={`flex justify-center items-center ${isFirstMessage ? 'mb-4' : 'mt-8 mb-4'}`}> |
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 className={`flex justify-center items-center ${isFirstMessage ? 'mb-4' : 'mt-8 mb-4'}`}> | |
| <div className={`flex justify-center items-center mb-4 ${!isFirstMessage && 'mt-8'}`}> |
사소하지만 이렇게 하면 공통 스타일 분리할 수 있을 것 같네요!
| <div | ||
| className={`px-3 pt-[7px] pb-[6px] break-all flex items-center ${ | ||
| isMe | ||
| ? 'bg-white text-gray-7' |
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.
여기는 조건부 제거해도 되지 않을까요??
| ref={textareaRef} | ||
| value={message} | ||
| onChange={(e) => setMessage(e.target.value)} | ||
| onKeyPress={handleKeyPress} |
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.
onKeyPress는 deprecated 된 방법이라 React에서는 onKeyDown을 사용하길 권장한다고 하네요!
onKeyPress deprecated (onKeyPress를...
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.
우왕 이 부분까지 구현하셨네요 👍 👍
| <img src={Filter} alt="필터" className="w-[22px] h-[22px] text-gray-0" /> | ||
| </button> | ||
| <button className='cursor-pointer'> | ||
| <img src={Image} alt="이미지" className="w-[22px] h-[22px]" /> |
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.
전체적으로 svg 파일을 img로 불러와서 쓰고 계신데, svg를 컴포넌트화 하면 상태에 따라 UI 변화를 tailwind로 대응하기 더 쉬울 것 같습니다!
|
|
||
| interface ChatroomHeaderProps { | ||
| title?: string; | ||
| onBack?: () => void; |
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에서 back btn을 눌렀을 때, 현재 선택된 유저가 기본으로 돌아오면 좋을 것 같아요! 지금은 그대로 저장되는 것 같은데 각 채팅방마다 저장된 값이 달라서 헷갈리네용
|
|
||
| <div className="flex items-center gap-1"> | ||
| <span className="text-body3-r text-gray-6">2</span> | ||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none"> |
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.
다른 아이콘들과 다르게 이 부분만 inline svg로 삽입하신 이유가 있을까요?? 동일한 패턴으로 통일하면 좋을 것 같습니다!
| return ( | ||
| <div className="flex items-center justify-between w-[335px] mx-auto py-3"> | ||
| <span className="text-body3-m1 text-gray-6">LINE 서비스</span> | ||
| {/* arrow-down.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.
👍
| </div> | ||
| {/* 우측 아이콘들 */} | ||
| <div className='gap-[7px] flex justify-center items-center'> | ||
| <img src={Signal} alt="mobile signal" className={isProfilePage ? 'brightness-0 invert' : ''} /> |
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.
| <img src={Signal} alt="mobile signal" className={isProfilePage ? 'brightness-0 invert' : ''} /> | |
| <img src={Signal} alt="mobile signal"className={isProfilePage && 'brightness-0 invert'} /> |
| const lastMessage = messages[messages.length - 1]; | ||
| let lastMessageDate: Date | null = null; | ||
| if (lastMessage) { | ||
| const lastTimestamp = new Date(lastMessage.timestamp); | ||
| lastMessageDate = new Date(lastTimestamp.getFullYear(), lastTimestamp.getMonth(), lastTimestamp.getDate()); |
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.
이 lastMessageDate 값을 만드는 이유가 isSameDate에 넘겨주기 위함인 것 같은데, isSameDate 내부 로직을 보니 여기서 수행하는 new Date(lastTimestamp.getFullYear(), lastTimestamp.getMonth(), lastTimestamp.getDate());랑 동일한 로직으로 작동하는 것 같아요!
이 부분을 const lastMessage = messages[messages.length - 1]; 만 남겨두고,
아래 조건문에서 if (!lastMessage || !isSameDate(new Date(lastMessage.timestamp), today)) 이런 식으로 처리할 수도 있을 것 같습니당!
| users={users} | ||
| currentUserId={currentUserId} | ||
| /> | ||
| )} |
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.
친구 프로필 이미지를 불러오는 데 어려움이 있었는데, 저도 주완님처럼 개인 프로필과 그룹프로필 나눠서 불러오면 더 쉬웠을 거 같습니다! 저도 나중에 리팩토링할때 반영해볼게요 👍
| [ | ||
| { | ||
| "chatId": "1", | ||
| "chatType": "individual", |
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.
chatType key가 있어서 채팅방 데이터 불러올 때 좀 더 편할 수 있곘네요!👍
| userId={user.id} | ||
| showProfile={showProfile} | ||
| isLastInGroup={isLastInGroup} | ||
| onUserNameClick={onUserNameClick} |
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 | ||
| className="flex justify-center items-center px-3 py-[6px] rounded-[30px]" | ||
| style={{ | ||
| background: 'rgba(255, 255, 255, 0.20)', |
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.
왜 tailwind로 다 적용안하고 분리했는지 궁금합니당
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.
내 프로필에서는 프로필 편집 버튼이라 이 아이콘이 맞는데 친구 프로필에서는 음성통화 버튼에 이 아이콘이 들어가 있습니다! 기능에 맞는 아이콘을 사용하면 더 좋을 거 같아요..!
| // 채팅방에 입장하면 unreadCount를 0으로 설정 | ||
| if (roomId) { | ||
| updateUnreadCount(roomId, 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.
이 기능까지 개발하셨네요👍
| return otherParticipants || currentChatRoom.chatName; | ||
| } | ||
| }; | ||
|
|
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.
컴포넌트 폴더에 있는 ChatListItem 파일에서
getChatRoomName 이름의 함수랑 똑같은 함수가 반복되는 거 같습니다..!
|
|
||
| {/* 친구 섹션 */} | ||
| <div className="flex flex-col gap-4 py-4"> | ||
| <CollapsibleSection title="친구" count={friends.length}> |
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 deleteMessage = (messageId: string) => { | ||
| setMessages(prev => prev.filter(msg => msg.id !== messageId)); |
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.
메시지 삭제 기능까지 구현하신 게 인상적입니다
| // 3초 후 자동으로 닫기 | ||
| const timer = setTimeout(() => { | ||
| onClose(); | ||
| }, 3000); |
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.
전 자동으로 닫는 것까지는 생각을 못했는데 유저 입장에서 편리할 것 같습니다!
jungyungee
left a comment
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.
과제 너무 수고 많으셨습니다..!
너무 잘하셔서 보면서 깜짝 놀랐습니다...
읽지 않은 메세지 읽고 나면 읽지 않은 숫자 표시 사라지는 것이나, 메세지 보낸 순서에 따라 메세지 리스트 창에서 정렬되는 것 (최근 메세지가 가장 위로 오게 하는 것!) 넘 좋습니다..
프로필에서 채팅방 넘어가는 것이나, 전화페이지 로딩스피너 등도 사소하지만 UI 적으로 신경 많이 쓰신 걸 느꼈습니다...
코드나 구현하신 것 보면서 저는 왜 이렇게 하지 않았지,,하고 많이 배웠습니다
너무 수고 많으셨어요.!!!
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.
favicon을 적용한 것 좋습니다..!
| {!isMe && showProfile && ( | ||
| <div className="flex flex-col items-center mr-2"> | ||
| <div className="w-8 h-8 rounded-full overflow-hidden flex-shrink-0"> | ||
| <img | ||
| src={userProfile} | ||
| alt="profile" | ||
| className="w-full h-full object-cover" | ||
| /> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* 상대방 메시지에서 프로필이 없을 때 공간 확보 */} | ||
| {!isMe && !showProfile && ( |
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 sortedChatRooms = [...chatRooms].sort((a, b) => { | ||
| return new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime(); | ||
| }); | ||
|
|
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.
이렇게 되면 렌더링 시마다 새로 배열이 생성되어서 useMemo를 사용해서
const sortedChatRooms = useMemo(() => {
return [...chatRooms].sort((a, b) =>
new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime()
);
}, [chatRooms]);
이런식으로 하면 더 좋을 것 같습니다.!!
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.
zustand를 사용해서 상태 관리를 하신 점 너무 좋습니다..!!
| <button className="flex flex-col items-center gap-2 cursor-pointer"> | ||
| <div className="w-[60px] h-[60px] rounded-[34px] bg-[#57CE82] bg-opacity-20 flex items-center justify-center"> | ||
| <img src={EditIcon} alt="편집" className="w-7 h-7" /> | ||
| </div> | ||
| <span className="text-caption1-m text-white">프로필 편집</span> | ||
| </button> | ||
|
|
||
| {/* 나의 메모 */} | ||
| <button className="flex flex-col items-center gap-2 cursor-pointer"> | ||
| <div className="w-[60px] h-[60px] rounded-[34px] bg-[#57CE82] bg-opacity-20 flex items-center justify-center"> | ||
| <img src={MessageIcon} alt="메모" className="w-8 h-8 brightness-0 invert" /> | ||
| </div> | ||
| <span className="text-caption1-m text-white">나의 메모</span> | ||
| </button> |
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.
버튼들이 구조가 유사한 것 같아, 컴포넌트화하고 이벤트를 props로 넘기는 방식도 좋을 것 같습니다..!
또 아이콘을 img 말고 svg 컴포넌트화하면 좋을 것 같구요..!
| <div className="w-[375px] h-full bg-white shadow-2xl flex flex-col relative"> | ||
| <StatusBar /> | ||
| {/* 나머지 공간을 모두 차지하도록 main 영역 설정 + 세로 스크롤바 숨김 처리 */} | ||
| <main className="flex-1 flex flex-col overflow-y-hidden relative"> | ||
| <Outlet /> | ||
| {/* 통화 로딩 모달 */} | ||
| <CallLoadingModal | ||
| isOpen={isCallModalOpen} | ||
| onClose={closeCallModal} | ||
| /> | ||
| </main> | ||
| {/* Navbar - 프로필 페이지 제외, 통화 모달 뜨는 동안 숨김 */} | ||
| {!hideNavbar && !isCallModalOpen && <Navbar onCallClick={openCallModal} />} | ||
| </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.
layout을 공통 프레임으로 사용하고 으로 하위 페이지들 동적으로 렌더링되도록 하는 것 좋습니다..!
| const isHome = location.pathname === '/home'; | ||
| const isChat = location.pathname === '/' || location.pathname.startsWith('/chatroom'); |
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.
useLocation으로 현재 경로에서 열린 페이지에 맞게 하단바 디자인 표시될 수 있도록 한 점 좋습니다..!
| // textarea 높이 자동 조정 | ||
| const adjustTextareaHeight = () => { | ||
| const textarea = textareaRef.current; | ||
| if (!textarea) return; | ||
|
|
||
| // 높이를 초기화하여 scrollHeight를 정확히 계산 | ||
| textarea.style.height = 'auto'; | ||
|
|
||
| const scrollHeight = textarea.scrollHeight; | ||
| const maxHeight = 40; // 2줄 이상일 때의 고정 높이 | ||
|
|
||
| if (scrollHeight <= 36) { // 1줄 높이 | ||
| // 1줄: 기본 높이 | ||
| setIsMultiline(false); | ||
| textarea.style.height = 'auto'; | ||
| } else { | ||
| // 2줄 이상: 고정 높이 | ||
| setIsMultiline(true); | ||
| textarea.style.height = `${maxHeight}px`; | ||
| } | ||
| }; | ||
|
|
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 className={`flex flex-1 gap-2 ${isMultiline ? 'items-end' : 'items-center'}`}> | ||
| <div className={`flex flex-1 ${isMultiline ? 'items-end' : 'items-center'}`}> | ||
| {/* 입력창 */} | ||
| <div | ||
| className="bg-gray-2 rounded-[10px] flex flex-1 py-2 pl-4 pr-3" | ||
| > |
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의 중첩이 많아서 알아보기 어려운 점이 있는 것 같습니다..! 중복된 것들 없애고 묶으면 더 깔끔할 것 같아요..!
배포 링크
🔗배포 링크
💅피그마 링크
🗓️QA 진행 노션
구현 화면
홈 페이지

채팅 목록 페이지

프로필 페이지 - 나

통화 페이지

느낀 점
제가 26일 일요일 오후에 시험이 끝나서 조급하게 했습니다. 저번 리뷰에 달아주신 내용을 반영하지는 못했습니다..🥺
우측 상단에 보낸 메시지 삭제 버튼 클릭시, 로컬 스토리지에 있는 메시지들이 삭제되게 해서, 리뷰를 더 편하게 하실 수 있도록 했습니다. 이번 과제 리뷰도 잘 부탁드립니다 ㅎ
✨ 주요 기능
🏠 홈 화면: 내 프로필, 친구 목록, 그룹 목록을 확인할 수 있습니다. 각 항목은 섹션별로 접고 펼칠 수 있는 UI를 적용했습니다.
💬 채팅 목록: 진행 중인 모든 채팅방의 목록을 보여줍니다. 각 채팅방의 마지막 메시지와 시간을 기준으로 정렬되며, 읽지 않은 메시지 수도 표시됩니다.
🗣️ 채팅방: 개인 채팅과 그룹 채팅을 지원합니다.
실시간 메시지 전송: 메시지를 입력하고 전송하면 채팅 내역이 실시간으로 업데이트됩니다.
사용자 시점 변환: 채팅방 내에서 다른 참여자의 프로필 사진을 클릭하여 해당 사용자의 시점으로 대화를 볼 수 있는 기능을 구현했습니다.
데이터 영속성: 주고받은 메시지는 로컬 스토리지를 사용하여 저장되므로, 브라우저를 새로고침해도 대화 내용이 유지됩니다.
👤 프로필 화면: 사용자의 프로필 이미지, 이름, 상태 메시지를 표시합니다. 내 프로필과 다른 사람의 프로필에 따라 다른 액션 버튼(예: 프로필 편집, 1:1 채팅, 음성 통화)이 나타납니다.
📞 통화 모달: 음성 통화 버튼 클릭 시 통화 연결 중임을 나타내는 로딩 모달이 나타납니다.
상태 관리: Zustand를 사용하여 사용자, 채팅, 모달에 대한 상태를 전역으로 관리하여 컴포넌트 간의 데이터 흐름을 효율적으로 처리합니다.
📂 폴더 구조
프로젝트는 기능별로 명확하게 역할을 분리하여 다음과 같이 구성했습니다.
Review Questions
React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
정의
:기호로 동적 세그먼트 정의기본 사용법
참고 자료
네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?
UI/UX 디자인 전략
스켈레톤 스크린
점진적 로딩
명확한 피드백
오프라인 지원
기술적 최적화 방법
이미지 최적화
Code Splitting
데이터 캐싱
기타 최적화
참고 자료
React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.
지역 상태 관리
useState
useReducer
Context API
특징
장점
단점
적합 케이스
전역 상태 라이브러리
Zustand (가볍고 간단)
Redux Toolkit (대규모 앱)
선택 가이드
실전 팁
참고 자료