feat: 프론트엔드 백엔드 API 연동

- Modpack.jsx: API fetch, 더미 데이터 제거, 다운로드 링크 연결
- Admin.jsx: 모드팩 목록 fetch, 업로드/수정/삭제 API 연동
- 파일 선택 UI, 로딩 상태, 에러 처리 구현
This commit is contained in:
caadiq 2025-12-23 16:42:43 +09:00
parent 83820c3951
commit 778a9597bd
2 changed files with 191 additions and 107 deletions

View file

@ -126,13 +126,11 @@ export default function Admin({ isMobile = false }) {
const [showModpackDialog, setShowModpackDialog] = useState(false); const [showModpackDialog, setShowModpackDialog] = useState(false);
const [modpackDialogMode, setModpackDialogMode] = useState('upload'); // 'upload' | 'edit' const [modpackDialogMode, setModpackDialogMode] = useState('upload'); // 'upload' | 'edit'
const [editingModpack, setEditingModpack] = useState(null); const [editingModpack, setEditingModpack] = useState(null);
const [modpackForm, setModpackForm] = useState({ version: '', changelog: '' }); const [modpackForm, setModpackForm] = useState({ changelog: '' });
const [modpacks, setModpacks] = useState([ const [modpackFile, setModpackFile] = useState(null); //
{ id: 1, version: '1.2.0', name: '테스트 서버 모드팩', date: '2024-12-20', size: '15.0 MB' }, const [modpacks, setModpacks] = useState([]);
{ id: 2, version: '1.1.0', name: '테스트 서버 모드팩', date: '2024-12-15', size: '12.0 MB' },
{ id: 3, version: '1.0.0', name: '테스트 서버 모드팩', date: '2024-12-01', size: '8.0 MB' },
]);
const [modpackDeleteTarget, setModpackDeleteTarget] = useState(null); // const [modpackDeleteTarget, setModpackDeleteTarget] = useState(null); //
const [modpackLoading, setModpackLoading] = useState(false); // /
// //
useEffect(() => { useEffect(() => {
@ -154,6 +152,121 @@ export default function Admin({ isMobile = false }) {
} }
}, [toast]); }, [toast]);
// fetch
const fetchModpacks = useCallback(async () => {
try {
const res = await fetch('/api/modpacks');
const data = await res.json();
// API UI
const formatted = data.map(mp => ({
id: mp.id,
version: mp.version,
name: mp.name,
date: new Date(mp.created_at).toISOString().split('T')[0],
size: (mp.file_size / (1024 * 1024)).toFixed(1) + ' MB',
}));
setModpacks(formatted);
} catch (error) {
console.error('모드팩 목록 로드 실패:', error);
}
}, []);
//
useEffect(() => {
if (activeTab === 'modpack') {
fetchModpacks();
}
}, [activeTab, fetchModpacks]);
//
const handleModpackUpload = async () => {
if (!modpackFile) {
setToast('.mrpack 파일을 선택해주세요.');
return;
}
setModpackLoading(true);
try {
const token = localStorage.getItem('token');
const formData = new FormData();
formData.append('file', modpackFile);
formData.append('changelog', modpackForm.changelog);
const res = await fetch('/api/admin/modpacks', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData,
});
const result = await res.json();
if (result.success) {
setToast(`${result.name} v${result.version} 업로드 완료!`);
setShowModpackDialog(false);
setModpackFile(null);
setModpackForm({ changelog: '' });
fetchModpacks();
} else {
setToast(result.error || '업로드 실패');
}
} catch (error) {
setToast('업로드 실패: ' + error.message);
} finally {
setModpackLoading(false);
}
};
// ( )
const handleModpackEdit = async () => {
if (!editingModpack) return;
setModpackLoading(true);
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/admin/modpacks/${editingModpack.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ changelog: modpackForm.changelog }),
});
const result = await res.json();
if (result.success) {
setToast('변경 로그가 수정되었습니다.');
setShowModpackDialog(false);
fetchModpacks();
} else {
setToast(result.error || '수정 실패');
}
} catch (error) {
setToast('수정 실패: ' + error.message);
} finally {
setModpackLoading(false);
}
};
//
const handleModpackDelete = async () => {
if (!modpackDeleteTarget) return;
setModpackLoading(true);
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/admin/modpacks/${modpackDeleteTarget.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` },
});
const result = await res.json();
if (result.success) {
setToast('모드팩이 삭제되었습니다.');
setModpackDeleteTarget(null);
fetchModpacks();
} else {
setToast(result.error || '삭제 실패');
}
} catch (error) {
setToast('삭제 실패: ' + error.message);
} finally {
setModpackLoading(false);
}
};
// fetch ( ) // fetch ( )
const fetchPlayers = useCallback(async () => { const fetchPlayers = useCallback(async () => {
try { try {
@ -1948,11 +2061,23 @@ export default function Admin({ isMobile = false }) {
{modpackDialogMode === 'upload' && ( {modpackDialogMode === 'upload' && (
<div className="mb-4"> <div className="mb-4">
<label className="block text-zinc-400 text-sm mb-2">파일 선택 (.mrpack)</label> <label className="block text-zinc-400 text-sm mb-2">파일 선택 (.mrpack)</label>
<div className="border-2 border-dashed border-zinc-700 rounded-xl p-6 text-center hover:border-mc-green/50 transition-colors cursor-pointer"> <label className="border-2 border-dashed border-zinc-700 rounded-xl p-6 text-center hover:border-mc-green/50 transition-colors cursor-pointer block">
<input
type="file"
accept=".mrpack"
className="hidden"
onChange={(e) => setModpackFile(e.target.files?.[0] || null)}
/>
<Upload className="mx-auto text-zinc-500 mb-2" size={24} /> <Upload className="mx-auto text-zinc-500 mb-2" size={24} />
{modpackFile ? (
<p className="text-mc-green text-sm">{modpackFile.name}</p>
) : (
<>
<p className="text-zinc-400 text-sm">클릭하여 파일 선택</p> <p className="text-zinc-400 text-sm">클릭하여 파일 선택</p>
<p className="text-zinc-600 text-xs mt-1">또는 파일을 여기에 드래그</p> <p className="text-zinc-600 text-xs mt-1">또는 파일을 여기에 드래그</p>
</div> </>
)}
</label>
</div> </div>
)} )}
@ -1984,19 +2109,21 @@ export default function Admin({ isMobile = false }) {
{/* 버튼 */} {/* 버튼 */}
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={() => setShowModpackDialog(false)} onClick={() => {
setShowModpackDialog(false);
setModpackFile(null);
setModpackForm({ changelog: '' });
}}
className="flex-1 px-4 py-2.5 bg-zinc-800 hover:bg-zinc-700 text-white rounded-xl font-medium transition-colors" className="flex-1 px-4 py-2.5 bg-zinc-800 hover:bg-zinc-700 text-white rounded-xl font-medium transition-colors"
> >
취소 취소
</button> </button>
<button <button
onClick={() => { onClick={modpackDialogMode === 'upload' ? handleModpackUpload : handleModpackEdit}
setToast(modpackDialogMode === 'upload' ? '모드팩이 업로드되었습니다.' : '모드팩이 수정되었습니다.'); disabled={modpackLoading}
setShowModpackDialog(false); className="flex-1 px-4 py-2.5 bg-mc-green hover:bg-mc-green/80 text-white rounded-xl font-medium transition-colors disabled:opacity-50"
}}
className="flex-1 px-4 py-2.5 bg-mc-green hover:bg-mc-green/80 text-white rounded-xl font-medium transition-colors"
> >
{modpackDialogMode === 'upload' ? '업로드' : '저장'} {modpackLoading ? '처리 중...' : (modpackDialogMode === 'upload' ? '업로드' : '저장')}
</button> </button>
</div> </div>
</motion.div> </motion.div>
@ -2038,14 +2165,11 @@ export default function Admin({ isMobile = false }) {
취소 취소
</button> </button>
<button <button
onClick={() => { onClick={handleModpackDelete}
setModpacks(prev => prev.filter(p => p.id !== modpackDeleteTarget.id)); disabled={modpackLoading}
setToast('모드팩이 삭제되었습니다.'); className="flex-1 px-4 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-xl font-medium transition-colors disabled:opacity-50"
setModpackDeleteTarget(null);
}}
className="flex-1 px-4 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-xl font-medium transition-colors"
> >
삭제 {modpackLoading ? '삭제 중...' : '삭제'}
</button> </button>
</div> </div>
</motion.div> </motion.div>

View file

@ -5,86 +5,6 @@ import React, { useState } from 'react';
import { Download, Package, ChevronDown, ChevronUp, Calendar, HardDrive, Gamepad2, Box, Image } from 'lucide-react'; import { Download, Package, ChevronDown, ChevronUp, Calendar, HardDrive, Gamepad2, Box, Image } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
//
const DUMMY_MODPACKS = [
{
id: 1,
version: '1.2.0',
name: '테스트 서버 모드팩',
minecraftVersion: '1.21.1',
modLoader: 'NeoForge',
modLoaderVersion: '21.1.213',
changelog: `### 새로운 기능
- Create 모드 추가
- JEI (Just Enough Items) 추가
- Jade 모드 추가
### 버그 수정
- 일부 텍스처 누락 문제 해결`,
fileSize: 15728640, // 15MB
downloadCount: 42,
createdAt: '2024-12-20T10:30:00Z',
contents: {
mods: [
{ name: 'Create', version: '0.5.1' },
{ name: 'Just Enough Items', version: '15.2.0' },
{ name: 'Jade', version: '11.7.1' },
{ name: 'JourneyMap', version: '5.9.18' },
{ name: 'Sodium', version: '0.5.8' },
],
resourcepacks: [
{ name: 'Faithful 32x', version: '1.21' },
],
shaderpacks: [
{ name: 'Complementary Shaders', version: '5.2.1' },
{ name: 'BSL Shaders', version: '8.2.09' },
],
},
},
{
id: 2,
version: '1.1.0',
name: '테스트 서버 모드팩',
minecraftVersion: '1.21.1',
modLoader: 'NeoForge',
modLoaderVersion: '21.1.200',
changelog: `### 변경사항
- JourneyMap 추가
- Sodium 성능 모드 추가`,
fileSize: 12582912, // 12MB
downloadCount: 128,
createdAt: '2024-12-15T14:00:00Z',
contents: {
mods: [
{ name: 'Just Enough Items', version: '15.1.0' },
{ name: 'JourneyMap', version: '5.9.17' },
{ name: 'Sodium', version: '0.5.7' },
],
resourcepacks: [],
shaderpacks: [],
},
},
{
id: 3,
version: '1.0.0',
name: '테스트 서버 모드팩',
minecraftVersion: '1.21.1',
modLoader: 'NeoForge',
modLoaderVersion: '21.1.180',
changelog: `### 최초 릴리즈
- 기본 모드 구성`,
fileSize: 8388608, // 8MB
downloadCount: 256,
createdAt: '2024-12-01T09:00:00Z',
contents: {
mods: [
{ name: 'Just Enough Items', version: '15.0.0' },
],
resourcepacks: [],
shaderpacks: [],
},
},
];
// //
const formatFileSize = (bytes) => { const formatFileSize = (bytes) => {
@ -136,10 +56,13 @@ const ModpackCard = ({ modpack, isLatest }) => {
</div> </div>
{/* 다운로드 버튼 */} {/* 다운로드 버튼 */}
<button className="flex items-center gap-2 px-4 py-2.5 bg-mc-green hover:bg-mc-green/80 text-white font-medium rounded-xl transition-colors"> <a
href={`/api/modpacks/${modpack.id}/download`}
className="flex items-center gap-2 px-4 py-2.5 bg-mc-green hover:bg-mc-green/80 text-white font-medium rounded-xl transition-colors"
>
<Download size={18} /> <Download size={18} />
<span>다운로드</span> <span>다운로드</span>
</button> </a>
</div> </div>
{/* 메타 정보 */} {/* 메타 정보 */}
@ -300,7 +223,44 @@ const ModpackCard = ({ modpack, isLatest }) => {
}; };
export default function Modpack() { export default function Modpack() {
const modpacks = DUMMY_MODPACKS; const [modpacks, setModpacks] = useState([]);
const [loading, setLoading] = useState(true);
// API
React.useEffect(() => {
const fetchModpacks = async () => {
try {
const res = await fetch('/api/modpacks');
const data = await res.json();
// API UI
const formatted = data.map(mp => ({
id: mp.id,
version: mp.version,
name: mp.name,
minecraftVersion: mp.minecraft_version,
modLoader: mp.mod_loader,
changelog: mp.changelog || '',
fileSize: mp.file_size,
createdAt: mp.created_at,
contents: mp.contents || { mods: [], resourcepacks: [], shaderpacks: [] },
}));
setModpacks(formatted);
} catch (error) {
console.error('모드팩 목록 로드 실패:', error);
} finally {
setLoading(false);
}
};
fetchModpacks();
}, []);
if (loading) {
return (
<div className="min-h-screen bg-mc-dark p-4 sm:p-6 flex items-center justify-center">
<div className="text-zinc-400">로딩 ...</div>
</div>
);
}
return ( return (
<div className="min-h-screen bg-mc-dark p-4 sm:p-6"> <div className="min-h-screen bg-mc-dark p-4 sm:p-6">