refactor: 서버 관리 UI 개선 - 상단 카드 + 시작/종료 통합
- 선택된 서버가 상단 카드에 표시 - 시작/종료 버튼 하나로 통합 - 서버 선택 시 바로 카드에 반영 - localStorage에 선택 저장 (새로고침 후에도 유지) - 실행 중이면 서버 변경 버튼으로 문구 변경
This commit is contained in:
parent
2c15101ad8
commit
2a62dc6739
1 changed files with 89 additions and 70 deletions
|
|
@ -110,7 +110,10 @@ export default function Admin({ isMobile = false }) {
|
||||||
|
|
||||||
// 서버 관리 상태
|
// 서버 관리 상태
|
||||||
const [isServerListExpanded, setIsServerListExpanded] = useState(false); // 서버 목록 펼침 상태
|
const [isServerListExpanded, setIsServerListExpanded] = useState(false); // 서버 목록 펼침 상태
|
||||||
const [selectedServer, setSelectedServer] = useState(null); // 선택된 서버 경로
|
const [selectedServer, setSelectedServerState] = useState(() => {
|
||||||
|
// localStorage에서 선택된 서버 복원
|
||||||
|
return localStorage.getItem('selectedMinecraftServer') || null;
|
||||||
|
});
|
||||||
const [servers, setServers] = useState([]); // 서버 목록 (API에서 가져옴)
|
const [servers, setServers] = useState([]); // 서버 목록 (API에서 가져옴)
|
||||||
const [serverLoading, setServerLoading] = useState(false); // 서버 시작/종료 로딩
|
const [serverLoading, setServerLoading] = useState(false); // 서버 시작/종료 로딩
|
||||||
const [serverDialog, setServerDialog] = useState({ show: false, action: null, server: null }); // 시작/종료 확인 다이얼로그
|
const [serverDialog, setServerDialog] = useState({ show: false, action: null, server: null }); // 시작/종료 확인 다이얼로그
|
||||||
|
|
@ -860,6 +863,16 @@ export default function Admin({ isMobile = false }) {
|
||||||
}
|
}
|
||||||
}, []); // 의존성 없음 - 마운트 시에만 실행
|
}, []); // 의존성 없음 - 마운트 시에만 실행
|
||||||
|
|
||||||
|
// 선택된 서버 설정 (localStorage에도 저장)
|
||||||
|
const setSelectedServer = (serverPath) => {
|
||||||
|
setSelectedServerState(serverPath);
|
||||||
|
if (serverPath) {
|
||||||
|
localStorage.setItem('selectedMinecraftServer', serverPath);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('selectedMinecraftServer');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 서버 목록 fetch
|
// 서버 목록 fetch
|
||||||
const fetchServers = async () => {
|
const fetchServers = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1581,11 +1594,14 @@ export default function Admin({ isMobile = false }) {
|
||||||
{/* 서버 관리 */}
|
{/* 서버 관리 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const runningServer = servers.find(s => s.running);
|
const runningServer = servers.find(s => s.running);
|
||||||
const otherServers = servers.filter(s => !s.running);
|
const selectedServerData = servers.find(s => s.path === selectedServer);
|
||||||
|
// 실행 중인 서버가 있으면 그것을, 없으면 선택된 서버를 표시
|
||||||
|
const displayServer = runningServer || selectedServerData;
|
||||||
|
const isRunning = !!runningServer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||||
{/* 헤더 - 실행 중인 서버 표시 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-white font-medium flex items-center gap-2">
|
<h3 className="text-white font-medium flex items-center gap-2">
|
||||||
🖥️ 서버 관리
|
🖥️ 서버 관리
|
||||||
|
|
@ -1593,62 +1609,87 @@ export default function Admin({ isMobile = false }) {
|
||||||
<Loader2 size={14} className="animate-spin text-zinc-500" />
|
<Loader2 size={14} className="animate-spin text-zinc-500" />
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
{runningServer && (
|
{isRunning && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
<span className="text-emerald-400 text-xs font-medium">{runningServer.id}</span>
|
<span className="text-emerald-400 text-xs font-medium">실행 중</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 실행 중인 서버가 있으면 상태 카드 표시 */}
|
{/* 선택된/실행 중인 서버 카드 */}
|
||||||
{runningServer && (
|
{displayServer ? (
|
||||||
<div className="bg-emerald-500/10 border border-emerald-500/30 rounded-xl p-3 mb-3">
|
<div className={`rounded-xl p-3 mb-3 ${
|
||||||
|
isRunning
|
||||||
|
? 'bg-emerald-500/10 border border-emerald-500/30'
|
||||||
|
: 'bg-zinc-800/50 border border-zinc-700'
|
||||||
|
}`}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-emerald-500/20 rounded-lg flex items-center justify-center">
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||||
<div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse" />
|
isRunning ? 'bg-emerald-500/20' : 'bg-zinc-700'
|
||||||
|
}`}>
|
||||||
|
{isRunning ? (
|
||||||
|
<div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
|
) : (
|
||||||
|
<div className="w-3 h-3 rounded-full bg-zinc-500" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white text-sm font-medium">{runningServer.id}</p>
|
<p className="text-white text-sm font-medium">{displayServer.id}</p>
|
||||||
<p className="text-zinc-400 text-xs">
|
<p className="text-zinc-400 text-xs">
|
||||||
<span className={runningServer.loader === 'NeoForge' || runningServer.loader === 'Neoforge' ? 'text-orange-400' : 'text-blue-400'}>
|
<span className={displayServer.loader === 'NeoForge' || displayServer.loader === 'Neoforge' ? 'text-orange-400' : 'text-blue-400'}>
|
||||||
{runningServer.loader}
|
{displayServer.loader}
|
||||||
</span>
|
</span>
|
||||||
<span className="mx-1">•</span>
|
<span className="mx-1">•</span>
|
||||||
{runningServer.path.split('/')[1]}
|
{displayServer.path.split('/')[1]}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 text-xs font-medium rounded-lg bg-red-600/20 text-red-400 hover:bg-red-600/30 transition-colors disabled:opacity-50"
|
className={`px-4 py-2 text-xs font-medium rounded-lg transition-colors disabled:opacity-50 flex items-center gap-2 ${
|
||||||
|
isRunning
|
||||||
|
? 'bg-red-600/20 text-red-400 hover:bg-red-600/30'
|
||||||
|
: 'bg-emerald-600 text-white hover:bg-emerald-500'
|
||||||
|
}`}
|
||||||
disabled={serverLoading}
|
disabled={serverLoading}
|
||||||
onClick={() => setServerDialog({ show: true, action: 'stop', server: runningServer })}
|
onClick={() => setServerDialog({
|
||||||
|
show: true,
|
||||||
|
action: isRunning ? 'stop' : 'start',
|
||||||
|
server: displayServer
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
종료
|
{serverLoading ? (
|
||||||
|
<Loader2 size={12} className="animate-spin" />
|
||||||
|
) : null}
|
||||||
|
{isRunning ? '종료' : '시작'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-zinc-800/30 border border-zinc-700 rounded-xl p-3 mb-3">
|
||||||
|
<p className="text-zinc-500 text-sm text-center">서버를 선택해주세요</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 서버 선택 (접힘/펼침) - 실행 중이면 비활성화 */}
|
{/* 서버 선택 (접힘/펼침) - 실행 중이면 비활성화 */}
|
||||||
<div className={`border border-zinc-800 rounded-xl overflow-hidden ${runningServer ? 'opacity-50' : ''}`}>
|
<div className={`border border-zinc-800 rounded-xl overflow-hidden ${isRunning ? 'opacity-50' : ''}`}>
|
||||||
<button
|
<button
|
||||||
onClick={() => !runningServer && setIsServerListExpanded(!isServerListExpanded)}
|
onClick={() => !isRunning && setIsServerListExpanded(!isServerListExpanded)}
|
||||||
disabled={!!runningServer}
|
disabled={isRunning}
|
||||||
className="w-full flex items-center justify-between p-3 bg-zinc-800/50 hover:bg-zinc-800 transition-colors disabled:cursor-not-allowed"
|
className="w-full flex items-center justify-between p-3 bg-zinc-800/50 hover:bg-zinc-800 transition-colors disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span className="text-zinc-300 text-sm">
|
<span className="text-zinc-300 text-sm">
|
||||||
{runningServer ? '서버 실행 중에는 선택 불가' : '서버 선택'}
|
{isRunning ? '서버 실행 중에는 변경 불가' : '서버 변경'}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={16}
|
size={16}
|
||||||
className={`text-zinc-400 transition-transform ${isServerListExpanded && !runningServer ? 'rotate-180' : ''}`}
|
className={`text-zinc-400 transition-transform ${isServerListExpanded && !isRunning ? 'rotate-180' : ''}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isServerListExpanded && !runningServer && (
|
{isServerListExpanded && !isRunning && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ height: 0, opacity: 0 }}
|
initial={{ height: 0, opacity: 0 }}
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
|
@ -1656,67 +1697,45 @@ export default function Admin({ isMobile = false }) {
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="p-2 space-y-1 max-h-[200px] overflow-y-auto custom-scrollbar">
|
<div className="p-2 space-y-1 max-h-[200px] overflow-y-auto custom-scrollbar">
|
||||||
{otherServers.length === 0 ? (
|
{servers.length === 0 ? (
|
||||||
<p className="text-zinc-500 text-sm text-center py-2">서버가 없습니다</p>
|
<p className="text-zinc-500 text-sm text-center py-2">서버가 없습니다</p>
|
||||||
) : (
|
) : (
|
||||||
otherServers.map((server) => (
|
servers.map((server) => (
|
||||||
<button
|
<button
|
||||||
key={server.path}
|
key={server.path}
|
||||||
onClick={() => setSelectedServer(server.path)}
|
onClick={() => {
|
||||||
className={`w-full flex items-center justify-between p-3 rounded-lg transition-colors ${
|
setSelectedServer(server.path);
|
||||||
|
setIsServerListExpanded(false);
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-colors ${
|
||||||
selectedServer === server.path
|
selectedServer === server.path
|
||||||
? 'bg-zinc-700 border border-zinc-600'
|
? 'bg-zinc-700 border border-zinc-600'
|
||||||
: 'bg-zinc-800/30 hover:bg-zinc-800/50'
|
: 'bg-zinc-800/30 hover:bg-zinc-800/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
|
||||||
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
|
selectedServer === server.path
|
||||||
selectedServer === server.path
|
? 'border-emerald-500'
|
||||||
? 'border-emerald-500'
|
: 'border-zinc-600'
|
||||||
: 'border-zinc-600'
|
}`}>
|
||||||
}`}>
|
{selectedServer === server.path && (
|
||||||
{selectedServer === server.path && (
|
<div className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||||
<div className="w-2 h-2 rounded-full bg-emerald-500" />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="text-left">
|
||||||
<div className="text-left">
|
<p className="text-white text-sm">{server.id}</p>
|
||||||
<p className="text-white text-sm">{server.id}</p>
|
<p className="text-zinc-500 text-xs">
|
||||||
<p className="text-zinc-500 text-xs">
|
<span className={server.loader === 'NeoForge' || server.loader === 'Neoforge' ? 'text-orange-400' : 'text-blue-400'}>
|
||||||
<span className={server.loader === 'NeoForge' || server.loader === 'Neoforge' ? 'text-orange-400' : 'text-blue-400'}>
|
{server.loader}
|
||||||
{server.loader}
|
</span>
|
||||||
</span>
|
<span className="mx-1">•</span>
|
||||||
<span className="mx-1">•</span>
|
{server.path.split('/')[1]}
|
||||||
{server.path.split('/')[1]}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 시작 버튼 */}
|
|
||||||
{selectedServer && (
|
|
||||||
<div className="p-2 pt-0">
|
|
||||||
<button
|
|
||||||
className="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 disabled:bg-zinc-700 text-white text-sm font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
|
|
||||||
disabled={serverLoading}
|
|
||||||
onClick={() => {
|
|
||||||
const server = servers.find(s => s.path === selectedServer);
|
|
||||||
if (server) {
|
|
||||||
setServerDialog({ show: true, action: 'start', server });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{serverLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={14} className="animate-spin" />
|
|
||||||
처리 중...
|
|
||||||
</>
|
|
||||||
) : '선택한 서버 시작'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue