-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
48 changed files
with
1,600 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
61 changes: 61 additions & 0 deletions
61
backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
48 changes: 48 additions & 0 deletions
48
backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
); | ||
} | ||
|
||
|
||
} |
27 changes: 27 additions & 0 deletions
27
backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
); | ||
} | ||
|
||
} |
78 changes: 78 additions & 0 deletions
78
backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.