들어가기
Live 방송의 UploadForm을 완성한다.
1. model(prisma)
2. UploadForm
3. DetailPage
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
referentialIntegrity = "prisma"
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
phone String? @unique
email String? @unique
name String
avatar String?
tokens Token[]
products Product[]
fav Fav[]
sales Sale[]
purchases Purchase[]
posts Post[]
answers Answer[]
wonderings Wondering[]
writtenReviews Review[] @relation("writtenRiviews")
receivedReviews Review[] @relation("receivedRiviews")
records Record[]
streams Stream[]
messages Message[]
}
model Token {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
payload String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
}
model Product {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
image String
name String
price Int
description String @db.MediumText
favs Fav[]
sales Sale[]
purchases Purchase[]
record Record[]
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
question String @db.MediumText
answers Answer[]
wondering Wondering[]
latitude Float?
longitude Float?
}
model Answer {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
answer String @db.MediumText
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId Int
}
model Wondering {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId Int
}
model Review {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
review String @db.MediumText
createdBy User @relation(name: "writtenRiviews", fields: [createdById], references: [id], onDelete: Cascade)
createdFor User @relation(name: "receivedRiviews", fields: [createdForId], references: [id], onDelete: Cascade)
createdById Int
createdForId Int
score Int @default(1)
}
model Fav {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
userId Int
productId Int
}
model Sale {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
userId Int
productId Int
}
model Purchase {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
userId Int
productId Int
}
model Record {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
userId Int
productId Int
kind Kind
}
/// -->/api/users/me/record?kind=sale (req.query로 확인가능)
enum Kind {
Purchase
Sale
Fav
}
model Stream { ///Live방송, name, price, user, messages잘봐둠.
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
description String @db.MediumText
price Int
user User @relation(fields: [userId], references: [id])
userId Int
messages Message[]
}
model Message { ///Live방송에 사용되는 채팅(message)가 들어감..
///크게 어려울것는 없음.
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
userId Int
message String @db.MediumText
stream Stream @relation(fields: [streamId], references: [id])
streamId Int
}
GET은 live방송을 다 깔아주는것, POST는 방송 Upload
여기서는 Upload만 다룸
import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import { withApiSession } from '@libs/server/withSession'
import client from '@libs/server/client'
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { user } = req.session
const { name, price, description } = req.body
if (req.method === 'POST') {
const stream = await client.stream.create({
///create.tsx에서 받은 body(name, price, description,
///user를 받아서 create해줌.
data: {
name,
price,
description,
user: {
connect: {
id: user?.id,
},
},
},
})
res.json({ ok: true, stream }) ///stream을 만들고 나서
///return해줌.
} else if (req.method === 'GET') {
const streams = await client.stream.findMany({
take: 5,
skip: 5, ///나중에 Pagination을 위해서 사용 예정.
})
res.json({ ok: true, streams })
}
}
export default withApiSession(
withHandler({
methods: ['GET', 'POST'],
handler,
})
)
import useMutation from '@libs/client/useMutation'
import { Stream } from '@prisma/client'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import Button from '../../components/button'
import Input from '../../components/input'
import Layout from '../../components/layout'
import TextArea from '../../components/textarea'
interface CreateForm {
name: string
price: string
description: string
}
///Upload에서 사용될 argument name, price, description
interface CreateResponse {
ok: boolean
stream: Stream
}
///return 받는 argument
const Create: NextPage = () => {
const router = useRouter()
const [createStream, { data, loading }] =
useMutation<CreateResponse>(`/api/streams`)
const { register, handleSubmit } = useForm<CreateForm>()
///react-hook-form
const onValid = (form: CreateForm) => {
if (loading) return
createStream(form)
}
///onSubmit함수에 사용될 onValid
useEffect(() => {
if (data && data.ok) {
router.push(`/streams/${data.stream.id}`)
}
}, [data, router])
///data.ok를 return받으면, 위의 Datail주소를 이동시켜줌.
return (
<Layout canGoBack title="Go Live">
<form onSubmit={handleSubmit(onValid)} className=" space-y-5 py-10 px-4">
<Input
register={register('name', { required: true })}
///react-hook-form사용
required
label="Name"
name="name"
type="text"
/>
<Input
register={register('price', { required: true, valueAsNumber: true })}
///valueAsNumber:true는 server에서 Number()않해줘도됨.
/// +id 등등 +를 않붙여줘도 됨.
///react-hook-form사용
required
label="Price"
placeholder="0.00"
name="price"
type="text"
kind="price"
/>
<TextArea
register={register('description', { required: true })}
///react-hook-form사용
name="description"
label="Description"
/>
<Button text={loading ? 'loading' : 'Go live!'} />
</form>
</Layout>
)
}
export default Create
import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import { withApiSession } from '@libs/server/withSession'
import client from '@libs/server/client'
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { id } = req.query
const stream = await client.stream.findUnique({
where: {
id: Number(id),
},
include: {
messages: {
select: {
id: true,
message: true,
user: {
select: {
avatar: true,
id: true,
},
},
},
},
},
})
///include에 messages를 넣어주는것을 유심히 잘 봐둔다.
///req.query로 id받아서 이동한 주소에 뿌려줌.
res.json({ ok: true, stream })
}
export default withApiSession(
withHandler({
methods: ['GET'],
handler,
})
)
detail Page에 사용되는 채팅!!
import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import { withApiSession } from '@libs/server/withSession'
import client from '@libs/server/client'
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { id } = req.query
const { message } = req.body
const { user } = req.session
///id, message, user를 받음.
const newMessage = await client.message.create({
data: {
message,
stream: {
connect: {
id: Number(id), ///stream에 연결해서 message를 만듬
},
},
user: {
connect: {
id: user?.id, ///loggedInUser를 연결해줌,
},
},
},
})
res.json({ ok: true, newMessage }) ///만들어진 message를
///return해줌.
}
export default withApiSession(
withHandler({
methods: ['POST'],
handler,
})
)
live방송 detailPage.
실시간 채팅과 관련된 부분이기 때문에 집중해서 봐둘것!!!!
import useMutation from '@libs/client/useMutation'
import useUser from '@libs/client/useUser'
import { Stream } from '@prisma/client'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import React, { useEffect, useRef } from 'react'
import { useForm } from 'react-hook-form'
import useSWR from 'swr'
import Layout from '../../components/layout'
import Message from '../../components/message'
interface StreamMessage {
message: string
id: number
user: {
avatar?: string
id: number
}
}
///만들어지는 message의 argument(message, id, user)
interface StreamWithMessages extends Stream {
messages: StreamMessage[]
}
///위의 StreamMessage를 배열로 만들어줌,
interface StreamResponse {
ok: true
stream: StreamWithMessages
}
///message를 만들고 return받음.
interface MessageForm {
message: string
}
///messageForm에서는 message만 만들어짐.
const Streams: NextPage = () => {
const boxRef = React.useRef<any>()
///채팅창에서 항상 맨 아래를 보여줄 수 있게 설정하기 위해
///useRef를 사용함.
//console.log(boxRef.current.scrollHeight, boxRef.current.clientHeight)
// DOM 조작 함수
const scrollToBottom = () => {
const { scrollHeight, clientHeight } = boxRef.current
boxRef.current.scrollTop = scrollHeight - clientHeight
}
///scroll을 맨 아래로 보내는 함수.
///잘잘잘 봐둘것!!
const { user } = useUser()
const router = useRouter()
const { register, handleSubmit, reset } = useForm<MessageForm>()
///react-hook-form
const { data, mutate } = useSWR<StreamResponse>(
router.query.id ? `/api/streams/${router.query.id}` : null,
{
refreshInterval: 1000,
}
)
///router.query.id가 있으면, 위의 Detail주소로 날려줌.
///refreshInterval은 1초단위로 위 API를 fetch함.
///NextJS에는 real-time없기 떄문에 이런방법으로
///real-time을 구현함.
const [sendMessage, { loading, data: sendMessageData }] = useMutation(
`/api/streams/${router.query.id}/messages`
)
///message를 보내는 useMutation.
const onValid = (form: MessageForm) => {
if (loading) return ///realTime을 위해서 server로 메세지를
///보내기전에 cache에 바로 write함.
///data형태를 그대로 구현하는게 ㅠㅠ
reset()
mutate(
(prev) =>
prev &&
({
...prev,
stream: {
...prev.stream,
messages: [
...prev.stream.messages,
{ id: Date.now(), message: form.message, user: { ...user } },
],
},
} as any),
false
)
sendMessage(form) ///message를 보내는 useMutation.
scrollToBottom() ///message를 보낼때마다, scroll이 맨
} ///아래를 나타내게 함수 실행시킴.
// useEffect(() => {
// if (sendMessageData && sendMessageData.ok) {
// mutate()
// }
// }, [sendMessageData, mutate])
///위의 방법이 아닌 useEffect로 메세지를 보낼때마다 mutate되게도
///가능함.
return (
<Layout canGoBack>
<div className="py-10 px-4 space-y-4">
<div className="w-full rounded-md shadow-xl bg-slate-300 aspect-video" />
<div className="mt-5">
<h1 className="text-3xl font-bold border-b p-3 text-gray-900">
{data?.stream?.name} ///api에서 받은 name
</h1>
<span className="text-2xl block mt-3 text-gray-900">
{' '}
{data?.stream?.price}원 ///api에서 받은 price
</span>
<p className=" my-6 text-gray-700">{data?.stream?.description}</p> ///api에서 받은 description
</div>
<div className="mb-5">
<h2 className="text-2xl font-bold shadow-xl border-t- p-3 text-orange-500 ">
Live Chat!!
</h2>
<div
ref={boxRef} ///scrollToBottom을 위해 ref설정.
className="py-28 pb-18 h-[50vh] overflow-y-scroll px-4 space-y-4"
> ///overflow-y-scroll은 채팅창을 스트롤로 설정하것.
{data?.stream?.messages.map((message) => (
<Message
key={message.id}
message={message.message}
reversed={message.user.id === user?.id}
/>
))} ///api롤 호출해서 받아온 message를 뿌려줌.
</div>
<form
onSubmit={handleSubmit(onValid)}
className="fixed py-2 bg-white shadow-lg bottom-0 inset-x-0"
>
<div className="flex relative max-w-md items-center w-full mx-auto">
///Input component를 사용하지 않고 react-hook-form
///사용
<input
{...register('message', { required: true })}
type="text"
className="shadow-sm rounded-full w-full border-gray-300 focus:ring-orange-500 focus:outline-none pr-12 focus:border-orange-500"
/>
<div className="absolute inset-y-0 flex py-1.5 pr-1.5 right-0">
<button className="flex focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 items-center bg-orange-500 rounded-full px-3 hover:bg-orange-600 text-sm text-white"> ///메세지 보내는 화살표 만드는것
→
</button>
</div>
</div>
</form>
</div>
</div>
</Layout>
)
}
export default Streams