From 3abdfbfb95a6bf1f09df182590f2b0c7b7178b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A2=85=ED=98=B8?= Date: Fri, 19 Sep 2025 16:31:16 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=EB=B6=80=EB=B6=84?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 ++ src/App.tsx | 14 +++++++++- src/lib/adminApi.ts | 28 +++++++++++++++++++ src/lib/adminAuth.ts | 5 ++++ src/lib/api.ts | 4 +-- src/lib/config.ts | 14 ++++++++++ src/pages/Home.tsx | 21 +++++++------- src/pages/admin/AdminLayout.tsx | 22 +++++++++++++++ src/pages/admin/Boards.tsx | 22 +++++++++++++++ src/pages/admin/Login.tsx | 35 ++++++++++++++++++++++++ src/pages/admin/RequireAdmin.tsx | 7 +++++ src/pages/admin/Users.tsx | 47 ++++++++++++++++++++++++++++++++ 12 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 src/lib/adminApi.ts create mode 100644 src/lib/adminAuth.ts create mode 100644 src/lib/config.ts create mode 100644 src/pages/admin/AdminLayout.tsx create mode 100644 src/pages/admin/Boards.tsx create mode 100644 src/pages/admin/Login.tsx create mode 100644 src/pages/admin/RequireAdmin.tsx create mode 100644 src/pages/admin/Users.tsx diff --git a/index.html b/index.html index 42c25b5..df6223e 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,8 @@ + 숙제노기 diff --git a/src/App.tsx b/src/App.tsx index 51812e4..9dd4675 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ // src/App.tsx 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 Layout from './components/Layout' import Home from './pages/Home' @@ -19,6 +19,12 @@ import GuidePage from './pages/Guide' import FriendListPage from './pages/FriendListPage' import FriendRequestsPage from './pages/FriendRequestsPage' 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({ palette: { @@ -52,6 +58,12 @@ function App() { } /> } /> } /> + } /> + }> + } /> + } /> + } /> + diff --git a/src/lib/adminApi.ts b/src/lib/adminApi.ts new file mode 100644 index 0000000..e80efca --- /dev/null +++ b/src/lib/adminApi.ts @@ -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(); +} \ No newline at end of file diff --git a/src/lib/adminAuth.ts b/src/lib/adminAuth.ts new file mode 100644 index 0000000..2cf57e8 --- /dev/null +++ b/src/lib/adminAuth.ts @@ -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"); +}; diff --git a/src/lib/api.ts b/src/lib/api.ts index 9c788fe..7f420d7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,8 +1,8 @@ import axios from 'axios' const api = axios.create({ - baseURL: 'https://api.biryu2000.kr', - // baseURL: 'http://localhost:8000', + // baseURL: 'https://api.biryu2000.kr', + baseURL: 'http://localhost:8000', }) // 요청 시 토큰 자동 추가 diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..ab59f63 --- /dev/null +++ b/src/lib/config.ts @@ -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 프리픽스 diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 48d8ae9..f1cc7e3 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -6,6 +6,7 @@ export default function Home() { const { isLoggedIn } = useAuth() 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.0', content: '친구 기능 추가: 친구목록/요청 관리 및 캐릭터 공개 보기 지원' }, { date: '2025-06-06', version: 'v1.1', content: '피드백 및 수정요청 및 기능개선은 nightbug@naver.com 으로 부탁드립니다.' }, @@ -58,16 +59,16 @@ export default function Home() { - - 라사서버 길드 '노인정'과 함께합니다. 누구마음대로? - - - 노인정길드 - + {/**/} + {/* 라사서버 길드 '노인정'과 함께합니다. 누구마음대로?*/} + {/**/} + {/**/} + {/* */} + {/**/} )} diff --git a/src/pages/admin/AdminLayout.tsx b/src/pages/admin/AdminLayout.tsx new file mode 100644 index 0000000..13c4f06 --- /dev/null +++ b/src/pages/admin/AdminLayout.tsx @@ -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 ( + + + + + + + + + + ); +} diff --git a/src/pages/admin/Boards.tsx b/src/pages/admin/Boards.tsx new file mode 100644 index 0000000..4b22f3d --- /dev/null +++ b/src/pages/admin/Boards.tsx @@ -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([]); + useEffect(()=>{ + const t = getAdminToken()!; + adminListBoards(t).then(setItems).catch(console.error); + }, []); + return ( + + 게시판 관리 + + {items.map(b=>( + + ))} + + + ); +} diff --git a/src/pages/admin/Login.tsx b/src/pages/admin/Login.tsx new file mode 100644 index 0000000..34c1a84 --- /dev/null +++ b/src/pages/admin/Login.tsx @@ -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(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 ( + + 관리자 로그인 + + + setU(e.target.value)} required /> + setP(e.target.value)} required /> + + {err && {err}} + + + + ); +} diff --git a/src/pages/admin/RequireAdmin.tsx b/src/pages/admin/RequireAdmin.tsx new file mode 100644 index 0000000..5fc0076 --- /dev/null +++ b/src/pages/admin/RequireAdmin.tsx @@ -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} : ; +} diff --git a/src/pages/admin/Users.tsx b/src/pages/admin/Users.tsx new file mode 100644 index 0000000..48d0de0 --- /dev/null +++ b/src/pages/admin/Users.tsx @@ -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([]); + useEffect(()=>{ + const t = getAdminToken()!; + adminListUsers(t).then(setRows).catch(console.error); + }, []); + return ( + + 회원관리 + + + + ID + 아이디 + 이메일 + 가입일 + 활성 + + + + {rows.map(r=>( + + {r.id} + {r.username} + {r.email ?? "-"} + {r.created_at ? new Date(r.created_at).toLocaleString() : "-"} + {r.is_active === false ? "N" : "Y"} + + ))} + +
+
+ ); +}