diff --git a/admin.html b/admin.html index c32109b..b865ff1 100644 --- a/admin.html +++ b/admin.html @@ -137,10 +137,24 @@ const { useState, useEffect, useRef, useCallback } = React; +// ── 인증 토큰 유틸 ──────────────────────────────────────────── + +const getToken = () => sessionStorage.getItem('admin_token'); +const setToken = (t) => sessionStorage.setItem('admin_token', t); +const clearToken = () => sessionStorage.removeItem('admin_token'); + // ── 유틸 ────────────────────────────────────────────────────── -async function apiFetch(path, options) { - const res = await fetch(path, options); +async function apiFetch(path, options = {}) { + const token = getToken(); + const headers = { ...(options.headers || {}) }; + if (token) headers['Authorization'] = `Bearer ${token}`; + const res = await fetch(path, { ...options, headers }); + if (res.status === 401) { + clearToken(); + window.__onUnauthorized?.(); + throw new Error('인증이 필요합니다'); + } const data = await res.json(); if (!res.ok) throw new Error(data.error || `API 오류 (${res.status})`); return data; @@ -170,6 +184,8 @@ const Icon = ({ name, size = 16, stroke = 1.75, ...rest }) => { "chevron-left": <>, "chevron-right": <>, settings: <>, + lock: <>, + "log-out": <>, }; return ( @@ -178,6 +194,82 @@ const Icon = ({ name, size = 16, stroke = 1.75, ...rest }) => { ); }; +// ── 로그인 화면 ─────────────────────────────────────────────── + +function LoginScreen({ onLogin }) { + const [pw, setPw] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { inputRef.current?.focus(); }, []); + + const submit = async (e) => { + e?.preventDefault(); + setError(''); + setLoading(true); + try { + const res = await fetch('/api/admin/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password: pw }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || '로그인 실패'); + onLogin(data.token); + } catch (e) { + setError(e.message); + setPw(''); + inputRef.current?.focus(); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+
+
자료실 관리자
+
Admin Console
+
+
+ +
+
+ + { setPw(e.target.value); setError(''); }} + placeholder="비밀번호를 입력하세요" + disabled={loading} + /> +
+ {error && ( +
+ {error} +
+ )} + +
+
+
+ ); +} + // ── Toast ───────────────────────────────────────────────────── const Toast = ({ msg, on }) => ( @@ -666,7 +758,7 @@ const NAV = [ { id: 'users', label: '사용자 관리', icon: 'user' }, ]; -function AdminApp() { +function AdminContent({ onLogout }) { const [active, setActive] = useState('categories'); const [toast, setToast] = useState({ on: false, msg: '' }); @@ -675,6 +767,17 @@ function AdminApp() { setTimeout(() => setToast(s => ({ ...s, on: false })), 2200); }; + const handleLogout = async () => { + try { + await fetch('/api/admin/logout', { + method: 'POST', + headers: { Authorization: `Bearer ${getToken()}` }, + }); + } catch (_) {} + clearToken(); + onLogout(); + }; + return (