Skip to content

Commit

Permalink
[BE] Feat/#378 Admin API ๊ตฌํ˜„ (#405)
Browse files Browse the repository at this point in the history
* feat: ์ „์ฒด ํšŒ์› ์กฐํšŒ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* feat: ํšŒ์› ์‚ญ์ œ(ํƒˆํ‡ด) ๊ธฐ๋Šฅ ๊ตฌํ˜„

* feat: ํšŒ์› ์‚ญ์ œ(ํƒˆํ‡ด)์‹œ Pin/Topic Soft-deleting ๊ตฌํ˜„

* refactor: Admin DTO ๋ถ„๋ฆฌ

* feat: Member ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* feat: Topic ์‚ญ์ œ ๋ฐ ์ด๋ฏธ์ง€ ์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* feat: Pin ์‚ญ์ œ ๋ฐ ์ด๋ฏธ์ง€ ์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* feat: Admin API ๊ตฌํ˜„

* refactor: Member ์ƒํƒœ(์ฐจ๋‹จ, ํƒˆํ‡ด ๋“ฑ) ํ•„๋“œ์— ๋”ฐ๋ฅธ ๋กœ๊ทธ์ธ ๋กœ์ง ์ˆ˜์ •

* refactor: @SqlDelete ์‚ญ์ œ ๋ฐ JPQL ๋Œ€์ฒด

* feat: AdminInterceptor ๊ตฌํ˜„

* test: Repository soft-deleting ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

* test: AdminQueryService ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

* test: AdminCommandService ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

* test: AdminController Restdocs ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

* test: AdminInterceptor Mocking

* test: ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

* refactor: ์˜คํƒˆ์ž ์ˆ˜์ •

* refactor: Auth ๊ด€๋ จ ์˜ˆ์™ธ ํด๋ž˜์Šค ์ถ”๊ฐ€

* refactor: ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ

* refactor: findMemberById ์˜ˆ์™ธ ์ˆ˜์ •

* test: GithubActions ์‹คํŒจ ํ…Œ์ŠคํŠธ ์ˆ˜์ •

* refactor: isAdmin() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

* refactor: ํšŒ์› ์‚ญ์ œ(ํƒˆํ‡ด)์‹œ, ์ถ”๊ฐ€ ์ •๋ณด(์ฆ๊ฒจ์ฐพ๊ธฐ ๋“ฑ) ์‚ญ์ œ
  • Loading branch information
cpot5620 authored Sep 15, 2023
1 parent de374f8 commit 4722faa
Show file tree
Hide file tree
Showing 48 changed files with 1,600 additions and 47 deletions.
29 changes: 29 additions & 0 deletions backend/src/docs/asciidoc/admin.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
== ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ

=== ์ „์ฒด ํšŒ์› ์กฐํšŒ

operation::admin-controller-test/find-all-member-details[snippets='http-request,http-response']

=== ํšŒ์› ์ƒ์„ธ ์กฐํšŒ

operation::admin-controller-test/find-member[snippets='http-request,http-response']

=== ํšŒ์› ์ฐจ๋‹จ(์‚ญ์ œ)

operation::admin-controller-test/delete-member[snippets='http-request,http-response']

=== ํ† ํ”ฝ ์‚ญ์ œ

operation::admin-controller-test/delete-topic[snippets='http-request,http-response']

=== ํ† ํ”ฝ ์ด๋ฏธ์ง€ ์‚ญ์ œ

operation::admin-controller-test/delete-topic-image[snippets='http-request,http-response']

=== ํ•€ ์‚ญ์ œ

operation::admin-controller-test/delete-pin[snippets='http-request,http-response']

=== ํ•€ ์ด๋ฏธ์ง€ ์‚ญ์ œ

operation::admin-controller-test/delete-pin-image[snippets='http-request,http-response']
1 change: 1 addition & 0 deletions backend/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ include::member.adoc[]
include::permission.adoc[]
include::oauth.adoc[]
include::bookmark.adoc[]
include::admin.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.mapbefine.mapbefine.admin.application;

import static com.mapbefine.mapbefine.permission.exception.PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN;
import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND;

import com.mapbefine.mapbefine.atlas.domain.AtlasRepository;
import com.mapbefine.mapbefine.auth.domain.AuthMember;
import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository;
import com.mapbefine.mapbefine.member.domain.Member;
import com.mapbefine.mapbefine.member.domain.MemberRepository;
import com.mapbefine.mapbefine.member.domain.Role;
import com.mapbefine.mapbefine.member.domain.Status;
import com.mapbefine.mapbefine.permission.domain.PermissionRepository;
import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException;
import com.mapbefine.mapbefine.pin.domain.Pin;
import com.mapbefine.mapbefine.pin.domain.PinImageRepository;
import com.mapbefine.mapbefine.pin.domain.PinRepository;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.domain.TopicRepository;
import com.mapbefine.mapbefine.topic.exception.TopicException;
import java.util.List;
import java.util.NoSuchElementException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class AdminCommandService {

private final MemberRepository memberRepository;
private final TopicRepository topicRepository;
private final PinRepository pinRepository;
private final PinImageRepository pinImageRepository;
private final PermissionRepository permissionRepository;
private final AtlasRepository atlasRepository;
private final BookmarkRepository bookmarkRepository;

public AdminCommandService(
MemberRepository memberRepository,
TopicRepository topicRepository,
PinRepository pinRepository,
PinImageRepository pinImageRepository,
PermissionRepository permissionRepository,
AtlasRepository atlasRepository,
BookmarkRepository bookmarkRepository
) {
this.memberRepository = memberRepository;
this.topicRepository = topicRepository;
this.pinRepository = pinRepository;
this.pinImageRepository = pinImageRepository;
this.permissionRepository = permissionRepository;
this.atlasRepository = atlasRepository;
this.bookmarkRepository = bookmarkRepository;
}

public void blockMember(AuthMember authMember, Long memberId) {
validateAdminPermission(authMember);

Member member = findMemberById(memberId);
member.updateStatus(Status.BLOCKED);

deleteAllRelatedMember(member);
}

private void deleteAllRelatedMember(Member member) {
List<Long> pinIds = extractPinIdsByMember(member);
Long memberId = member.getId();

pinImageRepository.deleteAllByPinIds(pinIds);
topicRepository.deleteAllByMemberId(memberId);
pinRepository.deleteAllByMemberId(memberId);
permissionRepository.deleteAllByMemberId(memberId);
atlasRepository.deleteAllByMemberId(memberId);
bookmarkRepository.deleteAllByMemberId(memberId);
}

private Member findMemberById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id));
}

private void validateAdminPermission(AuthMember authMember) {
if (authMember.isRole(Role.ADMIN)) {
return;
}

throw new PermissionForbiddenException(PERMISSION_FORBIDDEN_BY_NOT_ADMIN);
}

private List<Long> extractPinIdsByMember(Member member) {
return member.getCreatedPins()
.stream()
.map(Pin::getId)
.toList();
}

public void deleteTopic(AuthMember authMember, Long topicId) {
validateAdminPermission(authMember);

topicRepository.deleteById(topicId);
}

public void deleteTopicImage(AuthMember authMember, Long topicId) {
validateAdminPermission(authMember);

Topic topic = findTopicById(topicId);
topic.removeImage();
}

private Topic findTopicById(Long topicId) {
return topicRepository.findById(topicId)
.orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId)));
}

public void deletePin(AuthMember authMember, Long pinId) {
validateAdminPermission(authMember);

pinRepository.deleteById(pinId);
}

public void deletePinImage(AuthMember authMember, Long pinImageId) {
validateAdminPermission(authMember);

pinImageRepository.deleteById(pinImageId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mapbefine.mapbefine.admin.application;

import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse;
import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse;
import com.mapbefine.mapbefine.auth.domain.AuthMember;
import com.mapbefine.mapbefine.member.domain.Member;
import com.mapbefine.mapbefine.member.domain.MemberRepository;
import com.mapbefine.mapbefine.member.domain.Role;
import com.mapbefine.mapbefine.permission.exception.PermissionErrorCode;
import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException;
import com.mapbefine.mapbefine.pin.domain.Pin;
import com.mapbefine.mapbefine.topic.domain.Topic;
import java.util.List;
import java.util.NoSuchElementException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class AdminQueryService {

private final MemberRepository memberRepository;

public AdminQueryService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

public List<AdminMemberResponse> findAllMemberDetails(AuthMember authMember) {
validateAdminPermission(authMember);

List<Member> members = memberRepository.findAllByMemberInfoRole(Role.USER);

return members.stream()
.map(AdminMemberResponse::from)
.toList();
}

private Member findMemberById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id));
}

private void validateAdminPermission(AuthMember authMember) {
if (authMember.isRole(Role.ADMIN)) {
return;
}

throw new PermissionForbiddenException(PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN);
}

public AdminMemberDetailResponse findMemberDetail(AuthMember authMember, Long memberId) {
validateAdminPermission(authMember);

Member findMember = findMemberById(memberId);
List<Topic> topics = findMember.getCreatedTopics();
List<Pin> pins = findMember.getCreatedPins();

return AdminMemberDetailResponse.of(findMember, topics, pins);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.mapbefine.mapbefine.admin.dto;

import com.mapbefine.mapbefine.member.domain.Member;
import com.mapbefine.mapbefine.member.domain.MemberInfo;
import com.mapbefine.mapbefine.pin.domain.Pin;
import com.mapbefine.mapbefine.pin.dto.response.PinResponse;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.dto.response.TopicResponse;
import java.time.LocalDateTime;
import java.util.List;

public record AdminMemberDetailResponse(
Long id,
String nickName,
String email,
String imageUrl,
List<TopicResponse> topics,
List<PinResponse> pins,
LocalDateTime updatedAt
) {

// TODO: 2023/09/12 topics, pins ๋ชจ๋‘ member๋ฅผ ํ†ตํ•ด ์–ป์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค. Service์—์„œ ๊บผ๋‚ด์„œ ๋„˜๊ฒจ์ค„ ๊ฒƒ์ธ๊ฐ€ ? ์•„๋‹ˆ๋ฉด DTO์—์„œ ๊บผ๋‚ด์˜ฌ ๊ฒƒ์ธ๊ฐ€ ?
public static AdminMemberDetailResponse of(
Member member,
List<Topic> topics,
List<Pin> pins
) {
MemberInfo memberInfo = member.getMemberInfo();
List<TopicResponse> topicResponses = topics.stream()
.map(TopicResponse::fromGuestQuery)
.toList();
List<PinResponse> pinResponses = pins.stream()
.map(PinResponse::from)
.toList();

return new AdminMemberDetailResponse(
member.getId(),
memberInfo.getNickName(),
memberInfo.getEmail(),
memberInfo.getImageUrl(),
topicResponses,
pinResponses,
member.getUpdatedAt()
);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.mapbefine.mapbefine.admin.dto;

import com.mapbefine.mapbefine.member.domain.Member;
import com.mapbefine.mapbefine.member.domain.MemberInfo;
import java.time.LocalDateTime;

public record AdminMemberResponse(
Long id,
String nickName,
String email,
String imageUrl,
LocalDateTime updatedAt
) {

public static AdminMemberResponse from(Member member) {
MemberInfo memberInfo = member.getMemberInfo();

return new AdminMemberResponse(
member.getId(),
memberInfo.getNickName(),
memberInfo.getEmail(),
memberInfo.getImageUrl(),
member.getUpdatedAt()
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.mapbefine.mapbefine.admin.presentation;

import com.mapbefine.mapbefine.admin.application.AdminCommandService;
import com.mapbefine.mapbefine.admin.application.AdminQueryService;
import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse;
import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse;
import com.mapbefine.mapbefine.auth.domain.AuthMember;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/admin")
public class AdminController {

private final AdminQueryService adminQueryService;
private final AdminCommandService adminCommandService;


public AdminController(AdminQueryService adminQueryService, AdminCommandService adminCommandService) {
this.adminQueryService = adminQueryService;
this.adminCommandService = adminCommandService;
}

@GetMapping("/members")
public ResponseEntity<List<AdminMemberResponse>> findAllMembers(AuthMember authMember) {
List<AdminMemberResponse> responses = adminQueryService.findAllMemberDetails(authMember);

return ResponseEntity.ok(responses);
}

@DeleteMapping("/members/{memberId}")
public ResponseEntity<Void> deleteMember(AuthMember authMember, @PathVariable Long memberId) {
adminCommandService.blockMember(authMember, memberId);

return ResponseEntity.noContent().build();
}

@GetMapping("/members/{memberId}")
public ResponseEntity<AdminMemberDetailResponse> findMember(AuthMember authMember, @PathVariable Long memberId) {
AdminMemberDetailResponse response = adminQueryService.findMemberDetail(authMember, memberId);

return ResponseEntity.ok(response);
}

@DeleteMapping("/topics/{topicId}")
public ResponseEntity<Void> deleteTopic(AuthMember authMember, @PathVariable Long topicId) {
adminCommandService.deleteTopic(authMember, topicId);

return ResponseEntity.noContent().build();
}

@DeleteMapping("/topics/{topicId}/images")
public ResponseEntity<Void> deleteTopicImage(AuthMember authMember, @PathVariable Long topicId) {
adminCommandService.deleteTopicImage(authMember, topicId);

return ResponseEntity.noContent().build();
}

@DeleteMapping("/pins/{pinId}")
public ResponseEntity<Void> deletePin(AuthMember authMember, @PathVariable Long pinId) {
adminCommandService.deletePin(authMember, pinId);

return ResponseEntity.noContent().build();
}

@DeleteMapping("/pins/images/{imageId}")
public ResponseEntity<Void> deletePinImage(AuthMember authMember, @PathVariable Long imageId) {
adminCommandService.deletePinImage(authMember, imageId);

return ResponseEntity.noContent().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public interface AtlasRepository extends JpaRepository<Atlas, Long> {

void deleteByMemberIdAndTopicId(Long memberId, Long topicId);

void deleteAllByMemberId(Long memberId);
}
Loading

0 comments on commit 4722faa

Please sign in to comment.