feat: 댓글 구조 변경

작성자 표시를 위해 CommentResponse에 isPostAuthor 필드 추가
This commit is contained in:
pwy3282040@msecure.co
2025-12-27 00:14:57 +09:00
parent ef6ffa5670
commit 884853586d
9 changed files with 69 additions and 31 deletions

View File

@@ -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

View File

@@ -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() // 태그는 나중에 구현
) )

View File

@@ -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)
}
}

View File

@@ -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()
} }
} }

View File

@@ -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를 실행

View File

@@ -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>
// 카테고리 이름 중복 검사 (같은 레벨에서 중복 방지용) // 카테고리 이름 중복 검사 (같은 레벨에서 중복 방지용)

View File

@@ -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" // 버킷 이름

View File

@@ -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!!

View File

@@ -43,19 +43,18 @@ 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
bucket: my-blog-bucket # MinIO 콘솔에서 미리 생성해야 함 endpoint: http://minio:9000
endpoint: http://minio:9000 # Docker 내부 통신용 credentials:
credentials: access-key: admin
access-key: admin secret-key: password
secret-key: password region:
region: static: ap-northeast-2
static: ap-northeast-2 stack:
stack: auto: false
auto: false
# 5. JWT 설정 # 5. JWT 설정
jwt: jwt: