From 27689757524a60b06612619115344de3c00e5d0c Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 15 May 2026 11:49:12 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=EA=B3=BC=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=95=84=ED=84=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminPostForm.vue | 29 ++++- components/site/LeftSidebar.vue | 5 +- components/site/RightSidebar.vue | 5 +- components/site/SiteHeader.vue | 9 +- db/migrations/023_add_post_featured.sql | 5 + docs/changelog.md | 6 + docs/deploy.md | 3 + docs/history.md | 10 ++ docs/map.md | 12 +- docs/spec.md | 4 + docs/update.md | 11 ++ package-lock.json | 4 +- package.json | 2 +- pages/admin/posts/index.vue | 146 +++++++++++++++++++--- pages/index.vue | 12 +- pages/post/[slug].vue | 2 +- pages/tag/[slug].vue | 7 +- server/repositories/content-repository.js | 29 +++++ server/utils/admin-post-input.js | 1 + server/utils/content-schema.js | 2 + 20 files changed, 258 insertions(+), 46 deletions(-) create mode 100644 db/migrations/023_add_post_featured.sql diff --git a/components/admin/AdminPostForm.vue b/components/admin/AdminPostForm.vue index e29c0d5..7b1f0a7 100644 --- a/components/admin/AdminPostForm.vue +++ b/components/admin/AdminPostForm.vue @@ -105,6 +105,7 @@ const form = reactive({ excerpt: props.initialPost.excerpt || '', content: normalizeMarkdownContent(props.initialPost.content), featuredImage: props.initialPost.featuredImage || '', + isFeatured: Boolean(props.initialPost.isFeatured), noindex: Boolean(props.initialPost.noindex), status: props.initialPost.status || 'draft', publishedAt: toDateTimeLocalValue(props.initialPost.publishedAt), @@ -301,6 +302,7 @@ const createPostPayload = () => { excerpt: form.excerpt.trim(), content: normalizeMarkdownContent(form.content), featuredImage: form.featuredImage.trim() || null, + isFeatured: form.isFeatured, seoTitle: form.title.trim(), seoDescription: form.excerpt.trim(), canonicalUrl: '', @@ -330,6 +332,7 @@ const createAutosavePayload = () => ({ excerpt: form.excerpt, content: normalizeMarkdownContent(form.content), featuredImage: form.featuredImage, + isFeatured: form.isFeatured, noindex: form.noindex, status: form.status, publishedAt: form.publishedAt, @@ -347,7 +350,8 @@ const isEmptyAutosavePayload = (payload) => ![ payload.excerpt, payload.content, payload.featuredImage, - payload.tagsText + payload.tagsText, + payload.isFeatured ? 'featured' : '' ].some((value) => String(value || '').trim()) /** @@ -1026,6 +1030,29 @@ defineExpose({ + +

diff --git a/components/site/LeftSidebar.vue b/components/site/LeftSidebar.vue index 8a9adc3..be191e3 100644 --- a/components/site/LeftSidebar.vue +++ b/components/site/LeftSidebar.vue @@ -19,6 +19,9 @@ const { data: navigation } = await useFetch('/api/navigation', { }) }) +/** 저자 영역 공개 여부 */ +const showAuthorSection = false + const STORAGE_KEY = 'sori-primary-nav-expanded' /** @@ -173,7 +176,7 @@ onMounted(() => {

- diff --git a/server/repositories/content-repository.js b/server/repositories/content-repository.js index 70c28b6..59c2605 100644 --- a/server/repositories/content-repository.js +++ b/server/repositories/content-repository.js @@ -22,6 +22,8 @@ const mapPostRow = (row) => ({ content: row.content, excerpt: row.excerpt, featuredImage: row.featured_image, + isFeatured: Boolean(row.is_featured), + commentCount: Number(row.comment_count || 0), seoTitle: row.seo_title || '', seoDescription: row.seo_description || '', canonicalUrl: row.canonical_url || '', @@ -193,6 +195,12 @@ export const listPosts = async () => { const rows = await sql` SELECT posts.*, + ( + SELECT COUNT(*)::int + FROM comments + WHERE comments.post_id = posts.id + AND comments.status = 'published' + ) AS comment_count, COALESCE(array_agg(tags.slug) FILTER (WHERE tags.slug IS NOT NULL), '{}') AS tags FROM posts LEFT JOIN post_tags ON post_tags.post_id = posts.id @@ -223,6 +231,12 @@ export const listAdminPosts = async () => { const rows = await sql` SELECT posts.*, + ( + SELECT COUNT(*)::int + FROM comments + WHERE comments.post_id = posts.id + AND comments.status = 'published' + ) AS comment_count, COALESCE(array_agg(tags.slug) FILTER (WHERE tags.slug IS NOT NULL), '{}') AS tags FROM posts LEFT JOIN post_tags ON post_tags.post_id = posts.id @@ -249,6 +263,12 @@ export const getAdminPostById = async (id) => { const rows = await sql` SELECT posts.*, + ( + SELECT COUNT(*)::int + FROM comments + WHERE comments.post_id = posts.id + AND comments.status = 'published' + ) AS comment_count, COALESCE(array_agg(tags.slug) FILTER (WHERE tags.slug IS NOT NULL), '{}') AS tags FROM posts LEFT JOIN post_tags ON post_tags.post_id = posts.id @@ -281,6 +301,7 @@ export const createAdminPost = async (input) => { content, excerpt, featured_image, + is_featured, seo_title, seo_description, canonical_url, @@ -295,6 +316,7 @@ export const createAdminPost = async (input) => { ${input.content}, ${input.excerpt}, ${input.featuredImage}, + ${input.isFeatured}, ${input.seoTitle}, ${input.seoDescription}, ${input.canonicalUrl}, @@ -336,6 +358,7 @@ export const updateAdminPost = async (id, input) => { content = ${input.content}, excerpt = ${input.excerpt}, featured_image = ${input.featuredImage}, + is_featured = ${input.isFeatured}, seo_title = ${input.seoTitle}, seo_description = ${input.seoDescription}, canonical_url = ${input.canonicalUrl}, @@ -396,6 +419,12 @@ export const getPostBySlug = async (slug) => { const rows = await sql` SELECT posts.*, + ( + SELECT COUNT(*)::int + FROM comments + WHERE comments.post_id = posts.id + AND comments.status = 'published' + ) AS comment_count, COALESCE(array_agg(tags.slug) FILTER (WHERE tags.slug IS NOT NULL), '{}') AS tags FROM posts LEFT JOIN post_tags ON post_tags.post_id = posts.id diff --git a/server/utils/admin-post-input.js b/server/utils/admin-post-input.js index a4aa1fc..10a01ba 100644 --- a/server/utils/admin-post-input.js +++ b/server/utils/admin-post-input.js @@ -8,6 +8,7 @@ export const adminPostInputSchema = z.object({ content: z.preprocess(normalizeMarkdownContent, z.string()).default(''), excerpt: z.string().default(''), featuredImage: z.string().trim().nullable().default(null), + isFeatured: z.boolean().default(false), seoTitle: z.string().trim().default(''), seoDescription: z.string().trim().default(''), canonicalUrl: z.string().trim().url().or(z.literal('')).default(''), diff --git a/server/utils/content-schema.js b/server/utils/content-schema.js index 4fba16b..dd61a95 100644 --- a/server/utils/content-schema.js +++ b/server/utils/content-schema.js @@ -9,6 +9,8 @@ export const postSchema = z.object({ content: z.string(), excerpt: z.string().default(''), featuredImage: z.string().nullable().default(null), + isFeatured: z.boolean().default(false), + commentCount: z.number().int().default(0), seoTitle: z.string().default(''), seoDescription: z.string().default(''), canonicalUrl: z.string().default(''),