refactor: 서버 관리 UI 개선 - 상단 카드 + 시작/종료 통합

- 선택된 서버가 상단 카드에 표시
- 시작/종료 버튼 하나로 통합
- 서버 선택 시 바로 카드에 반영
- localStorage에 선택 저장 (새로고침 후에도 유지)
- 실행 중이면 서버 변경 버튼으로 문구 변경
This commit is contained in:
caadiq 2025-12-29 14:05:04 +09:00
parent 2c15101ad8
commit 2a62dc6739

View file

@ -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 ${
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-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,20 +1697,22 @@ 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'
@ -1689,34 +1732,10 @@ export default function Admin({ isMobile = false }) {
{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>