minecraft-web/frontend/src/pages/PlayersPage.jsx

183 lines
7 KiB
React
Raw Normal View History

2025-12-16 08:40:32 +09:00
import React, { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { Users, Clock, Circle, ServerOff } from 'lucide-react';
import { motion } from 'framer-motion';
import { io } from 'socket.io-client';
import { formatPlayTimeMs } from '../utils/formatters';
// 스티브 머리 기본 이미지 (로딩 전/실패 시 사용)
const STEVE_HEAD_BASE64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABWUlEQVRoge2ZPUsDQRCGk7DeGU1ysTDYpIuFjYUasMpHaZFKhJDWxj7gjwjYK/Y2wcJKsAppxEYkTcCU1oGcubt8gv6Bd4oBk3FhnvLZvb17GW7YvYsfF3Z+YgxSjuFMZxPMl9BPCZ9Y5cOsAw0gjQaQxvjBBA54qST0VJfgdqe/Wsf6CmgAaTSANMZ1NuDAVfkU+r3cLl5oKw39MhpDP5ktoPf9EfR33Tfora+ABpBGA0gTv788gycyz8vCC5Iu7loUVLfhQnUn6yugAaTRANIYqttc3DxAf15qQV8vfkBPdY/nzwr0j91r6NvNBvTWV0ADSKMBpGHvhSg6733W/MrRAWu+7oX+KxpAGusDmDCK4ECwCKF/ee1Bf5jfZ934tv0Efa16Av13iE921ldAA0ijAaQxxsU/6odj/NbPpnPoe18D1nx304Ge6jaZbfw9yvoKaABpNIA0v2NsVwyhlV0PAAAAAElFTkSuQmCC';
// 플레이어 아바타 컴포넌트 - 로딩 중에는 스티브, 로드 완료 시 실제 스킨 표시
const PlayerAvatar = ({ uuid, name }) => {
const [src, setSrc] = useState(STEVE_HEAD_BASE64);
useEffect(() => {
const img = new Image();
const realUrl = `https://mc-heads.net/avatar/${uuid}/48`;
img.onload = () => setSrc(realUrl);
img.src = realUrl;
}, [uuid]);
return (
<img
src={src}
alt={name}
className="w-12 h-12 rounded-lg"
/>
);
};
// 전체 플레이어 목록 페이지
const PlayersPage = ({ isMobile = false }) => {
const [players, setPlayers] = useState([]);
const [loading, setLoading] = useState(true);
const [serverOnline, setServerOnline] = useState(null);
const socketRef = useRef(null);
useEffect(() => {
// 소켓 연결
const socket = io('/', {
path: '/socket.io',
transports: ['websocket', 'polling']
});
socketRef.current = socket;
socket.on('status', (data) => {
setServerOnline(data.online);
if (!data.online) {
setLoading(false);
}
});
socket.on('players', (data) => {
setPlayers(data);
setLoading(false);
});
// 1초마다 갱신
const interval = setInterval(() => {
socket.emit('get_players');
}, 1000);
// 초기 요청
socket.emit('get_players');
return () => {
clearInterval(interval);
socket.disconnect();
};
}, []);
// 온라인 플레이어 먼저, 그 다음 이름순 정렬
const sortedPlayers = [...players].sort((a, b) => {
if (a.isOnline !== b.isOnline) return b.isOnline ? 1 : -1;
return a.name.localeCompare(b.name, 'ko');
});
const onlineCount = players.filter(p => p.isOnline).length;
// 서버 오프라인 상태 - 전체 화면 가운데 정렬
if (serverOnline === false) {
return (
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center p-8"
>
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-red-500/20 to-orange-500/20 border border-red-500/30 flex items-center justify-center">
<ServerOff className="w-10 h-10 text-red-400" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">서버 오프라인</h2>
<p className="text-gray-400">
마인크래프트 서버가 현재 오프라인 상태입니다.<br />
서버가 시작되면 플레이어 정보를 확인할 있습니다.
</p>
</motion.div>
</div>
);
}
return (
<div className="min-h-screen p-4 md:p-6 lg:p-8">
<div className="max-w-4xl mx-auto">
{/* 헤더 */}
<div className="mb-8">
<h1 className="text-3xl md:text-4xl font-bold text-white flex items-center gap-3">
<Users className="text-mc-diamond" />
플레이어
</h1>
<p className="text-gray-400 mt-2">
전체 플레이어 <span className="text-white font-bold">{players.length}</span>
<span className="mx-2"></span>
<span className="text-mc-green">온라인 {onlineCount}</span>
</p>
</div>
{/* 플레이어 목록 */}
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="w-12 h-12 border-4 border-mc-green/30 border-t-mc-green rounded-full animate-spin" />
</div>
) : sortedPlayers.length > 0 ? (
<div className="grid gap-3">
{sortedPlayers.map((player, index) => (
<motion.div
key={player.uuid}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.03 }}
>
<Link
to={`/player/${player.uuid}/stats`}
className="glow-card rounded-xl p-4 flex items-center gap-4 group hover:bg-white/5 transition-colors"
>
{/* 플레이어 아바타 */}
<div className="relative shrink-0">
<PlayerAvatar uuid={player.uuid} name={player.name} />
{/* 온라인 상태 표시 */}
<div className={`absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-mc-dark ${
player.isOnline ? 'bg-mc-green' : 'bg-gray-500'
}`} />
</div>
{/* 플레이어 정보 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-white font-bold truncate">{player.name}</h3>
<span className={`text-xs px-2 py-0.5 rounded-full ${
player.isOnline
? 'bg-mc-green/20 text-mc-green'
: 'bg-gray-500/20 text-gray-400'
}`}>
{player.isOnline ? '온라인' : '오프라인'}
</span>
</div>
<div className="flex items-center gap-1 text-gray-400 text-sm mt-1">
<Clock size={14} />
<span>{formatPlayTimeMs(player.totalPlayTimeMs)}</span>
</div>
</div>
{/* 통계 보기 안내 */}
<div className="text-gray-500 group-hover:text-mc-green transition-colors">
<span className="text-sm hidden md:block">통계 보기 </span>
</div>
</Link>
</motion.div>
))}
</div>
) : (
<div className="text-center py-20 text-gray-400">
<Users size={48} className="mx-auto mb-4 opacity-50" />
<p>등록된 플레이어가 없습니다.</p>
</div>
)}
</div>
</div>
);
};
export default PlayersPage;