PostgreSQL 데이터 계층 추가

This commit is contained in:
2026-04-29 15:22:54 +09:00
parent cbf5ed6c8c
commit 5ee6fcd54b
20 changed files with 429 additions and 34 deletions

View File

@@ -1,7 +1,7 @@
import { getSamplePages } from '../utils/sample-content'
import { listPages } from '../repositories/content-repository'
/**
* 공개 고정 페이지 목록 API
* @returns {Array} 고정 페이지 목록
*/
export default defineEventHandler(() => getSamplePages())
export default defineEventHandler(() => listPages())

View File

@@ -1,13 +1,13 @@
import { getSamplePageBySlug } from '../../utils/sample-content'
import { getPageBySlug } from '../../repositories/content-repository'
/**
* 공개 고정 페이지 상세 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Object} 고정 페이지 상세
*/
export default defineEventHandler((event) => {
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const page = getSamplePageBySlug(slug)
const page = await getPageBySlug(slug)
if (!page) {
throw createError({

View File

@@ -1,7 +1,7 @@
import { getSamplePosts } from '../utils/sample-content'
import { listPosts } from '../repositories/content-repository'
/**
* 공개 게시물 목록 API
* @returns {Array} 게시물 목록
*/
export default defineEventHandler(() => getSamplePosts())
export default defineEventHandler(() => listPosts())

View File

@@ -1,13 +1,13 @@
import { getSamplePostBySlug } from '../../utils/sample-content'
import { getPostBySlug } from '../../repositories/content-repository'
/**
* 공개 게시물 상세 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Object} 게시물 상세
*/
export default defineEventHandler((event) => {
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const post = getSamplePostBySlug(slug)
const post = await getPostBySlug(slug)
if (!post) {
throw createError({

View File

@@ -1,7 +1,7 @@
import { getSampleTags } from '../utils/sample-content'
import { listTags } from '../repositories/content-repository'
/**
* 공개 태그 목록 API
* @returns {Array} 태그 목록
*/
export default defineEventHandler(() => getSampleTags())
export default defineEventHandler(() => listTags())

View File

@@ -0,0 +1,170 @@
import {
getSamplePageBySlug,
getSamplePages,
getSamplePostBySlug,
getSamplePosts,
getSampleTags
} from '../utils/sample-content'
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,
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
})
/**
* 공개 게시물 목록 조회
* @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'
GROUP BY posts.id
ORDER BY posts.published_at DESC NULLS LAST, posts.created_at DESC
`
return rows.map(mapPostRow)
}
/**
* 공개 게시물 상세 조회
* @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'
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)
}
/**
* 공개 고정 페이지 상세 조회
* @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 name ASC
`
return rows.map(mapTagRow)
}

View File

@@ -0,0 +1,25 @@
import postgres from 'postgres'
let client = null
/**
* PostgreSQL 클라이언트 조회
* @returns {ReturnType<typeof postgres> | null} PostgreSQL 클라이언트
*/
export const getPostgresClient = () => {
const config = useRuntimeConfig()
if (!config.databaseUrl) {
return null
}
if (!client) {
client = postgres(config.databaseUrl, {
max: 5,
idle_timeout: 20,
connect_timeout: 10
})
}
return client
}