관리자 기능과 태그 표시 설정 추가
This commit is contained in:
@@ -50,9 +50,63 @@ const mapTagRow = (row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
slug: row.slug,
|
||||
description: row.description
|
||||
description: row.description,
|
||||
sortOrder: row.sort_order,
|
||||
color: row.color
|
||||
})
|
||||
|
||||
/**
|
||||
* 태그 슬러그 목록 정규화
|
||||
* @param {Array<string>} tags - 태그 슬러그 목록
|
||||
* @returns {Array<string>} 정규화된 태그 슬러그 목록
|
||||
*/
|
||||
const normalizeTagSlugs = (tags = []) => [...new Set(tags
|
||||
.map((tag) => String(tag).trim().toLowerCase())
|
||||
.filter(Boolean))]
|
||||
|
||||
/**
|
||||
* 태그 슬러그를 태그명으로 변환
|
||||
* @param {string} slug - 태그 슬러그
|
||||
* @returns {string} 태그명
|
||||
*/
|
||||
const getTagNameFromSlug = (slug) => slug
|
||||
.split('-')
|
||||
.filter(Boolean)
|
||||
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
||||
.join(' ')
|
||||
|
||||
/**
|
||||
* 게시물 태그 연결 저장
|
||||
* @param {Function} sql - PostgreSQL 트랜잭션 클라이언트
|
||||
* @param {string} postId - 게시물 ID
|
||||
* @param {Array<string>} tags - 태그 슬러그 목록
|
||||
* @returns {Promise<void>} 저장 결과
|
||||
*/
|
||||
const syncPostTags = async (sql, postId, tags) => {
|
||||
const tagSlugs = normalizeTagSlugs(tags)
|
||||
|
||||
await sql`
|
||||
DELETE FROM post_tags
|
||||
WHERE post_id = ${postId}
|
||||
`
|
||||
|
||||
for (const slug of tagSlugs) {
|
||||
const tagRows = await sql`
|
||||
INSERT INTO tags (name, slug)
|
||||
VALUES (${getTagNameFromSlug(slug)}, ${slug})
|
||||
ON CONFLICT (slug) DO UPDATE
|
||||
SET updated_at = now()
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
await sql`
|
||||
INSERT INTO post_tags (post_id, tag_id)
|
||||
VALUES (${postId}, ${tagRows[0].id})
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공개 게시물 목록 조회
|
||||
* @returns {Promise<Array>} 게시물 목록
|
||||
@@ -79,6 +133,163 @@ export const listPosts = async () => {
|
||||
return rows.map(mapPostRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 게시물 목록 조회
|
||||
* @returns {Promise<Array>} 관리자 게시물 목록
|
||||
*/
|
||||
export const listAdminPosts = async () => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
return getSamplePosts()
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
SELECT
|
||||
posts.*,
|
||||
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
|
||||
LEFT JOIN tags ON tags.id = post_tags.tag_id
|
||||
GROUP BY posts.id
|
||||
ORDER BY posts.updated_at DESC
|
||||
`
|
||||
|
||||
return rows.map(mapPostRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 게시물 상세 조회
|
||||
* @param {string} id - 게시물 ID
|
||||
* @returns {Promise<Object | null>} 관리자 게시물 상세
|
||||
*/
|
||||
export const getAdminPostById = async (id) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
return getSamplePosts().find((post) => post.id === id) || null
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
SELECT
|
||||
posts.*,
|
||||
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
|
||||
LEFT JOIN tags ON tags.id = post_tags.tag_id
|
||||
WHERE posts.id = ${id}
|
||||
GROUP BY posts.id
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
return rows[0] ? mapPostRow(rows[0]) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 게시물 생성
|
||||
* @param {Object} input - 게시물 입력값
|
||||
* @returns {Promise<Object>} 생성된 게시물
|
||||
*/
|
||||
export const createAdminPost = async (input) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
throw new Error('DATABASE_REQUIRED')
|
||||
}
|
||||
|
||||
const rows = await sql.begin(async (transaction) => {
|
||||
const insertedRows = await transaction`
|
||||
INSERT INTO posts (
|
||||
title,
|
||||
slug,
|
||||
content,
|
||||
excerpt,
|
||||
featured_image,
|
||||
status,
|
||||
published_at
|
||||
)
|
||||
VALUES (
|
||||
${input.title},
|
||||
${input.slug},
|
||||
${input.content},
|
||||
${input.excerpt},
|
||||
${input.featuredImage},
|
||||
${input.status},
|
||||
${input.publishedAt}
|
||||
)
|
||||
RETURNING *
|
||||
`
|
||||
|
||||
await syncPostTags(transaction, insertedRows[0].id, input.tags)
|
||||
|
||||
return insertedRows
|
||||
})
|
||||
|
||||
return getAdminPostById(rows[0].id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 게시물 수정
|
||||
* @param {string} id - 게시물 ID
|
||||
* @param {Object} input - 게시물 입력값
|
||||
* @returns {Promise<Object | null>} 수정된 게시물
|
||||
*/
|
||||
export const updateAdminPost = async (id, input) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
throw new Error('DATABASE_REQUIRED')
|
||||
}
|
||||
|
||||
const rows = await sql.begin(async (transaction) => {
|
||||
const updatedRows = await transaction`
|
||||
UPDATE posts
|
||||
SET
|
||||
title = ${input.title},
|
||||
slug = ${input.slug},
|
||||
content = ${input.content},
|
||||
excerpt = ${input.excerpt},
|
||||
featured_image = ${input.featuredImage},
|
||||
status = ${input.status},
|
||||
published_at = ${input.publishedAt},
|
||||
updated_at = now()
|
||||
WHERE id = ${id}
|
||||
RETURNING *
|
||||
`
|
||||
|
||||
if (!updatedRows[0]) {
|
||||
return []
|
||||
}
|
||||
|
||||
await syncPostTags(transaction, id, input.tags)
|
||||
|
||||
return updatedRows
|
||||
})
|
||||
|
||||
return rows[0] ? getAdminPostById(rows[0].id) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 게시물 삭제
|
||||
* @param {string} id - 게시물 ID
|
||||
* @returns {Promise<boolean>} 삭제 여부
|
||||
*/
|
||||
export const deleteAdminPost = async (id) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
throw new Error('DATABASE_REQUIRED')
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
DELETE FROM posts
|
||||
WHERE id = ${id}
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
return Boolean(rows[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* 공개 게시물 상세 조회
|
||||
* @param {string} slug - 게시물 슬러그
|
||||
@@ -163,8 +374,101 @@ export const listTags = async () => {
|
||||
const rows = await sql`
|
||||
SELECT *
|
||||
FROM tags
|
||||
ORDER BY name ASC
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
`
|
||||
|
||||
return rows.map(mapTagRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 태그 상세 조회
|
||||
* @param {string} id - 태그 ID
|
||||
* @returns {Promise<Object | null>} 태그 상세
|
||||
*/
|
||||
export const getAdminTagById = async (id) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
return getSampleTags().find((tag) => tag.id === id) || null
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE id = ${id}
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
return rows[0] ? mapTagRow(rows[0]) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 태그 생성
|
||||
* @param {Object} input - 태그 입력값
|
||||
* @returns {Promise<Object>} 생성된 태그
|
||||
*/
|
||||
export const createAdminTag = async (input) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
throw new Error('DATABASE_REQUIRED')
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
INSERT INTO tags (name, slug, description, sort_order, color)
|
||||
VALUES (${input.name}, ${input.slug}, ${input.description}, ${input.sortOrder}, ${input.color})
|
||||
RETURNING *
|
||||
`
|
||||
|
||||
return mapTagRow(rows[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 태그 수정
|
||||
* @param {string} id - 태그 ID
|
||||
* @param {Object} input - 태그 입력값
|
||||
* @returns {Promise<Object | null>} 수정된 태그
|
||||
*/
|
||||
export const updateAdminTag = async (id, input) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
throw new Error('DATABASE_REQUIRED')
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
UPDATE tags
|
||||
SET
|
||||
name = ${input.name},
|
||||
slug = ${input.slug},
|
||||
description = ${input.description},
|
||||
sort_order = ${input.sortOrder},
|
||||
color = ${input.color},
|
||||
updated_at = now()
|
||||
WHERE id = ${id}
|
||||
RETURNING *
|
||||
`
|
||||
|
||||
return rows[0] ? mapTagRow(rows[0]) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 태그 삭제
|
||||
* @param {string} id - 태그 ID
|
||||
* @returns {Promise<boolean>} 삭제 여부
|
||||
*/
|
||||
export const deleteAdminTag = async (id) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
throw new Error('DATABASE_REQUIRED')
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
DELETE FROM tags
|
||||
WHERE id = ${id}
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
return Boolean(rows[0])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user