Project

[javascript] 네이버 클론 코딩 + 블로그 1

끊임없이 개발하는 새럼 2025. 3. 6. 17:29

디자인은 내가 안 하고 전부 gpt한테 시킬 예정 [물론 백,프론트도 그렇지만...]

db도 따로 추가 안 하고 로컬 스토리지로만 진행하려고 하는데,

중간에 막히는 부분이 있다 싶으면 oracle을 사용할 예정이다. 

 

🚀 현재 프로젝트 환경 정리

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


📌 프로젝트 개요

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

 

App.js

import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Login from "./Login";
import Register from "./Register";
import Dashboard from "./Dashboard";
import FindPassword from "./FindPassword";
import Blog from "./Blog";

function App() {
  return (
    <Router>
      <div>
        <Routes>
          <Route path="/" element={<Login />} />
          <Route path="/register" element={<Register />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/find-password" element={<FindPassword />} />
          <Route path="/blog" element={<Blog />} />
        </Routes>
      </div>
    </Router>
  );
}

export default App;

 

Login.js

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

function Login() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isIpSecurityOn, setIsIpSecurityOn] = useState(true); 
  const navigate = useNavigate();

  const handleLogin = (e) => {
    e.preventDefault();

    const storedUser = JSON.parse(localStorage.getItem("user"));
    if (!storedUser || storedUser.username !== username || storedUser.password !== password) {
      alert("아이디 또는 비밀번호가 잘못되었습니다.");
      return;
    }

    alert("로그인 성공!");
    navigate("/dashboard");
  };

  return (
    <div className="login-container">
      <h1 className="logo">NAVER</h1>
      <div className="login-box">
        <form onSubmit={handleLogin}>
          <input type="text" placeholder="아이디 또는 전화번호" value={username} onChange={(e) => setUsername(e.target.value)} className="login-input" />
          <input type="password" placeholder="비밀번호" value={password} onChange={(e) => setPassword(e.target.value)} className="login-input" />
          

          <div className="options">
            <label>
              <input type="checkbox" /> 로그인 상태 유지
            </label>
            <span className="ip-security" onClick={() => setIsIpSecurityOn(!isIpSecurityOn)}>
              IP보안 <b>{isIpSecurityOn ? "ON" : "OFF"}</b>
            </span>
          </div>

          <button type="submit" className="login-button">로그인</button>
        </form>

        <div className="links">
          <span onClick={() => navigate("/find-password")}>비밀번호 찾기</span> | 
          <span>아이디 찾기</span> | 
          <span onClick={() => navigate("/register")}>회원가입</span>
        </div>
      </div>
    </div>
  );
}

export default Login;

 

Login.css

/* 전체 페이지 스타일 */
.login-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh;
    background-color: #f5f6f7;
  }
  
  /* 네이버 로고 스타일 */
  .logo {
    font-size: 32px;
    font-weight: bold;
    color: #03c75a;
    margin-bottom: 20px;
  }
  
  /* 로그인 박스 */
  .login-box {
    background: white;
    padding: 25px;
    width: 400px;
    border-radius: 10px;
    box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1);
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: auto;
  }
  
  /* 입력 필드 스타일 */
  .login-input {
    width: 100%;
    max-width: 360px;
    padding: 12px;
    margin: 8px 0;
    border: 1px solid #dadada;
    border-radius: 5px;
    font-size: 14px;
    outline: none;
  }
  
  /* 로그인 버튼 스타일 */
  .login-button {
    width: 100%;
    background-color: #03c75a;
    color: white;
    padding: 12px;
    border: none;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
    margin-top: 10px;
  }
  
  .login-button:hover {
    background-color: #029d4e;
  }
  
  /* 하단 링크 스타일 */
  .links {
    margin-top: 15px;
    font-size: 13px;
    color: #666;
  }
  
  .links span {
    cursor: pointer;
  }
  
  .links span:hover {
    text-decoration: underline;
  }
  
  /* 비밀번호 찾기 결과 스타일 */
  .found-password {
    margin-top: 15px;
    font-size: 16px;
    color: #333;
  }
  

  .options {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    max-width: 360px;
    font-size: 13px;
    margin: 10px 0;
    color: #666;
  }
  
  /* ✅ IP 보안 ON/OFF 버튼 스타일 */
  .ip-security {
    cursor: pointer;
    background-color: #f5f5f5;
    padding: 5px 10px;
    border-radius: 12px;
    font-weight: bold;
  }
  
  .ip-security:hover {
    background-color: #e0e0e0;
  }

  /* ✅ 로그인으로 돌아가기 버튼 (네이버 스타일 적용) */
.back-to-login {
    margin-top: 15px;
    background: none;
    border: none;
    font-size: 14px;
    color: #0366d6;
    cursor: pointer;
    font-weight: bold;
  }
  
  .back-to-login:hover {
    text-decoration: underline;
  }

회원가입

Register.js

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import "./Register.css"; // ✅ 네이버 스타일 적용

function Register() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [email, setEmail] = useState("");
  const [phone, setPhone] = useState("");
  const navigate = useNavigate();

  // ✅ 회원가입 처리
  const handleRegister = (e) => {
    e.preventDefault();

    // 비밀번호 확인 체크
    if (password !== confirmPassword) {
      alert("비밀번호가 일치하지 않습니다.");
      return;
    }

    // 로컬 스토리지에 회원 정보 저장 (DB 대체)
    const newUser = { username, password, email, phone };
    localStorage.setItem("user", JSON.stringify(newUser));

    alert("회원가입이 완료되었습니다! 로그인 페이지로 이동합니다.");
    navigate("/"); // 로그인 페이지로 이동
  };

  return (
    <div className="register-container">
      <h1 className="logo">NAVER</h1>
      <div className="register-box">
        <h2>회원가입</h2>
        <form onSubmit={handleRegister}>
          <input type="text" placeholder="아이디" value={username} onChange={(e) => setUsername(e.target.value)} required />
          <input type="password" placeholder="비밀번호" value={password} onChange={(e) => setPassword(e.target.value)} required />
          <input type="password" placeholder="비밀번호 확인" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} required />
          <input type="email" placeholder="이메일" value={email} onChange={(e) => setEmail(e.target.value)} required />
          <input type="tel" placeholder="휴대폰 번호" value={phone} onChange={(e) => setPhone(e.target.value)} required />
          <button type="submit" className="register-button">가입하기</button>
        </form>

        {/* 로그인으로 이동 */}
        <button onClick={() => navigate("/")} className="back-to-login">로그인으로 돌아가기</button>
      </div>
    </div>
  );
}

export default Register;

 

Register.css

/* ✅ 전체 회원가입 페이지 스타일 */
.register-container {
    display: flex;
    flex-direction: column;
    align-items: center;  /* 👉 가운데 정렬 */
    justify-content: center; /* 👉 수직 정렬 */
    height: 100vh;
    background-color: #f5f6f7;
  }
  
  /* ✅ 회원가입 박스 가운데 정렬 */
  .register-box {
    background: white;
    padding: 25px;
    width: 400px;  /* 👉 박스 크기 조절 */
    border-radius: 10px;
    box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1);
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center; /* 👉 내부 요소 가운데 정렬 */
  }
  
  /* ✅ 입력 필드 스타일 */
  .register-box input {
    width: 100%;  /* 👉 가로 너비 조절 */
    max-width: 360px;  /* 👉 최대 너비 설정 */
    padding: 12px;
    margin: 8px 0;
    border: 1px solid #dadada;
    border-radius: 5px;
    font-size: 14px;
    outline: none;
  }
  
  /* ✅ 가입하기 버튼 */
  .register-button {
    width: 100%;
    max-width: 360px;
    background-color: #03c75a;
    color: white;
    padding: 12px;
    border: none;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
    margin-top: 10px;
  }
  
  .register-button:hover {
    background-color: #029d4e;
  }
  
  /* ✅ 로그인으로 돌아가기 버튼 */
  .back-to-login {
    margin-top: 10px;
    background: none;
    border: none;
    font-size: 14px;
    color: #0366d6;
    cursor: pointer;
  }
  
  .back-to-login:hover {
    text-decoration: underline;
  }

대쉬보드 화면

 

Dashboard.js

import React from "react";
import { useNavigate } from "react-router-dom";
import "./Dashboard.css"; //

