-
Notifications
You must be signed in to change notification settings - Fork 4
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
[FEAT] 채팅방 컴포넌트 기본 퍼블리싱 #98
Changes from all commits
ee67a0f
bd40ea9
26d04a0
8a91fe6
4256ea7
4733490
695cdcd
c45aeb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import styled from 'styled-components'; | ||
import ThreePointIcon from '@assets/icons/three-point.svg'; | ||
import OutIcon from '@assets/icons/out.svg'; | ||
|
||
interface ChatHeaderProps { | ||
outBtnHandler?: () => void; | ||
} | ||
|
||
export const ChatHeader = ({ outBtnHandler }: ChatHeaderProps) => { | ||
return ( | ||
<ChatHeaderContainer> | ||
<HeaderBtn onClick={outBtnHandler}> | ||
<StyledIcon as={OutIcon} /> | ||
</HeaderBtn> | ||
<h2>채팅</h2> | ||
<HeaderBtn> | ||
<StyledIcon as={ThreePointIcon} /> | ||
</HeaderBtn> | ||
</ChatHeaderContainer> | ||
); | ||
}; | ||
export default ChatHeader; | ||
|
||
const ChatHeaderContainer = styled.div` | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
padding: 15px 15px; | ||
border-top: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']}; | ||
color: ${({ theme }) => theme.tokenColors['color-white']}; | ||
${({ theme }) => theme.tokenTypographys['display-bold20']}; | ||
`; | ||
|
||
const HeaderBtn = styled.button` | ||
display: flex; | ||
color: ${({ theme }) => theme.tokenColors['text-bold']}; | ||
cursor: pointer; | ||
`; | ||
|
||
const StyledIcon = styled.svg` | ||
width: 25px; | ||
height: 25px; | ||
cursor: pointer; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import styled, { css } from 'styled-components'; | ||
import SpeechBubbleIcon from '@assets/icons/speech-bubble.svg'; | ||
import QuestionIcon from '@assets/icons/question.svg'; | ||
import SendIcon from '@assets/icons/send.svg'; | ||
import { useRef, useEffect, useState } from 'react'; | ||
|
||
interface ChatInputProps { | ||
type: 'normal' | 'question'; | ||
} | ||
|
||
export const ChatInput = ({ type }: ChatInputProps) => { | ||
const [hasInput, setHasInput] = useState(false); | ||
const [isFocused, setIsFocused] = useState(false); | ||
const textareaRef = useRef<HTMLTextAreaElement | null>(null); | ||
|
||
useEffect(() => { | ||
const handleResize = () => { | ||
if (textareaRef.current) { | ||
textareaRef.current.style.height = '14px'; | ||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight - 5}px`; | ||
} | ||
}; | ||
|
||
Comment on lines
+17
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useRef로 채팅창 리사이징 하는 코드 좋네요 :) 'textareaRef.current' 자주 쓰이는것 같아서 변수로 따로 빼서 사용하는 것도 적절해 보입니다. 추후에 useAutoSize나 useChatInput 같이 리사이징이과 인풋 관련 로직의 관심사를 분리하는 방향으로 가면 좋을 것 같습니다! |
||
if (textareaRef.current) { | ||
textareaRef.current.addEventListener('input', handleResize); | ||
handleResize(); | ||
} | ||
|
||
return () => { | ||
if (textareaRef.current) { | ||
textareaRef.current.removeEventListener('input', handleResize); | ||
Check warning on line 31 in frontend/src/components/chat/ChatInput.tsx
|
||
} | ||
}; | ||
}, []); | ||
|
||
const handleBlur = () => { | ||
if (textareaRef.current) { | ||
setHasInput(textareaRef.current.value.length > 0); | ||
setIsFocused(false); | ||
} | ||
}; | ||
|
||
const handleFocus = () => { | ||
setIsFocused(true); | ||
}; | ||
|
||
return ( | ||
<ChatInputWrapper $hasInput={hasInput} $isFocused={isFocused}> | ||
<InputBtn aria-label={type}> | ||
{type === 'normal' ? <StyledIcon as={SpeechBubbleIcon} /> : <StyledIcon as={QuestionIcon} />} | ||
</InputBtn> | ||
<ChatInputArea | ||
ref={textareaRef} | ||
placeholder={`${type === 'normal' ? '채팅' : '질문'}을 입력해주세요`} | ||
onBlur={handleBlur} | ||
onFocus={handleFocus} | ||
/> | ||
<InputBtn aria-label="전송"> | ||
<StyledIcon as={SendIcon} /> | ||
</InputBtn> | ||
</ChatInputWrapper> | ||
); | ||
}; | ||
export default ChatInput; | ||
|
||
const ChatInputWrapper = styled.div<{ $hasInput: boolean; $isFocused: boolean }>` | ||
min-height: 20px; | ||
display: flex; | ||
padding: 5px 10px; | ||
gap: 10px; | ||
border: 3px solid ${({ theme }) => theme.tokenColors['text-weak']}; | ||
border-radius: 7px; | ||
background-color: transparent; | ||
|
||
${({ $isFocused, $hasInput }) => | ||
!$hasInput && | ||
!$isFocused && | ||
css` | ||
border: 3px solid transparent; | ||
background-color: #373a3f; | ||
`} | ||
`; | ||
|
||
const ChatInputArea = styled.textarea` | ||
width: 100%; | ||
max-height: 40px; | ||
overflow-y: auto; | ||
resize: none; | ||
border: none; | ||
outline: none; | ||
color: ${({ theme }) => theme.tokenColors['text-strong']}; | ||
${({ theme }) => theme.tokenTypographys['display-medium16']} | ||
background-color: transparent; | ||
`; | ||
|
||
const InputBtn = styled.button` | ||
display: flex; | ||
height: 25px; | ||
align-items: center; | ||
color: ${({ theme }) => theme.tokenColors['text-strong']}; | ||
cursor: pointer; | ||
`; | ||
|
||
const StyledIcon = styled.svg` | ||
width: 20px; | ||
height: 20px; | ||
cursor: pointer; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import styled from 'styled-components'; | ||
import QuestionCard from './QuestionCard'; | ||
import { useEffect, useRef } from 'react'; | ||
|
||
const sampleData = [ | ||
{ user: '고양이', message: 'ㅇㅅㅇ', type: 'normal' }, | ||
{ user: '강아지', message: 'ㅎㅇㅎㅇ', type: 'normal' }, | ||
{ | ||
user: '오리', | ||
message: | ||
'가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하', | ||
type: 'normal' | ||
} | ||
]; | ||
|
||
function getRandomBrightColor(): string { | ||
const hue = Math.floor(Math.random() * 360); | ||
const saturation = Math.floor(Math.random() * 50) + 50; | ||
const lightness = Math.floor(Math.random() * 30) + 50; | ||
|
||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`; | ||
} | ||
|
||
export const ChatList = () => { | ||
const chatListRef = useRef<HTMLDivElement | null>(null); | ||
|
||
useEffect(() => { | ||
if (chatListRef.current) { | ||
chatListRef.current.scrollTop = chatListRef.current.scrollHeight; | ||
} | ||
}, []); | ||
|
||
return ( | ||
<ChatListWrapper ref={chatListRef}> | ||
{[...Array(6)].map((_, i) => | ||
sampleData.map((chat, index) => ( | ||
<ChatItemWrapper key={`${i}-${index}`}> | ||
{chat.type === 'normal' ? ( | ||
<NormalChat $pointColor={getRandomBrightColor()}> | ||
<span className="text_point">{chat.user}</span> | ||
<span>{chat.message}</span> | ||
</NormalChat> | ||
) : ( | ||
<QuestionCard type="client" user={chat.user} message={chat.message} /> | ||
)} | ||
</ChatItemWrapper> | ||
)) | ||
)} | ||
</ChatListWrapper> | ||
); | ||
}; | ||
export default ChatList; | ||
|
||
const ChatListWrapper = styled.div` | ||
max-height: 100%; | ||
display: flex; | ||
flex-direction: column; | ||
overflow-y: auto; | ||
padding: 50px 20px 0 20px; | ||
scrollbar-width: none; | ||
`; | ||
|
||
const ChatItemWrapper = styled.div` | ||
margin-top: auto; | ||
padding: 5px 0; | ||
`; | ||
|
||
const NormalChat = styled.div<{ $pointColor: string }>` | ||
${({ theme }) => theme.tokenTypographys['display-medium14']}; | ||
color: ${({ theme }) => theme.tokenColors['color-white']}; | ||
.text_point { | ||
color: ${({ $pointColor }) => $pointColor}; | ||
margin-right: 5px; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { useState } from 'react'; | ||
import styled from 'styled-components'; | ||
import QuestionCard from './QuestionCard'; | ||
|
||
export const ChatQuestionSection = () => { | ||
const [expanded, setExpanded] = useState(false); | ||
|
||
const toggleSection = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 함수의 동작이 하나여서 아래 예시 처럼 작성하면 가독성이 더 좋아질 것 같아요!
추후 함수의 역할이 더 많아진다면 지금처럼 함수로 분리해도 좋은 것 같습니다 |
||
setExpanded((prev) => !prev); | ||
}; | ||
|
||
return ( | ||
<SectionContainer> | ||
<QuestionCard | ||
type="host" | ||
user="부스트" | ||
message="설명해주셨던 내용 중에 yarn-berry를 설정했던 방법이 인상깊었는데 거기서 생겼던 오류가 있나요?" | ||
/> | ||
{expanded && <QuestionCard type="host" user="라이부" message="다른 질문들" />} | ||
<SwipeBtn onClick={toggleSection} /> | ||
</SectionContainer> | ||
); | ||
}; | ||
export default ChatQuestionSection; | ||
|
||
const SectionContainer = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: center; | ||
min-height: 95px; | ||
padding: 13px 20px 0px 20px; | ||
gap: 10px; | ||
border-top: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']}; | ||
border-bottom: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']}; | ||
overflow: hidden; | ||
`; | ||
|
||
const SwipeBtn = styled.button` | ||
position: relative; | ||
width: 100%; | ||
height: 25px; | ||
cursor: pointer; | ||
|
||
&::before { | ||
content: ''; | ||
position: absolute; | ||
top: 35%; | ||
left: 50%; | ||
background-color: ${({ theme }) => theme.tokenColors['text-weak']}; | ||
border-radius: 2px; | ||
height: 5px; | ||
width: 50px; | ||
transform: translate(-50%, -50%); | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import styled from 'styled-components'; | ||
import ChatHeader from './ChatHeader'; | ||
import ChatInput from './ChatInput'; | ||
import ChatList from './ChatList'; | ||
import ChatQuestionSection from './ChatQuestionSection'; | ||
|
||
export const ChatRoom = () => { | ||
return ( | ||
<ChatRoomContainer> | ||
<ChatHeader /> | ||
|
||
<ChatQuestionSection /> | ||
|
||
<ChatListContainer> | ||
<ChatList /> | ||
</ChatListContainer> | ||
|
||
<ChatInputContainer> | ||
<ChatInput type="normal" /> | ||
</ChatInputContainer> | ||
</ChatRoomContainer> | ||
); | ||
}; | ||
export default ChatRoom; | ||
|
||
const ChatRoomContainer = styled.aside` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aside로 잡아주셔서 좋네요 :) |
||
display: flex; | ||
flex-direction: column; | ||
height: 100%; | ||
width: 380px; | ||
border-left: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']}; | ||
background: ${({ theme }) => theme.tokenColors['surface-default']}; | ||
`; | ||
|
||
const ChatListContainer = styled.div` | ||
display: flex; | ||
flex: 1; | ||
flex-direction: column; | ||
justify-content: end; | ||
overflow-y: auto; | ||
`; | ||
|
||
const ChatInputContainer = styled.div` | ||
padding: 10px 20px; | ||
`; |
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.
styled-components에서 as라는 것을 처음봤는데 좋네요!