Skip to content

[REFACTOR] #491 CampaignReviewUsecase 조회 흐름 분리#491

Merged
huncozyboy merged 5 commits intodevfrom
refactor/#490
Mar 21, 2026
Merged

[REFACTOR] #491 CampaignReviewUsecase 조회 흐름 분리#491
huncozyboy merged 5 commits intodevfrom
refactor/#490

Conversation

@huncozyboy
Copy link
Member

@huncozyboy huncozyboy commented Mar 21, 2026

Related issue 🛠

작업 내용 💻

  • BrandUsecase.getCreatorPerformances(...)의 조회 조립/수동 페이징 책임을 BrandCreatorPerformanceQueryService로 분리
  • CampaignReviewUsecase의 조회 메서드(getMyReviewableCampaign, getMyReviewables, getCompletedReviews)를 CampaignReviewReadService로 분리
  • BrandUsecase, CampaignReviewUsecase는 조회 진입점과 최소한의 orchestration만 담당하도록 정리
  • 브랜드 성과 조회 테스트를 usecase 위임 검증과 query service 조립 검증으로 분리
  • 캠페인 리뷰 읽기 흐름 관련 테스트 추가
  • 단일 플랫폼 캠페인 조회 시 secondContentPlatform == null일 때 발생할 수 있던 NPE 방지 처리

같이 얘기해보고 싶은 내용이 있다면 작성 📢

  • 이번 PR은 usecase를 없애기보다는, fat usecase에 몰려 있던 조회 조립 책임을 QueryService / ReadService로 걷어내는 정리 작업에 가까워요
  • 작업하다가 추가적으로 든 생각은 새로 만든 read, query service 안에는 조립이랑 상태 해석과 응답 생성 책임이 같이 들어있긴합니다 (해당 내용을 Assembler / Resolver 로 분리하는 방법도 있긴한데, 너무 과한거같아서 조회 전용 서비스 안에 응집시키는 선까지 작업을 진행했어요)

Summary by CodeRabbit

릴리스 노트

  • Refactor

    • 캠프페인 검토 기능의 내부 아키텍처를 개선하여 코드 유지보수성을 강화했습니다.
  • Tests

    • 캠프페인 검토 읽기 기능에 대한 포괄적인 테스트를 추가하여 안정성을 확보했습니다.

@huncozyboy huncozyboy self-assigned this Mar 21, 2026
@huncozyboy huncozyboy added 🔨 Refactor 코드 수정 🧪 Test 테스트 코드 작성 (이제 반드시 해야함....) labels Mar 21, 2026
@huncozyboy huncozyboy linked an issue Mar 21, 2026 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 21, 2026

Warning

Rate limit exceeded

@huncozyboy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 43 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 700ddf4c-21f4-4703-9342-c3ae6e606dad

📥 Commits

Reviewing files that changed from the base of the PR and between b6e705b and d8b84dd.

📒 Files selected for processing (2)
  • src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java
  • src/test/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadServiceTest.java

Walkthrough

읽기 및 쿼리 책임을 CampaignReviewUsecase에서 새로운 CampaignReviewReadService로 추출하는 리팩토링입니다. getMyReviewableCampaign, getMyReviewables, getCompletedReviews 메서드를 분리하고 Usecase는 경량 오케스트레이션만 수행하도록 단순화했으며, 해당 로직을 테스트하는 클래스를 추가했습니다.

Changes

Cohort / File(s) Summary
Campaign Review 읽기 로직 추출
src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java
검토 가능한 캠페인 조회, 완료된 리뷰 조회 등 읽기 로직을 담당하는 새로운 서비스 클래스 추가. 브랜드 노트 조회 표시, 라운드별 컨텐츠 상태 계산, 베타 기능 플래그 기반 응답 매핑 로직 포함.
Usecase 리팩토링
src/main/java/com/lokoko/domain/campaignReview/application/usecase/CampaignReviewUsecase.java
읽기/쿼리 책임을 CampaignReviewReadService로 위임. 기존 getMyReviewableCampaign, getMyReviewables, getCompletedReviews 메서드 구현을 제거하고 새로운 서비스 호출로 변경. 불필요한 의존성(CampaignMapper, CampaignReviewStatusManager) 제거.
서비스 테스트 추가
src/test/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadServiceTest.java
CampaignReviewReadService의 주요 메서드 테스트. 베타 플래그 활성화/비활성화에 따른 제어 흐름 검증, 예외 처리, 브랜드 노트 상태 업데이트 확인.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • hyoeunjoo

