페이지 섹션·공개 목록 설정 누적 변경 일괄 반영

페이지 섹션 관리와 홈 Pages 진열장 연동, 공개 목록 표시개수 설정 관련 누적 로컬 변경을 기능 단위로 정리해 반영한다.
문서 이력과 패키지 버전을 v1.9.23으로 맞춰 코드와 문서 상태를 동기화한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 18:48:42 +09:00
parent 6a3cc5d7ec
commit 00999404a4
37 changed files with 2681 additions and 123 deletions

View File

@@ -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>