/app/chat/
디렉토리 생성loading.tsx
복붙 생성ChatClient.tsx
생성/app/chat/page.tsx
import React from 'react'
import getCurrentUser from '../actions/getCurrentUser'
import ChatClient from './ ChatClient';
const ChatPage = async () => {
const currentUser = await getCurrentUser();
return (
<div>
<ChatClient currentUser={currentUser} />
</div>
)
}
export default ChatPage
/app/chat/ChatCLient.tsx
"use client";
import { User } from "@prisma/client";
import axios from "axios";
import React, { useEffect, useState } from "react";
interface ChatClientProps {
currentUser?: User | null;
}
const ChatClient = ({ currentUser }: ChatClientProps) => {
const [receiver, setReceiver] = useState({
receiverId: "",
receiverName: "",
receiverImage: "",
});
const [layout, setLayout] = useState(false);
return (
<main>
<div className="grid grid-cols-[1fr] md:grid-cols-[300px_1fr]">
{/* md보다 클때는 둘다 보여야함 */}
{/* md보다 작고 layout이 true일때는 contact안보임 */}
<section className={`md:flex ${layout && "hidden"}`}>
Contact Component
</section>
<section className={`md:flex ${!layout && "hidden"}`}>
Chat Component
</section>
</div>
</main>
);
};
export default ChatClient;
model User {
id String @id @default(cuid())
name String?
hashedPassword String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userType UserType @default(User)
favoriteIds String[]
products Product[]
conversations Conversation[]
sendMessages Message[] @relation("sender")
receivedMessages Message[] @relation("receiver")
}
...
model Conversation {
id String @id @default(cuid())
name String?
senderId String
receiverId String
users User[]
messages Message[]
createdAt DateTime @default(now())
}
model Message {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAp DateTime @updatedAt
text String?
image String?
sender User @relation(name: "sender", fields: [senderId], references: [id])
senderId String
receiver User @relation(name: "receiver", fields: [receiverId], references: [id])
receiverId String
conversation Conversation @relation(fields: [conversationId], references: [id])
conversationId String
}
npx prisma db push
여러가지
복잡한 네트워크에서 통신을 관리하기 위한 지침
Restful
유저가 요청을 보낼때만 서버가 응답하는 단방향 통신
브라우저가 서버에 요청 -> 새로고침 -> 데이터 받아옴
실시간 통신을 할때 주로 사용
서버가 요청을 받지 않아도 손님이나 드라이버에게 통신을 보낼 수 있음
즉시 데이터 받아옴
Rest API같은 경우는 계속 한방향으로 지속해서 요청을 보내야 데이터를 받을 수있다. 하지만 웹소켓의 경우 양방향으로 통신해서 바로 데이터를 받을 수 있음
클라이언트가 일정한 간격으로 서버에 요청을 보내서 결과를 전달받는 방식
이방법은 구현이 쉽다는 장점이 있지만 서버의 상태가 변하지 않았을때도 계속해서 요청을 보내서 받아와야하기에 필요 없는 요청이 많아지며, 또한 요청 간격을 정하기도 쉽지 않다.
const POLL_TIME = 1000;
setInterval(() => {
fetch('https://text.com/location');
}, POLL_TIME);
Polling의 단점으로 인해 새롭게 고안됨
롱폴링도 폴링처럼 계속 요청을 보냄
하지만 차이점은 일반 폴링은 주기적으로 요청을 보낸다면
롱폴링은 요청을 보내면 서버가 대기하고 있다가 이벤트가 발생하거나 타이아웃이 발생할때까지 기다린 후에 응답을 주게됨. 클라이언트를 응답을 다시 요청을 보내게됨
클라이언트에서 서버에 요청을 보내고 끊기지 않는 연결상태에서 계속 데이터를 수신한다.
양방향 소통보다는 서버에서 계속 요청을 받는것에 더 유용하다.
Polling, Long Polling, HTTP Streaming 의 공통점은
결국 HTTP 프로토콜을 이용하며 이 HTTP 요청과 응답에 Header가 같이 전달되는데 이 Header에 많은 데이터가 들어있어서 너무 많은 요청과 응답의 교환은 부담을 주게된다.
웹소켓은 처음에 접속 확립을 위해서만 HTTP 프로토콜을 이용하고 그 이후부터는 독립적인 프로토콜 ws를 사용하게 된다. 또한 HTTP요청은 응답이 온 후 연결이 끊기게 되지만 웹소켓은 핸드쉐이크가 완료되고 임의로 연결을 끊기 전까지는 계속 연결이 되어있다.
api호출
/src/api/chat/route.ts
import getCurrentUser from "@/app/actions/getCurrentUser";
import { NextResponse } from "next/server";
import prisma from "@/helpers/prismadb";
export async function GET(request: Request) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const users = await prisma?.user.findMany({
include: {
conversations: {
include: {
messages: {
include: {
sender: true,
receiver: true,
},
orderBy: {
createdAt: "asc",
},
},
users: true,
},
},
},
});
return NextResponse.json(users);
}
요청이 왜 실패인지알수가 없음..
다른 api는 잘되는데 chat만 서버오류 -> currentUser없음... 로그인해야햇다.. !
데이터를 가져오기 위한 React Hook 라이브러리
SWR은 원격데이터를 가져올 때 캐싱된 데이터가 있으면 그 데이터를 반환(stale)한 다음 요청(revalidate)을 보내고 마지막으로 최신 데이터와 함께 제공하는 라이브러리다.
Lightweight
Realtime
Suspense
Pagination
Backend Agnostic
SSR / SSG Ready
TypeScript Ready
Remote + Local
실시간 데이터를 표시해야 하는 경우
페이지 로딩 속도를 높여야 하는 경우
코드를 간결하고 명확하게 유지해야 하는 경우
안정적인 데이터 페칭을 구현해야 하는 경우
확장 가능한 데이터 페칭 솔루션이 필요한 경우
유용함
설치 npm i swr
import useSWR from 'swr'
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
ChatClient.tsx
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
const {data: users, error, isLoading} = useSWR('/api/chat', fetcher, {
refreshInterval: 1000
})
console.log(users);
const currentUserWithMessage = users?.find((user: TUserWithChat) => user.email === currentUser?.email)
현재 유저의 채팅데이터 찾기? -> 애초에 get할때 현재사용자의데이터만 가져오면 안되나?
TUserWithChat
타입은 src/types/index.ts
에 설정
import { Message, User } from "@prisma/client";
export type TUserWithChat = User & {
conversations: TConversation[]
}
export type TConversation = {
id: string;
messages: Message[];
users: User[];
}
채팅관련 컴포넌트 파일을 위와같이 생성
return (
<main>
<div className="grid grid-cols-[1fr] md:grid-cols-[300px_1fr]">
{/* md보다 클때는 둘다 보여야함 */}
{/* md보다 작고 layout이 true일때는 contact안보임 */}
<section className={`md:flex ${layout && "hidden"}`}>
<Contacts
users={users}
currentUser={currentUserWithMessage}
setLayout={setLayout}
setReceiver={setReceiver}
/>
</section>
<section className={`md:flex ${!layout && "hidden"}`}>
<Chat />
</section>
</div>
</main>
);
src/components/chat/Contacts.tsx
import { TUserWithChat } from '@/types';
import React from 'react'
import User from './User';
interface ContactsProps {
users: TUserWithChat[];
currentUser: TUserWithChat;
setLayout: (layout: boolean) => void;
setReceiver: (receiver: {
receiverId: string;
receiverName: string;
receiverImage: string;
}) => void;
}
const Contacts = ({
users,
currentUser,
setLayout,
setReceiver
}: ContactsProps) => {
const filterMessages = (userId: string, userName: string | null, userImage: string | null) => {
setReceiver({
receiverId: userId,
receiverName: userName || "",
receiverImage: userImage || "",
});
}
return (
<div className='w-full overflow-auto h-[calc(100vh_-_56px)] border-[1px]'>
<h1 className='m-4 text-2xl font-semibold'>Chat</h1>
<hr />
<div className='flex flex-col'>
{users.length > 0 &&
users
.filter((user) => user.id !== currentUser?.id)
.map((user) => {
return (
<div
key={user.id}
onClick={() => {
filterMessages(user.id, user.name, user.image)
setLayout(true)
}}
>
<User
user={user}
currentUserId={currentUser?.id}
/>
</div>
);
})
}
</div>
</div>
)
}
export default Contacts
대화 리스트의 컴포넌트인 User
컴포넌트 내용
배열에서 slice(-1)
을 하면 마지막거 가져올수있음 이용
const A = [1,2,3,4]
A.slice(-1); // [4] 출력
A.slice(-1)[0]; // 4 출력
src/components/chat/User.tsx
import { TConversation, TUserWithChat } from '@/types'
import React from 'react'
import Avatar from '../Avatar';
import { fromNow } from '@/helpers/dayjs';
interface UserProps {
user: TUserWithChat;
currentUserId: string;
}
const User = ({ user, currentUserId }: UserProps) => {
// 유저간의 대화 conversations에서 currentUserId
const messagesWithCurrentUser = user.conversations.find(
(conversation: TConversation) =>
conversation.users.find((user) => user.id === currentUserId)
);
// 최근 메시지 1건 가져옴
// 마지막대화에서 첫번째 인덱스 아이템 리턴
const latestMessage = messagesWithCurrentUser?.messages.slice(-1)[0];
return (
<div className='grid grid-cols-[40px_1fr_50px] grid-rows-[40px] gap-3 py-3 px-4 border-b-[1px] hover:cursor-pointer hover:bg-orange-500'>
<div>
<Avatar src={user.image} />
</div>
<div>
<h3>{user.name}</h3>
{latestMessage &&
<p className='overflow-hidden text-xs font-medium text-gray-600 break-words whitespace-pre-wrap'>{latestMessage.text}</p>
}
{latestMessage && latestMessage.image &&
<p className='text-xs font-medium text-gray-600'>이미지</p>
}
</div>
<div>
{latestMessage && (
<p>
{fromNow(latestMessage.createdAt)}
</p>
)}
</div>
</div>
)
}
export default User
import { TUserWithChat } from '@/types'
import React from 'react'
import Input from './Input';
interface ChatProps {
currentUser: TUserWithChat;
receiver: {
receiverId: string,
receiverName: string,
receiverImage: string
};
setLayout: (layout: boolean) => void;
}
const Chat = ({
currentUser,
receiver,
setLayout
}: ChatProps) => {
if(!receiver.receiverName || !currentUser) {
// return <div className='w-full h-full'></div>
}
return (
<div className='w-full'>
{/* chat header */}
<div className=''>
</div>
{/* chat body */}
<div className='flex flex-col gap-8 p-4 overflow-hidden h-[calc(100vh_-_60px-_-70px_-_80px)]'>
</div>
{/* input */}
<div>
<Input
receiverId={receiver?.receiverId}
currentUserId={currentUser?.id}
/>
</div>
</div>
)
}
export default Chat
import axios from 'axios';
import React, { FormEvent, useState } from 'react'
import { IoImageOutline } from 'react-icons/io5';
import { RiSendPlaneLine } from 'react-icons/ri';
import useSWR from 'swr';
interface InputProps {
receiverId: string;
currentUserId: string;
}
const Input = ({
receiverId,
currentUserId
}: InputProps) => {
const [message, setMessage] = useState('');
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const imageUrl = '';
if(message || imageUrl) {
try {
await axios.post('/api/chat', {
text: message,
image: imageUrl,
receiverId: receiverId,
senderId: currentUserId
})
}catch (error) {
console.error(error);
}
}
setMessage('');
}
return (
<form
onSubmit={handleSubmit}
className='relative flex items-center justify-between w-full gap-4 p-2 pl-4 border-[1px] border-gray-300 rounded-md shadow-sm'>
<input
className='w-full text-base outline-none'
type='text'
placeholder='메시지를 입력하세요.'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<div className='text-2xl text-gray-200 cursor-pointer'>
<IoImageOutline />
</div>
<button
type='submit'
className='flex items-center justify-center p-2 text-gray-900 bg-orange-500 rounded-lg cursor-pointer hover:bg-orange-600 disabled:opacity-60'
>
<RiSendPlaneLine className='text-white'/>
</button>
</form>
)
}
export default Input
A -> B 대화시
이미 대화했는지 체크 후
users는 conversation 생성할때 넣는 데이터
이미 대화했다면 conversation에는 안넣고 message에만 넣음
대화없다면 conversation과 message 생성
src/app/api/chat/route.ts
import getCurrentUser from "@/app/actions/getCurrentUser";
import { NextResponse } from "next/server";
import prisma from "@/helpers/prismadb";
export async function GET(request: Request) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const users = await prisma.user.findMany({
include: {
conversations: {
include: {
messages: {
include: {
sender: true,
receiver: true,
},
orderBy: {
createdAt: "asc",
},
},
users: true,
},
},
},
});
return NextResponse.json(users);
}
export async function POST(
request: Request
) {
const currentUser = await getCurrentUser();
if(!currentUser) {
return NextResponse.error();
}
const body = await request.json();
// 이미 둘이 대화를 한 conversation이 있는지 찾기
const conversation = await prisma.conversation.findFirst({
where: {
AND: [
{
users: {
some: {
id: body.senderId
}
}
},
{
users: {
some: {
id: body.receiverId
}
}
}
]
}
})
// 이미 둘이 대화를 한 conversation 있다면
//
if(conversation) {
try {
const message = await prisma.message.create({
data: {
text: body.text,
image: body.image,
senderId: body.senderId,
receiverId: body.receiverId,
conversationId: conversation.id
}
})
return NextResponse.json(message);
} catch (error) {
return NextResponse.json(error)
}
} else {
// 둘이 처음 대화하는거라면 Conversation과 Message 둘 다 생성
const newConversation = await prisma.conversation.create({
data: {
senderId: body.senderId,
receiverId: body.receiverId,
users: {
connect: [
{
id: body.senderId
},
{
id: body.receiverId
},
],
},
},
});
try {
const message = await prisma.message.create({
data: {
text: body.text,
image: body.image,
senderId: body.senderId,
receiverId: body.receiverId,
conversationId: newConversation.id,
},
});
return NextResponse.json(message);
} catch (error) {
return NextResponse.json(error);
}
}
}
setLayout과 상대방 프로필정보와 상대방의 마지막 채팅시간
const conversation =
currentUser?.conversations.find((conversation) =>
conversation.users.find((user) => user.id === receiver.receiverId))
...
<ChatHeader
setLayout={setLayout}
receiverName={receiver.receiverName}
receiverImage={receiver.receiverImage}
lastMessageTime={
conversation?.messages
.filter((message) => message.receiverId === currentUser.id)
.slice(-1)[0]?.createdAt
}
/>
import React from 'react'
import { IoChevronBackCircleSharp } from 'react-icons/io5';
import Avatar from '../Avatar';
import { formatTime } from '@/helpers/dayjs';
interface ChatHeaderProps {
setLayout: (layout: boolean) => void;
receiverName: string;
receiverImage: string;
lastMessageTime: Date | undefined;
}
const ChatHeader = ({
setLayout,
receiverName,
receiverImage,
lastMessageTime
}: ChatHeaderProps) => {
return (
<div className="pl-4 border-b-[1px]">
<div className="flex items-center h-16 gap-4">
<div className="flex items-center justify-center text-3xl text-gray-400 hover:text-gray-600">
<button onClick={() => setLayout(false)} className="md:hidden">
<IoChevronBackCircleSharp />
</button>
</div>
<div className="flex items-center gap-[0.6rem]">
<div>
<Avatar src={receiverImage} />
</div>
<h2 className="text-lg font-semibold">
{receiverName}
{lastMessageTime && <p>{formatTime(lastMessageTime)}</p>}
</h2>
</div>
</div>
</div>
);
}
export default ChatHeader
Chat.tsx
const conversation =
currentUser?.conversations.find((conversation) =>
conversation.users.find((user) => user.id === receiver.receiverId))
...
<div className="flex flex-col gap-8 p-4 overflow-auto h-[calc(100vh_-_60px_-_70px_-_80px)]">
{conversation &&
conversation.messages.map((message) => {
return (
<Message
key={message.id}
isSender={message.senderId === currentUser.id}
messageText={message.text}
messageImage={message.image}
receiverName={receiver.receiverName}
receiverImage={receiver.receiverImage}
senderImage={currentUser?.image}
time={message.createdAt}
/>
)
})
}
</div>
import React from 'react'
import Avatar from '../Avatar';
import { fromNow } from '@/helpers/dayjs';
interface MessageProps {
isSender: boolean;
messageText?: string | null;
messageImage?: string | null;
receiverName: string;
receiverImage: string;
senderImage: string | null;
time: Date;
}
const Message = ({
isSender,
messageText,
messageImage,
receiverName,
receiverImage,
senderImage,
time
}: MessageProps) => {
// 로그인된 유저의 메시지는 오른쪽
// 상대방의 메시지는 왼쪽
return (
<div
className={`grid w-full grid-cols-[40px_1fr] gap-3 mx-auto`}
style={{ direction: `${isSender ? 'rtl' : 'ltr'}` }}
>
<div>
<Avatar src={senderImage && isSender ? senderImage : receiverImage} />
</div>
<div className='flex flex-col items-start justify-center'>
<div className='flex items-center gap-2 mb-2 text-sm'>
<span className='font-medium'>{isSender ? "You" : receiverName}</span>
<span className="text-xs text-gray-500 opacity-80">
{fromNow(time)}
</span>
</div>
{messageText && (
<div
className={
`p-2 break-all text-white rounded-lg
${ isSender ? "bg-orange-500 rounded-tr-none" : "bg-gray-400 rounded-tl-none"}`
}
>
<p>{messageText}</p>
</div>
)}
</div>
</div>
);
}
export default Message
Chat컴포넌트에서 useRef객체 생성해서
렌더링이 될때마다 scrollToBottom()호출
const messagesEndRef = useRef<null | HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef?.current?.scrollIntoView({
behavior: "smooth"
});
}
// 렌더링될때마다 메시지 스크롤
useEffect(() => {
scrollToBottom();
});
...
<div className="flex flex-col gap-8 p-4 overflow-auto h-[calc(100vh_-_60px_-_70px_-_80px)]">
{conversation &&
conversation.messages.map((message) => {
return (
<Message
key={message.id}
isSender={message.senderId === currentUser.id}
messageText={message.text}
messageImage={message.image}
receiverName={receiver.receiverName}
receiverImage={receiver.receiverImage}
senderImage={currentUser?.image}
time={message.createdAt}
/>
)
})
}
<div ref={messagesEndRef} />
</div>
데이터를 가져오기 위한 React Hook 라이브러리이다.
SWR은 원격 데이터를 가져올때 캐싱된 데이터가 있으면 그 데이터를 먼저 반환(Stail)한 다음 요청(revalidate)를 보내고, 마지막으로 최신 데이터와 함께 제공하는 라이브러리이다.
채팅은 1초에 한번씩 데이터베이스에 요청을 해서 새로운 데이터를 가져옴
데이터베이스에 요청을 보내기전까지는 채팅이 바로 보여지지않음 -> 이문제 해결
원격데이터를 mutating,
SWR(캐시)를 이용해서 키에다가 값을 캐시를 시켜놓고
캐시가 있으면 그 데이터을 보여주고 다시 원격에서 데이터를 가져와서 최신데이터를 보여줌
https://swr.vercel.app/docs/mutation
useSWRConfig
Hook사용import { useSWRConfig } from "swr"
function App() {
const { mutate } = useSWRConfig()
mutate(key, data, options)
}
import { mutate } from "swr"
function App() {
mutate(key, data, options)
}
위 두개 mutate는 key가 필요함
import useSWR from 'swr'
function Profile () {
const { data, mutate } = useSWR('/api/user', fetcher)
return (
<div>
<h1>My name is {data.name}.</h1>
<button onClick={async () => {
const newName = data.name.toUpperCase()
// send a request to the API to update the data
await requestUpdateUsername(newName)
// update the local data immediately and revalidate (refetch)
// NOTE: key is not required when using useSWR's mutate as it's pre-bound
mutate({ ...data, name: newName })
}}>Uppercase my name!</button>
</div>
)
}
import useSWRMutation from 'swr/mutation'
// Fetcher implementation.
// The extra argument will be passed via the `arg` property of the 2nd parameter.
// In the example below, `arg` will be `'my_token'`
async function updateUser(url, { arg }: { arg: string }) {
await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${arg}`
}
})
}
function Profile() {
// A useSWR + mutate like API, but it will not start the request automatically.
const { trigger } = useSWRMutation('/api/user', updateUser, options)
return <button onClick={() => {
// Trigger `updateUser` with a specific argument.
trigger('my_token')
}}>Update User</button>
}
chat/Input.tsx
에서 기존에 axios이용하여 데이터 post하는 부분을 mutate로 변경할것임
const sendRequest = (url:string, {arg} : {
arg: {
text: string;
image: string;
receiverId: string;
senderId: string;
}
}) => {
return fetch(url, {
method: 'POST',
body: JSON.stringify(arg)
}).then(res => res.json());
}
const Input = ({
receiverId,
currentUserId
}: InputProps) => {
...
const { trigger } = useSWRMutation('/api/chat', sendRequest)
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const imageUrl = '';
if(message || imageUrl) {
try {
trigger({
text: message,
image: imageUrl,
receiverId: receiverId,
senderId: currentUserId
})
// await axios.post('/api/chat', {
// text: message,
// image: imageUrl,
// receiverId: receiverId,
// senderId: currentUserId
// })
}catch (error) {
console.error(error);
}
}
setMessage('');
}
채팅을 보내자마자 바로 메시지 컴포넌트가 보이는것을 확인 할 수 있다.
// 이미지업로드 : file input에 ref등록
const imageRef = useRef<HTMLInputElement>(null);
const chooseImage = () => {
imageRef.current?.click()
}
...
<input
type="file"
className="hidden"
ref={imageRef}
accept="image/*"
multiple={false}
/>
이미지 올리면 url을 받아서 프리뷰라는 state에 넣어줌
메시지 input부분에 프리뷰를 보여준다.
src/helperes/previewImage.ts
//
const previewImage = (e: any, setImagePreview: any, setImage: any) => {
console.log(e.target.files[0]);
}
export default previewImage;
const previewImage = (e: any, setImagePreview: any, setImage: any) => {
// flie정보
// console.log(e.target.files[0]);
// file을 url로 변경
const file = e.target.files[0];
setImage(file); // cloudinary에 업로드할것
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
// 인코딩된 url
// console.log(reader.result);
setImagePreview(reader.result);
}
}
export default previewImage;
const [image, setImage] = useState<File | null>(null); // cloudinary에 업로드할 이미지 데이터
const [imagePreview, setImagePreview] = useState<string | null>(null); // preview할 이미지 데이터
{imagePreview && (
<div className='absolute right-0 w-full overflow-hidden rounded-md bottom-[4.2rem] max-w-[300px] shadow-md'>
<img src={imagePreview} alt='' />
<span className='absolute flex items-center justify-center p-2 text-xl text-white bg-gray-900 cursor-pointer top-[0.4rem] right-[0.4rem] rounded-full opacity-60 hover:opacity-100'>
<CgClose />
</span>
</div>
)}
<input
const removeImage = () => {
setImagePreview(null);
setImage(null);
}
...
<span
onClick={() => removeImage()}
className='absolute flex items-center justify-center p-2 text-xl text-white bg-gray-900 cursor-pointer top-[0.4rem] right-[0.4rem] rounded-full opacity-60 hover:opacity-100'>
<CgClose />
</span>
uploadImage함수에서 cloudinary에 이미지 업로드한후
이미지 url은 db에 저장하기
src/helpers/uploadImages.ts
// cloudinary 업로드하기
const uploadImage = async (image: File) => {
const url = `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/upload`;
const formData = new FormData();
formData.append('file', image);
formData.append('upload_preset', process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET!);
const response = await fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
console.log(data);
return data.url;
}
export default uploadImage;
handleSubmit
에서 호출하고 imageUrl에 값받기
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const imageUrl = image ? await uploadImage(image as File) : null;
if (message || imageUrl) {
try {
trigger({
text: message,
image: imageUrl,
receiverId: receiverId,
senderId: currentUserId,
});
// await
} catch (error) {
console.error(error);
}
}
setMessage("");
setImagePreview(null);
setImage(null);
};
messageText 상단에 이미지내용 추가
{messageImage && (
<div className='overflow-hidden rounded-md mx-[0.6rem] max-w-[80%]'>
<Image
src={messageImage}
width={300}
height={300}
alt=""
/>
</div>
)}
이런 에러가 났는데 next.config.js에 해당 도메인의 주소가 등록이 안됐다고한다.
http 프로토콜도 추가했다.