From 760316e20ff6151e170c876fac0717fafc0cf8c8 Mon Sep 17 00:00:00 2001 From: minju Date: Tue, 18 Nov 2025 19:20:46 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat=20:=20=EC=93=B0=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20user=20=ED=95=84=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gotcha-domain/src/main/java/gotcha_domain/user/User.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index 08fb85d7..5708e461 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -58,8 +58,6 @@ public class User extends BaseTimeEntity { @Setter private LocalDateTime lastLogout; -// private Boolean isLocked; // UserStatus로 대체 - @Setter private int level; From d064e31566fd80e9a20d2abf5194827af00990e8 Mon Sep 17 00:00:00 2001 From: minju Date: Tue, 18 Nov 2025 19:26:17 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=97=85=EB=A1=9C=EB=93=9C.?= =?UTF-8?q?=20-=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20?= =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=EB=8B=89=EB=84=A4=EC=9E=84=EC=9D=84=20?= =?UTF-8?q?=EC=A3=BC=EC=A7=80=20=EC=95=8A=EC=9D=84=20=EC=8B=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=9C=A0=EC=A0=80=20=EB=B0=98=ED=99=98=20-=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20=ED=8A=B9?= =?UTF-8?q?=EC=A0=95=20=EB=8B=89=EB=84=A4=EC=9E=84=EC=9D=84=20=EC=A4=84=20?= =?UTF-8?q?=EC=8B=9C,=20=ED=95=B4=EB=8B=B9=20=EC=9C=A0=EC=A0=80=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20=EC=A0=95=EB=B3=B4=EB=A7=8C=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gotcha_user/service/UserService.java | 7 +++ .../repository/UserReportRepository.java | 2 + .../report/service/UserReportService.java | 4 ++ .../domain/sanction/api/SanctionApi.java | 11 ++++- .../controller/SanctionController.java | 22 ++++++--- .../sanction/dto/SanctionUserListRes.java | 12 +++++ .../exception/SanctionExceptionCode.java | 3 +- .../sanction/service/SanctionService.java | 45 ++++++++++++++++++- 8 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionUserListRes.java diff --git a/gotcha-user/src/main/java/gotcha_user/service/UserService.java b/gotcha-user/src/main/java/gotcha_user/service/UserService.java index 2c0c7d5e..8624134e 100644 --- a/gotcha-user/src/main/java/gotcha_user/service/UserService.java +++ b/gotcha-user/src/main/java/gotcha_user/service/UserService.java @@ -12,6 +12,8 @@ import gotcha_user.exceptionCode.UserExceptionCode; import gotcha_user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +32,11 @@ public class UserService { private static final long NICKNAME_VERIFY_EXPIRATION_TIME = 10 * 60; + @Transactional(readOnly = true) + public Page findAllUsers(Pageable pageable) { + return userRepository.findAll(pageable); + } + @Transactional(readOnly = true) public void checkNickname(String nickname) { if (userRepository.existsByNickname(nickname)) { diff --git a/gotcha/src/main/java/Gotcha/domain/report/repository/UserReportRepository.java b/gotcha/src/main/java/Gotcha/domain/report/repository/UserReportRepository.java index 52c88630..036354ad 100644 --- a/gotcha/src/main/java/Gotcha/domain/report/repository/UserReportRepository.java +++ b/gotcha/src/main/java/Gotcha/domain/report/repository/UserReportRepository.java @@ -15,4 +15,6 @@ public interface UserReportRepository extends JpaRepository { WHERE (:keyword IS NULL OR :keyword = '' OR LOWER(u.nickname) LIKE LOWER(CONCAT(:keyword, '%'))) """) Page findAllByNickname(@Param("keyword") String keyword, Pageable pageable); + + int countByUser_Id(Long userId); } diff --git a/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java b/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java index 9c689c66..79d41b99 100644 --- a/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java +++ b/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java @@ -40,6 +40,10 @@ public void reportUser(UserReportReq reportReq, SecurityUserDetails userDetails) userReportRepository.save(userReport); } + public int countReportByUserId(Long userId) { + return userReportRepository.countByUser_Id(userId); + } + public UserReport findUserReportById(Long reportId){ return userReportRepository.findById(reportId) .orElseThrow(() -> new CustomException(ReportExceptionCode.REPORT_NOT_FOUND)); diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java index 962f1fdf..9b875534 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -11,9 +11,10 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; - +import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "[관리자 제재 API]", description = "관리자용 사용자 제재 관련 API") @@ -95,4 +96,12 @@ public interface SanctionApi { ResponseEntity applySanction( @Valid @RequestBody SanctionReq sanctionReq, SecurityUserDetails userDetails); + + + ResponseEntity getUserList( + SecurityUserDetails userDetails, + @RequestParam(value = "nickname", required = false) String nickname, + @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page + ); + } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java b/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java index 3c2ef519..319c7528 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java @@ -5,32 +5,34 @@ import Gotcha.domain.sanction.dto.SanctionRes; import Gotcha.domain.sanction.service.SanctionService; import gotcha_domain.auth.SecurityUserDetails; +import gotcha_user.service.UserService; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; @RestController -@RequestMapping("/api/v1/admin/sanctions") +@RequestMapping("/api/v1/admin") @RequiredArgsConstructor public class SanctionController implements SanctionApi { - private final SanctionService sanctionService; - - @PostMapping + @PostMapping("/sanctions") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity applySanction( @Valid @RequestBody SanctionReq sanctionReq, - @AuthenticationPrincipal SecurityUserDetails userDetails) { + @AuthenticationPrincipal SecurityUserDetails userDetails) { String adminId = userDetails.getUuid(); @@ -38,4 +40,14 @@ public ResponseEntity applySanction( return ResponseEntity.status(HttpStatus.CREATED).body(response); } + + @GetMapping("/user/list") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getUserList( + @AuthenticationPrincipal SecurityUserDetails userDetails, + @RequestParam(value = "nickname", required = false) String nickname, + @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page + ) { + return ResponseEntity.ok(sanctionService.getSanctionUsers(nickname, page)); + } } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionUserListRes.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionUserListRes.java new file mode 100644 index 00000000..46bf15ac --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionUserListRes.java @@ -0,0 +1,12 @@ +package Gotcha.domain.sanction.dto; + +import java.time.LocalDate; + +public record SanctionUserListRes( + String nickname, + LocalDate createDate, + String email, + int reportedCount, + int warningCount +) { +} diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java b/gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java index d1764dac..3eb91f3f 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java @@ -6,7 +6,8 @@ @AllArgsConstructor public enum SanctionExceptionCode implements ExceptionCode { - NOT_SUSPENDED_USER(HttpStatus.BAD_REQUEST, "SANCTION-400-001", "해당 유저는 정지 상태가 아닙니다."); + NOT_SUSPENDED_USER(HttpStatus.BAD_REQUEST, "SANCTION-400-001", "해당 유저는 정지 상태가 아닙니다."), + INVALID_PAGE_FOR_SEARCH(HttpStatus.BAD_REQUEST, "SANCTION-400-002", "검색 시 page 값은 0이어야 합니다."); private final HttpStatus status; private final String code; diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 2903c630..a4b15fdc 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -1,8 +1,11 @@ package Gotcha.domain.sanction.service; +import static Gotcha.domain.sanction.exception.SanctionExceptionCode.INVALID_PAGE_FOR_SEARCH; + import Gotcha.domain.report.service.UserReportService; import Gotcha.domain.sanction.dto.SanctionReq; import Gotcha.domain.sanction.dto.SanctionRes; +import Gotcha.domain.sanction.dto.SanctionUserListRes; import Gotcha.domain.sanction.exception.SanctionExceptionCode; import Gotcha.domain.sanction.repository.SanctionRepository; import gotcha_common.exception.CustomException; @@ -12,7 +15,11 @@ import gotcha_domain.user.User; import gotcha_domain.user.UserStatus; import gotcha_user.service.UserService; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,10 +28,46 @@ @Service @RequiredArgsConstructor public class SanctionService { - private final SanctionRepository sanctionRepository; private final UserService userService; private final UserReportService userReportService; + private final Integer USERS_PER_PAGE = 6; + + @Transactional(readOnly = true) + public Page getSanctionUsers(String nickname, int page) { + boolean isSearchModeByNickname = nickname != null && !nickname.isBlank(); + + Page users = isSearchModeByNickname + ? searchUser(nickname, page) + : findAllUsers(page); + + return users.map(this::toResponse); + } + + private Page searchUser(String nickname, int page) { + if (page != 0) { + throw new CustomException(INVALID_PAGE_FOR_SEARCH); + } + + User user = userService.findUserByNickname(nickname); + return new PageImpl<>(List.of(user), PageRequest.of(0, USERS_PER_PAGE), 1); + } + + private Page findAllUsers(int page) { + return userService.findAllUsers( PageRequest.of(page, USERS_PER_PAGE)); + } + + + private SanctionUserListRes toResponse(User user) { + return new SanctionUserListRes( + user.getNickname(), + user.getCreatedAt().toLocalDate(), + user.getEmail(), + userReportService.countReportByUserId(user.getId()), + user.getWarningCount() + ); + } + @Transactional public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { From da1053b59400e4886eebc434c948847117858689 Mon Sep 17 00:00:00 2001 From: minju Date: Tue, 18 Nov 2025 19:38:18 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs=20:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/sanction/api/SanctionApi.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java index 9b875534..331c577f 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -97,6 +97,82 @@ ResponseEntity applySanction( @Valid @RequestBody SanctionReq sanctionReq, SecurityUserDetails userDetails); + @Operation( + summary = "사용자 목록 반환 API", + description = """ + 관리자 전용 사용자 조회 API입니다. + + **조회 규칙** + - nickname 파라미터가 없는 경우: 전체 사용자 목록을 페이지 단위로 조회합니다. + - nickname 파라미터가 있는 경우: 해당 닉네임과 정확히 일치하는 유저 1명만 반환합니다. + - 이때 page 파라미터는 반드시 0이어야 합니다. + """ + ) + @SecurityRequirement(name = "bearerAuth") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "사용자 목록 조회 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "content": [ + { + "nickname": "다06fn6", + "createDate": "2025-09-03", + "email": "test30@naver.com", + "reportedCount": 1, + "warningCount": 1 + } + ], + "page": { + "size": 6, + "number": 0, + "totalElements": 1, + "totalPages": 1 + } + } + """ + ) + ) + ), + + @ApiResponse( + responseCode = "400", + description = "닉네임 검색 시 page 값이 0이 아닌 경우", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "code": "SANCTION-400-002", + "status": "BAD_REQUEST", + "message": "검색 시 page 값은 0이어야 합니다." + } + """ + ) + ) + ), + + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 사용자 조회 시", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "code": "USER-404-001", + "status": "NOT_FOUND", + "message": "존재하지 않는 사용자입니다." + } + """ + ) + ) + ) + }) ResponseEntity getUserList( SecurityUserDetails userDetails,