게시물과 페이지 공개 상태 확장 v1.5.4

This commit is contained in:
2026-05-26 16:07:10 +09:00
parent b989193dab
commit 6333c4254f
20 changed files with 252 additions and 38 deletions

View File

@@ -1,7 +1,11 @@
import { listPosts } from '../repositories/content-repository'
import { getMemberSession } from '../utils/member-auth'
/**
* 공개 게시물 목록 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Array} 게시물 목록
*/
export default defineEventHandler(() => listPosts())
export default defineEventHandler((event) => listPosts({
includeMembers: Boolean(getMemberSession(event))
}))

View File

@@ -1,4 +1,5 @@
import { getPostBySlug } from '../../repositories/content-repository'
import { getMemberSession } from '../../utils/member-auth'
/**
* 공개 게시물 상세 API
@@ -7,7 +8,9 @@ import { getPostBySlug } from '../../repositories/content-repository'
*/
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const post = await getPostBySlug(slug)
const post = await getPostBySlug(slug, {
includeMembers: Boolean(getMemberSession(event))
})
if (!post) {
throw createError({

View File

@@ -35,7 +35,7 @@ const mapPostRow = (row) => ({
canonicalUrl: row.canonical_url || '',
noindex: Boolean(row.noindex),
ogImage: row.og_image || null,
status: row.status === 'private' ? 'draft' : row.status,
status: row.status,
publishedAt: row.published_at ? row.published_at.toISOString() : null,
createdAt: row.created_at.toISOString(),
updatedAt: row.updated_at.toISOString(),
@@ -63,6 +63,7 @@ const mapPageRow = (row) => ({
slug: row.slug,
content: row.content,
renderMode: row.render_mode || 'markdown',
status: row.status || 'published',
featuredImage: row.featured_image,
createdAt: row.created_at.toISOString(),
updatedAt: row.updated_at.toISOString()
@@ -210,9 +211,10 @@ const syncPostTags = async (sql, postId, tags) => {
/**
* 공개 게시물 목록 조회
* @param {{ includeMembers?: boolean }} [options] - 회원 전용 글 포함 여부
* @returns {Promise<Array>} 게시물 목록
*/
export const listPosts = async () => {
export const listPosts = async ({ includeMembers = false } = {}) => {
const sql = getPostgresClient()
if (!sql) {
@@ -232,9 +234,13 @@ export const listPosts = async () => {
FROM posts
LEFT JOIN post_tags ON post_tags.post_id = posts.id
LEFT JOIN tags ON tags.id = post_tags.tag_id
WHERE posts.status = 'published'
WHERE (
posts.status = 'published'
OR (${includeMembers} = true AND posts.status = 'members')
)
AND (
posts.published_at IS NULL
posts.status = 'members'
OR posts.published_at IS NULL
OR posts.published_at <= now()
)
GROUP BY posts.id
@@ -439,9 +445,10 @@ export const deleteAdminPost = async (id) => {
/**
* 공개 게시물 상세 조회
* @param {string} slug - 게시물 슬러그
* @param {{ includeMembers?: boolean }} [options] - 회원 전용 글 포함 여부
* @returns {Promise<Object | null>} 게시물 상세
*/
export const getPostBySlug = async (slug) => {
export const getPostBySlug = async (slug, { includeMembers = false } = {}) => {
const sql = getPostgresClient()
if (!sql) {
@@ -462,9 +469,13 @@ export const getPostBySlug = async (slug) => {
LEFT JOIN post_tags ON post_tags.post_id = posts.id
LEFT JOIN tags ON tags.id = post_tags.tag_id
WHERE posts.slug = ${slug}
AND posts.status = 'published'
AND (
posts.published_at IS NULL
posts.status = 'published'
OR (${includeMembers} = true AND posts.status = 'members')
)
AND (
posts.status = 'members'
OR posts.published_at IS NULL
OR posts.published_at <= now()
)
GROUP BY posts.id
@@ -488,6 +499,7 @@ export const listPages = async () => {
const rows = await sql`
SELECT *
FROM pages
WHERE status = 'published'
ORDER BY created_at DESC
`
@@ -498,7 +510,21 @@ export const listPages = async () => {
* 관리자 고정 페이지 목록 조회
* @returns {Promise<Array>} 관리자 고정 페이지 목록
*/
export const listAdminPages = async () => listPages()
export const listAdminPages = async () => {
const sql = getPostgresClient()
if (!sql) {
return getSamplePages()
}
const rows = await sql`
SELECT *
FROM pages
ORDER BY created_at DESC
`
return rows.map(mapPageRow)
}
/**
* 관리자 고정 페이지 상세 조회
@@ -540,14 +566,16 @@ export const createAdminPage = async (input) => {
slug,
content,
render_mode,
featured_image
featured_image,
status
)
VALUES (
${input.title},
${input.slug},
${input.content},
${input.renderMode},
${input.featuredImage}
${input.featuredImage},
${input.status}
)
RETURNING *
`
@@ -576,6 +604,7 @@ export const updateAdminPage = async (id, input) => {
content = ${input.content},
render_mode = ${input.renderMode},
featured_image = ${input.featuredImage},
status = ${input.status},
updated_at = now()
WHERE id = ${id}
RETURNING *
@@ -621,6 +650,7 @@ export const getPageBySlug = async (slug) => {
SELECT *
FROM pages
WHERE slug = ${slug}
AND status = 'published'
LIMIT 1
`

View File

@@ -1,10 +1,12 @@
import { z } from 'zod'
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
import { pageStatusSchema } from './content-schema.js'
export const adminPageInputSchema = z.object({
title: z.string().trim().min(1),
slug: z.string().trim().min(1).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
renderMode: z.enum(['markdown', 'html_document']).default('html_document'),
status: pageStatusSchema.default('published'),
content: z.string().default(''),
featuredImage: z.string().trim().nullable().default(null)
}).transform((input) => ({

View File

@@ -18,7 +18,7 @@ export const adminPostInputSchema = z.object({
canonicalUrl: z.string().trim().url().or(z.literal('')).default(''),
noindex: z.boolean().default(false),
ogImage: z.string().trim().nullable().default(null),
status: z.preprocess((val) => (val === 'private' ? 'draft' : val), postStatusSchema).default('draft'),
status: postStatusSchema.default('draft'),
publishedAt: z.string().datetime().nullable().default(null),
tags: z.array(z.string().trim().min(1)).default([])
})

View File

@@ -1,6 +1,7 @@
import { z } from 'zod'
export const postStatusSchema = z.enum(['published', 'draft'])
export const postStatusSchema = z.enum(['published', 'draft', 'members', 'private'])
export const pageStatusSchema = z.enum(['published', 'draft', 'private'])
export const postSchema = z.object({
id: z.string().uuid(),
@@ -29,6 +30,7 @@ export const pageSchema = z.object({
slug: z.string().min(1),
content: z.string(),
renderMode: z.enum(['markdown', 'html_document']).default('markdown'),
status: pageStatusSchema.default('published'),
featuredImage: z.string().nullable().default(null),
createdAt: z.string(),
updatedAt: z.string()