🚀 현재 프로젝트 환경 정리
✅ 네이버 스타일 로그인 & 블로그 시스템 구현 중
✅ 사용된 기술 및 개발 도구 정리
✅ 코드가 수정될 때마다 게시글 계속 업데이트 중
📌 프로젝트 개요
- 프로젝트명: 네이버 스타일 로그인 & 블로그 클론 코딩
- 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. 공감 카운트 기능
'Project' 카테고리의 다른 글
| [바이브 코딩] 비개발자 AI 바이브코딩 강의 #00 (1) | 2025.09.08 |
|---|---|
| [javascript] 네이버 클론 코딩 + 블로그 5: 게시글 수정 (0) | 2025.03.13 |
| [javascript] 네이버 클론 코딩 + 블로그 4: JWT 발급 (0) | 2025.03.12 |
| [javascript] 네이버 클론 코딩 + 블로그 3: 블로그 댓글 구현 (1) | 2025.03.11 |
| [javascript] 네이버 클론 코딩 + 블로그 2: 글쓰기 에디터 + 경고 메시지 (1) | 2025.03.10 |