Skip to content

feat: AI 파생 포스트 기능 구현 및 테스트 코드 작성#155

Merged
yooooonshine merged 4 commits intodevelopfrom
feature/154-implement-ai-derived-post
Aug 25, 2025
Merged

feat: AI 파생 포스트 기능 구현 및 테스트 코드 작성#155
yooooonshine merged 4 commits intodevelopfrom
feature/154-implement-ai-derived-post

Conversation

@yooooonshine
Copy link
Contributor

@yooooonshine yooooonshine commented Aug 25, 2025

개요

생성된 AI 이미지를 기반으로 파생 포스트를 생성하고 관리할 수 있는 기능을 구현했습니다.

구현 내용

1. 비즈니스 로직

  • AiDerivedPostService: AI 파생 포스트 생성 및 삭제 로직
    • AI 이미지 소유권 검증
    • AI 이미지를 PostImage로 변환하여 포스트 생성
    • AI 파생 포스트 여부 검증 후 삭제

2. API 엔드포인트

  • AiDerivedPostController: RESTful API 제공
    • POST /api/ai/derived-posts: AI 파생 포스트 생성
    • DELETE /api/ai/derived-posts/{postId}: AI 파생 포스트 삭제
    • Swagger 문서화 및 예외 처리 명시

3. DTO 및 검증

  • CreateAiDerivedPostRequest: 요청 DTO with validation
    • @NotNull, @NotBlank, @PositiveOrZero 검증
    • 모든 필드에 대한 유효성 검사

4. 엔티티 확장

  • PostEntity: isAiDerivedPost 필드 추가
  • PostType: AI 파생 포스트 구분을 위한 열거형
  • ErrorCode: AI 관련 예외 코드 추가

테스트 내용

1. Factory 클래스

  • AiFactory: CreatedAiImageEntity 생성을 위한 팩토리
  • PostFactory: AI 파생 포스트 생성 메소드 추가

2. 단위 테스트

  • AiDerivedPostServiceTest: 서비스 로직 테스트

    • 성공 케이스: AI 파생 포스트 생성/삭제 성공
    • 실패 케이스: AI 이미지 미존재, 권한 없음, 일반 포스트 삭제 시도 등
  • AiDerivedPostControllerTest: 컨트롤러 테스트

    • HTTP 요청/응답 검증
    • 입력 데이터 유효성 검사
    • 예외 상황별 적절한 HTTP 상태 코드 반환

3. 통합 테스트

  • AiDerivedPostControllerIntegrationTest: 실제 데이터베이스 연동 테스트
    • 전체 플로우 검증
    • S3 파일 업로드/삭제 포함
    • 실제 데이터베이스 상태 검증

4. 테스트 커버리지

  • 모든 성공/실패 시나리오 커버
  • Mock을 통한 의존성 격리
  • 실제 환경과 유사한 통합 테스트

주요 특징

  • 🔒 보안: AI 이미지 소유권 검증을 통한 접근 제어
  • 📝 검증: 입력 데이터 유효성 검사 및 예외 처리
  • 📖 문서화: Swagger를 통한 API 문서 자동 생성
  • 테스트: 포괄적인 단위/통합 테스트 제공
  • 🏗️ 확장성: 기존 Post 시스템과의 일관성 유지

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 신기능
    • AI 파생 포스트 생성/삭제 API 추가 (/api/ai/derived-posts).
    • 게시글 목록에 postType 필터(ALL, ORIGINAL, AI_DERIVED) 지원.
    • 포스트 단건/목록 응답에 isAiDerivedPost 및 derivedPostIds 필드 추가.
    • AI 이미지 연계로 포스트 생성 시 이미지·소유자 연동 및 삭제 위임 구현.
  • 버그 수정 / 오류 처리
    • AI 이미지/포스트 관련 상세 오류 코드(예: 이미지 없음·권한 없음·비AI포스트 등) 추가.
  • 테스트
    • 관련 단위·통합 테스트 대폭 추가/보강.
  • 작업
    • 애플리케이션 설정 단순화(기본 프로필을 local로 지정).

yooooonshine and others added 3 commits August 25, 2025 15:52
- 기본 프로필을 local로 설정
- 환경별 설정 분리를 위한 리팩토링

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@yooooonshine yooooonshine linked an issue Aug 25, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Aug 25, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

AI 파생 포스트 생성/삭제 기능이 추가되었고, AI 이미지 엔터티에 userId/postId가 포함되며 포스트 엔티티와 조회 로직에 AI 파생 플래그가 도입됐다. 에러코드가 확장되고 관련 컨트롤러, 서비스, DTO, 리포지토리 및 광범위한 단위·통합 테스트가 추가/수정됐다.

Changes

Cohort / File(s) Summary
Error handling
src/main/java/.../common/error/ErrorCode.java
AI 이미지 및 포스트 접근/유효성 검사용 에러 코드 4종(AI-010, AI-011, P-003, P-004) 추가.
AI image persistence
src/main/java/.../domain/ai/domain/CreatedAiImageEntity.java, src/main/java/.../domain/ai/listener/AiImageCreatedListener.java
CreatedAiImageEntitypostId, userId 칼럼 및 빌더 인자 추가. 리스너가 aiRequest의 postId/userId를 매핑해 저장.
AI-derived post feature (service & controller & DTO)
src/main/java/.../domain/ai/service/AiDerivedPostService.java, src/main/java/.../web/post/controller/AiDerivedPostController.java, src/main/java/.../web/post/dto/request/CreateAiDerivedPostRequest.java
AI 파생 포스트 생성/삭제 서비스와 REST 엔드포인트 추가. 입력 유효성, 소유권/존재/타입 검사 및 관련 에러 코드 사용.
Post model & filtering
src/main/java/.../domain/post/entity/PostEntity.java, src/main/java/.../domain/post/enums/PostType.java, src/main/java/.../domain/post/repository/PostEntityRepository.java, src/main/java/.../domain/post/service/PostService.java, src/main/java/.../web/post/controller/PostController.java
PostEntityisAiDerivedPostparentPostId 추가. PostType enum 도입 및 getPosts에 타입 기반 필터링(ALL/ORIGINAL/AI_DERIVED) 적용. 리포지토리에 findAllByIsAiDerivedPost 및 derived-id 조회 추가.
Post response DTOs
src/main/java/.../web/post/dto/response/GetPostResponse.java, src/main/java/.../web/post/dto/response/GetPostsResponse.java
응답에 isAiDerivedPost 플래그 추가 및 GetPostResponsederivedPostIds 추가. 팩토리/오버로드와 직렬화 시그니처 갱신.
Repository cleanup
src/main/java/.../domain/ai/repository/CreatedAiImageRepository.java
더 이상 사용되지 않는 existsByImagePath 메서드 제거.
Configuration
src/main/resources/application.yml
application.yml을 축약하여 spring.profiles.active: local만 남김.
Tests & factories
src/test/java/.../domain/ai/entityfactory/AiFactory.java, src/test/java/.../domain/ai/service/AiDerivedPostServiceTest.java, src/test/java/.../domain/post/entityfactory/PostFactory.java, src/test/java/.../domain/post/service/PostServiceTest.java, src/test/java/.../web/post/controller/AiDerivedPostControllerIntegrationTest.java, src/test/java/.../web/post/controller/AiDerivedPostControllerTest.java, src/test/java/.../web/post/controller/PostControllerTest.java, src/test/java/.../web/post/controller/PublicPostControllerTest.java
AI 파생 포스트 관련 단위 및 통합 테스트 추가/수정. 테스트용 엔티티 팩토리 보강(일반/AI 파생 구분), PostType 반영 및 응답 DTO 필드 검증 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Controller as AiDerivedPostController
  participant Service as AiDerivedPostService
  participant AiRepo as CreatedAiImageRepository
  participant PostRepo as PostEntityRepository
  participant PostImageRepo as PostImageEntityRepository

  User->>Controller: POST /api/ai/derived-posts (createdAiImageId, title, prices...)
  Controller->>Service: createAiDerivedPost(userId, createdAiImageId, title, ...)
  Service->>AiRepo: findById(createdAiImageId)
  alt not found
    AiRepo-->>Service: empty
    Service-->>Controller: throw AI_IMAGE_NOT_FOUND_EXCEPTION
    Controller-->>User: 404
  else found
    AiRepo-->>Service: CreatedAiImageEntity(userId, path, postId...)
    alt owner mismatch
      Service-->>Controller: throw AI_IMAGE_ACCESS_DENIED_EXCEPTION
      Controller-->>User: 403
    else ok
      Service->>PostRepo: save(PostEntity{isAiDerivedPost=true,...})
      PostRepo-->>Service: savedPost(id)
      Service->>PostImageRepo: save(PostImageEntity(mapped from AI image))
      PostImageRepo-->>Service: savedImage
      Service-->>Controller: CreatePostResponse(postId)
      Controller-->>User: 201 {data.postId}
    end
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Controller as AiDerivedPostController
  participant Service as AiDerivedPostService
  participant PostRepo as PostEntityRepository
  participant PostService as PostService

  User->>Controller: DELETE /api/ai/derived-posts/{postId}
  Controller->>Service: deleteAiDerivedPost(userId, postId)
  Service->>PostRepo: findById(postId)
  alt not found
    PostRepo-->>Service: empty
    Service-->>Controller: throw POST_NOT_FOUND_EXCEPTION
    Controller-->>User: 404
  else found
    PostRepo-->>Service: PostEntity(userId, isAiDerivedPost?)
    alt owner mismatch
      Service-->>Controller: throw POST_ACCESS_DENIED_EXCEPTION
      Controller-->>User: 403
    else not AI-derived
      Service-->>Controller: throw NOT_AI_DERIVED_POST_EXCEPTION
      Controller-->>User: 400
    else deletable
      Service->>PostService: deletePost(userId, postId)
      PostService-->>Service: void
      Service-->>Controller: void
      Controller-->>User: 200
    end
  end
Loading
sequenceDiagram
  autonumber
  actor Client
  participant PostController
  participant PostService
  participant PostRepo

  Client->>PostController: GET /api/posts?postType=AI_DERIVED...
  PostController->>PostService: getPosts(sort,page,size,postType)
  alt ALL
    PostService->>PostRepo: findAll(Pageable)
  else ORIGINAL
    PostService->>PostRepo: findAllByIsAiDerivedPost(false, Pageable)
  else AI_DERIVED
    PostService->>PostRepo: findAllByIsAiDerivedPost(true, Pageable)
  end
  PostRepo-->>PostService: Page<PostEntity>
  PostService-->>PostController: PageResponse<GetPostsResponse{isAiDerivedPost,...}>
  PostController-->>Client: 200 {items...}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

AI가 그린 조각, 당근보다 반짝이는 픽셀 한 줌
토끼는 깡충, 엔티티에 user와 post를 심네 🐇
에러 코드 새로 달고 문지기 단단히 세워
파생은 true, 원본은 false, 목록은 골라 보렴
201로 웃고 200으로 살금 삭제해 — 호호 ✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f99662a and 837545b.

