Project

[javascript] 네이버 클론 코딩 + 블로그 5: 답글 & 공감

끊임없이 개발하는 새럼 2025. 3. 12. 15:46

🚀 현재 프로젝트 환경 정리

 네이버 스타일 로그인 & 블로그 시스템 구현 중
 사용된 기술 및 개발 도구 정리

코드가 수정될 때마다 게시글 계속 업데이트 중


📌 프로젝트 개요

  • 프로젝트명: 네이버 스타일 로그인 & 블로그 클론 코딩
  • DB: 로컬 스토리지 (LocalStorage) 사용
  • 프레임워크: React (JSX 기반 UI 구성)
  • 백엔드: Node.js
  • 개발 언어: Javascript
  • 개발툴: Visual Studio Code

 

1. 디자인 구상

 

게시글 상세 화면 구현

 

디자인은 실제 네이버 디자인을 참고해서 그렸다.

 

 

 

게시글 상세 화면

 

 

PostDetail.js

import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import "./Blog.css";

function PostDetail() {
    const { id } = useParams();
    const navigate = useNavigate();
    const [post, setPost] = useState(null);
    const [comments, setComments] = useState([]);
    const [commentText, setCommentText] = useState("");
    const [showOptions, setShowOptions] = useState(false); // 수정/삭제 버튼 표시 여부
    const [showComments, setShowComments] = useState(false);  // 댓글 창 상태 추가
    const [likes, setLikes] = useState(10);  // 공감 수 상태 추가
    const loggedInUser = JSON.parse(localStorage.getItem("loggedInUser")); // 현재 로그인한 사용자 정보

    useEffect(() => {
        const savedPosts = JSON.parse(localStorage.getItem("posts")) || [];
        const foundPost = savedPosts.find(p => p.id === Number(id));
        setPost(foundPost);

        const savedComments = JSON.parse(localStorage.getItem(`comments_${id}`)) || [];
        setComments(savedComments);
    }, [id]);

    const handleAddComment = () => {
        if (!commentText.trim()) return;
        const newComment = {
            id: Date.now(),
            text: commentText,
            author: loggedInUser?.username || "익명"
        };

        const updatedComments = [...comments, newComment];
        setComments(updatedComments);
        localStorage.setItem(`comments_${id}`, JSON.stringify(updatedComments));
        setCommentText("");
    };

    const handleDeleteComment = (commentId) => {
        if (!window.confirm("댓글을 삭제하시겠습니까?")) return;
        const updatedComments = comments.filter(comment => comment.id !== commentId);
        setComments(updatedComments);
        localStorage.setItem(`comments_${id}`, JSON.stringify(updatedComments));
    };

    const handleDeletePost = () => {
        if (window.confirm("정말 게시글을 삭제하시겠습니까?")) {
            const savedPosts = JSON.parse(localStorage.getItem("posts")) || [];
            const updatedPosts = savedPosts.filter(p => p.id !== Number(id));
            localStorage.setItem("posts", JSON.stringify(updatedPosts));
            navigate("/blog"); // 삭제 후 메인 페이지로 이동
        }
    };

    const handleEditPost = () => {
        alert("수정 기능 구현 필요");
    };

    const handleKeyPress = (event) => {
        if (event.key === "Enter") {
            handleAddComment();
        }
    };

    if (!post) return <p>게시글을 찾을 수 없습니다.</p>;

    return (
        <div className="post-detail-container">
            <div className="post-header">
                {/* ✅ 제목 (한 줄 차지) */}
                <h2 className="post-title">{post.title}</h2>

                {/* ✅ 작성자 정보 + 점 세 개 버튼 (한 줄 차지) */}
                <div className="post-meta-container">
                    <p className="post-meta">
                        {post.author} | {post.createdAt ? new Date(post.createdAt).toLocaleString() : ""}
                        {loggedInUser?.username === post.author && <span className="author-tag">작성자</span>}
                    </p>

                    {loggedInUser?.username === post.author && (
                        <div className="post-options">
                            <button className="more-button" onClick={() => setShowOptions(!showOptions)}>⋮</button>
                            {showOptions && (
                                <div className="options-menu">
                                    <button onClick={handleEditPost}>수정</button>
                                    <button onClick={handleDeletePost} className="delete-btn">삭제</button>
                                </div>
                            )}
                        </div>
                    )}
                </div>
            </div>

            <hr className="divider" />
            <div className="post-content" dangerouslySetInnerHTML={{ __html: post.content }} />
            <hr className="divider" />
            <div>
            {/* ✅ 공감 & 댓글 버튼 */}
            <div className="interaction-buttons">
                {/* 공감 버튼 */}
                <button className="like-button">
                    <span className="icon">❤️</span> 공감 <span className="dropdown-icon">▼</span>
                </button>

                {/* 댓글 버튼 (토글 기능 추가) */}
                <button className="comment-toggle-button" onClick={() => setShowComments(!showComments)}>
                    <span className="icon">💬</span> 댓글 {comments.length}
                    <span className="dropdown-icon">{showComments ? "▲" : "▼"}</span>
                </button>
            </div>

            {/* ✅ 댓글 리스트 & 입력창 (토글) */}
            {showComments && (
                <div className="comment-section">
                    <ul className="comment-list">
                        {comments.length === 0 ? (
                            <p className="no-comments">댓글이 없습니다.</p>
                        ) : (
                            comments.map(comment => (
                                <li key={comment.id} className="comment-item">
                                    <strong>{comment.author}</strong>
                                    {loggedInUser?.username === comment.author && <span className="author-tag">작성자</span>}
                                    <p>{comment.text}</p>
                                    {loggedInUser?.username === comment.author && (
                                        <button onClick={() => handleDeleteComment(comment.id)} className="delete-comment-button">
                                            삭제
                                        </button>
                                    )}
                                    <hr className="divider" />
                                </li>
                            ))
                        )}
                    </ul>

                    {/* ✅ 댓글 작성 입력창 */}
                    <div className="comment-input-container">
                        <input
                            type="text"
                            placeholder="댓글을 입력하세요"
                            value={commentText}
                            onChange={(e) => setCommentText(e.target.value)}
                            className="comment-input"
                        />
                        <button onClick={handleAddComment} className="comment-button">댓글 작성</button>
                    </div>
                </div>
            )}
        </div>

            <button onClick={() => navigate(-1)} className="back-button">← 뒤로가기</button>
        </div>
    );
}

export default PostDetail;

 

 

Blog.css

.post-detail-container {
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
  border-radius: 8px;
  background-color: #fff;
}

.post-header {
  display: flex;
  flex-direction: column; /* ✅ 제목과 작성자 정보를 다른 줄로 배치 */
  gap: 5px; /* ✅ 제목과 작성자 정보 사이 간격 추가 */
}


/* 제목 왼쪽 정렬 */
.post-title {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 5px;
  text-align: left;
}

/* 작성자 및 날짜 왼쪽 정렬 */
.post-meta {
  margin: 0;
  font-size: 14px;
  color: #333;
  flex-grow: 1; /* ✅ 작성자 정보가 왼쪽으로 정렬됨 */
}

.post-content {
  padding: 20px 0;
}

.author-tag {
  background-color: #04cf5c;
  color: white;
  padding: 2px 5px;
  border-radius: 3px;
  margin-left: 5px;
}


.more-button {
  border: none;
  background: none;
  font-size: 20px;
  cursor: pointer;
}

.options-menu {
  position: absolute;
  top: 100%;
  right: 0;
  background: white;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
  border-radius: 5px;
  width: 80px;
}

.options-menu button {
  display: block;
  width: 100%;
  padding: 8px;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
}

.post-actions {
  display: flex;
  gap: 10px;
  margin-top: 15px;
}

.options-menu button:hover {
  background: #f5f5f5;
}

.delete-btn {
  color: red;
}

/* ✅ 댓글 섹션 */
.comment-section {
  margin-top: 10px;
  padding-top: 10px;
}


/* ✅ 댓글 입력 창 */
.comment-input-container {
  display: flex;
  gap: 10px;
  margin-top: 10px;
}


.comment-input {
  flex: 1;
  padding: 5px;
  border: 1px solid #ddd;
}

.comment-submit {
  background: black;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 5px;
  cursor: pointer;
}

/* ✅ 댓글 리스트 */
.comment-list {
  margin-top: 10px;
  list-style: none;
  padding: 0;
}

.comment-item {
  padding: 5px 0;
}

.no-comments {
  color: #777;
}

.post-options {
  margin-left: auto; /* ✅ 점 세 개 버튼을 오른쪽으로 이동 */
  position: relative;
}

.more-button {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
}

.post-meta-container {
  display: flex;
  align-items: center;
  justify-content: space-between; /* ✅ 작성자 정보는 왼쪽, 점 세 개 버튼은 오른쪽 */
  width: 100%;
}

.divider {
  border: none; /* 기존 기본 테두리 제거 */
  border-top: 1px solid #e0e0e0; /* ✅ 연한 회색으로 변경 */
  margin: 10px 0; /* ✅ 위아래 여백 추가 */
  width: 100%; /* ✅ 가로 길이 전체 적용 */
}



/* like button & comment button */
.interaction-buttons {
  display: flex;
  gap: 10px; /* 버튼 간격 */
  margin-top: 10px;
}

/* ✅ 버튼 공통 스타일 */
.like-button, .comment-toggle-button, .delete-comment-button, .comment-button, .back-button {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 12px;
  font-size: 14px;
  border: 1px solid #ccc;
  background: white;
  cursor: pointer;
  border-radius: 5px;
  transition: 0.2s ease-in-out;
}

.back-button {
  margin-top: 20px;
}

/* ✅ 버튼 호버 효과 */
.like-button:hover, .comment-toggle-button:hover {
  background-color: #f5f5f5;
}

/* ✅ 아이콘 스타일 */
.icon {
  font-size: 16px;
}

/* ✅ 드롭다운 아이콘 스타일 */
.dropdown-icon {
  font-size: 12px;
  color: #666;
}

 

post.author 부분에서 자꾸 null이 떨어져서 시간 좀  썼다.

useState는 비동기적으로 업데이트 되기 때문에 post 값이 실제로 반영되는 순간은

React가 다시 렌더링될 때라고 한다.

즉 setPost를 호출한 후 다음 렌더링 때 null 이 아닌 실제 값이 뜬다고...

 

그래서 post.author 말고 foundPost.author 을 해야지 console.log에 찍힌다.

여기서도 문제가 있었는데...

console.log("foundPost : " + foundPost) 해봤자 안 뜬다.

console.log("foundPost:", foundPost) 로 해야 뜬다.

javascript에서 + 연산자로 문자열과 합치면 자동으로 [Object, object] 형식으로 떠서 , 를 사용해 줘야지 정확한 값을 확인할 수 있다.

 

현재 로그인한 아이디와 글 작성자가 동일할 경우 작성자 태그가 표시되도록 설정하였으며,

작성자는 게시글 수정 및 삭제 기능을 선택할 수 있는 버튼을 사용할 수 있다.

다만, 수정 기능은 아직 구현되지 않았다.

현재 디자인적인 요소는 아직 손댈 필요 없을 것 같아서,

(댓글의 삭제 버튼을 답글로 변경할 것)

이제 공감 카운트 기능을 구현할 예정이다.

 

2. 공감 카운트 기능