From 1022b51394a619c23a6b6b5d013f4b20c1ab76d8 Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Mon, 24 Mar 2025 18:58:47 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/entities/post/lib/error-messages.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/entities/post/lib/error-messages.ts b/apps/web/entities/post/lib/error-messages.ts index c01da3b..8e35011 100644 --- a/apps/web/entities/post/lib/error-messages.ts +++ b/apps/web/entities/post/lib/error-messages.ts @@ -1,7 +1,9 @@ export const ERROR_MESSAGES = { INVALID_TYPE: '유효하지 않은 입력 형식입니다.', + INVALID_ID: '유효하지 않은 게시글 입니다.', REQUIRED_TITLE: '제목을 입력해주세요', REQUIRED_CONTENT: '내용을 입력해주세요', REQUIRED_AUTHOR: '작성자를 입력해주세요', SAVE_FAILED: '게시글 저장에 실패했습니다:', + UPDATE_FAILED: '게시글 수정에 실패했습니다.', } as const; From da53854a9ba510beaf272747eee7c819e4d78f65 Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Mon, 24 Mar 2025 19:43:08 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20Public=20API=20=EB=B0=8F=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/entities/post/index.ts | 2 +- apps/web/features/post/actions/index.ts | 2 +- .../web/features/post/actions/post-actions.ts | 36 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/web/entities/post/index.ts b/apps/web/entities/post/index.ts index e2d601d..a75804a 100644 --- a/apps/web/entities/post/index.ts +++ b/apps/web/entities/post/index.ts @@ -1,4 +1,4 @@ -export { createPost, getPostById, getPosts } from './api/post-api'; +export { createPost, getPostById, getPosts, updatePost } from './api/post-api'; export { ERROR_MESSAGES } from './lib/error-messages'; export { validateFormField } from './lib/form-validation'; export type { CreatePostDTO, Post, UpdatePostDTO } from './model/post-types'; diff --git a/apps/web/features/post/actions/index.ts b/apps/web/features/post/actions/index.ts index e6f3ea0..315c60b 100644 --- a/apps/web/features/post/actions/index.ts +++ b/apps/web/features/post/actions/index.ts @@ -1 +1 @@ -export { createPostAction } from './post-actions'; +export { createPostAction, updatePostAction } from './post-actions'; diff --git a/apps/web/features/post/actions/post-actions.ts b/apps/web/features/post/actions/post-actions.ts index a953795..be9e5f2 100644 --- a/apps/web/features/post/actions/post-actions.ts +++ b/apps/web/features/post/actions/post-actions.ts @@ -7,6 +7,8 @@ import { createPost, CreatePostDTO, ERROR_MESSAGES, + updatePost, + UpdatePostDTO, validateFormField, } from '@/entities/post'; @@ -42,3 +44,37 @@ export async function createPostAction(_: unknown, formData: FormData) { error: '처리 중 예상치 못한 문제가 발생했습니다.', }; } +export async function updatePostAction(_: unknown, formData: FormData) { + const postId = formData.get('postId') as string; + if (!postId) { + return { status: false, error: ERROR_MESSAGES.INVALID_ID }; + } + + try { + const updatedData: UpdatePostDTO = { + title: formData.get('title') + ? validateFormField(formData.get('title'), 'title') + : undefined, + content: formData.get('content') + ? validateFormField(formData.get('content'), 'content') + : undefined, + author: formData.get('author') + ? validateFormField(formData.get('author'), 'author') + : undefined, + }; + + await updatePost(postId, updatedData); + + revalidatePath('/'); + } catch (error) { + return { + status: false, + error: + error instanceof Error + ? `${ERROR_MESSAGES.UPDATE_FAILED} ${error.message}` + : `${ERROR_MESSAGES.UPDATE_FAILED} 알 수 없는 오류`, + }; + } + + redirect(`/posts/${postId}`); +} From 745ba33ea30ece324ca9ebc724ee232db6c301fd Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Mon, 24 Mar 2025 20:00:26 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refactor:=20PostForm=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EC=83=9D=EC=84=B1/=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=AA=A8=EB=93=9C=EB=A1=9C=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/post-create/ui/post-form.tsx | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/apps/web/features/post/post-create/ui/post-form.tsx b/apps/web/features/post/post-create/ui/post-form.tsx index 935a58d..30d1219 100644 --- a/apps/web/features/post/post-create/ui/post-form.tsx +++ b/apps/web/features/post/post-create/ui/post-form.tsx @@ -4,32 +4,65 @@ import { Button } from '@workspace/ui/components/button'; import { useRouter } from 'next/navigation'; import { useActionState } from 'react'; -import { createPostAction } from '@/features/post/actions'; +import { Post } from '@/entities/post'; +import { createPostAction, updatePostAction } from '@/features/post/actions'; import { TextField } from '@/shared/ui'; -export function PostForm() { +interface PostFormProps { + post?: Post; +} + +export function PostForm({ post }: PostFormProps) { const router = useRouter(); + const isEditMode = Boolean(post); + const [actionResult, formAction, isPending] = useActionState( - createPostAction, + isEditMode ? updatePostAction : createPostAction, null ); return (
-

새 게시글 작성

- +

+ {isEditMode ? '게시글 수정' : '새 게시글 작성'} +

+ + {post?.id && } + + + - + {actionResult?.status === false && (

{actionResult.error}

)} - + + ); } From 72bcb69f3402dbeef6e8898e651218e00c923c1b Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Wed, 26 Mar 2025 18:32:51 +0900 Subject: [PATCH 08/12] =?UTF-8?q?refactor:=20updatePostAction=EC=97=90?= =?UTF-8?q?=EC=84=9C=20validation=20=EB=8B=A8=EA=B3=84=20=EB=82=98?= =?UTF-8?q?=EB=88=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/features/post/actions/post-actions.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/web/features/post/actions/post-actions.ts b/apps/web/features/post/actions/post-actions.ts index be9e5f2..a3c8dab 100644 --- a/apps/web/features/post/actions/post-actions.ts +++ b/apps/web/features/post/actions/post-actions.ts @@ -51,19 +51,21 @@ export async function updatePostAction(_: unknown, formData: FormData) { } try { - const updatedData: UpdatePostDTO = { - title: formData.get('title') - ? validateFormField(formData.get('title'), 'title') - : undefined, - content: formData.get('content') - ? validateFormField(formData.get('content'), 'content') - : undefined, - author: formData.get('author') - ? validateFormField(formData.get('author'), 'author') - : undefined, - }; + const rawTitle = formData.get('title'); + const rawContent = formData.get('content'); + const rawAuthor = formData.get('author'); + + const title = rawTitle ? validateFormField(rawTitle, 'title') : undefined; + const content = rawContent + ? validateFormField(rawContent, 'content') + : undefined; + const author = rawAuthor + ? validateFormField(rawAuthor, 'author') + : undefined; + + const validatedData: UpdatePostDTO = { title, content, author }; - await updatePost(postId, updatedData); + await updatePost(postId, validatedData); revalidatePath('/'); } catch (error) { From 199b47ede41bd0bf04aa2250ad2b27fae29b8b2a Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Wed, 26 Mar 2025 18:35:20 +0900 Subject: [PATCH 09/12] =?UTF-8?q?refactor:=20updatePostAction=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=84=EB=93=9C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=EB=A5=BC=20=EB=B0=98=EB=B3=B5=EB=AC=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/features/post/actions/post-actions.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/web/features/post/actions/post-actions.ts b/apps/web/features/post/actions/post-actions.ts index a3c8dab..f8650f9 100644 --- a/apps/web/features/post/actions/post-actions.ts +++ b/apps/web/features/post/actions/post-actions.ts @@ -51,19 +51,15 @@ export async function updatePostAction(_: unknown, formData: FormData) { } try { - const rawTitle = formData.get('title'); - const rawContent = formData.get('content'); - const rawAuthor = formData.get('author'); + const fields: (keyof UpdatePostDTO)[] = ['title', 'content', 'author']; + const validatedData: UpdatePostDTO = {}; - const title = rawTitle ? validateFormField(rawTitle, 'title') : undefined; - const content = rawContent - ? validateFormField(rawContent, 'content') - : undefined; - const author = rawAuthor - ? validateFormField(rawAuthor, 'author') - : undefined; - - const validatedData: UpdatePostDTO = { title, content, author }; + for (const field of fields) { + const value = formData.get(field); + if (value) { + validatedData[field] = validateFormField(value, field); + } + } await updatePost(postId, validatedData); From 769bf17dd17a0d0b85a7f3a4cc1dc29c39e48b21 Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Wed, 26 Mar 2025 18:38:59 +0900 Subject: [PATCH 10/12] =?UTF-8?q?refactor:=20getValidatedField=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=8F=84=EC=9E=85=ED=95=98=EC=97=AC=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/features/post/actions/post-actions.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/web/features/post/actions/post-actions.ts b/apps/web/features/post/actions/post-actions.ts index f8650f9..f17a708 100644 --- a/apps/web/features/post/actions/post-actions.ts +++ b/apps/web/features/post/actions/post-actions.ts @@ -52,14 +52,17 @@ export async function updatePostAction(_: unknown, formData: FormData) { try { const fields: (keyof UpdatePostDTO)[] = ['title', 'content', 'author']; - const validatedData: UpdatePostDTO = {}; - for (const field of fields) { + const getValidatedField = (field: K) => { const value = formData.get(field); - if (value) { - validatedData[field] = validateFormField(value, field); - } - } + return value ? validateFormField(value, field) : undefined; + }; + + const validatedData = fields.reduce((acc, field) => { + const validatedValue = getValidatedField(field); + if (validatedValue !== undefined) acc[field] = validatedValue; + return acc; + }, {} as UpdatePostDTO); await updatePost(postId, validatedData); From 73ef6d4714c1176414422681a6eabbe0c3894d63 Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Wed, 26 Mar 2025 18:40:44 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20getValidatedField=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=9E=AC=EC=82=AC=EC=9A=A9=EC=84=B1=20=ED=96=A5?= =?UTF-8?q?=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/entities/post/index.ts | 2 +- apps/web/entities/post/lib/form-validation.ts | 10 +++++++++- apps/web/features/post/actions/post-actions.ts | 14 +++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/web/entities/post/index.ts b/apps/web/entities/post/index.ts index 6c547d6..e9f0d83 100644 --- a/apps/web/entities/post/index.ts +++ b/apps/web/entities/post/index.ts @@ -1,6 +1,6 @@ export { createPost, getPostById, getPosts, updatePost } from './api/post-api'; export { ERROR_MESSAGES } from './lib/error-messages'; -export { validateFormField } from './lib/form-validation'; +export { getValidatedField, validateFormField } from './lib/form-validation'; export type { CreatePostDTO, GetPostsCursor, diff --git a/apps/web/entities/post/lib/form-validation.ts b/apps/web/entities/post/lib/form-validation.ts index ec203c3..10b2b8f 100644 --- a/apps/web/entities/post/lib/form-validation.ts +++ b/apps/web/entities/post/lib/form-validation.ts @@ -1,4 +1,4 @@ -import { ERROR_MESSAGES } from '@/entities/post/'; +import { ERROR_MESSAGES, UpdatePostDTO } from '@/entities/post/'; export const validateFormField = ( value: FormDataEntryValue | null, @@ -16,3 +16,11 @@ export const validateFormField = ( return value; }; + +export const getValidatedField = ( + formData: FormData, + field: K +) => { + const value = formData.get(field); + return value ? validateFormField(value, field) : undefined; +}; diff --git a/apps/web/features/post/actions/post-actions.ts b/apps/web/features/post/actions/post-actions.ts index f17a708..e7bccfe 100644 --- a/apps/web/features/post/actions/post-actions.ts +++ b/apps/web/features/post/actions/post-actions.ts @@ -7,6 +7,7 @@ import { createPost, CreatePostDTO, ERROR_MESSAGES, + getValidatedField, updatePost, UpdatePostDTO, validateFormField, @@ -51,15 +52,14 @@ export async function updatePostAction(_: unknown, formData: FormData) { } try { - const fields: (keyof UpdatePostDTO)[] = ['title', 'content', 'author']; - - const getValidatedField = (field: K) => { - const value = formData.get(field); - return value ? validateFormField(value, field) : undefined; - }; + const fields = [ + 'title', + 'content', + 'author', + ] as const satisfies readonly (keyof UpdatePostDTO)[]; const validatedData = fields.reduce((acc, field) => { - const validatedValue = getValidatedField(field); + const validatedValue = getValidatedField(formData, field); if (validatedValue !== undefined) acc[field] = validatedValue; return acc; }, {} as UpdatePostDTO); From 06460224e02582a9143f721973eb4a069aab580e Mon Sep 17 00:00:00 2001 From: Seono-Na Date: Sun, 30 Mar 2025 18:59:20 +0900 Subject: [PATCH 12/12] =?UTF-8?q?refactor:=20=ED=8F=BC=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B2=80=EC=A6=9D=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/entities/post/index.ts | 7 +++- apps/web/entities/post/lib/form-validation.ts | 37 ++++++++++++++-- .../web/features/post/actions/post-actions.ts | 42 +++++++------------ 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/apps/web/entities/post/index.ts b/apps/web/entities/post/index.ts index e9f0d83..c31956d 100644 --- a/apps/web/entities/post/index.ts +++ b/apps/web/entities/post/index.ts @@ -1,6 +1,11 @@ export { createPost, getPostById, getPosts, updatePost } from './api/post-api'; export { ERROR_MESSAGES } from './lib/error-messages'; -export { getValidatedField, validateFormField } from './lib/form-validation'; +export { + extractFormData, + getValidatedField, + getValidatedFields, + validateFormField, +} from './lib/form-validation'; export type { CreatePostDTO, GetPostsCursor, diff --git a/apps/web/entities/post/lib/form-validation.ts b/apps/web/entities/post/lib/form-validation.ts index 10b2b8f..237906e 100644 --- a/apps/web/entities/post/lib/form-validation.ts +++ b/apps/web/entities/post/lib/form-validation.ts @@ -1,8 +1,21 @@ -import { ERROR_MESSAGES, UpdatePostDTO } from '@/entities/post/'; +import { CreatePostDTO, ERROR_MESSAGES, UpdatePostDTO } from '@/entities/post/'; + +export const extractFormData = >( + formData: FormData, + fields: Extract[] +): Partial, string | null>> => { + return fields.reduce( + (acc, field) => { + acc[field] = formData.get(field) as string | null; + return acc; + }, + {} as Partial, string | null>> + ); +}; export const validateFormField = ( value: FormDataEntryValue | null, - fieldName: 'title' | 'content' | 'author' + fieldName: keyof (CreatePostDTO & UpdatePostDTO) ): string => { if (typeof value !== 'string') { throw new Error(ERROR_MESSAGES.INVALID_TYPE); @@ -17,10 +30,28 @@ export const validateFormField = ( return value; }; -export const getValidatedField = ( +export const getValidatedField = < + K extends keyof (CreatePostDTO & UpdatePostDTO), +>( formData: FormData, field: K ) => { const value = formData.get(field); return value ? validateFormField(value, field) : undefined; }; + +export function getValidatedFields( + rawData: Partial> +): T { + return Object.fromEntries( + Object.entries(rawData) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => [ + key, + validateFormField( + value ?? null, + key as keyof (CreatePostDTO & UpdatePostDTO) + ), + ]) + ) as T; +} diff --git a/apps/web/features/post/actions/post-actions.ts b/apps/web/features/post/actions/post-actions.ts index e7bccfe..57fdc7b 100644 --- a/apps/web/features/post/actions/post-actions.ts +++ b/apps/web/features/post/actions/post-actions.ts @@ -7,23 +7,25 @@ import { createPost, CreatePostDTO, ERROR_MESSAGES, - getValidatedField, + extractFormData, + getValidatedFields, updatePost, UpdatePostDTO, - validateFormField, } from '@/entities/post'; +// 게시글 작성 서버 액션 export async function createPostAction(_: unknown, formData: FormData) { let postId: string | null = null; try { - const title = validateFormField(formData.get('title'), 'title'); - const content = validateFormField(formData.get('content'), 'content'); - const author = validateFormField(formData.get('author'), 'author'); - - const postData: CreatePostDTO = { title, content, author }; + const rawData = extractFormData(formData, [ + 'title', + 'content', + 'author', + ]); + const validatedData: CreatePostDTO = getValidatedFields(rawData); - postId = (await createPost(postData)).id; + postId = (await createPost(validatedData)).id; revalidatePath('/'); } catch (error) { @@ -35,16 +37,10 @@ export async function createPostAction(_: unknown, formData: FormData) { : `${ERROR_MESSAGES.SAVE_FAILED} 알 수 없는 오류`, }; } - - if (postId) { - redirect(`/posts/${postId}`); - } - - return { - status: false, - error: '처리 중 예상치 못한 문제가 발생했습니다.', - }; + redirect(`/posts/${postId}`); } + +// 게시글 수정 서버 액션 export async function updatePostAction(_: unknown, formData: FormData) { const postId = formData.get('postId') as string; if (!postId) { @@ -52,17 +48,12 @@ export async function updatePostAction(_: unknown, formData: FormData) { } try { - const fields = [ + const rawData = extractFormData(formData, [ 'title', 'content', 'author', - ] as const satisfies readonly (keyof UpdatePostDTO)[]; - - const validatedData = fields.reduce((acc, field) => { - const validatedValue = getValidatedField(formData, field); - if (validatedValue !== undefined) acc[field] = validatedValue; - return acc; - }, {} as UpdatePostDTO); + ]); + const validatedData: UpdatePostDTO = getValidatedFields(rawData); await updatePost(postId, validatedData); @@ -76,6 +67,5 @@ export async function updatePostAction(_: unknown, formData: FormData) { : `${ERROR_MESSAGES.UPDATE_FAILED} 알 수 없는 오류`, }; } - redirect(`/posts/${postId}`); }