diff --git a/backend/server.js b/backend/server.js index 8ad7626..2d58392 100644 --- a/backend/server.js +++ b/backend/server.js @@ -123,6 +123,17 @@ async function refreshAndBroadcast() { const { cachedStatus, cachedPlayers } = await refreshData(); io.emit("status", cachedStatus); io.emit("players", cachedPlayers); + + // 월드 정보도 브로드캐스트 (시간/날씨 포함) + try { + const response = await fetch(`${MOD_API_URL}/worlds`); + if (response.ok) { + const data = await response.json(); + io.emit("worlds", data); + } + } catch (error) { + // 연결 오류 무시 + } } // 로그 캐시 (중복 브로드캐스트 방지) diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 82c9e2f..2e6f799 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -13,6 +13,7 @@ import { } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { io } from 'socket.io-client'; +import Tooltip from '../components/Tooltip'; // 더미 로그 데이터 const DUMMY_LOGS = [ @@ -28,15 +29,7 @@ const DUMMY_LOGS = [ // 더미 로그 파일 데이터 (삭제됨 - API 사용) -// 더미 게임규칙 데이터 -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(); @@ -79,7 +72,8 @@ export default function Admin({ isMobile = false }) { const [actionReason, setActionReason] = useState(''); // 설정 관련 상태 - const [gamerules, setGamerules] = useState(DUMMY_GAMERULES); + const [gameRules, setGameRules] = useState({}); // 소켓에서 가져온 게임 규칙 + const [gameRuleDescriptions, setGameRuleDescriptions] = useState({}); // 게임 규칙 설명 const [difficulty, setDifficulty] = useState('normal'); const [timeOfDay, setTimeOfDay] = useState('day'); const [weather, setWeather] = useState('clear'); @@ -402,12 +396,71 @@ export default function Admin({ isMobile = false }) { setPlayers(playersList); } }); + + // 서버 상태에서 게임 규칙 가져오기 + socket.on('status', (status) => { + if (status?.gameRules) { + setGameRules(status.gameRules); + } + if (status?.difficulty) { + // 난이도를 영문 ID로 변환 + const difficultyMap = { + '평화로움': 'peaceful', + '쉬움': 'easy', + '보통': 'normal', + '어려움': 'hard' + }; + setDifficulty(difficultyMap[status.difficulty] || 'normal'); + } + }); + + // 월드 정보에서 시간/날씨 가져오기 + socket.on('worlds', (data) => { + const worlds = data?.worlds || data; + // 오버월드 찾기 + const overworld = worlds?.find(w => w.dimension === 'minecraft:overworld'); + if (overworld) { + // 날씨 설정 (thunderstorm -> thunder로 변환) + if (overworld.weather?.type) { + const weatherMap = { + 'clear': 'clear', + 'rain': 'rain', + 'thunderstorm': 'thunder' + }; + setWeather(weatherMap[overworld.weather.type] || 'clear'); + } + + // 시간 설정 (틱 기반으로 대략적인 시간대 결정) + if (overworld.time?.dayTime !== undefined) { + const dayTime = overworld.time.dayTime; + // 0-6000: 아침/낮, 6000-12000: 낮/오후, 12000-24000: 밤 + if (dayTime >= 0 && dayTime < 6000) { + setTimeOfDay('day'); + } else if (dayTime >= 6000 && dayTime < 12000) { + setTimeOfDay('noon'); + } else { + setTimeOfDay('night'); + } + } + } + }); + + // 월드 정보 요청 + socket.emit('get_worlds'); return () => { socket.disconnect(); }; }, [isAdmin]); + // 게임 규칙 설명 데이터 로드 + useEffect(() => { + fetch('/api/gamerules') + .then(res => res.json()) + .then(data => setGameRuleDescriptions(data)) + .catch(err => console.error('게임 규칙 설명 로드 실패:', err)); + }, []); + // 명령어 실행 (실제 API 호출) const handleCommand = async () => { if (!command.trim()) return; @@ -488,12 +541,30 @@ export default function Admin({ isMobile = false }) { setActionReason(''); }; - // 게임규칙 토글 - const toggleGamerule = (name) => { - setGamerules(prev => prev.map(rule => - rule.name === name ? { ...rule, value: !rule.value } : rule - )); - setToast('게임규칙이 변경되었습니다.'); + // 게임규칙 토글 (서버에 gamerule 명령어 전송) + const toggleGamerule = async (name) => { + const currentValue = gameRules[name]; + const newValue = !currentValue; + + // 낙관적 UI 업데이트 + setGameRules(prev => ({ ...prev, [name]: newValue })); + + try { + const token = localStorage.getItem('token'); + await fetch('/api/admin/command', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ command: `gamerule ${name} ${newValue}` }) + }); + setToast(`${name}: ${newValue ? 'true' : 'false'}`); + } catch (error) { + // 실패 시 롤백 + setGameRules(prev => ({ ...prev, [name]: currentValue })); + setToast('게임규칙 변경 실패'); + } }; // 로그 색상 (hex 값 반환) @@ -1019,23 +1090,43 @@ export default function Admin({ isMobile = false }) { > {/* 게임규칙 */}