Stream.(1) Prisma, model, UploadForm, DetailPage, chat

김종민·2022년 8월 14일
0

apple-market

목록 보기
25/37


들어가기
Live 방송의 UploadForm을 완성한다.
1. model(prisma)
2. UploadForm
3. DetailPage

1. prisma.schema

// 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
}

2. pages/api/streams/index.ts

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,
  })
)

3. pages/streams/create.tsx

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

4. pages/api/streams/[id]/index.ts

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,
  })
)

5. pages/api/streams/[id]/messages.ts

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,
  })
)

6. pages/streams/[id].tsx

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"> ///메세지 보내는 화살표 만드는것
                
                  &rarr;
                </button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </Layout>
  )
}

export default Streams
profile
코딩하는초딩쌤

0개의 댓글