Conversation
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>
There was a problem hiding this comment.
Pull request overview
User 서비스 도메인에 사용자 밴/언밴 상태 전이와 언밴 이벤트를 추가하고, 관리자용 밴/언밴 API를 제공해 상태 변경을 명시적으로 관리할 수 있게 하는 PR입니다.
Changes:
UserStatus를INACTIVE중심에서BANNED중심으로 재정리하고unban()도메인 동작 추가- 언밴 유스케이스/서비스 및
UserUnbannedEvent,USER_UNBANNED이벤트 타입 추가 - 관리자 API에
PATCH /api/user/{nickname}/unban엔드포인트 추가 (+ PR 자동화 프롬프트 문서 추가)
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| services/user/src/main/java/kr/magicbox/user/domain/exception/UserNotBannedException.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 | 상태 모델을 BANNED 중심으로 변경 |
| services/user/src/main/java/kr/magicbox/user/domain/aggregate/User.java | 밴(기존 deactivate 의미 변경) 및 언밴 상태 전이 로직 추가 |
| services/user/src/main/java/kr/magicbox/user/application/service/UnbanUserService.java | 언밴 유스케이스 구현 및 이벤트 저장 추가 |
| services/user/src/main/java/kr/magicbox/user/application/service/BanUserService.java | 포맷팅 변경(기능 로직은 동일) |
| services/user/src/main/java/kr/magicbox/user/application/port/in/UnbanUserUseCase.java | 언밴 유스케이스 포트 추가 |
| services/user/src/main/java/kr/magicbox/user/application/port/in/BanUserUseCase.java | 포맷팅 변경 |
| services/user/src/main/java/kr/magicbox/user/adapter/in/web/AdminUserCommandController.java | 관리자 밴/언밴 PATCH 엔드포인트 제공 |
| .github/prompts/pr-automation.prompt.md | PR 자동화 프롬프트(브랜치/머메이드 규칙 등) 문서 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -2,6 +2,6 @@ | |||
|
|
|||
| public enum UserStatus { | |||
| ACTIVE, | |||
There was a problem hiding this comment.
UserStatus에서 INACTIVE를 제거하고 BANNED로 교체하면, DB(users.status)에 기존 값이 INACTIVE로 저장돼 있는 레코드를 JPA가 로딩할 때 IllegalArgumentException: No enum constant ...로 실패합니다(현재 @Enumerated(EnumType.STRING)). 배포/롤백을 안전하게 하려면 (1) DB 마이그레이션으로 INACTIVE -> BANNED 변환을 선행하거나, (2) 과도기에는 INACTIVE를 enum에 유지하고 도메인에서만 BANNED로 매핑/정규화하는 방식 등 호환 전략이 필요합니다.
| ACTIVE, | |
| ACTIVE, | |
| /** | |
| * Legacy value kept for backward compatibility with persisted EnumType.STRING data. | |
| * Existing DB rows may still contain "INACTIVE" during rollout/rollback windows. | |
| */ | |
| @Deprecated | |
| INACTIVE, |
| this.status = UserStatus.INACTIVE; | ||
| this.status = UserStatus.BANNED; | ||
| this.isActive = false; | ||
| } |
There was a problem hiding this comment.
메서드명이 accountDeactivate()인데 실제로는 status = BANNED(정지) 상태 전이를 수행하고 있습니다. 현재 호출부가 밴 유스케이스뿐이라도, 이름과 동작이 불일치해서 추후 다른 개발자가 '비활성화/휴면' 의미로 오용하기 쉽습니다. ban()/banAccount() 등으로 명확히 분리하거나, 정말 'deactivate' 개념이 필요하면 INACTIVE 상태를 별도로 두고 상태 전이를 구분하는 방향을 권장합니다.
| User user = userRepositoryPort.getUserByNickname(nickname) | ||
| .orElseThrow(UserNotFoundException::new); | ||
|
|
||
| user.unban(); | ||
| userRepositoryPort.updateUser(user); | ||
|
|
There was a problem hiding this comment.
현재 userRepositoryPort.updateUser(user)가 상태 변경을 실제 DB에 반영하지 못할 가능성이 큽니다. UserJpaAdapter.updateUser()는 UserMapper.updateEntity() → UserEntity.updateFromDomain() 경로를 타는데, 그 구현이 status 필드를 업데이트하지 않습니다. 이 상태로는 user.unban()으로 바뀐 status가 저장되지 않아 언밴(및 밴) API가 재조회 시 원복된 것처럼 보일 수 있습니다. updateFromDomain()(또는 별도 업데이트 메서드)에서 status까지 반영하도록 수정이 필요합니다.
| @PatchMapping("/{nickname}/ban") | ||
| public ResponseEntity<Void> banUser(@PathVariable String nickname) { | ||
| banUserUseCase.banUser(Nickname.of(nickname)); | ||
| return ResponseEntity.noContent().build(); | ||
| } | ||
| } No newline at end of file | ||
|
|
||
| @PatchMapping("/{nickname}/unban") | ||
| public ResponseEntity<Void> unbanUser(@PathVariable String nickname) { | ||
| unbanUserUseCase.unbanUser(Nickname.of(nickname)); | ||
| return ResponseEntity.noContent().build(); | ||
| } |
There was a problem hiding this comment.
/api/user/{nickname}/ban|unban는 매우 민감한 관리자 기능인데, 현재 보안 설정(SecurityConfiguration에서 anyRequest().permitAll()) 기준으로는 인증/인가 없이 외부에서 호출 가능해집니다. 최소한 (1) 해당 엔드포인트를 internal 네트워크에서만 접근 가능하도록 IP 제한/게이트웨이 보호를 적용하거나, (2) Spring Security에서 경로별로 hasRole('ADMIN') 같은 인가를 걸고 컨트롤러에는 @PreAuthorize 등을 추가하는 식의 차단이 필요합니다.
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>
f-lab-ted
left a comment
There was a problem hiding this comment.
리뷰 요약
전반적으로 헥사고날 아키텍처 패턴을 잘 따르고 있으며, BanUserService와 대칭적인 UnbanUserService 구현이 일관성 있게 작성되었습니다. 도메인 이벤트, UseCase 포트, 서비스 구현 모두 기존 컨벤션과 잘 맞습니다.
다만 몇 가지 도메인 일관성 이슈가 있습니다:
unban()시isActive상태 복원 누락 —ban()에서false로 변경하지만unban()에서 복원하지 않아 비정합 상태 발생 가능UserNotBannedException이BusinessException이 아닌BaseException을 상속 — 프로젝트 내 다른 도메인 예외(UserNotFoundException,InvalidFieldException)와 계층 구조 불일치INACTIVE→BANNEDenum 변경이 기존 DB 데이터와 호환되는지 확인 필요
|
|
||
| public void unban() { | ||
| if (this.status != UserStatus.BANNED) throw new UserNotBannedException(); | ||
| this.status = UserStatus.ACTIVE; |
There was a problem hiding this comment.
ban()에서 this.isActive = false로 설정하지만, unban()에서 isActive를 true로 복원하지 않습니다.
언밴 후 사용자의 상태가 ACTIVE이지만 isActive는 false인 비정합 상태가 됩니다. UserEntity.updateFromDomain()에서 isActive를 그대로 저장하므로 DB에도 잘못된 상태가 영속됩니다.
public void unban() {
if (this.status != UserStatus.BANNED) throw new UserNotBannedException();
this.status = UserStatus.ACTIVE;
this.isActive = true;
}| import kr.magicbox.user.global.exception.BaseException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public class UserNotBannedException extends BaseException { |
There was a problem hiding this comment.
UserNotBannedException이 BaseException을 직접 상속하고 있습니다. 프로젝트 내 다른 도메인 예외(UserNotFoundException, InvalidFieldException)는 모두 BusinessException을 상속하여 4xx 상태코드 검증을 거칩니다.
상속 깊이 경고로 변경한 것으로 보이지만, BusinessException의 4xx 검증 로직을 우회하게 되어 예외 계층 일관성이 깨집니다. SonarCloud 경고는 suppress하거나, 3단계 상속(RuntimeException→BaseException→BusinessException→X)이 이 프로젝트에서는 합리적인 깊이라고 판단하여 허용하는 것이 나을 수 있습니다.
public class UserNotBannedException extends BusinessException {
public UserNotBannedException() {
super("정지 상태가 아닌 사용자는 정지 해제할 수 없습니다.", HttpStatus.BAD_REQUEST);
}
}There was a problem hiding this comment.
넵 BusinessException으로 변경하겠습니다!
|
|
||
| public void accountDeactivate() { | ||
| this.status = UserStatus.INACTIVE; | ||
| public void ban() { |
There was a problem hiding this comment.
ban() 메서드에 가드 조건이 없어 이미 BANNED이거나 DELETED 상태인 사용자도 다시 밴할 수 있습니다. unban()에는 사전 조건 검증이 있으므로, ban()에도 대칭적으로 상태 검증을 추가하면 도메인 무결성이 강화됩니다.
public void ban() {
if (this.status == UserStatus.BANNED) throw new AlreadyBannedException();
if (this.status == UserStatus.DELETED) throw new UserDeletedException();
this.status = UserStatus.BANNED;
this.isActive = false;
}최소한 DELETED 상태에서의 밴 방지는 고려해 볼 만합니다.
There was a problem hiding this comment.
사용자 계정 상태 정책에 의해서 BANNED정도만 검증하는 것이 적절하다 생각하여, 그렇게 하였습니다!
|



📌 관련 이슈
📝 변경 사항 요약
작업 유형
(기능 단위 완료 후 PR)(버그 1개 = PR 1개)(작업 단위 완료 후 PR)(작업 단위 완료 후 PR)변경 내용
INACTIVE→BANNED중심으로 정리하고 언밴 도메인 동작 추가PATCH /api/user/{nickname}/unbanUnbanUserUseCase,UnbanUserService,UserUnbannedEvent,UserNotBannedException추가UserDomainEventType에USER_UNBANNED이벤트 타입 추가feat/98,refactor/98) 및 Mermaid 필수 규칙 강화변경 이유
✅ 테스트 체크리스트
📸 스크린샷 / 로그
펼쳐보기
ac427b1feat/98 :: 사용자 밴 상태 모델링 및 언밴 가드 추가0ed3801feat/98 :: 관리자 사용자 밴·언밴 유스케이스와 API 추가9da5c8cfeat/98 :: 자동화 프롬프트 브랜치 네이밍 규칙을 숫자 티켓으로 고정🔄 동작 플로우 (Mermaid)
flowchart TD A[Admin PATCH /api/user/:nickname/ban or /api/user/:nickname/unban] --> B[AdminUserCommandController] B --> C[BanUserUseCase / UnbanUserUseCase] C --> D[BanUserService / UnbanUserService] D --> E[UserRepositoryPort.getUserByNickname] E --> F[User Aggregate 상태 전이] F -->|ban| G[status = BANNED] F -->|unban| H[status = ACTIVE<br/>if not banned then UserNotBannedException] G --> I[UserRepositoryPort.updateUser] H --> I I --> J[UserDomainEventRepositoryPort.save] J --> K[UserBannedEvent / UserUnbannedEvent] K --> L[Outbox to Kafka Consumer - 후속 연동]💬 리뷰어에게
Useraggregate의 상태 전이(deactivate,unban)와 예외 처리(UserNotBannedException)가 의도대로 동작하는지 확인 부탁드립니다.UnbanUserService에서 이벤트 저장(UserUnbannedEvent)까지 트랜잭션 경계가 맞는지 중점 리뷰 부탁드립니다.