Ansible관련 fe추가
All checks were successful
Build And Test / build-and-push (push) Successful in 1m7s

This commit is contained in:
2025-05-20 19:06:08 +09:00
parent a015c52dbd
commit 01d1fbc72d
11 changed files with 393 additions and 13 deletions

View File

@ -1,3 +1,4 @@
REACT_APP_API_AUTH=http://127.0.0.1:8000 REACT_APP_API_AUTH=http://127.0.0.1:8000
REACT_APP_API_BLOG=http://127.0.0.1:8800 REACT_APP_API_BLOG=http://127.0.0.1:8800
REACT_APP_API_TODO=http://127.0.0.1:8880 REACT_APP_API_TODO=http://127.0.0.1:8880
REACT_APP_API_ANSIBLE=http://127.0.0.1:8888

View File

@ -1,3 +1,4 @@
REACT_APP_API_AUTH=https://www.icurfer.com REACT_APP_API_AUTH=https://www.icurfer.com
REACT_APP_API_BLOG=https://www.icurfer.com REACT_APP_API_BLOG=https://www.icurfer.com
REACT_APP_API_TODO=https://www.icurfer.com REACT_APP_API_TODO=https://www.icurfer.com
REACT_APP_API_ANSIBLE=https://www.icurfer.com

View File

@ -13,6 +13,7 @@ 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 AnsiblePage from "./pages/Ansible";
import { AuthProvider } from "./context/AuthContext"; import { AuthProvider } from "./context/AuthContext";
@ -35,6 +36,7 @@ function App() {
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/profile" element={<Profile />} /> <Route path="/profile" element={<Profile />} />
<Route path="/tasks" element={<TaskPage />} /> <Route path="/tasks" element={<TaskPage />} />
<Route path="/ansible" element={<AnsiblePage />} />
</Routes> </Routes>
</main> </main>
<Footer /> <Footer />

12
src/api/ansibleApi.js Normal file
View File

@ -0,0 +1,12 @@
// src/api/todoApi.js
import axios from 'axios';
import { attachAuthInterceptors } from './attachInterceptors';
const ansibleApi = axios.create({
baseURL: process.env.REACT_APP_API_ANSIBLE,
headers: { 'Content-Type': 'application/json' }
});
attachAuthInterceptors(ansibleApi);
export default ansibleApi;

View File

@ -0,0 +1,135 @@
import React, { useState, useEffect } from "react";
const AnsibleDetail = ({ task, onClose, onRun, onDelete, onEdit }) => {
const [editMode, setEditMode] = useState(false);
const [form, setForm] = useState({
name: "",
playbook_content: "",
inventory_content: "",
});
// task가 변경될 때 form 초기화
useEffect(() => {
if (task) {
setForm({
name: task.name || "",
playbook_content: task.playbook_content || "",
inventory_content: task.inventory_content || "",
});
}
}, [task]);
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSave = () => {
if (!form.name.trim()) return alert("이름을 입력해주세요.");
onEdit(task.id, form); // 부모에게 수정 요청
setEditMode(false);
};
if (!task) return null;
return (
<div className="fixed top-0 right-0 w-full sm:w-1/2 md:w-1/3 h-full bg-white shadow-lg p-6 overflow-y-auto z-50">
<button onClick={onClose} className="text-gray-600 hover:text-black float-right">
</button>
<h2 className="text-xl font-bold text-blue-600 mb-4">
{editMode ? "작업 수정" : task.name}
</h2>
{/* 이름 */}
<div className="mb-4">
<label className="font-semibold">이름</label>
{editMode ? (
<input
name="name"
value={form.name}
onChange={handleChange}
className="w-full border px-2 py-1 rounded mt-1"
/>
) : (
<div>{task.name}</div>
)}
</div>
{/* Playbook */}
<div className="mb-4">
<label className="font-semibold">Playbook</label>
{editMode ? (
<textarea
name="playbook_content"
value={form.playbook_content}
onChange={handleChange}
rows={6}
className="w-full border px-2 py-1 rounded mt-1"
/>
) : (
<pre className="bg-gray-100 p-2 rounded whitespace-pre-wrap">
{task.playbook_content}
</pre>
)}
</div>
{/* Inventory */}
<div className="mb-4">
<label className="font-semibold">Inventory</label>
{editMode ? (
<textarea
name="inventory_content"
value={form.inventory_content}
onChange={handleChange}
rows={4}
className="w-full border px-2 py-1 rounded mt-1"
/>
) : (
<pre className="bg-gray-100 p-2 rounded whitespace-pre-wrap">
{task.inventory_content}
</pre>
)}
</div>
{/* 버튼 영역 */}
<div className="flex justify-end space-x-2">
{editMode ? (
<>
<button onClick={handleSave} className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
💾 저장
</button>
<button onClick={() => setEditMode(false)} className="bg-gray-400 text-white px-4 py-2 rounded">
취소
</button>
</>
) : (
<>
<button onClick={() => setEditMode(true)} className="bg-yellow-400 text-white px-4 py-2 rounded hover:bg-yellow-500">
수정
</button>
<button onClick={() => onRun(task.id)} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
실행
</button>
<button onClick={() => onDelete(task.id)} className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
🗑 삭제
</button>
</>
)}
</div>
<div className="mb-4">
{task.output && (
<div className="mb-4">
<strong>실행 결과:</strong>
<pre className="bg-black text-green-400 p-3 rounded mt-1 text-sm whitespace-pre-wrap overflow-auto max-h-96">
{task.output}
</pre>
</div>
)}
</div>
</div>
);
};
export default AnsibleDetail;

View File

@ -0,0 +1,69 @@
import React, { useState } from "react";
const AnsibleForm = ({ onCreate, onCancel }) => {
const [form, setForm] = useState({
name: "",
playbook_content: "",
inventory_content: "",
});
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
if (!form.name.trim()) return alert("이름을 입력하세요.");
onCreate(form);
setForm({ name: "", playbook_content: "", inventory_content: "" });
};
return (
<form onSubmit={handleSubmit} className="bg-white p-6 rounded shadow mb-6">
<h2 className="text-xl font-bold mb-4 text-[#3B82F6]">📝 작업 생성</h2>
<div className="mb-4">
<label className="block text-sm font-semibold mb-1">이름</label>
<input
name="name"
value={form.name}
onChange={handleChange}
className="w-full border px-3 py-2 rounded"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-semibold mb-1">Playbook 내용</label>
<textarea
name="playbook_content"
value={form.playbook_content}
onChange={handleChange}
rows={6}
className="w-full border px-3 py-2 rounded"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-semibold mb-1">Inventory 내용</label>
<textarea
name="inventory_content"
value={form.inventory_content}
onChange={handleChange}
rows={4}
className="w-full border px-3 py-2 rounded"
/>
</div>
<div className="flex justify-end space-x-2">
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
등록
</button>
<button type="button" onClick={onCancel} className="bg-gray-400 text-white px-4 py-2 rounded">
취소
</button>
</div>
</form>
);
};
export default AnsibleForm;

