diff --git a/src/main/java/com/sb/web/admin/controller/MemberAdminController.java b/src/main/java/com/sb/web/admin/controller/MemberAdminController.java new file mode 100644 index 0000000..38eec53 --- /dev/null +++ b/src/main/java/com/sb/web/admin/controller/MemberAdminController.java @@ -0,0 +1,54 @@ +package com.sb.web.admin.controller; + +import com.sb.web.admin.dto.RoleUpdateRequest; +import com.sb.web.admin.dto.StatusUpdateRequest; +import com.sb.web.admin.service.MemberAdminService; +import com.sb.web.auth.dto.MemberAdminResponse; +import com.sb.web.auth.dto.SessionUser; +import com.sb.web.auth.web.AuthInterceptor; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 관리자용 회원 관리 API. (/api/admin/members — AdminInterceptor 로 관리자만 접근) + */ +@RestController +@RequestMapping("/api/admin/members") +@RequiredArgsConstructor +public class MemberAdminController { + + private final MemberAdminService memberAdminService; + + @GetMapping + public List list() { + return memberAdminService.list(); + } + + @PutMapping("/{id}/role") + public MemberAdminResponse updateRole( + @PathVariable Long id, + @Valid @RequestBody RoleUpdateRequest req, + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + return memberAdminService.updateRole(id, req.getRole(), current.getId()); + } + + @PutMapping("/{id}/status") + public MemberAdminResponse updateStatus( + @PathVariable Long id, + @Valid @RequestBody StatusUpdateRequest req, + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + return memberAdminService.updateStatus(id, req.getStatus(), current.getId()); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete( + @PathVariable Long id, + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + memberAdminService.delete(id, current.getId()); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/sb/web/admin/dto/RoleUpdateRequest.java b/src/main/java/com/sb/web/admin/dto/RoleUpdateRequest.java new file mode 100644 index 0000000..f702aad --- /dev/null +++ b/src/main/java/com/sb/web/admin/dto/RoleUpdateRequest.java @@ -0,0 +1,14 @@ +package com.sb.web.admin.dto; + +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +/** + * 회원 역할 변경 요청. + */ +@Data +public class RoleUpdateRequest { + + @Pattern(regexp = "USER|ADMIN", message = "역할은 USER 또는 ADMIN 이어야 합니다.") + private String role; +} diff --git a/src/main/java/com/sb/web/admin/dto/StatusUpdateRequest.java b/src/main/java/com/sb/web/admin/dto/StatusUpdateRequest.java new file mode 100644 index 0000000..3fa6d79 --- /dev/null +++ b/src/main/java/com/sb/web/admin/dto/StatusUpdateRequest.java @@ -0,0 +1,14 @@ +package com.sb.web.admin.dto; + +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +/** + * 회원 상태 변경 요청. + */ +@Data +public class StatusUpdateRequest { + + @Pattern(regexp = "ACTIVE|SUSPENDED|WITHDRAWN", message = "상태 값이 올바르지 않습니다.") + private String status; +} diff --git a/src/main/java/com/sb/web/admin/service/MemberAdminService.java b/src/main/java/com/sb/web/admin/service/MemberAdminService.java new file mode 100644 index 0000000..3831497 --- /dev/null +++ b/src/main/java/com/sb/web/admin/service/MemberAdminService.java @@ -0,0 +1,66 @@ +package com.sb.web.admin.service; + +import com.sb.web.auth.domain.Member; +import com.sb.web.auth.dto.MemberAdminResponse; +import com.sb.web.auth.mapper.MemberMapper; +import com.sb.web.common.exception.ApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 관리자용 회원 관리 서비스. (목록/역할/상태/삭제) + * 본인 계정에 대한 역할·상태·삭제는 잠금 방지를 위해 차단한다. + */ +@Service +@RequiredArgsConstructor +public class MemberAdminService { + + private final MemberMapper memberMapper; + + public List list() { + return memberMapper.findAll().stream().map(MemberAdminResponse::from).toList(); + } + + @Transactional + public MemberAdminResponse updateRole(Long targetId, String role, Long currentAdminId) { + Member target = mustFind(targetId); + if (targetId.equals(currentAdminId)) { + throw new ApiException(HttpStatus.BAD_REQUEST, "본인의 역할은 변경할 수 없습니다."); + } + memberMapper.updateRole(targetId, role); + target.setRole(role); + return MemberAdminResponse.from(target); + } + + @Transactional + public MemberAdminResponse updateStatus(Long targetId, String status, Long currentAdminId) { + Member target = mustFind(targetId); + if (targetId.equals(currentAdminId)) { + throw new ApiException(HttpStatus.BAD_REQUEST, "본인의 상태는 변경할 수 없습니다."); + } + memberMapper.updateStatus(targetId, status); + target.setStatus(status); + return MemberAdminResponse.from(target); + } + + @Transactional + public void delete(Long targetId, Long currentAdminId) { + mustFind(targetId); + if (targetId.equals(currentAdminId)) { + throw new ApiException(HttpStatus.BAD_REQUEST, "본인 계정은 삭제할 수 없습니다."); + } + memberMapper.deleteById(targetId); + } + + private Member mustFind(Long id) { + Member m = memberMapper.findById(id); + if (m == null) { + throw new ApiException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."); + } + return m; + } +} diff --git a/src/main/java/com/sb/web/auth/dto/MemberAdminResponse.java b/src/main/java/com/sb/web/auth/dto/MemberAdminResponse.java new file mode 100644 index 0000000..16b7695 --- /dev/null +++ b/src/main/java/com/sb/web/auth/dto/MemberAdminResponse.java @@ -0,0 +1,37 @@ +package com.sb.web.auth.dto; + +import com.sb.web.auth.domain.Member; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 관리자용 회원 정보 응답 (비밀번호 제외). + */ +@Data +@Builder +public class MemberAdminResponse { + + private Long id; + private String loginId; + private String name; + private String email; + private String role; // USER / ADMIN + private String status; // ACTIVE / SUSPENDED / WITHDRAWN + private String provider; // LOCAL / 소셜 + private LocalDateTime createdAt; + + public static MemberAdminResponse from(Member m) { + return MemberAdminResponse.builder() + .id(m.getId()) + .loginId(m.getLoginId()) + .name(m.getName()) + .email(m.getEmail()) + .role(m.getRole()) + .status(m.getStatus()) + .provider(m.getProvider()) + .createdAt(m.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/sb/web/auth/mapper/MemberMapper.java b/src/main/java/com/sb/web/auth/mapper/MemberMapper.java index a185f7b..b0f0daa 100644 --- a/src/main/java/com/sb/web/auth/mapper/MemberMapper.java +++ b/src/main/java/com/sb/web/auth/mapper/MemberMapper.java @@ -4,6 +4,8 @@ import com.sb.web.auth.domain.Member; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import java.util.List; + /** * 회원 MyBatis 매퍼. SQL 은 resources/mapper/MemberMapper.xml 에 정의. */ @@ -12,6 +14,9 @@ public interface MemberMapper { Member findById(@Param("id") Long id); + /** 관리자: 전체 회원 목록 */ + List findAll(); + Member findByLoginId(@Param("loginId") String loginId); Member findByProvider(@Param("provider") String provider, @@ -22,4 +27,10 @@ public interface MemberMapper { int insert(Member member); int updatePassword(@Param("id") Long id, @Param("password") String password); + + int updateRole(@Param("id") Long id, @Param("role") String role); + + int updateStatus(@Param("id") Long id, @Param("status") String status); + + int deleteById(@Param("id") Long id); } diff --git a/src/main/resources/mapper/MemberMapper.xml b/src/main/resources/mapper/MemberMapper.xml index 6a889c7..5dbb870 100644 --- a/src/main/resources/mapper/MemberMapper.xml +++ b/src/main/resources/mapper/MemberMapper.xml @@ -25,6 +25,10 @@ SELECT FROM member WHERE id = #{id} + + @@ -50,4 +54,16 @@ UPDATE member SET password = #{password}, updated_at = NOW() WHERE id = #{id} + + UPDATE member SET role = #{role}, updated_at = NOW() WHERE id = #{id} + + + + UPDATE member SET status = #{status}, updated_at = NOW() WHERE id = #{id} + + + + DELETE FROM member WHERE id = #{id} + +