This commit is contained in:
2025-04-22 00:05:11 +09:00
parent 58b57a1e02
commit 527bb293b7
16 changed files with 654 additions and 28 deletions

167
package-lock.json generated
View File

@ -8,14 +8,24 @@
"name": "msa-fe",
"version": "0.1.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"echarts": "^5.6.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-scripts": "5.0.1",
"react-router-dom": "^7.5.1",
"react-scripts": "^5.0.1",
"swiper": "^11.2.6",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"with": "^7.0.2"
}
},
"node_modules/@adobe/css-tools": {
@ -2458,6 +2468,15 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": {
"node": ">=6"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -4800,6 +4819,13 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
"node_modules/assert-never": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz",
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
"dev": true,
"license": "MIT"
},
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@ -5160,6 +5186,19 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/babel-walk": {
"version": "3.0.0-canary-5",
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.9.6"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -6819,6 +6858,22 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -13926,6 +13981,54 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.1.tgz",
"integrity": "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.1.tgz",
"integrity": "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==",
"license": "MIT",
"dependencies": {
"react-router": "7.5.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-router/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -14841,6 +14944,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -15854,6 +15963,25 @@
"node": ">=4"
}
},
"node_modules/swiper": {
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.6.tgz",
"integrity": "sha512-8aXpYKtjy3DjcbzZfz+/OX/GhcU5h+looA6PbAzHMZT6ESSycSp9nAjPCenczgJyslV+rUGse64LMGpWE3PX9Q==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -16219,6 +16347,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -17057,6 +17191,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/with": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.9.6",
"@babel/types": "^7.9.6",
"assert-never": "^1.2.1",
"babel-walk": "3.0.0-canary-5"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -17537,6 +17687,21 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
}
}
}

View File

@ -3,13 +3,17 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"echarts": "^5.6.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-scripts": "5.0.1",
"react-router-dom": "^7.5.1",
"react-scripts": "^5.0.1",
"swiper": "^11.2.6",
"web-vitals": "^2.1.4"
},
"scripts": {
@ -35,5 +39,11 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"with": "^7.0.2"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,25 +1,31 @@
import logo from './logo.svg';
import './App.css';
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import Home from './pages/Home';
import About from './pages/About';
import Board from './pages/Board';
import PostCategory from './pages/PostCategory';
import Login from './pages/Login';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<Router>
<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="/board" element={<Board />} />
<Route path="/post/:category" element={<PostCategory />} />
<Route path="/login" element={<Login />} />
</Routes>
</main>
<Footer />
</div>
</Router>
);
}
export default App;
export default App;

47
src/components/Footer.js Normal file
View File

@ -0,0 +1,47 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
const Footer = () => {
const navigate = useNavigate();
return (
<footer className="bg-gray-900 text-white mt-auto">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h3 className="text-xl font-bold mb-4">TechEdu</h3>
<p className="text-gray-400">최고의 IT 교육 플랫폼으로 당신의 커리어를 성장시키세요.</p>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Quick Links</h4>
<ul className="space-y-2">
<li><button onClick={() => navigate('/')} className="text-gray-400 hover:text-white">Home</button></li>
<li><button onClick={() => navigate('/about')} className="text-gray-400 hover:text-white">About</button></li>
<li><button onClick={() => navigate('/board')} className="text-gray-400 hover:text-white">Board</button></li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Categories</h4>
<ul className="space-y-2">
<li><button onClick={() => navigate('/post/developer')} className="text-gray-400 hover:text-white">Developer</button></li>
<li><button onClick={() => navigate('/post/systemengineer')} className="text-gray-400 hover:text-white">System Engineer</button></li>
<li><button onClick={() => navigate('/post/etc')} className="text-gray-400 hover:text-white">Other Posts</button></li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Contact</h4>
<ul className="space-y-2 text-gray-400">
<li className="flex items-center"><i className="fas fa-envelope mr-2"></i> contact@techedu.com</li>
<li className="flex items-center"><i className="fas fa-phone mr-2"></i> 02-1234-5678</li>
<li className="flex items-center"><i className="fas fa-map-marker-alt mr-2"></i> 123</li>
</ul>
</div>
</div>
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
<p>&copy; 2025 TechEdu. All rights reserved.</p>
</div>
</div>
</footer>
);
};
export default Footer;

57
src/components/Navbar.js Normal file
View File

@ -0,0 +1,57 @@
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
const Navbar = () => {
const menuItems = ['home', 'about', 'board', 'etc'];
const postCategories = ['developer', 'systemengineer', 'etc'];
const navigate = useNavigate();
return (
<nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm">
<div className="container mx-auto px-4">
<div className="flex justify-between items-center h-16">
<Link to="/" className="text-2xl font-bold text-[#3B82F6]">TechEdu</Link>
<div className="hidden md:flex items-center space-x-8">
{menuItems.map((item) => (
<Link
key={item}
to={item === 'home' ? '/' : `/${item}`}
className="text-gray-600 hover:text-[#D4AF37]"
>
{item.charAt(0).toUpperCase() + item.slice(1)}
</Link>
))}
{/* ▼ Post Dropdown */}
<div className="relative group">
<button className="text-gray-600 hover:text-[#D4AF37] flex items-center">
Post <i className="fas fa-chevron-down ml-1 text-xs"></i>
</button>
<div className="absolute hidden group-hover:block bg-white shadow-md mt-2 rounded-md py-2">
{postCategories.map(category => (
<button
key={category}
onClick={() => navigate(`/post/${category.toLowerCase()}`)}
className="block px-4 py-2 text-gray-600 hover:bg-gray-50 hover:text-[#D4AF37] w-full text-left"
>
{category.charAt(0).toUpperCase() + category.slice(1)}
</button>
))}
</div>
</div>
{/* ✅ 로그인 버튼 */}
<Link
to="/login"
className="bg-[#3B82F6] text-white px-4 py-2 rounded hover:bg-blue-700 transition"
>
로그인
</Link>
</div>
</div>
</div>
</nav>
);
};
export default Navbar;

View File

@ -0,0 +1,19 @@
import React from 'react';
const PostCard = ({ post }) => {
return (
<div className="bg-white shadow-lg rounded-lg overflow-hidden transition-transform duration-300 hover:-translate-y-2">
<img src={post.image} alt={post.title} className="w-full h-48 object-cover" />
<div className="p-6">
<h3 className="text-xl font-semibold mb-2">{post.title}</h3>
<p className="text-gray-600 mb-4">{post.content}</p>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">{post.date}</span>
<button className="text-[#D4AF37] hover:text-[#000080]"> 보기</button>
</div>
</div>
</div>
);
};
export default PostCard;

22
src/data/sampleData.js Normal file
View File

@ -0,0 +1,22 @@
export const posts = [
{
id: 1,
title: '인공지능 기초 학습 가이드',
content: '인공지능 학습을 시작하는 분들을 위한 완벽 가이드...',
date: '2025-02-19',
image: 'https://public.readdy.ai/ai/img_res/c22ebfe39e631e808ad948c508839779.jpg'
},
// ...
];
export const boardItems = [
{
id: 1,
title: '신규 프로젝트 진행 현황 보고',
content: '2025년 1분기 신규 프로젝트의 진행 상황...',
author: '김지원',
updatedAt: '2025-02-19'
},
// ...
];

View File

@ -11,3 +11,10 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 추가로 FontAwesome을 위한 CSS */
@import '@fortawesome/fontawesome-free/css/all.min.css';

View File

@ -2,16 +2,10 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
);

81
src/pages/About.js Normal file
View File

@ -0,0 +1,81 @@
import React, { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
const About = () => {
const chartRef = useRef(null);
useEffect(() => {
const chart = echarts.init(chartRef.current);
chart.setOption({
animation: false,
radar: {
indicator: [
{ name: '프로그래밍', max: 100 },
{ name: '클라우드', max: 100 },
{ name: '인공지능', max: 100 },
{ name: '데이터 분석', max: 100 },
{ name: '보안', max: 100 }
]
},
series: [{
type: 'radar',
data: [{
value: [95, 90, 85, 88, 92],
name: '교육 역량',
areaStyle: { color: 'rgba(59, 130, 246, 0.2)' },
lineStyle: { color: '#3B82F6' }
}]
}]
});
}, []);
return (
<div className="min-h-screen bg-white">
<div className="container mx-auto px-4 py-20">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-6">About Us</h2>
<p className="text-xl text-gray-600">최고의 IT 교육 플랫폼을 만들어갑니다</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20">
<div>
<h3 className="text-2xl font-bold mb-6">우리의 비전</h3>
<p className="text-gray-600 leading-relaxed">
실무 중심의 IT 교육을 통해 많은 사람들이 디지털 시대의 전문가로
성장할 있도록 돕고, 글로벌 IT 인재 양성의 허브가 되고자 합니다.
</p>
</div>
<div>
<h3 className="text-2xl font-bold mb-6">핵심 가치</h3>
<ul className="space-y-4">
<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>
<li className="flex items-center">
<i className="fas fa-check text-[#3B82F6] mr-3"></i><span> </span>
</li>
</ul>
</div>
</div>
<div className="mb-20">
<h3 className="text-2xl font-bold mb-8 text-center">전문 역량</h3>
<div ref={chartRef} className="w-full h-[400px]"></div>
</div>
<div className="text-center">
<h3 className="text-2xl font-bold mb-6">Contact Us</h3>
<div className="space-y-4">
<p><i className="fas fa-envelope mr-2"></i> contact@example.com</p>
<p><i className="fas fa-phone mr-2"></i> 02-1234-5678</p>
<p><i className="fas fa-map-marker-alt mr-2"></i> 123</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default About;

80
src/pages/Board.js Normal file
View File

@ -0,0 +1,80 @@
import React, { useState } from 'react';
import { boardItems } from '../data/sampleData';
const Board = () => {
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const filteredItems = boardItems.filter(item =>
item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.author.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="min-h-screen bg-gray-50 py-20">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<div className="relative">
<input
type="text"
placeholder="검색어를 입력하세요"
className="w-full px-4 py-3 pl-12 border-none rounded-lg bg-white shadow-md text-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<i className="fas fa-search absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
</div>
</div>
<div className="bg-white rounded-lg shadow-md overflow-hidden">
<table className="w-full">
<thead className="bg-[#3B82F6] text-white">
<tr>
<th className="px-6 py-4 text-left w-20">번호</th>
<th className="px-6 py-4 text-left">제목</th>
<th className="px-6 py-4 text-left">내용</th>
<th className="px-6 py-4 text-left w-32">작성자</th>
<th className="px-6 py-4 text-left w-32">수정일</th>
</tr>
</thead>
<tbody>
{filteredItems
.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
.map(item => (
<tr key={item.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="px-6 py-4">{item.id}</td>
<td className="px-6 py-4 font-medium">{item.title}</td>
<td className="px-6 py-4 text-gray-600">{item.content.substring(0, 30)}...</td>
<td className="px-6 py-4">{item.author}</td>
<td className="px-6 py-4 text-gray-500">{item.updatedAt}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 flex justify-center">
<div className="flex space-x-2">
{Array.from({ length: Math.ceil(filteredItems.length / itemsPerPage) }, (_, i) => (
<button
key={i + 1}
onClick={() => setCurrentPage(i + 1)}
className={`px-4 py-2 rounded ${
currentPage === i + 1
? 'bg-[#3B82F6] text-white'
: 'bg-white text-gray-600 hover:bg-gray-100'
}`}
>
{i + 1}
</button>
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default Board;

38
src/pages/Home.js Normal file
View File

@ -0,0 +1,38 @@
import React from 'react';
import { posts } from '../data/sampleData';
import PostCard from '../components/PostCard';
const Home = () => {
return (
<div className="min-h-screen">
<div className="relative h-screen">
<div className="absolute inset-0">
<img
src="https://public.readdy.ai/ai/img_res/41c06ed594c02f95a02361b098edd073.jpg"
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">IT 교육의 새로운 기준</h1>
<p className="text-xl mb-8">실무 중심의 IT 교육 콘텐츠로 당신의 커리어를 성장시키세요</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>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
</div>
);
};
export default Home;

58
src/pages/Login.js Normal file
View File

@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Login = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleLogin = async () => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/api/auth/login/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!response.ok) throw new Error('로그인 실패');
const data = await response.json();
localStorage.setItem('access', data.access);
localStorage.setItem('refresh', data.refresh);
alert('로그인 성공!');
navigate('/');
} catch (error) {
alert('로그인 실패: ' + error.message);
}
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
<h2 className="text-2xl font-bold mb-6 text-center text-[#3B82F6]">로그인</h2>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-2 border rounded mb-4"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border rounded mb-6"
/>
<button
onClick={handleLogin}
className="w-full bg-[#3B82F6] text-white py-2 rounded hover:bg-blue-700"
>
로그인
</button>
</div>
</div>
);
};
export default Login;

25
src/pages/PostCategory.js Normal file
View File

@ -0,0 +1,25 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { posts } from '../data/sampleData';
import PostCard from '../components/PostCard';
const PostCategory = () => {
const { category } = useParams();
return (
<div className="min-h-screen bg-gray-50 py-20">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold mb-12 text-center">
{category.charAt(0).toUpperCase() + category.slice(1)} 관련 포스트
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
</div>
);
};
export default PostCategory;

11
tailwind.config.js Normal file
View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}"
],
theme: {
extend: {},
},
plugins: [],
}