🐰 코드 리뷰 책임 분리,
우아한 추출 완성!
읽기와 쓰기 분명히,
테스트로 증명한 신뢰,
캠페인 검토 흐름 깨끗해!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 연결된 이슈 #480은 브랜드 도메인 테스트 작성을 요구하지만, 이 PR은 CampaignReviewUsecase의 조회 흐름 분리에 주로 집중하여 요구사항 불일치가 있습니다. PR이 이슈 #480의 주요 요구사항(브랜드 회원가입, 마이페이지, 지원자 관리 테스트)을 충분히 다루지 않으므로, 올바른 이슈를 연결하거나 이슈 범위를 재평가하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '[REFACTOR] #491 CampaignReviewUsecase 조회 흐름 분리'로, 주요 변경사항인 CampaignReviewUsecase의 조회 책임을 분리하는 것을 명확하게 설명하고 있습니다.
Out of Scope Changes check ✅ Passed PR은 CampaignReviewUsecase 조회 책임 분리, 관련 테스트 추가, NPE 방지 처리를 포함하며, 이는 모두 PR 목표 및 이슈와 관련된 범위 내 변경사항입니다.
Description check ✅ Passed PR 설명서가 구조적으로 템플릿을 따르고 있으며, 관련 이슈, 작업 내용(체크리스트), 추가 논의 사항이 포함되어 있습니다. 스크린샷 섹션만 누락되었으나, 이는 리팩토링 작업의 특성상 필수가 아닙니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#490

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

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

🧹 Nitpick comments (4)
src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java (3)

213-261: 완료된 리뷰 조회 로직을 간소화할 수 있습니다.

현재 findContentTypesByRound로 타입 목록을 조회한 후, 각 타입별로 findByContentType을 다시 호출합니다. getAllByCreatorCampaignAndRound (Line 265에서 이미 사용 중)를 활용하면 단일 조회로 처리할 수 있습니다.

♻️ 간소화 예시 (createCompletedFirstReviewContents)
     private List<CompletedReviewResponse.CompletedReviewContent> createCompletedFirstReviewContents(
             CreatorCampaign creatorCampaign
     ) {
-        List<ContentType> existingFirstTypes =
-                campaignReviewGetService.findContentTypesByRound(creatorCampaign.getId(), ReviewRound.FIRST);
-
-        return existingFirstTypes.stream()
-                .map(contentType -> {
-                    Optional<CampaignReview> firstReview = campaignReviewGetService
-                            .findByContentType(creatorCampaign.getId(), ReviewRound.FIRST, contentType);
-
-                    if (firstReview.isPresent()) {
-                        CampaignReview review = firstReview.get();
-                        return CompletedReviewResponse.CompletedReviewContent.builder()
-                                .contentType(contentType)
-                                .captionWithHashtags(review.getCaptionWithHashtags())
-                                .mediaUrls(campaignReviewGetService.getOrderedMediaUrls(review))
-                                .build();
-                    }
-                    return null;
-                })
-                .filter(Objects::nonNull)
-                .toList();
+        List<CampaignReview> firstReviews =
+                campaignReviewGetService.getAllByCreatorCampaignAndRound(creatorCampaign, ReviewRound.FIRST);
+
+        return firstReviews.stream()
+                .map(review -> CompletedReviewResponse.CompletedReviewContent.builder()
+                        .contentType(review.getContentType())
+                        .captionWithHashtags(review.getCaptionWithHashtags())
+                        .mediaUrls(campaignReviewGetService.getOrderedMediaUrls(review))
+                        .build())
+                .toList();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java`
around lines 213 - 261, Both createCompletedReviewContents and
createCompletedFirstReviewContents make N+1 lookups by calling
campaignReviewGetService.findContentTypesByRound then findByContentType per
type; replace that by a single call to
campaignReviewGetService.getAllByCreatorCampaignAndRound(creatorCampaign,
ReviewRound.FIRST/SECOND) to fetch all CampaignReview objects, then map each
CampaignReview to a CompletedReviewResponse.CompletedReviewContent using
review.getContentType(), review.getCaptionWithHashtags(), and
campaignReviewGetService.getOrderedMediaUrls(review); remove the intermediate
findContentTypesByRound/findByContentType calls and eliminate null handling
since mapping from the returned reviews will produce only valid contents.

119-141: createReviewContentStatuses에서도 동일한 중복 조회 패턴이 있습니다.

currentRound == ReviewRound.SECOND일 때 hasFirstReview는 항상 true입니다(Line 117 로직). 따라서 Lines 122-129와 134-141에서 동일한 1차 리뷰를 두 번 조회합니다.

♻️ 통합 리팩토링 제안
                     String brandNote = null;
                     Instant revisionRequestedAt = null;
-                    if (currentRound == ReviewRound.SECOND) {
-                        Optional<CampaignReview> firstReviewForContentType = campaignReviewGetService
-                                .findByContentType(creatorCampaign.getId(), ReviewRound.FIRST, contentType);
-                        if (firstReviewForContentType.isPresent()) {
-                            CampaignReview review = firstReviewForContentType.get();
-                            brandNote = review.getBrandNote();
-                            revisionRequestedAt = review.getRevisionRequestedAt();
-                        }
-                    }
-
                     String captionWithHashtags = null;
                     List<String> mediaUrls = null;
                     if (hasFirstReview) {
                         Optional<CampaignReview> existingReview = campaignReviewGetService
                                 .findByContentType(creatorCampaign.getId(), ReviewRound.FIRST, contentType);
                         if (existingReview.isPresent()) {
                             CampaignReview review = existingReview.get();
+                            if (currentRound == ReviewRound.SECOND) {
+                                brandNote = review.getBrandNote();
+                                revisionRequestedAt = review.getRevisionRequestedAt();
+                            }
                             captionWithHashtags = review.getCaptionWithHashtags();
                             mediaUrls = campaignReviewGetService.getOrderedMediaUrls(review);
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java`
around lines 119 - 141, There are duplicate lookups of the first review when
currentRound == ReviewRound.SECOND (and hasFirstReview is always true), causing
two calls to campaignReviewGetService.findByContentType; refactor to perform a
single lookup once (e.g., call
campaignReviewGetService.findByContentType(creatorCampaign.getId(),
ReviewRound.FIRST, contentType) into an Optional<CampaignReview> firstReview)
and then extract brandNote, revisionRequestedAt, captionWithHashtags and
mediaUrls from that single review using review.getBrandNote(),
review.getRevisionRequestedAt(), review.getCaptionWithHashtags() and
campaignReviewGetService.getOrderedMediaUrls(review); apply the same
consolidation in createReviewContentStatuses (or extract a small helper like
getFirstReviewForContentType / populateFirstReviewFields) to reuse the result
instead of duplicating the service call.

179-199: 동일한 리뷰를 중복 조회하고 있습니다.

targetRound == ReviewRound.SECOND일 때 firstReviewForContentType을 두 번 조회합니다(Lines 180-187, 192-199). 하나의 조회로 통합하면 불필요한 DB 호출을 줄일 수 있습니다.

♻️ 중복 조회 통합 제안
                     String brandNote = null;
                     Instant revisionRequestedAt = null;
-                    if (targetRound == ReviewRound.SECOND) {
-                        Optional<CampaignReview> firstReviewForContentType = campaignReviewGetService
-                                .findByContentType(creatorCampaign.getId(), ReviewRound.FIRST, contentType);
-                        if (firstReviewForContentType.isPresent()) {
-                            CampaignReview review = firstReviewForContentType.get();
-                            brandNote = review.getBrandNote();
-                            revisionRequestedAt = review.getRevisionRequestedAt();
-                        }
-                    }
-
                     String captionWithHashtags = null;
                     List<String> mediaUrls = null;
                     if (targetRound == ReviewRound.SECOND) {
                         Optional<CampaignReview> firstReviewForContentType = campaignReviewGetService
                                 .findByContentType(creatorCampaign.getId(), ReviewRound.FIRST, contentType);
                         if (firstReviewForContentType.isPresent()) {
                             CampaignReview review = firstReviewForContentType.get();
+                            brandNote = review.getBrandNote();
+                            revisionRequestedAt = review.getRevisionRequestedAt();
                             captionWithHashtags = review.getCaptionWithHashtags();
                             mediaUrls = campaignReviewGetService.getOrderedMediaUrls(review);
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java`
around lines 179 - 199, When targetRound == ReviewRound.SECOND you call
campaignReviewGetService.findByContentType(...) twice; replace the two separate
lookups with a single Optional<CampaignReview> firstReviewForContentType =
campaignReviewGetService.findByContentType(creatorCampaign.getId(),
ReviewRound.FIRST, contentType) and, if present, populate brandNote,
revisionRequestedAt, captionWithHashtags and mediaUrls (use
campaignReviewGetService.getOrderedMediaUrls(review) for mediaUrls) from that
one review instance to eliminate the duplicate DB call.
src/test/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadServiceTest.java (1)

130-145: 추가 테스트 커버리지를 고려해 주세요.

현재 테스트는 주요 시나리오를 잘 다루고 있습니다. 다음 케이스들을 추후 추가하면 테스트 커버리지가 더 강화됩니다:

  • getMyReviewables() 메서드 테스트
  • getCompletedReviews() non-beta 플로우 (2차 리뷰 반환)
  • ReviewRound.FIRST/ReviewRound.SECOND 파라미터에 따른 필터링 로직

추가 테스트 케이스 생성을 도와드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadServiceTest.java`
around lines 130 - 145, Add unit tests to increase coverage: create tests in
CampaignReviewReadServiceTest for getMyReviewables(), for getCompletedReviews()
non-beta flow verifying it returns second-round reviews (use
campaignReviewReadService.getCompletedReviews and mock necessary services like
campaignGetService and creatorCampaignGetService), and tests asserting filtering
by ReviewRound.FIRST and ReviewRound.SECOND parameters (exercise methods that
accept ReviewRound and verify returned lists are correctly filtered). Mock/stub
dependencies (campaignGetService, creatorCampaignGetService, review
repositories) and use the existing assert patterns to validate returned elements
and exception-free behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java`:
- Around line 213-261: Both createCompletedReviewContents and
createCompletedFirstReviewContents make N+1 lookups by calling
campaignReviewGetService.findContentTypesByRound then findByContentType per
type; replace that by a single call to
campaignReviewGetService.getAllByCreatorCampaignAndRound(creatorCampaign,
ReviewRound.FIRST/SECOND) to fetch all CampaignReview objects, then map each
CampaignReview to a CompletedReviewResponse.CompletedReviewContent using
review.getContentType(), review.getCaptionWithHashtags(), and
campaignReviewGetService.getOrderedMediaUrls(review); remove the intermediate
findContentTypesByRound/findByContentType calls and eliminate null handling
since mapping from the returned reviews will produce only valid contents.
- Around line 119-141: There are duplicate lookups of the first review when
currentRound == ReviewRound.SECOND (and hasFirstReview is always true), causing
two calls to campaignReviewGetService.findByContentType; refactor to perform a
single lookup once (e.g., call
campaignReviewGetService.findByContentType(creatorCampaign.getId(),
ReviewRound.FIRST, contentType) into an Optional<CampaignReview> firstReview)
and then extract brandNote, revisionRequestedAt, captionWithHashtags and
mediaUrls from that single review using review.getBrandNote(),
review.getRevisionRequestedAt(), review.getCaptionWithHashtags() and
campaignReviewGetService.getOrderedMediaUrls(review); apply the same
consolidation in createReviewContentStatuses (or extract a small helper like
getFirstReviewForContentType / populateFirstReviewFields) to reuse the result
instead of duplicating the service call.
- Around line 179-199: When targetRound == ReviewRound.SECOND you call
campaignReviewGetService.findByContentType(...) twice; replace the two separate
lookups with a single Optional<CampaignReview> firstReviewForContentType =
campaignReviewGetService.findByContentType(creatorCampaign.getId(),
ReviewRound.FIRST, contentType) and, if present, populate brandNote,
revisionRequestedAt, captionWithHashtags and mediaUrls (use
campaignReviewGetService.getOrderedMediaUrls(review) for mediaUrls) from that
one review instance to eliminate the duplicate DB call.

In
`@src/test/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadServiceTest.java`:
- Around line 130-145: Add unit tests to increase coverage: create tests in
CampaignReviewReadServiceTest for getMyReviewables(), for getCompletedReviews()
non-beta flow verifying it returns second-round reviews (use
campaignReviewReadService.getCompletedReviews and mock necessary services like
campaignGetService and creatorCampaignGetService), and tests asserting filtering
by ReviewRound.FIRST and ReviewRound.SECOND parameters (exercise methods that
accept ReviewRound and verify returned lists are correctly filtered). Mock/stub
dependencies (campaignGetService, creatorCampaignGetService, review
repositories) and use the existing assert patterns to validate returned elements
and exception-free behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 904a6b48-ff14-4dea-89fd-e0176215ad06

📥 Commits

Reviewing files that changed from the base of the PR and between 574c35a and b6e705b.

📒 Files selected for processing (3)
  • src/main/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadService.java
  • src/main/java/com/lokoko/domain/campaignReview/application/usecase/CampaignReviewUsecase.java
  • src/test/java/com/lokoko/domain/campaignReview/application/service/CampaignReviewReadServiceTest.java

@huncozyboy huncozyboy merged commit f3e964d into dev Mar 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 Refactor 코드 수정 🧪 Test 테스트 코드 작성 (이제 반드시 해야함....)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] CampaignReviewUsecase 조회 흐름 분리

1 participant