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";
@ -15,16 +16,24 @@ 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]);
function App() {
return ( return (
<AuthProvider>
<Router> <Router>
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
<Navbar /> <Navbar />
<main className="flex-grow pt-16"> <Layout>
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
@ -40,10 +49,17 @@ function App() {
<Route path="/boards" element={<BoardsPage />} /> <Route path="/boards" element={<BoardsPage />} />
<Route path="/ansible" element={<AnsiblePage />} /> <Route path="/ansible" element={<AnsiblePage />} />
</Routes> </Routes>
</main> </Layout>
<Footer /> <Footer />
</div> </div>
</Router> </Router>
);
}
function App() {
return (
<AuthProvider>
<AppContent /> {/* ✅ useAuth는 이 내부에서만 사용 가능 */}
</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,59 +1,57 @@
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">
<div className="hidden md:flex items-center space-x-8">
{menuItems.map((item) => ( {menuItems.map((item) => (
<Link <Link
key={item.name} key={item.name}
to={item.path} to={item.path}
className="text-gray-600 hover:text-[#D4AF37]" className={`hover:text-yellow-400 transition capitalize`}
> >
{item.name.charAt(0).toUpperCase() + item.name.slice(1)} {item.name}
</Link> </Link>
))} ))}
</div>
<div className="space-x-2 flex items-center ">
{isLoggedIn ? ( {isLoggedIn ? (
<> <>
<Link <Link
to="/profile" to="/profile"
className="bg-[#8b0000] text-white px-4 py-2 rounded hover:bg-gray-300" className={`px-4 py-2 rounded-full shadow ${isHome ? "bg-white text-black" : "bg-gray-200 text-gray-800"}`}
> >
정보 {user?.grade && `(${user.grade})`} 정보 | 등급: {user?.grade && `(${user.grade})`}
</Link> </Link>
<button <button
onClick={() => { onClick={() => {
logout(); logout();
navigate("/login"); navigate("/login");
}} }}
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300" className={`px-4 py-2 rounded-full ${isHome ? "bg-white text-black" : "bg-gray-200 text-gray-800"} hover:bg-gray-300`}
> >
로그아웃 로그아웃
</button> </button>
@ -62,13 +60,13 @@ const Navbar = () => {
<> <>
<Link <Link
to="/login" to="/login"
className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700" className={`px-4 py-2 rounded-full ${isHome ? "bg-gray-200 text-gray-700 hover:bg-gray-300" : "bg-blue-600 text-white"}`}
> >
로그인 로그인
</Link> </Link>
<Link <Link
to="/register" to="/register"
className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300" className={`px-4 py-2 rounded-full ${isHome ? "bg-gray-700 text-white hover:bg-gray-300" : "bg-gray-200 text-gray-800"}`}
> >
회원가입 회원가입
</Link> </Link>
@ -76,7 +74,6 @@ const Navbar = () => {
)} )}
</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>
<AuthProvider> {/* ✅ 전역 로그인 상태 관리 */}
<App /> <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">
</div> Ubuntu Linux, Ansible, Docker, Kubernetes, etc.
</div> </p>
</div> <br/>
<h1 className="text-5xl font-bold mb-6">
System Management<br />
<div className="container mx-auto px-4 py-20"> </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>
</section>
{/* 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