refactor: 프로필/관리자 페이지 UI 개선
- 모바일 툴바 h-14 통일 - 데스크탑 사이드바 레이아웃 통합 - 관리자 페이지 프로필과 동일한 스타일 적용 - 불필요한 hooks/api.js 파일 제거 - body 배경 gradient에서 단색으로 변경
This commit is contained in:
parent
9adc0fe19b
commit
ba907ec8eb
5 changed files with 186 additions and 140 deletions
|
|
@ -34,8 +34,8 @@ function App() {
|
||||||
const isAuthPage = ['/login', '/register'].includes(location.pathname) ||
|
const isAuthPage = ['/login', '/register'].includes(location.pathname) ||
|
||||||
location.pathname.startsWith('/verify/');
|
location.pathname.startsWith('/verify/');
|
||||||
|
|
||||||
// 별도 레이아웃 페이지 (관리자, 프로필)
|
// 별도 레이아웃 페이지 (모바일에서만 standalone)
|
||||||
const isStandalonePage = ['/admin', '/profile'].includes(location.pathname);
|
const isStandalonePage = (location.pathname === '/admin' || location.pathname === '/profile') && isMobile;
|
||||||
|
|
||||||
// 라우트 전환 시 스크롤 맨 위로
|
// 라우트 전환 시 스크롤 맨 위로
|
||||||
useEffect(() => {
|
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>
|
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-[0.03] pointer-events-none"></div>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<Routes location={location} key={location.pathname}>
|
<Routes location={location} key={location.pathname}>
|
||||||
<Route path="/admin" element={<PageWrapper><Admin /></PageWrapper>} />
|
<Route path="/admin" element={<PageWrapper><Admin isMobile={isMobile} /></PageWrapper>} />
|
||||||
<Route path="/profile" element={<PageWrapper><ProfilePage /></PageWrapper>} />
|
<Route path="/profile" element={<PageWrapper><ProfilePage isMobile={isMobile} /></PageWrapper>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -95,6 +95,8 @@ function App() {
|
||||||
<Route path="/players" element={<PageWrapper><PlayersPage isMobile={isMobile} /></PageWrapper>} />
|
<Route path="/players" element={<PageWrapper><PlayersPage isMobile={isMobile} /></PageWrapper>} />
|
||||||
<Route path="/player/:uuid/stats" element={<PageWrapper><PlayerStatsPage 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="/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>
|
</Routes>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
setMinecraftLink(data);
|
setMinecraftLink(data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('연동 상태 확인 실패:', error);
|
// 에러 무시
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -72,41 +72,41 @@ const Sidebar = ({ isMobile = false }) => {
|
||||||
// 서버 상태 확인 (socket.io) + 닉네임 변경 시에만 동기화
|
// 서버 상태 확인 (socket.io) + 닉네임 변경 시에만 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = io(window.location.origin, { path: '/socket.io' });
|
const socket = io(window.location.origin, { path: '/socket.io' });
|
||||||
|
let isSyncing = false;
|
||||||
|
|
||||||
// 닉네임 동기화 함수 (변경 시에만 호출됨)
|
socket.on('status', async (status) => {
|
||||||
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) => {
|
|
||||||
setServerOnline(status?.online || false);
|
setServerOnline(status?.online || false);
|
||||||
|
|
||||||
|
// 동기화 중이면 스킵
|
||||||
|
if (isSyncing) return;
|
||||||
|
|
||||||
// 연동된 유저인 경우, 소켓에서 받은 닉네임과 현재 닉네임 비교
|
// 연동된 유저인 경우, 소켓에서 받은 닉네임과 현재 닉네임 비교
|
||||||
if (status?.online && minecraftLink?.minecraftUuid && status?.players?.list) {
|
if (status?.online && minecraftLink?.minecraftUuid && status?.players?.list) {
|
||||||
const playerInGame = status.players.list.find(p => p.uuid === minecraftLink.minecraftUuid);
|
const playerInGame = status.players.list.find(p => p.uuid === minecraftLink.minecraftUuid);
|
||||||
// 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화
|
// 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화
|
||||||
if (playerInGame && playerInGame.name !== user?.name) {
|
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();
|
return () => socket.disconnect();
|
||||||
}, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name]);
|
}, [isLoggedIn, minecraftLink?.minecraftUuid, user?.name, checkAuth]);
|
||||||
|
|
||||||
// 토스트 자동 숨기기
|
// 토스트 자동 숨기기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* 기본 body 스타일 - 다크 배경 (약간 밝게) */
|
/* 기본 body 스타일 - 다크 배경 */
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(135deg, #141414 0%, #181a18 50%, #141414 100%);
|
background: #141414;
|
||||||
background-attachment: fixed;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
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 { 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';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
export default function Admin() {
|
export default function Admin({ isMobile = false }) {
|
||||||
const { isLoggedIn, isAdmin, user, loading, logout } = useAuth();
|
const { isLoggedIn, isAdmin, user, loading } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [toast, setToast] = useState(null);
|
const [toast, setToast] = useState(null);
|
||||||
|
|
@ -18,7 +18,6 @@ export default function Admin() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
// 로그인 후 다시 admin으로 돌아올 수 있도록 현재 경로 전달
|
|
||||||
navigate('/login', { state: { from: location.pathname } });
|
navigate('/login', { state: { from: location.pathname } });
|
||||||
} else if (!isAdmin) {
|
} else if (!isAdmin) {
|
||||||
setToast('관리자 권한이 필요합니다.');
|
setToast('관리자 권한이 필요합니다.');
|
||||||
|
|
@ -29,11 +28,6 @@ export default function Admin() {
|
||||||
}
|
}
|
||||||
}, [isLoggedIn, isAdmin, loading, navigate, location.pathname]);
|
}, [isLoggedIn, isAdmin, loading, navigate, location.pathname]);
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
logout();
|
|
||||||
navigate('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 토스트 자동 숨기기
|
// 토스트 자동 숨기기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (toast) {
|
if (toast) {
|
||||||
|
|
@ -74,87 +68,128 @@ export default function Admin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
<div className="pb-8">
|
||||||
{/* 헤더 */}
|
{/* 모바일용 헤더 */}
|
||||||
<div className="flex items-center justify-between mb-8">
|
{isMobile && (
|
||||||
<div className="flex items-center gap-3">
|
<header className="bg-mc-dark border-b border-white/10 sticky top-0 z-50 backdrop-blur-xl">
|
||||||
<div className="p-3 rounded-xl bg-yellow-500/20 border border-yellow-500/30">
|
<div className="flex items-center h-14 px-4">
|
||||||
<Shield className="w-6 h-6 text-yellow-500" />
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="p-2 -ml-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<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>
|
</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>
|
<h1 className="text-2xl font-bold text-white">관리자 페이지</h1>
|
||||||
<p className="text-sm text-zinc-400">서버 관리 및 설정</p>
|
<p className="text-sm text-zinc-500 mt-1">서버 관리 및 설정</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<button
|
{/* 관리자 정보 카드 */}
|
||||||
onClick={handleLogout}
|
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition-colors"
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||||
>
|
<Shield size={20} className="text-yellow-500" />
|
||||||
<LogOut className="w-4 h-4" />
|
관리자 정보
|
||||||
로그아웃
|
</h2>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 사용자 정보 */}
|
<div className="flex items-center gap-6">
|
||||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 mb-6">
|
<div className="w-16 h-16 rounded-2xl bg-yellow-500/20 border border-yellow-500/30 flex items-center justify-center">
|
||||||
<h2 className="text-lg font-semibold text-white mb-4">로그인 정보</h2>
|
<User size={32} className="text-yellow-500" />
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
</div>
|
||||||
<div>
|
|
||||||
<span className="text-zinc-500">이름</span>
|
|
||||||
<p className="text-white mt-1">{user?.name || '-'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-zinc-500">이메일</span>
|
|
||||||
<p className="text-white mt-1">{user?.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 관리 기능 카드 */}
|
<div className="flex-1 space-y-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div>
|
||||||
{/* 서버 상태 */}
|
<p className="text-sm text-zinc-500">닉네임</p>
|
||||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
<p className="text-lg font-medium text-white">{user?.name || '-'}</p>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
</div>
|
||||||
<Server className="w-5 h-5 text-mc-green" />
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">서버 상태</h2>
|
<p className="text-sm text-zinc-500">이메일</p>
|
||||||
|
<p className="text-white">{user?.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-zinc-400 text-sm">
|
</section>
|
||||||
마인크래프트 서버 상태 모니터링 및 관리
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 플레이어 관리 */}
|
{/* 서버 관리 섹션 */}
|
||||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||||
<Users className="w-5 h-5 text-blue-400" />
|
<Server size={20} className="text-mc-green" />
|
||||||
<h2 className="text-lg font-semibold text-white">플레이어 관리</h2>
|
서버 관리
|
||||||
|
</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-sm text-zinc-400 mb-3">
|
||||||
|
마인크래프트 서버에 명령어를 전송합니다.
|
||||||
|
</p>
|
||||||
|
<div className="text-zinc-500 text-sm">
|
||||||
|
추후 업데이트 예정
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 공지 전송 */}
|
||||||
|
<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-sm text-zinc-400 mb-3">
|
||||||
|
게임 내 모든 플레이어에게 공지를 전송합니다.
|
||||||
|
</p>
|
||||||
|
<div className="text-zinc-500 text-sm">
|
||||||
|
추후 업데이트 예정
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-zinc-400 text-sm">
|
</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>
|
</p>
|
||||||
<div className="mt-4 text-zinc-500 text-sm">
|
<div className="mt-4 text-zinc-500 text-sm">
|
||||||
추후 업데이트 예정
|
추후 업데이트 예정
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{/* 설정 */}
|
{/* 설정 섹션 */}
|
||||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 md:col-span-2">
|
<section className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||||
<Settings className="w-5 h-5 text-zinc-400" />
|
<Settings size={20} className="text-zinc-400" />
|
||||||
<h2 className="text-lg font-semibold text-white">설정</h2>
|
설정
|
||||||
</div>
|
</h2>
|
||||||
<p className="text-zinc-400 text-sm">
|
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
대시보드 설정 및 구성 관리
|
대시보드 설정 및 구성 관리
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 text-zinc-500 text-sm">
|
<div className="mt-4 text-zinc-500 text-sm">
|
||||||
추후 업데이트 예정
|
추후 업데이트 예정
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { ArrowLeft, Copy, Check, Gamepad2, Link as LinkIcon, Unlink, RefreshCw,
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { io } from 'socket.io-client';
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage({ isMobile = false }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, isLoggedIn, loading, checkAuth, logout } = useAuth();
|
const { user, isLoggedIn, loading, checkAuth, logout } = useAuth();
|
||||||
|
|
@ -32,6 +32,20 @@ export default function ProfilePage() {
|
||||||
}, [loading, isLoggedIn, navigate, location.pathname]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
fetchLinkStatus();
|
fetchLinkStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -46,7 +60,6 @@ export default function ProfilePage() {
|
||||||
setPolling(false);
|
setPolling(false);
|
||||||
setLinkToken(null);
|
setLinkToken(null);
|
||||||
setCommand('');
|
setCommand('');
|
||||||
// 유저 정보 새로고침
|
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
@ -59,10 +72,8 @@ export default function ProfilePage() {
|
||||||
if (!linkToken) return;
|
if (!linkToken) return;
|
||||||
|
|
||||||
const handleBeforeUnload = (e) => {
|
const handleBeforeUnload = (e) => {
|
||||||
// 토큰 무효화 API 호출 (sendBeacon으로 페이지 이탈 전 전송)
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
navigator.sendBeacon('/link/cancel', JSON.stringify({ authToken: token }));
|
navigator.sendBeacon('/link/cancel', JSON.stringify({ authToken: token }));
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.returnValue = '연동이 진행 중입니다. 페이지를 떠나시겠습니까?';
|
e.returnValue = '연동이 진행 중입니다. 페이지를 떠나시겠습니까?';
|
||||||
return e.returnValue;
|
return e.returnValue;
|
||||||
|
|
@ -77,15 +88,22 @@ export default function ProfilePage() {
|
||||||
if (!linkStatus?.minecraftUuid) return;
|
if (!linkStatus?.minecraftUuid) return;
|
||||||
|
|
||||||
const socket = io(window.location.origin, { path: '/socket.io' });
|
const socket = io(window.location.origin, { path: '/socket.io' });
|
||||||
|
let isSyncing = false;
|
||||||
|
|
||||||
socket.on('status', async (status) => {
|
socket.on('status', async (status) => {
|
||||||
if (status?.online && status?.players?.list) {
|
if (!status?.online || !status?.players?.list) return;
|
||||||
const playerInGame = status.players.list.find(p => p.uuid === linkStatus.minecraftUuid);
|
if (isSyncing) return;
|
||||||
// 게임 내 닉네임과 현재 저장된 닉네임이 다르면 동기화
|
|
||||||
if (playerInGame && playerInGame.name !== user?.name) {
|
const playerInGame = status.players.list.find(p => p.uuid === linkStatus.minecraftUuid);
|
||||||
// fetchLinkStatus가 DB 업데이트 → 완료 후 checkAuth로 user.name 갱신
|
if (playerInGame && playerInGame.name !== user?.name) {
|
||||||
|
isSyncing = true;
|
||||||
|
try {
|
||||||
await fetchLinkStatus();
|
await fetchLinkStatus();
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
|
} catch (error) {
|
||||||
|
// 무시
|
||||||
|
} finally {
|
||||||
|
isSyncing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -93,21 +111,6 @@ export default function ProfilePage() {
|
||||||
return () => socket.disconnect();
|
return () => socket.disconnect();
|
||||||
}, [linkStatus?.minecraftUuid, user?.name]);
|
}, [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 () => {
|
const handleRequestLink = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -175,26 +178,33 @@ export default function ProfilePage() {
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-mc-bg pb-8">
|
<div className="pb-8">
|
||||||
{/* 헤더 */}
|
{/* 모바일용 헤더 */}
|
||||||
<header className="bg-mc-dark border-b border-white/10 sticky top-0 z-50 backdrop-blur-xl">
|
{isMobile && (
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
|
<header className="bg-mc-dark border-b border-white/10 sticky top-0 z-50 backdrop-blur-xl">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center h-14 px-4">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
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} />
|
<ArrowLeft size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div className="ml-2">
|
||||||
<h1 className="text-xl font-bold text-white">프로필</h1>
|
<h1 className="text-lg font-bold text-white">프로필</h1>
|
||||||
<p className="text-sm text-zinc-500">계정 정보 및 마인크래프트 연동</p>
|
<p className="text-xs text-zinc-500">계정 정보 및 마인크래프트 연동</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</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">
|
<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">
|
<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>
|
||||||
|
|
||||||
{/* 계정 관리 섹션 */}
|
{/* 계정 관리 섹션 */}
|
||||||
<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">
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||||
<User size={20} className="text-zinc-400" />
|
<User size={20} className="text-zinc-400" />
|
||||||
계정 관리
|
계정 관리
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<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 className="text-sm text-zinc-400 mb-3">
|
||||||
회원 탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다.
|
회원 탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue