Skip to content

feat/98 :: 사용자 언밴 기능 추가#10

Open
lian2945 wants to merge 13 commits intorefactor/35from
feat/98
Open

feat/98 :: 사용자 언밴 기능 추가#10
lian2945 wants to merge 13 commits intorefactor/35from
feat/98

Conversation

@lian2945
Copy link
Copy Markdown
Collaborator

@lian2945 lian2945 commented Apr 5, 2026

📌 관련 이슈

  • close #98

📝 변경 사항 요약

작업 유형

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

변경 내용

  • User 도메인 상태를 INACTIVEBANNED 중심으로 정리하고 언밴 도메인 동작 추가
  • 관리자 API에 언밴 엔드포인트 추가: PATCH /api/user/{nickname}/unban
  • UnbanUserUseCase, UnbanUserService, UserUnbannedEvent, UserNotBannedException 추가
  • UserDomainEventTypeUSER_UNBANNED 이벤트 타입 추가
  • PR 자동화 프롬프트 규칙에 브랜치 네이밍(feat/98, refactor/98) 및 Mermaid 필수 규칙 강화

변경 이유

  • 밴/언밴 상태 전이를 도메인에서 명시적으로 관리해 정책 일관성을 높이기 위해
  • 언밴 이벤트를 통해 후속 연동(이벤트 소비자/세션 처리) 확장 기반을 마련하기 위해
  • PR 자동화 시 브랜치 규칙과 Mermaid 누락 방지 규칙을 명확히 고정하기 위해

✅ 테스트 체크리스트

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

📸 스크린샷 / 로그

펼쳐보기
  • 커밋 로그
    • ac427b1 feat/98 :: 사용자 밴 상태 모델링 및 언밴 가드 추가
    • 0ed3801 feat/98 :: 관리자 사용자 밴·언밴 유스케이스와 API 추가
    • 9da5c8c feat/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 - 후속 연동]
Loading

💬 리뷰어에게

  • User aggregate의 상태 전이(deactivate, unban)와 예외 처리(UserNotBannedException)가 의도대로 동작하는지 확인 부탁드립니다.
  • UnbanUserService에서 이벤트 저장(UserUnbannedEvent)까지 트랜잭션 경계가 맞는지 중점 리뷰 부탁드립니다.
  • 이번 PR에는 PR 자동화 프롬프트 파일 업데이트도 포함되어 있습니다.

lian2945 and others added 3 commits April 5, 2026 23:10
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>
Copilot AI review requested due to automatic review settings April 5, 2026 14:22
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

User 서비스 도메인에 사용자 밴/언밴 상태 전이와 언밴 이벤트를 추가하고, 관리자용 밴/언밴 API를 제공해 상태 변경을 명시적으로 관리할 수 있게 하는 PR입니다.

Changes:

  • UserStatusINACTIVE 중심에서 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,
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

UserStatus에서 INACTIVE를 제거하고 BANNED로 교체하면, DB(users.status)에 기존 값이 INACTIVE로 저장돼 있는 레코드를 JPA가 로딩할 때 IllegalArgumentException: No enum constant ...로 실패합니다(현재 @Enumerated(EnumType.STRING)). 배포/롤백을 안전하게 하려면 (1) DB 마이그레이션으로 INACTIVE -> BANNED 변환을 선행하거나, (2) 과도기에는 INACTIVE를 enum에 유지하고 도메인에서만 BANNED로 매핑/정규화하는 방식 등 호환 전략이 필요합니다.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +120
this.status = UserStatus.INACTIVE;
this.status = UserStatus.BANNED;
this.isActive = false;
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

메서드명이 accountDeactivate()인데 실제로는 status = BANNED(정지) 상태 전이를 수행하고 있습니다. 현재 호출부가 밴 유스케이스뿐이라도, 이름과 동작이 불일치해서 추후 다른 개발자가 '비활성화/휴면' 의미로 오용하기 쉽습니다. ban()/banAccount() 등으로 명확히 분리하거나, 정말 'deactivate' 개념이 필요하면 INACTIVE 상태를 별도로 두고 상태 전이를 구분하는 방향을 권장합니다.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +31
User user = userRepositoryPort.getUserByNickname(nickname)
.orElseThrow(UserNotFoundException::new);

user.unban();
userRepositoryPort.updateUser(user);

Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

현재 userRepositoryPort.updateUser(user)가 상태 변경을 실제 DB에 반영하지 못할 가능성이 큽니다. UserJpaAdapter.updateUser()UserMapper.updateEntity()UserEntity.updateFromDomain() 경로를 타는데, 그 구현이 status 필드를 업데이트하지 않습니다. 이 상태로는 user.unban()으로 바뀐 status가 저장되지 않아 언밴(및 밴) API가 재조회 시 원복된 것처럼 보일 수 있습니다. updateFromDomain()(또는 별도 업데이트 메서드)에서 status까지 반영하도록 수정이 필요합니다.

Copilot uses AI. Check for mistakes.
Comment on lines 21 to +31
@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();
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

/api/user/{nickname}/ban|unban는 매우 민감한 관리자 기능인데, 현재 보안 설정(SecurityConfiguration에서 anyRequest().permitAll()) 기준으로는 인증/인가 없이 외부에서 호출 가능해집니다. 최소한 (1) 해당 엔드포인트를 internal 네트워크에서만 접근 가능하도록 IP 제한/게이트웨이 보호를 적용하거나, (2) Spring Security에서 경로별로 hasRole('ADMIN') 같은 인가를 걸고 컨트롤러에는 @PreAuthorize 등을 추가하는 식의 차단이 필요합니다.

Copilot uses AI. Check for mistakes.
@lian2945 lian2945 changed the title feat/98 :: 사용자 밴/언밴 기능 추가 feat/98 :: 사용자 언밴 기능 추가 Apr 5, 2026
lian2945 and others added 4 commits April 6, 2026 00:27
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 7, 2026 20:59
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.

리뷰 요약

전반적으로 헥사고날 아키텍처 패턴을 잘 따르고 있으며, BanUserService와 대칭적인 UnbanUserService 구현이 일관성 있게 작성되었습니다. 도메인 이벤트, UseCase 포트, 서비스 구현 모두 기존 컨벤션과 잘 맞습니다.

다만 몇 가지 도메인 일관성 이슈가 있습니다:

  1. unban()isActive 상태 복원 누락 — ban()에서 false로 변경하지만 unban()에서 복원하지 않아 비정합 상태 발생 가능
  2. UserNotBannedExceptionBusinessException이 아닌 BaseException을 상속 — 프로젝트 내 다른 도메인 예외(UserNotFoundException, InvalidFieldException)와 계층 구조 불일치
  3. INACTIVEBANNED enum 변경이 기존 DB 데이터와 호환되는지 확인 필요


public void unban() {
if (this.status != UserStatus.BANNED) throw new UserNotBannedException();
this.status = UserStatus.ACTIVE;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ban()에서 this.isActive = false로 설정하지만, unban()에서 isActivetrue로 복원하지 않습니다.

언밴 후 사용자의 상태가 ACTIVE이지만 isActivefalse인 비정합 상태가 됩니다. UserEntity.updateFromDomain()에서 isActive를 그대로 저장하므로 DB에도 잘못된 상태가 영속됩니다.

public void unban() {
    if (this.status != UserStatus.BANNED) throw new UserNotBannedException();
    this.status = UserStatus.ACTIVE;
    this.isActive = true;
}

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.

넵 수정하겠습니다!

import kr.magicbox.user.global.exception.BaseException;
import org.springframework.http.HttpStatus;

public class UserNotBannedException extends BaseException {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

UserNotBannedExceptionBaseException을 직접 상속하고 있습니다. 프로젝트 내 다른 도메인 예외(UserNotFoundException, InvalidFieldException)는 모두 BusinessException을 상속하여 4xx 상태코드 검증을 거칩니다.

상속 깊이 경고로 변경한 것으로 보이지만, BusinessException의 4xx 검증 로직을 우회하게 되어 예외 계층 일관성이 깨집니다. SonarCloud 경고는 suppress하거나, 3단계 상속(RuntimeException→BaseException→BusinessException→X)이 이 프로젝트에서는 합리적인 깊이라고 판단하여 허용하는 것이 나을 수 있습니다.

public class UserNotBannedException extends BusinessException {
    public UserNotBannedException() {
        super("정지 상태가 아닌 사용자는 정지 해제할 수 없습니다.", HttpStatus.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.

넵 BusinessException으로 변경하겠습니다!


public void accountDeactivate() {
this.status = UserStatus.INACTIVE;
public void ban() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 상태에서의 밴 방지는 고려해 볼 만합니다.

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.

사용자 계정 상태 정책에 의해서 BANNED정도만 검증하는 것이 적절하다 생각하여, 그렇게 하였습니다!

@sonarqubecloud
Copy link
Copy Markdown

@lian2945 lian2945 requested a review from f-lab-ted April 14, 2026 00:10
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