관리자부분 추가
This commit is contained in:
parent
700e1ce666
commit
3abdfbfb95
@ -4,6 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2283573567825482"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
<title>숙제노기</title>
|
<title>숙제노기</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
14
src/App.tsx
14
src/App.tsx
@ -1,6 +1,6 @@
|
|||||||
// src/App.tsx
|
// src/App.tsx
|
||||||
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material'
|
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material'
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { useAuth } from './contexts/AuthContext'
|
import { useAuth } from './contexts/AuthContext'
|
||||||
import Layout from './components/Layout'
|
import Layout from './components/Layout'
|
||||||
import Home from './pages/Home'
|
import Home from './pages/Home'
|
||||||
@ -19,6 +19,12 @@ import GuidePage from './pages/Guide'
|
|||||||
import FriendListPage from './pages/FriendListPage'
|
import FriendListPage from './pages/FriendListPage'
|
||||||
import FriendRequestsPage from './pages/FriendRequestsPage'
|
import FriendRequestsPage from './pages/FriendRequestsPage'
|
||||||
import FriendCharacterDashboard from './pages/FriendCharacterDashboard'
|
import FriendCharacterDashboard from './pages/FriendCharacterDashboard'
|
||||||
|
import { ADMIN_BASE } from './lib/config';
|
||||||
|
import RequireAdmin from './pages/admin/RequireAdmin';
|
||||||
|
import AdminLayout from './pages/admin/AdminLayout';
|
||||||
|
import AdminLogin from './pages/admin/Login';
|
||||||
|
import AdminBoards from './pages/admin/Boards';
|
||||||
|
import AdminUsers from './pages/admin/Users';
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@ -52,6 +58,12 @@ function App() {
|
|||||||
<Route path="/friends" element={<FriendListPage />} />
|
<Route path="/friends" element={<FriendListPage />} />
|
||||||
<Route path="/friends/:friend_id/characters" element={<FriendCharacterDashboard />} />
|
<Route path="/friends/:friend_id/characters" element={<FriendCharacterDashboard />} />
|
||||||
<Route path="/friends/requests" element={<FriendRequestsPage />} />
|
<Route path="/friends/requests" element={<FriendRequestsPage />} />
|
||||||
|
<Route path={`${ADMIN_BASE}/login`} element={<AdminLogin />} />
|
||||||
|
<Route path={ADMIN_BASE} element={<RequireAdmin><AdminLayout /></RequireAdmin>}>
|
||||||
|
<Route index element={<Navigate to="boards" replace />} />
|
||||||
|
<Route path="boards" element={<AdminBoards />} />
|
||||||
|
<Route path="users" element={<AdminUsers />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
28
src/lib/adminApi.ts
Normal file
28
src/lib/adminApi.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { API_BASE, ADMIN_API_PREFIX } from "./config";
|
||||||
|
|
||||||
|
export async function adminLogin(username: string, password: string) {
|
||||||
|
const body = new URLSearchParams({ username, password });
|
||||||
|
const res = await fetch(`${API_BASE}${ADMIN_API_PREFIX}/auth/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(await res.text().catch(()=>res.statusText));
|
||||||
|
return res.json() as Promise<{ access_token: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function adminListBoards(token: string) {
|
||||||
|
const res = await fetch(`${API_BASE}${ADMIN_API_PREFIX}/boards`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(await res.text().catch(()=>res.statusText));
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function adminListUsers(token: string) {
|
||||||
|
const res = await fetch(`${API_BASE}${ADMIN_API_PREFIX}/members`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(await res.text().catch(()=>res.statusText));
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
5
src/lib/adminAuth.ts
Normal file
5
src/lib/adminAuth.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const getAdminToken = () => localStorage.getItem("admin_token");
|
||||||
|
export const setAdminToken = (t: string | null) => {
|
||||||
|
if (t) localStorage.setItem("admin_token", t);
|
||||||
|
else localStorage.removeItem("admin_token");
|
||||||
|
};
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'https://api.biryu2000.kr',
|
// baseURL: 'https://api.biryu2000.kr',
|
||||||
// baseURL: 'http://localhost:8000',
|
baseURL: 'http://localhost:8000',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 요청 시 토큰 자동 추가
|
// 요청 시 토큰 자동 추가
|
||||||
|
|||||||
14
src/lib/config.ts
Normal file
14
src/lib/config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// 환경변수는 '있으면' 쓰고, 없으면 기본값으로 동작합니다.
|
||||||
|
const env = (k: string) => (import.meta as any)?.env?.[k];
|
||||||
|
|
||||||
|
export const API_BASE =
|
||||||
|
env("VITE_API_BASE_URL") ||
|
||||||
|
(location.hostname === "localhost"
|
||||||
|
? "http://localhost:8000" // 로컬 기본: FastAPI
|
||||||
|
: `${location.origin}`); // 배포 기본: 동일 오리진 프록시 가정
|
||||||
|
|
||||||
|
const rawAdminBase = env("VITE_ADMIN_BASE_PATH") ?? "/siteManage";
|
||||||
|
export const ADMIN_BASE = rawAdminBase.startsWith("/") ? rawAdminBase : `/${rawAdminBase}`;
|
||||||
|
|
||||||
|
export const ADMIN_API_PREFIX =
|
||||||
|
env("VITE_ADMIN_API_PREFIX") || "/admin"; // 백엔드 관리자 API 프리픽스
|
||||||
@ -6,6 +6,7 @@ export default function Home() {
|
|||||||
const { isLoggedIn } = useAuth()
|
const { isLoggedIn } = useAuth()
|
||||||
|
|
||||||
const updates = [
|
const updates = [
|
||||||
|
{ date: '2025-09-19', version: 'v2.0', content: '구글 애드센스를 붙였습니다. \'돈 좀 벌어보고싶다\'는 아니고. 그냥 휌해서...광고라도...' },
|
||||||
{ date: '2025-06-11', version: 'v2.01', content: '회원가입 bug fix' },
|
{ date: '2025-06-11', version: 'v2.01', content: '회원가입 bug fix' },
|
||||||
{ date: '2025-06-11', version: 'v2.0', content: '친구 기능 추가: 친구목록/요청 관리 및 캐릭터 공개 보기 지원' },
|
{ date: '2025-06-11', version: 'v2.0', content: '친구 기능 추가: 친구목록/요청 관리 및 캐릭터 공개 보기 지원' },
|
||||||
{ date: '2025-06-06', version: 'v1.1', content: '피드백 및 수정요청 및 기능개선은 nightbug@naver.com 으로 부탁드립니다.' },
|
{ date: '2025-06-06', version: 'v1.1', content: '피드백 및 수정요청 및 기능개선은 nightbug@naver.com 으로 부탁드립니다.' },
|
||||||
@ -58,16 +59,16 @@ export default function Home() {
|
|||||||
<Divider sx={{ mt: 2 }} />
|
<Divider sx={{ mt: 2 }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h5" gutterBottom sx={{ mt: 6, textAlign: 'center' }}>
|
{/*<Typography variant="h5" gutterBottom sx={{ mt: 6, textAlign: 'center' }}>*/}
|
||||||
라사서버 길드 '노인정'과 함께합니다. 누구마음대로?
|
{/* 라사서버 길드 '노인정'과 함께합니다. 누구마음대로?*/}
|
||||||
</Typography>
|
{/*</Typography>*/}
|
||||||
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
{/*<Box sx={{ mt: 4, textAlign: 'center' }}>*/}
|
||||||
<img
|
{/* <img*/}
|
||||||
src="/guild.png"
|
{/* src="/guild.png"*/}
|
||||||
alt="노인정길드"
|
{/* alt="노인정길드"*/}
|
||||||
style={{ maxWidth: '100%', height: 'auto', borderRadius: '16px' }}
|
{/* style={{ maxWidth: '100%', height: 'auto', borderRadius: '16px' }}*/}
|
||||||
/>
|
{/* />*/}
|
||||||
</Box>
|
{/*</Box>*/}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
22
src/pages/admin/AdminLayout.tsx
Normal file
22
src/pages/admin/AdminLayout.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
|
||||||
|
import { Box, Stack, Button } from "@mui/material";
|
||||||
|
import { setAdminToken } from "../../lib/adminAuth";
|
||||||
|
import { ADMIN_BASE } from "../../lib/config";
|
||||||
|
|
||||||
|
export default function AdminLayout() {
|
||||||
|
const loc = useLocation();
|
||||||
|
const is = (s: string) => loc.pathname.endsWith(s);
|
||||||
|
return (
|
||||||
|
<Box p={2}>
|
||||||
|
<Stack direction="row" spacing={1} mb={2}>
|
||||||
|
<Button component={RouterLink} to="boards" variant={is(`${ADMIN_BASE}/boards`) ? "contained" : "outlined"}>게시판관리</Button>
|
||||||
|
<Button component={RouterLink} to="users" variant={is(`${ADMIN_BASE}/users`) ? "contained" : "outlined"}>회원관리</Button>
|
||||||
|
<Box flex={1} />
|
||||||
|
<Button color="error" onClick={()=>{ setAdminToken(null); location.href = location.pathname.replace(/\/[^\/]*$/, `${ADMIN_BASE}/login`); }}>
|
||||||
|
로그아웃
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Outlet />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/pages/admin/Boards.tsx
Normal file
22
src/pages/admin/Boards.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { adminListBoards } from "../../lib/adminApi";
|
||||||
|
import { getAdminToken } from "../../lib/adminAuth";
|
||||||
|
import { Box, Typography, List, ListItem, ListItemText } from "@mui/material";
|
||||||
|
|
||||||
|
export default function AdminBoards() {
|
||||||
|
const [items, setItems] = useState<any[]>([]);
|
||||||
|
useEffect(()=>{
|
||||||
|
const t = getAdminToken()!;
|
||||||
|
adminListBoards(t).then(setItems).catch(console.error);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" mb={1}>게시판 관리</Typography>
|
||||||
|
<List dense>
|
||||||
|
{items.map(b=>(
|
||||||
|
<ListItem key={b.id}><ListItemText primary={`${b.code} — ${b.name}`} secondary={b.description} /></ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/pages/admin/Login.tsx
Normal file
35
src/pages/admin/Login.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Box, Stack, TextField, Button, Typography, Alert } from "@mui/material";
|
||||||
|
import { adminLogin } from "../../lib/adminApi";
|
||||||
|
import { setAdminToken } from "../../lib/adminAuth";
|
||||||
|
import { ADMIN_BASE } from "../../lib/config";
|
||||||
|
|
||||||
|
export default function AdminLogin() {
|
||||||
|
const nav = useNavigate();
|
||||||
|
const [username, setU] = useState(""); const [password, setP] = useState("");
|
||||||
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function onSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const { access_token } = await adminLogin(username, password);
|
||||||
|
setAdminToken(access_token);
|
||||||
|
nav(`${ADMIN_BASE}/users`, { replace: true });
|
||||||
|
} catch (e:any) { setErr(e?.message || "로그인 실패"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box maxWidth={360} m="64px auto">
|
||||||
|
<Typography variant="h5" mb={2}>관리자 로그인</Typography>
|
||||||
|
<Box component="form" onSubmit={onSubmit}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<TextField label="아이디" value={username} onChange={e=>setU(e.target.value)} required />
|
||||||
|
<TextField label="비밀번호" type="password" value={password} onChange={e=>setP(e.target.value)} required />
|
||||||
|
<Button type="submit" variant="contained">로그인</Button>
|
||||||
|
{err && <Alert severity="error">{err}</Alert>}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/pages/admin/RequireAdmin.tsx
Normal file
7
src/pages/admin/RequireAdmin.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { getAdminToken } from "../../lib/adminAuth";
|
||||||
|
import { ADMIN_BASE } from "../../lib/config";
|
||||||
|
|
||||||
|
export default function RequireAdmin({ children }: { children: React.ReactNode }) {
|
||||||
|
return getAdminToken() ? <>{children}</> : <Navigate to={`${ADMIN_BASE}/login`} replace />;
|
||||||
|
}
|
||||||
47
src/pages/admin/Users.tsx
Normal file
47
src/pages/admin/Users.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { adminListUsers } from "../../lib/adminApi";
|
||||||
|
import { getAdminToken } from "../../lib/adminAuth";
|
||||||
|
import { Box, Typography, Table, TableHead, TableRow, TableCell, TableBody } from "@mui/material";
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
is_active?: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminUsers() {
|
||||||
|
const [rows, setRows] = useState<Row[]>([]);
|
||||||
|
useEffect(()=>{
|
||||||
|
const t = getAdminToken()!;
|
||||||
|
adminListUsers(t).then(setRows).catch(console.error);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" mb={1}>회원관리</Typography>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>ID</TableCell>
|
||||||
|
<TableCell>아이디</TableCell>
|
||||||
|
<TableCell>이메일</TableCell>
|
||||||
|
<TableCell>가입일</TableCell>
|
||||||
|
<TableCell>활성</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map(r=>(
|
||||||
|
<TableRow key={r.id}>
|
||||||
|
<TableCell>{r.id}</TableCell>
|
||||||
|
<TableCell>{r.username}</TableCell>
|
||||||
|
<TableCell>{r.email ?? "-"}</TableCell>
|
||||||
|
<TableCell>{r.created_at ? new Date(r.created_at).toLocaleString() : "-"}</TableCell>
|
||||||
|
<TableCell>{r.is_active === false ? "N" : "Y"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user