feat: 관리자 페이지 탭 UI 구현
- 탭 UI (콘솔/플레이어/설정) - 콘솔: 로그 영역 + 명령어 입력 + 로그 파일 목록 - 플레이어: 전신 아바타 + 필터 + 킥/밴/OP - 설정: 게임규칙 토글 + 난이도 + 시간 + 날씨 - 더미 데이터로 UI 미리보기
This commit is contained in:
parent
ba907ec8eb
commit
1bb52f58d5
1 changed files with 526 additions and 98 deletions
|
|
@ -1,19 +1,84 @@
|
|||
/**
|
||||
* 관리자 페이지
|
||||
* - 탭 UI: 콘솔 / 플레이어 / 설정
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useNavigate, useLocation, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Shield, ArrowLeft, Settings, Server, Users, Loader2, User, Terminal, MessageSquare } from 'lucide-react';
|
||||
import {
|
||||
Shield, ArrowLeft, Loader2, Terminal, Users, Settings,
|
||||
Send, Ban, UserX, Crown, Sun, Moon, Cloud, CloudRain, CloudLightning,
|
||||
ChevronDown, FileText, Download, Trash2, Check
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// 더미 로그 데이터
|
||||
const DUMMY_LOGS = [
|
||||
{ time: '15:01:23', type: 'info', message: '[Server] Starting minecraft server version 1.21.1' },
|
||||
{ time: '15:01:24', type: 'info', message: '[Server] Loading properties' },
|
||||
{ time: '15:01:25', type: 'info', message: '[Server] Preparing level "world"' },
|
||||
{ time: '15:01:28', type: 'info', message: '[Server] Done (3.245s)! For help, type "help"' },
|
||||
{ time: '15:05:12', type: 'info', message: '[Server] 비머[/127.0.0.1:54321] logged in' },
|
||||
{ time: '15:05:15', type: 'info', message: '[Server] 비머 joined the game' },
|
||||
{ time: '15:10:30', type: 'warning', message: '[Server] Can\'t keep up! Is the server overloaded?' },
|
||||
{ time: '15:15:00', type: 'info', message: '[Server] 비머부캐 joined the game' },
|
||||
];
|
||||
|
||||
// 더미 로그 파일 데이터
|
||||
const DUMMY_LOG_FILES = [
|
||||
{ name: '2024-12-22.log', size: '2.4 MB', date: '2024-12-22' },
|
||||
{ name: '2024-12-21.log', size: '1.8 MB', date: '2024-12-21' },
|
||||
{ name: '2024-12-20.log', size: '3.1 MB', date: '2024-12-20' },
|
||||
];
|
||||
|
||||
// 더미 플레이어 데이터
|
||||
const DUMMY_PLAYERS = [
|
||||
{ uuid: '1234-5678-9012-3456', name: '비머', isOnline: true, isOp: true },
|
||||
{ uuid: '2345-6789-0123-4567', name: '비머부캐', isOnline: true, isOp: false },
|
||||
{ uuid: '3456-7890-1234-5678', name: 'Steve', isOnline: false, isOp: false },
|
||||
{ uuid: '4567-8901-2345-6789', name: 'Alex', isOnline: false, isOp: false },
|
||||
];
|
||||
|
||||
// 더미 게임규칙 데이터
|
||||
const DUMMY_GAMERULES = [
|
||||
{ name: 'keepInventory', value: false, label: '인벤토리 유지' },
|
||||
{ name: 'doDaylightCycle', value: true, label: '낮/밤 주기' },
|
||||
{ name: 'doMobSpawning', value: true, label: '몹 스폰' },
|
||||
{ name: 'doFireTick', value: true, label: '불 번짐' },
|
||||
{ name: 'mobGriefing', value: true, label: '몹 그리핑' },
|
||||
{ name: 'pvp', value: true, label: 'PvP' },
|
||||
];
|
||||
|
||||
export default function Admin({ isMobile = false }) {
|
||||
const { isLoggedIn, isAdmin, user, loading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [toast, setToast] = useState(null);
|
||||
|
||||
// 탭 상태
|
||||
const [activeTab, setActiveTab] = useState('console');
|
||||
|
||||
// 콘솔 관련 상태
|
||||
const [logs, setLogs] = useState(DUMMY_LOGS);
|
||||
const [command, setCommand] = useState('');
|
||||
const [logFiles] = useState(DUMMY_LOG_FILES);
|
||||
const logEndRef = useRef(null);
|
||||
|
||||
// 플레이어 관련 상태
|
||||
const [players, setPlayers] = useState(DUMMY_PLAYERS);
|
||||
const [playerFilter, setPlayerFilter] = useState('all'); // all, online, offline, banned
|
||||
const [selectedPlayer, setSelectedPlayer] = useState(null);
|
||||
const [showPlayerDialog, setShowPlayerDialog] = useState(false);
|
||||
const [dialogAction, setDialogAction] = useState(null); // kick, ban, op
|
||||
const [actionReason, setActionReason] = useState('');
|
||||
|
||||
// 설정 관련 상태
|
||||
const [gamerules, setGamerules] = useState(DUMMY_GAMERULES);
|
||||
const [difficulty, setDifficulty] = useState('normal');
|
||||
const [timeOfDay, setTimeOfDay] = useState('day');
|
||||
const [weather, setWeather] = useState('clear');
|
||||
|
||||
// 권한 확인
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
|
|
@ -21,9 +86,7 @@ export default function Admin({ isMobile = false }) {
|
|||
navigate('/login', { state: { from: location.pathname } });
|
||||
} else if (!isAdmin) {
|
||||
setToast('관리자 권한이 필요합니다.');
|
||||
setTimeout(() => {
|
||||
navigate('/');
|
||||
}, 1500);
|
||||
setTimeout(() => navigate('/'), 1500);
|
||||
}
|
||||
}
|
||||
}, [isLoggedIn, isAdmin, loading, navigate, location.pathname]);
|
||||
|
|
@ -36,6 +99,78 @@ export default function Admin({ isMobile = false }) {
|
|||
}
|
||||
}, [toast]);
|
||||
|
||||
// 로그 스크롤
|
||||
useEffect(() => {
|
||||
logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [logs]);
|
||||
|
||||
// 명령어 실행 (더미)
|
||||
const handleCommand = () => {
|
||||
if (!command.trim()) return;
|
||||
|
||||
const newLogs = [
|
||||
{ time: new Date().toLocaleTimeString('ko-KR', { hour12: false }), type: 'command', message: `> ${command}` },
|
||||
{ time: new Date().toLocaleTimeString('ko-KR', { hour12: false }), type: 'info', message: `[Server] 명령어 실행됨: ${command}` }
|
||||
];
|
||||
|
||||
setLogs(prev => [...prev, ...newLogs]);
|
||||
setCommand('');
|
||||
setToast('명령어가 실행되었습니다.');
|
||||
};
|
||||
|
||||
// 플레이어 액션 핸들러
|
||||
const handlePlayerAction = () => {
|
||||
if (!selectedPlayer || !dialogAction) return;
|
||||
|
||||
let message = '';
|
||||
switch (dialogAction) {
|
||||
case 'kick':
|
||||
message = `${selectedPlayer.name}님을 추방했습니다.`;
|
||||
break;
|
||||
case 'ban':
|
||||
message = `${selectedPlayer.name}님을 차단했습니다.`;
|
||||
break;
|
||||
case 'op':
|
||||
const isOp = players.find(p => p.uuid === selectedPlayer.uuid)?.isOp;
|
||||
setPlayers(prev => prev.map(p =>
|
||||
p.uuid === selectedPlayer.uuid ? { ...p, isOp: !isOp } : p
|
||||
));
|
||||
message = isOp ? `${selectedPlayer.name}님의 OP를 해제했습니다.` : `${selectedPlayer.name}님에게 OP를 부여했습니다.`;
|
||||
break;
|
||||
}
|
||||
|
||||
setShowPlayerDialog(false);
|
||||
setSelectedPlayer(null);
|
||||
setDialogAction(null);
|
||||
setActionReason('');
|
||||
setToast(message);
|
||||
};
|
||||
|
||||
// 게임규칙 토글
|
||||
const toggleGamerule = (name) => {
|
||||
setGamerules(prev => prev.map(rule =>
|
||||
rule.name === name ? { ...rule, value: !rule.value } : rule
|
||||
));
|
||||
setToast('게임규칙이 변경되었습니다.');
|
||||
};
|
||||
|
||||
// 로그 색상
|
||||
const getLogColor = (type) => {
|
||||
switch (type) {
|
||||
case 'error': return 'text-red-400';
|
||||
case 'warning': return 'text-yellow-400';
|
||||
case 'command': return 'text-mc-green';
|
||||
default: return 'text-zinc-300';
|
||||
}
|
||||
};
|
||||
|
||||
// 필터된 플레이어
|
||||
const filteredPlayers = players.filter(p => {
|
||||
if (playerFilter === 'online') return p.isOnline;
|
||||
if (playerFilter === 'offline') return !p.isOnline;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
|
|
@ -47,7 +182,6 @@ export default function Admin({ isMobile = false }) {
|
|||
if (!isLoggedIn || !isAdmin) {
|
||||
return (
|
||||
<>
|
||||
{/* 토스트 알림 */}
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
<motion.div
|
||||
|
|
@ -67,27 +201,44 @@ export default function Admin({ isMobile = false }) {
|
|||
);
|
||||
}
|
||||
|
||||
// 탭 설정
|
||||
const tabs = [
|
||||
{ id: 'console', label: '콘솔', icon: Terminal },
|
||||
{ id: 'players', label: '플레이어', icon: Users },
|
||||
{ id: 'settings', label: '설정', icon: Settings },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="pb-8">
|
||||
{/* 토스트 */}
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 50 }}
|
||||
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-[200] bg-mc-green/90 backdrop-blur-sm text-white px-6 py-3 rounded-xl text-center font-medium shadow-lg"
|
||||
>
|
||||
{toast}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 모바일용 헤더 */}
|
||||
{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"
|
||||
>
|
||||
<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>
|
||||
</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`}>
|
||||
<main className={`max-w-4xl mx-auto px-4 sm:px-6 ${isMobile ? 'py-4' : 'py-8'}`}>
|
||||
{/* 데스크탑용 타이틀 */}
|
||||
{!isMobile && (
|
||||
<div className="mb-6">
|
||||
|
|
@ -96,100 +247,377 @@ export default function Admin({ isMobile = false }) {
|
|||
</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 gap-1 p-1 bg-zinc-900 rounded-xl mb-6">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-lg font-medium transition-all ${
|
||||
activeTab === tab.id
|
||||
? 'bg-zinc-800 text-white'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/50'
|
||||
}`}
|
||||
>
|
||||
<tab.icon size={18} />
|
||||
<span className={isMobile ? 'hidden sm:inline' : ''}>{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{/* 탭 콘텐츠 */}
|
||||
<AnimatePresence mode="wait">
|
||||
{/* 콘솔 탭 */}
|
||||
{activeTab === 'console' && (
|
||||
<motion.div
|
||||
key="console"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* 로그 영역 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden">
|
||||
<div className="h-80 overflow-y-auto bg-zinc-950 p-4 font-mono text-sm">
|
||||
{logs.map((log, index) => (
|
||||
<div key={index} className={`${getLogColor(log.type)} leading-relaxed`}>
|
||||
<span className="text-zinc-600">[{log.time}]</span> {log.message}
|
||||
</div>
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-3">
|
||||
<div>
|
||||
<p className="text-sm text-zinc-500">닉네임</p>
|
||||
<p className="text-lg font-medium text-white">{user?.name || '-'}</p>
|
||||
{/* 명령어 입력 */}
|
||||
<div className="p-4 border-t border-zinc-800 flex gap-2">
|
||||
<div className="flex-1 flex items-center bg-zinc-800 rounded-lg px-3">
|
||||
<span className="text-mc-green mr-2">{'>'}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCommand()}
|
||||
placeholder="명령어 입력..."
|
||||
className="flex-1 bg-transparent py-3 text-white placeholder-zinc-500 focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCommand}
|
||||
disabled={!command.trim()}
|
||||
className="px-4 py-3 bg-mc-green hover:bg-mc-green/80 disabled:bg-zinc-700 disabled:text-zinc-500 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-zinc-500">이메일</p>
|
||||
<p className="text-white">{user?.email}</p>
|
||||
|
||||
{/* 로그 파일 목록 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||
<FileText size={18} className="text-zinc-400" />
|
||||
로그 파일
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{logFiles.map((file, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-zinc-800/50 rounded-xl">
|
||||
<div>
|
||||
<p className="text-white text-sm">{file.name}</p>
|
||||
<p className="text-xs text-zinc-500">{file.size}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-700 rounded-lg transition-colors">
|
||||
<Download size={16} />
|
||||
</button>
|
||||
<button className="p-2 text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-lg transition-colors">
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</motion.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">
|
||||
<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>
|
||||
{/* 플레이어 탭 */}
|
||||
{activeTab === 'players' && (
|
||||
<motion.div
|
||||
key="players"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* 필터 */}
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ id: 'all', label: '전체' },
|
||||
{ id: 'online', label: '온라인' },
|
||||
{ id: 'offline', label: '오프라인' },
|
||||
].map(filter => (
|
||||
<button
|
||||
key={filter.id}
|
||||
onClick={() => setPlayerFilter(filter.id)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
playerFilter === filter.id
|
||||
? 'bg-mc-green text-white'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400 mb-3">
|
||||
마인크래프트 서버에 명령어를 전송합니다.
|
||||
</p>
|
||||
<div className="text-zinc-500 text-sm">
|
||||
추후 업데이트 예정
|
||||
|
||||
{/* 플레이어 그리드 */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||
{filteredPlayers.map((player) => (
|
||||
<div
|
||||
key={player.uuid}
|
||||
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 text-center relative group"
|
||||
>
|
||||
{/* OP 뱃지 */}
|
||||
{player.isOp && (
|
||||
<div className="absolute top-2 right-2 bg-yellow-500 p-1 rounded-lg">
|
||||
<Crown size={12} className="text-black" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 온/오프라인 표시 */}
|
||||
<div className={`absolute top-2 left-2 w-3 h-3 rounded-full ${player.isOnline ? 'bg-green-500' : 'bg-zinc-600'}`} />
|
||||
|
||||
{/* 전신 아바타 */}
|
||||
<img
|
||||
src={`https://mc-heads.net/body/${player.uuid}/100`}
|
||||
alt={player.name}
|
||||
className="w-16 h-32 mx-auto mb-3 drop-shadow-lg"
|
||||
/>
|
||||
|
||||
<p className="text-white font-medium mb-3">{player.name}</p>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex justify-center gap-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPlayer(player);
|
||||
setDialogAction('op');
|
||||
setShowPlayerDialog(true);
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
player.isOp
|
||||
? 'bg-yellow-500/20 text-yellow-500 hover:bg-yellow-500/30'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:text-yellow-500'
|
||||
}`}
|
||||
title="OP"
|
||||
>
|
||||
<Crown size={16} />
|
||||
</button>
|
||||
{player.isOnline && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPlayer(player);
|
||||
setDialogAction('kick');
|
||||
setShowPlayerDialog(true);
|
||||
}}
|
||||
className="p-2 bg-zinc-800 text-zinc-400 hover:text-orange-500 rounded-lg transition-colors"
|
||||
title="킥"
|
||||
>
|
||||
<UserX size={16} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPlayer(player);
|
||||
setDialogAction('ban');
|
||||
setShowPlayerDialog(true);
|
||||
}}
|
||||
className="p-2 bg-zinc-800 text-zinc-400 hover:text-red-500 rounded-lg transition-colors"
|
||||
title="밴"
|
||||
>
|
||||
<Ban size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.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>
|
||||
{/* 설정 탭 */}
|
||||
{activeTab === 'settings' && (
|
||||
<motion.div
|
||||
key="settings"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* 게임규칙 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||
<h3 className="text-white font-medium mb-4">🎮 게임규칙</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{gamerules.map(rule => (
|
||||
<button
|
||||
key={rule.name}
|
||||
onClick={() => toggleGamerule(rule.name)}
|
||||
className={`flex items-center justify-between p-3 rounded-xl transition-colors border ${
|
||||
rule.value ? 'bg-mc-green/20 border-mc-green/30' : 'bg-zinc-800/50 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<span className="text-white text-sm">{rule.label}</span>
|
||||
<div className={`w-10 h-6 rounded-full relative transition-colors ${rule.value ? 'bg-mc-green' : 'bg-zinc-600'}`}>
|
||||
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${rule.value ? 'left-5' : 'left-1'}`} />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400 mb-3">
|
||||
게임 내 모든 플레이어에게 공지를 전송합니다.
|
||||
</p>
|
||||
<div className="text-zinc-500 text-sm">
|
||||
추후 업데이트 예정
|
||||
|
||||
{/* 난이도 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||
<h3 className="text-white font-medium mb-4">⚔️ 난이도</h3>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{[
|
||||
{ id: 'peaceful', label: '평화로움' },
|
||||
{ id: 'easy', label: '쉬움' },
|
||||
{ id: 'normal', label: '보통' },
|
||||
{ id: 'hard', label: '어려움' },
|
||||
].map(d => (
|
||||
<button
|
||||
key={d.id}
|
||||
onClick={() => { setDifficulty(d.id); setToast('난이도가 변경되었습니다.'); }}
|
||||
className={`flex-1 min-w-[80px] py-3 px-4 rounded-xl font-medium transition-colors ${
|
||||
difficulty === d.id
|
||||
? 'bg-mc-green text-white'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{d.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
{/* 시간 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||
<h3 className="text-white font-medium mb-4">🕐 시간</h3>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{[
|
||||
{ id: 'day', label: '아침', icon: Sun },
|
||||
{ id: 'noon', label: '낮', icon: Sun },
|
||||
{ id: 'night', label: '밤', icon: Moon },
|
||||
].map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => { setTimeOfDay(t.id); setToast('시간이 변경되었습니다.'); }}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-medium transition-colors ${
|
||||
timeOfDay === t.id
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<t.icon size={18} />
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-zinc-400">
|
||||
접속 중인 플레이어 목록 및 관리 기능
|
||||
</p>
|
||||
<div className="mt-4 text-zinc-500 text-sm">
|
||||
추후 업데이트 예정
|
||||
</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">
|
||||
<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>
|
||||
</section>
|
||||
{/* 날씨 */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||
<h3 className="text-white font-medium mb-4">🌤️ 날씨</h3>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{[
|
||||
{ id: 'clear', label: '맑음', icon: Sun },
|
||||
{ id: 'rain', label: '비', icon: CloudRain },
|
||||
{ id: 'thunder', label: '천둥', icon: CloudLightning },
|
||||
].map(w => (
|
||||
<button
|
||||
key={w.id}
|
||||
onClick={() => { setWeather(w.id); setToast('날씨가 변경되었습니다.'); }}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-medium transition-colors ${
|
||||
weather === w.id
|
||||
? 'bg-cyan-500 text-white'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<w.icon size={18} />
|
||||
{w.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
|
||||
{/* 플레이어 액션 다이얼로그 */}
|
||||
<AnimatePresence>
|
||||
{showPlayerDialog && selectedPlayer && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4"
|
||||
onClick={() => setShowPlayerDialog(false)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="bg-zinc-900 border border-zinc-700 rounded-2xl p-6 w-full max-w-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
{dialogAction === 'kick' && <><UserX size={20} className="text-orange-500" /> 플레이어 킥</>}
|
||||
{dialogAction === 'ban' && <><Ban size={20} className="text-red-500" /> 플레이어 밴</>}
|
||||
{dialogAction === 'op' && <><Crown size={20} className="text-yellow-500" /> OP {players.find(p => p.uuid === selectedPlayer.uuid)?.isOp ? '해제' : '부여'}</>}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<img
|
||||
src={`https://mc-heads.net/body/${selectedPlayer.uuid}/60`}
|
||||
alt={selectedPlayer.name}
|
||||
className="w-10 h-20"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-white font-medium">{selectedPlayer.name}</p>
|
||||
<p className="text-xs text-zinc-500">{selectedPlayer.uuid}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(dialogAction === 'kick' || dialogAction === 'ban') && (
|
||||
<input
|
||||
type="text"
|
||||
value={actionReason}
|
||||
onChange={(e) => setActionReason(e.target.value)}
|
||||
placeholder="사유 (선택)"
|
||||
className="w-full bg-zinc-800 rounded-xl p-3 text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-mc-green/50 mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowPlayerDialog(false)}
|
||||
className="flex-1 py-3 bg-zinc-800 hover:bg-zinc-700 text-white rounded-xl transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePlayerAction}
|
||||
className={`flex-1 py-3 font-medium rounded-xl transition-colors ${
|
||||
dialogAction === 'kick' ? 'bg-orange-500 hover:bg-orange-600 text-white' :
|
||||
dialogAction === 'ban' ? 'bg-red-500 hover:bg-red-600 text-white' :
|
||||
'bg-yellow-500 hover:bg-yellow-600 text-black'
|
||||
}`}
|
||||
>
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue