146 lines
5.1 KiB
Vue
146 lines
5.1 KiB
Vue
<script setup>
|
|
const props = defineProps({
|
|
page: {
|
|
type: Number,
|
|
default: 1
|
|
},
|
|
totalPages: {
|
|
type: Number,
|
|
default: 1
|
|
},
|
|
label: {
|
|
type: String,
|
|
default: '페이지'
|
|
},
|
|
basePath: {
|
|
type: String,
|
|
required: true
|
|
}
|
|
})
|
|
|
|
const normalizedTotalPages = computed(() => Math.max(1, Number(props.totalPages) || 1))
|
|
const currentPage = computed(() => Math.max(1, Math.min(Number(props.page) || 1, normalizedTotalPages.value)))
|
|
const previousPage = computed(() => Math.max(1, currentPage.value - 1))
|
|
const nextPage = computed(() => Math.min(normalizedTotalPages.value, currentPage.value + 1))
|
|
const canGoPrevious = computed(() => currentPage.value > 1)
|
|
const canGoNext = computed(() => currentPage.value < normalizedTotalPages.value)
|
|
|
|
/**
|
|
* 연속 페이지 번호를 만든다.
|
|
* @param {number} start - 시작 페이지
|
|
* @param {number} end - 끝 페이지
|
|
* @returns {Array<number>} 페이지 번호 목록
|
|
*/
|
|
const range = (start, end) => {
|
|
const pages = []
|
|
|
|
for (let page = start; page <= end; page += 1) {
|
|
pages.push(page)
|
|
}
|
|
|
|
return pages
|
|
}
|
|
|
|
const pageItems = computed(() => {
|
|
const total = normalizedTotalPages.value
|
|
const current = currentPage.value
|
|
|
|
if (total <= 8) {
|
|
return range(1, total).map((page) => ({ type: 'page', page }))
|
|
}
|
|
|
|
const pages = new Set([1, total])
|
|
|
|
if (current <= 5) {
|
|
range(1, Math.min(total - 1, Math.max(4, current + 3))).forEach((page) => pages.add(page))
|
|
} else if (current >= total - 4) {
|
|
range(Math.max(2, total - 7), total).forEach((page) => pages.add(page))
|
|
} else {
|
|
range(Math.max(2, current - 3), Math.min(total - 1, current + 3)).forEach((page) => pages.add(page))
|
|
}
|
|
|
|
const sortedPages = [...pages].sort((left, right) => left - right)
|
|
const items = []
|
|
|
|
sortedPages.forEach((page, index) => {
|
|
const previous = sortedPages[index - 1]
|
|
|
|
if (previous && page - previous > 1) {
|
|
items.push({
|
|
type: 'ellipsis',
|
|
key: `ellipsis-${previous}-${page}`
|
|
})
|
|
}
|
|
|
|
items.push({
|
|
type: 'page',
|
|
page
|
|
})
|
|
})
|
|
|
|
return items
|
|
})
|
|
|
|
/**
|
|
* 페이지 링크 대상을 만든다.
|
|
* @param {number} page - 페이지 번호
|
|
* @returns {Object} Nuxt 링크 대상
|
|
*/
|
|
const getPageLinkTo = (page) => ({
|
|
path: props.basePath,
|
|
query: page > 1 ? { page } : {}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<nav class="site-pagination site-section" :aria-label="label">
|
|
<div class="site-pagination__body site-section-body mx-auto flex max-w-[720px] items-center justify-start gap-3 overflow-x-auto text-sm sm:justify-center sm:gap-4">
|
|
<NuxtLink
|
|
class="site-pagination__arrow inline-flex size-10 items-center justify-center rounded-full border border-[var(--site-line)] text-[var(--site-muted)] transition hover:border-[var(--site-soft)] hover:text-[var(--site-text)]"
|
|
:class="!canGoPrevious ? 'pointer-events-none opacity-35' : ''"
|
|
:to="getPageLinkTo(previousPage)"
|
|
:aria-disabled="!canGoPrevious"
|
|
:aria-label="`${label} 이전으로 이동`"
|
|
>
|
|
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="m15 18-6-6 6-6" />
|
|
</svg>
|
|
</NuxtLink>
|
|
|
|
<ol class="site-pagination__pages flex min-w-max items-center justify-center gap-1 sm:gap-2">
|
|
<li v-for="item in pageItems" :key="item.type === 'page' ? `page-${item.page}` : item.key">
|
|
<span
|
|
v-if="item.type === 'ellipsis'"
|
|
class="site-pagination__ellipsis inline-flex min-w-9 items-center justify-center px-1 text-base font-semibold text-[var(--site-muted)]"
|
|
aria-hidden="true"
|
|
>
|
|
...
|
|
</span>
|
|
<NuxtLink
|
|
v-else
|
|
class="site-pagination__page inline-flex min-w-9 items-center justify-center rounded-full px-2 py-2 text-base font-semibold text-[var(--site-muted)] transition hover:text-[var(--site-text)]"
|
|
:class="item.page === currentPage ? 'site-pagination__page--current pointer-events-none !bg-[var(--site-accent)] !text-white shadow-sm' : ''"
|
|
:to="getPageLinkTo(item.page)"
|
|
:aria-current="item.page === currentPage ? 'page' : undefined"
|
|
:aria-label="`${item.page}페이지로 이동`"
|
|
>
|
|
{{ item.page }}
|
|
</NuxtLink>
|
|
</li>
|
|
</ol>
|
|
|
|
<NuxtLink
|
|
class="site-pagination__arrow inline-flex size-10 items-center justify-center rounded-full border border-[var(--site-line)] text-[var(--site-muted)] transition hover:border-[var(--site-soft)] hover:text-[var(--site-text)]"
|
|
:class="!canGoNext ? 'pointer-events-none opacity-35' : ''"
|
|
:to="getPageLinkTo(nextPage)"
|
|
:aria-disabled="!canGoNext"
|
|
:aria-label="`${label} 다음으로 이동`"
|
|
>
|
|
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="m9 18 6-6-6-6" />
|
|
</svg>
|
|
</NuxtLink>
|
|
</div>
|
|
</nav>
|
|
</template>
|