function Dashboard() {
  const navigate = useNavigate();
  const storedUser = JSON.parse(localStorage.getItem("user"));

  const handleLogout = () => {
    alert("로그아웃 되었습니다.");
    navigate("/");
  };

  return (
    <div className="dashboard-container">

      <div className="nav-bar">
        <h1 className="nav-logo">NAVER</h1>
        <input type="text" className="search-bar" placeholder="검색어를 입력하세요" />
        <button className="search-button">검색</button>
      </div>


      <div className="main-content">

        <div className="user-info">
          <h2>{storedUser ? `${storedUser.username}님, 환영합니다!` : "사용자"}</h2>
          <button onClick={handleLogout} className="logout-button">로그아웃</button>
        </div>

        <div className="blog-section">
          <h3>내 블로그</h3>
          <p>내 블로그에 글을 작성하고 관리하세요.</p>
          <button onClick={() => navigate("/blog")} className="blog-button">블로그로 이동</button>
        </div>
      </div>
    </div>
  );
}

export default Dashboard;

 

Dashboard.css

/* ✅ 전체 대시보드 스타일 */
.dashboard-container {
    font-family: Arial, sans-serif;
    background-color: #f5f6f7;
    height: 100vh;
  }
  
  /* ✅ 네이버 상단바 */
  .nav-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background-color: #03c75a;
    padding: 10px 20px;
    color: white;
  }
  
  .nav-logo {
    font-size: 24px;
    font-weight: bold;
  }
  
  .search-bar {
    width: 400px;
    padding: 8px;
    border: none;
    border-radius: 20px;
    font-size: 14px;
  }
  
  .search-button {
    margin-left: 10px;
    padding: 8px 12px;
    background-color: white;
    color: #03c75a;
    border: none;
    border-radius: 20px;
    font-weight: bold;
    cursor: pointer;
  }
  
  .search-button:hover {
    background-color: #f0f0f0;
  }
  
  /* ✅ 메인 콘텐츠 */
  .main-content {
    padding: 20px;
  }
  
  /* ✅ 로그인된 유저 정보 */
  .user-info {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: white;
    padding: 15px;
    border-radius: 10px;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
    margin-bottom: 20px;
  }
  
  .logout-button {
    background-color: #d9534f;
    color: white;
    border: none;
    padding: 8px 12px;
    border-radius: 5px;
    cursor: pointer;
  }
  
  .logout-button:hover {
    background-color: #c9302c;
  }
  
  /* ✅ 블로그 섹션 */
  .blog-section {
    background: white;
    padding: 15px;
    border-radius: 10px;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
  }
  
  .blog-list {
    display: flex;
    gap: 15px;
    margin-top: 10px;
  }
  
  .blog-card {
    background: #f9f9f9;
    padding: 15px;
    border-radius: 8px;
    width: 30%;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
  }
  
  .blog-button {
    margin-top: 10px;
    padding: 8px 12px;
    background-color: #03c75a;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
  }
  
  .blog-button:hover {
    background-color: #029d4e;
  }
  
  .blog-section {
    background: white;
    padding: 15px;
    border-radius: 10px;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
    margin-top: 20px;
  }
  
  .blog-button {
    background-color: #03c75a;
    color: white;
    padding: 10px;
    border: none;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
  }
  
  .blog-button:hover {
    background-color: #029d4e;
  }

 

FindPassword.js -> 미완성

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

function FindPassword() {
  const [username, setUsername] = useState("");
  const [foundPassword, setFoundPassword] = useState(null);
  const navigate = useNavigate();

  const handleFindPassword = (e) => {
    e.preventDefault();


    const storedUser = JSON.parse(localStorage.getItem("user"));

    if (!storedUser || storedUser.username !== username) {
      alert("아이디가 존재하지 않습니다.");
      setFoundPassword(null);
      return;
    }


    setFoundPassword(storedUser.password);
  };

  return (
    <div className="login-container">
      <h1 className="logo">NAVER</h1>
      <div className="login-box">
        <h2>비밀번호 찾기</h2>
        <form onSubmit={handleFindPassword}>
          <input
            type="text"
            placeholder="아이디를 입력하세요"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            className="login-input"
          />
          <button type="submit" className="login-button">비밀번호 찾기</button>
        </form>

        {foundPassword && (
          <p className="found-password">
            당신의 비밀번호: <strong>{foundPassword}</strong>
          </p>
        )}


        <button className="back-to-login" onClick={() => navigate("/")}>
          로그인으로 돌아가기
        </button>
      </div>
    </div>
  );
}

export default FindPassword;

 

FindPassword.css

.find-password-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh;
    background-color: #f5f6f7;
  }
  
  .find-password-input {
    width: 300px;
    padding: 12px;
    margin: 8px 0;
    border: 1px solid #dadada;
    border-radius: 5px;
    font-size: 14px;
    outline: none;
  }
  
  .find-password-button {
    width: 320px;
    background-color: #03c75a;
    color: white;
    padding: 12px;
    border: none;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
    margin-top: 10px;
  }
  
  .find-password-button:hover {
    background-color: #029d4e;
  }
  
  .found-password {
    margin-top: 20px;
    font-size: 18px;
    color: #333;
  }
  
  .back-button {
    margin-top: 10px;
    background: none;
    border: none;
    font-size: 14px;
    color: #0366d6;
    cursor: pointer;
  }
  
  .back-button:hover {
    text-decoration: underline;
  }

 

블로그 화면

Blog.js

import React, { useState, useEffect } from "react";
import "./Blog.css"; // ✅ 네이버 스타일 적용
import { useNavigate } from "react-router-dom";

function Blog() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [posts, setPosts] = useState([]);
  const navigate = useNavigate;

  // 로컬 스토리지에서 기존 글 불러오기
  useEffect(() => {
    const savedPosts = JSON.parse(localStorage.getItem("posts")) || [];
    setPosts(savedPosts);
  }, []);

  // 글 저장 함수
  const handleSavePost = () => {
    if (title.trim() === "" || content.trim() === "") {
      alert("제목과 내용을 입력해주세요.");
      return;
    }

    const newPost = { id: Date.now(), title, content };
    const updatedPosts = [newPost, ...posts];

    setPosts(updatedPosts);
    localStorage.setItem("posts", JSON.stringify(updatedPosts));

    setTitle("");
    setContent("");
  };

  // 글 삭제 함수
  const handleDeletePost = (id) => {
    const updatedPosts = posts.filter((post) => post.id !== id);
    setPosts(updatedPosts);
    localStorage.setItem("posts", JSON.stringify(updatedPosts));
  };

  return (
    <div className="blog-container">
      <h2>네이버 블로그</h2>

      {/* ✅ 글쓰기 폼 */}
      <div className="blog-form">
        <input
          type="text"
          placeholder="제목을 입력하세요"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          className="blog-input"
        />
        <textarea
          placeholder="내용을 입력하세요"
          value={content}
          onChange={(e) => setContent(e.target.value)}
          className="blog-textarea"
        ></textarea>
        <button onClick={handleSavePost} className="blog-button">글 작성</button>
      </div>

      {/* ✅ 글 목록 */}
      <div className="blog-list">
        {posts.length === 0 ? (
          <p className="no-posts">작성된 글이 없습니다.</p>
        ) : (
          posts.map((post) => (
            <div key={post.id} className="blog-card">
              <h3>{post.title}</h3>
              <p>{post.content}</p>
              <button onClick={() => handleDeletePost(post.id)} className="delete-button">삭제</button>
            </div>
          ))
        )}
      </div>
      <button onClick={() => navigate(-1)} className="back-button">뒤로가기</button>
    </div>
  );
}

export default Blog;

 

 

Blog.css

/* ✅ 전체 블로그 스타일 */
.blog-container {
    max-width: 600px;
    margin: auto;
    padding: 20px;
    font-family: Arial, sans-serif;
  }
  
  /* ✅ 글쓰기 폼 */
  .blog-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
    background: white;
    padding: 15px;
    border-radius: 10px;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
  }
  
  .blog-input, .blog-textarea {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
    font-size: 14px;
  }
  
  .blog-textarea {
    height: 100px;
  }
  
  .blog-button {
    background-color: #03c75a;
    color: white;
    padding: 10px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
  }
  
  .blog-button:hover {
    background-color: #029d4e;
  }
  
  /* ✅ 글 목록 */
  .blog-list {
    margin-top: 20px;
  }
  
  .blog-card {
    background: white;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
    margin-bottom: 10px;
  }
  
  .delete-button {
    background-color: #d9534f;
    color: white;
    border: none;
    padding: 8px;
    border-radius: 5px;
    cursor: pointer;
    margin-top: 10px;
  }
  
  .delete-button:hover {
    background-color: #c9302c;
  }

 

✅ Blog.js 는 썸머노트(글쓰기 에디터)를 사용하여 실제 에디어로 작성하도록 수정
✅ FindPassword.js 는 권한 및 인증 없이 패스워드를 찾을 수 있으므로 인증 기능 추가