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"}
+
+ ))}
+
+
+
+ );
+}