Skip to content

Commit

Permalink
✨ Feat(create-post): 게시글 생성 기능 추가
Browse files Browse the repository at this point in the history
related to: #169
  • Loading branch information
ppochaco committed Aug 15, 2024
1 parent 9af61c0 commit 53861f2
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
'use client'

import { useEffect, useMemo, useState } from 'react'
import { useFormContext } from 'react-hook-form'

import { Block, BlockNoteEditor, PartialBlock } from '@blocknote/core'
import { BlockNoteView } from '@blocknote/mantine'
import '@blocknote/mantine/style.css'
import { useCreateBlockNote } from '@blocknote/react'

import { usePostEditorStore } from '~create-post/_store/post-editor'
import { FormField, FormItem, FormMessage } from '@/components/ui/form'
import { CreatePost } from '@/schema/post'

export const PostContentFieldEditor = () => {
const { setPostContent, getPostContent } = usePostEditorStore()
const [initialContent, setInitialContent] = useState<
'loading' | PartialBlock[] | undefined
>('loading')
const { control } = useFormContext<CreatePost>()

useEffect(() => {
const storedBlocks = getPostContent()
setInitialContent(storedBlocks)
}, [getPostContent])

const editor = useMemo(() => {
if (initialContent === 'loading') {
return undefined
}
return BlockNoteEditor.create({ initialContent })
}, [initialContent])

if (editor === undefined) {
return <div>글 불러오는 중...</div>
}
const editor = useCreateBlockNote({ initialContent: [{}] })

return (
<BlockNoteView
editor={editor}
onChange={() => {
setPostContent(editor.document as Block[])
}}
className="h-96 overflow-auto rounded-md border pt-4"
<FormField
control={control}
name="postContent"
render={({ field }) => (
<FormItem>
<BlockNoteView
editor={editor}
onChange={() => field.onChange(JSON.stringify(editor.document))}
className="h-96 overflow-auto rounded-md border pt-4"
/>
<div className="flex justify-end">
<FormMessage />
</div>
</FormItem>
)}
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
'use client'

import { useEffect } from 'react'
import { useForm } from 'react-hook-form'

import { zodResolver } from '@hookform/resolvers/zod'
import { useAction } from 'next-safe-action/hooks'
import { usePathname, useRouter } from 'next/navigation'

import { Button } from '@/components/ui/button'
import { Form } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Seperator } from '@/components/ui/seperator'
import { useToast } from '@/components/ui/use-toast'
import { CreatePost, CreatePostSchema } from '@/schema/post'
import { createActivityPostAction } from '@/service/server/post/create-post'

import { ActivityFormField } from '~activity/_components/ActivityFormField'
import { ActivityImageInput } from '~activity/_components/ActivityImageInput'
import { usePostEditorStore } from '~create-post/_store/post-editor'

import { ActivityDateFieldDialog } from './ActivityDateFieldDialog'
import { PostContentFieldEditor } from './PostContentFieldEditor'

export const CreatePostForm = () => {
const getPostContent = usePostEditorStore((state) => state.getPostContent)
const clearPostContent = usePostEditorStore((state) => state.clearPostContent)
type CreatePostFormProps = {
boardId: number
}

export const CreatePostForm = ({ boardId }: CreatePostFormProps) => {
const { toast } = useToast()
const router = useRouter()
const pathName = usePathname()

const basePath = pathName.split('/').slice(0, -1).join('/')

const {
execute: createPost,
result,
isExecuting,
} = useAction(createActivityPostAction)

const form = useForm<CreatePost>({
resolver: zodResolver(CreatePostSchema),
defaultValues: {
boardId,
postTitle: '',
postContent: '',
imageFile: new File([], ''),
Expand All @@ -33,22 +52,24 @@ export const CreatePostForm = () => {
},
})

const onSubmit = (values: CreatePost) => {
console.log(values)
}
useEffect(() => {
if (result.data?.isSuccess) {
toast({
title: result.data.message,
duration: 3000,
})
router.push(basePath)
}
}, [result])

const onClick = () => {
const storedContent = getPostContent()
form.setValue('postContent', JSON.stringify(storedContent))
clearPostContent()

form.handleSubmit(onSubmit)()
const onSubmit = (values: CreatePost) => {
createPost(values)
}

return (
<Form {...form}>
<form
onSubmit={(e) => e.preventDefault()}
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<ActivityFormField name="postTitle" label="게시글 제목">
Expand All @@ -63,7 +84,7 @@ export const CreatePostForm = () => {
{(field) => <ActivityImageInput field={field} />}
</ActivityFormField>
<div className="flex justify-end">
<Button type="submit" onClick={onClick}>
<Button type="submit" disabled={isExecuting}>
게시글 업로드
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const CreatePostPage = ({ params }: CreatePostPageParams) => {
activityId={Number(params.activityId)}
boardId={Number(params.boardId)}
/>
<CreatePostForm />
<CreatePostForm boardId={Number(params.boardId)} />
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ControllerRenderProps, FieldValues } from 'react-hook-form'
import { ControllerRenderProps } from 'react-hook-form'

import { Input } from '@/components/ui/input'

Expand Down
14 changes: 10 additions & 4 deletions src/schema/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import { z } from 'zod'

export const CreatePostSchema = z.object({
boardId: z.number(),
postTitle: z
.string()
.min(1, { message: '게시글 제목을 입력해주세요.' })
.max(50, { message: '게시글 제목은 50자 이내이어야 합니다.' }),
postContent: z.string(),
imageFile: z.instanceof(File).refine((f) => f.size < 5000000, {
message: '이미지 파일 크기는 5MB 이하만 가능합니다.',
}),
postContent: z.string().min(1, { message: '게시글 제목을 입력해주세요.' }),
imageFile: z
.instanceof(File)
.refine((f) => f.size < 5000000, {
message: '이미지 파일 크기는 5MB 이하만 가능합니다.',
})
.refine((f) => !!f.name, {
message: '게시글 대표 이미지를 선택해주세요.',
}),
activityDate: z
.object({
start: z.date().optional(),
Expand Down
76 changes: 76 additions & 0 deletions src/service/server/post/create-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { kstFormat } from '@toss/date'
import { AxiosError } from 'axios'
import { z } from 'zod'

import { API_ERROR_MESSAGES } from '@/constant/errorMessage'
import { actionClient } from '@/lib/safe-action'
import { AUTHORIZATION_API, BACKEND_API } from '@/service/config'

import { generatePresignedUrl } from './index'

const CreatePostServerSchema = z.object({
boardId: z.number(),
postTitle: z.string(),
postContent: z.string(),
imageFile: z.instanceof(File),
activityDate: z.object({
start: z.date().optional(),
end: z.date().optional(),
}),
})

type CreatePostRequest = {
postTitle: string
postContent: string
postImageUrl: string
postActivityStartDate?: string
postActivityEndDate?: string
postType: 'ACTIVITY' | 'NOTICE' | 'EVENT'
}

export const createActivityPostAction = actionClient
.schema(CreatePostServerSchema)
.action(
async ({
parsedInput: { boardId, postTitle, postContent, imageFile, activityDate },
}) => {
try {
const { preSignedUrl, imageUrl } = await generatePresignedUrl()

await BACKEND_API.put(preSignedUrl, imageFile, {
headers: {
'Content-Type': imageFile.type,
},
})

if (!activityDate.start) throw new Error('날짜 입력 에러')

const tempDateFormat = kstFormat(activityDate.start, 'yyyy-MM-dd') // date input 수정 전까지 임시 날짜 사용

const createActivityPostRequest: CreatePostRequest = {
postTitle,
postContent,
postImageUrl: imageUrl,
postActivityStartDate: tempDateFormat,
postActivityEndDate: tempDateFormat,
postType: 'ACTIVITY',
}

const response = await AUTHORIZATION_API.post(
`/boards/${boardId}/posts`,
createActivityPostRequest,
)

return { isSuccess: true, message: response.data.message }
} catch (error) {
if (error instanceof AxiosError) {
const response = error.response

if (response?.status === 404) {
return { message: response.data.message }
}
}
return { message: API_ERROR_MESSAGES.UNKNOWN_ERROR }
}
},
)
14 changes: 14 additions & 0 deletions src/service/server/post/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AUTHORIZATION_API } from '@/service/config'

type generatePresignedUrlResposne = {
preSignedUrl: string
imageUrl: string
}

export const generatePresignedUrl = async () => {
const response = await AUTHORIZATION_API.get<generatePresignedUrlResposne>(
'/posts/generate-presigned-url',
)

return response.data
}

0 comments on commit 53861f2

Please sign in to comment.