Skip to content

Commit

Permalink
Merge pull request #38 from Help-M-Ssaem/feat/#37
Browse files Browse the repository at this point in the history
[Feat] 게시판 사진 포함 CRUD
  • Loading branch information
uiop5809 authored Aug 20, 2024
2 parents 7a9b65e + 72eebd1 commit 5dab8ff
Show file tree
Hide file tree
Showing 21 changed files with 717 additions and 104 deletions.
10 changes: 9 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@
// storybook 개발 의존성에 포함된 패키지 import 에러 x
"import/no-extraneous-dependencies": [
"error",
{ "devDependencies": ["**/*.stories.tsx"] }
{
"devDependencies": [
"**/*.stories.tsx",
"**/test/**", // 테스트 폴더
"**/scripts/**", // 스크립트 폴더
"**/*.test.js", // 테스트 파일
"**/*.spec.js" // 테스트 파일
]
}
],
"react/function-component-definition": [
"off",
Expand Down
5 changes: 4 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
const nextConfig = {
reactStrictMode: false,
images: {
domains: ['mssaem-bucket.s3.ap-northeast-2.amazonaws.com'],
domains: [
'mssaem-bucket.s3.ap-northeast-2.amazonaws.com',
'mssaem-bucket-v2.s3.ap-northeast-2.amazonaws.com',
],
},
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"@storybook/nextjs": "^8.2.1",
"@tanstack/react-query": "^5.51.23",
"@tanstack/react-query-devtools": "^5.51.21",
"@toast-ui/editor": "^3.2.2",
"@toast-ui/react-editor": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"axios": "^1.7.3",
Expand Down
3 changes: 3 additions & 0 deletions public/images/common/arrow_down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 56 additions & 23 deletions src/app/board/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,31 @@ import Container from '@/components/common/Container'
import Profile from '@/components/common/Profile'
import {
useBoardDetail,
useDeleteBoard,
usePostBoardLike,
} from '@/service/board/useBoardService'
import { useParams } from 'next/navigation'
import { useParams, useRouter } from 'next/navigation'
import CommentList from '@/components/board/CommentList'
import { useUserInfo } from '@/service/user/useUserService'
import { useQueryClient } from '@tanstack/react-query'
import { queryKeys } from '@/service/board/BoardQueries'

const BoardDetail = () => {
const { id } = useParams()
const { data: boardDetail } = useBoardDetail(Number(id))
const { mutate } = usePostBoardLike()
const boardId = Number(id)
const router = useRouter()

const queryClient = useQueryClient()

const { data: userInfo } = useUserInfo()
const { data: boardDetail } = useBoardDetail(boardId)
const { mutate: postBoardLike } = usePostBoardLike()
const { mutate: deleteBoard } = useDeleteBoard()

const [likeCount, setLikeCount] = useState(boardDetail?.likeCount || 0)
const [isLiked, setIsLiked] = useState(boardDetail?.isLiked || false)
const [commentCount, setCommentCount] = useState(
boardDetail?.commentCount || 0,
(boardDetail && boardDetail.commentCount) || 0,
)

useEffect(() => {
Expand All @@ -33,7 +44,10 @@ const BoardDetail = () => {
}, [boardDetail])

const handleLikeToggle = () => {
mutate(Number(id), {
if (userInfo && userInfo.id === boardDetail?.memberSimpleInfo.id) {
return
}
postBoardLike(boardId, {
onSuccess: () => {
setIsLiked(!isLiked)
setLikeCount((prevCount) => (isLiked ? prevCount - 1 : prevCount + 1))
Expand All @@ -45,6 +59,15 @@ const BoardDetail = () => {
setCommentCount(newCount)
}

const handleDelete = () => {
deleteBoard(boardId, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.boardList })
router.back()
},
})
}

return (
<div>
{boardDetail && (
Expand All @@ -55,26 +78,32 @@ const BoardDetail = () => {
게시판
</div>
<Container color="purple">
<div className="flex justify-end gap-2.5 mb-5">
<Button
text="수정"
color="PURPLE"
size="small"
onClick={() => {}}
/>
<Button
text="삭제"
color="PURPLE"
size="small"
onClick={() => {}}
/>
</div>
<div className="h-[1px] bg-main" />
{userInfo && userInfo.id === boardDetail.memberSimpleInfo.id && (
<>
<div className="flex justify-end gap-2.5 mb-5">
<Button
text="수정"
color="PURPLE"
size="small"
onClick={() => {
router.push(`/board/${id}/update`)
}}
/>
<Button
text="삭제"
color="PURPLE"
size="small"
onClick={handleDelete}
/>
</div>
<div className="h-[1px] bg-main" />
</>
)}

<div className="flex justify-between my-7.5">
<Profile user={boardDetail.memberSimpleInfo} />
<div className="flex gap-3.5 text-caption text-gray2">
<p>조회수 {boardDetail.hits}</p> |{' '}
<p>조회수 {boardDetail.hits}</p> |
<p>{boardDetail.createdAt}</p>
</div>
</div>
Expand All @@ -96,12 +125,16 @@ const BoardDetail = () => {
width={80}
height={80}
alt="like_btn"
className="cursor-pointer my-10"
className={`my-10 ${
userInfo && userInfo.id === boardDetail.memberSimpleInfo.id
? 'cursor-default'
: 'cursor-pointer'
}`}
onClick={handleLikeToggle}
/>
</div>
<CommentList
id={Number(id)}
id={boardId}
page={0}
size={50}
commentCount={commentCount}
Expand Down
184 changes: 184 additions & 0 deletions src/app/board/[id]/update/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use client'

// eslint-disable-next-line import/no-extraneous-dependencies
import '@toast-ui/editor/dist/toastui-editor.css'
import { Editor } from '@toast-ui/react-editor'
import { useEffect, useRef, useState } from 'react'
import {
usePostBoardImage,
usePatchBoard,
useBoardDetail,
} from '@/service/board/useBoardService'
import { useRouter, useParams } from 'next/navigation'
import Button from '@/components/common/Button'
import Container from '@/components/common/Container'
import MbtiSelect from '@/components/board/MbtiSelect'
import { useQueryClient } from '@tanstack/react-query'
import { queryKeys } from '@/service/board/BoardQueries'
import { MBTI } from '@/types/mbtiTypes'

const BoardUpdatePage = () => {
const router = useRouter()
const queryClient = useQueryClient()
const { id } = useParams()
const boardId = Number(id)

const editorRef = useRef<any>(null)

const [mbti, setMbti] = useState<MBTI | null>(null)
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [image, setImage] = useState<string[]>([])
const [uploadImage, setUploadImage] = useState<string[]>([])
const [initialContent, setInitialContent] = useState('')

const { data: boardDetail } = useBoardDetail(boardId)
const { mutate: updateBoard } = usePatchBoard()
const { mutate: postBoardImage } = usePostBoardImage()

const extractImageUrls = (text: string) => {
const imgTagRegex = /<img[^>]*src="([^"]+)"[^>]*>/g
const matches = text.match(imgTagRegex)
if (!matches) {
return []
}
const imageUrls = matches.map((match) => {
const srcMatch = match.match(/src="([^"]+)"/)
return srcMatch ? srcMatch[1] : null
})
return imageUrls.filter((url) => url !== null)
}

useEffect(() => {
if (boardDetail && editorRef.current) {
setTitle(boardDetail.title)
setContent(boardDetail.content)
setMbti(boardDetail.boardMbti as MBTI)

const extractedImageUrls = extractImageUrls(boardDetail.content)
setImage(extractedImageUrls)

setInitialContent(boardDetail.content)

editorRef.current.getInstance().setHTML(boardDetail.content)
}
}, [boardDetail, editorRef])

useEffect(() => {
const extractedImageUrls = extractImageUrls(content)
const filteredImageUrls = extractedImageUrls.filter(
(url) => url !== null,
) as string[]
setUploadImage(filteredImageUrls)
}, [content])

const handleContentChange = () => {
const updatedContent = editorRef.current?.getInstance().getHTML() || ''
setContent(updatedContent)
}

const handleUploadImage = async (blob: Blob) => {
const formImage = new FormData()
formImage.append('image', blob)

return new Promise<string>((resolve, reject) => {
postBoardImage(formImage, {
onSuccess: (imgUrl) => {
resolve(imgUrl)
},
onError: (error) => {
reject(error)
},
})
})
}

const formData = new FormData()
const data = {
title,
content,
mbti,
}
formData.append(
'patchBoardReq',
new Blob([JSON.stringify(data)], { type: 'application/json' }),
)
formData.append(
'image',
new Blob([JSON.stringify(image)], { type: 'application/json' }),
)
formData.append(
'uploadImage',
new Blob([JSON.stringify(uploadImage)], { type: 'application/json' }),
)

const handleSubmit = () => {
if (!title) {
alert('제목을 입력해주세요.')
return
} else if (!content) {
alert('내용을 입력해주세요.')
return
}
updateBoard(
{ id: boardId, board: formData },
{
onSuccess: async () => {
await queryClient.refetchQueries({ queryKey: queryKeys.board(boardId) });
router.push(`/board/${boardId}`);
},
},
)
}

return (
<div className="w-full-vw ml-half-vw px-4% py-8 sm:px-8% md:px-13% bg-main3">
<Container color="white" className="bg-white p-10">
<MbtiSelect mbti={mbti && mbti.toUpperCase()} setMbti={setMbti} />
<div className="text-headline font-normal text-gray2 mb-5">
제목을 입력해주세요.
</div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full h-10 border border-gray-300 rounded-md p-2 mb-4"
/>
<div className="text-headline font-normal text-gray2 mb-5">
내용을 입력해주세요.
</div>
<Editor
ref={editorRef}
initialValue={initialContent}
previewStyle="vertical"
height="30rem"
initialEditType="wysiwyg"
onChange={handleContentChange}
hooks={{
addImageBlobHook: async (blob: Blob, callback: any) => {
const imgUrl = await handleUploadImage(blob)
setImage((prev: any) => [...prev, imgUrl])
callback(imgUrl, 'image')
},
}}
/>
<div className="flex gap-2.5 justify-end mt-4">
<Button
text="취소하기"
color="PURPLE"
size="small"
onClick={() => router.back()}
/>
<Button
text="수정하기"
color="PURPLE"
size="small"
onClick={handleSubmit}
/>
</div>
</Container>
</div>
)
}

export default BoardUpdatePage
Loading

0 comments on commit 5dab8ff

Please sign in to comment.