feat: 프론트엔드 백엔드 API 연동
- Modpack.jsx: API fetch, 더미 데이터 제거, 다운로드 링크 연결 - Admin.jsx: 모드팩 목록 fetch, 업로드/수정/삭제 API 연동 - 파일 선택 UI, 로딩 상태, 에러 처리 구현
This commit is contained in:
parent
83820c3951
commit
778a9597bd
2 changed files with 191 additions and 107 deletions
|
|
@ -126,13 +126,11 @@ export default function Admin({ isMobile = false }) {
|
|||
const [showModpackDialog, setShowModpackDialog] = useState(false);
|
||||
const [modpackDialogMode, setModpackDialogMode] = useState('upload'); // 'upload' | 'edit'
|
||||
const [editingModpack, setEditingModpack] = useState(null);
|
||||
const [modpackForm, setModpackForm] = useState({ version: '', changelog: '' });
|
||||
const [modpacks, setModpacks] = useState([
|
||||
{ id: 1, version: '1.2.0', name: '테스트 서버 모드팩', date: '2024-12-20', size: '15.0 MB' },
|
||||
{ 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 [modpackForm, setModpackForm] = useState({ changelog: '' });
|
||||
const [modpackFile, setModpackFile] = useState(null); // 업로드할 파일
|
||||
const [modpacks, setModpacks] = useState([]);
|
||||
const [modpackDeleteTarget, setModpackDeleteTarget] = useState(null); // 삭제 확인 다이얼로그용
|
||||
const [modpackLoading, setModpackLoading] = useState(false); // 업로드/삭제 로딩
|
||||
|
||||
// 권한 확인
|
||||
useEffect(() => {
|
||||
|
|
@ -154,6 +152,121 @@ export default function Admin({ isMobile = false }) {
|
|||
}
|
||||
}, [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 (안정적인 참조)
|
||||
const fetchPlayers = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -1948,11 +2061,23 @@ export default function Admin({ isMobile = false }) {
|
|||
{modpackDialogMode === 'upload' && (
|
||||
<div className="mb-4">
|
||||
<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} />
|
||||
<p className="text-zinc-400 text-sm">클릭하여 파일 선택</p>
|
||||
<p className="text-zinc-600 text-xs mt-1">또는 파일을 여기에 드래그</p>
|
||||
</div>
|
||||
{modpackFile ? (
|
||||
<p className="text-mc-green text-sm">{modpackFile.name}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-zinc-400 text-sm">클릭하여 파일 선택</p>
|
||||
<p className="text-zinc-600 text-xs mt-1">또는 파일을 여기에 드래그</p>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1984,19 +2109,21 @@ export default function Admin({ isMobile = false }) {
|
|||
{/* 버튼 */}
|
||||
<div className="flex gap-3">
|
||||
<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"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setToast(modpackDialogMode === 'upload' ? '모드팩이 업로드되었습니다.' : '모드팩이 수정되었습니다.');
|
||||
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"
|
||||
onClick={modpackDialogMode === 'upload' ? handleModpackUpload : handleModpackEdit}
|
||||
disabled={modpackLoading}
|
||||
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"
|
||||
>
|
||||
{modpackDialogMode === 'upload' ? '업로드' : '저장'}
|
||||
{modpackLoading ? '처리 중...' : (modpackDialogMode === 'upload' ? '업로드' : '저장')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -2038,14 +2165,11 @@ export default function Admin({ isMobile = false }) {
|
|||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModpacks(prev => prev.filter(p => p.id !== modpackDeleteTarget.id));
|
||||
setToast('모드팩이 삭제되었습니다.');
|
||||
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"
|
||||
onClick={handleModpackDelete}
|
||||
disabled={modpackLoading}
|
||||
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"
|
||||
>
|
||||
삭제
|
||||
{modpackLoading ? '삭제 중...' : '삭제'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -5,86 +5,6 @@ import React, { useState } from 'react';
|
|||
import { Download, Package, ChevronDown, ChevronUp, Calendar, HardDrive, Gamepad2, Box, Image } from 'lucide-react';
|
||||
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) => {
|
||||
|
|
@ -136,10 +56,13 @@ const ModpackCard = ({ modpack, isLatest }) => {
|
|||
</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} />
|
||||
<span>다운로드</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* 메타 정보 */}
|
||||
|
|
@ -300,7 +223,44 @@ const ModpackCard = ({ modpack, isLatest }) => {
|
|||
};
|
||||
|
||||
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 (
|
||||
<div className="min-h-screen bg-mc-dark p-4 sm:p-6">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue