Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
144 changes: 115 additions & 29 deletions front-end/src/app/posts/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,81 @@

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useSelector } from 'react-redux';
import { updatePost } from '@/app/api/postAPI';
import apiClient from '@/components/axios/apiClient';
import { RootState } from '@/redux/store';
import '@/styles/pages/post/post.scss';

interface Post {
id: number;
title: string;
content: string;
category: 'FREE' | 'QNA';
writtenAt: string;
email: string;
nickname: string;
}

export default function EditPost({ params }: { params: { id: string } }) {
const router = useRouter();
const userState = useSelector((state: RootState) => state.user);
const currentUser = userState.userInfo;

const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [post, setPost] = useState<Post | null>(null);

useEffect(() => {
const fetchPost = async () => {
try {
const response = await apiClient.get(`/post/${params.id}`);
const { title, content } = response.data;
setTitle(title);
setContent(content);
// 게시글 목록에서 해당 게시글 찾기
let foundPost = null;
const categories = ['FREE', 'QNA'];
for (const category of categories) {
const response = await apiClient.get(`/post/category/${category}?page=0`);
foundPost = response.data.content.find((post: Post) => post.id.toString() === params.id);
if (foundPost) break;
}

if (!foundPost) {
throw new Error('게시글을 찾을 수 없습니다.');
}

// 현재 사용자가 게시글 작성자인지 확인
if (currentUser && foundPost.email !== currentUser.email) {
throw new Error('게시글을 수정할 권한이 없습니다.');
}

setPost(foundPost);
setTitle(foundPost.title);
setContent(foundPost.content);
} catch (error) {
console.error('게시글 불러오기 실패:', error);
alert('게시글을 불러오지 못했습니다.');
router.back();
setError(error instanceof Error ? error.message : '게시글을 불러오지 못했습니다.');
} finally {
setLoading(false);
}
};

fetchPost();
}, [params.id, router]);
}, [params.id, currentUser]);

const handleUpdate = async () => {
if (!title || !content) {
alert('제목과 내용을 입력해주세요.');
if (!title.trim()) {
alert('제목을 입력해주세요.');
return;
}

if (!content.trim()) {
alert('내용을 입력해주세요.');
return;
}

try {
await updatePost(params.id, { title, content });
await updatePost(params.id, { title: title.trim(), content: content.trim() });
alert('게시글이 수정되었습니다.');
router.push(`/posts/${params.id}`);
} catch (error) {
Expand All @@ -47,30 +85,78 @@ export default function EditPost({ params }: { params: { id: string } }) {
}
};

if (loading) return <p>게시글을 불러오는 중입니다...</p>;
const handleCancel = () => {
if (title !== post?.title || content !== post?.content) {
const shouldCancel = confirm('수정사항이 있습니다. 정말로 취소하시겠습니까?');
if (!shouldCancel) return;
}
router.push(`/posts/${params.id}`);
};

if (loading) {
return (
<div className="posts_container new_post">
<div className="inner">
<div className="loading_state">
<div className="icon">📝</div>
<p className="message">게시글을 불러오는 중...</p>
</div>
</div>
</div>
);
}

if (error) {
return (
<div className="posts_container new_post">
<div className="inner">
<div className="error_state">
<div className="icon">⚠️</div>
<p className="message">{error}</p>
<button className="back_button" onClick={() => router.push('/posts')}>
목록으로 돌아가기
</button>
</div>
</div>
</div>
);
}

return (
<div className="posts_container new_post">
<div className="inner">
<h2>게시글 수정</h2>
<label htmlFor="title">제목</label>
<input
id="title"
type="text"
placeholder="제목을 입력하세요"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<label htmlFor="content">내용</label>
<textarea
id="content"
placeholder="내용을 입력하세요"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<h2 className="section_title">게시글 수정</h2>

<div className="form_group">
<label htmlFor="title">제목</label>
<input
id="title"
type="text"
placeholder="제목을 입력하세요"
value={title}
onChange={(e) => setTitle(e.target.value)}
maxLength={100}
/>
</div>

<div className="form_group">
<label htmlFor="content">내용</label>
<textarea
id="content"
placeholder="내용을 입력하세요"
value={content}
onChange={(e) => setContent(e.target.value)}
maxLength={2000}
/>
</div>

<div className="button_group">
<button onClick={handleUpdate}>수정</button>
<button onClick={() => router.push(`/posts/${params.id}`)}>취소</button>
<button className="submit_btn" onClick={handleUpdate}>
수정 완료
</button>
<button className="cancel_btn" onClick={handleCancel}>
취소
</button>
</div>
</div>
</div>
Expand Down
79 changes: 67 additions & 12 deletions front-end/src/app/posts/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useSelector } from 'react-redux';
import { fetchPosts, deletePost } from '@/app/api/postAPI';
import { RootState } from '@/redux/store';
import '@/styles/pages/post/post.scss';

interface Post {
Expand All @@ -17,10 +19,15 @@ interface Post {

export default function PostDetail({ params }: { params: { id: string } }) {
const router = useRouter();
const userState = useSelector((state: RootState) => state.user);
const currentUser = userState.userInfo;
const [post, setPost] = useState<Post | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

// 현재 사용자가 게시글 작성자인지 확인
const isAuthor = currentUser && post && currentUser.email === post.email;

useEffect(() => {
const fetchPost = async () => {
try {
Expand All @@ -47,6 +54,10 @@ export default function PostDetail({ params }: { params: { id: string } }) {
}, [params.id]);

const handleDelete = async () => {
if (!confirm('정말로 이 게시글을 삭제하시겠습니까?')) {
return;
}

try {
await deletePost(params.id);
alert('게시글이 삭제되었습니다.');
Expand All @@ -57,23 +68,67 @@ export default function PostDetail({ params }: { params: { id: string } }) {
}
};

if (loading) return <p>게시글을 불러오는 중...</p>;
if (error) return <p>{error}</p>;
if (loading) {
return (
<div className="posts_container post_detail">
<div className="inner">
<div className="loading_state">
<div className="icon">📖</div>
<p className="message">게시글을 불러오는 중...</p>
</div>
</div>
</div>
);
}

if (error) {
return (
<div className="posts_container post_detail">
<div className="inner">
<div className="error_state">
<div className="icon">⚠️</div>
<p className="message">{error}</p>
</div>
</div>
</div>
);
}

return (
<div className="posts_container post_detail">
<div className="inner">
<h2>{post?.title}</h2>
<p>카테고리: {post?.category === 'FREE' ? '자유게시판' : '질문게시판'}</p>
<p>
작성자: {post?.nickname} ({post?.email})
</p>
<p>작성일: {post?.writtenAt ? new Date(post.writtenAt).toLocaleString() : '-'}</p>
<p>{post?.content}</p>
<div className="post_header">
<h2 className="section_title">{post?.title}</h2>
<div className="post_meta">
<span className="category">
{post?.category === 'FREE' ? '자유게시판' : '질문게시판'}
</span>
<span className="author">작성자: {post?.nickname}</span>
<span className="date">
{post?.writtenAt ? new Date(post.writtenAt).toLocaleDateString() : '-'}
</span>
</div>
</div>

<div className="post_content">
<p>{post?.content}</p>
</div>

<button onClick={() => router.push(`/posts/${params.id}/edit`)}>수정</button>
<button onClick={handleDelete}>삭제</button>
<button onClick={() => router.push('/posts')}>목록으로</button>
<div className="post_actions">
{isAuthor && (
<>
<button className="edit_btn" onClick={() => router.push(`/posts/${params.id}/edit`)}>
수정
</button>
<button className="delete_btn" onClick={handleDelete}>
삭제
</button>
</>
)}
<button className="list_btn" onClick={() => router.push('/posts')}>
목록으로
</button>
</div>
</div>
</div>
);
Expand Down
64 changes: 38 additions & 26 deletions front-end/src/app/posts/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,34 +50,46 @@ export default function PostForm() {
return (
<div className="posts_container new_post">
<div className="inner">
<h2 className="section_title">커뮤니티 게시판</h2>
<label>카테고리</label>
<select
value={newPost.category}
onChange={(e) => setNewPost({ ...newPost, category: e.target.value as 'FREE' | 'QNA' })}
>
<option value="FREE">자유게시판</option>
<option value="QNA">질문게시판</option>
</select>
<h2 className="section_title">새 글 작성</h2>

<input
type="text"
placeholder="제목"
value={newPost.title}
onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
/>
<textarea
placeholder="내용"
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
/>
<div className="form_group">
<label>카테고리</label>
<select
value={newPost.category}
onChange={(e) => setNewPost({ ...newPost, category: e.target.value as 'FREE' | 'QNA' })}
>
<option value="FREE">자유게시판</option>
<option value="QNA">질문게시판</option>
</select>
</div>

<button onClick={handleSubmit} disabled={loading}>
{loading ? '작성 중...' : '작성'}
</button>
<button onClick={() => router.back()} disabled={loading}>
취소
</button>
<div className="form_group">
<label>제목</label>
<input
type="text"
placeholder="제목을 입력하세요"
value={newPost.title}
onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
/>
</div>

<div className="form_group">
<label>내용</label>
<textarea
placeholder="내용을 입력하세요"
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
/>
</div>

<div className="button_group">
<button className="submit_btn" onClick={handleSubmit} disabled={loading}>
{loading ? '작성 중...' : '작성하기'}
</button>
<button className="cancel_btn" onClick={() => router.back()} disabled={loading}>
취소
</button>
</div>
</div>
</div>
);
Expand Down
Loading