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,79 @@
package me.wypark.blogbackend.domain.post
import me.wypark.blogbackend.api.dto.PostResponse
import me.wypark.blogbackend.api.dto.PostSaveRequest
import me.wypark.blogbackend.api.dto.PostSummaryResponse
import me.wypark.blogbackend.domain.category.CategoryRepository
import me.wypark.blogbackend.domain.tag.PostTag
import me.wypark.blogbackend.domain.tag.Tag
import me.wypark.blogbackend.domain.tag.TagRepository
import me.wypark.blogbackend.domain.user.MemberRepository
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional(readOnly = true)
class PostService(
private val postRepository: PostRepository,
private val categoryRepository: CategoryRepository,
private val memberRepository: MemberRepository,
private val tagRepository: TagRepository
) {
/**
* [Public] 전체 게시글 목록 조회 (페이징)
*/
fun getPosts(pageable: Pageable): Page<PostSummaryResponse> {
return postRepository.findAll(pageable)
.map { PostSummaryResponse.from(it) }
}
/**
* [Public] 게시글 상세 조회 (Slug 기반) + 조회수 증가
*/
@Transactional
fun getPostBySlug(slug: String): PostResponse {
val post = postRepository.findBySlug(slug)
?: throw IllegalArgumentException("해당 게시글을 찾을 수 없습니다: $slug")
post.increaseViewCount() // 조회수 1 증가 (Dirty Checking)
return PostResponse.from(post)
}
/**
* [Admin] 게시글 작성
*/
@Transactional
fun createPost(request: PostSaveRequest, email: String): Long {
if (postRepository.existsBySlug(request.slug)) { throw IllegalArgumentException("이미 존재하는 Slug입니다.") }
val member = memberRepository.findByEmail(email) ?: throw IllegalArgumentException("회원 없음")
val category = request.categoryId?.let { categoryRepository.findByIdOrNull(it) }
val post = Post(
title = request.title,
content = request.content,
slug = request.slug,
member = member,
category = category
)
val postTags = request.tags.map { tagName ->
val tag = tagRepository.findByName(tagName)
?: tagRepository.save(Tag(name = tagName))
PostTag(post = post, tag = tag)
}
post.addTags(postTags)
return postRepository.save(post).id!!
}
fun searchPosts(keyword: String?, categoryName: String?, tagName: String?, pageable: Pageable): Page<PostSummaryResponse> {
return postRepository.search(keyword, categoryName, tagName, pageable)
}
}