781 lines
18 KiB
JavaScript
781 lines
18 KiB
JavaScript
import {
|
|
getSamplePageBySlug,
|
|
getSamplePages,
|
|
getSamplePostBySlug,
|
|
getSamplePosts,
|
|
getSampleTags
|
|
} from '../utils/sample-content'
|
|
import { getDefaultNavigationItems, groupNavigationItems } from '../utils/navigation-items'
|
|
import { getDefaultSiteSettings } from '../utils/site-settings'
|
|
import { getPostgresClient } from './postgres-client'
|
|
|
|
/**
|
|
* 게시물 행을 API 응답 구조로 변환
|
|
* @param {Object} row - 게시물 행
|
|
* @returns {Object} 게시물 응답
|
|
*/
|
|
const mapPostRow = (row) => ({
|
|
id: row.id,
|
|
title: row.title,
|
|
slug: row.slug,
|
|
content: row.content,
|
|
excerpt: row.excerpt,
|
|
featuredImage: row.featured_image,
|
|
seoTitle: row.seo_title || '',
|
|
seoDescription: row.seo_description || '',
|
|
canonicalUrl: row.canonical_url || '',
|
|
noindex: Boolean(row.noindex),
|
|
status: row.status,
|
|
publishedAt: row.published_at ? row.published_at.toISOString() : null,
|
|
createdAt: row.created_at.toISOString(),
|
|
updatedAt: row.updated_at.toISOString(),
|
|
tags: row.tags || []
|
|
})
|
|
|
|
/**
|
|
* 고정 페이지 행을 API 응답 구조로 변환
|
|
* @param {Object} row - 고정 페이지 행
|
|
* @returns {Object} 고정 페이지 응답
|
|
*/
|
|
const mapPageRow = (row) => ({
|
|
id: row.id,
|
|
title: row.title,
|
|
slug: row.slug,
|
|
content: row.content,
|
|
featuredImage: row.featured_image,
|
|
createdAt: row.created_at.toISOString(),
|
|
updatedAt: row.updated_at.toISOString()
|
|
})
|
|
|
|
/**
|
|
* 태그 행을 API 응답 구조로 변환
|
|
* @param {Object} row - 태그 행
|
|
* @returns {Object} 태그 응답
|
|
*/
|
|
const mapTagRow = (row) => ({
|
|
id: row.id,
|
|
name: row.name,
|
|
slug: row.slug,
|
|
description: row.description,
|
|
sortOrder: row.sort_order,
|
|
color: row.color
|
|
})
|
|
|
|
/**
|
|
* 사이트 설정 행을 API 응답 구조로 변환
|
|
* @param {Object} row - 사이트 설정 행
|
|
* @returns {Object} 사이트 설정 응답
|
|
*/
|
|
const mapSiteSettingsRow = (row) => ({
|
|
title: row.title,
|
|
description: row.description,
|
|
siteUrl: row.site_url,
|
|
logoText: row.logo_text,
|
|
copyrightText: row.copyright_text,
|
|
updatedAt: row.updated_at.toISOString()
|
|
})
|
|
|
|
/**
|
|
* 네비게이션 행을 API 응답 구조로 변환
|
|
* @param {Object} row - 네비게이션 행
|
|
* @returns {Object} 네비게이션 응답
|
|
*/
|
|
const mapNavigationItemRow = (row) => ({
|
|
id: row.id,
|
|
label: row.label,
|
|
url: row.url,
|
|
location: row.location,
|
|
sortOrder: row.sort_order,
|
|
isVisible: row.is_visible,
|
|
createdAt: row.created_at.toISOString(),
|
|
updatedAt: row.updated_at.toISOString()
|
|
})
|
|
|
|
/**
|
|
* 태그 슬러그 목록 정규화
|
|
* @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>} 게시물 목록
|
|
*/
|
|
export const listPosts = 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
|
|
WHERE posts.status = 'published'
|
|
AND (
|
|
posts.published_at IS NULL
|
|
OR posts.published_at <= now()
|
|
)
|
|
GROUP BY posts.id
|
|
ORDER BY posts.published_at DESC NULLS LAST, posts.created_at DESC
|
|
`
|
|
|
|
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,
|
|
seo_title,
|
|
seo_description,
|
|
canonical_url,
|
|
noindex,
|
|
status,
|
|
published_at
|
|
)
|
|
VALUES (
|
|
${input.title},
|
|
${input.slug},
|
|
${input.content},
|
|
${input.excerpt},
|
|
${input.featuredImage},
|
|
${input.seoTitle},
|
|
${input.seoDescription},
|
|
${input.canonicalUrl},
|
|
${input.noindex},
|
|
${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},
|
|
seo_title = ${input.seoTitle},
|
|
seo_description = ${input.seoDescription},
|
|
canonical_url = ${input.canonicalUrl},
|
|
noindex = ${input.noindex},
|
|
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 - 게시물 슬러그
|
|
* @returns {Promise<Object | null>} 게시물 상세
|
|
*/
|
|
export const getPostBySlug = async (slug) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
return getSamplePostBySlug(slug)
|
|
}
|
|
|
|
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.slug = ${slug}
|
|
AND posts.status = 'published'
|
|
AND (
|
|
posts.published_at IS NULL
|
|
OR posts.published_at <= now()
|
|
)
|
|
GROUP BY posts.id
|
|
LIMIT 1
|
|
`
|
|
|
|
return rows[0] ? mapPostRow(rows[0]) : null
|
|
}
|
|
|
|
/**
|
|
* 공개 고정 페이지 목록 조회
|
|
* @returns {Promise<Array>} 고정 페이지 목록
|
|
*/
|
|
export const listPages = async () => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
return getSamplePages()
|
|
}
|
|
|
|
const rows = await sql`
|
|
SELECT *
|
|
FROM pages
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
return rows.map(mapPageRow)
|
|
}
|
|
|
|
/**
|
|
* 관리자 고정 페이지 목록 조회
|
|
* @returns {Promise<Array>} 관리자 고정 페이지 목록
|
|
*/
|
|
export const listAdminPages = async () => listPages()
|
|
|
|
/**
|
|
* 관리자 고정 페이지 상세 조회
|
|
* @param {string} id - 페이지 ID
|
|
* @returns {Promise<Object | null>} 관리자 고정 페이지 상세
|
|
*/
|
|
export const getAdminPageById = async (id) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
return getSamplePages().find((page) => page.id === id) || null
|
|
}
|
|
|
|
const rows = await sql`
|
|
SELECT *
|
|
FROM pages
|
|
WHERE id = ${id}
|
|
LIMIT 1
|
|
`
|
|
|
|
return rows[0] ? mapPageRow(rows[0]) : null
|
|
}
|
|
|
|
/**
|
|
* 관리자 고정 페이지 생성
|
|
* @param {Object} input - 페이지 입력값
|
|
* @returns {Promise<Object>} 생성된 페이지
|
|
*/
|
|
export const createAdminPage = async (input) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
throw new Error('DATABASE_REQUIRED')
|
|
}
|
|
|
|
const rows = await sql`
|
|
INSERT INTO pages (
|
|
title,
|
|
slug,
|
|
content,
|
|
featured_image
|
|
)
|
|
VALUES (
|
|
${input.title},
|
|
${input.slug},
|
|
${input.content},
|
|
${input.featuredImage}
|
|
)
|
|
RETURNING *
|
|
`
|
|
|
|
return mapPageRow(rows[0])
|
|
}
|
|
|
|
/**
|
|
* 관리자 고정 페이지 수정
|
|
* @param {string} id - 페이지 ID
|
|
* @param {Object} input - 페이지 입력값
|
|
* @returns {Promise<Object | null>} 수정된 페이지
|
|
*/
|
|
export const updateAdminPage = async (id, input) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
throw new Error('DATABASE_REQUIRED')
|
|
}
|
|
|
|
const rows = await sql`
|
|
UPDATE pages
|
|
SET
|
|
title = ${input.title},
|
|
slug = ${input.slug},
|
|
content = ${input.content},
|
|
featured_image = ${input.featuredImage},
|
|
updated_at = now()
|
|
WHERE id = ${id}
|
|
RETURNING *
|
|
`
|
|
|
|
return rows[0] ? mapPageRow(rows[0]) : null
|
|
}
|
|
|
|
/**
|
|
* 관리자 고정 페이지 삭제
|
|
* @param {string} id - 페이지 ID
|
|
* @returns {Promise<boolean>} 삭제 여부
|
|
*/
|
|
export const deleteAdminPage = async (id) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
throw new Error('DATABASE_REQUIRED')
|
|
}
|
|
|
|
const rows = await sql`
|
|
DELETE FROM pages
|
|
WHERE id = ${id}
|
|
RETURNING id
|
|
`
|
|
|
|
return Boolean(rows[0])
|
|
}
|
|
|
|
/**
|
|
* 공개 고정 페이지 상세 조회
|
|
* @param {string} slug - 페이지 슬러그
|
|
* @returns {Promise<Object | null>} 고정 페이지 상세
|
|
*/
|
|
export const getPageBySlug = async (slug) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
return getSamplePageBySlug(slug)
|
|
}
|
|
|
|
const rows = await sql`
|
|
SELECT *
|
|
FROM pages
|
|
WHERE slug = ${slug}
|
|
LIMIT 1
|
|
`
|
|
|
|
return rows[0] ? mapPageRow(rows[0]) : null
|
|
}
|
|
|
|
/**
|
|
* 공개 태그 목록 조회
|
|
* @returns {Promise<Array>} 태그 목록
|
|
*/
|
|
export const listTags = async () => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
return getSampleTags()
|
|
}
|
|
|
|
const rows = await sql`
|
|
SELECT *
|
|
FROM tags
|
|
ORDER BY sort_order ASC, name ASC
|
|
`
|
|
|
|
return rows.map(mapTagRow)
|
|
}
|
|
|
|
/**
|
|
* 사이트 설정 조회
|
|
* @returns {Promise<Object>} 사이트 설정
|
|
*/
|
|
export const getSiteSettings = async () => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
return getDefaultSiteSettings()
|
|
}
|
|
|
|
const rows = await sql`
|
|
SELECT *
|
|
FROM site_settings
|
|
WHERE id = 1
|
|
LIMIT 1
|
|
`
|
|
|
|
return rows[0] ? mapSiteSettingsRow(rows[0]) : getDefaultSiteSettings()
|
|
}
|
|
|
|
/**
|
|
* 관리자 사이트 설정 수정
|
|
* @param {Object} input - 사이트 설정 입력값
|
|
* @returns {Promise<Object>} 수정된 사이트 설정
|
|
*/
|
|
export const updateSiteSettings = async (input) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
throw new Error('DATABASE_REQUIRED')
|
|
}
|
|
|
|
const rows = await sql`
|
|
INSERT INTO site_settings (
|
|
id,
|
|
title,
|
|
description,
|
|
site_url,
|
|
logo_text,
|
|
copyright_text,
|
|
updated_at
|
|
)
|
|
VALUES (
|
|
1,
|
|
${input.title},
|
|
${input.description},
|
|
${input.siteUrl},
|
|
${input.logoText},
|
|
${input.copyrightText},
|
|
now()
|
|
)
|
|
ON CONFLICT (id) DO UPDATE
|
|
SET
|
|
title = EXCLUDED.title,
|
|
description = EXCLUDED.description,
|
|
site_url = EXCLUDED.site_url,
|
|
logo_text = EXCLUDED.logo_text,
|
|
copyright_text = EXCLUDED.copyright_text,
|
|
updated_at = now()
|
|
RETURNING *
|
|
`
|
|
|
|
return mapSiteSettingsRow(rows[0])
|
|
}
|
|
|
|
/**
|
|
* 네비게이션 항목 목록 조회
|
|
* @param {Object} options - 조회 옵션
|
|
* @param {boolean} options.visibleOnly - 표시 항목만 조회할지 여부
|
|
* @returns {Promise<Array>} 네비게이션 항목 목록
|
|
*/
|
|
export const listNavigationItems = async ({ visibleOnly = false } = {}) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
return getDefaultNavigationItems()
|
|
.filter((item) => !visibleOnly || item.isVisible)
|
|
}
|
|
|
|
const rows = visibleOnly
|
|
? await sql`
|
|
SELECT *
|
|
FROM navigation_items
|
|
WHERE is_visible = true
|
|
ORDER BY location ASC, sort_order ASC, label ASC
|
|
`
|
|
: await sql`
|
|
SELECT *
|
|
FROM navigation_items
|
|
ORDER BY location ASC, sort_order ASC, label ASC
|
|
`
|
|
|
|
return rows.map(mapNavigationItemRow)
|
|
}
|
|
|
|
/**
|
|
* 공개 네비게이션 조회
|
|
* @returns {Promise<{primary: Array<Object>, footer: Array<Object>}>} 위치별 공개 네비게이션
|
|
*/
|
|
export const getPublicNavigation = async () => groupNavigationItems(await listNavigationItems({ visibleOnly: true }))
|
|
|
|
/**
|
|
* 관리자 네비게이션 항목 일괄 저장
|
|
* @param {Array<Object>} items - 저장할 네비게이션 항목 목록
|
|
* @returns {Promise<Array>} 저장된 네비게이션 항목 목록
|
|
*/
|
|
export const updateNavigationItems = async (items) => {
|
|
const sql = getPostgresClient()
|
|
|
|
if (!sql) {
|
|
throw new Error('DATABASE_REQUIRED')
|
|
}
|
|
|
|
await sql.begin(async (transaction) => {
|
|
await transaction`
|
|
DELETE FROM navigation_items
|
|
`
|
|
|
|
for (const item of items) {
|
|
await transaction`
|
|
INSERT INTO navigation_items (
|
|
label,
|
|
url,
|
|
location,
|
|
sort_order,
|
|
is_visible
|
|
)
|
|
VALUES (
|
|
${item.label},
|
|
${item.url},
|
|
${item.location},
|
|
${item.sortOrder},
|
|
${item.isVisible}
|
|
)
|
|
`
|
|
}
|
|
})
|
|
|
|
return listNavigationItems()
|
|
}
|
|
|
|
/**
|
|
* 관리자 태그 상세 조회
|
|
* @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])
|
|
}
|