From 884853586d4e4c6e065d05dd315c5a250feaa354 Mon Sep 17 00:00:00 2001 From: "pwy3282040@msecure.co" Date: Sat, 27 Dec 2025 00:14:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 작성자 표시를 위해 CommentResponse에 isPostAuthor 필드 추가 --- docker-compose.yml | 8 +++--- .../me/wypark/blogbackend/api/dto/PostDtos.kt | 2 +- .../blogbackend/core/config/QueryDslConfig.kt | 18 +++++++++++++ .../blogbackend/core/config/S3Config.kt | 14 +++++----- .../blogbackend/core/config/SecurityConfig.kt | 2 +- .../domain/category/CategoryRepository.kt | 2 +- .../blogbackend/domain/image/ImageService.kt | 2 +- .../blogbackend/domain/post/PostService.kt | 27 ++++++++++++++++--- src/main/resources/application-prod.yml | 25 +++++++++-------- 9 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 src/main/kotlin/me/wypark/blogbackend/core/config/QueryDslConfig.kt diff --git a/docker-compose.yml b/docker-compose.yml index 5c046d5..1cae445 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,10 +15,10 @@ services: - SPRING_DATA_REDIS_HOST=redis - SPRING_DATA_REDIS_PORT=6379 # AWS S3 / MinIO (Docker 내부 통신용) - - CLOUD_AWS_CREDENTIALS_ACCESS_KEY=admin - - CLOUD_AWS_CREDENTIALS_SECRET_KEY=password - - CLOUD_AWS_REGION_STATIC=ap-northeast-2 - - CLOUD_AWS_S3_ENDPOINT=http://minio:9000 + - SPRING_CLOUD_AWS_CREDENTIALS_ACCESS_KEY=admin + - SPRING_CLOUD_AWS_CREDENTIALS_SECRET_KEY=password + - SPRING_CLOUD_AWS_REGION_STATIC=ap-northeast-2 + - SPRING_CLOUD_AWS_S3_ENDPOINT=http://minio:9000 # SMTP 메일 설정 - SPRING_MAIL_HOST=smtp.gmail.com - SPRING_MAIL_PORT=587 diff --git a/src/main/kotlin/me/wypark/blogbackend/api/dto/PostDtos.kt b/src/main/kotlin/me/wypark/blogbackend/api/dto/PostDtos.kt index 369bad4..2958643 100644 --- a/src/main/kotlin/me/wypark/blogbackend/api/dto/PostDtos.kt +++ b/src/main/kotlin/me/wypark/blogbackend/api/dto/PostDtos.kt @@ -56,7 +56,7 @@ data class PostSummaryResponse( data class PostSaveRequest( val title: String, val content: String, // 마크다운 원문 - val slug: String, + val slug: String? = null, val categoryId: Long? = null, val tags: List = emptyList() // 태그는 나중에 구현 ) \ No newline at end of file diff --git a/src/main/kotlin/me/wypark/blogbackend/core/config/QueryDslConfig.kt b/src/main/kotlin/me/wypark/blogbackend/core/config/QueryDslConfig.kt new file mode 100644 index 0000000..6f84437 --- /dev/null +++ b/src/main/kotlin/me/wypark/blogbackend/core/config/QueryDslConfig.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/me/wypark/blogbackend/core/config/S3Config.kt b/src/main/kotlin/me/wypark/blogbackend/core/config/S3Config.kt index e4f123c..4f916f3 100644 --- a/src/main/kotlin/me/wypark/blogbackend/core/config/S3Config.kt +++ b/src/main/kotlin/me/wypark/blogbackend/core/config/S3Config.kt @@ -11,21 +11,21 @@ import java.net.URI @Configuration class S3Config( - @Value("\${cloud.aws.credentials.access-key}") private val accessKey: String, - @Value("\${cloud.aws.credentials.secret-key}") private val secretKey: String, - @Value("\${cloud.aws.region.static}") private val region: String, - @Value("\${cloud.aws.s3.endpoint}") private val endpoint: String + @Value("\${spring.cloud.aws.credentials.access-key:admin}") private val accessKey: String, + @Value("\${spring.cloud.aws.credentials.secret-key:password}") private val secretKey: String, + @Value("\${spring.cloud.aws.region.static:ap-northeast-2}") private val regionStr: String, // 변수명 regionStr 확인 + @Value("\${spring.cloud.aws.s3.endpoint:http://minio:9000}") private val endpoint: String ) { @Bean fun s3Client(): S3Client { return S3Client.builder() - .region(Region.of(region)) + .region(Region.of(regionStr)) .credentialsProvider( StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)) ) - .endpointOverride(URI.create(endpoint)) - .forcePathStyle(true) // MinIO 필수 설정 (도메인 방식이 아닌 경로 방식 사용) + .endpointOverride(URI.create(endpoint)) // MinIO 주소 + .forcePathStyle(true) // MinIO 필수 설정 .build() } } \ No newline at end of file diff --git a/src/main/kotlin/me/wypark/blogbackend/core/config/SecurityConfig.kt b/src/main/kotlin/me/wypark/blogbackend/core/config/SecurityConfig.kt index 7a1b826..0e8bb80 100644 --- a/src/main/kotlin/me/wypark/blogbackend/core/config/SecurityConfig.kt +++ b/src/main/kotlin/me/wypark/blogbackend/core/config/SecurityConfig.kt @@ -32,7 +32,7 @@ class SecurityConfig( auth.requestMatchers("/api/auth/**").permitAll() auth.requestMatchers(HttpMethod.GET, "/api/posts/**", "/api/categories/**", "/api/tags/**").permitAll() auth.requestMatchers(HttpMethod.POST, "/api/comments/**").permitAll() // 비회원 댓글 허용 - auth.requestMatchers("/api/admin/**").hasRole("ADMIN") + auth.requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN") auth.anyRequest().authenticated() } // 필터 등록: UsernamePasswordAuthenticationFilter 앞에 JwtFilter를 실행 diff --git a/src/main/kotlin/me/wypark/blogbackend/domain/category/CategoryRepository.kt b/src/main/kotlin/me/wypark/blogbackend/domain/category/CategoryRepository.kt index 5c8687b..0a5f477 100644 --- a/src/main/kotlin/me/wypark/blogbackend/domain/category/CategoryRepository.kt +++ b/src/main/kotlin/me/wypark/blogbackend/domain/category/CategoryRepository.kt @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.Query interface CategoryRepository : JpaRepository { // 부모가 없는 최상위 카테고리들만 조회 (이걸 가져오면 자식들은 줄줄이 딸려옵니다) - @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 // 카테고리 이름 중복 검사 (같은 레벨에서 중복 방지용) diff --git a/src/main/kotlin/me/wypark/blogbackend/domain/image/ImageService.kt b/src/main/kotlin/me/wypark/blogbackend/domain/image/ImageService.kt index 3fad935..cfb9ac6 100644 --- a/src/main/kotlin/me/wypark/blogbackend/domain/image/ImageService.kt +++ b/src/main/kotlin/me/wypark/blogbackend/domain/image/ImageService.kt @@ -12,7 +12,7 @@ import java.util.* @Service class ImageService( 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" // 버킷 이름 diff --git a/src/main/kotlin/me/wypark/blogbackend/domain/post/PostService.kt b/src/main/kotlin/me/wypark/blogbackend/domain/post/PostService.kt index a6e9718..2c4cf77 100644 --- a/src/main/kotlin/me/wypark/blogbackend/domain/post/PostService.kt +++ b/src/main/kotlin/me/wypark/blogbackend/domain/post/PostService.kt @@ -49,18 +49,38 @@ class PostService( */ @Transactional fun createPost(request: PostSaveRequest, email: String): Long { - if (postRepository.existsBySlug(request.slug)) { throw IllegalArgumentException("이미 존재하는 Slug입니다.") } - val member = memberRepository.findByEmail(email) ?: throw IllegalArgumentException("회원 없음") + val member = memberRepository.findByEmail(email) + ?: throw IllegalArgumentException("회원 없음") + 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( title = request.title, content = request.content, - slug = request.slug, + slug = uniqueSlug, // 👈 중복 처리된 슬러그 member = member, category = category ) + // 3. 태그 처리 (작성하신 로직 그대로 활용) + // 리스트를 순회하며 없으면 저장(save), 있으면 조회(find) val postTags = request.tags.map { tagName -> val tag = tagRepository.findByName(tagName) ?: tagRepository.save(Tag(name = tagName)) @@ -68,6 +88,7 @@ class PostService( PostTag(post = post, tag = tag) } + // 연관관계 편의 메서드 사용 (Post 내부에 구현되어 있다고 가정) post.addTags(postTags) return postRepository.save(post).id!! diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index a331df4..fa27978 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -43,19 +43,18 @@ spring: host: redis # Docker 서비스명 port: 6379 -# 4. AWS S3 / MinIO 설정 (아까 이야기한 부분) -cloud: - aws: - s3: - bucket: my-blog-bucket # MinIO 콘솔에서 미리 생성해야 함 - endpoint: http://minio:9000 # Docker 내부 통신용 - credentials: - access-key: admin - secret-key: password - region: - static: ap-northeast-2 - stack: - auto: false + cloud: + aws: + s3: + bucket: my-blog-bucket + endpoint: http://minio:9000 + credentials: + access-key: admin + secret-key: password + region: + static: ap-northeast-2 + stack: + auto: false # 5. JWT 설정 jwt: