들어가기
Post중 하나를 클릭 했을떄, Detail Page로 이동하게됨
DetailPost에는 3개의 API가 들어감.
1. Post Detatl.(useSWR) /api/posts/{router.query.id}/answers
3. 궁금해요 클릭이벤트. (useMutation) /api/posts/${router.query.id}/wonder
router.query.id 는 const router = useRouter()로 받을 수 있음.
import useMutation from '@libs/client/useMutation'
import { cls } from '@libs/client/utils'
import { Answer, Post, User } from '@prisma/client'
import type { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import useSWR from 'swr'
import Layout from '../../components/layout'
import TextArea from '../../components/textarea'
interface AnswerWithUser extends Answer {
user: User
_count: {
wondering: number
answers: number
}
}
///하나의 Post에 대한 댓글(answers)을 불러줄때, 댓글에
///댓글을 단 user와 _count를 포함시켜줌.
///약간 복잡하게 느껴질 수 도 있음.
interface PostWithUser extends Post {
user: User
_count: {
answers: number
wondering: number
}
answers: AnswerWithUser[]
}
///바로 밑의 하나의 Post를 요청했을때,
///그 Post의 user와, _count와 answer를 같이 불러줌.
interface CommunityPostResponse {
ok: boolean
post: PostWithUser
isWondering: boolean
}
///나열된 Post에서 하나의 Post를 클릭했을떄, 받는 data
interface AnswerForm {
answer: string
}
///댓글달기 Form의 Type.
interface AnswerResponse {
ok: boolean
answer: Answer
}
///댓글달기를 useMutation한 후, Response한 data.
const CommunityPostDetail: NextPage = () => {
const router = useRouter()
const { register, handleSubmit, reset } = useForm<AnswerForm>()
///DetailPosr page에서 댓글을 달 수 있는 Form을 만들어 놓음.
const { data, mutate } = useSWR<CommunityPostResponse>(
router.query.id ? `/api/posts/${router.query.id}` : null
)
///community페이지에서 하나의 질문을 클릭 했을떄,
///DetailPost 페이지에서 router.query.id 로 클릭된 Post id를 받음.
///위와같이 해주어야 undefinde가 안뜸.
///useSWR을 이용해서 위의 API로 요청 보내고, data를 받음.
///DetailPost API는 밑에서 만들 예정.
const [wonder, { loading }] = useMutation(
`/api/posts/${router.query.id}/wonder`
)
/// DetailPost페이지에서 궁금해요를 클릭했을떄, 궁금해요 숫자가 +1되고,
///loggedInUser가 눌렀을떄, 색이 변하게, 내가 눌렀는지를 true/false
///로 받을 수 있는 API
const [sendAnswer, { data: answerData, loading: answerLoading }] =
useMutation<AnswerResponse>(`/api/posts/${router.query.id}/answers`)
///댓글을 달고 onSubmit했을떄 실행되는 API
///data가 겹치는 이유로 rename해줌.
const onWonderClick = () => {
///궁금해요 버튼을 눌렀을떄, 실행되는 함수,
if (!data) return
mutate(
{ ///useSWR에서 받은 data모양에서, data?.post._count.wondering만
///cache로 다시 써줌. data모양을 그대로 적는거 집중해서 봐 둘것!!!
...data,
post: {
...data.post,
_count: {
...data.post._count,
wondering: data.isWondering
? data?.post._count.wondering - 1
: data?.post._count.wondering + 1,
},
},
isWondering: !data.isWondering,
},
false ///useSWR을 사용함, DB에 다시 reload안되게 설정.
)
if (!loading) {
wonder({})
///위에서 cache를 다시 쓴 다음 `/api/posts/${router.query.id}/wonder`
///API를 실행시킴. 단 !loading가 아닐떄만~
}
}
const onValid = (form: AnswerForm) => {
if (answerLoading) return
sendAnswer(form)
}
///answer(댓글)을 달고 나서 onSubmit했을 시, 실행되는 함수.
useEffect(() => {
if (answerData && answerData.ok) {
reset()
mutate()
}
}, [answerData, reset, mutate])
///댓글을 달고나서 sendAnswer API 실행후, ok data를 받고나서,
///reset()댓글다는 칸을 지우고, mutate()로 useSWR을 실행(data reload)함.
return (
<Layout canGoBack>
<span className="inline-flex my-3 ml-4 items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
동네질문
</span>
<div className="flex mb-3 px-4 cursor-pointer pb-3 border-b items-center space-x-3">
<div className="w-10 h-10 rounded-full bg-slate-300" />
<Link href={`/users/profile/${data?.post?.user.id}`}> ///글쓴이의 profile로 이동
<a>
<p className="text-sm font-medium text-gray-700">
{data?.post?.user?.name}
</p>
<p className="text-xs font-medium text-gray-500">
View profile →
</p>
</a>
</Link>
</div>
<div>
<div className="mt-10 my-10 px-4 text-gray-700">
<span className="text-orange-500 font-medium">Q.</span>{' '}
<span className="text-lg font-extrabold">{data?.post?.question}</span>
</div>
<div className="flex px-4 space-x-5 mt-3 text-gray-700 py-2.5 border-t border-b-[2px] w-full">
<button
onClick={onWonderClick} ///궁금해요를 눌렀을떄,
///onWonderClick함수가 실행되게~
className={cls(
'flex space-x-2 items-center text-sm',
data?.isWondering ? 'text-teal-400' : ''
)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>궁금해요 {data?.post?._count?.wondering}</span>
/// 궁금해요 뒤에 궁금해요 갯수를 count해줌.
</button>
<span className="flex space-x-2 items-center text-sm">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
></path>
</svg>
<span>답변 {data?.post._count.answers}</span>
///답변을 count해줌.
</span>
</div>
</div>
<div className="px-4 my-3 space-y-5">
{data?.post?.answers?.map((answer) => ( ///DetailPost의 댓글들을
///map으로 뿌려줌.
<div key={answer.id} className="flex items-start space-x-3 border-b">
<div className="w-8 h-8 bg-slate-200 rounded-full" />
<div>
<div className="flex space-x-2">
<span className="text-sm block font-medium text-gray-700">
{answer.user.name}
</span>
<span className="text-xs mt-1 text-gray-500 block ">
2시간전
</span>
</div>
<p className="text-gray-700 text-sm mt-2 mb-2">{answer.answer}</p>
</div>
</div>
))}
</div>
<form className="px-4" onSubmit={handleSubmit(onValid)}>
///react-hook-form.
///댓글을 다는 Form
<TextArea
register={register('answer', { required: true, minLength: 5 })}
name="description"
label="Answer"
required
placeholder="Answer this question!"
/>
<button className="mt-2 w-full bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 focus:outline-none ">
{answerLoading ? 'Loading....' : 'Reply'}
</button>
</form>
</Layout>
)
}
export default CommunityPostDetail
궁금해요를 클릭했을떄, 그 요청을 처리하는 API
import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import client from '@libs/server/client'
import { withApiSession } from '@libs/server/withSession'
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { question } = req.body ///front에서 온 Data를 받음.
const { user } = req.session ///loggedInUser 를 확인시켜줌.
const { id } = req.query ///post를 클릭했을떄, 그 Post id 확인.
///위 3개는 그냥 고정으로
const alreadyExists = await client.wondering.findFirst({
where: {
userId: user?.id,
postId: Number(id),
},
select: {
id: true,
},
})
///궁금해요를 클릭했을떄, 궁금해요를 클릭한 Post가 DB에 존재하는지 확인
///존재하는지의 확인 여부는 userId, PostId 둘다를 확인함으로써,
///user가 궁금해요를 눌렀는지 아닌지를 확인하는 여부가 된다.
if (alreadyExists) {
await client.wondering.delete({
where: {
id: alreadyExists.id,
},
}) ///user가 눌렀다면, 한번 더 누름으로써, delete(false)로 만들어줌 -1시킴.
} else {
await client.wondering.create({ ///user가 누른상태가 아니라면,
///궁금해요를 true로 만들어줌.
///userId와 PostId를 connect하는게
///create하는 것임.
data: {
user: {
connect: {
id: user?.id,
},
},
post: {
connect: {
id: Number(id),
},
},
},
})
}
res.json({ ok: true }) ///ok를 return 해줌.
}
export default withApiSession(
withHandler({
methods: ['POST'],
handler,
})
)
댓글(answer)받아서 처리하는 API
import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import client from '@libs/server/client'
import { withApiSession } from '@libs/server/withSession'
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { answer } = req.body //client에서 보내온 댓글(answer)받음.
const { user } = req.session ///loggedInUser
const { id } = req.query ///Detail Post Id
// const post = await client.post.findUnique({
// where: {
// id: Number(id),
// },
// select: {
// id: true,
// },
// })
// if (!post) return false
--->더 정확히 코딩하자면, 해당 Post를 찾아서 없으면 return false해야됨.
const newAnswer = await client.answer.create({
data: {
user: {
connect: {
id: user?.id,
},
},
post: {
connect: {
id: Number(id),
},
},
answer,
},
}) ///answer를 req.body로 받아서, user와 post를 connect하고ㅡ
///answer를 넣어줌.
console.log(newAnswer)
res.json({ ok: true, answer: newAnswer })
}
///만들어진 answer을 return해줌, answer가 볍쳐서 rename해줌.
export default withApiSession(
withHandler({
methods: ['POST'],
handler,
})
)
import withHandler, { ResponseType } from '@libs/server/withHandler'
import { NextApiRequest, NextApiResponse } from 'next'
import client from '@libs/server/client'
import { withApiSession } from '@libs/server/withSession'
async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseType>
) {
const { question } = req.body
const { user } = req.session
const { id } = req.query
///3개는 위에서 계속 다뤘죠^^!!
const post = await client.post.findUnique({ ///클릭된 Post를 찾음.
where: {
id: Number(id), ///req.query로 받은 id로 Post찾음.
},
include: { ///찾은 Post에 user, answers, _count를 포함시킴.
///prisma에서 post는 user, answers와 relation되어 있으므로
///relation된 model을 어떻게 include하는지 잘 봐 놓을것.
///그리고 relation된 answers와 wondering을 count하는것도
///잘 봐 놓을 것!!
user: {
select: {
id: true,
name: true,
avatar: true,
},
},
answers: {
select: {
answer: true,
createdAt: true,
id: true,
user: {
select: {
id: true,
avatar: true,
name: true,
},
},
},
},
_count: {
select: {
answers: true,
wondering: true,
},
},
},
})
const isWondering = Boolean(
await client.wondering.findFirst({
where: {
postId: Number(id),
userId: user?.id,
},
select: {
id: true,
},
})
)
///loggedInUser가 이 Post에 궁금해요를 눌렀는지 아닌지를
///Boolean 즉, true, false로 return해줌.
res.json({ ok: true, post, isWondering })
///post와 okWondering을 returngowna..
}
export default withApiSession(
withHandler({
methods: ['GET'],
handler,
})
)
DetailPost에는 3개의 API가 들어가므로
집중집중해서 잘 봐놓는다!!