refactor: 프로필/관리자 페이지 UI 개선

- 모바일 툴바 h-14 통일

- 데스크탑 사이드바 레이아웃 통합

- 관리자 페이지 프로필과 동일한 스타일 적용

- 불필요한 hooks/api.js 파일 제거

- body 배경 gradient에서 단색으로 변경
This commit is contained in:
caadiq 2025-12-22 14:57:34 +09:00
parent 9adc0fe19b
commit ba907ec8eb
5 changed files with 186 additions and 140 deletions

View file

@ -34,8 +34,8 @@ function App() {
const isAuthPage = ['/login', '/register'].includes(location.pathname) ||
location.pathname.startsWith('/verify/');
// (, )
const isStandalonePage = ['/admin', '/profile'].includes(location.pathname);
// ( standalone)
const isStandalonePage = (location.pathname === '/admin' || location.pathname === '/profile') && isMobile;
//
useEffect(() => {
@ -68,8 +68,8 @@ function App() {
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
<Route path="/admin" element={<PageWrapper><Admin /></PageWrapper>} />
<Route path="/profile" element={<PageWrapper><ProfilePage /></PageWrapper>} />
<Route path="/admin" element={<PageWrapper><Admin isMobile={isMobile} /></PageWrapper>} />
<Route path="/profile" element={<PageWrapper><ProfilePage isMobile={isMobile} /></PageWrapper>} />
</Routes>
</AnimatePresence>
</div>
@ -95,6 +95,8 @@ function App() {
<Route path="/players" element={<PageWrapper><PlayersPage isMobile={isMobile} /></PageWrapper>} />
<Route path="/player/:uuid/stats" element={<PageWrapper><PlayerStatsPage isMobile={isMobile} /></PageWrapper>} />
<Route path="/worldmap" element={<PageWrapper><WorldMapPage isMobile={isMobile} /></PageWrapper>} />
<Route path="/profile" element={<PageWrapper><ProfilePage isMobile={isMobile} /></PageWrapper>} />
<Route path="/admin" element={<PageWrapper><Admin isMobile={isMobile} /></PageWrapper>} />
</Routes>
</AnimatePresence>
</main>

View file

@ -62,7 +62,7 @@ const Sidebar = ({ isMobile = false }) => {
setMinecraftLink(data);
}
} catch (error) {
console.error('연동 상태 확인 실패:', error);
//
}
};
@ -72,10 +72,20 @@ const Sidebar = ({ isMobile = false }) => {
// (socket.io) +
useEffect(() => {
const socket = io(window.location.origin, { path: '/socket.io' });
let isSyncing = false;
// ( )
const syncNickname = async () => {
if (!isLoggedIn) return;
socket.on('status', async (status) => {
setServerOnline(status?.online || false);
//
if (isSyncing) return;
// ,
if (status?.online && minecraftLink?.minecraftUuid && status?.players?.list) {
const playerInGame = status.players.list.find(p => p.uuid === minecraftLink.minecraftUuid);
//
if (playerInGame && playerInGame.name !== user?.name) {
isSyncing = true;
try {
const token = localStorage.getItem('token');
const res = await fetch('/link/status', {
@ -84,29 +94,19 @@ const Sidebar = ({ isMobile = false }) => {
const data = await res.json();
if (data.linked) {
setMinecraftLink(data);
// fetch checkAuth user.name
await checkAuth();
}
} catch (error) {
//
} finally {
isSyncing = false;
}
};
socket.on('status', (status) => {
setServerOnline(status?.online || false);
// ,
if (status?.online && minecraftLink?.minecraftUuid && status?.players?.list) {
const playerInGame = status.players.list.find(p => p.uuid === minecraftLink.minecraftUuid);
//
if (playerInGame && playerInGame.name !== user?.name) {
syncNickname();
}
}
});
return () => socket.disconnect();
}, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name]);
}, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name, checkAuth]);
//
useEffect(() => {

View file

@ -2,10 +2,9 @@
@tailwind components;
@tailwind utilities;
/* 기본 body 스타일 - 다크 배경 (약간 밝게) */
/* 기본 body 스타일 - 다크 배경 */
body {
background: linear-gradient(135deg, #141414 0%, #181a18 50%, #141414 100%);
background-attachment: fixed;
background: #141414;
min-height: 100vh;
}

View file

@ -3,13 +3,13 @@
*/
import { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useNavigate, useLocation, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { Shield, LogOut, Settings, Server, Users, Loader2 } from 'lucide-react';
import { Shield, ArrowLeft, Settings, Server, Users, Loader2, User, Terminal, MessageSquare } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
export default function Admin() {
const { isLoggedIn, isAdmin, user, loading, logout } = useAuth();
export default function Admin({ isMobile = false }) {
const { isLoggedIn, isAdmin, user, loading } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [toast, setToast] = useState(null);
@ -18,7 +18,6 @@ export default function Admin() {
useEffect(() => {
if (!loading) {
if (!isLoggedIn) {
// admin
navigate('/login', { state: { from: location.pathname } });
} else if (!isAdmin) {
setToast('관리자 권한이 필요합니다.');
@ -29,11 +28,6 @@ export default function Admin() {
}
}, [isLoggedIn, isAdmin, loading, navigate, location.pathname]);
const handleLogout = () => {
logout();
navigate('/');
};
//
useEffect(() => {
if (toast) {
@ -74,87 +68,128 @@ export default function Admin() {
}
return (
<div className="p-6 max-w-4xl mx-auto">
{/* 헤더 */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-yellow-500/20 border border-yellow-500/30">
<Shield className="w-6 h-6 text-yellow-500" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">관리자 페이지</h1>
<p className="text-sm text-zinc-400">서버 관리 설정</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition-colors"
<div className="pb-8">
{/* 모바일용 헤더 */}
{isMobile && (
<header className="bg-mc-dark border-b border-white/10 sticky top-0 z-50 backdrop-blur-xl">
<div className="flex items-center h-14 px-4">
<Link
to="/"
className="p-2 -ml-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
>
<LogOut className="w-4 h-4" />
로그아웃
</button>
<ArrowLeft size={20} />
</Link>
<div className="ml-2">
<h1 className="text-lg font-bold text-white">관리자</h1>
<p className="text-xs text-zinc-500">서버 관리 설정</p>
</div>
</div>
</header>
)}
<main className={`max-w-4xl mx-auto px-4 sm:px-6 ${isMobile ? 'py-4' : 'py-8'} space-y-4 sm:space-y-6`}>
{/* 데스크탑용 타이틀 */}
{!isMobile && (
<div className="mb-6">
<h1 className="text-2xl font-bold text-white">관리자 페이지</h1>
<p className="text-sm text-zinc-500 mt-1">서버 관리 설정</p>
</div>
)}
{/* 관리자 정보 카드 */}
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
<Shield size={20} className="text-yellow-500" />
관리자 정보
</h2>
<div className="flex items-center gap-6">
<div className="w-16 h-16 rounded-2xl bg-yellow-500/20 border border-yellow-500/30 flex items-center justify-center">
<User size={32} className="text-yellow-500" />
</div>
{/* 사용자 정보 */}
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 mb-6">
<h2 className="text-lg font-semibold text-white mb-4">로그인 정보</h2>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex-1 space-y-3">
<div>
<span className="text-zinc-500">이름</span>
<p className="text-white mt-1">{user?.name || '-'}</p>
<p className="text-sm text-zinc-500">닉네임</p>
<p className="text-lg font-medium text-white">{user?.name || '-'}</p>
</div>
<div>
<span className="text-zinc-500">이메일</span>
<p className="text-white mt-1">{user?.email}</p>
<p className="text-sm text-zinc-500">이메일</p>
<p className="text-white">{user?.email}</p>
</div>
</div>
</div>
</section>
{/* 관리 기능 카드 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 서버 상태 */}
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<Server className="w-5 h-5 text-mc-green" />
<h2 className="text-lg font-semibold text-white">서버 상태</h2>
{/* 서버 관리 섹션 */}
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
<Server size={20} className="text-mc-green" />
서버 관리
</h2>
<div className="space-y-4">
{/* 서버 명령어 */}
<div className="p-4 bg-zinc-800/50 rounded-xl">
<div className="flex items-center gap-3 mb-2">
<Terminal size={18} className="text-zinc-400" />
<p className="font-medium text-white">서버 명령어</p>
</div>
<p className="text-zinc-400 text-sm">
마인크래프트 서버 상태 모니터링 관리
<p className="text-sm text-zinc-400 mb-3">
마인크래프트 서버에 명령어를 전송합니다.
</p>
<div className="mt-4 py-2 px-3 bg-mc-green/10 border border-mc-green/20 rounded-lg text-mc-green text-sm inline-block">
정상 작동
<div className="text-zinc-500 text-sm">
추후 업데이트 예정
</div>
</div>
{/* 플레이어 관리 */}
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<Users className="w-5 h-5 text-blue-400" />
<h2 className="text-lg font-semibold text-white">플레이어 관리</h2>
{/* 공지 전송 */}
<div className="p-4 bg-zinc-800/50 rounded-xl">
<div className="flex items-center gap-3 mb-2">
<MessageSquare size={18} className="text-zinc-400" />
<p className="font-medium text-white">공지 전송</p>
</div>
<p className="text-zinc-400 text-sm">
<p className="text-sm text-zinc-400 mb-3">
게임 모든 플레이어에게 공지를 전송합니다.
</p>
<div className="text-zinc-500 text-sm">
추후 업데이트 예정
</div>
</div>
</div>
</section>
{/* 플레이어 관리 섹션 */}
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
<Users size={20} className="text-blue-400" />
플레이어 관리
</h2>
<p className="text-sm text-zinc-400">
접속 중인 플레이어 목록 관리 기능
</p>
<div className="mt-4 text-zinc-500 text-sm">
추후 업데이트 예정
</div>
</div>
</section>
{/* 설정 */}
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 md:col-span-2">
<div className="flex items-center gap-3 mb-4">
<Settings className="w-5 h-5 text-zinc-400" />
<h2 className="text-lg font-semibold text-white">설정</h2>
</div>
<p className="text-zinc-400 text-sm">
{/* 설정 섹션 */}
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
<Settings size={20} className="text-zinc-400" />
설정
</h2>
<p className="text-sm text-zinc-400">
대시보드 설정 구성 관리
</p>
<div className="mt-4 text-zinc-500 text-sm">
추후 업데이트 예정
</div>
</div>
</div>
</section>
</main>
</div>
);
}

View file

@ -10,7 +10,7 @@ import { ArrowLeft, Copy, Check, Gamepad2, Link as LinkIcon, Unlink, RefreshCw,
import { motion, AnimatePresence } from 'framer-motion';
import { io } from 'socket.io-client';
export default function ProfilePage() {
export default function ProfilePage({ isMobile = false }) {
const navigate = useNavigate();
const location = useLocation();
const { user, isLoggedIn, loading, checkAuth, logout } = useAuth();
@ -32,6 +32,20 @@ export default function ProfilePage() {
}, [loading, isLoggedIn, navigate, location.pathname]);
//
const fetchLinkStatus = async () => {
try {
const token = localStorage.getItem('token');
const res = await fetch('/link/status', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
setLinkStatus(data);
return data;
} catch (error) {
return null;
}
};
useEffect(() => {
fetchLinkStatus();
}, []);
@ -46,7 +60,6 @@ export default function ProfilePage() {
setPolling(false);
setLinkToken(null);
setCommand('');
//
await checkAuth();
}
}, 3000);
@ -59,10 +72,8 @@ export default function ProfilePage() {
if (!linkToken) return;
const handleBeforeUnload = (e) => {
// API (sendBeacon )
const token = localStorage.getItem('token');
navigator.sendBeacon('/link/cancel', JSON.stringify({ authToken: token }));
e.preventDefault();
e.returnValue = '연동이 진행 중입니다. 페이지를 떠나시겠습니까?';
return e.returnValue;
@ -77,15 +88,22 @@ export default function ProfilePage() {
if (!linkStatus?.minecraftUuid) return;
const socket = io(window.location.origin, { path: '/socket.io' });
let isSyncing = false;
socket.on('status', async (status) => {
if (status?.online && status?.players?.list) {
if (!status?.online || !status?.players?.list) return;
if (isSyncing) return;
const playerInGame = status.players.list.find(p => p.uuid === linkStatus.minecraftUuid);
//
if (playerInGame && playerInGame.name !== user?.name) {
// fetchLinkStatus DB checkAuth user.name
isSyncing = true;
try {
await fetchLinkStatus();
await checkAuth();
} catch (error) {
//
} finally {
isSyncing = false;
}
}
});
@ -93,21 +111,6 @@ export default function ProfilePage() {
return () => socket.disconnect();
}, [linkStatus?.minecraftUuid, user?.name]);
const fetchLinkStatus = async () => {
try {
const token = localStorage.getItem('token');
const res = await fetch('/link/status', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
setLinkStatus(data);
return data;
} catch (error) {
console.error('연동 상태 확인 실패:', error);
return null;
}
};
const handleRequestLink = async () => {
setIsLoading(true);
try {
@ -175,26 +178,33 @@ export default function ProfilePage() {
if (!user) return null;
return (
<div className="bg-mc-bg pb-8">
{/* 헤더 */}
<div className="pb-8">
{/* 모바일용 헤더 */}
{isMobile && (
<header className="bg-mc-dark border-b border-white/10 sticky top-0 z-50 backdrop-blur-xl">
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center h-14 px-4">
<Link
to="/"
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
className="p-2 -ml-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
>
<ArrowLeft size={20} />
</Link>
<div>
<h1 className="text-xl font-bold text-white">프로필</h1>
<p className="text-sm text-zinc-500">계정 정보 마인크래프트 연동</p>
</div>
<div className="ml-2">
<h1 className="text-lg font-bold text-white">프로필</h1>
<p className="text-xs text-zinc-500">계정 정보 마인크래프트 연동</p>
</div>
</div>
</header>
)}
<main className="max-w-4xl mx-auto px-4 sm:px-6 py-6 sm:py-8 space-y-4 sm:space-y-6">
<main className={`max-w-4xl mx-auto px-4 sm:px-6 ${isMobile ? 'py-4' : 'py-8'} space-y-4 sm:space-y-6`}>
{/* 데스크탑용 타이틀 */}
{!isMobile && (
<div className="mb-6">
<h1 className="text-2xl font-bold text-white">프로필 관리</h1>
<p className="text-sm text-zinc-500 mt-1">계정 정보 마인크래프트 연동</p>
</div>
)}
{/* 프로필 정보 카드 */}
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
@ -310,14 +320,14 @@ export default function ProfilePage() {
</section>
{/* 계정 관리 섹션 */}
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<section className="bg-zinc-900 rounded-2xl p-6">
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
<User size={20} className="text-zinc-400" />
계정 관리
</h2>
<div className="space-y-4">
<div className="p-4 bg-zinc-800/50 border border-zinc-700 rounded-xl">
<div className="p-4 bg-zinc-800/50 rounded-xl">
<p className="text-sm text-zinc-400 mb-3">
회원 탈퇴 모든 데이터가 삭제되며 복구할 없습니다.
</p>