UI 개선: 업데이트 히스토리 페이지네이션 및 최대 표기 제한(50개) 적용
This commit is contained in:
parent
0ee02e68ee
commit
cd61726b8e
@ -496,7 +496,7 @@ router.get('/version/remote', isAuthenticated, hasRole('admin'), async (req, res
|
|||||||
// Also ensure we are looking at the remote tags directly if possible for the 'latest' check
|
// Also ensure we are looking at the remote tags directly if possible for the 'latest' check
|
||||||
// but for history we still use the fetched local tags
|
// but for history we still use the fetched local tags
|
||||||
const format = '%(refname:short)|%(contents:subject)|%(contents:body)|%(creatordate:iso8601)';
|
const format = '%(refname:short)|%(contents:subject)|%(contents:body)|%(creatordate:iso8601)';
|
||||||
const historyCmd = `git for-each-ref refs/tags --sort=-creatordate --format="${format}" --count=10`;
|
const historyCmd = `git for-each-ref refs/tags --sort=-creatordate --format="${format}" --count=50`;
|
||||||
|
|
||||||
exec(historyCmd, (err, stdout, stderr) => {
|
exec(historyCmd, (err, stdout, stderr) => {
|
||||||
const lines = stdout ? stdout.trim().split('\n') : [];
|
const lines = stdout ? stdout.trim().split('\n') : [];
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Card } from '../../shared/ui/Card';
|
import { Card } from '../../shared/ui/Card';
|
||||||
import { apiClient } from '../../shared/api/client';
|
import { apiClient } from '../../shared/api/client';
|
||||||
import { Info, Cpu, Database, Server, Hash, Calendar, RefreshCw, AlertTriangle, CheckCircle2 } from 'lucide-react';
|
import { Info, Cpu, Database, Server, Hash, Calendar, RefreshCw, AlertTriangle, CheckCircle2, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
interface VersionInfo {
|
interface VersionInfo {
|
||||||
status: string;
|
status: string;
|
||||||
@ -36,6 +36,8 @@ export function VersionPage() {
|
|||||||
const [checkingRemote, setCheckingRemote] = useState(false);
|
const [checkingRemote, setCheckingRemote] = useState(false);
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
|
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const ITEMS_PER_PAGE = 5;
|
||||||
|
|
||||||
const fetchVersion = async () => {
|
const fetchVersion = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -59,6 +61,7 @@ export function VersionPage() {
|
|||||||
console.error('Failed to fetch remote version info', err);
|
console.error('Failed to fetch remote version info', err);
|
||||||
} finally {
|
} finally {
|
||||||
setCheckingRemote(false);
|
setCheckingRemote(false);
|
||||||
|
setCurrentPage(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -286,31 +289,68 @@ export function VersionPage() {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{remoteInfo?.history && remoteInfo.history.length > 0 ? (
|
{remoteInfo?.history && remoteInfo.history.length > 0 ? (
|
||||||
remoteInfo.history.map((entry, idx) => (
|
<>
|
||||||
<Card key={entry.version} className={`p-6 border-slate-200 shadow-sm transition-all hover:border-indigo-200 ${idx === 0 ? 'bg-indigo-50/20 border-indigo-100 ring-2 ring-indigo-50/50 shadow-indigo-100/50' : ''}`}>
|
{remoteInfo.history
|
||||||
<div className="flex flex-col md:flex-row md:items-center gap-4 mb-4">
|
.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE)
|
||||||
<div className="flex items-center gap-2">
|
.map((entry, idx) => (
|
||||||
<span className={`px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider ${entry.type === 'feature' ? 'bg-indigo-600 text-white' : entry.type === 'urgent' ? 'bg-red-600 text-white' : 'bg-slate-200 text-slate-700'}`}>
|
<Card key={entry.version} className={`p-6 border-slate-200 shadow-sm transition-all hover:border-indigo-200 ${idx === 0 && currentPage === 1 ? 'bg-indigo-50/20 border-indigo-100 ring-2 ring-indigo-50/50 shadow-indigo-100/50' : ''}`}>
|
||||||
{entry.type}
|
<div className="flex flex-col md:flex-row md:items-center gap-4 mb-4">
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-bold text-slate-900 font-mono text-base">v{entry.version}</span>
|
<span className={`px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider ${entry.type === 'feature' ? 'bg-indigo-600 text-white' : entry.type === 'urgent' ? 'bg-red-600 text-white' : 'bg-slate-200 text-slate-700'}`}>
|
||||||
|
{entry.type}
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-slate-900 font-mono text-base">v{entry.version}</span>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block w-px h-4 bg-slate-200 mx-2"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-bold text-slate-800">{entry.title}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-400 font-medium px-2 py-1 bg-slate-50 rounded italic">{entry.date}</div>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{entry.changes.map((change, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-2 text-[13px] text-slate-600 leading-relaxed">
|
||||||
|
<div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-indigo-500/50 flex-shrink-0 animate-pulse"></div>
|
||||||
|
<span>{change}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
{remoteInfo.history.length > ITEMS_PER_PAGE && (
|
||||||
|
<div className="flex justify-center items-center gap-4 mt-8 pt-4 border-t border-slate-100">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="p-2 rounded-lg border border-slate-200 text-slate-500 hover:bg-slate-50 disabled:opacity-30 disabled:hover:bg-transparent transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{Array.from({ length: Math.ceil(Math.min(50, remoteInfo.history.length) / ITEMS_PER_PAGE) }).map((_, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
className={`w-8 h-8 rounded-lg text-sm font-bold transition-all ${currentPage === i + 1 ? 'bg-indigo-600 text-white shadow-md shadow-indigo-200' : 'text-slate-400 hover:text-indigo-600 hover:bg-indigo-50'}`}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block w-px h-4 bg-slate-200 mx-2"></div>
|
|
||||||
<div className="flex-1">
|
<button
|
||||||
<h4 className="text-sm font-bold text-slate-800">{entry.title}</h4>
|
onClick={() => setCurrentPage(p => Math.min(Math.ceil(remoteInfo.history.length / ITEMS_PER_PAGE), p + 1))}
|
||||||
</div>
|
disabled={currentPage === Math.ceil(remoteInfo.history.length / ITEMS_PER_PAGE)}
|
||||||
<div className="text-xs text-slate-400 font-medium px-2 py-1 bg-slate-50 rounded italic">{entry.date}</div>
|
className="p-2 rounded-lg border border-slate-200 text-slate-500 hover:bg-slate-50 disabled:opacity-30 disabled:hover:bg-transparent transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-2">
|
)}
|
||||||
{entry.changes.map((change, i) => (
|
</>
|
||||||
<li key={i} className="flex items-start gap-2 text-[13px] text-slate-600 leading-relaxed">
|
|
||||||
<div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-indigo-500/50 flex-shrink-0 animate-pulse"></div>
|
|
||||||
<span>{change}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Card>
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 text-slate-400 italic bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
<div className="text-center py-12 text-slate-400 italic bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
||||||
원격 저장소에서 업데이트 내역을 가져오는 중이거나 내역이 없습니다.
|
원격 저장소에서 업데이트 내역을 가져오는 중이거나 내역이 없습니다.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user