📒 Files selected for processing (11)
  • src/main/java/hanium/modic/backend/domain/ai/repository/CreatedAiImageRepository.java (0 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/post/entity/PostEntity.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/post/repository/PostEntityRepository.java (2 hunks)
  • src/main/java/hanium/modic/backend/domain/post/service/PostService.java (6 hunks)
  • src/main/java/hanium/modic/backend/web/post/dto/response/GetPostResponse.java (4 hunks)
  • src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (2 hunks)
  • src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (10 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java (4 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/PublicPostControllerTest.java (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/154-implement-ai-derived-post

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/hanium/modic/backend/domain/ai/listener/AiImageCreatedListener.java (1)

33-37: 요청 미발견 시 단순 return → 메시지 유실 위험 (RabbitMQ 재처리 불가)

현재는 AI 요청을 찾지 못하면 로그만 남기고 바로 반환합니다. @RabbitListener 기본 Ack 모드(AUTO)에서는 예외가 발생하지 않으면 메시지가 ack 되어 재시도가 불가능합니다. DLQ 라우팅 또는 재시도를 위해 예외를 던지거나 명시적으로 NACK 처리하는 쪽이 안전합니다.

적용 예시:

-		if (aiRequestOpt.isEmpty()) {
-			log.error("[AI 이미지 처리 실패] AI 요청을 찾을 수 없습니다. requestId: {}", message.requestId());
-			return;
-		}
+		if (aiRequestOpt.isEmpty()) {
+			log.error("[AI 이미지 처리 실패] AI 요청을 찾을 수 없습니다. requestId: {}", message.requestId());
+			throw new IllegalStateException("AI 요청 미존재: requestId=" + message.requestId());
+		}

운영 정책에 따라 AppException으로 래핑하거나, 컨테이너 Ack/Nack 설정을 조정하는 방안도 고려해 주세요.

src/main/java/hanium/modic/backend/domain/post/service/PostService.java (2)

142-145: 주석-구현 불일치: 'null로 설정'이라고 적혀 있으나 실제로는 false 할당

공개 조회에서 isLikedByCurrentUser는 false를 반환하고 있습니다. 주석을 구현에 맞추거나, 정말 null을 의도했다면 DTO/테스트를 포함해 전반 동작을 null로 정렬해야 합니다. 현재 테스트들은 false를 가정합니다.

-		// 비로그인 사용자이므로 좋아요 여부는 null로 설정
-		Boolean isLikedByCurrentUser = false;
+		// 비로그인 사용자는 좋아요 여부를 false로 고정
+		Boolean isLikedByCurrentUser = false;

154-159: 목록이 비어있을 때 404 예외 반환은 리스트 API 관례와 상충

일반적으로 리스트 API는 빈 결과를 200 + 빈 페이지로 반환합니다. 현 구현은 POST_NOT_FOUND_EXCEPTION을 던져 클라이언트 UX를 해칠 수 있고, 페이지네이션/무한스크롤 구현에도 불리합니다. 빈 페이지를 그대로 반환하도록 변경을 권장합니다.

-		if (posts.isEmpty()) {
-			throw new AppException(POST_NOT_FOUND_EXCEPTION);
-		}
+		// 빈 결과도 정상 응답으로 반환
🧹 Nitpick comments (38)
src/main/resources/application.yml (1)

3-3: 파일 말미 개행 누락(newline at EOF)

정적 분석 지적(new-line-at-end-of-file)처럼, POSIX/여러 툴 친화성을 위해 마지막 줄에 개행을 추가해 주세요.

src/main/java/hanium/modic/backend/web/post/dto/request/CreateAiDerivedPostRequest.java (3)

7-10: ID 값에 @positive 추가 권장

createdAiImageId는 식별자 특성상 0/음수일 수 없습니다. @NotNull만으로는 0/음수를 허용하므로 @positive 추가를 권장합니다.

적용 diff:

 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.PositiveOrZero;
+import jakarta.validation.constraints.Positive;

 public record CreateAiDerivedPostRequest(
-	@NotNull(message = "생성된 AI 이미지 ID는 필수입니다.")
+	@NotNull(message = "생성된 AI 이미지 ID는 필수입니다.")
+	@Positive(message = "생성된 AI 이미지 ID는 양수여야 합니다.")
 	Long createdAiImageId,

17-27: 가격들 간의 관계 제약(선택): 교차 필드 검증 고려

  • 비상업/티켓 가격이 상업 가격보다 클 수 없다 등의 정책이 있다면, 클래스 레벨 커스텀 Constraint로 교차 필드 검증을 추가하세요.
  • 현재는 각 필드의 개별 범위(0 이상)만 체크합니다.

원하시면 커스텀 Validator 템플릿까지 드리겠습니다.


11-16: 제목/설명 길이 제한 추가 권고
PostEntity의 title 컬럼은 @Column에 별도 length가 지정되어 있지 않아 JPA 기본값인 VARCHAR(255)가 적용됩니다. 따라서 DTO에서도 동일한 최대 길이(255자)를 @Size로 검증하는 것이 좋습니다.
description 컬럼은 columnDefinition="TEXT"로 DB 차원에 별도 길이 제한이 없으므로, 비즈니스 정책에 따라 적절한 최대 길이를 정해 @Size를 추가하시거나 무제한으로 허용하셔도 무방합니다.

수정 위치

  • 파일: src/main/java/hanium/modic/backend/web/post/dto/request/CreateAiDerivedPostRequest.java
    • title 필드에 @Size(max = 255, message = "제목은 255자 이하여야 합니다.")
    • description 필드에 @Size(max = {비즈니스정책_길이}, message = "설명은 {max}자 이하여야 합니다.") (또는 TEXT 타입 최대 길이 65535로 설정)

예시 diff

 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.PositiveOrZero;
+import jakarta.validation.constraints.Size;

  @NotBlank(message = "제목은 필수입니다.")
+ @Size(max = 255, message = "제목은 255자 이하여야 합니다.")
  String title,

  @NotBlank(message = "설명은 필수입니다.")
+ @Size(max = 65535, message = "설명은 65535자 이하여야 합니다.") // TEXT 타입 최대 길이, 필요 시 조정
  String description,
src/main/java/hanium/modic/backend/domain/post/enums/PostType.java (1)

3-7: 요청 파라미터 바인딩/문서화를 위한 소소한 개선 제안

  • 컨트롤러에서 @RequestParam PostType postType(defaultValue="ALL") 형태라면, 대소문자 입력 시 실패를 방지하고자 Case-insensitive Converter를 등록하거나, Swagger 문서에 허용 값과 기본값을 명시해 주세요.
  • OpenAPI 문서화 사용 중이라면 @Schema(enumAsRef = true, description = "...") 등을 붙이면 클라이언트 측 가독성이 좋아집니다.

선택 사항이라 부담 없이 넘어가셔도 됩니다.

src/main/java/hanium/modic/backend/domain/post/repository/PostEntityRepository.java (1)

14-14: null 파라미터 방지 확인 및 선택적 리팩터 제안

  • PostService#getPostsByType 구현을 확인한 결과,
    • ORIGINAL → findAllByIsAiDerivedPost(false, pageable)
    • AI_DERIVED → findAllByIsAiDerivedPost(true, pageable)
    • ALL → findAll(pageable)
    로 null이 절대 넘어가지 않도록 설계되어 있어, findAllByIsAiDerivedPost(null,…)에 따른 IS NULL 조회 리스크가 없습니다.

  • 리포지토리에 다음 메서드를 선택적으로 추가하면, PostType → 메서드 매핑 의도를 더욱 명시적으로 표현할 수 있습니다.

  Page<PostEntity> findAllByIsAiDerivedPost(Boolean isAiDerivedPost, Pageable pageable);
+ Page<PostEntity> findAllByIsAiDerivedPostTrue(Pageable pageable);
+ Page<PostEntity> findAllByIsAiDerivedPostFalse(Pageable pageable);
src/main/java/hanium/modic/backend/domain/ai/listener/AiImageCreatedListener.java (1)

47-49: postId 및 userId에 대한 NULL 안전성 검증

CreatedAiImageEntityAiRequestEntity 모두 postId/userId 필드에 nullable = false 제약이 설정되어 있어, JPA와 DB 레벨에서 해당 값이 null이 아님이 보장됩니다. 따라서 예외 없이 안전하게 사용할 수 있습니다.

  • CreatedAiImageEntity (src/main/java/hanium/modic/backend/domain/ai/domain/CreatedAiImageEntity.java)
    @Column(name = "post_id", nullable = false)
    private Long postId;
    
    @Column(name = "user_id", nullable = false)
    private Long userId;
  • AiRequestEntity (src/main/java/hanium/modic/backend/domain/ai/domain/AiRequestEntity.java)
    @Column(name = "post_id", nullable = false)
    private Long postId;
    
    @Column(name = "user_id", nullable = false)
    private Long userId;

결론적으로 AiImageCreatedListener.handleImageCreated()에서 호출하는

.postId(aiRequest.getPostId())
.userId(aiRequest.getUserId())

는 null이 될 가능성이 없어, 추가적인 null 체크(Objects.requireNonNull)는 선택적입니다.
– 빠른 실패(fail-fast)나 디버깅 편의를 위해 원한다면 Guard 추가를 고려하세요.
– 반드시 필요하지는 않습니다.

src/main/java/hanium/modic/backend/web/post/controller/PostController.java (2)

68-78: OpenAPI에 postType 허용값 명시로 클라이언트 UX 개선

문서 설명에만 (ALL, ORIGINAL, AI_DERIVED)가 언급되어 있습니다. 스키마에 allowableValues를 명시하면 스웨거 UI에서 자동 제약이 반영되어 오입력 방지에 도움이 됩니다.

+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
@@
 	@Operation(
 		summary = "게시글 목록 조회 API",
 		description = """
 			게시글 목록을 조회합니다. 정렬 기준, 페이지 번호, 페이지 크기, 포스트 타입을 입력받습니다.
 			postType은 (ALL, ORIGINAL, AI_DERIVED)가 존재한다.
 			""",
 		responses = {
 			@ApiResponse(responseCode = "400", description = "사용자 입력 오류[C-001]")
 		}
 	)
 	public ResponseEntity<AppResponse<PageResponse<GetPostsResponse>>> getPosts(
 		@RequestParam(required = false, defaultValue = "LATEST") String sort,
 		@RequestParam(required = false, defaultValue = "0") @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다") Integer page,
 		@RequestParam(required = false, defaultValue = "10") @Min(value = 10, message = "페이지 크기는 10 이상이어야 합니다.") @Max(value = 20, message = "페이지 크기는 20 이하여야 합니다.") Integer size,
-		@RequestParam(required = false, defaultValue = "ALL") PostType postType
+		@Parameter(
+			description = "포스트 타입 필터",
+			schema = @Schema(allowableValues = {"ALL","ORIGINAL","AI_DERIVED"})
+		)
+		@RequestParam(required = false, defaultValue = "ALL") PostType postType
 	) {

82-86: Enum 바인딩 대소문자 민감도 주의 및 기본값 문서화

Spring의 기본 Enum 바인딩은 대소문자에 민감합니다. 클라이언트가 "ai_derived"처럼 소문자/스네이크로 보낼 가능성이 있다면, WebDataBinder에 커스텀 컨버터를 추가해 유연하게 받거나, API 문서에 엄격 대문자 요구사항을 명시해 주세요.

필요 시 컨버터 추가 예시(다른 파일에 적용):

// @ControllerAdvice
@InitBinder
public void initBinder(WebDataBinder binder) {
  binder.registerCustomEditor(PostType.class, new PropertyEditorSupport() {
    @Override public void setAsText(String text) {
      setValue(PostType.valueOf(text.trim().toUpperCase(Locale.ROOT)));
    }
  });
}
src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java (1)

161-179: 삭제 성공 케이스: 서비스 위임 호출 검증 명확 — 추가로 이미지 정리 플로우도 고려

현재는 postService.deletePost 위임만 검증됩니다. 파생 포스트 특성상 생성 시 붙은 이미지 정리 정책이 있다면 해당 시나리오도 별도 단위/통합 테스트로 커버하는 것을 권장합니다.

src/test/java/hanium/modic/backend/web/post/controller/PublicPostControllerTest.java (1)

86-89: 주석-검증 불일치: 'null 값 확인'이지만 기대값은 false

비로그인 사용자의 좋아요 여부를 false로 기대하고 있으므로, 주석을 코드/서비스 동작에 맞게 정정해 주세요. (서비스 단에서도 동일 주석 불일치가 존재합니다. 아래 PostService 코멘트 참고)

-			.andExpect(jsonPath("$.data.isLikedByCurrentUser").value(false)) // null 값 확인
+			.andExpect(jsonPath("$.data.isLikedByCurrentUser").value(false)) // 비로그인 사용자는 false
src/main/java/hanium/modic/backend/domain/post/service/PostService.java (2)

72-73: 신규 생성 포스트의 기본값을 명시적으로 false로 설정 — 의도 명확하여 좋습니다

파생 포스트는 별도 플로우에서만 true가 되므로, 일반 생성 경로에서 false 지정은 명확합니다. 엔티티/DB 레벨에서도 기본값(default false)을 맞추면 일관성이 더 좋아집니다.


283-290: 타입별 조회 스위치 문 가독성/확장성 양호 — 리포지토리 쿼리 분배 적절

PostType이 확장될 가능성을 염두에 두면, 매핑 로직을 별도 Strategy/Map으로 분리하는 것도 선택지입니다. 현재 규모에서는 충분히 간결합니다.

src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (2)

12-26: spy로 ID 주입 대신 리플렉션으로 필드 주입을 권장합니다

Mockito.spy + when(getId())는 equals/hashCode, 프록시 동작에 미묘한 부작용을 만들 수 있습니다. 테스트 유틸에서는 리플렉션으로 private id를 직접 세팅하는 편이 더 단순하고 안정적입니다.

아래처럼 변경을 제안합니다:

-		PostEntity spyPost = Mockito.spy(post);
-		when(spyPost.getId()).thenReturn(id);
-
-		return spyPost;
+		try {
+			java.lang.reflect.Field idField = PostEntity.class.getDeclaredField("id");
+			idField.setAccessible(true);
+			idField.set(post, id);
+			return post;
+		} catch (NoSuchFieldException | IllegalAccessException e) {
+			throw new RuntimeException(e);
+		}

41-56: AI 파생 포스트 팩토리 도입은 적절합니다. 다만 가격 상수는 중복 제거를 권장합니다

두 메서드에서 동일 상수를 반복 사용합니다. 테스트 유지보수를 위해 클래스 상단에 상수로 추출해 사용하세요.

아래와 같이 상수화를 제안합니다(클래스 상단 추가):

// outside selected lines (추가 삽입용 참고 코드)
private static final long AI_COMMERCIAL_PRICE = 15_000L;
private static final long AI_NON_COMMERCIAL_PRICE = 8_000L;
private static final long AI_TICKET_PRICE = 5L;

그리고 현재 블록 내 숫자 리터럴을 다음과 같이 바꿔주세요:

-			.commercialPrice(15000L)
-			.nonCommercialPrice(8000L)
-			.ticketPrice(5L)
+			.commercialPrice(AI_COMMERCIAL_PRICE)
+			.nonCommercialPrice(AI_NON_COMMERCIAL_PRICE)
+			.ticketPrice(AI_TICKET_PRICE)

Also applies to: 58-67

src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerTest.java (4)

24-30: 불필요한 import 제거

CreatePostResponse import가 사용되지 않습니다. 정리해 주세요.

 import hanium.modic.backend.web.post.dto.request.CreateAiDerivedPostRequest;
-import hanium.modic.backend.web.post.dto.response.CreatePostResponse;

38-42: ObjectMapper는 스프링 빈을 주입받아 공통 설정을 따르세요

커스텀 직렬화 설정/모듈과의 일관성을 위해 new ObjectMapper() 대신 빈 주입을 권장합니다.

-	@Autowired
-	private MockMvc mockMvc;
-
-	private final ObjectMapper objectMapper = new ObjectMapper();
+	@Autowired
+	private MockMvc mockMvc;
+
+	@Autowired
+	private ObjectMapper objectMapper;

63-198: 경계 케이스 보강 제안: 공백-only 문자열, 0 값 허용 여부

  • 제목/설명에 공백만 있는 경우(" ")도 @notblank 위반 케이스로 추가하면 좋습니다.
  • 가격 필드(상업/비상업/티켓)에 대해 0이 허용 범위인지(현재 메시지는 "0 이상")를 성공/실패 케이스로 각각 명확히 커버하면 회귀에 유리합니다.

31-33: 성공 케이스(201/200) 반환 및 응답 본문 단언 추가를 제안합니다

현재는 실패/예외 흐름 위주입니다. 정상 생성(예: 201 Created, 혹은 200 OK) 시 응답 DTO 필드 단언을 추가해 회귀 방어력을 높이세요. 필요하다면 이미 import 된 CreatePostResponse를 활용할 수 있습니다.

Also applies to: 200-227

src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java (1)

254-264: 잘못된 파라미터 시 서비스 미호출 검증 추가 제안

입력 검증 실패 테스트에서 verify(postService, never()).getPosts(...) 단언을 추가하면 컨트롤러 레이어의 차단 책임을 더 확실히 담보할 수 있습니다.

src/main/java/hanium/modic/backend/domain/post/entity/PostEntity.java (2)

43-45: 조회/필터링 성능을 위해 인덱스 고려

목록에서 AI 파생/원본 필터링이 빈번하다면 is_ai_derived_post 컬럼에 인덱스를 추가하는 것을 권장합니다. 마이그레이션에 포함해 주세요.


46-63: Boolean 대신 primitive boolean + @Builder.Default도 선택지입니다

래퍼 타입(Boolean)은 불필요한 null 처리를 유발할 수 있습니다. 다음과 같이 단순화 가능하지만, 퍼블릭 API 영향 범위를 고려해 선택하세요.

예시(참고용, 외부 라인 변경 포함):

// outside selected lines (참고 코드)
@Builder.Default
@Column(name = "is_ai_derived_post", nullable = false)
private boolean aiDerivedPost = false;
src/test/java/hanium/modic/backend/domain/ai/entityfactory/AiFactory.java (1)

28-38: 리플렉션 대신 Spring ReflectionTestUtils 사용 권장 + 예외 메시지 현지화.

  • 테스트에서 private id 설정은 ReflectionTestUtils.setField가 간결하고 JDK 접근 제약에 안전합니다.
  • 예외 메시지("Failed to set id")는 프로젝트 내 다른 메시지들과 한글/영문 일관성이 맞는지 확인해 주세요.

아래 변경을 제안합니다.

-    try {
-      var field = CreatedAiImageEntity.class.getDeclaredField("id");
-      field.setAccessible(true);
-      field.set(entity, id);
-    } catch (Exception e) {
-      throw new RuntimeException("Failed to set id", e);
-    }
+    org.springframework.test.util.ReflectionTestUtils.setField(entity, "id", id);
src/main/java/hanium/modic/backend/web/post/dto/response/GetPostResponse.java (1)

27-27: AI 파생 여부(Boolean) → boolean 전환 고려.

isAiDerivedPost가 항상 도메인에서 결정 가능한 값이면 원시형 boolean이 더 간결하고 tri-state(Null)로 인한 의미적 모호성을 제거합니다. 현재 @JsonInclude(NON_NULL)로 null을 숨기지만, 해당 필드는 숨길 필요가 없어 보입니다. 단, 외부 API 스키마 영향이 있으므로 변경 시 버저닝 또는 릴리스 노트 반영 권장.

-   Boolean isAiDerivedPost,
+   boolean isAiDerivedPost,
...
-   postEntity.getIsAiDerivedPost(),
+   postEntity.getIsAiDerivedPost(),

Also applies to: 67-67

src/main/java/hanium/modic/backend/web/post/dto/response/GetPostsResponse.java (1)

17-17: 목록 응답의 AI 파생 여부도 boolean 전환 일관성 제안.

단건/목록 응답 모두에서 isAiDerivedPostboolean으로 통일하면 클라이언트 처리 단순화에 도움이 됩니다. 도입 시 호환성 고려 필요.

-   Boolean isAiDerivedPost,
+   boolean isAiDerivedPost,
...
-   postEntity.getIsAiDerivedPost(),
+   postEntity.getIsAiDerivedPost(),

Also applies to: 41-41

src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (4)

37-37: PostType 의존성 추가 OK.

서비스 시그니처 변경에 맞춘 PostType 반영이 적절합니다. 추후 ORIGINAL/AI_DERIVED 필터 케이스의 테스트도 보강해 주세요.


107-107: 생성 시 AI 파생 여부 검증 추가는 적절합니다 + ticketPrice 단언 보강 제안.

현재 타 필드 단언과 함께 ticketPrice에 대한 단언도 추가하면 회귀 방지에 유익합니다.

   assertThat(savedPost.getNonCommercialPrice()).isEqualTo(nonCommercialPrice);
+  assertThat(savedPost.getTicketPrice()).isEqualTo(ticketPrice);
   assertThat(savedPost.getIsAiDerivedPost()).isFalse();

311-312: getPosts 시그니처 변경 반영 OK — PostType.ALL.

정렬/페이징은 mocking으로 충분합니다. 추가로 ORIGINAL/AI_DERIVED 각각 필터링 테스트가 있으면 좋겠습니다(리포지토리 쿼리 호출 브랜치까지 검증 가능).


149-149: PostServiceTest에 AI 파생 포스트(isAiDerivedPost=true) 테스트 추가 권장

단건 조회(좋아요 O)에서 isAiDerivedPost()false인 케이스는 정상적으로 검증되고 있습니다.
그러나 AI로 생성된 포스트(isAiDerivedPost == true)에 대한 단위 테스트가 현재 존재하지 않음을 확인했습니다.

– 파일: src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java
– 추가할 내용:
• AI 파생 포스트인 경우(isAiDerivedPost == true)를 모킹하고,
• 조회 응답의 response.isAiDerivedPost()true임을 검증하는 테스트 메서드

이 케이스를 별도 테스트로 구현하면 도메인 기본값 변경 시 회귀를 방지하는 데 도움이 됩니다.

src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java (3)

53-55: Long 비교 시 NPE 예방을 위해 Objects.equals 사용 권장

현재 createdAiImage.getUserId().equals(userId)getUserId()가 null일 경우 NPE가 발생할 수 있습니다. 서비스 계층은 방어적으로 두는 편이 안전합니다.

아래와 같이 변경을 제안합니다.

+import java.util.Objects;
...
-        if (!createdAiImage.getUserId().equals(userId)) {
+        if (!Objects.equals(createdAiImage.getUserId(), userId)) {
             throw new AppException(ErrorCode.AI_IMAGE_ACCESS_DENIED_EXCEPTION);
         }

101-103: Boolean 래퍼 null-safe 체크

post.getIsAiDerivedPost()가 래퍼(Boolean)라면 NPE 위험이 있으므로 null-safe 비교 권장.

-        if (!post.getIsAiDerivedPost()) {
+        if (!Boolean.TRUE.equals(post.getIsAiDerivedPost())) {
             throw new AppException(ErrorCode.NOT_AI_DERIVED_POST_EXCEPTION);
         }

95-107: 권한 검증 책임 일원화 검토

AiDerivedPostService.deleteAiDerivedPost에서 이미 엔티티를 조회한 뒤

  • if (!post.getUserId().equals(userId)) throw AppException(POST_ACCESS_DENIED_EXCEPTION);
    로 권한을 검증하고,
    이후 내부적으로 PostService.deletePost(userId, postId)를 호출하며
  • postEntityRepository.findById(postId)validatePostRole(userId, post.getUserId())(POST_ROLE_EXCEPTION)
    를 다시 수행하고 있습니다.

이로 인해

  • 중복 조회/검증으로 인한 비용 증가
  • 서로 다른 ErrorCode 사용에 따른 일관성 저해

가 발생할 수 있으므로, 아래 중 한 가지 방안을 선택적으로 고려해 보시길 권장드립니다.

  • AiDerivedPostService에서의 권한 검증 로직 제거 후 PostService.deletePost(userId, postId)에 위임하여 에러 코드 통일
  • 이미 조회·검증된 PostEntity를 사용하는 deletePost(PostEntity post) 오버로드 추가

위 리팩터링은 현 시점에서 필수 사항은 아니지만, 코드 가독성과 유지보수성을 높이는 데 도움이 될 것입니다.

src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerIntegrationTest.java (6)

62-73: 테스트 데이터의 postId 하드코딩 제거 권장

CreatedAiImageEntity 생성 시 postId(999L)는 실제 흐름과 맞지 않고(파생 포스트 생성 전), 도메인 규칙에 혼란을 줄 수 있습니다. null 또는 미설정이 더 자연스럽습니다.

-        CreatedAiImageEntity createdAiImage = CreatedAiImageEntity.builder()
+        CreatedAiImageEntity createdAiImage = CreatedAiImageEntity.builder()
             .userId(currentUser.getId())
-            .postId(999L) // 임시 값
+            // 파생 포스트 생성 전이므로 postId는 설정하지 않음
             .requestId("test-request-123")

85-99: 응답으로 반환된 postId를 사용해 단언 강화

타이틀로 검색하여 첫 번째 엔티티를 찾는 대신, 응답의 postId를 파싱해 정확한 엔티티를 로드하면 테스트가 더 견고해집니다(동일 타이틀이 여러 개일 때도 안전).

-        // then
-        result.andExpect(status().isCreated())
-            .andExpect(jsonPath("$.data.postId").exists());
-
-        // 데이터베이스에서 검증
-        PostEntity savedPost = postEntityRepository.findAll().stream()
-            .filter(post -> "AI Generated Post".equals(post.getTitle()))
-            .findFirst()
-            .orElseThrow(() -> new AssertionError("AI 파생 포스트가 데이터베이스에 저장되지 않았습니다"));
+        // then
+        result.andExpect(status().isCreated())
+            .andExpect(jsonPath("$.data.postId").exists());
+
+        String body = result.andReturn().getResponse().getContentAsString();
+        Long postId = objectMapper.readTree(body).path("data").path("postId").asLong();
+
+        // 데이터베이스에서 검증
+        PostEntity savedPost = postEntityRepository.findById(postId)
+            .orElseThrow(() -> new AssertionError("AI 파생 포스트가 데이터베이스에 저장되지 않았습니다"));

108-115: 포스트 이미지 단언 보강(리스트 크기 및 NPE 방지)

get(0) 호출 전 리스트가 비어있지 않은지 확인하면 실패 시 메시지가 더 명확합니다.

-        // 포스트 이미지 검증
-        PostImageEntity savedPostImage = postImageEntityRepository.findAllByPostId(savedPost.getId()).get(0);
+        // 포스트 이미지 검증
+        var savedImages = postImageEntityRepository.findAllByPostId(savedPost.getId());
+        assertThat(savedImages).isNotEmpty();
+        PostImageEntity savedPostImage = savedImages.get(0);

161-173: 다른 사용자 AI 이미지 테스트에서도 postId 하드코딩 제거

위와 동일한 이유로 postId(999L) 설정을 제거하는 편이 자연스럽습니다.

-        CreatedAiImageEntity otherUserAiImage = CreatedAiImageEntity.builder()
+        CreatedAiImageEntity otherUserAiImage = CreatedAiImageEntity.builder()
             .userId(otherUser.getId())
-            .postId(999L)
+            // 파생 포스트 생성 전이므로 postId는 설정하지 않음
             .requestId("other-user-request")

227-240: S3 객체 삭제/보존 시맨틱 검증 테스트 추가 제안

파생 포스트 삭제가 S3 물리 파일에 미치는 영향을 검증하는 통합 테스트가 있으면(원본 AI 이미지와 파생 포스트 이미지의 키가 분리되었는지, 삭제 시 어떤 키가 제거되는지) 회귀를 방지할 수 있습니다. 현재 클래스에 AmazonS3/S3Properties가 주입돼 있으므로 다음 시나리오 추가를 제안합니다.

  • 생성: AI 응답 키(src) 업로드 → 파생 포스트 생성 시 posts/ 하위 키(dst) 복사 여부 확인
  • 삭제: 파생 포스트 삭제 후 dst만 제거되고 src는 남는지 확인

필요 시 테스트 템플릿 생성을 도와드리겠습니다.


39-51: 미사용 의존성 정리로 테스트 단순화

AmazonS3, S3Properties가 본 클래스 내에서 사용되지 않습니다(향후 테스트 추가 예정이 아니라면 제거 권장). 테스트 실행 시간/가독성에 작은 도움이 됩니다.

-    @Autowired
-    private AmazonS3 amazonS3;
-    @Autowired
-    private S3Properties s3Properties;
+    // 필요 시 S3 관련 통합 테스트에서만 주입하세요.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 22e9c94 and f99662a.

📒 Files selected for processing (22)
  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/domain/CreatedAiImageEntity.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/listener/AiImageCreatedListener.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/post/entity/PostEntity.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/post/enums/PostType.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/post/repository/PostEntityRepository.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/post/service/PostService.java (5 hunks)
  • src/main/java/hanium/modic/backend/web/post/controller/AiDerivedPostController.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/post/controller/PostController.java (2 hunks)
  • src/main/java/hanium/modic/backend/web/post/dto/request/CreateAiDerivedPostRequest.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/post/dto/response/GetPostResponse.java (2 hunks)
  • src/main/java/hanium/modic/backend/web/post/dto/response/GetPostsResponse.java (2 hunks)
  • src/main/resources/application.yml (1 hunks)
  • src/test/java/hanium/modic/backend/domain/ai/entityfactory/AiFactory.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (2 hunks)
  • src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (8 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerIntegrationTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java (4 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/PublicPostControllerTest.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerIntegrationTest.java (1)
src/test/java/hanium/modic/backend/base/login/ContextHolderUtil.java (1)
  • ContextHolderUtil (10-25)
src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java (3)
src/test/java/hanium/modic/backend/domain/ai/entityfactory/AiFactory.java (1)
  • AiFactory (7-39)
src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (1)
  • PostFactory (10-69)
src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
  • UserFactory (9-29)
src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java (1)
src/main/java/hanium/modic/backend/domain/post/service/PostService.java (1)
  • Service (36-291)
src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerTest.java (1)
src/test/java/hanium/modic/backend/base/BaseControllerTest.java (1)
  • BaseControllerTest (13-49)
src/main/java/hanium/modic/backend/web/post/controller/AiDerivedPostController.java (1)
src/main/java/hanium/modic/backend/web/post/controller/PostController.java (1)
  • RestController (38-136)
🪛 GitHub Actions: compile-test
src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java

[error] 218-218: Command './gradlew test -Dspring.profiles.active=test' failed: NullPointerException.


[error] 196-196: Command './gradlew test -Dspring.profiles.active=test' failed: NullPointerException.

src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerTest.java

[error] 202-202: InvalidUseOfMatchersException - 게시물 목록 조회 실패 - 기본 파라미터.

src/test/java/hanium/modic/backend/web/post/controller/PublicPostControllerTest.java

[error] 202-202: InvalidUseOfMatchersException - 게시물 목록 조회 실패 - 기본 파라미터.

src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java

[error] 202-202: InvalidUseOfMatchersException - 게시물 목록 조회 실패 - 기본 파라미터.

🪛 YAMLlint (1.37.1)
src/main/resources/application.yml

[error] 3-3: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (24)
src/main/java/hanium/modic/backend/domain/post/enums/PostType.java (1)

3-7: PostType 정의 자체는 명확합니다. LGTM

  • ALL/ORIGINAL/AI_DERIVED로 사용처(서비스/리포지토리) 매핑이 직관적입니다.
src/main/java/hanium/modic/backend/domain/post/repository/PostEntityRepository.java (1)

9-15: 리포지토리 확장은 적절합니다. LGTM

  • 페이징/정렬은 Pageable에서 제어되며, PostType 분기에 필요한 최소 쿼리 구성이 갖춰졌습니다.
src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java (6)

104-128: AI 이미지 미존재 시 예외 경로 테스트 적절 — 저장 호출 금지 검증 LGTM

존재하지 않는 경우 save 호출이 없음을 검증하고 있으며, ErrorCode도 올바르게 단언하고 있습니다.


130-159: 타 사용자 이미지 접근 차단 케이스 적절 — 저장 호출 금지 검증 LGTM

권한 차단 흐름과 레포지토리 호출 제약 검증이 명확합니다.


181-197: 삭제 실패(미존재) 경로 테스트 적절 — 예외 코드 단언 LGTM


199-219: 삭제 실패(권한 없음) 경로 테스트 적절 — 외부 서비스 호출 없음 검증 LGTM


221-240: 삭제 실패(비-파생 포스트) 경로 테스트 적절 — 도메인 제약 보호


32-49: AiDerivedPostServiceTest의 의존성은 이미 모두 모킹되어 있습니다
AiDerivedPostService가 @requiredargsconstructor로 주입받는 네 가지 필드
(CreatedAiImageRepository, PostEntityRepository, PostImageEntityRepository, PostService)
모두 테스트 클래스에 @mock으로 선언되어 있으므로, 추가 모킹은 불필요합니다.

– 테스트 클래스: src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java
– 서비스 클래스 의존성:
• CreatedAiImageRepository
• PostEntityRepository
• PostImageEntityRepository
• PostService

NPE가 발생한다면, 다른 원인(예: 스텁 설정 누락, 테스트 데이터 준비 미흡 등)을 점검해 주세요.

Likely an incorrect or invalid review comment.

src/test/java/hanium/modic/backend/web/post/controller/PublicPostControllerTest.java (1)

68-69: 신규 응답 필드 isAiDerivedPost 단언 추가 LGTM

공개 조회 응답에서 파생 여부 노출이 명확히 검증됩니다.

src/main/java/hanium/modic/backend/domain/post/service/PostService.java (1)

150-156: getPosts 시그니처 변경 반영 완료
검증 결과, 모든 Mockito 스텁 및 호출부가 4번째 인자(PostType)까지 올바르게 업데이트된 것이 확인되었습니다. 더 이상의 수정은 필요하지 않습니다.

  • src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java (202행, 229행): when() 스텁에 PostType.ALL 인자 반영
  • src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (311행, 361행, 390행): getPosts 호출부에 PostType.ALL 인자 반영
src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (1)

20-20: isAiDerivedPost(false) 명시 추가 좋습니다

기존(원본) 포스트 팩토리에서 AI 파생 여부를 명확히 false로 고정한 점이 테스트의 의도를 분명히 해줍니다.

Also applies to: 37-39

src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerTest.java (2)

43-61: 유효성 실패 시 서비스 미호출 검증 포함: 훌륭합니다

입력 검증 실패 시 서비스 레이어 호출이 차단됐는지 검증하는 패턴이 잘 적용되어 있습니다.


200-227: 예외-HTTP 상태 매핑 테스트 케이스 충실

404/403/400 매핑과 메시지 단언이 명확합니다. 컨트롤러의 예외 전략을 신뢰할 수 있게 해줍니다.

Also applies to: 230-257, 258-271, 273-287, 288-301, 303-316

src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java (1)

193-201: 목 데이터 구성과 페이지 응답 단언이 명확합니다

목 페이지 구성과 페이징 단언이 간결하고 이해하기 쉽습니다.

src/main/java/hanium/modic/backend/domain/ai/domain/CreatedAiImageEntity.java (2)

40-54: 빌더 확장은 일관적입니다

상위 Image 생성자 호출 후 신규 필드 할당이 명확합니다. 도메인 불변식(요청-유저-포스트 연계)이 코드 레벨에서 강제되는 것도 좋습니다.


34-39: 마이그레이션·백필 스크립트 확인 필요
현재 src/main/java/hanium/modic/backend/domain/ai/domain/CreatedAiImageEntity.javapost_id·user_id 컬럼이 nullable=false로 추가되었으나, 아래 경로에서 관련 Flyway/Liquibase 변경셋을 찾을 수 없습니다.

  • src/main/resources/db/migration
  • src/main/resources/db/changelog

또한, 엔티티의 @Table 어노테이션에는 기존에 request_id만 인덱스로 정의되어 있어 post_id·user_id 조회 성능을 위해 별도 인덱스 추가 검토가 필요합니다.

확인 요청 사항

  • post_id·user_id 컬럼에 대한 마이그레이션(DDL 변경셋) 파일이 실제로 존재하는지 검증
  • 백필(backfill) 로직 또는 적절한 기본값 설정 포함 여부 확인
  • post_id, user_id 조회 패턴에 따른 인덱스 추가 여부 검토

요구하신 스크립트 검증 결과 마이그레이션 파일이 발견되지 않아, 수동 확인을 부탁드립니다.

src/main/java/hanium/modic/backend/domain/post/entity/PostEntity.java (1)

43-45: AI 파생 여부 필드 추가 및 기본값 처리 적절

null 방지를 위한 기본값(false) 처리로 DB/애플리케이션 간 일관성이 좋아졌습니다.

Also applies to: 62-63

src/test/java/hanium/modic/backend/domain/ai/entityfactory/AiFactory.java (1)

12-23: 확인 완료: imagePurpose 파라미터는 ImagePrefix 타입입니다.

  • CreatedAiImageEntity의 빌더 시그니처에 정의된 파라미터는 아래와 같습니다.
    ... ImageExtension extension, ImagePrefix imagePurpose, String requestId, Long postId, Long userId
    ImagePrefix.AI_RESPONSE 전달이 타입 불일치 없이 올바르게 동작합니다.

따라서 ImagePrefix vs ImagePurpose 혼동 우려는 없습니다.

src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (4)

184-184: 단건 조회(좋아요 X)에서도 기본값 검증 OK.

중복이지만 의도 명확합니다.


241-241: 공개 단건 조회에서도 값 검증 OK.

비로그인 시나리오까지 커버되어 신뢰도 높습니다.


361-362: 목록 조회 성공 케이스에서도 PostType 적용 OK.

중복 호출 경로 검증에 유용합니다.


390-391: 게시글 없음 예외 케이스도 새 시그니처로 반영 OK.

예외 코드 검증까지 포함되어 적절합니다.

src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java (2)

18-21: 클래스 수준 readOnly 기본 + 메서드 단위 트랜잭션 전환 설계 좋습니다

쓰기 연산이 필요한 공개 메서드에만 명시적으로 @transactional을 부여한 점이 깔끔합니다.


82-83: 해당 스크립트로 AiRequestEntity의 내용을 확인하겠습니다.
완료되면 다시 안내드리겠습니다.

Comment on lines +84 to +85
AI_IMAGE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "AI-010", "생성된 AI 이미지를 찾을 수 없습니다."),
AI_IMAGE_ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "AI-011", "AI 이미지에 대한 권한이 없습니다."),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

AI 이미지 ‘미존재’ 에러가 중복/상충합니다: CREATED_AI_IMAGE_NOT_FOUND(A-002) vs AI_IMAGE_NOT_FOUND_EXCEPTION(AI-010)

  • 의미와 한글 메시지가 사실상 동일합니다. 서로 다른 코드 스킴(A-xxx vs AI-xxx)과 함께 공존하면 클라이언트/문서화에 혼란을 줍니다.
  • 본 PR의 서비스는 AI_IMAGE_NOT_FOUND_EXCEPTION을 사용하고, 기존 코드들은 CREATED_AI_IMAGE_NOT_FOUND를 사용할 가능성이 큽니다. 하나로 통일하고 나머지는 제거(또는 @deprecated) + 전면 치환을 권장합니다.

정리 옵션:

  • 옵션 A(권장): 기존 CREATED_AI_IMAGE_NOT_FOUND(A-002)로 통일 → 본 PR의 참조를 해당 상수로 변경.
  • 옵션 B: 새 상수로 통일 → 레거시 참조를 일괄 치환하고, 코드 규칙을 ‘AI-xxx’로 재정의.

우선순위: “비즈니스 코드(code) 값의 유일성” 보장.

확인 스크립트:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== 두 상수 참조 위치 =="
rg -nP '\b(CREATED_AI_IMAGE_NOT_FOUND|AI_IMAGE_NOT_FOUND_EXCEPTION)\b' src/main/java || true

echo
echo "== ErrorCode 중복 code 값 점검 =="
python - << 'PY'
import re, collections, pathlib
p = pathlib.Path("src/main/java/hanium/modic/backend/common/error/ErrorCode.java")
s = p.read_text(encoding="utf-8")
codes = re.findall(r'\(\s*HttpStatus\.[A-Z_]+,\s*"([^"]+)"', s)
for k,v in collections.Counter(codes).items():
    if v>1:
        print(f"DUP {k}: {v} occurrences")
PY

Length of output: 1244


AI 이미지 관련 ErrorCode 상수 중복 제거 및 통합 필요

현재 CREATED_AI_IMAGE_NOT_FOUND("A-002")AI_IMAGE_NOT_FOUND_EXCEPTION("AI-010")가 동일 의미(“생성된 AI 이미지를 찾을 수 없습니다”)를 가지며, 코드 값 "A-002"가 중복 사용되고 있어 비즈니스 코드 유일성이 훼손되고 있습니다.

점검된 참조 위치:

  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java
    • 라인 76: CREATED_AI_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "A-002", …)
    • 라인 84: AI_IMAGE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "AI-010", …)
  • src/main/java/hanium/modic/backend/domain/ai/service/CreatedAiImageService.java 라인 23, 31 (CREATED_AI_IMAGE_NOT_FOUND)
  • src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java 라인 97 (CREATED_AI_IMAGE_NOT_FOUND)
  • src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java 라인 51 (AI_IMAGE_NOT_FOUND_EXCEPTION)

권장 통합 방안:

  • 옵션 A (권장):
    기존 CREATED_AI_IMAGE_NOT_FOUND("A-002")로 통일 → AI_IMAGE_NOT_FOUND_EXCEPTION 제거 또는 @Deprecated 후 모든 참조를 변경
  • 옵션 B:
    새 스킴인 AI_IMAGE_NOT_FOUND_EXCEPTION("AI-010")로 통일 → CREATED_AI_IMAGE_NOT_FOUND 제거 또는 @Deprecated 후 참조 일괄 변경

비즈니스 에러 코드의 유일성 보장을 위해 위 중 하나를 선택하여 전면 통합 및 중복 코드 제거를 반드시 진행해 주세요.

🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/common/error/ErrorCode.java (lines ~76 and
84-85) there are duplicate AI image error constants (CREATED_AI_IMAGE_NOT_FOUND
with code "A-002" and AI_IMAGE_NOT_FOUND_EXCEPTION with code "AI-010") causing
code value duplication; choose option A (recommended): keep
CREATED_AI_IMAGE_NOT_FOUND("A-002"), remove or mark AI_IMAGE_NOT_FOUND_EXCEPTION
as @Deprecated and delete it after migrating usages; update all call sites
(src/main/java/hanium/modic/backend/domain/ai/service/CreatedAiImageService.java
lines ~23 and ~31,
src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java
line ~97,
src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java
line ~51) to reference CREATED_AI_IMAGE_NOT_FOUND, run tests/compile to ensure
no remaining references, and then remove the redundant enum entry.

Comment on lines +86 to +87
POST_ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "P-003", "포스트에 대한 접근 권한이 없습니다."),
NOT_AI_DERIVED_POST_EXCEPTION(HttpStatus.BAD_REQUEST, "P-004", "AI 파생 포스트가 아닙니다."),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

POST 권한 관련 코드가 이중화되어 있습니다: POST_ROLE_EXCEPTION(P-002) vs POST_ACCESS_DENIED_EXCEPTION(P-003)

  • 메시지도 유사하여 사용 기준이 모호합니다(역할 기반 접근 거부 vs 소유권/권한 거부?). 하나로 통일하거나, 명확히 의미를 분리(예: ROLE/OWNERSHIP)하고 메시지를 구분해 주세요.
  • 클라이언트는 P-002/P-003을 다르게 처리할 수 있으므로, 문서화와 사용 기준을 확정하는 것이 좋습니다.

필요 시 전체 참조 위치 치환 PR을 함께 제안드릴 수 있습니다.


🏁 Script executed:

#!/bin/bash
# POST_ROLE_EXCEPTION 정의 위치 및 메시지 중복 여부 확인
rg -nP "POST_ROLE_EXCEPTION" -C2 src

Length of output: 1017


🏁 Script executed:

#!/bin/bash
# POST_ACCESS_DENIED_EXCEPTION 사용 위치 확인
rg -nP "POST_ACCESS_DENIED_EXCEPTION" -C2 src

Length of output: 4043


POST 권한 예외 코드 중복 – 의미 명확화 또는 통합 필요

현재 ErrorCode.java에 아래 두 예외가 모두 “포스트 소유자 불일치” 상황에서 사용되고 있어 의미가 중복되고, 클라이언트가 처리 기준을 혼동할 수 있습니다.

발견 위치

  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java
    • POST_ROLE_EXCEPTION (P-002) – “포스트에 대한 권한이 없습니다.”
    • POST_ACCESS_DENIED_EXCEPTION (P-003) – “포스트에 대한 접근 권한이 없습니다.”
  • 사용처
    • PostService.java (245–249행): 소유자 불일치 시 POST_ROLE_EXCEPTION 발생
    • AiDerivedPostService.java (95–99행): 소유자 불일치 시 POST_ACCESS_DENIED_EXCEPTION 발생

제안

  • 의미가 동일하다면 코드(P-002/P-003)와 메시지를 하나로 통합
  • 아니면 역할 기반 접근 거부(ROLE) vs 소유권 기반 접근 거부(OWNERSHIP)를 명확히 분리하고,
    • 예외명과 메시지를 구체적으로 구분 (예: POST_OWNERSHIP_DENIED, POST_ROLE_DENIED)
    • API 문서 및 클라이언트 처리 가이드에 기준을 추가

필요 시 전체 참조 위치 치환 PR을 별도 제안해 드릴 수 있습니다.

🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/common/error/ErrorCode.java around lines
86–87, two error codes represent the same post-access/ownership denial which
causes ambiguity (POST_ROLE_EXCEPTION P-002 vs POST_ACCESS_DENIED_EXCEPTION
P-003); either merge them into a single canonical error code (pick P-002 or
P-003) and a single clear message, then replace all references (e.g.,
PostService.java lines ~245–249 and AiDerivedPostService.java lines ~95–99) to
use that one enum constant, OR explicitly split semantics by renaming to
ownership vs role (e.g., POST_OWNERSHIP_DENIED and POST_ROLE_DENIED), update
their messages to be distinct, then update all call sites, unit tests, and API
docs to reflect the chosen canonical names and codes; ensure compilation by
adjusting imports/usages and run tests after refactoring.

Comment on lines +68 to +83
PostEntity savedPost = postEntityRepository.save(aiDerivedPost);

// AI 이미지를 포스트 이미지로 변환
PostImageEntity postImage = PostImageEntity.builder()
.imagePath(createdAiImage.getImagePath())
.fullImageName(createdAiImage.getFullImageName())
.imageName(createdAiImage.getImageName())
.extension(createdAiImage.getExtension())
.imagePurpose(createdAiImage.getImagePurpose())
.build();
postImage.updatePost(savedPost);

postImageEntityRepository.save(postImage);

return CreatePostResponse.of(savedPost.getId());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

통계 초기화(좋아요 등) 누락 가능성 — PostService.createPost와 행위 일치 필요

PostService.createPost는 생성 직후 asyncPostStatisticsService.initializeStatistics(postId)를 호출합니다. 여기서는 직접 PostEntityRepository/PostImageEntityRepository로 생성하여 통계 초기화가 빠져 있습니다. 통계 테이블을 사용해 카운트를 관리한다면 조회 시 불일치가 생길 수 있습니다.

두 가지 방안 중 하나를 권장합니다.

  • 본 서비스에 AsyncPostStatisticsService를 주입해 초기화 호출 추가.
  • 혹은 PostService에 "AI 파생 포스트 생성" 전용 팩토리/메서드를 추가하여 모든 생성 경로를 한곳으로 통합.

예시 diff:

+import hanium.modic.backend.domain.post.service.AsyncPostStatisticsService;
...
 public class AiDerivedPostService {
 ...
     private final PostService postService;
+    private final AsyncPostStatisticsService asyncPostStatisticsService;
 ...
     PostEntity savedPost = postEntityRepository.save(aiDerivedPost);
 ...
     postImageEntityRepository.save(postImage);
+
+    // 통계 초기화: createPost와 동일한 행위 보장
+    asyncPostStatisticsService.initializeStatistics(savedPost.getId());

검증 스크립트(통계 초기화 사용 여부와 의존성 명 확인):


🏁 Script executed:

#!/bin/bash
rg -nP "AsyncPostStatisticsService" -C3
rg -nP "initializeStatistics\\s*\\(" -C3

Length of output: 6889


AI 파생 포스트 생성 시 통계 초기화 누락
AiDerivedPostService에서 PostService.createPost와 달리 asyncPostStatisticsService.initializeStatistics(postId) 호출이 빠져 있어, 통계 테이블에 신규 행이 만들어지지 않습니다. 통계 기반 카운트 조회 시 불일치나 예외가 발생할 수 있습니다.

해결 방안 (둘 중 하나 선택):

  • AsyncPostStatisticsService 주입 및 호출 추가
    AiDerivedPostService에 AsyncPostStatisticsService를 주입하고, postImageEntityRepository.save(postImage) 직후 initializeStatistics(savedPost.getId())를 호출합니다.
  • 생성 로직 통합
    PostService에 “AI 파생 포스트 생성” 전용 메서드를 추가해 모든 포스트 생성 경로를 한곳으로 모읍니다.

수정 위치:

  • src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java

예시 diff:

+ import hanium.modic.backend.domain.postLike.service.AsyncPostStatisticsService;
   public class AiDerivedPostService {
       private final PostEntityRepository postEntityRepository;
       private final PostImageEntityRepository postImageEntityRepository;
+      private final AsyncPostStatisticsService asyncPostStatisticsService;
       …

       public CreatePostResponse createAiDerivedPost(…) {
           PostEntity savedPost = postEntityRepository.save(aiDerivedPost);

           // AI 이미지를 포스트 이미지로 변환
           PostImageEntity postImage = PostImageEntity.builder()
               …  
               .build();
           postImage.updatePost(savedPost);

           postImageEntityRepository.save(postImage);
+          // PostService.createPost와 동일하게 통계 초기화
+          asyncPostStatisticsService.initializeStatistics(savedPost.getId());

           return CreatePostResponse.of(savedPost.getId());
       }
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java
around lines 68-83, the code saves the post and image but omits initializing
post statistics; inject AsyncPostStatisticsService into AiDerivedPostService and
call asyncPostStatisticsService.initializeStatistics(savedPost.getId())
immediately after postImageEntityRepository.save(postImage); alternatively,
refactor to delegate AI-derived post creation to PostService (adding a dedicated
method that handles saving post, image and initializing statistics) so all
post-creation paths share the same initialization logic.

Comment on lines +70 to +81
// AI 이미지를 포스트 이미지로 변환
PostImageEntity postImage = PostImageEntity.builder()
.imagePath(createdAiImage.getImagePath())
.fullImageName(createdAiImage.getFullImageName())
.imageName(createdAiImage.getImageName())
.extension(createdAiImage.getExtension())
.imagePurpose(createdAiImage.getImagePurpose())
.build();
postImage.updatePost(savedPost);

postImageEntityRepository.save(postImage);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

AI 원본 S3 오브젝트를 그대로 참조 → 파생 포스트 삭제 시 원본까지 삭제될 위험

PostImageEntity를 생성하면서 CreatedAiImageEntityimagePathimagePurpose(AI_RESPONSE)를 그대로 복제하고 있습니다. 이후 postService.deletePostPostImageService.deleteImage를 통해 S3 오브젝트를 삭제한다면, 파생 포스트 삭제가 AI 생성 원본 오브젝트까지 삭제하는 부작용을 일으킬 수 있습니다(원본/파생이 동일 키를 참조).

권장안(택1 또는 조합):

  • 파생 포스트용으로 S3 객체를 새 경로(예: posts/)에 copy하고 imagePurpose를 게시물 전용(prefix)으로 설정.
  • 삭제 로직에서 ImagePrefix.AI_RESPONSE인 경우 물리 파일 삭제를 건너뛰고 참조만 제거(레퍼런스 카운팅 등).
  • 도메인 규칙상 원본과 동일 파일을 공유하더라도, 최소한 목적(purpose)은 POST 계열로 분리하여 삭제 필터링이 가능하도록 보장.

가능한 최소 변경 diff 예시(실제 enum/유틸 이름에 맞게 조정 필요):

-        PostImageEntity postImage = PostImageEntity.builder()
-            .imagePath(createdAiImage.getImagePath())
-            .fullImageName(createdAiImage.getFullImageName())
-            .imageName(createdAiImage.getImageName())
-            .extension(createdAiImage.getExtension())
-            .imagePurpose(createdAiImage.getImagePurpose())
-            .build();
+        // 1) S3 오브젝트를 게시물 전용 경로로 복사한 뒤 새 경로/목적을 사용하도록 전환하세요.
+        //    (예: postImageService.copyFromExistingKey(createdAiImage.getImagePath()) 사용 또는 AmazonS3.copyObject 사용)
+        final String postImagePath = /* TODO: posts/ 경로로 복사한 새 key 생성 */;
+        PostImageEntity postImage = PostImageEntity.builder()
+            .imagePath(postImagePath)
+            .fullImageName(createdAiImage.getFullImageName())
+            .imageName(createdAiImage.getImageName())
+            .extension(createdAiImage.getExtension())
+            .imagePurpose(ImagePrefix.POST_IMAGE /* 혹은 프로젝트에서 사용하는 게시물 전용 prefix */)
+            .build();

직접 S3 복사를 수행하는 경우(참고용 코드 스니펫):

String srcBucket = s3Properties.getBucket();
String srcKey = createdAiImage.getImagePath();
String dstKey = imagePathUtil.buildPostImageKey(savedPost.getId(), createdAiImage.getExtension());

amazonS3.copyObject(srcBucket, srcKey, srcBucket, dstKey);
// 이후 postImagePath = dstKey, imagePurpose = POST_IMAGE 등으로 저장

삭제 정책이 어떻게 구현돼있는지 확인이 필요하면 아래 스크립트로 점검 가능합니다.


🏁 Script executed:

#!/bin/bash
# PostImageService 삭제 구현과 ImagePrefix 용도 확인
rg -nP "class\\s+PostImageService\\b" -C5
rg -nP "deleteImage\\s*\\(" -C5
rg -nP "enum\\s+ImagePrefix\\b" -C5
rg -nP "\\bimagePurpose\\b" src/main/java -C2

Length of output: 41831


AI 원본 이미지 삭제 위험 – 파생 포스트용 S3 객체 복사 및 용도 분리 필요
현재 AiDerivedPostService에서 생성된 AI 이미지의 S3 키(imagePath)와 용도(imagePurpose)를 그대로 복제하여 PostImageEntity로 저장하고 있습니다. 이 상태에서 게시글 삭제 시 PostImageService.deleteImage가 해당 키를 물리 삭제하기 때문에, 원본 AI 이미지(ImagePrefix.AI_RESPONSE)도 함께 삭제되는 심각한 부작용이 발생합니다.

조치 방안(택1 또는 조합):

  • AiDerivedPostService에서 파생 포스트용 S3 객체를 새로운 키(예: posts/{postId}/… 경로)로 복사하고, 저장 시 imagePurposeImagePrefix.POST로 변경합니다.
  • 또는, 삭제 로직(PostImageService.deleteImage)에서 imagePurpose == ImagePrefix.AI_RESPONSE인 경우 실제 S3 삭제(imageUtil.deleteImage)를 건너뛰고, DB 참조만 제거하도록 분기 처리합니다.

변경이 필요한 위치:

  • src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java (약 70–81행)
  • (선택) src/main/java/hanium/modic/backend/domain/post/service/PostImageService.javadeleteImage 메서드에 용도별 분기 추가

예시 최소 변경안:

--- a/src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java
+++ b/src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java
@@ -73,8 +73,16 @@ public class AiDerivedPostService {
-       PostImageEntity postImage = PostImageEntity.builder()
-           .imagePath(createdAiImage.getImagePath())
-           .fullImageName(createdAiImage.getFullImageName())
-           .imageName(createdAiImage.getImageName())
-           .extension(createdAiImage.getExtension())
-           .imagePurpose(createdAiImage.getImagePurpose())
-           .build();
+       // 1) S3에서 원본 AI 이미지 복사 후 새로운 키(post 전용 경로) 사용
+       String dstKey = imagePathUtil.buildPostImageKey(savedPost.getId(), createdAiImage.getExtension());
+       amazonS3.copyObject(
+           s3Properties.getBucketName(),
+           createdAiImage.getImagePath(),
+           s3Properties.getBucketName(),
+           dstKey
+       );
+       PostImageEntity postImage = PostImageEntity.builder()
+           .imagePath(dstKey)
+           .fullImageName(createdAiImage.getFullImageName())
+           .imageName(createdAiImage.getImageName())
+           .extension(createdAiImage.getExtension())
+           .imagePurpose(ImagePrefix.POST)
+           .build();
        postImage.updatePost(savedPost);
        postImageEntityRepository.save(postImage);
--- a/src/main/java/hanium/modic/backend/domain/post/service/PostImageService.java
+++ b/src/main/java/hanium/modic/backend/domain/post/service/PostImageService.java
@@ -43,6 +43,12 @@ public class PostImageService extends ImageService {
     postImageEntityRepository.delete(image);
-    imageUtil.deleteImage(image.getImagePath());
+    // AI 원본 이미지는 삭제하지 않도록 용도 분기
+    if (image.getImagePurpose() != ImagePrefix.AI_RESPONSE) {
+        imageUtil.deleteImage(image.getImagePath());
+    }
 }

위 조치 중 하나 이상을 적용하여 “파생(post) 이미지 삭제 시 AI 원본이 삭제”되는 문제를 반드시 방지해 주시기 바랍니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// AI 이미지를 포스트 이미지로 변환
PostImageEntity postImage = PostImageEntity.builder()
.imagePath(createdAiImage.getImagePath())
.fullImageName(createdAiImage.getFullImageName())
.imageName(createdAiImage.getImageName())
.extension(createdAiImage.getExtension())
.imagePurpose(createdAiImage.getImagePurpose())
.build();
postImage.updatePost(savedPost);
postImageEntityRepository.save(postImage);
// AI 이미지를 포스트 이미지로 변환
// 1) S3에서 원본 AI 이미지 복사 후 새로운 키(post 전용 경로) 사용
String dstKey = imagePathUtil.buildPostImageKey(savedPost.getId(), createdAiImage.getExtension());
amazonS3.copyObject(
s3Properties.getBucketName(),
createdAiImage.getImagePath(),
s3Properties.getBucketName(),
dstKey
);
PostImageEntity postImage = PostImageEntity.builder()
.imagePath(dstKey)
.fullImageName(createdAiImage.getFullImageName())
.imageName(createdAiImage.getImageName())
.extension(createdAiImage.getExtension())
.imagePurpose(ImagePrefix.POST)
.build();
postImage.updatePost(savedPost);
postImageEntityRepository.save(postImage);
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/ai/service/AiDerivedPostService.java
around lines 70–81, the code currently reuses the AI-generated image's S3 key
and purpose when creating the PostImageEntity, which risks deleting the original
AI image when a post image is removed; fix this by copying the AI S3 object to a
new post-specific key (e.g., posts/{postId}/...), set the
PostImageEntity.imagePath to that new key and imagePurpose to ImagePrefix.POST
before saving; alternatively (or additionally), modify
src/main/java/hanium/modic/backend/domain/post/service/PostImageService.java
deleteImage to skip physical S3 deletion when imagePurpose ==
ImagePrefix.AI_RESPONSE and only remove the DB reference—implement one of these
approaches to ensure deleting a derived post image cannot remove the AI
original.

Comment on lines +55 to +57
return ResponseEntity.status(HttpStatus.CREATED)
.body(AppResponse.ok(response));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

201(CREATED) 상태와 AppResponse.ok 조합은 불일치 — created로 통일.

본문에 성공 wrapper로 ok를 쓰면 메타/스키마 상 혼란이 생깁니다. AppResponse.created(...)로 맞춰 주세요. 기존 PostController와도 일관됩니다.

-    return ResponseEntity.status(HttpStatus.CREATED)
-      .body(AppResponse.ok(response));
+    return ResponseEntity.status(HttpStatus.CREATED)
+      .body(AppResponse.created(response));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return ResponseEntity.status(HttpStatus.CREATED)
.body(AppResponse.ok(response));
}
return ResponseEntity.status(HttpStatus.CREATED)
.body(AppResponse.created(response));
}
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/web/post/controller/AiDerivedPostController.java
around lines 55 to 57, the controller returns HTTP 201 (CREATED) but wraps the
body with AppResponse.ok which is inconsistent; change the response wrapper to
AppResponse.created(response) so the body meta/schema matches the 201 status and
remains consistent with PostController (i.e., return
ResponseEntity.status(HttpStatus.CREATED).body(AppResponse.created(response));).

Comment on lines +65 to +73
@DeleteMapping("/{postId}")
public ResponseEntity<AppResponse<Void>> deleteAiDerivedPost(
@CurrentUser UserEntity currentUser,
@PathVariable Long postId) {

aiDerivedPostService.deleteAiDerivedPost(currentUser.getId(), postId);

return ResponseEntity.ok().build();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

반환 타입 불일치로 컴파일 실패 가능 — 삭제 API는 204 + AppResponse.noContent로 반환 필요.

메서드 시그니처는 ResponseEntity<AppResponse<Void>>인데, 현재 ResponseEntity.ok().build()ResponseEntity<Void>를 반환합니다. 제네릭이 달라 컴파일 오류가 발생합니다. 또한 삭제 성공은 204(NO_CONTENT)가 REST 관례 및 기존 PostController와 일치합니다. 스웨거 응답 코드도 204를 추가해 주세요.

 @DeleteMapping("/{postId}")
 public ResponseEntity<AppResponse<Void>> deleteAiDerivedPost(
   @CurrentUser UserEntity currentUser,
   @PathVariable Long postId) {

   aiDerivedPostService.deleteAiDerivedPost(currentUser.getId(), postId);

-  return ResponseEntity.ok().build();
+  return ResponseEntity.status(HttpStatus.NO_CONTENT)
+    .body(AppResponse.noContent());
 }

스웨거 응답 추가(권장):

 @ApiResponses(value = {
+  @ApiResponse(responseCode = "204", description = "삭제 성공"),
   @ApiResponse(responseCode = "400", description = "AI 파생 포스트가 아닙니다.[P-004]"),
   @ApiResponse(responseCode = "403", description = "포스트에 대한 접근 권한이 없습니다.[P-003]"),
   @ApiResponse(responseCode = "404", description = "해당 포스트를 찾을 수 없습니다.[P-001]")
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@DeleteMapping("/{postId}")
public ResponseEntity<AppResponse<Void>> deleteAiDerivedPost(
@CurrentUser UserEntity currentUser,
@PathVariable Long postId) {
aiDerivedPostService.deleteAiDerivedPost(currentUser.getId(), postId);
return ResponseEntity.ok().build();
}
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "삭제 성공"),
@ApiResponse(responseCode = "400", description = "AI 파생 포스트가 아닙니다.[P-004]"),
@ApiResponse(responseCode = "403", description = "포스트에 대한 접근 권한이 없습니다.[P-003]"),
@ApiResponse(responseCode = "404", description = "해당 포스트를 찾을 수 없습니다.[P-001]")
})
@DeleteMapping("/{postId}")
public ResponseEntity<AppResponse<Void>> deleteAiDerivedPost(
@CurrentUser UserEntity currentUser,
@PathVariable Long postId) {
aiDerivedPostService.deleteAiDerivedPost(currentUser.getId(), postId);
return ResponseEntity.status(HttpStatus.NO_CONTENT)
.body(AppResponse.noContent());
}
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/web/post/controller/AiDerivedPostController.java
around lines 65-73, the method currently returns ResponseEntity.ok().build()
which yields ResponseEntity<Void> and causes a generic mismatch with the
declared ResponseEntity<AppResponse<Void>>; change the method to return
ResponseEntity.status(HttpStatus.NO_CONTENT).body(AppResponse.noContent()) so
the generics match and the endpoint returns 204 NO_CONTENT per REST conventions,
and add the corresponding Swagger/OpenAPI response annotation for 204 to the
controller method (e.g., an @ApiResponse/@ApiResponses entry documenting 204).

Comment on lines +2 to +3
profiles:
active: local No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

profiles.active를 기본 yml에 고정하면 CI/테스트/배포 환경이 ‘local’로 강제될 수 있습니다

  • application.yml에서 spring.profiles.active를 지정하면, 별도 오버라이드가 없는 한 모든 실행(로컬, 테스트, CI/CD, 운영)에서 local 프로필이 적용됩니다. 이 PR에서 통합 테스트(S3/DB 연동 등)를 강조하셨는데, 테스트가 원치 않게 local 설정을 물고 실패/오작동할 가능성이 큽니다.
  • 일반적으로 application.yml은 공통 기본값만 두고, 프로필 활성화는 실행 파라미터/환경변수나 @activeprofiles, Spring Config(컨피그 서버)로 제어합니다.

권장 수정:

  • application.yml에서 active 설정을 제거하고, application-local.yml에만 로컬용 설정을 두세요.
  • 개발 실행 시에는 환경변수 SPRING_PROFILES_ACTIVE=local 또는 JVM 옵션 --spring.profiles.active=local 로 지정하세요.

예시 diff(해당 범위 내 수정):

 spring:
-  profiles:
-    active: local
+  # 프로필 활성화는 환경 변수나 실행 파라미터로 지정하세요.

다음 스크립트로 프로필 파일 보유 현황과 테스트 프로필 강제 여부를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== 존재하는 스프링 설정 파일 목록 =="
fd -a '^application(-[a-zA-Z0-9_\-]+)?\.ya?ml$' src/main/resources || true

echo
echo "== 코드/설정 내 spring.profiles.active 직접 지정 위치(로컬 제외) =="
rg -n 'spring\.profiles\.active' -g '!**/build/**' -g '!src/main/resources/application-local.yml' || true

echo
echo "== 테스트 코드에서 @ActiveProfiles 사용 위치 =="
rg -n '@ActiveProfiles\(' -g 'src/test/**.java' -C2 || true

Length of output: 676


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Directory structure under src/main/resources:"
find src/main/resources -maxdepth 2 -type f || true

echo
echo "All application*.yml files under project:"
find . -type f \( -name 'application*.yml' -o -name 'application*.yaml' \) || true

Length of output: 418


application.yml에서 기본 프로필 설정 제거 및 프로필별 분리 필수

현재 src/main/resources/application.yml에만 존재하는 설정 파일에서 spring.profiles.active: local을 고정하면
CI/테스트/운영 등 모든 실행 환경에서 무조건 local 프로필이 적용됩니다.

확인 결과:

  • src/main/resources 및 프로젝트 전체에 profile별 YAML(application-local.yml, application-dev.yml 등)이 전혀 없습니다.
  • 코드나 테스트(@activeprofiles) 레벨에서도 별도 프로필 지정이 이뤄지고 있지 않습니다.

따라서 아래 사항을 반드시 반영해주세요:

  • src/main/resources/application.yml
    • 2–3번 줄의 spring.profiles.active: local 설정 제거
  • src/main/resources/application-local.yml (신규 파일 생성)
    • local 환경 전용 설정만 정의
  • 기타 프로필(테스트, CI, 운영)이 필요하다면 application-{profile}.yml 파일을 추가로 생성

예시 diff (application.yml):

 spring:
-  profiles:
-    active: local
+  # 프로필 활성화는 환경변수(SPRING_PROFILES_ACTIVE)나 JVM 옵션(--spring.profiles.active)으로 지정하세요.

추가로, 로컬 실행 시에는 아래 중 하나를 사용해 local 프로필을 활성화하세요:

  • 환경변수:
    export SPRING_PROFILES_ACTIVE=local
  • JVM 옵선:
    --spring.profiles.active=local

[src/main/resources/application.yml:2–3]

🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 3-3: no new line character at the end of file

(new-line-at-end-of-file)

🤖 Prompt for AI Agents
In src/main/resources/application.yml around lines 2–3, remove the hardcoded
spring.profiles.active: local entry so the application does not force the local
profile at runtime; create a new src/main/resources/application-local.yml
containing only local-specific settings (move any local-only properties from
application.yml into that file), and if needed add other profile files like
application-dev.yml or application-prod.yml for CI/test/prod environments;
ensure local activation is done via SPRING_PROFILES_ACTIVE env var or the
--spring.profiles.active JVM arg rather than a default in application.yml.

Comment on lines 50 to 103
@Test
@DisplayName("AI 파생 포스트 생성 성공")
void createAiDerivedPost_Success() {
// given
Long userId = 1L;
Long createdAiImageId = 100L;
String title = "AI Generated Post";
String description = "This is an AI derived post";
Long commercialPrice = 2000L;
Long nonCommercialPrice = 1000L;
Long ticketPrice = 300L;

UserEntity mockUser = UserFactory.createMockUser(userId);
CreatedAiImageEntity mockAiImage = createMockCreatedAiImageWithId(
createdAiImageId, userId, 1L, "request-123");
PostEntity mockSavedPost = createMockPostWithId(1L, mockUser);

when(createdAiImageRepository.findById(createdAiImageId)).thenReturn(Optional.of(mockAiImage));
when(postEntityRepository.save(any(PostEntity.class))).thenReturn(mockSavedPost);

// when
CreatePostResponse response = aiDerivedPostService.createAiDerivedPost(
userId, createdAiImageId, title, description,
commercialPrice, nonCommercialPrice, ticketPrice);

// then
assertThat(response).isNotNull();
assertThat(response.postId()).isEqualTo(mockSavedPost.getId());

// PostEntity 저장 검증
ArgumentCaptor<PostEntity> postCaptor = ArgumentCaptor.forClass(PostEntity.class);
verify(postEntityRepository, times(1)).save(postCaptor.capture());
PostEntity savedPost = postCaptor.getValue();
assertThat(savedPost.getUserId()).isEqualTo(userId);
assertThat(savedPost.getTitle()).isEqualTo(title);
assertThat(savedPost.getDescription()).isEqualTo(description);
assertThat(savedPost.getCommercialPrice()).isEqualTo(commercialPrice);
assertThat(savedPost.getNonCommercialPrice()).isEqualTo(nonCommercialPrice);
assertThat(savedPost.getTicketPrice()).isEqualTo(ticketPrice);
assertThat(savedPost.getIsAiDerivedPost()).isTrue();

// PostImageEntity 저장 검증
ArgumentCaptor<PostImageEntity> imageCaptor = ArgumentCaptor.forClass(PostImageEntity.class);
verify(postImageEntityRepository, times(1)).save(imageCaptor.capture());
PostImageEntity savedImage = imageCaptor.getValue();
assertThat(savedImage.getImagePath()).isEqualTo(mockAiImage.getImagePath());
assertThat(savedImage.getFullImageName()).isEqualTo(mockAiImage.getFullImageName());
assertThat(savedImage.getImageName()).isEqualTo(mockAiImage.getImageName());
assertThat(savedImage.getExtension()).isEqualTo(mockAiImage.getExtension());
assertThat(savedImage.getImagePurpose()).isEqualTo(mockAiImage.getImagePurpose());

verify(createdAiImageRepository, times(1)).findById(createdAiImageId);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

성공 케이스: 이미지-포스트 연결 관계까지 단언 강화 필요

현재 PostImageEntity에 복사된 파일 메타 정보는 검증되지만, 저장된 이미지가 생성된 포스트에 연결되었는지(postId) 검증이 없습니다. 관계 단언을 추가해 회귀를 막아주세요.

 		ArgumentCaptor<PostImageEntity> imageCaptor = ArgumentCaptor.forClass(PostImageEntity.class);
 		verify(postImageEntityRepository, times(1)).save(imageCaptor.capture());
 		PostImageEntity savedImage = imageCaptor.getValue();
 		assertThat(savedImage.getImagePath()).isEqualTo(mockAiImage.getImagePath());
 		assertThat(savedImage.getFullImageName()).isEqualTo(mockAiImage.getFullImageName());
 		assertThat(savedImage.getImageName()).isEqualTo(mockAiImage.getImageName());
 		assertThat(savedImage.getExtension()).isEqualTo(mockAiImage.getExtension());
 		assertThat(savedImage.getImagePurpose()).isEqualTo(mockAiImage.getImagePurpose());
+		assertThat(savedImage.getPostId()).isEqualTo(mockSavedPost.getId());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
@DisplayName("AI 파생 포스트 생성 성공")
void createAiDerivedPost_Success() {
// given
Long userId = 1L;
Long createdAiImageId = 100L;
String title = "AI Generated Post";
String description = "This is an AI derived post";
Long commercialPrice = 2000L;
Long nonCommercialPrice = 1000L;
Long ticketPrice = 300L;
UserEntity mockUser = UserFactory.createMockUser(userId);
CreatedAiImageEntity mockAiImage = createMockCreatedAiImageWithId(
createdAiImageId, userId, 1L, "request-123");
PostEntity mockSavedPost = createMockPostWithId(1L, mockUser);
when(createdAiImageRepository.findById(createdAiImageId)).thenReturn(Optional.of(mockAiImage));
when(postEntityRepository.save(any(PostEntity.class))).thenReturn(mockSavedPost);
// when
CreatePostResponse response = aiDerivedPostService.createAiDerivedPost(
userId, createdAiImageId, title, description,
commercialPrice, nonCommercialPrice, ticketPrice);
// then
assertThat(response).isNotNull();
assertThat(response.postId()).isEqualTo(mockSavedPost.getId());
// PostEntity 저장 검증
ArgumentCaptor<PostEntity> postCaptor = ArgumentCaptor.forClass(PostEntity.class);
verify(postEntityRepository, times(1)).save(postCaptor.capture());
PostEntity savedPost = postCaptor.getValue();
assertThat(savedPost.getUserId()).isEqualTo(userId);
assertThat(savedPost.getTitle()).isEqualTo(title);
assertThat(savedPost.getDescription()).isEqualTo(description);
assertThat(savedPost.getCommercialPrice()).isEqualTo(commercialPrice);
assertThat(savedPost.getNonCommercialPrice()).isEqualTo(nonCommercialPrice);
assertThat(savedPost.getTicketPrice()).isEqualTo(ticketPrice);
assertThat(savedPost.getIsAiDerivedPost()).isTrue();
// PostImageEntity 저장 검증
ArgumentCaptor<PostImageEntity> imageCaptor = ArgumentCaptor.forClass(PostImageEntity.class);
verify(postImageEntityRepository, times(1)).save(imageCaptor.capture());
PostImageEntity savedImage = imageCaptor.getValue();
assertThat(savedImage.getImagePath()).isEqualTo(mockAiImage.getImagePath());
assertThat(savedImage.getFullImageName()).isEqualTo(mockAiImage.getFullImageName());
assertThat(savedImage.getImageName()).isEqualTo(mockAiImage.getImageName());
assertThat(savedImage.getExtension()).isEqualTo(mockAiImage.getExtension());
assertThat(savedImage.getImagePurpose()).isEqualTo(mockAiImage.getImagePurpose());
verify(createdAiImageRepository, times(1)).findById(createdAiImageId);
}
// PostImageEntity 저장 검증
ArgumentCaptor<PostImageEntity> imageCaptor = ArgumentCaptor.forClass(PostImageEntity.class);
verify(postImageEntityRepository, times(1)).save(imageCaptor.capture());
PostImageEntity savedImage = imageCaptor.getValue();
assertThat(savedImage.getImagePath()).isEqualTo(mockAiImage.getImagePath());
assertThat(savedImage.getFullImageName()).isEqualTo(mockAiImage.getFullImageName());
assertThat(savedImage.getImageName()).isEqualTo(mockAiImage.getImageName());
assertThat(savedImage.getExtension()).isEqualTo(mockAiImage.getExtension());
assertThat(savedImage.getImagePurpose()).isEqualTo(mockAiImage.getImagePurpose());
assertThat(savedImage.getPostId()).isEqualTo(mockSavedPost.getId());
🤖 Prompt for AI Agents
In
src/test/java/hanium/modic/backend/domain/ai/service/AiDerivedPostServiceTest.java
around lines 50 to 103, the test currently asserts the saved PostImageEntity's
file metadata but does not assert that the saved image is associated with the
created Post; add an assertion to verify the image's post relationship (e.g.,
assert savedImage.getPostId() or savedImage.getPost().getId() equals
savedPost.getId()) immediately after retrieving savedImage, ensuring the
PostImageEntity saved by the repository has its post foreign-key set to the
saved Post's id and thus prevents regressions in the image-post linkage.

PageResponse<GetPostsResponse> pageResponse = PageResponse.of(page);

when(postService.getPosts(any(String.class), anyInt(), anyInt())).thenReturn(pageResponse);
when(postService.getPosts(any(String.class), anyInt(), anyInt(), PostType.ALL)).thenReturn(pageResponse);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Mockito matchers 혼용으로 인한 InvalidUseOfMatchersException 수정

when(postService.getPosts(any(...), anyInt(), anyInt(), PostType.ALL))에서 matcher와 실제 값(PostType.ALL)을 혼용했습니다. 모든 파라미터에 matcher를 사용하거나 마지막 인자를 eq(PostType.ALL)로 바꾸면 해결됩니다. CI 실패 메시지와 일치합니다.

-		when(postService.getPosts(any(String.class), anyInt(), anyInt(), PostType.ALL)).thenReturn(pageResponse);
+		when(postService.getPosts(any(String.class), anyInt(), anyInt(), eq(PostType.ALL))).thenReturn(pageResponse);
-		when(postService.getPosts(sort, pageNumber, size, PostType.ALL)).thenReturn(pageResponse);
+		when(postService.getPosts(eq(sort), eq(pageNumber), eq(size), eq(PostType.ALL))).thenReturn(pageResponse);

Also applies to: 229-229

🧰 Tools
🪛 GitHub Actions: compile-test

[error] 202-202: InvalidUseOfMatchersException - 게시물 목록 조회 실패 - 기본 파라미터.

🤖 Prompt for AI Agents
In
src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java
around lines 202 and 229, the Mockito stubbing mixes matchers with a raw value
which causes InvalidUseOfMatchersException; change the fourth argument to a
matcher (e.g., use eq(PostType.ALL)) or replace all args with matchers (e.g.,
any(...) for that param) so every parameter uses matchers consistently, and
update both occurrences at lines 202 and 229 accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

파생 포스트 기능 구현

1 participant