UI 개선: 업데이트 히스토리 페이지네이션 및 최대 표기 제한(50개) 적용

This commit is contained in:
choibk 2026-01-25 01:24:12 +09:00
parent 0ee02e68ee
commit cd61726b8e
2 changed files with 65 additions and 25 deletions

View File

@ -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
// but for history we still use the fetched local tags
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) => {
const lines = stdout ? stdout.trim().split('\n') : [];

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { Card } from '../../shared/ui/Card';
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 {
status: string;
@ -36,6 +36,8 @@ export function VersionPage() {
const [checkingRemote, setCheckingRemote] = useState(false);
const [updating, setUpdating] = useState(false);
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const ITEMS_PER_PAGE = 5;
const fetchVersion = async () => {
setLoading(true);
@ -59,6 +61,7 @@ export function VersionPage() {
console.error('Failed to fetch remote version info', err);
} finally {
setCheckingRemote(false);
setCurrentPage(1);
}
};
@ -286,31 +289,68 @@ export function VersionPage() {
<div className="space-y-4">
{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' : ''}`}>
<div className="flex flex-col md:flex-row md:items-center gap-4 mb-4">
<div className="flex items-center gap-2">
<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>
<>
{remoteInfo.history
.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE)
.map((entry, idx) => (
<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' : ''}`}>
<div className="flex flex-col md:flex-row md:items-center gap-4 mb-4">
<div className="flex items-center gap-2">
<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 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>
<button
onClick={() => setCurrentPage(p => Math.min(Math.ceil(remoteInfo.history.length / ITEMS_PER_PAGE), p + 1))}
disabled={currentPage === Math.ceil(remoteInfo.history.length / ITEMS_PER_PAGE)}
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>
<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">
.