Skip to content

feat/48 :: 크리에이터 권한 요청 승인/거절 API 구현#13

Open
lian2945 wants to merge 10 commits intofeat/98from
feat/48
Open

feat/48 :: 크리에이터 권한 요청 승인/거절 API 구현#13
lian2945 wants to merge 10 commits intofeat/98from
feat/48

Conversation

@lian2945
Copy link
Copy Markdown
Collaborator

@lian2945 lian2945 commented Apr 6, 2026

📌 관련 이슈

  • close #48

📝 변경 사항 요약

작업 유형

  • ✨ feat: 새로운 기능 추가 (기능 단위 완료 후 PR)
  • 🐛 fix: 버그 수정 (버그 1개 = PR 1개)
  • ♻️ refactor: 코드 리팩토링 (작업 단위 완료 후 PR)
  • ✅ test: 테스트 코드 추가/수정 (작업 단위 완료 후 PR)

변경 내용

  • 관리자 pending 목록 조회 API를 추가했습니다.
  • 승인/거절 처리 API 및 상태 전이 로직을 구현했습니다.
  • 심사 커서 페이징/검증을 반영했습니다.

변경 이유

  • 요청 처리(심사)를 요청 생성 기능과 분리해 운영 단위를 명확히 하기 위해서입니다.

✅ 테스트 체크리스트

  • 단위 테스트 작성 및 통과
  • 통합 테스트 통과
  • 기존 기능 정상 동작 확인 (Regression)
  • API 응답값 확인
  • 예외 케이스 처리 확인
  • 로컬 환경에서 직접 테스트 완료

📸 스크린샷 / 로그

펼쳐보기

🔄 동작 플로우 (Mermaid)

%%{init: {'flowchart': {'useMaxWidth': true, 'htmlLabels': true}} }%%
flowchart TD
    A[pending 조회] --> B[대상 선택]
    B --> C{승인/거절}
    C -->|승인| D[APPROVED 업데이트]
    C -->|거절| E[REJECTED 업데이트]
    D --> F[승인 이벤트 발행]
    E --> G[거절 이벤트 발행]
Loading

💬 리뷰어에게

크리에이터 권한 요청 승인/거절 API 구현 범위를 중심으로 리뷰 부탁드립니다.

Copilot AI review requested due to automatic review settings April 6, 2026 10:50
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

크리에이터 권한(인증) 심사 흐름을 분리/추가하기 위해 creator 서비스에 인증(심사) 도메인/영속/통신 기반을 깔고, user 서비스에는 닉네임 조회 gRPC 및 사용자 상태(정지/해제 등) 관련 변경을 확장하며, auth/user의 일부 유스케이스 시그니처를 Command/Query DTO 기반으로 정리한 PR입니다.

Changes:

  • user 서비스: GetUserNickname gRPC 추가, 정지/정지해제 이벤트 및 상태 전이 로직 확장, Repository Port 메서드 정리(save/update/lock)
  • creator 서비스: 인증(심사) 도메인/영속/JPA/예외처리/커서 페이징 유틸 및 user 닉네임 gRPC 클라이언트 추가
  • auth 서비스: 일부 유스케이스 입력을 Command DTO로 변경 및 DTO 패키지 정리

Reviewed changes

Copilot reviewed 51 out of 75 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
services/user/src/main/proto/user.proto user gRPC에 닉네임 조회 RPC 추가
services/user/src/main/java/kr/magicbox/user/domain/exception/UserSessionNotActiveException.java 세션 비활성 예외 추가
services/user/src/main/java/kr/magicbox/user/domain/exception/UserNotBannedException.java 정지해제 불가 예외 추가
services/user/src/main/java/kr/magicbox/user/domain/exception/UserBannedException.java 정지 사용자 제한 예외 추가
services/user/src/main/java/kr/magicbox/user/domain/exception/UserAlreadyInactiveException.java (삭제) 기존 비활성 예외 제거
services/user/src/main/java/kr/magicbox/user/domain/event/UserUnbannedEvent.java 유저 정지해제 도메인 이벤트 추가
services/user/src/main/java/kr/magicbox/user/domain/event/UserDomainEventType.java USER_UNBANNED 타입 추가
services/user/src/main/java/kr/magicbox/user/domain/enums/UserStatus.java INACTIVE 제거, BANNED 추가
services/user/src/main/java/kr/magicbox/user/domain/aggregate/User.java activate/ban/unban/delete 상태전이 정리
services/user/src/main/java/kr/magicbox/user/application/service/WithdrawUserService.java withdraw 커맨드화 + 락 조회 적용
services/user/src/main/java/kr/magicbox/user/application/service/UserQueryService.java 프로필 조회 Query DTO/Result 패키지 정리
services/user/src/main/java/kr/magicbox/user/application/service/UserCommandService.java 업데이트 커맨드 시그니처 정리
services/user/src/main/java/kr/magicbox/user/application/service/UnbanUserService.java 유저 정지해제 유스케이스 추가
services/user/src/main/java/kr/magicbox/user/application/service/ManageUserSessionService.java 세션 관리 Command DTO로 변경
services/user/src/main/java/kr/magicbox/user/application/service/LoginService.java 저장/업데이트 메서드명 변경 반영
services/user/src/main/java/kr/magicbox/user/application/service/GetUserNicknameService.java 닉네임 조회 유스케이스 구현 추가
services/user/src/main/java/kr/magicbox/user/application/service/CheckUserActiveService.java active 체크 Query DTO 도입
services/user/src/main/java/kr/magicbox/user/application/service/BanUserService.java ban 도메인 메서드로 변경
services/user/src/main/java/kr/magicbox/user/application/port/out/UserRepositoryPort.java save/update 및 withLock 조회 추가
services/user/src/main/java/kr/magicbox/user/application/port/out/ReviewQueryPort.java UserReviewResult 패키지 변경
services/user/src/main/java/kr/magicbox/user/application/port/in/WithdrawUserUseCase.java WithdrawUserCommand로 시그니처 변경
services/user/src/main/java/kr/magicbox/user/application/port/in/UserQueryUseCase.java GetUserProfileQuery로 시그니처 변경
services/user/src/main/java/kr/magicbox/user/application/port/in/UserCommandUseCase.java UpdateUserProfileCommand로 시그니처 변경
services/user/src/main/java/kr/magicbox/user/application/port/in/UnbanUserUseCase.java unban 유스케이스 포트 추가
services/user/src/main/java/kr/magicbox/user/application/port/in/ManageUserSessionUseCase.java Start/EndSessionCommand로 변경
services/user/src/main/java/kr/magicbox/user/application/port/in/LoadUserCredentialUseCase.java command/result 패키지 정리
services/user/src/main/java/kr/magicbox/user/application/port/in/GetUserNicknameUseCase.java 닉네임 조회 포트 추가
services/user/src/main/java/kr/magicbox/user/application/port/in/CheckUserActiveUseCase.java CheckUserActiveQuery로 변경
services/user/src/main/java/kr/magicbox/user/application/port/in/BanUserUseCase.java 포맷 정리(실질 변경 없음)
services/user/src/main/java/kr/magicbox/user/application/dto/UpdateUserProfileCommand.java (삭제) 기존 커맨드 위치 제거
services/user/src/main/java/kr/magicbox/user/application/dto/result/UserReviewResult.java result 패키지로 이동
services/user/src/main/java/kr/magicbox/user/application/dto/result/LoadUserCredentialResult.java result 패키지로 이동
services/user/src/main/java/kr/magicbox/user/application/dto/result/GetUserProfileResult.java result 패키지로 이동
services/user/src/main/java/kr/magicbox/user/application/dto/query/GetUserProfileQuery.java 프로필 조회 Query DTO 추가
services/user/src/main/java/kr/magicbox/user/application/dto/query/CheckUserActiveQuery.java active 체크 Query DTO 추가
services/user/src/main/java/kr/magicbox/user/application/dto/command/WithdrawUserCommand.java withdraw Command DTO 추가
services/user/src/main/java/kr/magicbox/user/application/dto/command/UpdateUserProfileCommand.java update Command DTO로 재정의
services/user/src/main/java/kr/magicbox/user/application/dto/command/UnbanUserCommand.java unban Command DTO 추가
services/user/src/main/java/kr/magicbox/user/application/dto/command/StartSessionCommand.java start session Command DTO 추가
services/user/src/main/java/kr/magicbox/user/application/dto/command/LoadUserCredentialCommand.java command 패키지로 이동
services/user/src/main/java/kr/magicbox/user/application/dto/command/EndSessionCommand.java end session Command DTO 추가
services/user/src/main/java/kr/magicbox/user/application/dto/command/BanUserCommand.java ban Command DTO 추가
services/user/src/main/java/kr/magicbox/user/adapter/out/persistence/UserJpaAdapter.java withLock 조회/메서드명(save/update) 반영
services/user/src/main/java/kr/magicbox/user/adapter/out/persistence/repository/UserJpaRepository.java PESSIMISTIC_WRITE 조회 추가
services/user/src/main/java/kr/magicbox/user/adapter/out/persistence/entity/UserEntity.java status 업데이트 반영(+버전 제거)
services/user/src/main/java/kr/magicbox/user/adapter/out/communication/grpc/ReviewQueryGrpcAdapter.java UserReviewResult 패키지 변경
services/user/src/main/java/kr/magicbox/user/adapter/in/web/UserQueryController.java Query DTO 기반 호출로 변경
services/user/src/main/java/kr/magicbox/user/adapter/in/web/UserCommandController.java Command DTO 기반 호출로 변경
services/user/src/main/java/kr/magicbox/user/adapter/in/web/exception/handler/GlobalExceptionHandler.java optimistic lock handler 제거
services/user/src/main/java/kr/magicbox/user/adapter/in/web/dto/response/GetUserProfileResponse.java response 패키지로 이동/의존 변경
services/user/src/main/java/kr/magicbox/user/adapter/in/web/dto/request/UpdateUserProfileRequest.java request 패키지로 이동 + userId 주입
services/user/src/main/java/kr/magicbox/user/adapter/in/web/AdminUserCommandController.java unban 엔드포인트 추가
services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java session 유스케이스 Command DTO 반영
services/user/src/main/java/kr/magicbox/user/adapter/in/grpc/UserGrpcService.java CheckUserActive Query DTO + GetUserNickname gRPC 구현
services/creator/src/main/resources/application-prod.yml creator-prod 환경 설정 추가
services/creator/src/main/resources/application-local.yml creator-local 설정 확장(grpc/kafka/jpa 등)
services/creator/src/main/resources/application-dev.yml creator-dev 환경 설정 추가
services/creator/src/main/proto/user.proto creator 측 user gRPC stub용 proto 추가
services/creator/src/main/java/kr/magicbox/creator/global/exception/SystemError.java creator 예외 베이스(서버에러) 추가
services/creator/src/main/java/kr/magicbox/creator/global/exception/BusinessException.java creator 예외 베이스(비즈니스) 추가
services/creator/src/main/java/kr/magicbox/creator/global/exception/BaseException.java creator 공통 예외 추가
services/creator/src/main/java/kr/magicbox/creator/global/configuration/PropertiesConfiguration.java @ConfigurationPropertiesScan 추가
services/creator/src/main/java/kr/magicbox/creator/domain/vo/UserId.java creator UserId VO 추가
services/creator/src/main/java/kr/magicbox/creator/domain/vo/Nickname.java creator Nickname VO 추가
services/creator/src/main/java/kr/magicbox/creator/domain/vo/CreatorId.java creator CreatorId VO 추가
services/creator/src/main/java/kr/magicbox/creator/domain/vo/CreatorCertificationResult.java 심사 결과 VO 추가
services/creator/src/main/java/kr/magicbox/creator/domain/vo/CreatorCertificationRequest.java 심사 요청 VO 추가
services/creator/src/main/java/kr/magicbox/creator/domain/vo/CreatorCertificationId.java 인증 ID VO 추가
services/creator/src/main/java/kr/magicbox/creator/domain/exception/InvalidFieldException.java 입력 검증 예외 추가
services/creator/src/main/java/kr/magicbox/creator/domain/exception/InvalidCertificationReviewStatusException.java PENDING 리뷰 금지 예외
services/creator/src/main/java/kr/magicbox/creator/domain/exception/CreatorNotBannedException.java 정지해제 불가 예외
services/creator/src/main/java/kr/magicbox/creator/domain/exception/CreatorAlreadyExistsException.java 중복 creator 예외 추가
services/creator/src/main/java/kr/magicbox/creator/domain/exception/CertificationNotFoundException.java 인증 신청 미존재 예외
services/creator/src/main/java/kr/magicbox/creator/domain/exception/CertificationAlreadyReviewedException.java 중복 심사 예외
services/creator/src/main/java/kr/magicbox/creator/domain/event/CreatorDomainEventType.java creator 이벤트 타입 추가
services/creator/src/main/java/kr/magicbox/creator/domain/event/CreatorDomainEvent.java creator 이벤트 인터페이스 추가
services/creator/src/main/java/kr/magicbox/creator/domain/event/CertificationRejectedEvent.java 인증 거절 이벤트 추가
services/creator/src/main/java/kr/magicbox/creator/domain/event/CertificationApprovedEvent.java 인증 승인 이벤트 추가
services/creator/src/main/java/kr/magicbox/creator/domain/enums/MagicGenre.java 장르 enum 추가
services/creator/src/main/java/kr/magicbox/creator/domain/enums/CreatorStatus.java creator 상태 enum 추가
services/creator/src/main/java/kr/magicbox/creator/domain/enums/CreatorCertificationStatus.java 인증 상태 enum 추가
services/creator/src/main/java/kr/magicbox/creator/domain/constants/CreatorPolicyConstants.java 정책 상수 추가
services/creator/src/main/java/kr/magicbox/creator/domain/aggregate/CreatorCertification.java 인증 도메인 로직 추가
services/creator/src/main/java/kr/magicbox/creator/domain/aggregate/Creator.java creator 도메인 로직 추가
services/creator/src/main/java/kr/magicbox/creator/application/service/certification/ReviewCertificationService.java 심사 승인/거절 유스케이스 추가
services/creator/src/main/java/kr/magicbox/creator/application/service/certification/GetAllPendingCertificationsService.java pending 목록 조회 유스케이스 추가
services/creator/src/main/java/kr/magicbox/creator/application/port/out/UserNicknameQueryPort.java user 닉네임 조회 포트
services/creator/src/main/java/kr/magicbox/creator/application/port/out/CreatorRepositoryPort.java creator 저장소 포트
services/creator/src/main/java/kr/magicbox/creator/application/port/out/CreatorDomainEventRepositoryPort.java creator 이벤트 저장 포트
services/creator/src/main/java/kr/magicbox/creator/application/port/out/CreatorCertificationRepositoryPort.java 인증 저장소 포트
services/creator/src/main/java/kr/magicbox/creator/application/port/in/ReviewCreatorCertificationUseCase.java (중복) 심사 유스케이스 포트 추가
services/creator/src/main/java/kr/magicbox/creator/application/port/in/ReviewCertificationUseCase.java 심사 유스케이스 포트 추가
services/creator/src/main/java/kr/magicbox/creator/application/port/in/GetAllPendingCreatorCertificationsUseCase.java pending 조회 포트 추가
services/creator/src/main/java/kr/magicbox/creator/application/dto/result/PendingCertificationResult.java pending 응답 result 추가
services/creator/src/main/java/kr/magicbox/creator/application/dto/query/GetAllPendingCertificationsQuery.java pending 조회 query 추가
services/creator/src/main/java/kr/magicbox/creator/application/dto/command/ReviewCertificationCommand.java 심사 커맨드 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/vo/CreatorCertificationResultVO.java 인증 결과 VO(임베디드)
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/vo/CreatorCertificationRequestVO.java 인증 요청 VO(임베디드)
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/repository/CreatorJpaRepository.java creator 조회/락/커서 쿼리 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/repository/CreatorDomainEventRepository.java creator 이벤트 JPA repo 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/repository/CreatorCertificationJpaRepository.java 인증 JPA repo + 커서 쿼리 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/mapper/CreatorMapper.java creator mapper 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/mapper/CreatorCertificationRequestMapper.java request VO mapper 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/mapper/CreatorCertificationMapper.java 인증 mapper 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/mapper/CertificationResultMapper.java result VO mapper 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/entity/CreatorEntity.java creator 엔티티 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/entity/CreatorDomainEventEntity.java creator 이벤트 엔티티 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/entity/CreatorCertificationEntity.java 인증 엔티티(+@Version) 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/entity/BaseEntity.java 공통 엔티티 베이스 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/CreatorJpaAdapter.java creator JPA 어댑터 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/CreatorDomainEventAdapter.java creator 이벤트 저장 어댑터 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/CreatorCertificationJpaAdapter.java 인증 JPA 어댑터 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/persistence/configuration/JpaConfiguration.java JPA Auditing 활성화
services/creator/src/main/java/kr/magicbox/creator/adapter/out/communication/ServiceHost.java grpc 채널 host enum 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/out/communication/grpc/UserNicknameQueryGrpcAdapter.java user 닉네임 gRPC 클라이언트
services/creator/src/main/java/kr/magicbox/creator/adapter/out/communication/grpc/exception/UserServiceUnavailableException.java user 서비스 장애 예외
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/validation/CursorSizeValidator.java size 범위 validator 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/validation/CursorSize.java size 검증 어노테이션 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/exception/handler/GlobalExceptionHandler.java creator 글로벌 예외 처리 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/exception/handler/ErrorResponse.java 에러 응답 모델 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/dto/response/PendingCertificationResponse.java pending 응답 DTO 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/dto/response/CursorResponse.java 커서 응답 래퍼 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/dto/request/ReviewCertificationRequest.java 심사 요청 DTO 추가
services/creator/src/main/java/kr/magicbox/creator/adapter/in/web/constants/CursorConstants.java 커서 상수 추가
services/creator/build.gradle creator 모듈 의존성/protobuf 설정 추가
services/auth/src/main/java/kr/magicbox/auth/domain/exception/UserInactiveException.java 비활성 사용자 예외 추가/명칭 변경
services/auth/src/main/java/kr/magicbox/auth/domain/exception/UserBannedException.java 정지 사용자 예외 추가
services/auth/src/main/java/kr/magicbox/auth/domain/exception/InActiveUserException.java (삭제) 기존 예외 제거
services/auth/src/main/java/kr/magicbox/auth/application/service/RefreshTokenService.java RefreshTokenCommand 도입
services/auth/src/main/java/kr/magicbox/auth/application/service/LogoutService.java LogoutCommand 도입 + 예외 변경
services/auth/src/main/java/kr/magicbox/auth/application/service/LoginService.java DTO 패키지 정리(import 변경)
services/auth/src/main/java/kr/magicbox/auth/application/service/HandleUserWithdrawnService.java HandleUserWithdrawnCommand 도입
services/auth/src/main/java/kr/magicbox/auth/application/service/HandleUserBannedService.java HandleUserBannedCommand 도입
services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserCredentialPort.java UserResult 패키지 변경
services/auth/src/main/java/kr/magicbox/auth/application/port/in/RefreshTokenUseCase.java RefreshTokenCommand 시그니처
services/auth/src/main/java/kr/magicbox/auth/application/port/in/LogoutUseCase.java LogoutCommand 시그니처
services/auth/src/main/java/kr/magicbox/auth/application/port/in/LoginUseCase.java DTO 패키지 정리
services/auth/src/main/java/kr/magicbox/auth/application/port/in/HandleUserWithdrawnUseCase.java HandleUserWithdrawnCommand 시그니처
services/auth/src/main/java/kr/magicbox/auth/application/port/in/HandleUserBannedUseCase.java HandleUserBannedCommand 시그니처
services/auth/src/main/java/kr/magicbox/auth/application/dto/result/UserResult.java result 패키지로 이동
services/auth/src/main/java/kr/magicbox/auth/application/dto/result/TokenResult.java result 패키지로 이동
services/auth/src/main/java/kr/magicbox/auth/application/dto/result/IssueTokenResult.java result 패키지로 이동
services/auth/src/main/java/kr/magicbox/auth/application/dto/command/RefreshTokenCommand.java refresh command 추가
services/auth/src/main/java/kr/magicbox/auth/application/dto/command/LogoutCommand.java logout command 추가
services/auth/src/main/java/kr/magicbox/auth/application/dto/command/LoginCommand.java command 패키지로 이동
services/auth/src/main/java/kr/magicbox/auth/application/dto/command/HandleUserWithdrawnCommand.java withdrawn command 추가
services/auth/src/main/java/kr/magicbox/auth/application/dto/command/HandleUserBannedCommand.java banned command 추가
services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserGrpcAdapter.java UserResult 패키지 변경
services/auth/src/main/java/kr/magicbox/auth/adapter/in/web/dto/response/AccessTokenResponse.java response 패키지로 이동
services/auth/src/main/java/kr/magicbox/auth/adapter/in/web/dto/request/LoginRequest.java request 패키지로 이동
services/auth/src/main/java/kr/magicbox/auth/adapter/in/web/AuthCommandController.java refresh/logout command 적용 + DTO 경로 변경
services/auth/src/main/java/kr/magicbox/auth/adapter/in/security/oauth2/OAuth2LoginSuccessHandler.java UserResult 패키지 변경
services/auth/src/main/java/kr/magicbox/auth/adapter/in/kafka/UserEventKafkaListener.java 이벤트 핸들링 Command DTO 적용
.github/workflows/sonarcloud-analyze.yml Sonar 분석용 JDK/바이너리 설정 추가

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +22 to +47
@Service
@RequiredArgsConstructor
public class ReviewCertificationService implements ReviewCertificationUseCase {

private final CreatorCertificationRepositoryPort certificationRepositoryPort;
private final CreatorRepositoryPort creatorRepositoryPort;
private final CreatorDomainEventRepositoryPort eventRepositoryPort;
private final UserNicknameQueryPort userNicknameQueryPort;

@Transactional
@Override
public void reviewCertification(ReviewCertificationCommand command) {
CreatorCertification certification = certificationRepositoryPort.findById(command.certificationId())
.orElseThrow(CertificationNotFoundException::new);

certification.review(command.certificationStatus(), CreatorCertificationResult.of(command.reviewMessage()));
certificationRepositoryPort.update(certification);

if (certification.isApproved()) {
createCreator(certification);
eventRepositoryPort.save(buildApprovedEvent(certification));
}
else {
eventRepositoryPort.save(buildRejectedEvent(certification));
}
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

PR 설명에는 'pending 목록 조회 API' 및 '승인/거절 처리 API' 구현이 포함되어 있는데, 현재 creator 서비스 코드에는 이를 노출하는 Web Controller(@RestController/@RequestMapping)가 추가되지 않았습니다(검색 기준 controller 클래스 부재). 실제 API 엔드포인트를 추가하거나, 본 PR 범위를 '도메인/애플리케이션 레이어 구현'으로 수정해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +8
package kr.magicbox.creator.application.port.in;

import kr.magicbox.creator.application.dto.command.ReviewCertificationCommand;

public interface ReviewCreatorCertificationUseCase {

void reviewCreatorCertification(ReviewCertificationCommand command);
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

ReviewCreatorCertificationUseCaseReviewCertificationUseCase가 동일한 책임(심사 승인/거절)을 가지는 것으로 보이는데, 현재 구현체는 ReviewCertificationUseCase만 구현하고 다른 인터페이스는 사용처가 없습니다. 중복/미사용 인터페이스는 삭제하거나 하나로 통일해 API 경계를 명확히 해 주세요.

Copilot uses AI. Check for mistakes.
lian2945 and others added 8 commits April 6, 2026 20:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lian2945 lian2945 requested a review from f-lab-ted April 8, 2026 02:54
Copy link
Copy Markdown

@f-lab-ted f-lab-ted left a comment

Choose a reason for hiding this comment

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

리뷰 요약

파악 내용

이 PR은 크리에이터 권한 요청에 대한 관리자 심사(승인/거절) API를 구현한 것으로, 헥사고날 아키텍처를 기반으로 Admin 전용 Command/Query 컨트롤러, 심사 UseCase/Service, 커서 기반 페이징, 도메인 이벤트 발행 등을 포함하고 있습니다. 도메인 레이어에서 상태 전이 검증(review() 메서드)과 비즈니스 예외 처리가 잘 분리되어 있으며, 승인 시 Creator 생성 + 이벤트 발행, 거절 시 이벤트 발행의 흐름이 명확하게 구현되어 있습니다. 커서 페이징, 커스텀 Validation 어노테이션(@CursorSize), CursorResponse 제네릭 유틸 등 재사용 가능한 컴포넌트도 잘 설계되어 있습니다.

잘한 점

도메인 어그리거트(CreatorCertification)에 상태 전이 로직과 검증을 집중시켜 비즈니스 규칙이 명확히 드러나며, 이벤트 기반 비동기 처리를 위한 도메인 이벤트 저장 구조가 잘 갖춰져 있습니다.

보완할 점

  1. 관리자 인증/인가 부재: Admin 컨트롤러에 @PreAuthorize, @Secured 등 관리자 권한 검증이 전혀 없어 일반 사용자도 심사 API에 접근 가능한 보안 취약점이 존재합니다.
  2. 커서 페이징 중복 조회 버그: findAllByStatusWithCursor 쿼리에서 c.id >= :cursorId 조건으로 인해 이전 페이지의 마지막 항목이 다음 페이지에 중복 포함됩니다.
  3. 심사 요청 시 PENDING 상태 허용: API 레벨에서 CreatorCertificationStatus 전체를 받아들이므로, 클라이언트가 PENDING을 보내면 도메인 예외가 발생합니다. API 경계에서 입력 가능한 상태를 제한해야 합니다.
  4. 심사자 정보 미기록: 누가 승인/거절했는지 추적할 수 없어 감사(audit) 관점에서 문제가 됩니다.

결론

관리자 인증/인가 부재와 커서 페이징 버그는 반드시 수정 바랍니다. Request Changes 드립니다.

@RestController
@RequestMapping("/api/creator/certification")
@RequiredArgsConstructor
public class AdminCreatorCertificationCommandController {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Critical] 관리자 인증/인가 검증 부재

이 컨트롤러는 관리자만 접근해야 하는 심사 API인데, @PreAuthorize("hasRole('ADMIN')") 또는 @Secured("ROLE_ADMIN") 같은 인가 처리가 전혀 없습니다. 일반 사용자도 이 엔드포인트를 호출하여 인증 요청을 승인/거절할 수 있는 보안 취약점입니다.

또한 @AuthenticationPrincipal도 없어서 누가 심사했는지 기록할 수 없습니다. 감사(audit) 추적을 위해 심사자 ID를 command에 포함시키는 것을 권장합니다.

@PreAuthorize("hasRole('ADMIN')")
@PatchMapping("/{creatorCertificationId}/review")
public ResponseEntity<Void> reviewCertification(
        @AuthenticationPrincipal AdminId adminId,
        @PathVariable Long creatorCertificationId,
        @Valid @RequestBody ReviewCertificationRequest request
) {
    reviewCreatorCertificationUseCase.reviewCreatorCertification(
        request.toCommand(creatorCertificationId, adminId));
    ...
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

관리자 인증은 Istio 딴에서 처리하려고 하고 있습니다

@Query("SELECT c FROM CreatorCertificationEntity c WHERE c.userId = :userId AND (:cursorId IS NULL OR c.id <= :cursorId) ORDER BY c.id DESC LIMIT :size")
List<CreatorCertificationEntity> findAllByUserIdWithCursor(@Param("userId") Long userId, @Param("cursorId") Long cursorId, @Param("size") int size);

@Query("SELECT c FROM CreatorCertificationEntity c WHERE c.status = :status AND (:cursorId IS NULL OR c.id >= :cursorId) ORDER BY c.id ASC LIMIT :size")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Critical] 커서 페이징 중복 조회 버그

c.id >= :cursorId 조건으로 인해 이전 페이지의 마지막 항목이 다음 페이지에도 포함됩니다.

컨트롤러에서 CursorResponsenextCursor는 현재 페이지 마지막 항목의 ID로 설정됩니다. 다음 요청 시 해당 ID가 cursor로 들어오면 id >= cursor이므로 해당 항목이 다시 조회됩니다.

>=>로 변경해야 합니다:

-- Before
WHERE c.status = :status AND (:cursorId IS NULL OR c.id >= :cursorId)
-- After  
WHERE c.status = :status AND (:cursorId IS NULL OR c.id > :cursorId)

참고로 같은 파일의 findAllByUserIdWithCursor도 동일한 이슈가 있습니다 (<=<).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 변경하였습니다!


@Builder
public record ReviewCertificationRequest(
@NotNull(message = "인증 상태는 필수입니다.") CreatorCertificationStatus status,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] API 경계에서 심사 가능 상태를 제한해야 합니다

CreatorCertificationStatus에는 PENDING, APPROVED, REJECTED 세 값이 있는데, 심사 요청에서 PENDING을 보내면 도메인 레이어(CreatorCertification.review())에서 InvalidCertificationReviewStatusException이 발생합니다.

도메인 예외에 의존하기보다, API 경계에서 입력 가능한 값을 제한하는 것이 바람직합니다. 방법 예시:

  1. 심사 전용 enum을 별도로 정의 (예: ReviewDecision { APPROVED, REJECTED })하고 request에서 사용
  2. 또는 커스텀 validation으로 PENDING 값을 거부

이렇게 하면 잘못된 입력에 대해 400 Bad Request를 명확한 메시지와 함께 응답할 수 있고, 도메인 로직 진입 전에 빠르게 실패합니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 수정하였습니다!

@RestController
@RequestMapping("/api/creator/certification")
@RequiredArgsConstructor
@Validated
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] 관리자 인가 검증 부재

Command 컨트롤러와 동일하게, pending 목록 조회도 관리자 전용 기능이지만 인가 처리가 없습니다. @PreAuthorize("hasRole('ADMIN')") 등을 추가하거나, URL 경로를 /api/admin/creator/certification으로 분리하여 Spring Security filter chain에서 일괄 처리하는 것을 권장합니다.

현재 /api/creator/certification 경로를 사용자 컨트롤러(CreatorCertificationCommandController)와 공유하고 있어, 경로만으로는 관리자 API를 구분할 수 없습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

관리자 인증은 Istio 딴에서 처리하려고 하고 있습니다

@@ -0,0 +1,77 @@
package kr.magicbox.creator.application.service.certification;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] 심사자(Reviewer) 정보 미기록

현재 ReviewCertificationCommand에 심사자 ID가 포함되어 있지 않아, 누가 해당 인증을 승인/거절했는지 추적할 수 없습니다. 운영 관점에서 감사(audit) 추적이 불가능한 것은 문제가 될 수 있습니다.

ReviewCertificationCommandreviewerId (또는 adminId)를 추가하고, CreatorCertificationResult에도 심사자 정보를 포함시키는 것을 권장합니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 그것이 맞다고 생각하여서 포함시켰습니다!

@@ -0,0 +1,77 @@
package kr.magicbox.creator.application.service.certification;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Minor] 이벤트의 reviewedAt과 도메인의 reviewedAt 불일치 가능성

CreatorCertificationResult.of(command.reviewMessage())에서 Instant.now()reviewedAt이 설정되고, 이후 buildApprovedEvent/buildRejectedEvent에서 다시 Instant.now()를 호출합니다. 두 시점 사이에 미세한 시간 차이가 발생할 수 있습니다.

이벤트의 reviewedAt은 도메인에 기록된 값을 사용하는 것이 정합성 측면에서 바람직합니다:

private CertificationApprovedEvent buildApprovedEvent(CreatorCertification certification) {
    return CertificationApprovedEvent.builder()
            .userId(certification.getUserId())
            .certificationId(certification.getId())
            .reviewedAt(certification.getResult().reviewedAt()) // 도메인 값 사용
            .build();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 그렇게 사용하는 것이 더 정확할 것 같습니다!

@@ -0,0 +1,77 @@
package kr.magicbox.creator.application.service.certification;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] 승인 시 Creator 중복 생성 방지 로직 부재

createCreator()에서 creatorRepositoryPort.save(creator)를 호출하는데, 해당 userId로 이미 Creator가 존재하는 경우에 대한 검증이 없습니다.

예를 들어, 동일 사용자가 과거에 승인되어 Creator가 이미 존재하는 상태에서 새로운 인증 요청이 승인되면 중복 Creator가 생성될 수 있습니다. DB 유니크 제약조건(CreatorEntityuserId unique)에 의해 예외가 발생하겠지만, 비즈니스 로직에서 명시적으로 검증하는 것이 바람직합니다.

private void createCreator(CreatorCertification certification) {
    creatorRepositoryPort.findByUserId(certification.getUserId())
        .ifPresent(c -> { throw new CreatorAlreadyExistsException(); });
    ...
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

existsByUserId로 검증했습니다!


@RestController
@RequestMapping("/api/creator/certification")
@RequiredArgsConstructor
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Admin API 경로 분리 제안

현재 Admin 컨트롤러와 사용자 컨트롤러가 동일한 base path(/api/creator/certification)를 사용합니다. Admin API를 /api/admin/creator/certification으로 분리하면:

  1. Spring Security에서 /api/admin/** 패턴으로 일괄 인가 처리 가능
  2. API Gateway 레벨에서 admin 요청 라우팅/로깅 분리 가능
  3. API 문서에서 관리자/사용자 API 구분이 명확해짐

마이크로서비스 환경에서 관리자 트래픽과 사용자 트래픽을 분리하는 것은 운영 측면에서도 유리합니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

이것은 지금 현재 pr들이 머지되면 한 번에 반영하겠습니다

@sonarqubecloud
Copy link
Copy Markdown

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.

3 participants