페이지 섹션·공개 목록 설정 누적 변경 일괄 반영
페이지 섹션 관리와 홈 Pages 진열장 연동, 공개 목록 표시개수 설정 관련 누적 로컬 변경을 기능 단위로 정리해 반영한다. 문서 이력과 패키지 버전을 v1.9.23으로 맞춰 코드와 문서 상태를 동기화한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -314,6 +314,15 @@ const deletePage = async (page) => {
|
||||
<NuxtLink class="admin-pages__title-link font-semibold hover:opacity-70" :to="`/admin/pages/${page.id}`">
|
||||
{{ page.title }}
|
||||
</NuxtLink>
|
||||
<span v-if="page.isListed" class="admin-pages__listed-badge ml-2 inline-flex h-5 items-center rounded bg-[#eff1f2] px-2 text-[11px] font-semibold text-[#7c8b9a]">
|
||||
홈 표시
|
||||
</span>
|
||||
<span v-if="page.isListed && page.listingSection" class="admin-pages__section-badge ml-1 inline-flex h-5 items-center rounded bg-white px-2 text-[11px] font-semibold text-[#7c8b9a] ring-1 ring-[#e3e6e8]">
|
||||
{{ page.listingSection }}
|
||||
</span>
|
||||
<span v-if="page.isFeatured" class="admin-pages__featured-badge ml-1 inline-flex h-5 items-center rounded bg-[#15171a] px-2 text-[11px] font-semibold text-white">
|
||||
추천
|
||||
</span>
|
||||
<div class="admin-pages__slug-row mt-1 flex items-center gap-1 text-xs text-muted">
|
||||
<span class="admin-pages__slug truncate">/pages/{{ page.slug }}</span>
|
||||
<NuxtLink
|
||||
|
||||
@@ -23,6 +23,16 @@ import {
|
||||
MIN_POST_TAG_LIMIT,
|
||||
normalizePostTagLimit
|
||||
} from '~/lib/post-tag-limit.js'
|
||||
import {
|
||||
DEFAULT_FEATURED_POST_LIMIT,
|
||||
DEFAULT_HOME_LATEST_POST_LIMIT,
|
||||
DEFAULT_PAGE_SECTION_ITEM_LIMIT,
|
||||
DEFAULT_POST_LIST_PAGE_SIZE,
|
||||
DEFAULT_TAG_POST_LIST_PAGE_SIZE,
|
||||
DISPLAY_COUNT_OPTIONS,
|
||||
POST_LIST_COUNT_OPTIONS,
|
||||
normalizeDisplayCount
|
||||
} from '~/lib/display-count-settings.js'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
@@ -110,7 +120,12 @@ const socialSnapshot = reactive({
|
||||
/** 편집 시작 시점의 POST 설정(취소 시 복원용) */
|
||||
const postSnapshot = reactive({
|
||||
showPostUpdatedAt: false,
|
||||
postTagLimit: DEFAULT_POST_TAG_LIMIT
|
||||
postTagLimit: DEFAULT_POST_TAG_LIMIT,
|
||||
homeLatestPostLimit: DEFAULT_HOME_LATEST_POST_LIMIT,
|
||||
postListPageSize: DEFAULT_POST_LIST_PAGE_SIZE,
|
||||
tagPostListPageSize: DEFAULT_TAG_POST_LIST_PAGE_SIZE,
|
||||
featuredPostLimit: DEFAULT_FEATURED_POST_LIMIT,
|
||||
pageSectionItemLimit: DEFAULT_PAGE_SECTION_ITEM_LIMIT
|
||||
})
|
||||
/** 편집 시작 시점의 메인 화면 커버(취소 시 복원용) */
|
||||
const homeCoverSnapshot = reactive({
|
||||
@@ -174,6 +189,11 @@ const form = reactive({
|
||||
socialLinks: normalizeSocialLinks(settings.value?.socialLinks || []),
|
||||
showPostUpdatedAt: Boolean(settings.value?.showPostUpdatedAt),
|
||||
postTagLimit: normalizePostTagLimit(settings.value?.postTagLimit),
|
||||
homeLatestPostLimit: normalizeDisplayCount(settings.value?.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT),
|
||||
postListPageSize: normalizeDisplayCount(settings.value?.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE),
|
||||
tagPostListPageSize: normalizeDisplayCount(settings.value?.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE),
|
||||
featuredPostLimit: normalizeDisplayCount(settings.value?.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT),
|
||||
pageSectionItemLimit: normalizeDisplayCount(settings.value?.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT),
|
||||
homeCoverImageUrl: settings.value?.homeCoverImageUrl || '',
|
||||
homeCoverDarkImageUrl: settings.value?.homeCoverDarkImageUrl || '',
|
||||
homeCoverTitle: settings.value?.homeCoverTitle || '',
|
||||
@@ -233,6 +253,11 @@ const hasPostChanges = computed(() => editPost.value
|
||||
&& (
|
||||
form.showPostUpdatedAt !== postSnapshot.showPostUpdatedAt
|
||||
|| normalizePostTagLimit(form.postTagLimit) !== normalizePostTagLimit(postSnapshot.postTagLimit)
|
||||
|| normalizeDisplayCount(form.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT) !== normalizeDisplayCount(postSnapshot.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT)
|
||||
|| normalizeDisplayCount(form.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE) !== normalizeDisplayCount(postSnapshot.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE)
|
||||
|| normalizeDisplayCount(form.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE) !== normalizeDisplayCount(postSnapshot.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE)
|
||||
|| normalizeDisplayCount(form.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT) !== normalizeDisplayCount(postSnapshot.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT)
|
||||
|| normalizeDisplayCount(form.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT) !== normalizeDisplayCount(postSnapshot.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT)
|
||||
))
|
||||
|
||||
/**
|
||||
@@ -1134,6 +1159,11 @@ const buildSiteSettingsPayload = () => ({
|
||||
socialLinks: normalizeSocialLinks(form.socialLinks),
|
||||
showPostUpdatedAt: Boolean(form.showPostUpdatedAt),
|
||||
postTagLimit: normalizePostTagLimit(form.postTagLimit),
|
||||
homeLatestPostLimit: normalizeDisplayCount(form.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT),
|
||||
postListPageSize: normalizeDisplayCount(form.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE),
|
||||
tagPostListPageSize: normalizeDisplayCount(form.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE),
|
||||
featuredPostLimit: normalizeDisplayCount(form.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT),
|
||||
pageSectionItemLimit: normalizeDisplayCount(form.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT),
|
||||
homeCoverImageUrl: form.homeCoverImageUrl || '',
|
||||
homeCoverDarkImageUrl: form.homeCoverDarkImageUrl || '',
|
||||
homeCoverTitle: form.homeCoverTitle || '',
|
||||
@@ -1364,7 +1394,17 @@ const saveSocialSection = async () => {
|
||||
const beginEditPost = () => {
|
||||
postSnapshot.showPostUpdatedAt = form.showPostUpdatedAt
|
||||
postSnapshot.postTagLimit = normalizePostTagLimit(form.postTagLimit)
|
||||
postSnapshot.homeLatestPostLimit = normalizeDisplayCount(form.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT)
|
||||
postSnapshot.postListPageSize = normalizeDisplayCount(form.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE)
|
||||
postSnapshot.tagPostListPageSize = normalizeDisplayCount(form.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE)
|
||||
postSnapshot.featuredPostLimit = normalizeDisplayCount(form.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT)
|
||||
postSnapshot.pageSectionItemLimit = normalizeDisplayCount(form.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT)
|
||||
form.postTagLimit = normalizePostTagLimit(form.postTagLimit)
|
||||
form.homeLatestPostLimit = postSnapshot.homeLatestPostLimit
|
||||
form.postListPageSize = postSnapshot.postListPageSize
|
||||
form.tagPostListPageSize = postSnapshot.tagPostListPageSize
|
||||
form.featuredPostLimit = postSnapshot.featuredPostLimit
|
||||
form.pageSectionItemLimit = postSnapshot.pageSectionItemLimit
|
||||
editPost.value = true
|
||||
}
|
||||
|
||||
@@ -1375,6 +1415,11 @@ const beginEditPost = () => {
|
||||
const cancelEditPost = () => {
|
||||
form.showPostUpdatedAt = postSnapshot.showPostUpdatedAt
|
||||
form.postTagLimit = normalizePostTagLimit(postSnapshot.postTagLimit)
|
||||
form.homeLatestPostLimit = normalizeDisplayCount(postSnapshot.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT)
|
||||
form.postListPageSize = normalizeDisplayCount(postSnapshot.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE)
|
||||
form.tagPostListPageSize = normalizeDisplayCount(postSnapshot.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE)
|
||||
form.featuredPostLimit = normalizeDisplayCount(postSnapshot.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT)
|
||||
form.pageSectionItemLimit = normalizeDisplayCount(postSnapshot.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT)
|
||||
editPost.value = false
|
||||
}
|
||||
|
||||
@@ -1395,7 +1440,17 @@ const savePostSection = async () => {
|
||||
if (ok) {
|
||||
postSnapshot.showPostUpdatedAt = form.showPostUpdatedAt
|
||||
postSnapshot.postTagLimit = normalizePostTagLimit(form.postTagLimit)
|
||||
postSnapshot.homeLatestPostLimit = normalizeDisplayCount(form.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT)
|
||||
postSnapshot.postListPageSize = normalizeDisplayCount(form.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE)
|
||||
postSnapshot.tagPostListPageSize = normalizeDisplayCount(form.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE)
|
||||
postSnapshot.featuredPostLimit = normalizeDisplayCount(form.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT)
|
||||
postSnapshot.pageSectionItemLimit = normalizeDisplayCount(form.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT)
|
||||
form.postTagLimit = postSnapshot.postTagLimit
|
||||
form.homeLatestPostLimit = postSnapshot.homeLatestPostLimit
|
||||
form.postListPageSize = postSnapshot.postListPageSize
|
||||
form.tagPostListPageSize = postSnapshot.tagPostListPageSize
|
||||
form.featuredPostLimit = postSnapshot.featuredPostLimit
|
||||
form.pageSectionItemLimit = postSnapshot.pageSectionItemLimit
|
||||
editPost.value = false
|
||||
}
|
||||
}
|
||||
@@ -2399,7 +2454,7 @@ onBeforeUnmount(() => {
|
||||
class="mr-5 mt-1 text-pretty text-sm leading-relaxed text-[#657080]"
|
||||
>
|
||||
공개 글 상세·관리자 글 목록에서 발행 후 수정이 있었을 때 수정일을 함께 표시합니다.
|
||||
글쓰기에서 선택할 수 있는 태그 최대 개수도 함께 관리합니다.
|
||||
글쓰기 태그 제한과 공개 목록 표시개수도 함께 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="-mr-1 mt-[-5px] flex shrink-0 items-center justify-start gap-2">
|
||||
@@ -2457,6 +2512,34 @@ onBeforeUnmount(() => {
|
||||
{{ normalizePostTagLimit(form.postTagLimit) }}개
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-3 border-t border-[#eceff2] pt-5">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<span class="font-bold text-[#15171a]">표시개수 설정</span>
|
||||
<span class="text-xs text-[#657080]">공개 목록 기준</span>
|
||||
</div>
|
||||
<div class="grid gap-2 text-xs text-[#657080] sm:grid-cols-2">
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border border-[#edf0f2] px-3 py-2">
|
||||
<span>Latest</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ normalizeDisplayCount(form.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT) }}개</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border border-[#edf0f2] px-3 py-2">
|
||||
<span>Posts</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ normalizeDisplayCount(form.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE) }}개</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border border-[#edf0f2] px-3 py-2">
|
||||
<span>태그 게시물</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ normalizeDisplayCount(form.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE) }}개</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border border-[#edf0f2] px-3 py-2">
|
||||
<span>Featured</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ normalizeDisplayCount(form.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT) }}개</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border border-[#edf0f2] px-3 py-2 sm:col-span-2">
|
||||
<span>Pages 섹션</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ normalizeDisplayCount(form.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT) }}개</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -2491,6 +2574,76 @@ onBeforeUnmount(() => {
|
||||
최소 {{ MIN_POST_TAG_LIMIT }}개, 최대 {{ MAX_POST_TAG_LIMIT }}개까지 설정할 수 있습니다. 기본값은 {{ DEFAULT_POST_TAG_LIMIT }}개입니다.
|
||||
</span>
|
||||
</label>
|
||||
<div class="admin-settings-screen__field grid gap-3 border-t border-[#eceff2] pt-5">
|
||||
<div>
|
||||
<span class="font-bold text-[#15171a]">표시개수 설정</span>
|
||||
<p class="mt-1 text-xs leading-relaxed text-[#657080]">
|
||||
공개 목록에서 한 번에 불러오고 보여줄 개수를 선택합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-xs font-semibold text-[#657080]">Latest</span>
|
||||
<span class="relative">
|
||||
<select
|
||||
v-model.number="form.homeLatestPostLimit"
|
||||
class="h-10 w-full appearance-none rounded-md border border-[#dce0e5] bg-white px-3 pr-10 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
>
|
||||
<option v-for="option in POST_LIST_COUNT_OPTIONS" :key="`latest-${option}`" :value="option">{{ option }}개</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-xs font-semibold text-[#657080]">Posts</span>
|
||||
<span class="relative">
|
||||
<select
|
||||
v-model.number="form.postListPageSize"
|
||||
class="h-10 w-full appearance-none rounded-md border border-[#dce0e5] bg-white px-3 pr-10 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
>
|
||||
<option v-for="option in POST_LIST_COUNT_OPTIONS" :key="`posts-${option}`" :value="option">{{ option }}개</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-xs font-semibold text-[#657080]">태그 게시물</span>
|
||||
<span class="relative">
|
||||
<select
|
||||
v-model.number="form.tagPostListPageSize"
|
||||
class="h-10 w-full appearance-none rounded-md border border-[#dce0e5] bg-white px-3 pr-10 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
>
|
||||
<option v-for="option in POST_LIST_COUNT_OPTIONS" :key="`tag-posts-${option}`" :value="option">{{ option }}개</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-xs font-semibold text-[#657080]">Featured</span>
|
||||
<span class="relative">
|
||||
<select
|
||||
v-model.number="form.featuredPostLimit"
|
||||
class="h-10 w-full appearance-none rounded-md border border-[#dce0e5] bg-white px-3 pr-10 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
>
|
||||
<option v-for="option in DISPLAY_COUNT_OPTIONS" :key="`featured-${option}`" :value="option">{{ option }}개</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label class="grid gap-1.5 sm:col-span-2">
|
||||
<span class="text-xs font-semibold text-[#657080]">Pages 섹션</span>
|
||||
<span class="relative">
|
||||
<select
|
||||
v-model.number="form.pageSectionItemLimit"
|
||||
class="h-10 w-full appearance-none rounded-md border border-[#dce0e5] bg-white px-3 pr-10 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
>
|
||||
<option v-for="option in DISPLAY_COUNT_OPTIONS" :key="`pages-${option}`" :value="option">{{ option }}개</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ if (!page.value) {
|
||||
|
||||
onMounted(() => {
|
||||
if (page.value?.renderMode === 'html_document') {
|
||||
window.location.replace(`/pages/${page.value.slug}`)
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,51 @@
|
||||
<script setup>
|
||||
const { data: posts } = await useFetch('/api/posts', {
|
||||
default: () => []
|
||||
import {
|
||||
DEFAULT_POST_LIST_PAGE_SIZE,
|
||||
normalizeDisplayCount,
|
||||
normalizeListPage
|
||||
} from '~/lib/display-count-settings.js'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
default: () => ({})
|
||||
})
|
||||
|
||||
const { data: tags } = await useFetch('/api/tags?tagType=all', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const currentPage = computed(() => normalizeListPage(route.query.page))
|
||||
const postListPageSize = computed(() => normalizeDisplayCount(siteSettings.value?.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE))
|
||||
|
||||
watch(currentPage, () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
})
|
||||
|
||||
const { data: postsResponse } = await useFetch('/api/posts', {
|
||||
query: computed(() => ({
|
||||
paginated: '1',
|
||||
page: currentPage.value,
|
||||
limit: postListPageSize.value
|
||||
})),
|
||||
default: () => ({
|
||||
items: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: DEFAULT_POST_LIST_PAGE_SIZE,
|
||||
total: 0,
|
||||
totalPages: 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 태그 슬러그로 공개 태그 정보를 찾는다.
|
||||
* @param {string | undefined} slug - 태그 슬러그
|
||||
@@ -28,7 +67,14 @@ const getTagMeta = (slug) => {
|
||||
}
|
||||
}
|
||||
|
||||
const postCards = computed(() => posts.value.map((post) => {
|
||||
const pagination = computed(() => postsResponse.value?.pagination || {
|
||||
page: 1,
|
||||
limit: postListPageSize.value,
|
||||
total: 0,
|
||||
totalPages: 1
|
||||
})
|
||||
|
||||
const postCards = computed(() => (postsResponse.value?.items || []).map((post) => {
|
||||
const primaryTag = getTagMeta(post.tags?.[0])
|
||||
|
||||
return {
|
||||
@@ -45,6 +91,16 @@ const postCards = computed(() => posts.value.map((post) => {
|
||||
to: `/post/${post.slug}`
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* 게시물 목록 페이지 링크를 만든다.
|
||||
* @param {number} page - 페이지 번호
|
||||
* @returns {Object} Nuxt 링크 대상
|
||||
*/
|
||||
const getPostsPageTo = (page) => ({
|
||||
path: '/posts',
|
||||
query: page > 1 ? { page } : {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -61,5 +117,33 @@ const postCards = computed(() => posts.value.map((post) => {
|
||||
</section>
|
||||
|
||||
<PostCard v-for="post in postCards" :key="post.to" :post="post" />
|
||||
|
||||
<nav
|
||||
v-if="pagination.totalPages > 1"
|
||||
class="posts-index__pagination site-section"
|
||||
aria-label="게시물 페이지"
|
||||
>
|
||||
<div class="site-section-body mx-auto flex max-w-[720px] items-center justify-between gap-3 text-sm">
|
||||
<NuxtLink
|
||||
class="rounded border border-[var(--site-line)] px-3 py-2 font-semibold transition hover:bg-[var(--site-panel)]"
|
||||
:class="pagination.page <= 1 ? 'pointer-events-none opacity-35' : ''"
|
||||
:to="getPostsPageTo(Math.max(1, pagination.page - 1))"
|
||||
:aria-disabled="pagination.page <= 1"
|
||||
>
|
||||
이전
|
||||
</NuxtLink>
|
||||
<span class="site-muted">
|
||||
{{ pagination.page }} / {{ pagination.totalPages }}
|
||||
</span>
|
||||
<NuxtLink
|
||||
class="rounded border border-[var(--site-line)] px-3 py-2 font-semibold transition hover:bg-[var(--site-panel)]"
|
||||
:class="pagination.page >= pagination.totalPages ? 'pointer-events-none opacity-35' : ''"
|
||||
:to="getPostsPageTo(Math.min(pagination.totalPages, pagination.page + 1))"
|
||||
:aria-disabled="pagination.page >= pagination.totalPages"
|
||||
>
|
||||
다음
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</nav>
|
||||
</MainColumn>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<script setup>
|
||||
import {
|
||||
DEFAULT_TAG_POST_LIST_PAGE_SIZE,
|
||||
normalizeDisplayCount,
|
||||
normalizeListPage
|
||||
} from '~/lib/display-count-settings.js'
|
||||
|
||||
const route = useRoute()
|
||||
const slug = computed(() => String(route.params.slug || ''))
|
||||
|
||||
@@ -6,8 +12,40 @@ const { data: tags } = await useFetch('/api/tags?tagType=all', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const { data: posts } = await useFetch('/api/posts', {
|
||||
default: () => []
|
||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
default: () => ({})
|
||||
})
|
||||
|
||||
const currentPage = computed(() => normalizeListPage(route.query.page))
|
||||
const tagPostListPageSize = computed(() => normalizeDisplayCount(siteSettings.value?.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE))
|
||||
|
||||
watch(currentPage, () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
})
|
||||
|
||||
const { data: postsResponse } = await useFetch('/api/posts', {
|
||||
query: computed(() => ({
|
||||
paginated: '1',
|
||||
tag: slug.value,
|
||||
page: currentPage.value,
|
||||
limit: tagPostListPageSize.value
|
||||
})),
|
||||
default: () => ({
|
||||
items: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: DEFAULT_TAG_POST_LIST_PAGE_SIZE,
|
||||
total: 0,
|
||||
totalPages: 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const tag = computed(() => tags.value.find((item) => item.slug === slug.value))
|
||||
@@ -20,8 +58,14 @@ const hasTagHeaderImage = computed(() => Boolean(tag.value?.headerImageUrl))
|
||||
*/
|
||||
const findTagMeta = (tagSlug) => tags.value.find((item) => item.slug === tagSlug) || null
|
||||
|
||||
const tagPosts = computed(() => posts.value
|
||||
.filter((post) => post.tags.includes(slug.value))
|
||||
const pagination = computed(() => postsResponse.value?.pagination || {
|
||||
page: 1,
|
||||
limit: tagPostListPageSize.value,
|
||||
total: 0,
|
||||
totalPages: 1
|
||||
})
|
||||
|
||||
const tagPosts = computed(() => (postsResponse.value?.items || [])
|
||||
.map((post) => {
|
||||
const primaryTagSlug = post.tags?.[0] || slug.value
|
||||
const primaryTag = findTagMeta(primaryTagSlug)
|
||||
@@ -44,6 +88,16 @@ const tagPosts = computed(() => posts.value
|
||||
to: `/post/${post.slug}`
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* 태그 게시물 목록 페이지 링크를 만든다.
|
||||
* @param {number} page - 페이지 번호
|
||||
* @returns {Object} Nuxt 링크 대상
|
||||
*/
|
||||
const getTagPageTo = (page) => ({
|
||||
path: `/tag/${slug.value}`,
|
||||
query: page > 1 ? { page } : {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -148,6 +202,34 @@ const tagPosts = computed(() => posts.value
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav
|
||||
v-if="pagination.totalPages > 1"
|
||||
class="tag-posts-pagination site-section"
|
||||
aria-label="태그 게시물 페이지"
|
||||
>
|
||||
<div class="site-section-body mx-auto flex max-w-[720px] items-center justify-between gap-3 text-sm">
|
||||
<NuxtLink
|
||||
class="rounded border border-[var(--site-line)] px-3 py-2 font-semibold transition hover:bg-[var(--site-panel)]"
|
||||
:class="pagination.page <= 1 ? 'pointer-events-none opacity-35' : ''"
|
||||
:to="getTagPageTo(Math.max(1, pagination.page - 1))"
|
||||
:aria-disabled="pagination.page <= 1"
|
||||
>
|
||||
이전
|
||||
</NuxtLink>
|
||||
<span class="site-muted">
|
||||
{{ pagination.page }} / {{ pagination.totalPages }}
|
||||
</span>
|
||||
<NuxtLink
|
||||
class="rounded border border-[var(--site-line)] px-3 py-2 font-semibold transition hover:bg-[var(--site-panel)]"
|
||||
:class="pagination.page >= pagination.totalPages ? 'pointer-events-none opacity-35' : ''"
|
||||
:to="getTagPageTo(Math.min(pagination.totalPages, pagination.page + 1))"
|
||||
:aria-disabled="pagination.page >= pagination.totalPages"
|
||||
>
|
||||
다음
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section v-if="tagPosts.length === 0" class="tag-posts-empty site-section">
|
||||
<div class="tag-posts-empty__body site-section-body mx-auto max-w-[720px] text-sm site-muted">
|
||||
이 태그에 연결된 글이 없습니다.
|
||||
|
||||
@@ -3,16 +3,12 @@ const { data: tags } = await useFetch('/api/tags', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const { data: posts } = await useFetch('/api/posts', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
/**
|
||||
* 태그에 연결된 게시물 수 조회
|
||||
* @param {string} slug - 태그 슬러그
|
||||
* @param {Object} tag - 태그
|
||||
* @returns {number} 게시물 수
|
||||
*/
|
||||
const getPostCount = (slug) => posts.value.filter((post) => post.tags.includes(slug)).length
|
||||
const getPostCount = (tag) => Number(tag?.postCount || 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -65,7 +61,7 @@ const getPostCount = (slug) => posts.value.filter((post) => post.tags.includes(s
|
||||
{{ tag.description || `${tag.name} 관련 주제를 확인하세요.` }}
|
||||
</p>
|
||||
<span class="text-[0.8rem] font-medium leading-tight site-muted">
|
||||
{{ getPostCount(tag.slug) }} posts
|
||||
{{ getPostCount(tag) }} posts
|
||||
</span>
|
||||
<span class="pointer-events-none absolute inset-0 rounded-[12px] rounded-l-none border border-transparent transition-colors duration-200 group-hover:border-[color:var(--tag-color)] group-hover:opacity-25" />
|
||||
<span class="pointer-events-none absolute inset-0 rounded-[12px] rounded-l-none opacity-0 transition-opacity duration-200 group-hover:opacity-[0.06]" :style="{ backgroundColor: tag.color }" />
|
||||
|
||||
Reference in New Issue
Block a user