Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/web/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from 'next/link';

export default function NotFound() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-6">
<div className="w-full max-w-md rounded-lg bg-white p-8 text-center shadow-lg">
<h2 className="mb-4 text-2xl font-bold text-gray-800">
어머! 없어요...
</h2>
<p className="mb-6 text-gray-600">아니 없어요 그냥...</p>
<Link
href="/"
className="inline-block rounded bg-blue-500 px-6 py-3 text-white transition hover:bg-blue-600"
>
홈으로 가기
</Link>
</div>
</div>
);
}
3 changes: 0 additions & 3 deletions apps/web/app/posts/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import Link from 'next/link';

import { PostDetail } from '@/features/post/post-detail';

// 특정 게시글 페이지 - 게시글 생성 후 넘어가지는지 확인용으로 만든 임시 페이지
export default function PostPage() {
return (
<main>
<PostDetail />
<Link href={'/'}>목록으로</Link>
</main>
);
}
23 changes: 23 additions & 0 deletions apps/web/app/posts/edit/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { notFound } from 'next/navigation';

import { getPostById } from '@/entities/post';
import { PostForm } from '@/features/post/post-editor';

interface PostEditPageProps {
params: { id: string };
}

export default async function PostEditPage({ params }: PostEditPageProps) {
const postId = (await params).id;
const post = await getPostById(postId);

if (!post) {
notFound();
}

return (
<main className="mx-auto max-w-2xl p-6">
<PostForm post={post} />
</main>
);
}
4 changes: 2 additions & 2 deletions apps/web/app/posts/write/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PostForm } from '@/features/post/post-create';
import { PostForm } from '@/features/post/post-editor';

