nav구조 변경
All checks were successful
Build And Test / build-and-push (push) Successful in 1m8s

This commit is contained in:
2025-05-24 00:58:04 +09:00
parent 8997539c9a
commit b94f6cea2e
12 changed files with 263 additions and 137 deletions

BIN
public/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

View File

@ -1,4 +1,5 @@
import React from "react"; // src/App.js
import React, { useEffect } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import Footer from "./components/Footer"; import Footer from "./components/Footer";
@ -12,38 +13,53 @@ import PostCategory from "./pages/PostCategory";
import Login from "./pages/Login"; import Login from "./pages/Login";
import Register from './pages/Register'; import Register from './pages/Register';
import Profile from "./pages/Profile"; import Profile from "./pages/Profile";
import TaskPage from "./pages/TaskPage"; import TaskPage from "./pages/TaskPage";
import BoardsPage from "./pages/BoardsPage"; import BoardsPage from "./pages/BoardsPage";
import AnsiblePage from "./pages/Ansible"; import AnsiblePage from "./pages/Ansible";
import Layout from "./components/Layout/Layout";
import { AuthProvider } from "./context/AuthContext"; import { AuthProvider, useAuth } from "./context/AuthContext";
import authApi from "./api/authApi";
import { attachAuthInterceptors } from "./api/attachInterceptors";
function AppContent() {
const { logout } = useAuth();
useEffect(() => {
attachAuthInterceptors(authApi, logout); // ✅ axios 인터셉터에 logout 연결
}, [logout]);
return (
<Router>
<div className="flex flex-col min-h-screen">
<Navbar />
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:id" element={<PostDetail />} />
<Route path="/postCreate" element={<PostCreate />} />
<Route path="/posts/:id/edit" element={<PostEdit />} />
<Route path="/post/:category" element={<PostCategory />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/profile" element={<Profile />} />
<Route path="/tasks" element={<TaskPage />} />
<Route path="/boards" element={<BoardsPage />} />
<Route path="/ansible" element={<AnsiblePage />} />
</Routes>
</Layout>
<Footer />
</div>
</Router>
);
}
function App() { function App() {
return ( return (
<AuthProvider> <AuthProvider>
<Router> <AppContent /> {/* ✅ useAuth는 이 내부에서만 사용 가능 */}
<div className="flex flex-col min-h-screen">
<Navbar />
<main className="flex-grow pt-16">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:id" element={<PostDetail />} />
<Route path="/postCreate" element={<PostCreate />} />
<Route path="/posts/:id/edit" element={<PostEdit />} />
<Route path="/post/:category" element={<PostCategory />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/profile" element={<Profile />} />
<Route path="/tasks" element={<TaskPage />} />
<Route path="/boards" element={<BoardsPage />} />
<Route path="/ansible" element={<AnsiblePage />} />
</Routes>
</main>
<Footer />
</div>
</Router>
</AuthProvider> </AuthProvider>
); );
} }

View File

@ -1,7 +1,7 @@
// src/api/attachInterceptors.js
import { refreshAccessToken } from './tokenUtils'; import { refreshAccessToken } from './tokenUtils';
export const attachAuthInterceptors = (axiosInstance) => { export const attachAuthInterceptors = (axiosInstance, logout) => {
// 요청 시 access token 자동 첨부
axiosInstance.interceptors.request.use(config => { axiosInstance.interceptors.request.use(config => {
const token = localStorage.getItem('access'); const token = localStorage.getItem('access');
if (token) { if (token) {
@ -10,7 +10,6 @@ export const attachAuthInterceptors = (axiosInstance) => {
return config; return config;
}); });
// 응답 시 access 만료 → refresh 재시도
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
res => res, res => res,
async err => { async err => {
@ -29,9 +28,8 @@ export const attachAuthInterceptors = (axiosInstance) => {
return axiosInstance(originalRequest); return axiosInstance(originalRequest);
} catch (refreshError) { } catch (refreshError) {
console.error('토큰 갱신 실패', refreshError); console.error('토큰 갱신 실패', refreshError);
localStorage.removeItem('access'); if (logout) logout(); // ✅ Context logout 반영
localStorage.removeItem('refresh'); window.location.href = '/login';
window.location.href = '/login'; // 또는 useNavigate 사용 가능
} }
} }

View File

@ -0,0 +1,16 @@
// src/components/Layout/Layout.js
import React from "react";
import { useLocation } from "react-router-dom";
const Layout = ({ children }) => {
const location = useLocation();
const isHome = location.pathname === "/";
return (
<main className={`${isHome ? "pt-0" : "pt-16"} flex-grow`}>
{children}
</main>
);
};
export default Layout;

View File

@ -1,80 +1,77 @@
import React from "react"; import React from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
const Navbar = () => { const Navbar = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const { isLoggedIn, logout, user } = useAuth(); const { isLoggedIn, logout, user } = useAuth();
// 로그인 상태에 따라 메뉴 구성 const isHome = location.pathname === "/";
const menuItems = [ const menuItems = [
{ name: "home", path: "/" }, { name: "home", path: "/" },
{ name: "about", path: "/about" }, { name: "about", path: "/about" },
{ name: "posts", path: "/posts" }, { name: "posts", path: "/posts" },
{ name: "boards", path: "/boards" }, { name: "boards", path: "/boards" },
...(isLoggedIn ? [
{ name: "tasks", path: "/tasks" },
{ name: "ansible", path: "/ansible" },
] : []),
]; ];
// 로그인 상태일 때 추가되는 메뉴
if (isLoggedIn) {
menuItems.push({ name: "tasks", path: "/tasks" });
menuItems.push({ name: "ansible", path: "/ansible" });
// menuItems.push({ name: "boards", path: "/boards" });
}
return ( return (
<nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm"> <nav className={`fixed top-0 w-full z-50 h-16 ${isHome ? "text-white bg-transparent" : "text-black bg-white shadow"}`}>
<div className="container mx-auto px-4"> <div className="container mx-auto px-4 py-3 flex justify-between items-center">
<div className="flex justify-between items-center h-16"> <Link to="/" className="text-xl font-bold tracking-wide">
<Link to="/" className="text-2xl font-bold text-[#3B82F6]"> ICURFER
Dev </Link>
</Link> <div className="hidden md:flex space-x-6 font-semibold">
{menuItems.map((item) => (
<div className="hidden md:flex items-center space-x-8"> <Link
{menuItems.map((item) => ( key={item.name}
to={item.path}
className={`hover:text-yellow-400 transition capitalize`}
>
{item.name}
</Link>
))}
</div>
<div className="space-x-2 flex items-center ">
{isLoggedIn ? (
<>
<Link <Link
key={item.name} to="/profile"
to={item.path} className={`px-4 py-2 rounded-full shadow ${isHome ? "bg-white text-black" : "bg-gray-200 text-gray-800"}`}
className="text-gray-600 hover:text-[#D4AF37]"
> >
{item.name.charAt(0).toUpperCase() + item.name.slice(1)} 정보 | 등급: {user?.grade && `(${user.grade})`}
</Link> </Link>
))} <button
onClick={() => {
{isLoggedIn ? ( logout();
<> navigate("/login");
<Link }}
to="/profile" className={`px-4 py-2 rounded-full ${isHome ? "bg-white text-black" : "bg-gray-200 text-gray-800"} hover:bg-gray-300`}
className="bg-[#8b0000] text-white px-4 py-2 rounded hover:bg-gray-300" >
> 로그아웃
정보 {user?.grade && `(${user.grade})`} </button>
</Link> </>
<button ) : (
onClick={() => { <>
logout(); <Link
navigate("/login"); to="/login"
}} className={`px-4 py-2 rounded-full ${isHome ? "bg-gray-200 text-gray-700 hover:bg-gray-300" : "bg-blue-600 text-white"}`}
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300" >
> 로그인
로그아웃 </Link>
</button> <Link
</> to="/register"
) : ( className={`px-4 py-2 rounded-full ${isHome ? "bg-gray-700 text-white hover:bg-gray-300" : "bg-gray-200 text-gray-800"}`}
<> >
<Link 회원가입
to="/login" </Link>
className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700" </>
> )}
로그인
</Link>
<Link
to="/register"
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
>
회원가입
</Link>
</>
)}
</div>
</div> </div>
</div> </div>
</nav> </nav>

View File

@ -6,16 +6,19 @@ const Navbar = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { isLoggedIn, logout, user } = useAuth(); const { isLoggedIn, logout, user } = useAuth();
// 로그인 상태에 따라 메뉴 구성 // 로그인 상태에 따라 메뉴 구성
const menuItems = [ const menuItems = [
{ name: "home", path: "/" }, { name: "home", path: "/" },
{ name: "about", path: "/about" }, { name: "about", path: "/about" },
{ name: "posts", path: "/posts" }, { name: "posts", path: "/posts" },
{ name: "boards", path: "/boards" },
]; ];
// 로그인 상태일 때 추가되는 메뉴
if (isLoggedIn) { if (isLoggedIn) {
menuItems.push({ name: "tasks", path: "/tasks" }); menuItems.push({ name: "tasks", path: "/tasks" });
menuItems.push({ name: "ansible", path: "/ansible" }); menuItems.push({ name: "ansible", path: "/ansible" });
// menuItems.push({ name: "boards", path: "/boards" });
} }
return ( return (

View File

@ -1,11 +1,15 @@
// src/index.js
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import { AuthProvider } from './context/AuthContext'; // ✅ 추가
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <AuthProvider> {/* ✅ 전역 로그인 상태 관리 */}
<App />
</AuthProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -10,10 +10,10 @@ const About = () => {
animation: false, animation: false,
radar: { radar: {
indicator: [ indicator: [
{ name: '클라우드', max: 100 }, { name: '클라우드', max: 120 },
{ name: '리눅스', max: 100 }, { name: '리눅스(우분투)', max: 130 },
{ name: '데브옵스', max: 100 }, { name: '쿠버네티스', max: 110 },
{ name: 'CI/CD', max: 100 }, { name: 'CI/CD', max: 120 },
{ name: '기타', max: 100 } { name: '기타', max: 100 }
] ]
}, },
@ -39,18 +39,26 @@ const About = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20"> <div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20">
<div> <div>
<h3 className="text-2xl font-bold mb-6">우리의 비전</h3> <h3 className="text-2xl font-bold mb-6">우리의 비전</h3>
<p className="text-gray-600 leading-relaxed"> <ul className="space-y-4">
엔지니어에게 필요한 관리 포탈을 만듭니다. <li className="flex items-center">
</p> <i className="fas fa-check text-[#3B82F6] mr-3"></i><span> </span>
</li>
<li className="flex items-center">
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span> </span>
</li>
<li className="flex items-center">
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>-</span>
</li>
</ul>
</div> </div>
<div> <div>
<h3 className="text-2xl font-bold mb-6">핵심 가치</h3> <h3 className="text-2xl font-bold mb-6">핵심 가치</h3>
<ul className="space-y-4"> <ul className="space-y-4">
<li className="flex items-center"> <li className="flex items-center">
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span> </span> <i className="fas fa-check text-[#3B82F6] mr-3"></i><span></span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>-</span> <i className="fas fa-check text-[#3B82F6] mr-3"></i><span></span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span>-</span> <i className="fas fa-check text-[#3B82F6] mr-3"></i><span>-</span>
@ -59,7 +67,7 @@ const About = () => {
</div> </div>
</div> </div>
<div className="mb-20"> <div className="mb-20">
<h3 className="text-2xl font-bold mb-8 text-center">전문 역량</h3> <h3 className="text-2xl font-bold mb-8 text-center">교육 역량</h3>
<div ref={chartRef} className="w-full h-[400px]"></div> <div ref={chartRef} className="w-full h-[400px]"></div>
</div> </div>
<div className="text-center"> <div className="text-center">

View File

@ -13,7 +13,7 @@ const Board = () => {
); );
return ( return (
<div className="min-h-screen bg-gray-50 py-20"> <div className="min-h-screen bg-gray-50 pt-20">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="mb-8"> <div className="mb-8">

View File

@ -1,60 +1,81 @@
import React, { useEffect, useState } from 'react'; // src/pages/Home.js
import blogApi from '../api/blogApi'; import React, { useEffect, useState } from "react";
import { useNavigate } from 'react-router-dom'; import blogApi from "../api/blogApi";
import { Link, useNavigate } from "react-router-dom";
const Home = () => { const Home = () => {
const [posts, setPosts] = useState([]); const [posts, setPosts] = useState([]);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
blogApi.get('/api/blog/posts/') blogApi
.then(res => setPosts(res.data)) .get("/api/blog/posts/")
.catch(err => console.error('게시글 목록 조회 실패:', err)); .then((res) => setPosts(res.data))
.catch((err) => console.error("게시글 목록 조회 실패:", err));
}, []); }, []);
return ( return (
<div className="min-h-screen"> <div className="min-h-screen bg-white">
<div className="relative h-screen"> {/* Hero Section */}
<div className="absolute inset-0"> <section className="relative h-[80vh]">
<img <img
src="https://public.readdy.ai/ai/img_res/41c06ed594c02f95a02361b098edd073.jpg" src="/bg.png"
className="w-full h-full object-cover" alt="Hero Background"
alt="Hero Background" className="absolute inset-0 w-full h-full object-cover"
/> />
</div> <div className="absolute inset-0 bg-gradient-to-r from-[#000000cc] to-[#00000000]" />
<div className="absolute inset-0 bg-gradient-to-r from-black/70 to-transparent"> <div className="relative z-10 flex items-center justify-start h-full container mx-auto px-4">
<div className="container mx-auto px-4 h-full flex items-center"> <div className="text-white max-w-xl">
<div className="max-w-2xl text-white"> <h1 className="text-5xl font-bold mb-6">
<h1 className="text-5xl font-bold mb-6">System Management Portal</h1> IT Education <br />
<p className="text-xl mb-8">데모 사이트 입니다.</p> Basic & Advanced
<button className="bg-[#3B82F6] text-white px-8 py-3 rounded-lg">개발 입니다.</button> </h1>
</div> <p className="text-lg mb-6">
Ubuntu Linux, Ansible, Docker, Kubernetes, etc.
</p>
<br/>
<h1 className="text-5xl font-bold mb-6">
System Management<br />
</h1>
<p className="text-lg mb-6">
IT System Management Portal
</p>
<Link
to="/about"
className="inline-block bg-white text-black px-6 py-3 rounded-lg font-semibold hover:opacity-90"
>
자세히 보기 &gt;
</Link>
</div> </div>
</div> </div>
</div> </section>
<div className="container mx-auto px-4 py-20"> {/* Posts Section */}
<section className="container mx-auto px-4 py-20">
<h2 className="text-3xl font-bold mb-12 text-center">최신 포스트</h2> <h2 className="text-3xl font-bold mb-12 text-center">최신 포스트</h2>
<ul className="grid grid-cols-1 md:grid-cols-3 gap-8"> <ul className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.length > 0 ? ( {posts.length > 0 ? (
posts.map(post => ( posts.map((post) => (
<li <li
key={post.id} key={post.id}
className="p-4 border rounded shadow-sm bg-white hover:bg-gray-50 cursor-pointer list-none" className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition cursor-pointer list-none border"
onClick={() => navigate(`/posts/${post.id}`)} onClick={() => navigate(`/posts/${post.id}`)}
> >
<h2 className="text-xl font-semibold">{post.title}</h2> <h3 className="text-xl font-semibold mb-2">{post.title}</h3>
<p className="text-gray-600">작성자: {post.author_name}</p> <p className="text-gray-700 mb-1">작성자: {post.author_name}</p>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-sm">
작성일: {new Date(post.created_at).toLocaleString()} {new Date(post.created_at).toLocaleString()}
</p> </p>
</li> </li>
)) ))
) : ( ) : (
<p className="text-center col-span-3 text-gray-500">게시글이 없습니다.</p> <p className="text-center col-span-3 text-gray-500">
게시글이 없습니다.
</p>
)} )}
</ul> </ul>
</div> </section>
</div> </div>
); );
}; };

63
src/pages/_unused_Home.js Normal file
View File

@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import blogApi from '../api/blogApi';
import { useNavigate } from 'react-router-dom';
const Home = () => {
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
useEffect(() => {
blogApi.get('/api/blog/posts/')
.then(res => setPosts(res.data))
.catch(err => console.error('게시글 목록 조회 실패:', err));
}, []);
return (
<div className="min-h-screen">
<div className="relative h-screen">
<div className="absolute inset-0">
<img
src="/bg.png"
className="w-full h-full object-cover"
// className="w-full h-full object-cover"
alt="Hero Background"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-r from-black/70 to-transparent">
<div className="container mx-auto px-4 h-full flex items-center">
<div className="max-w-2xl text-white">
<h1 className="text-5xl font-bold mb-6">System Management Portal</h1>
<p className="text-xl mb-8">데모 사이트 입니다.</p>
<button className="bg-[#3B82F6] text-white px-8 py-3 rounded-lg">개발 입니다.</button>
</div>
</div>
</div>
</div>
<div className="container mx-auto px-4 py-20">
<h2 className="text-3xl font-bold mb-12 text-center">최신 포스트</h2>
<ul className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.length > 0 ? (
posts.map(post => (
<li
key={post.id}
className="p-4 border rounded shadow-sm bg-white hover:bg-gray-50 cursor-pointer list-none"
onClick={() => navigate(`/posts/${post.id}`)}
>
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600">작성자: {post.author_name}</p>
<p className="text-gray-400 text-sm">
작성일: {new Date(post.created_at).toLocaleString()}
</p>
</li>
))
) : (
<p className="text-center col-span-3 text-gray-500">게시글이 없습니다.</p>
)}
</ul>
</div>
</div>
);
};
export default Home;

View File

@ -1 +1 @@
0.0.15 0.0.16