src/components/Modal/ImageModal.jsx 생성
src/components/Modal/ImageModal.jsx
이미지 추가 모달을 구현하는 컴포넌트
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Input,
} from '@mui/material'
import React, { useCallback, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import '../../firebase'
import {
getDownloadURL,
getStorage,
ref as ref_storage,
uploadBytesResumable,
} from 'firebase/storage'
import { getDatabase, push, ref, serverTimestamp, set } from 'firebase/database'
import { useSelector } from 'react-redux'
export default function ImageModal({
open,
handleClose,
setPercent,
setUploading,
}) {
// channer, user 가져오기
const { channel, user } = useSelector(state => state)
// 가져온 파일 set
const [file, setFile] = useState(null)
// onChange로 추가된 파일 set
const onChangeAddFile = useCallback(e => {
const addedFile = e.target.files[0]
if (addedFile) setFile(addedFile)
}, [])
// 이미지를 추가한 메시지 생성
const createImageMessage = useCallback(
fileUrl => ({
timestamp: serverTimestamp(),
user: {
id: user.currentUser.uid,
name: user.currentUser.displayName,
avatar: user.currentUser.photoURL,
},
image: fileUrl,
}),
[
user.currentUser.uid,
user.currentUser.displayName,
user.currentUser.photoURL,
]
)
// 파일 업로드
const uploadFile = useCallback(() => {
setUploading(true) // 업로딩 중으로
const filePath = `chat/${uuidv4()}.${file.name.split('.').pop()}` // 경로 지정 (split으로 확장자와 분리 후 pop으로 확장자 제거)
// firebase에 업로드할 메소드
const uploadTask = uploadBytesResumable(
ref_storage(getStorage(), filePath),
file
)
// firebase에 업로드
// 3개의 observers를 등록해야함.
// 1. 'state_changed observer
const unsubscribe = uploadTask.on(
'state_changed',
// progress 구현
snap => {
const percentUploaded = Math.round(
(snap.bytesTransferred / snap.totalBytes) * 100
)
setPercent(percentUploaded)
},
// 2. error observer
error => {
console.error(error)
setUploading(false)
},
// 3. completion observer
async () => {
try {
const downloadUrl = await getDownloadURL(uploadTask.snapshot.ref)
await set(
push(ref(getDatabase(), 'messages/' + channel.currentChannel?.id)),
createImageMessage(downloadUrl)
)
setUploading(false)
unsubscribe()
} catch (error) {
console.error(error)
setUploading(false)
unsubscribe()
}
}
)
}, [
channel.currentChannel?.id,
createImageMessage,
file,
setPercent,
setUploading,
])
// onClick으로 메시지 보내기
const handleSendFile = useCallback(() => {
uploadFile() //파일 업로드
handleClose() // 창 닫기
setFile(null) // 파일 초기화
}, [handleClose, uploadFile])
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>이미지 보내기</DialogTitle>
<DialogContent>
<Input
margin="dense"
inputProps={{ accept: 'image/jpeg, image/jpg, image/png, image/gif' }}
type="file"
fullWidth
variant="standard"
onChange={onChangeAddFile}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>취소</Button>
<Button onClick={handleSendFile}>전송</Button>
</DialogActions>
</Dialog>
)
}
src/components/Chat/ChatMessage.jsx
hasOwnProperty() 메소드를 사용한다.
Object.prototype.hasOwnProperty() - JavaScript | MDN
해당 객체가 property를 가지고 있는지 확인해주는 메소드로 message내부에 image 프로퍼티가 있는지 확인해준다.
// message의 프로퍼티 중 image가 있는지 확인해주는 메소드
const IsImage = message => message.hasOwnProperty('image')
위에서 message에 image가 있는지 확인하여 3항연산자로 img태그를 출력하도록 한다.
{/* 이미지가 메시지에 있으면 img태그 출력 *
{IsImage(message) ? (
<img
alt="message"
src={message.image}
style={{ maxWidth: '100%' }}
/>
) : (
<ListItemText
align="left"
xs={{ wordBreak: 'break-all' }}
primary={message.content}
/>
)}
채팅 입력 컴포넌트에 ImageModal을 추가하고 관련 state들을 전달해준다. src/components/Chat/ChatInput.jsx
import {
Grid,
IconButton,
InputAdornment,
LinearProgress,
TextField,
} from '@mui/material'
import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon'
import ImageIcon from '@mui/icons-material/Image'
import SendIcon from '@mui/icons-material/Send'
import React, { useCallback, useState } from 'react'
import '../../firebase'
import { getDatabase, push, ref, serverTimestamp, set } from 'firebase/database'
import { useSelector } from 'react-redux'
import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react'
import ImageModal from '../Modal/ImageModal'
export default function ChatInput() {
// 채널 정보 가져오기
const { channel, user } = useSelector(state => state)
//message 상태
const [message, setMessage] = useState('')
//loading 상태
const [loading, setLoading] = useState(false)
//Emoji toggle
const [showEmoji, setShowEmoji] = useState(false)
//이미지 추가 모달 토글
const [imageModalOpen, SetImageModalOpen] = useState(false)
// 이미지 업로드
const [uploading, setUploading] = useState(false)
// 이미지 업로드 퍼센트
const [percent, setPercent] = useState(0)
// 이미지 모달 열고 닫기
const handleClickOpen = () => SetImageModalOpen(true)
const handleClickClose = () => SetImageModalOpen(false)
//이모지
const handleTogglePicker = useCallback(() => {
setShowEmoji(show => !show)
}, [])
//message onChange로 저장
const handleChange = useCallback(e => {
setMessage(e.target.value)
}, [])
// 메세지 생성
const createMessage = useCallback(
() => ({
timestamp: serverTimestamp(),
user: {
id: user.currentUser.uid,
name: user.currentUser.displayName,
avatar: user.currentUser.photoURL,
},
content: message,
}),
[
message,
user.currentUser.uid,
user.currentUser.displayName,
user.currentUser.photoURL,
]
)
// 메시지 전송
const clickSendMessage = useCallback(async () => {
// 메세지가 없을경우 방어
if (!message) return
setLoading(true)
// firebass realtimedatabase에 메세지 저장
try {
await set(
push(ref(getDatabase(), 'messages/' + channel.currentChannel.id)),
createMessage()
)
setLoading(false)
setMessage('')
} catch (error) {
console.error(error)
setLoading(false)
}
}, [message, channel.currentChannel?.id, createMessage])
// 이모지를 파싱하는 메소드
const handleSelectEmoji = useCallback(e => {
const sym = e.unified.split('-')
const codesArray = []
sym.forEach(el => codesArray.push('0x' + el))
const emoji = String.fromCodePoint(...codesArray)
setMessage(messageValue => messageValue + emoji)
}, [])
return (
<Grid container sx={{ p: '20px' }}>
<Grid item xs={12} sx={{ position: 'relative' }}>
{showEmoji && (
<Picker
data={data}
onEmojiSelect={handleSelectEmoji}
className="emojipicker"
title="이모지를 선택하세요."
emoji="point_up"
style={{ position: 'absolute', bottom: '20px', right: '20px' }}
theme="light"
/>
)}
<TextField
InputProps={{
startAdornment: (
<InputAdornment position="start">
<IconButton onClick={handleTogglePicker}>
<InsertEmoticonIcon />
</IconButton>
<IconButton onClick={handleClickOpen}>
<ImageIcon />
</IconButton>
</InputAdornment>
),
endAdornment: (
<InputAdornment position="start">
<IconButton disabled={loading} onClick={clickSendMessage}>
<SendIcon />
</IconButton>
</InputAdornment>
),
}}
autoComplete="off"
label="메세지 입력"
fullWidth
value={message}
onChange={handleChange}
/>
{uploading ? (
<Grid item xs={12} sx={{ m: '10px' }}>
<LinearProgress variant="determinate" value={percent} />
</Grid>
) : null}
<ImageModal
open={imageModalOpen}
handleClose={handleClickClose}
setPercent={setPercent}
setUploading={setUploading}
/>
</Grid>
</Grid>
)
}