feat(admin): 활동 로그 페이지에 페이지 번호 직접 입력 기능 추가

- 페이지네이션을 3-column grid로 배치해 버튼 그룹은 중앙 정렬, 오른쪽 구석에 입력 박스 추가
- 숫자만 입력, Enter 또는 blur 시 페이지 이동 (1~totalPages로 clamp)
- Enter 시 blur() 호출로 포커스 자동 해제

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-22 11:47:59 +09:00
parent 8ece4b1850
commit 39bb6f77f9

View file

@ -27,6 +27,7 @@ function Logs() {
const [dateFrom, setDateFrom] = useState(''); const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState(''); const [dateTo, setDateTo] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageInput, setPageInput] = useState('1');
const [actorDropdownOpen, setActorDropdownOpen] = useState(false); const [actorDropdownOpen, setActorDropdownOpen] = useState(false);
const [categoryDropdownOpen, setCategoryDropdownOpen] = useState(false); const [categoryDropdownOpen, setCategoryDropdownOpen] = useState(false);
const [selectedLog, setSelectedLog] = useState(null); const [selectedLog, setSelectedLog] = useState(null);
@ -65,6 +66,20 @@ function Logs() {
const total = data?.total || 0; const total = data?.total || 0;
const totalPages = data?.totalPages || 0; const totalPages = data?.totalPages || 0;
//
useEffect(() => { setPageInput(String(currentPage)); }, [currentPage]);
const goToPageFromInput = () => {
const n = parseInt(pageInput, 10);
if (!Number.isFinite(n) || n < 1) {
setPageInput(String(currentPage));
return;
}
const clamped = Math.min(totalPages, n);
setCurrentPage(clamped);
setPageInput(String(clamped));
};
// //
const toggleCategory = (cat) => { const toggleCategory = (cat) => {
setSelectedCategories((prev) => setSelectedCategories((prev) =>
@ -343,52 +358,74 @@ function Logs() {
{/* 페이지네이션 */} {/* 페이지네이션 */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="flex items-center justify-center gap-2 mt-6"> <div className="grid grid-cols-3 items-center mt-6">
<button <div />
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} <div className="flex items-center justify-center gap-2">
disabled={currentPage === 1} <button
className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
> disabled={currentPage === 1}
<ChevronLeft size={18} /> className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
</button> >
{Array.from({ length: totalPages }, (_, i) => i + 1) <ChevronLeft size={18} />
.filter((page) => { </button>
if (totalPages <= 7) return true; {Array.from({ length: totalPages }, (_, i) => i + 1)
if (page === 1 || page === totalPages) return true; .filter((page) => {
if (Math.abs(page - currentPage) <= 2) return true; if (totalPages <= 7) return true;
return false; if (page === 1 || page === totalPages) return true;
}) if (Math.abs(page - currentPage) <= 2) return true;
.reduce((acc, page, i, arr) => { return false;
if (i > 0 && page - arr[i - 1] > 1) { })
acc.push({ type: 'ellipsis', key: `e-${page}` }); .reduce((acc, page, i, arr) => {
} if (i > 0 && page - arr[i - 1] > 1) {
acc.push({ type: 'page', value: page, key: page }); acc.push({ type: 'ellipsis', key: `e-${page}` });
return acc; }
}, []) acc.push({ type: 'page', value: page, key: page });
.map((item) => return acc;
item.type === 'ellipsis' ? ( }, [])
<span key={item.key} className="w-9 h-9 flex items-center justify-center text-sm text-gray-400">...</span> .map((item) =>
) : ( item.type === 'ellipsis' ? (
<button <span key={item.key} className="w-9 h-9 flex items-center justify-center text-sm text-gray-400">...</span>
key={item.key} ) : (
onClick={() => setCurrentPage(item.value)} <button
className={`w-9 h-9 rounded-lg text-sm font-medium transition-colors ${ key={item.key}
currentPage === item.value onClick={() => setCurrentPage(item.value)}
? 'bg-primary text-white' className={`w-9 h-9 rounded-lg text-sm font-medium transition-colors ${
: 'text-gray-600 hover:bg-gray-100' currentPage === item.value
}`} ? 'bg-primary text-white'
> : 'text-gray-600 hover:bg-gray-100'
{item.value} }`}
</button> >
) {item.value}
)} </button>
<button )
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} )}
disabled={currentPage === totalPages} <button
className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
> disabled={currentPage === totalPages}
<ChevronRight size={18} /> className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
</button> >
<ChevronRight size={18} />
</button>
</div>
<div className="flex items-center justify-end gap-1.5">
<input
type="text"
inputMode="numeric"
value={pageInput}
onChange={(e) => setPageInput(e.target.value.replace(/\D/g, ''))}
onBlur={goToPageFromInput}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
goToPageFromInput();
e.currentTarget.blur();
}
}}
className="w-12 h-9 text-center tabular-nums border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
aria-label="페이지 번호 입력"
/>
<span className="text-sm text-gray-400 tabular-nums">/ {totalPages}</span>
</div>
</div> </div>
)} )}
</div> </div>