feat: 블로그 핵심 기능 구현 (댓글, 태그, 카테고리, 이미지)

[댓글 시스템]
- 계층형(대댓글) 구조 및 회원/비회원 하이브리드 작성 로직 구현
- 비회원 댓글용 비밀번호 암호화 저장 (PasswordEncoder 적용)
- 관리자용 댓글 강제 삭제 및 전체 댓글 모니터링(Dashboard) API 구현

[태그 & 카테고리]
- N:M 태그 시스템(PostTag) 엔티티 설계 및 게시글 작성 시 자동 저장 로직
- 계층형(Tree) 카테고리 구조 구현 및 관리자 생성/삭제 API
- QueryDSL 검색 조건에 태그 및 카테고리 필터링 추가

[이미지 업로드]
- AWS S3 (MinIO) 연동 및 Bucket 자동 생성/Public 정책 설정
- 마크다운 에디터용 이미지 업로드 API 구현
This commit is contained in:
pwy3282040@msecure.co
2025-12-26 14:47:48 +09:00
parent 6fbfcaf90b
commit 60d645f47b
26 changed files with 1084 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
package me.wypark.blogbackend.api.dto
import me.wypark.blogbackend.domain.category.Category
// [요청] 카테고리 생성
data class CategoryCreateRequest(
val name: String,
val parentId: Long? = null // null이면 최상위(Root) 카테고리
)
// [응답] 카테고리 트리 구조 (재귀)
data class CategoryResponse(
val id: Long,
val name: String,
val children: List<CategoryResponse> // 자식들
) {
companion object {
// Entity -> DTO 변환 (재귀 호출)
fun from(category: Category): CategoryResponse {
return CategoryResponse(
id = category.id!!,
name = category.name,
// 자식들을 DTO로 변환하여 리스트에 담음
children = category.children.map { from(it) }
)
}
}
}
// [Admin용 응답] 관리자 대시보드 목록용
data class AdminCommentResponse(
val id: Long,
val content: String,
val author: String,
val postTitle: String, // 어떤 글인지 식별
val postSlug: String, // 클릭 시 해당 글로 이동용
val createdAt: java.time.LocalDateTime
) {
companion object {
fun from(comment: me.wypark.blogbackend.domain.comment.Comment): AdminCommentResponse {
return AdminCommentResponse(
id = comment.id!!,
content = comment.content,
author = comment.getAuthorName(),
postTitle = comment.post.title,
postSlug = comment.post.slug,
createdAt = comment.createdAt
)
}
}
}

View File

@@ -0,0 +1,41 @@
package me.wypark.blogbackend.api.dto
import me.wypark.blogbackend.domain.comment.Comment
import java.time.LocalDateTime
// [응답] 댓글 (계층형 구조)
data class CommentResponse(
val id: Long,
val content: String,
val author: String, // 회원 닉네임 또는 비회원 닉네임
val createdAt: LocalDateTime,
val children: List<CommentResponse> // 대댓글 리스트
) {
companion object {
fun from(comment: Comment): CommentResponse {
return CommentResponse(
id = comment.id!!,
content = comment.content,
author = comment.getAuthorName(), // Entity에 만들어둔 편의 메서드 사용
createdAt = comment.createdAt,
children = comment.children.map { from(it) } // 재귀적으로 자식 변환
)
}
}
}
// [요청] 댓글 작성
data class CommentSaveRequest(
val postSlug: String,
val content: String,
val parentId: Long? = null, // 대댓글일 경우 부모 ID
// 비회원 전용 필드 (회원은 null 가능)
val guestNickname: String? = null,
val guestPassword: String? = null
)
// [요청] 댓글 삭제 (비회원용 비밀번호 전달)
data class CommentDeleteRequest(
val guestPassword: String? = null
)

View File

@@ -0,0 +1,62 @@
package me.wypark.blogbackend.api.dto
import me.wypark.blogbackend.domain.post.Post
import java.time.LocalDateTime
// [응답] 게시글 상세 정보
data class PostResponse(
val id: Long,
val title: String,
val content: String,
val slug: String,
val categoryName: String?,
val viewCount: Long,
val createdAt: LocalDateTime
) {
// Entity -> DTO 변환 편의 메서드
companion object {
fun from(post: Post): PostResponse {
return PostResponse(
id = post.id!!,
title = post.title,
content = post.content,
slug = post.slug,
categoryName = post.category?.name,
viewCount = post.viewCount,
createdAt = post.createdAt
)
}
}
}
// [응답] 게시글 목록용 (본문 제외, 가볍게)
data class PostSummaryResponse(
val id: Long,
val title: String,
val slug: String,
val categoryName: String?,
val viewCount: Long,
val createdAt: LocalDateTime
) {
companion object {
fun from(post: Post): PostSummaryResponse {
return PostSummaryResponse(
id = post.id!!,
title = post.title,
slug = post.slug,
categoryName = post.category?.name,
viewCount = post.viewCount,
createdAt = post.createdAt
)
}
}
}
// [요청] 게시글 작성/수정
data class PostSaveRequest(
val title: String,
val content: String, // 마크다운 원문
val slug: String,
val categoryId: Long? = null,
val tags: List<String> = emptyList() // 태그는 나중에 구현
)