SHIN
Next.js App Router로 풀스택 블로그 만들기
9편블로그에 필요한 모델과 관계를 먼저 도식화합니다.
User ──┐
├──< Post >──< PostTag >──< Tag
│ │
│ ├──< Comment (트리 구조)
│ └──< SeriesPost >──< Series
└──< Commentgenerator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
}
model User {
id String @id @default(cuid())
name String?
email String @unique
password String?
role String @default("ADMIN")
bio String?
github String?
twitter String?
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
comments Comment[]
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
excerpt String?
content String
coverImage String?
status String @default("DRAFT") // DRAFT | PUBLISHED | ARCHIVED
featured Boolean @default(false)
pinned Boolean @default(false)
viewCount Int @default(0)
likeCount Int @default(0)
readingTime Int?
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
author User @relation(fields: [authorId], references: [id])
categoryId String?
category Category? @relation(fields: [categoryId], references: [id])
tags PostTag[]
comments Comment[]
series SeriesPost[]
}Post ↔ Tag는 다대다 관계입니다. Prisma에서는 명시적 중간 테이블을 권장합니다.
model PostTag {
postId String
tagId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([postId, tagId])
}명시적 중간 테이블을 쓰면 나중에 order 같은 컬럼을 추가하기 쉽습니다.
model Comment {
id String @id @default(cuid())
content String
status String @default("PENDING") // PENDING | APPROVED | REJECTED
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
authorId String?
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
// 셀프 참조로 대댓글 구현
parentId String?
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
replies Comment[] @relation("CommentReplies")
}# 최초 마이그레이션
npx prisma migrate dev --name init
# 스키마 변경 후
npx prisma migrate dev --name add-series
# Prisma Client 재생성
npx prisma generateNext.js 개발 환경에서는 Hot Reload 때마다 새 연결이 생겨 리소스가 낭비됩니다.
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import path from 'path'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
function createClient() {
const url = `file:${path.join(process.cwd(), 'prisma/dev.db')}`
const adapter = new PrismaBetterSqlite3({ url })
return new PrismaClient({ adapter } as any)
}
export const prisma = globalForPrisma.prisma ?? createClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}globalThis에 인스턴스를 캐싱해 Hot Reload에도 단일 연결을 유지합니다.
포스트를 저장할 때 내용 길이를 기반으로 읽기 시간을 계산합니다.
export function calcReadingTime(content: string): number {
const words = content.trim().split(/\s+/).length
return Math.max(1, Math.round(words / 200)) // 분당 200단어
}다음 편에서는 NextAuth.js v5를 사용해 관리자 인증 시스템을 구현합니다.