View File

@ -0,0 +1,14 @@
const AnsibleItem = ({ job, onClick }) => {
return (
<div
className="p-4 border rounded shadow-sm bg-white mb-2 cursor-pointer hover:bg-gray-50"
onClick={() => onClick(job)}
>
<div className="text-sm text-gray-500">ID: {job.id}</div>
<div className="text-lg font-bold">{job.name}</div>
<div className="text-blue-600 font-medium">Status: {job.status}</div>
</div>
);
};
export default AnsibleItem;

View File

@ -0,0 +1,16 @@
import React from "react";
import AnsibleItem from "./AnsibleItem";
const AnsibleList = ({ jobs, onItemClick }) => {
if (jobs.length === 0) return <p>등록된 작업이 없습니다.</p>;
return (
<div className="space-y-2">
{jobs.map((job) => (
<AnsibleItem key={job.id} job={job} onClick={onItemClick} />
))}
</div>
);
};
export default AnsibleList;

View File

@ -6,8 +6,17 @@ const Navbar = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { isLoggedIn, logout, user } = useAuth(); const { isLoggedIn, logout, user } = useAuth();
// const menuItems = ["home", "about", "posts", "paas", "infra", "tasks"]; // ✅ 로그인 상태에 따라 메뉴 구성
const menuItems = ["home", "about", "posts", "tasks"]; const menuItems = [
{ name: "home", path: "/" },
{ name: "about", path: "/about" },
{ name: "posts", path: "/posts" },
];
if (isLoggedIn) {
menuItems.push({ name: "tasks", path: "/tasks" });
menuItems.push({ name: "ansible", path: "/ansible" });
}
return ( return (
<nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm"> <nav className="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm">
@ -20,19 +29,18 @@ const Navbar = () => {
<div className="hidden md:flex items-center space-x-8"> <div className="hidden md:flex items-center space-x-8">
{menuItems.map((item) => ( {menuItems.map((item) => (
<Link <Link
key={item} key={item.name}
to={item === "home" ? "/" : `/${item}`} to={item.path}
className="text-gray-600 hover:text-[#D4AF37]" className="text-gray-600 hover:text-[#D4AF37]"
> >
{item.charAt(0).toUpperCase() + item.slice(1)} {item.name.charAt(0).toUpperCase() + item.name.slice(1)}
</Link> </Link>
))} ))}
{isLoggedIn && ( {isLoggedIn ? (
<> <>
<Link <Link
to="/profile" to="/profile"
// className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-[#8b0000]"
className="bg-[#8b0000] text-white px-4 py-2 rounded hover:bg-gray-300" className="bg-[#8b0000] text-white px-4 py-2 rounded hover:bg-gray-300"
> >
정보 {user?.grade && `(${user.grade})`} 정보 {user?.grade && `(${user.grade})`}
@ -47,9 +55,7 @@ const Navbar = () => {
로그아웃 로그아웃
</button> </button>
</> </>
)} ) : (
{!isLoggedIn && (
<> <>
<Link <Link
to="/login" to="/login"

124
src/pages/Ansible.js Normal file
View File

@ -0,0 +1,124 @@
import React, { useEffect, useState } from "react";
import AnsibleList from "../components/Ansible/AnsibleList";
import AnsibleDetail from "../components/Ansible/AnsibleDetail";
import AnsibleForm from "../components/Ansible/AnsibleForm";
import axios from "axios";
const AnsiblePage = () => {
const [jobs, setJobs] = useState([]);
const [selectedJob, setSelectedJob] = useState(null);
const [showForm, setShowForm] = useState(false);
const fetchJobs = async () => {
try {
const token = localStorage.getItem("access");
const res = await axios.get(`${process.env.REACT_APP_API_ANSIBLE}/api/ansible/tasks/`, {
headers: { Authorization: `Bearer ${token}` },
});
setJobs(res.data);
} catch (err) {
console.error("❌ 작업 목록 조회 실패", err);
alert("작업 목록을 불러오는 데 실패했습니다.");
}
};
const handleCreate = async (form) => {
try {
const token = localStorage.getItem("access");
await axios.post(`${process.env.REACT_APP_API_ANSIBLE}/api/ansible/tasks/`, form, {
headers: { Authorization: `Bearer ${token}` },
});
alert("✅ 작업이 생성되었습니다.");
setShowForm(false);
fetchJobs();
} catch (err) {
console.error("❌ 작업 생성 실패", err);
alert("작업 생성에 실패했습니다.");
}
};
const handleRun = async (id) => {
try {
const token = localStorage.getItem("access");
await axios.post(`${process.env.REACT_APP_API_ANSIBLE}/api/ansible/tasks/${id}/run/`, {}, {
headers: { Authorization: `Bearer ${token}` },
});
alert("✅ 작업 실행 요청 완료");
fetchJobs();
} catch (err) {
console.error("❌ 작업 실행 실패", err);
alert("작업 실행에 실패했습니다.");
}
};
const handleDelete = async (id) => {
if (!window.confirm("정말로 삭제하시겠습니까?")) return;
try {
const token = localStorage.getItem("access");
await axios.delete(`${process.env.REACT_APP_API_ANSIBLE}/api/ansible/tasks/${id}/`, {
headers: { Authorization: `Bearer ${token}` },
});
alert("✅ 삭제 완료");
fetchJobs();
setSelectedJob(null);
} catch (err) {
console.error("❌ 삭제 실패", err);
alert("삭제 실패");
}
};
const handleEdit = async (id, updatedData) => {
try {
const token = localStorage.getItem("access");
await axios.put(
`${process.env.REACT_APP_API_ANSIBLE}/api/ansible/tasks/${id}/`,
updatedData,
{ headers: { Authorization: `Bearer ${token}` } }
);
alert("✅ 작업 수정 완료");
fetchJobs();
setSelectedJob(null);
} catch (err) {
console.error("❌ 작업 수정 실패", err);
alert("작업 수정에 실패했습니다.");
}
};
useEffect(() => {
fetchJobs();
}, []);
return (
<div className="min-h-screen bg-gray-100 py-10 px-6">
<div className="max-w-3xl mx-auto">
{/* 상단 버튼 */}
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold text-[#3B82F6]">Ansible 작업 목록</h1>
<button
onClick={() => setShowForm(!showForm)}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
{showForm ? "닫기" : " 작업 생성"}
</button>
</div>
{/* 생성 폼 */}
{showForm && <AnsibleForm onCreate={handleCreate} onCancel={() => setShowForm(false)} />}
{/* 작업 리스트 */}
<AnsibleList jobs={jobs} onItemClick={setSelectedJob} />
</div>
{/* 상세창 Drawer */}
<AnsibleDetail
task={selectedJob}
onClose={() => setSelectedJob(null)}
onRun={handleRun}
onDelete={handleDelete}
onEdit={handleEdit}
/>
</div>
);
};
export default AnsiblePage;

View File

@ -1 +1 @@
0.0.12 0.0.13-rc1