diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index dfd8706..d27d291 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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() {
- } /> - } /> + } /> + } /> @@ -95,6 +95,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 86bbb58..b65f5d7 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -62,7 +62,7 @@ const Sidebar = ({ isMobile = false }) => { setMinecraftLink(data); } } catch (error) { - console.error('연동 상태 확인 실패:', error); + // 에러 무시 } }; @@ -72,41 +72,41 @@ 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; - try { - const token = localStorage.getItem('token'); - const res = await fetch('/link/status', { - headers: { 'Authorization': `Bearer ${token}` } - }); - const data = await res.json(); - if (data.linked) { - setMinecraftLink(data); - // fetch 완료 후 checkAuth로 user.name 갱신 - await checkAuth(); - } - } catch (error) { - // 무시 - } - }; - - socket.on('status', (status) => { + 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) { - syncNickname(); + isSyncing = true; + try { + const token = localStorage.getItem('token'); + const res = await fetch('/link/status', { + headers: { 'Authorization': `Bearer ${token}` } + }); + const data = await res.json(); + if (data.linked) { + setMinecraftLink(data); + await checkAuth(); + } + } catch (error) { + // 무시 + } finally { + isSyncing = false; + } } } }); return () => socket.disconnect(); - }, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name]); + }, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name, checkAuth]); // 토스트 자동 숨기기 useEffect(() => { diff --git a/frontend/src/index.css b/frontend/src/index.css index a8299f7..0d69a4f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; } diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index d8bf58a..b6279a8 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -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 ( -
- {/* 헤더 */} -
-
-
- +
+ {/* 모바일용 헤더 */} + {isMobile && ( +
+
+ + + +
+

관리자

+

서버 관리 및 설정

+
-
+
+ )} + +
+ {/* 데스크탑용 타이틀 */} + {!isMobile && ( +

관리자 페이지

-

서버 관리 및 설정

+

서버 관리 및 설정

-
- - -
+ )} - {/* 사용자 정보 */} -
-

로그인 정보

-
-
- 이름 -

{user?.name || '-'}

+ {/* 관리자 정보 카드 */} +
+

+ + 관리자 정보 +

+ +
+
+ +
+ +
+
+

닉네임

+

{user?.name || '-'}

+
+
+

이메일

+

{user?.email}

+
+
-
- 이메일 -

{user?.email}

-
-
-
+ - {/* 관리 기능 카드 */} -
- {/* 서버 상태 */} -
-
- -

서버 상태

-
-

- 마인크래프트 서버 상태 모니터링 및 관리 -

-
- 정상 작동 중 -
-
+ {/* 서버 관리 섹션 */} +
+

+ + 서버 관리 +

+ +
+ {/* 서버 명령어 */} +
+
+ +

서버 명령어

+
+

+ 마인크래프트 서버에 명령어를 전송합니다. +

+
+ 추후 업데이트 예정 +
+
- {/* 플레이어 관리 */} -
-
- -

플레이어 관리

+ {/* 공지 전송 */} +
+
+ +

공지 전송

+
+

+ 게임 내 모든 플레이어에게 공지를 전송합니다. +

+
+ 추후 업데이트 예정 +
+
-

+

+ + {/* 플레이어 관리 섹션 */} +
+

+ + 플레이어 관리 +

+ +

접속 중인 플레이어 목록 및 관리 기능

추후 업데이트 예정
-
+ - {/* 설정 */} -
-
- -

설정

-
-

+ {/* 설정 섹션 */} +

+

+ + 설정 +

+ +

대시보드 설정 및 구성 관리

추후 업데이트 예정
-
-
+ +
); } + diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx index 1a856e0..1591a72 100644 --- a/frontend/src/pages/ProfilePage.jsx +++ b/frontend/src/pages/ProfilePage.jsx @@ -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) { - const playerInGame = status.players.list.find(p => p.uuid === linkStatus.minecraftUuid); - // 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화 - if (playerInGame && playerInGame.name !== user?.name) { - // fetchLinkStatus가 DB 업데이트 → 완료 후 checkAuth로 user.name 갱신 + 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) { + 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 ( -
- {/* 헤더 */} -
-
-
+
+ {/* 모바일용 헤더 */} + {isMobile && ( +
+
-
-

프로필

-

계정 정보 및 마인크래프트 연동

+
+

프로필

+

계정 정보 및 마인크래프트 연동

-
-
+
+ )} -
+
+ {/* 데스크탑용 타이틀 */} + {!isMobile && ( +
+

프로필 관리

+

계정 정보 및 마인크래프트 연동

+
+ )} {/* 프로필 정보 카드 */}

@@ -310,14 +320,14 @@ export default function ProfilePage() {

{/* 계정 관리 섹션 */} -
+

계정 관리

-
+

회원 탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다.