feat: 댓글 구조 변경
작성자 표시를 위해 CommentResponse에 isPostAuthor 필드 추가
This commit is contained in:
@@ -15,10 +15,10 @@ services:
|
|||||||
- SPRING_DATA_REDIS_HOST=redis
|
- SPRING_DATA_REDIS_HOST=redis
|
||||||
- SPRING_DATA_REDIS_PORT=6379
|
- SPRING_DATA_REDIS_PORT=6379
|
||||||
# AWS S3 / MinIO (Docker 내부 통신용)
|
# AWS S3 / MinIO (Docker 내부 통신용)
|
||||||
- CLOUD_AWS_CREDENTIALS_ACCESS_KEY=admin
|
- SPRING_CLOUD_AWS_CREDENTIALS_ACCESS_KEY=admin
|
||||||
- CLOUD_AWS_CREDENTIALS_SECRET_KEY=password
|
- SPRING_CLOUD_AWS_CREDENTIALS_SECRET_KEY=password
|
||||||
- CLOUD_AWS_REGION_STATIC=ap-northeast-2
|
- SPRING_CLOUD_AWS_REGION_STATIC=ap-northeast-2
|
||||||
- CLOUD_AWS_S3_ENDPOINT=http://minio:9000
|
- SPRING_CLOUD_AWS_S3_ENDPOINT=http://minio:9000
|
||||||
# SMTP 메일 설정
|
# SMTP 메일 설정
|
||||||
- SPRING_MAIL_HOST=smtp.gmail.com
|
- SPRING_MAIL_HOST=smtp.gmail.com
|
||||||
- SPRING_MAIL_PORT=587
|
- SPRING_MAIL_PORT=587
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ data class PostSummaryResponse(
|
|||||||
data class PostSaveRequest(
|
data class PostSaveRequest(
|
||||||
val title: String,
|
val title: String,
|
||||||
val content: String, // 마크다운 원문
|
val content: String, // 마크다운 원문
|
||||||
val slug: String,
|
val slug: String? = null,
|
||||||
val categoryId: Long? = null,
|
val categoryId: Long? = null,
|
||||||
val tags: List<String> = emptyList() // 태그는 나중에 구현
|
val tags: List<String> = emptyList() // 태그는 나중에 구현
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package me.wypark.blogbackend.core.config
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import jakarta.persistence.EntityManager
|
||||||
|
import jakarta.persistence.PersistenceContext
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class QueryDslConfig(
|
||||||
|
@PersistenceContext
|
||||||
|
private val entityManager: EntityManager
|
||||||
|
) {
|
||||||
|
@Bean
|
||||||
|
fun jpaQueryFactory(): JPAQueryFactory {
|
||||||
|
return JPAQueryFactory(entityManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,21 +11,21 @@ import java.net.URI
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class S3Config(
|
class S3Config(
|
||||||
@Value("\${cloud.aws.credentials.access-key}") private val accessKey: String,
|
@Value("\${spring.cloud.aws.credentials.access-key:admin}") private val accessKey: String,
|
||||||
@Value("\${cloud.aws.credentials.secret-key}") private val secretKey: String,
|
@Value("\${spring.cloud.aws.credentials.secret-key:password}") private val secretKey: String,
|
||||||
@Value("\${cloud.aws.region.static}") private val region: String,
|
@Value("\${spring.cloud.aws.region.static:ap-northeast-2}") private val regionStr: String, // 변수명 regionStr 확인
|
||||||
@Value("\${cloud.aws.s3.endpoint}") private val endpoint: String
|
@Value("\${spring.cloud.aws.s3.endpoint:http://minio:9000}") private val endpoint: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun s3Client(): S3Client {
|
fun s3Client(): S3Client {
|
||||||
return S3Client.builder()
|
return S3Client.builder()
|
||||||
.region(Region.of(region))
|
.region(Region.of(regionStr))
|
||||||
.credentialsProvider(
|
.credentialsProvider(
|
||||||
StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey))
|
StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey))
|
||||||
)
|
)
|
||||||
.endpointOverride(URI.create(endpoint))
|
.endpointOverride(URI.create(endpoint)) // MinIO 주소
|
||||||
.forcePathStyle(true) // MinIO 필수 설정 (도메인 방식이 아닌 경로 방식 사용)
|
.forcePathStyle(true) // MinIO 필수 설정
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ class SecurityConfig(
|
|||||||
auth.requestMatchers("/api/auth/**").permitAll()
|
auth.requestMatchers("/api/auth/**").permitAll()
|
||||||
auth.requestMatchers(HttpMethod.GET, "/api/posts/**", "/api/categories/**", "/api/tags/**").permitAll()
|
auth.requestMatchers(HttpMethod.GET, "/api/posts/**", "/api/categories/**", "/api/tags/**").permitAll()
|
||||||
auth.requestMatchers(HttpMethod.POST, "/api/comments/**").permitAll() // 비회원 댓글 허용
|
auth.requestMatchers(HttpMethod.POST, "/api/comments/**").permitAll() // 비회원 댓글 허용
|
||||||
auth.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
auth.requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")
|
||||||
auth.anyRequest().authenticated()
|
auth.anyRequest().authenticated()
|
||||||
}
|
}
|
||||||
// 필터 등록: UsernamePasswordAuthenticationFilter 앞에 JwtFilter를 실행
|
// 필터 등록: UsernamePasswordAuthenticationFilter 앞에 JwtFilter를 실행
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.Query
|
|||||||
interface CategoryRepository : JpaRepository<Category, Long> {
|
interface CategoryRepository : JpaRepository<Category, Long> {
|
||||||
|
|
||||||
// 부모가 없는 최상위 카테고리들만 조회 (이걸 가져오면 자식들은 줄줄이 딸려옵니다)
|
// 부모가 없는 최상위 카테고리들만 조회 (이걸 가져오면 자식들은 줄줄이 딸려옵니다)
|
||||||
@Query("SELECT c FROM Category c JOIN FETCH c.children WHERE c.parent IS NULL")
|
@Query("SELECT DISTINCT c FROM Category c LEFT JOIN FETCH c.children WHERE c.parent IS NULL")
|
||||||
fun findAllRoots(): List<Category>
|
fun findAllRoots(): List<Category>
|
||||||
|
|
||||||
// 카테고리 이름 중복 검사 (같은 레벨에서 중복 방지용)
|
// 카테고리 이름 중복 검사 (같은 레벨에서 중복 방지용)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import java.util.*
|
|||||||
@Service
|
@Service
|
||||||
class ImageService(
|
class ImageService(
|
||||||
private val s3Client: S3Client,
|
private val s3Client: S3Client,
|
||||||
@Value("\${cloud.aws.s3.endpoint}") private val endpoint: String
|
@Value("\${spring.cloud.aws.s3.endpoint:http://minio:9000}") private val endpoint: String
|
||||||
) {
|
) {
|
||||||
private val bucketName = "blog-images" // 버킷 이름
|
private val bucketName = "blog-images" // 버킷 이름
|
||||||
|
|
||||||
|
|||||||
@@ -49,18 +49,38 @@ class PostService(
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createPost(request: PostSaveRequest, email: String): Long {
|
fun createPost(request: PostSaveRequest, email: String): Long {
|
||||||
if (postRepository.existsBySlug(request.slug)) { throw IllegalArgumentException("이미 존재하는 Slug입니다.") }
|
val member = memberRepository.findByEmail(email)
|
||||||
val member = memberRepository.findByEmail(email) ?: throw IllegalArgumentException("회원 없음")
|
?: throw IllegalArgumentException("회원 없음")
|
||||||
|
|
||||||
val category = request.categoryId?.let { categoryRepository.findByIdOrNull(it) }
|
val category = request.categoryId?.let { categoryRepository.findByIdOrNull(it) }
|
||||||
|
|
||||||
|
val rawSlug = if (!request.slug.isNullOrBlank()) {
|
||||||
|
request.slug
|
||||||
|
} else {
|
||||||
|
request.title.trim().replace("\\s+".toRegex(), "-").lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) DB 중복 검사: 중복되면 -1, -2, -3... 붙여나감
|
||||||
|
var uniqueSlug = rawSlug
|
||||||
|
var count = 1
|
||||||
|
|
||||||
|
while (postRepository.existsBySlug(uniqueSlug)) {
|
||||||
|
uniqueSlug = "$rawSlug-$count"
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
// 2. 게시글 객체 생성 (uniqueSlug 사용)
|
||||||
val post = Post(
|
val post = Post(
|
||||||
title = request.title,
|
title = request.title,
|
||||||
content = request.content,
|
content = request.content,
|
||||||
slug = request.slug,
|
slug = uniqueSlug, // 👈 중복 처리된 슬러그
|
||||||
member = member,
|
member = member,
|
||||||
category = category
|
category = category
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 3. 태그 처리 (작성하신 로직 그대로 활용)
|
||||||
|
// 리스트를 순회하며 없으면 저장(save), 있으면 조회(find)
|
||||||
val postTags = request.tags.map { tagName ->
|
val postTags = request.tags.map { tagName ->
|
||||||
val tag = tagRepository.findByName(tagName)
|
val tag = tagRepository.findByName(tagName)
|
||||||
?: tagRepository.save(Tag(name = tagName))
|
?: tagRepository.save(Tag(name = tagName))
|
||||||
@@ -68,6 +88,7 @@ class PostService(
|
|||||||
PostTag(post = post, tag = tag)
|
PostTag(post = post, tag = tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 연관관계 편의 메서드 사용 (Post 내부에 구현되어 있다고 가정)
|
||||||
post.addTags(postTags)
|
post.addTags(postTags)
|
||||||
|
|
||||||
return postRepository.save(post).id!!
|
return postRepository.save(post).id!!
|
||||||
|
|||||||
@@ -43,12 +43,11 @@ spring:
|
|||||||
host: redis # Docker 서비스명
|
host: redis # Docker 서비스명
|
||||||
port: 6379
|
port: 6379
|
||||||
|
|
||||||
# 4. AWS S3 / MinIO 설정 (아까 이야기한 부분)
|
cloud:
|
||||||
cloud:
|
|
||||||
aws:
|
aws:
|
||||||
s3:
|
s3:
|
||||||
bucket: my-blog-bucket # MinIO 콘솔에서 미리 생성해야 함
|
bucket: my-blog-bucket
|
||||||
endpoint: http://minio:9000 # Docker 내부 통신용
|
endpoint: http://minio:9000
|
||||||
credentials:
|
credentials:
|
||||||
access-key: admin
|
access-key: admin
|
||||||
secret-key: password
|
secret-key: password
|
||||||
|
|||||||
Reference in New Issue
Block a user