2025-12-23 16:14:51 +09:00
|
|
|
/**
|
|
|
|
|
* 모드팩 페이지 - GitHub Release 스타일
|
|
|
|
|
*/
|
|
|
|
|
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 formatFileSize = (bytes) => {
|
|
|
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
|
|
|
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
|
|
|
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
|
|
|
return `${(bytes / 1073741824).toFixed(2)} GB`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 날짜 포맷
|
|
|
|
|
const formatDate = (dateString) => {
|
|
|
|
|
const date = new Date(dateString);
|
|
|
|
|
return date.toLocaleDateString('ko-KR', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 모드팩 카드 컴포넌트
|
|
|
|
|
const ModpackCard = ({ modpack, isLatest }) => {
|
2025-12-23 21:58:45 +09:00
|
|
|
const [showChangelog, setShowChangelog] = useState(false);
|
2025-12-23 16:14:51 +09:00
|
|
|
const [showContents, setShowContents] = useState(false);
|
|
|
|
|
|
|
|
|
|
const totalMods = modpack.contents.mods.length;
|
|
|
|
|
const totalResourcepacks = modpack.contents.resourcepacks.length;
|
|
|
|
|
const totalShaderpacks = modpack.contents.shaderpacks.length;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`bg-zinc-900 border rounded-2xl overflow-hidden ${isLatest ? 'border-mc-green' : 'border-zinc-800'}`}>
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
<div className="p-5">
|
2025-12-24 16:53:29 +09:00
|
|
|
{/* 상단: 아이콘 + 버전 정보 */}
|
|
|
|
|
<div className="flex items-center gap-3 mb-3">
|
|
|
|
|
<div className={`p-2.5 rounded-xl ${isLatest ? 'bg-mc-green/20' : 'bg-zinc-800'}`}>
|
|
|
|
|
<Package className={isLatest ? 'text-mc-green' : 'text-zinc-400'} size={24} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<h3 className="text-xl font-bold text-white">v{modpack.version}</h3>
|
|
|
|
|
{isLatest && (
|
|
|
|
|
<span className="px-2 py-0.5 bg-mc-green/20 text-mc-green text-xs font-medium rounded-full">
|
|
|
|
|
최신
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2025-12-23 16:14:51 +09:00
|
|
|
</div>
|
2025-12-24 16:53:29 +09:00
|
|
|
<p className="text-sm text-zinc-400 mt-0.5 truncate">{modpack.name}</p>
|
2025-12-23 16:14:51 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-24 16:53:29 +09:00
|
|
|
|
|
|
|
|
{/* 다운로드 버튼 - 모바일에서 전체 너비 */}
|
|
|
|
|
<a
|
|
|
|
|
href={`/api/modpacks/${modpack.id}/download`}
|
|
|
|
|
className="flex items-center justify-center gap-2 w-full sm:w-auto 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>
|
|
|
|
|
</a>
|
2025-12-23 16:14:51 +09:00
|
|
|
|
|
|
|
|
{/* 메타 정보 */}
|
2025-12-24 16:53:29 +09:00
|
|
|
<div className="flex flex-wrap gap-x-4 gap-y-2 mt-4 text-sm text-zinc-400">
|
2025-12-23 16:14:51 +09:00
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<Gamepad2 size={14} />
|
|
|
|
|
<span>MC {modpack.minecraftVersion}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<Box size={14} />
|
2025-12-24 16:53:29 +09:00
|
|
|
<span>{modpack.modLoader}</span>
|
2025-12-23 16:14:51 +09:00
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<HardDrive size={14} />
|
|
|
|
|
<span>{formatFileSize(modpack.fileSize)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<Calendar size={14} />
|
|
|
|
|
<span>{formatDate(modpack.createdAt)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 포함 콘텐츠 요약 */}
|
2025-12-24 16:53:29 +09:00
|
|
|
<div className="flex flex-wrap gap-2 mt-4">
|
2025-12-23 16:14:51 +09:00
|
|
|
{totalMods > 0 && (
|
|
|
|
|
<span className="px-2.5 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-lg">
|
|
|
|
|
모드 {totalMods}개
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{totalResourcepacks > 0 && (
|
|
|
|
|
<span className="px-2.5 py-1 bg-purple-500/20 text-purple-400 text-xs rounded-lg">
|
|
|
|
|
리소스팩 {totalResourcepacks}개
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{totalShaderpacks > 0 && (
|
|
|
|
|
<span className="px-2.5 py-1 bg-orange-500/20 text-orange-400 text-xs rounded-lg">
|
2025-12-23 21:58:45 +09:00
|
|
|
쉐이더 {totalShaderpacks}개
|
2025-12-23 16:14:51 +09:00
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 변경 로그 토글 */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowChangelog(!showChangelog)}
|
2025-12-23 21:58:45 +09:00
|
|
|
className="w-full flex items-center justify-between px-5 py-3 bg-zinc-800/50 hover:bg-zinc-800 text-zinc-300 text-sm transition-colors border-t border-zinc-800"
|
2025-12-23 16:14:51 +09:00
|
|
|
>
|
2025-12-23 21:58:45 +09:00
|
|
|
<span>체인지 로그</span>
|
2025-12-23 16:14:51 +09:00
|
|
|
{showChangelog ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* 변경 로그 내용 */}
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
{showChangelog && (
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ height: 0, opacity: 0 }}
|
|
|
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
|
|
|
exit={{ height: 0, opacity: 0 }}
|
|
|
|
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
|
|
|
|
className="overflow-hidden"
|
|
|
|
|
>
|
|
|
|
|
<div className="px-5 py-4 bg-zinc-800/30 border-t border-zinc-800">
|
2025-12-23 21:58:45 +09:00
|
|
|
<div className="prose prose-invert max-w-none">
|
2025-12-23 16:14:51 +09:00
|
|
|
{modpack.changelog.split('\n').map((line, i) => {
|
|
|
|
|
if (line.startsWith('###')) {
|
2025-12-23 21:58:45 +09:00
|
|
|
return <h4 key={i} className="text-white font-semibold mt-2 mb-2">{line.replace('### ', '')}</h4>;
|
2025-12-23 16:14:51 +09:00
|
|
|
}
|
|
|
|
|
if (line.startsWith('- ')) {
|
2025-12-23 21:58:45 +09:00
|
|
|
return <p key={i} className="text-zinc-400 my-1.5 pl-4">• {line.replace('- ', '')}</p>;
|
2025-12-23 16:14:51 +09:00
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
|
|
|
|
{/* 포함 콘텐츠 토글 */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowContents(!showContents)}
|
2025-12-23 21:58:45 +09:00
|
|
|
className="w-full flex items-center justify-between px-5 py-3 bg-zinc-800/50 hover:bg-zinc-800 text-zinc-300 text-sm transition-colors border-t border-zinc-800"
|
2025-12-23 16:14:51 +09:00
|
|
|
>
|
|
|
|
|
<span>포함된 콘텐츠</span>
|
|
|
|
|
{showContents ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* 포함 콘텐츠 내용 */}
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
{showContents && (
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ height: 0, opacity: 0 }}
|
|
|
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
|
|
|
exit={{ height: 0, opacity: 0 }}
|
|
|
|
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
|
|
|
|
className="overflow-hidden"
|
|
|
|
|
>
|
|
|
|
|
<div className="px-5 py-4 bg-zinc-800/30 border-t border-zinc-800">
|
|
|
|
|
{/* 모드 */}
|
|
|
|
|
{modpack.contents.mods.length > 0 && (
|
2025-12-23 21:58:45 +09:00
|
|
|
<div className="mb-6">
|
|
|
|
|
<h4 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
|
2025-12-23 16:14:51 +09:00
|
|
|
<Box size={14} className="text-blue-400" />
|
2025-12-23 21:58:45 +09:00
|
|
|
모드 ({modpack.contents.mods.length}개)
|
2025-12-23 16:14:51 +09:00
|
|
|
</h4>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
2025-12-23 21:58:45 +09:00
|
|
|
{[...modpack.contents.mods].sort((a, b) => {
|
|
|
|
|
const titleA = (typeof a === 'object' ? a.title : a).toLowerCase();
|
|
|
|
|
const titleB = (typeof b === 'object' ? b.title : b).toLowerCase();
|
|
|
|
|
return titleA.localeCompare(titleB);
|
|
|
|
|
}).map((mod, i) => {
|
|
|
|
|
// mod가 객체인 경우 (Modrinth 연동 후) vs 문자열인 경우 (현재)
|
|
|
|
|
const isObject = typeof mod === 'object';
|
|
|
|
|
const title = isObject ? mod.title : mod;
|
|
|
|
|
const version = isObject ? mod.version : null;
|
|
|
|
|
const iconUrl = isObject ? mod.icon_url : null;
|
|
|
|
|
const slug = isObject ? mod.slug : null;
|
|
|
|
|
|
|
|
|
|
const content = (
|
|
|
|
|
<div className={`flex items-center gap-3 p-3 bg-zinc-800/80 rounded-xl border border-zinc-700/50 transition-all h-[68px] ${slug ? 'hover:bg-blue-500/10 hover:border-blue-500/30 cursor-pointer' : ''}`}>
|
|
|
|
|
{/* 아이콘 */}
|
|
|
|
|
<div className="w-12 h-12 rounded-lg bg-zinc-700 flex items-center justify-center overflow-hidden flex-shrink-0">
|
|
|
|
|
{iconUrl ? (
|
|
|
|
|
<img src={iconUrl} alt="" className="w-full h-full object-cover" />
|
|
|
|
|
) : (
|
|
|
|
|
<Box size={20} className="text-blue-400" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{/* 정보 */}
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<p className="text-white font-medium truncate">{title}</p>
|
|
|
|
|
<p className="text-zinc-500 text-sm truncate h-5">{version || ''}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return slug ? (
|
|
|
|
|
<a key={i} href={`https://modrinth.com/mod/${slug}`} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
{content}
|
|
|
|
|
</a>
|
|
|
|
|
) : (
|
|
|
|
|
<div key={i}>{content}</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-12-23 16:14:51 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 리소스팩 */}
|
|
|
|
|
{modpack.contents.resourcepacks.length > 0 && (
|
2025-12-23 21:58:45 +09:00
|
|
|
<div className="mb-6">
|
|
|
|
|
<h4 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
|
2025-12-23 16:14:51 +09:00
|
|
|
<Image size={14} className="text-purple-400" />
|
2025-12-23 21:58:45 +09:00
|
|
|
리소스팩 ({modpack.contents.resourcepacks.length}개)
|
2025-12-23 16:14:51 +09:00
|
|
|
</h4>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
2025-12-23 21:58:45 +09:00
|
|
|
{[...modpack.contents.resourcepacks].sort((a, b) => {
|
|
|
|
|
const titleA = (typeof a === 'object' ? a.title : a).toLowerCase();
|
|
|
|
|
const titleB = (typeof b === 'object' ? b.title : b).toLowerCase();
|
|
|
|
|
return titleA.localeCompare(titleB);
|
|
|
|
|
}).map((pack, i) => {
|
|
|
|
|
const isObject = typeof pack === 'object';
|
|
|
|
|
const title = isObject ? pack.title : pack;
|
|
|
|
|
const version = isObject ? pack.version : null;
|
|
|
|
|
const iconUrl = isObject ? pack.icon_url : null;
|
|
|
|
|
const slug = isObject ? pack.slug : null;
|
|
|
|
|
|
|
|
|
|
const content = (
|
|
|
|
|
<div className={`flex items-center gap-3 p-3 bg-zinc-800/80 rounded-xl border border-zinc-700/50 transition-all h-[68px] ${slug ? 'hover:bg-purple-500/10 hover:border-purple-500/30 cursor-pointer' : ''}`}>
|
|
|
|
|
<div className="w-12 h-12 rounded-lg bg-zinc-700 flex items-center justify-center overflow-hidden flex-shrink-0">
|
|
|
|
|
{iconUrl ? (
|
|
|
|
|
<img src={iconUrl} alt="" className="w-full h-full object-cover" />
|
|
|
|
|
) : (
|
|
|
|
|
<Image size={20} className="text-purple-400" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<p className="text-white font-medium truncate">{title}</p>
|
|
|
|
|
<p className="text-zinc-500 text-sm truncate h-5">{version || ''}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return slug ? (
|
|
|
|
|
<a key={i} href={`https://modrinth.com/resourcepack/${slug}`} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
{content}
|
|
|
|
|
</a>
|
|
|
|
|
) : (
|
|
|
|
|
<div key={i}>{content}</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-12-23 16:14:51 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-23 21:58:45 +09:00
|
|
|
{/* 쉐이더 */}
|
2025-12-23 16:14:51 +09:00
|
|
|
{modpack.contents.shaderpacks.length > 0 && (
|
|
|
|
|
<div>
|
2025-12-23 21:58:45 +09:00
|
|
|
<h4 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
|
2025-12-23 16:14:51 +09:00
|
|
|
<span className="text-orange-400">✨</span>
|
2025-12-23 21:58:45 +09:00
|
|
|
쉐이더 ({modpack.contents.shaderpacks.length}개)
|
2025-12-23 16:14:51 +09:00
|
|
|
</h4>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
2025-12-23 21:58:45 +09:00
|
|
|
{[...modpack.contents.shaderpacks].sort((a, b) => {
|
|
|
|
|
const titleA = (typeof a === 'object' ? a.title : a).toLowerCase();
|
|
|
|
|
const titleB = (typeof b === 'object' ? b.title : b).toLowerCase();
|
|
|
|
|
return titleA.localeCompare(titleB);
|
|
|
|
|
}).map((shader, i) => {
|
|
|
|
|
const isObject = typeof shader === 'object';
|
|
|
|
|
const title = isObject ? shader.title : shader;
|
|
|
|
|
const version = isObject ? shader.version : null;
|
|
|
|
|
const iconUrl = isObject ? shader.icon_url : null;
|
|
|
|
|
const slug = isObject ? shader.slug : null;
|
|
|
|
|
|
|
|
|
|
const content = (
|
|
|
|
|
<div className={`flex items-center gap-3 p-3 bg-zinc-800/80 rounded-xl border border-zinc-700/50 transition-all ${slug ? 'hover:bg-orange-500/10 hover:border-orange-500/30 cursor-pointer' : ''}`}>
|
|
|
|
|
<div className="w-12 h-12 rounded-lg bg-zinc-700 flex items-center justify-center overflow-hidden flex-shrink-0">
|
|
|
|
|
{iconUrl ? (
|
|
|
|
|
<img src={iconUrl} alt="" className="w-full h-full object-cover" />
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-xl">✨</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<p className="text-white font-medium truncate">{title}</p>
|
|
|
|
|
{version && <p className="text-zinc-500 text-sm truncate">{version}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return slug ? (
|
|
|
|
|
<a key={i} href={`https://modrinth.com/shader/${slug}`} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
{content}
|
|
|
|
|
</a>
|
|
|
|
|
) : (
|
|
|
|
|
<div key={i}>{content}</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-12-23 16:14:51 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function Modpack() {
|
2025-12-23 16:42:43 +09:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-23 16:14:51 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-mc-dark p-4 sm:p-6">
|
|
|
|
|
<div className="max-w-4xl mx-auto">
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
|
|
|
|
|
<Package className="text-mc-green" />
|
|
|
|
|
모드팩
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-zinc-400 mt-1">서버 접속에 필요한 모드팩을 다운로드하세요</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 모드팩 목록 */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{modpacks.map((modpack, index) => (
|
|
|
|
|
<ModpackCard key={modpack.id} modpack={modpack} isLatest={index === 0} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 빈 상태 (모드팩이 없을 때) */}
|
|
|
|
|
{modpacks.length === 0 && (
|
|
|
|
|
<div className="text-center py-16">
|
|
|
|
|
<Package className="mx-auto text-zinc-600 mb-4" size={48} />
|
|
|
|
|
<p className="text-zinc-400">등록된 모드팩이 없습니다</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|