Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ src/main/generated/
/.cursor/

### AntiGravity ###
/.agent/
/.agent/

### Claude ###
/.claude/
CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.dreamteam.alter.adapter.inbound.admin.user.controller;

import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUpdateUserPasswordRequestDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUpdateUserStatusRequestDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUserDetailResponseDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUserListFilterDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUserListResponseDto;
import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
import com.dreamteam.alter.adapter.inbound.common.dto.PageRequestDto;
import com.dreamteam.alter.adapter.inbound.common.dto.PaginatedResponseDto;
import com.dreamteam.alter.application.aop.AdminActionContext;
import com.dreamteam.alter.domain.user.context.AdminActor;
import com.dreamteam.alter.domain.user.port.inbound.AdminGetUserDetailUseCase;
import com.dreamteam.alter.domain.user.port.inbound.AdminGetUserListUseCase;
import com.dreamteam.alter.domain.user.port.inbound.AdminUpdateUserPasswordUseCase;
import com.dreamteam.alter.domain.user.port.inbound.AdminUpdateUserStatusUseCase;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/admin/users")
@PreAuthorize("hasAnyRole('ADMIN')")
@RequiredArgsConstructor
@Validated
public class AdminUserController implements AdminUserControllerSpec {

@Resource(name = "adminGetUserList")
private final AdminGetUserListUseCase adminGetUserList;

@Resource(name = "adminGetUserDetail")
private final AdminGetUserDetailUseCase adminGetUserDetail;

@Resource(name = "adminUpdateUserPassword")
private final AdminUpdateUserPasswordUseCase adminUpdateUserPassword;

@Resource(name = "adminUpdateUserStatus")
private final AdminUpdateUserStatusUseCase adminUpdateUserStatus;

@Override
@GetMapping
public ResponseEntity<PaginatedResponseDto<AdminUserListResponseDto>> getUserList(
PageRequestDto request,
AdminUserListFilterDto filter
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

return ResponseEntity.ok(adminGetUserList.execute(request, filter, actor));
}

@Override
@GetMapping("/{userId}")
public ResponseEntity<CommonApiResponse<AdminUserDetailResponseDto>> getUserDetail(
@PathVariable Long userId
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

return ResponseEntity.ok(CommonApiResponse.of(adminGetUserDetail.execute(userId, actor)));
}

@Override
@PutMapping("/{userId}/password")
public ResponseEntity<CommonApiResponse<Void>> updateUserPassword(
@PathVariable Long userId,
@Valid @RequestBody AdminUpdateUserPasswordRequestDto request
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

adminUpdateUserPassword.execute(userId, request, actor);
return ResponseEntity.ok(CommonApiResponse.empty());
}

@Override
@PutMapping("/{userId}/status")
public ResponseEntity<CommonApiResponse<Void>> updateUserStatus(
@PathVariable Long userId,
@Valid @RequestBody AdminUpdateUserStatusRequestDto request
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

adminUpdateUserStatus.execute(userId, request, actor);
return ResponseEntity.ok(CommonApiResponse.empty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.dreamteam.alter.adapter.inbound.admin.user.controller;

import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUpdateUserPasswordRequestDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUpdateUserStatusRequestDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUserDetailResponseDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUserListFilterDto;
import com.dreamteam.alter.adapter.inbound.admin.user.dto.AdminUserListResponseDto;
import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
import com.dreamteam.alter.adapter.inbound.common.dto.ErrorResponse;
import com.dreamteam.alter.adapter.inbound.common.dto.PageRequestDto;
import com.dreamteam.alter.adapter.inbound.common.dto.PaginatedResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "ADMIN - 관리자 회원 관리 API")
public interface AdminUserControllerSpec {

@Operation(summary = "회원 목록 조회", description = "관리자가 회원 목록을 오프셋 페이징으로 조회합니다. 상태, 역할, 키워드로 필터링할 수 있습니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "회원 목록 조회 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "잘못된 요청 (유효하지 않은 페이지, 페이지 크기 등)",
value = "{\"code\" : \"B001\"}"
)
}))
})
ResponseEntity<PaginatedResponseDto<AdminUserListResponseDto>> getUserList(
PageRequestDto request,
AdminUserListFilterDto filter
);

@Operation(summary = "회원 상세 조회", description = "관리자가 회원 상세 정보를 조회합니다. 평판 정보가 포함됩니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "회원 상세 조회 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "존재하지 않는 회원",
value = "{\"code\" : \"B011\"}"
)
}))
})
ResponseEntity<CommonApiResponse<AdminUserDetailResponseDto>> getUserDetail(
@Parameter(description = "회원 ID", example = "1") @PathVariable Long userId
);

@Operation(summary = "회원 비밀번호 변경", description = "관리자가 회원 비밀번호를 변경합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "회원 비밀번호 변경 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "존재하지 않는 회원",
value = "{\"code\" : \"B011\"}"
),
@ExampleObject(
name = "잘못된 비밀번호 형식",
value = "{\"code\" : \"B002\"}"
),
@ExampleObject(
name = "잘못된 요청 (유효성 검증 실패)",
value = "{\"code\" : \"B001\"}"
)
}))
})
ResponseEntity<CommonApiResponse<Void>> updateUserPassword(
@Parameter(description = "회원 ID", example = "1") @PathVariable Long userId,
@Valid @RequestBody AdminUpdateUserPasswordRequestDto request
);

@Operation(summary = "회원 상태 변경", description = "관리자가 회원 상태를 변경합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "회원 상태 변경 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "존재하지 않는 회원",
value = "{\"code\" : \"B011\"}"
),
@ExampleObject(
name = "잘못된 요청 (유효하지 않은 상태 값 등)",
value = "{\"code\" : \"B001\"}"
)
}))
})
ResponseEntity<CommonApiResponse<Void>> updateUserStatus(
@Parameter(description = "회원 ID", example = "1") @PathVariable Long userId,
@Valid @RequestBody AdminUpdateUserStatusRequestDto request
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.dreamteam.alter.adapter.inbound.admin.user.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "회원 비밀번호 갱신 요청 DTO")
public class AdminUpdateUserPasswordRequestDto {

@NotBlank
@Schema(description = "새 비밀번호 (영문, 숫자, 특수문자 포함 8-20자)")
private String newPassword;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dreamteam.alter.adapter.inbound.admin.user.dto;

import com.dreamteam.alter.domain.user.type.UserStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "회원 상태 변경 요청 DTO")
public class AdminUpdateUserStatusRequestDto {

@NotNull
@Schema(description = "변경할 상태")
private UserStatus status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.dreamteam.alter.adapter.inbound.admin.user.dto;

import com.dreamteam.alter.adapter.inbound.common.dto.DescribedEnumDto;
import com.dreamteam.alter.adapter.outbound.user.persistence.readonly.AdminUserDetailResponse;
import com.dreamteam.alter.domain.user.type.UserGender;
import com.dreamteam.alter.domain.user.type.UserRole;
import com.dreamteam.alter.domain.user.type.UserStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Schema(description = "회원 상세 응답 DTO")
public class AdminUserDetailResponseDto {

@Schema(description = "회원 ID", example = "1")
private Long id;

@Schema(description = "이메일", example = "user@example.com")
private String email;

@Schema(description = "이름", example = "홍길동")
private String name;

@Schema(description = "닉네임", example = "알터유저")
private String nickname;

@Schema(description = "연락처", example = "010-1234-5678")
private String contact;

@Schema(description = "생년월일", example = "19900101")
private String birthday;

@Schema(description = "성별")
private DescribedEnumDto<UserGender> gender;

@Schema(description = "역할")
private DescribedEnumDto<UserRole> role;

@Schema(description = "상태")
private DescribedEnumDto<UserStatus> status;

@Schema(description = "생성일시", example = "2025-01-01T12:00:00")
private LocalDateTime createdAt;

@Schema(description = "수정일시", example = "2025-01-01T12:00:00")
private LocalDateTime updatedAt;

@Schema(description = "평판 요약")
private AdminUserReputationSummaryDto reputationSummary;

public static AdminUserDetailResponseDto from(AdminUserDetailResponse response) {
return AdminUserDetailResponseDto.builder()
.id(response.getId())
.email(response.getEmail())
.name(response.getName())
.nickname(response.getNickname())
.contact(response.getContact())
.birthday(response.getBirthday())
.gender(DescribedEnumDto.of(response.getGender(), UserGender.describe()))
.role(DescribedEnumDto.of(response.getRole(), UserRole.describe()))
.status(DescribedEnumDto.of(response.getStatus(), UserStatus.describe()))
.createdAt(response.getCreatedAt())
.updatedAt(response.getUpdatedAt())
.reputationSummary(AdminUserReputationSummaryDto.from(response.getReputationSummary()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.dreamteam.alter.adapter.inbound.admin.user.dto;

import com.dreamteam.alter.domain.user.type.UserRole;
import com.dreamteam.alter.domain.user.type.UserStatus;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springdoc.core.annotations.ParameterObject;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ParameterObject
@Schema(description = "회원 목록 필터 DTO")
public class AdminUserListFilterDto {

@Parameter(description = "회원 상태")
private UserStatus status;

@Parameter(description = "회원 역할")
private UserRole role;

@Parameter(description = "이메일 검색어")
private String email;

@Parameter(description = "이름 검색어")
private String name;

@Parameter(description = "닉네임 검색어")
private String nickname;

@Parameter(description = "연락처 검색어")
private String contact;
}
Loading