export default function NewPostPage() {
return (
<main>
<main className="mx-auto max-w-2xl p-6">
<PostForm />
</main>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/entities/post/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/entities/post/lib/error-messages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export const ERROR_MESSAGES = {
INVALID_TYPE: '유효하지 않은 입력 형식입니다.',
INVALID_ID: '유효하지 않은 게시글 입니다.',
REQUIRED_TITLE: '제목을 입력해주세요',
REQUIRED_CONTENT: '내용을 입력해주세요',
REQUIRED_AUTHOR: '작성자를 입력해주세요',
SAVE_FAILED: '게시글 저장에 실패했습니다:',
UPDATE_FAILED: '게시글 수정에 실패했습니다.',
} as const;
2 changes: 1 addition & 1 deletion apps/web/features/post/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { createPostAction } from './post-actions';
export { createPostAction, updatePostAction } from './post-actions';
36 changes: 36 additions & 0 deletions apps/web/features/post/actions/post-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
createPost,
CreatePostDTO,
ERROR_MESSAGES,
updatePost,
UpdatePostDTO,
validateFormField,
} from '@/entities/post';

Expand Down Expand Up @@ -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,
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 updateData 객체를 생성하면서 값 추출과 밸리데이션이 함께 이루어지고 있는데, 객체를 생성하면서 바로 밸리데이션 검증을 진행하기보다는,

밸리데이션을 별도의 단계로 나눠서 유효성 검사를 진행하고, 문제가 없다는 것이 확인된 후에 updateData 객체를 생성하거나 다음 로직을 실행하는 흐름으로 구성하면 가독성이 더 좋아질 것 같습니다. ✨

이렇게 하면 코드 흐름이 더 직관적으로 보이고, 각 단계가 명확하게 구분되어 유지보수에도 도움이 될 것 같아요. 🛠️

이런 방향은 어떠신가요? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@choi1five 님 좋은 리뷰 감사합니다!🙏
피드백 주신 대로 밸리데이션 검증을 별도로 분리하는 방향으로 개선했습니다.

기존에는 updateData 객체를 만들면서 동시에 validateFormField를 호출했는데,
이를 별도의 유효성 검사 함수getValidatedField로 분리하여 먼저 검증을 수행한 후,
reduce를 활용해 validatedData 객체를 생성하는 방식으로 변경했습니다.

getValidatedField의 경우 공용으로 사용할 수 있어 shared/util로 분리할 수 있었지만,
아직 post 관련 부분에서만 사용하고 있고 validateFormField과 함꼐 사용하고 있기 떄문에
entities/post/lib/form-validation.ts안에 넣어두었습니다.

이렇게 수정하면서 코드의 가독성이 향상되었고, 각 단계가 명확히 구분되었습니다.🔧

혹시 더 개선할 부분이 있다면 피드백 부탁드립니다. 감사합니다! 😊


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}`);
}
40 changes: 0 additions & 40 deletions apps/web/features/post/post-create/ui/post-form.tsx

This file was deleted.

35 changes: 25 additions & 10 deletions apps/web/features/post/post-detail/ui/post-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useParams } from 'next/navigation';
import { Button } from '@workspace/ui/components/button';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';

import { getPostById } from '@/entities/post';
Expand All @@ -10,8 +11,9 @@ import { formatToLocaleDate } from '@/shared/lib';
// 특정 게시글 페이지 - 게시글 생성 후 넘어가지는지 확인용으로 만든 임시 컴포넌트
export function PostDetail() {
const { id } = useParams();
const router = useRouter();
const [post, setPost] = useState<Post | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
Expand All @@ -32,16 +34,29 @@ export function PostDetail() {
fetchPost();
}, [id]);

if (isLoading) return <p>로딩 중...</p>;
if (error) return <p>{error}</p>;
if (!post) return <p>게시글을 찾을 수 없습니다.</p>;
if (isLoading) return <p className="text-center text-gray-500">로딩 중...</p>;
if (error) return <p className="text-center text-red-500">{error}</p>;
if (!post)
return (
<p className="text-center text-gray-500">게시글을 찾을 수 없습니다.</p>
);

return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
<p>작성자: {post.author}</p>
<p>작성일: {formatToLocaleDate(post.createdAt)}</p>
<div className="mx-auto max-w-2xl space-y-4 rounded-lg bg-white p-6 shadow-md">
<h1 className="text-2xl font-bold text-gray-900">{post.title}</h1>
<p className="whitespace-pre-line text-gray-700">{post.content}</p>
<div className="text-sm text-gray-500">
<p>작성자: {post.author}</p>
<p>작성일: {formatToLocaleDate(post.createdAt)}</p>
</div>
<div className="mt-4 flex justify-between">
<Button variant="outline" onClick={() => router.push('/')}>
목록으로
</Button>
<Button onClick={() => router.push(`/posts/edit/${post.id}`)}>
수정하기
</Button>
</div>
</div>
);
}
73 changes: 73 additions & 0 deletions apps/web/features/post/post-editor/ui/post-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';

import { Button } from '@workspace/ui/components/button';
import { useRouter } from 'next/navigation';
import { useActionState } from 'react';

import { Post } from '@/entities/post';
import { createPostAction, updatePostAction } from '@/features/post/actions';
import { TextField } from '@/shared/ui';

interface PostFormProps {
post?: Post;
}

export function PostForm({ post }: PostFormProps) {
const router = useRouter();
const isEditMode = Boolean(post);

const [actionResult, formAction, isPending] = useActionState(
isEditMode ? updatePostAction : createPostAction,
null
);

return (
<form action={formAction}>
<h1 className="mb-4 text-xl font-bold">
{isEditMode ? '게시글 수정' : '새 게시글 작성'}
</h1>

{post?.id && <input type="hidden" name="postId" value={post.id} />}

<TextField
name="title"
label="제목"
required
disabled={isPending}
defaultValue={post?.title || ''}
/>
<TextField
name="content"
label="내용"
required
disabled={isPending}
isTextArea
defaultValue={post?.content || ''}
/>
<TextField
name="author"
label="작성자"
required
disabled={isPending}
readOnly={!!post}
defaultValue={post?.author || ''}
/>

{actionResult?.status === false && (
<p className="mt-2 text-sm text-red-500">{actionResult.error}</p>
)}

<Button
type="button"
variant="outline"
onClick={() => router.back()}
disabled={isPending}
>
취소
</Button>
<Button type="submit" disabled={isPending}>
{isPending ? '저장 중...' : '작성하기'}
</Button>
</form>
);
}