diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx
index b6279a8..306b895 100644
--- a/frontend/src/pages/Admin.jsx
+++ b/frontend/src/pages/Admin.jsx
@@ -1,18 +1,83 @@
/**
* 관리자 페이지
+ * - 탭 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(() => {
@@ -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 (
@@ -47,7 +182,6 @@ export default function Admin({ isMobile = false }) {
if (!isLoggedIn || !isAdmin) {
return (
<>
- {/* 토스트 알림 */}
{toast && (
+ {/* 토스트 */}
+
+ {toast && (
+
+ {toast}
+
+ )}
+
+
{/* 모바일용 헤더 */}
{isMobile && (
)}
-
+
{/* 데스크탑용 타이틀 */}
{!isMobile && (
@@ -96,100 +247,377 @@ export default function Admin({ isMobile = false }) {
)}
- {/* 관리자 정보 카드 */}
-
-
-
- 관리자 정보
-
-
-
-
-
-
-
-
-
-
닉네임
-
{user?.name || '-'}
-
-
-
-
-
+ {/* 탭 네비게이션 */}
+
+ {tabs.map(tab => (
+
+ ))}
+
- {/* 서버 관리 섹션 */}
-
-
-
- 서버 관리
-
-
-
- {/* 서버 명령어 */}
-
-
-
-
서버 명령어
+ {/* 탭 콘텐츠 */}
+
+ {/* 콘솔 탭 */}
+ {activeTab === 'console' && (
+
+ {/* 로그 영역 */}
+
+
+ {logs.map((log, index) => (
+
+ [{log.time}] {log.message}
+
+ ))}
+
+
+
+ {/* 명령어 입력 */}
+
+
+ {'>'}
+ 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"
+ />
+
+
+
-
- 마인크래프트 서버에 명령어를 전송합니다.
-
-
- 추후 업데이트 예정
-
-
- {/* 공지 전송 */}
-
-
-
-
공지 전송
+ {/* 로그 파일 목록 */}
+
+
+
+ 로그 파일
+
+
+ {logFiles.map((file, index) => (
+
+
+
{file.name}
+
{file.size}
+
+
+
+
+
+
+ ))}
+
-
- 게임 내 모든 플레이어에게 공지를 전송합니다.
-
-
- 추후 업데이트 예정
+
+ )}
+
+ {/* 플레이어 탭 */}
+ {activeTab === 'players' && (
+
+ {/* 필터 */}
+
+ {[
+ { id: 'all', label: '전체' },
+ { id: 'online', label: '온라인' },
+ { id: 'offline', label: '오프라인' },
+ ].map(filter => (
+
+ ))}
-
-
-
- {/* 플레이어 관리 섹션 */}
-
-
-
- 플레이어 관리
-
-
-
- 접속 중인 플레이어 목록 및 관리 기능
-
-
- 추후 업데이트 예정
-
-
+ {/* 플레이어 그리드 */}
+
+ {filteredPlayers.map((player) => (
+
+ {/* OP 뱃지 */}
+ {player.isOp && (
+
+
+
+ )}
+
+ {/* 온/오프라인 표시 */}
+
+
+ {/* 전신 아바타 */}
+

+
+
{player.name}
+
+ {/* 액션 버튼 */}
+
+
+ {player.isOnline && (
+
+ )}
+
+
+
+ ))}
+
+
+ )}
- {/* 설정 섹션 */}
-
-
-
- 설정
-
-
-
- 대시보드 설정 및 구성 관리
-
-
- 추후 업데이트 예정
-
-
+ {/* 설정 탭 */}
+ {activeTab === 'settings' && (
+
+ {/* 게임규칙 */}
+
+
🎮 게임규칙
+
+ {gamerules.map(rule => (
+
+ ))}
+
+
+
+ {/* 난이도 */}
+
+
⚔️ 난이도
+
+ {[
+ { id: 'peaceful', label: '평화로움' },
+ { id: 'easy', label: '쉬움' },
+ { id: 'normal', label: '보통' },
+ { id: 'hard', label: '어려움' },
+ ].map(d => (
+
+ ))}
+
+
+
+ {/* 시간 */}
+
+
🕐 시간
+
+ {[
+ { id: 'day', label: '아침', icon: Sun },
+ { id: 'noon', label: '낮', icon: Sun },
+ { id: 'night', label: '밤', icon: Moon },
+ ].map(t => (
+
+ ))}
+
+
+
+ {/* 날씨 */}
+
+
🌤️ 날씨
+
+ {[
+ { id: 'clear', label: '맑음', icon: Sun },
+ { id: 'rain', label: '비', icon: CloudRain },
+ { id: 'thunder', label: '천둥', icon: CloudLightning },
+ ].map(w => (
+
+ ))}
+
+
+
+ )}
+
+
+ {/* 플레이어 액션 다이얼로그 */}
+
+ {showPlayerDialog && selectedPlayer && (
+ setShowPlayerDialog(false)}
+ >
+ e.stopPropagation()}
+ >
+
+ {dialogAction === 'kick' && <> 플레이어 킥>}
+ {dialogAction === 'ban' && <> 플레이어 밴>}
+ {dialogAction === 'op' && <> OP {players.find(p => p.uuid === selectedPlayer.uuid)?.isOp ? '해제' : '부여'}>}
+
+
+
+

+
+
{selectedPlayer.name}
+
{selectedPlayer.uuid}
+
+
+
+ {(dialogAction === 'kick' || dialogAction === 'ban') && (
+ 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"
+ />
+ )}
+
+
+
+
+
+
+
+ )}
+
);
}
-