Skip to content

Conversation

@eraser502
Copy link
Contributor

Overview

  • 관리자 앱 목록 조회에 status/name/redot_member_id 필터와 createdAt 정렬을 추가하고, 앱 단건 조회 및 상태 변경(remark 포함) API를 구현했어요.
  • redot_apps 테이블에 remark 컬럼을 추가하고 RedotAppStatus에 BANNED 값을 넣어 상태·비고 정보를 노출/수정할 수 있게 했어요.
  • 관리자 사칭(impersonate) API가 CMS OWNER 계정의 토큰을 발급하도록 변경하여 실제 CMS 관리자 권한과 일치하도록 했어요.

Related Issue


PR Checklist

  • [ ]

Additional Information

@eraser502 eraser502 self-assigned this Dec 17, 2025
@eraser502 eraser502 added this to Admin Dec 17, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

관리자용 Redot 앱 관리 API가 추가되었습니다. AdminRedotAppController에 앱 목록 조회(검색·페이징), 단일 조회, 상태(및 remark) 업데이트 엔드포인트가 추가되었고, RedotApp 엔티티에 remark 필드와 updateStatus 메서드, 상태 열거형에 BANNED가 추가되었습니다. QueryDSL 기반의 커스텀 레포지토리(RedotAppRepositoryCustom/Impl)가 페이징 검색과 조인 조회를 제공하며, 관련 리포지토리들에 다건 조회 메서드가 추가되었습니다. AdminImpersonation 흐름은 서버사이드 OWNER 멤버 조회로 변경되었습니다.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as 관리자 클라이언트
    participant Controller as AdminRedotAppController
    participant Service as AdminRedotAppService
    participant Repo as RedotAppRepository / 기타Repo
    participant DB as Database

    rect rgb(200,220,240)
        Note over Admin,DB: 앱 목록 조회 (검색 · 페이징 · 정렬)
        Admin->>Controller: GET /api/v1/redot/admin/app\n(searchCondition, pageable)
        Controller->>Service: getRedotAppInfoList(searchCondition, pageable)
        Service->>Repo: findAllWithSiteInfo / findAllBySearchCondition(searchCondition, pageable)
        Repo->>DB: QueryDSL 동적 쿼리 (name, ownerId, status, joins, order, limit/offset)
        DB-->>Repo: 결과(Row/Tuple)
        Repo-->>Service: Page<RedotApp> / List<RedotAppWithSiteInfo>
        Service->>Service: 도메인/스타일/사이트 매핑 및 toRedotAppInfoResponse 변환
        Service-->>Controller: PageResponse<RedotAppInfoResponse>
        Controller-->>Admin: 200 OK + 페이징 응답
    end

    rect rgb(240,200,220)
        Note over Admin,DB: 앱 상태 업데이트 (remark 포함)
        Admin->>Controller: POST /api/v1/redot/admin/app/{id}/status\n(status, remark)
        Controller->>Service: updateRedotAppStatus(appId, request)
        Service->>Repo: findById(appId)
        Repo->>DB: SELECT * FROM redot_apps WHERE id=...
        DB-->>Repo: RedotApp
        Repo-->>Service: RedotApp
        Service->>Service: redotApp.updateStatus(status, remark)
        Service->>Repo: save(redotApp)
        Repo->>DB: UPDATE redot_apps SET status=..., remark=...
        DB-->>Repo: 업데이트 완료
        Service->>Service: toRedotAppInfoResponse(redotApp)
        Service-->>Controller: RedotAppInfoResponse
        Controller-->>Admin: 200 OK + 업데이트된 앱 정보
    end

    rect rgb(220,240,200)
        Note over Admin,DB: 임퍼소네이션 변경 — OWNER 조회로 토큰 발급
        Admin->>Controller: POST /api/v1/auth/cms/impersonate\n(CMSAdminImpersonationRequest)
        Controller->>Service: impersonateAsCMSAdmin(request)
        Service->>Repo: findFirstByRedotApp_IdAndRoleOrderByIdAsc(appId, OWNER)
        Repo->>DB: SELECT ... FROM cms_members WHERE redot_app_id=? AND role='OWNER' ORDER BY id ASC LIMIT 1
        DB-->>Repo: Optional<CMSMember>
        Repo-->>Service: CMSMember(OWNER) or empty -> 예외
        Service->>Service: TokenContext(owner.id, owner.role)
        Service-->>Controller: AuthResult(tokenResponse)
        Controller-->>Admin: 200 OK + TokenResponse (body) + 쿠키/헤더
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

  • 주의할 파일/영역:
    • RedotAppRepositoryImpl.java: QueryDSL 동적 필터·정렬·페이징 정확성 및 성능
    • AdminRedotAppService.java: 엔티티 조합(toRedotAppInfoResponse)에서 N+1, null 처리, 이미지 URL 변환
    • AdminImpersonationService.java: OWNER 조회 변경에 따른 보안/권한 영향 및 예외 처리
    • RedotApp 엔티티 및 V6 migration: JPA 매핑과 마이그레이션 일관성
    • API Docs 인터페이스(AdminRedotAppControllerDocs): 스웨거/파라미터 매핑 검증

Possibly related PRs

Poem

🐰 나는 작은 토끼, 데이터 밭을 지나
새 필드에 remark를 심고 왔네 🌱
BANNED도 담고 상태를 고치며,
OWNER의 토큰으로 길을 밝히네 ✨
훌쩍뛰어 서버 로그에 웃음을 남기네 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feature / admin / app' is vague and generic, using a placeholder-like format without describing the specific implementation (filtering, endpoints, status changes). PR 제목을 'Admin app management API: filtering, retrieval, status updates' 같이 구체적으로 변경하여 주요 변경 사항을 명확히 표현해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 구조적으로 완전하며, Overview, Related Issue, PR Checklist, Additional Information 섹션이 모두 포함되어 있고 변경 사항이 잘 설명되어 있습니다.
Linked Issues check ✅ Passed PR이 연결된 이슈 #133의 모든 주요 요구사항을 충족합니다: 필터링 지원 앱 목록 조회, 단건 조회, 상태 변경 API, BANNED 상태 추가, remark 필드 추가, 및 CMS 사칭 API의 Owner 토큰 발급 변경.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 이슈 #133의 요구 범위 내에 있으며, 앱 관리 기능과 CMS 사칭 토큰 발급 관련 구현으로 제한되어 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/admin/app

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

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

🧹 Nitpick comments (2)
src/main/java/redot/redot_server/domain/redot/app/entity/RedotApp.java (1)

85-88: status null 허용 여부 확인 필요.

status 필드는 @Column(nullable = false)로 선언되어 있지만, updateStatus 메서드에서 null 체크 없이 직접 할당합니다. 호출부(DTO)에서 @NotNull 검증이 있지만, 방어적 코딩 관점에서 메서드 내부에서도 검증을 고려해 볼 수 있습니다.

 public void updateStatus(RedotAppStatus status, String remark) {
+    if (status == null) {
+        throw new IllegalArgumentException("status cannot be null");
+    }
     this.status = status;
     this.remark = remark;
 }
src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java (1)

52-58: REST 규약에 맞게 @PutMapping 또는 @PatchMapping 사용 권장

상태 변경은 리소스 수정 작업이므로 @PostMapping 대신 @PutMapping 또는 @PatchMapping이 더 적절합니다. PR 목표에서도 "PUT endpoint to modify app information"을 명시하고 있으며, AdminConsultationController에서도 수정 작업에 @PutMapping을 사용합니다.

-    @PostMapping("/{redotAppId}/status")
+    @PutMapping("/{redotAppId}/status")
     @Override
     public ResponseEntity<RedotAppInfoResponse> updateRedotAppStatus(
             @PathVariable("redotAppId") Long redotAppId,
             @Valid @RequestBody RedotAppStatusUpdateRequest request) {
         return ResponseEntity.ok(redotAppService.updateRedotAppStatus(redotAppId, request));
     }

PutMapping import 추가도 필요합니다:

 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffa6710 and bf15c3f.

📒 Files selected for processing (16)
  • src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java (2 hunks)
  • src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java (2 hunks)
  • src/main/java/redot/redot_server/domain/admin/dto/request/RedotAppInfoSearchCondition.java (1 hunks)
  • src/main/java/redot/redot_server/domain/admin/dto/request/RedotAppStatusUpdateRequest.java (1 hunks)
  • src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java (1 hunks)
  • src/main/java/redot/redot_server/domain/auth/controller/AdminImpersonationController.java (1 hunks)
  • src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java (1 hunks)
  • src/main/java/redot/redot_server/domain/auth/service/AdminImpersonationService.java (2 hunks)
  • src/main/java/redot/redot_server/domain/cms/member/repository/CMSMemberRepository.java (2 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/dto/response/RedotAppResponse.java (2 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/entity/RedotApp.java (2 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/entity/RedotAppStatus.java (1 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepository.java (1 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryCustom.java (1 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryImpl.java (1 hunks)
  • src/main/resources/db/migration/V6__add_redot_app_remark.sql (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java (5)
src/main/java/redot/redot_server/domain/redot/app/exception/RedotAppException.java (1)
  • RedotAppException (5-9)
src/main/java/redot/redot_server/domain/site/domain/exception/DomainException.java (1)
  • DomainException (5-9)
src/main/java/redot/redot_server/domain/site/setting/exception/SiteSettingException.java (1)
  • SiteSettingException (5-9)
src/main/java/redot/redot_server/domain/site/style/exception/StyleInfoException.java (1)
  • StyleInfoException (5-9)
src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryImpl.java (1)
  • RequiredArgsConstructor (18-79)
src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java (2)
src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java (1)
  • RestController (22-56)
src/main/java/redot/redot_server/domain/redot/app/controller/RedotAppController.java (1)
  • RestController (26-63)
src/main/java/redot/redot_server/domain/auth/service/AdminImpersonationService.java (1)
src/main/java/redot/redot_server/domain/auth/exception/AuthException.java (1)
  • AuthException (5-13)
🔇 Additional comments (21)
src/main/java/redot/redot_server/domain/redot/app/entity/RedotAppStatus.java (1)

8-9: LGTM!

BANNED 상태 추가가 잘 되어 있습니다. PAYMENT_DELAYED에 trailing comma를 추가한 것도 향후 확장성을 고려한 좋은 패턴입니다.

src/main/resources/db/migration/V6__add_redot_app_remark.sql (1)

1-2: LGTM!

IF NOT EXISTS 사용으로 멱등성이 보장되고, TEXT 타입이 엔티티의 columnDefinition = "text"와 일치합니다. nullable 컬럼으로 선택적 remark를 적절히 지원합니다.

src/main/java/redot/redot_server/domain/admin/dto/request/RedotAppInfoSearchCondition.java (1)

5-9: LGTM!

검색 조건 DTO로 record를 사용한 것이 적절합니다. 모든 필드가 nullable로 선택적 필터링을 올바르게 지원합니다.

src/main/java/redot/redot_server/domain/redot/app/entity/RedotApp.java (1)

44-46: LGTM!

remark 필드 추가가 마이그레이션과 일치하며, nullable 컬럼으로 적절히 정의되어 있습니다.

src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryCustom.java (1)

8-11: LGTM!

Spring Data 커스텀 리포지토리 패턴을 올바르게 따르고 있으며, 페이지네이션과 검색 조건을 지원하는 메서드 시그니처가 적절합니다.

src/main/java/redot/redot_server/domain/admin/dto/request/RedotAppStatusUpdateRequest.java (1)

6-9: LGTM!

@NotNull 검증으로 status 필수값을 보장하고, remark는 선택적으로 적절히 설계되었습니다.

src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepository.java (1)

8-8: LGTM!

RedotAppRepositoryCustom 확장으로 커스텀 쿼리 기능이 올바르게 통합되었습니다. Spring Data가 RedotAppRepositoryImpl을 자동으로 연결합니다.

src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryImpl.java (3)

23-53: LGTM!

QueryDSL을 활용한 동적 쿼리 구현이 잘 되어 있습니다. null-safe한 검색 조건 처리와 페이지네이션이 적절합니다.


55-62: 정렬 필드가 createdAt으로 제한됨.

현재 resolveOrdercreatedAt 필드만 처리하고, 다른 정렬 필드 요청은 무시하고 기본값(createdAt DESC)으로 대체됩니다. 의도된 동작이라면 괜찮지만, 향후 다른 필드(name, status 등) 정렬이 필요할 경우 확장이 필요합니다.


64-78: LGTM!

predicate 헬퍼 메서드들이 깔끔하게 구현되어 있습니다. containsIgnoreCase로 대소문자 무관 검색을 지원하고, hasText 유틸리티로 빈 문자열 처리가 잘 되어 있습니다.

src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java (1)

26-44: API 문서가 잘 작성되었습니다.

새로운 엔드포인트 3개(목록 조회, 단건 조회, 상태 변경)에 대한 Swagger 문서가 적절하게 정의되어 있습니다. @ParameterObject@io.swagger.v3.oas.annotations.Parameter 어노테이션이 올바르게 적용되었습니다.

src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java (1)

37-43: 목록 조회 엔드포인트 구현이 적절합니다.

페이지네이션과 검색 조건이 @ParameterObject로 올바르게 바인딩되어 있으며, 기본 정렬이 createdAt DESC로 설정되어 있습니다. AdminConsultationController의 패턴과 일관성 있게 구현되었습니다.

src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java (3)

52-59: 페이지네이션 및 목록 조회 로직이 잘 구현되었습니다.

findAllBySearchCondition을 통해 검색 조건과 페이지네이션을 적용한 후 PageResponse로 변환하는 흐름이 적절합니다.


79-84: 단건 조회 로직이 적절합니다.

앱 ID로 조회 후 not found 예외 처리가 올바르게 구현되어 있습니다. 단건 조회에서는 관련 엔티티가 없을 때 예외를 던지는 것이 적절합니다.


86-94: 상태 업데이트 로직이 올바르게 구현되었습니다.

@Transactional이 적용되어 있고, 엔티티 조회 후 updateStatus 메서드를 통해 상태와 remark를 업데이트합니다. Dirty checking을 통해 자동으로 persist됩니다.

src/main/java/redot/redot_server/domain/redot/app/dto/response/RedotAppResponse.java (1)

16-25: NPE 우려는 타당하지 않습니다. RedotApp 엔티티의 plan 필드는 @JoinColumn(name = "plan_id", nullable = false)로 정의되어 있어 데이터베이스 수준에서 NOT NULL 제약이 적용됩니다. 따라서 redotApp.getPlan().getId() 호출 시 NPE가 발생할 수 없습니다.

Likely an incorrect or invalid review comment.

src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java (1)

21-22: 변경사항이 적절합니다.

컨트롤러의 시그니처 변경과 일치하도록 문서 인터페이스가 올바르게 업데이트되었습니다.

src/main/java/redot/redot_server/domain/auth/controller/AdminImpersonationController.java (1)

33-33: 응답 본문 추가는 적절합니다.

쿠키와 함께 응답 본문에도 토큰 정보를 포함하도록 개선되어, 클라이언트가 쿠키와 본문 중 선택하여 사용할 수 있습니다.

src/main/java/redot/redot_server/domain/auth/service/AdminImpersonationService.java (2)

34-35: 토큰 컨텍스트가 OWNER 권한으로 올바르게 설정되었습니다.

이전의 하드코딩된 CMSMemberRole.ADMIN 대신 실제 OWNER의 역할(owner.getRole().name())을 사용하여 토큰을 발급하도록 개선되었습니다. 이는 CMS 관리자 권한이 실제 OWNER 권한과 일치하도록 하려는 PR 목표와 부합합니다.


28-31: CMS OWNER 계정으로 토큰 발급 로직이 올바르게 구현되었습니다.

AuthErrorCode.CMS_MEMBER_NOT_FOUND가 존재하며 적절한 메시지를 포함합니다. RedotApp 생성 시 createManager() 메서드에서 OWNER 역할의 CMSMember가 반드시 생성되므로, 애플리케이션 수준에서 OWNER 존재를 보장합니다. 저장소 쿼리에서 findFirstByRedotApp_IdAndRoleOrderByIdAsc를 사용하므로, 여러 OWNER가 있는 경우에도 안전하게 첫 번째를 반환합니다.

src/main/java/redot/redot_server/domain/cms/member/repository/CMSMemberRepository.java (1)

25-25: 고유 제약조건이 OWNER 단일성을 보장합니다.

데이터베이스 마이그레이션 파일에서 부분 고유 인덱스 ux_cms_members_owner_per_redot_app(라인 168-170)이 정의되어 있어, (redot_app_id) 조합에서 role = 'OWNER' AND status = 'ACTIVE' 조건 하에서 정확히 하나의 OWNER만 존재할 수 있도록 강제합니다. 따라서 findFirstByRedotApp_IdAndRoleOrderByIdAsc 메서드는 최대 하나의 결과를 반환하도록 보장되며, Optional로 반환하는 것과 함께 이 메서드의 사용은 적절합니다.

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: 1

🧹 Nitpick comments (1)
src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryImpl.java (1)

90-103: Predicate 생성 로직 중복 개선 고려.

nameContains, ownerIdEq, statusEq predicate 배열 생성 로직이 findAllBySearchCondition, findAllWithSiteInfo, countBySearchCondition 세 메서드에서 반복됩니다.

Private 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다:

+    private BooleanExpression[] buildPredicates(RedotAppInfoSearchCondition searchCondition) {
+        String name = searchCondition == null ? null : searchCondition.name();
+        Long ownerId = searchCondition == null ? null : searchCondition.redotMemberId();
+        RedotAppStatus status = searchCondition == null ? null : searchCondition.status();
+
+        return new BooleanExpression[]{
+                nameContains(name),
+                ownerIdEq(ownerId),
+                statusEq(status)
+        };
+    }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf15c3f and 0ea440f.

📒 Files selected for processing (7)
  • src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java (1 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryCustom.java (1 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryImpl.java (1 hunks)
  • src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppWithSiteInfo.java (1 hunks)
  • src/main/java/redot/redot_server/domain/site/domain/repository/DomainRepository.java (2 hunks)
  • src/main/java/redot/redot_server/domain/site/setting/repository/SiteSettingRepository.java (1 hunks)
  • src/main/java/redot/redot_server/domain/site/style/repository/StyleInfoRepository.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryCustom.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java (2)
src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryImpl.java (1)
  • RequiredArgsConstructor (23-139)
src/main/java/redot/redot_server/domain/redot/app/exception/RedotAppException.java (1)
  • RedotAppException (5-9)
🔇 Additional comments (12)
src/main/java/redot/redot_server/domain/site/setting/repository/SiteSettingRepository.java (1)

11-12: LGTM!

Spring Data JPA 네이밍 규칙을 올바르게 따르는 배치 조회 메서드입니다. In 접미사와 Collection<Long> 파라미터 조합이 적절합니다.

src/main/java/redot/redot_server/domain/site/style/repository/StyleInfoRepository.java (1)

11-12: LGTM!

기존 findByRedotApp_Id 메서드와 일관된 네이밍 규칙을 사용하며, 중첩 속성 탐색을 위한 언더스코어 표기법이 올바르게 적용되었습니다.

src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppWithSiteInfo.java (1)

8-14: LGTM!

쿼리 결과를 집계하기 위한 불변 레코드로 적절합니다. Left join 사용으로 인해 domain, siteSetting, styleInfo가 nullable일 수 있다는 점이 설계에 반영되어 있습니다.

src/main/java/redot/redot_server/domain/site/domain/repository/DomainRepository.java (1)

20-21: LGTM!

기존 findByRedotAppId 메서드와 일관되게 @EntityGraph 없이 구현되었습니다. 배치 조회를 위한 표준 Spring Data JPA 패턴입니다.

src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepositoryImpl.java (3)

28-52: LGTM!

QueryDSL을 활용한 동적 쿼리 구현이 깔끔합니다. Null-safe 파라미터 처리와 페이지네이션 로직이 적절합니다.


54-88: LGTM!

Left join을 통해 관련 엔티티들을 효율적으로 조회하고 있습니다. filter(appWithInfo -> appWithInfo.redotApp() != null) 처리로 null 결과를 안전하게 필터링합니다.


115-122: 정렬 옵션이 createdAt으로 제한됨.

현재 구현은 createdAt 외의 정렬 속성을 조용히 무시합니다. PR 목표에 따르면 createdAt 정렬만 필요하므로 현재 구현은 적절하지만, 향후 확장 시 다른 정렬 필드 지원이 필요할 수 있습니다.

src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java (5)

50-82: 이전 리뷰의 N+1 쿼리 문제가 해결되었습니다!

목록 조회 시 관련 엔티티(Domain, StyleInfo, SiteSetting)를 IN 쿼리로 일괄 조회하여 Map으로 구성하는 방식으로 변경되어, 이전 리뷰에서 지적된 N+1 쿼리 문제가 효과적으로 해결되었습니다. 또한 빈 목록에 대한 가드 조건도 적절히 처리되어 있습니다.


84-110: 이전 리뷰의 예외 처리 전략이 개선되었습니다!

관련 엔티티가 없을 때 예외를 던지지 않고 null을 반환하도록 변경되어, 이전 리뷰에서 지적된 부분 데이터 표시 문제가 해결되었습니다. nullable 파라미터를 받는 오버로드 메서드가 null 체크를 통해 안전하게 응답을 구성합니다.

단일 엔티티 조회 시 개별 쿼리 실행은 허용 가능한 수준입니다.


112-117: LGTM!

단일 앱 조회 로직이 적절하며, 엔티티가 없을 때 예외를 던지는 것이 타당합니다.


119-127: LGTM!

상태 업데이트 로직이 적절하며, @Transactional 어노테이션이 올바르게 적용되어 있습니다. 엔티티의 updateStatus 메서드를 호출하여 도메인 로직을 캡슐화한 것도 좋은 설계입니다.


59-71: 배치 조회 메서드 확인 완료

요청한 메서드들이 모두 정의되어 있습니다:

  • DomainRepository.findByRedotAppIdIn (Line 20)
  • StyleInfoRepository.findByRedotApp_IdIn (Line 12)
  • SiteSettingRepository.findByRedotAppIdIn (Line 12)

메서드 시그니처와 사용 방식이 올바르게 일치하므로 특별한 조치가 필요하지 않습니다.

Comment on lines +58 to +71
Map<Long, Domain> domainMap = redotAppIds.isEmpty() ? Map.of()
: domainRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(domain -> domain.getRedotApp().getId(), Function.identity()));

Map<Long, StyleInfo> styleInfoMap = redotAppIds.isEmpty() ? Map.of()
: styleInfoRepository.findByRedotApp_IdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(style -> style.getRedotApp().getId(), Function.identity()));

Map<Long, SiteSetting> siteSettingMap = redotAppIds.isEmpty() ? Map.of()
: siteSettingRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(setting -> setting.getRedotApp().getId(), Function.identity()));
Copy link

@coderabbitai coderabbitai bot Dec 17, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

중복 키 발생 시 예외 처리 필요

Collectors.toMap에 merge function이 없어 하나의 RedotApp에 여러 Domain/StyleInfo/SiteSetting이 존재할 경우 IllegalStateException이 발생합니다. 또한 getRedotApp()이 null을 반환하면 NPE가 발생할 수 있습니다.

다음 diff를 적용하여 중복 키와 null 처리를 개선하세요:

 Map<Long, Domain> domainMap = redotAppIds.isEmpty() ? Map.of()
         : domainRepository.findByRedotAppIdIn(redotAppIds)
         .stream()
+        .filter(domain -> domain.getRedotApp() != null)
-        .collect(Collectors.toMap(domain -> domain.getRedotApp().getId(), Function.identity()));
+        .collect(Collectors.toMap(
+                domain -> domain.getRedotApp().getId(),
+                Function.identity(),
+                (existing, replacement) -> existing));

 Map<Long, StyleInfo> styleInfoMap = redotAppIds.isEmpty() ? Map.of()
         : styleInfoRepository.findByRedotApp_IdIn(redotAppIds)
         .stream()
+        .filter(style -> style.getRedotApp() != null)
-        .collect(Collectors.toMap(style -> style.getRedotApp().getId(), Function.identity()));
+        .collect(Collectors.toMap(
+                style -> style.getRedotApp().getId(),
+                Function.identity(),
+                (existing, replacement) -> existing));

 Map<Long, SiteSetting> siteSettingMap = redotAppIds.isEmpty() ? Map.of()
         : siteSettingRepository.findByRedotAppIdIn(redotAppIds)
         .stream()
+        .filter(setting -> setting.getRedotApp() != null)
-        .collect(Collectors.toMap(setting -> setting.getRedotApp().getId(), Function.identity()));
+        .collect(Collectors.toMap(
+                setting -> setting.getRedotApp().getId(),
+                Function.identity(),
+                (existing, replacement) -> existing));
📝 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
Map<Long, Domain> domainMap = redotAppIds.isEmpty() ? Map.of()
: domainRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(domain -> domain.getRedotApp().getId(), Function.identity()));
Map<Long, StyleInfo> styleInfoMap = redotAppIds.isEmpty() ? Map.of()
: styleInfoRepository.findByRedotApp_IdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(style -> style.getRedotApp().getId(), Function.identity()));
Map<Long, SiteSetting> siteSettingMap = redotAppIds.isEmpty() ? Map.of()
: siteSettingRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(setting -> setting.getRedotApp().getId(), Function.identity()));
Map<Long, Domain> domainMap = redotAppIds.isEmpty() ? Map.of()
: domainRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.filter(domain -> domain.getRedotApp() != null)
.collect(Collectors.toMap(
domain -> domain.getRedotApp().getId(),
Function.identity(),
(existing, replacement) -> existing));
Map<Long, StyleInfo> styleInfoMap = redotAppIds.isEmpty() ? Map.of()
: styleInfoRepository.findByRedotApp_IdIn(redotAppIds)
.stream()
.filter(style -> style.getRedotApp() != null)
.collect(Collectors.toMap(
style -> style.getRedotApp().getId(),
Function.identity(),
(existing, replacement) -> existing));
Map<Long, SiteSetting> siteSettingMap = redotAppIds.isEmpty() ? Map.of()
: siteSettingRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.filter(setting -> setting.getRedotApp() != null)
.collect(Collectors.toMap(
setting -> setting.getRedotApp().getId(),
Function.identity(),
(existing, replacement) -> existing));
🤖 Prompt for AI Agents
In
src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java
around lines 58 to 71, the three Collectors.toMap calls can throw
IllegalStateException on duplicate keys and NPE if getRedotApp() returns null;
update each stream to filter out entries with null getRedotApp() (or null
getRedotApp().getId()) before collecting and supply a merge function to
Collectors.toMap to resolve duplicate keys (for example, choose the first or
last occurrence consistently), ensuring no NPE and no duplicate-key exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

한개의 앱에는 하나의 domain, styleinfo, sitesetting만 존재해요.

Copy link

Choose a reason for hiding this comment

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

@eraser502 이해했습니다! 1:1 관계라면 중복 키 문제는 발생하지 않겠네요. 명확하게 설명해 주셔서 감사합니다. 😊


✏️ Learnings added
Learnt from: eraser502
Repo: redotlabs/server PR: 134
File: src/main/java/redot/redot_server/domain/admin/service/AdminRedotAppService.java:58-71
Timestamp: 2025-12-17T01:39:43.440Z
Learning: In the RedotApp domain model, there is a 1:1 relationship between RedotApp and Domain, StyleInfo, and SiteSetting entities. Each RedotApp has exactly one Domain, one StyleInfo, and one SiteSetting.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@eraser502 eraser502 merged commit abc9ff8 into develop Dec 17, 2025
1 check passed
@github-project-automation github-project-automation bot moved this to Done in Admin Dec 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Feature / Admin / App API

2 participants