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,72 @@
package me.wypark.blogbackend.domain.image
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.GetUrlRequest
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.util.*
@Service
class ImageService(
private val s3Client: S3Client,
@Value("\${cloud.aws.s3.endpoint}") private val endpoint: String
) {
private val bucketName = "blog-images" // 버킷 이름
init {
createBucketIfNotExists()
}
fun uploadImage(file: MultipartFile): String {
// 1. 파일명 중복 방지 (UUID 사용)
val originalName = file.originalFilename ?: "image.jpg"
val ext = originalName.substringAfterLast(".", "jpg")
val fileName = "${UUID.randomUUID()}.$ext"
// 2. S3(MinIO)로 업로드
val putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(fileName)
.contentType(file.contentType)
.build()
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.inputStream, file.size))
// 3. 접속 가능한 URL 반환
// 로컬 개발 환경에서는 localhost 주소를 직접 조합해서 줍니다.
// 배포 시에는 실제 도메인이나 CloudFront 주소로 변경해야 합니다.
return "$endpoint/$bucketName/$fileName"
}
private fun createBucketIfNotExists() {
try {
// 버킷 존재 여부 확인 (없으면 에러 발생하므로 try-catch)
s3Client.headBucket { it.bucket(bucketName) }
} catch (e: Exception) {
// 버킷 생성
s3Client.createBucket { it.bucket(bucketName) }
// ⭐ 버킷을 Public(공개)으로 설정 (이미지 조회를 위해 필수)
val policy = """
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": ["*"] },
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::$bucketName/*"]
}
]
}
""".trimIndent()
s3Client.putBucketPolicy {
it.bucket(bucketName).policy(policy)
}
}
}
}