diff --git a/.github/workflows/be-merge-dev.yml b/.github/workflows/be-merge-dev.yml index 33b2781d6..e19fbb58c 100644 --- a/.github/workflows/be-merge-dev.yml +++ b/.github/workflows/be-merge-dev.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [ develop-BE ] + branches: [ develop-BE-2 ] types: [ closed ] paths: backend/** diff --git a/.github/workflows/be-pull-request.yml b/.github/workflows/be-pull-request.yml index cea976b96..65236d826 100644 --- a/.github/workflows/be-pull-request.yml +++ b/.github/workflows/be-pull-request.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [ main, develop-BE ] + branches: [ main, develop-BE-2 ] paths: backend/** permissions: diff --git a/backend/src/docs/asciidoc/admin.adoc b/backend/src/docs/asciidoc/admin.adoc index 3d2137c4e..64b338446 100644 --- a/backend/src/docs/asciidoc/admin.adoc +++ b/backend/src/docs/asciidoc/admin.adoc @@ -1,29 +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'] \ No newline at end of file +// == 관리자 기능 +// +// === 전체 회원 조회 +// +// 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'] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java index 0dfaf5184..273e7aee5 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java @@ -1,28 +1,25 @@ 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; +import java.util.List; +import java.util.NoSuchElementException; + @Service @Transactional public class AdminCommandService { @@ -53,9 +50,7 @@ public AdminCommandService( this.bookmarkRepository = bookmarkRepository; } - public void blockMember(AuthMember authMember, Long memberId) { - validateAdminPermission(authMember); - + public void blockMember(Long memberId) { Member member = findMemberById(memberId); member.updateStatus(Status.BLOCKED); @@ -79,14 +74,6 @@ private Member findMemberById(Long 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 extractPinIdsByMember(Member member) { return member.getCreatedPins() .stream() @@ -94,15 +81,11 @@ private List extractPinIdsByMember(Member member) { .toList(); } - public void deleteTopic(AuthMember authMember, Long topicId) { - validateAdminPermission(authMember); - + public void deleteTopic(Long topicId) { topicRepository.deleteById(topicId); } - public void deleteTopicImage(AuthMember authMember, Long topicId) { - validateAdminPermission(authMember); - + public void deleteTopicImage(Long topicId) { Topic topic = findTopicById(topicId); topic.removeImage(); } @@ -112,15 +95,11 @@ private Topic findTopicById(Long topicId) { .orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId))); } - public void deletePin(AuthMember authMember, Long pinId) { - validateAdminPermission(authMember); - + public void deletePin(Long pinId) { pinRepository.deleteById(pinId); } - public void deletePinImage(AuthMember authMember, Long pinImageId) { - validateAdminPermission(authMember); - + public void deletePinImage(Long pinImageId) { pinImageRepository.deleteById(pinImageId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java index f7ca346a9..2319c360b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java @@ -2,19 +2,17 @@ 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; +import java.util.List; +import java.util.NoSuchElementException; + @Service @Transactional(readOnly = true) public class AdminQueryService { @@ -25,9 +23,7 @@ public AdminQueryService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } - public List findAllMemberDetails(AuthMember authMember) { - validateAdminPermission(authMember); - + public List findAllMemberDetails() { List members = memberRepository.findAllByMemberInfoRole(Role.USER); return members.stream() @@ -40,17 +36,7 @@ private Member findMemberById(Long 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); - + public AdminMemberDetailResponse findMemberDetail(Long memberId) { Member findMember = findMemberById(memberId); List topics = findMember.getCreatedTopics(); List pins = findMember.getCreatedPins(); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java index fbd5e04d9..8fd0ce7c7 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java @@ -6,6 +6,7 @@ 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; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java index f041f8f17..20f55e4c2 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java @@ -2,6 +2,7 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberInfo; + import java.time.LocalDateTime; public record AdminMemberResponse( diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java index 2a34fec12..fb7533ce2 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java @@ -4,8 +4,6 @@ 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; @@ -13,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController @RequestMapping("/admin") public class AdminController { @@ -27,50 +27,50 @@ public AdminController(AdminQueryService adminQueryService, AdminCommandService } @GetMapping("/members") - public ResponseEntity> findAllMembers(AuthMember authMember) { - List responses = adminQueryService.findAllMemberDetails(authMember); + public ResponseEntity> findAllMembers() { + List responses = adminQueryService.findAllMemberDetails(); return ResponseEntity.ok(responses); } @DeleteMapping("/members/{memberId}") - public ResponseEntity deleteMember(AuthMember authMember, @PathVariable Long memberId) { - adminCommandService.blockMember(authMember, memberId); + public ResponseEntity deleteMember(@PathVariable Long memberId) { + adminCommandService.blockMember(memberId); return ResponseEntity.noContent().build(); } @GetMapping("/members/{memberId}") - public ResponseEntity findMember(AuthMember authMember, @PathVariable Long memberId) { - AdminMemberDetailResponse response = adminQueryService.findMemberDetail(authMember, memberId); + public ResponseEntity findMember(@PathVariable Long memberId) { + AdminMemberDetailResponse response = adminQueryService.findMemberDetail(memberId); return ResponseEntity.ok(response); } @DeleteMapping("/topics/{topicId}") - public ResponseEntity deleteTopic(AuthMember authMember, @PathVariable Long topicId) { - adminCommandService.deleteTopic(authMember, topicId); + public ResponseEntity deleteTopic(@PathVariable Long topicId) { + adminCommandService.deleteTopic(topicId); return ResponseEntity.noContent().build(); } @DeleteMapping("/topics/{topicId}/images") - public ResponseEntity deleteTopicImage(AuthMember authMember, @PathVariable Long topicId) { - adminCommandService.deleteTopicImage(authMember, topicId); + public ResponseEntity deleteTopicImage(@PathVariable Long topicId) { + adminCommandService.deleteTopicImage(topicId); return ResponseEntity.noContent().build(); } @DeleteMapping("/pins/{pinId}") - public ResponseEntity deletePin(AuthMember authMember, @PathVariable Long pinId) { - adminCommandService.deletePin(authMember, pinId); + public ResponseEntity deletePin(@PathVariable Long pinId) { + adminCommandService.deletePin(pinId); return ResponseEntity.noContent().build(); } @DeleteMapping("/pins/images/{imageId}") - public ResponseEntity deletePinImage(AuthMember authMember, @PathVariable Long imageId) { - adminCommandService.deletePinImage(authMember, imageId); + public ResponseEntity deletePinImage(@PathVariable Long imageId) { + adminCommandService.deletePinImage(imageId); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminViewController.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminViewController.java deleted file mode 100644 index 15f416131..000000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminViewController.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.mapbefine.mapbefine.admin.presentation; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping("/admin") -public class AdminViewController { - - @GetMapping - public String home() { - - return "admin/index.html"; - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index bd37a9baa..664cdc944 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java @@ -7,7 +7,6 @@ public interface AtlasRepository extends JpaRepository { boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); - void deleteByMemberIdAndTopicId(Long memberId, Long topicId); void deleteAllByMemberId(Long memberId); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java index 6f34de32e..78d7ce1f7 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java @@ -15,10 +15,11 @@ import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import java.util.NoSuchElementException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.NoSuchElementException; + @Service @Transactional public class BookmarkCommandService { @@ -84,7 +85,17 @@ private Member findMemberById(AuthMember authMember) { public void deleteTopicInBookmark(AuthMember authMember, Long topicId) { validateBookmarkDeletingPermission(authMember, topicId); - bookmarkRepository.deleteByMemberIdAndTopicId(authMember.getMemberId(), topicId); + Bookmark bookmark = findBookmarkByMemberIdAndTopicId(authMember.getMemberId(), topicId); + Topic topic = getTopicById(topicId); + + topic.removeBookmark(bookmark); + } + + private Bookmark findBookmarkByMemberIdAndTopicId(Long memberId, Long topicId) { + return bookmarkRepository.findByMemberIdAndTopicId(memberId, topicId) + .orElseThrow(() -> new NoSuchElementException( + "findBookmarkByMemberIdAndTopicId; memberId=" + memberId + " topicId=" + topicId + )); } private void validateBookmarkDeletingPermission(AuthMember authMember, Long topicId) { @@ -95,6 +106,7 @@ private void validateBookmarkDeletingPermission(AuthMember authMember, Long topi throw new BookmarkForbiddenException(FORBIDDEN_TOPIC_DELETE); } + @Deprecated public void deleteAllBookmarks(AuthMember authMember) { bookmarkRepository.deleteAllByMemberId(authMember.getMemberId()); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java index 3ce145277..34c58d3ce 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java @@ -1,12 +1,17 @@ package com.mapbefine.mapbefine.bookmark.domain; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; + +import java.util.Optional; public interface BookmarkRepository extends JpaRepository { boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); + @Modifying(clearAutomatically = true) void deleteAllByMemberId(Long memberId); - void deleteByMemberIdAndTopicId(Long memberId, Long topicId); + Optional findByMemberIdAndTopicId(Long memberId, Long topicId); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java index 7a7cb1bbd..5ce84076d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java @@ -1,14 +1,12 @@ package com.mapbefine.mapbefine.common.interceptor; -import com.mapbefine.mapbefine.auth.application.AuthService; -import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; import com.mapbefine.mapbefine.auth.exception.AuthException; -import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; -import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.Objects; +import org.springframework.beans.factory.annotation.Value; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; @@ -17,18 +15,12 @@ @Component public class AdminAuthInterceptor implements HandlerInterceptor { - private final AuthorizationExtractor authorizationExtractor; - private final AuthService authService; - private final TokenProvider tokenProvider; + private final String secretKey; public AdminAuthInterceptor( - AuthorizationExtractor authorizationExtractor, - AuthService authService, - TokenProvider tokenProvider + @Value("${security.admin.key}") String secretKey ) { - this.authorizationExtractor = authorizationExtractor; - this.authService = authService; - this.tokenProvider = tokenProvider; + this.secretKey = secretKey; } @Override @@ -41,29 +33,15 @@ public boolean preHandle( return true; } - Long memberId = extractMemberIdFromToken(request); - - validateAdmin(memberId); - request.setAttribute("memberId", memberId); - + String secretKey = request.getHeader(AUTHORIZATION); + validateAdmin(secretKey); return true; } - private Long extractMemberIdFromToken(HttpServletRequest request) { - AuthInfo authInfo = authorizationExtractor.extract(request); - if (Objects.isNull(authInfo)) { - return null; - } - tokenProvider.validateAccessToken(authInfo.accessToken()); - - return Long.parseLong(tokenProvider.getPayload(authInfo.accessToken())); - } - - private void validateAdmin(Long memberId) { - if (authService.isAdmin(memberId)) { + private void validateAdmin(String key) { + if (secretKey.equals(key)) { return; } - throw new AuthException.AuthForbiddenException(AuthErrorCode.FORBIDDEN_ADMIN_ACCESS); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Location.java b/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Location.java index 504ec9092..098debd5e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Location.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Location.java @@ -10,11 +10,12 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @NoArgsConstructor(access = PROTECTED) @Getter diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index df5296f96..762d77d17 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java @@ -1,8 +1,9 @@ package com.mapbefine.mapbefine.member.application; import com.mapbefine.mapbefine.atlas.domain.Atlas; +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +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.dto.response.MemberDetailResponse; @@ -11,19 +12,33 @@ import com.mapbefine.mapbefine.member.exception.MemberException.MemberNotFoundException; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; -import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @Transactional(readOnly = true) public class MemberQueryService { private final MemberRepository memberRepository; + private final AtlasRepository atlasRepository; + private final BookmarkRepository bookmarkRepository; + + private final TopicRepository topicRepository; - public MemberQueryService(MemberRepository memberRepository) { + public MemberQueryService( + MemberRepository memberRepository, + AtlasRepository atlasRepository, + BookmarkRepository bookmarkRepository, + TopicRepository topicRepository + ) { this.memberRepository = memberRepository; + this.atlasRepository = atlasRepository; + this.bookmarkRepository = bookmarkRepository; + this.topicRepository = topicRepository; } public MemberDetailResponse findById(Long id) { @@ -48,13 +63,11 @@ public List findAll() { public List findAllTopicsInBookmark(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - List bookMarkedTopics = findBookMarkedTopics(member); - List topicsInAtlas = findTopicsInAtlas(member); - + List bookMarkedTopics = topicRepository.findTopicsByBookmarksMemberId(authMember.getMemberId()); return bookMarkedTopics.stream() .map(topic -> TopicResponse.from( topic, - isInAtlas(topicsInAtlas, topic), + isInAtlas(member.getId(), topic.getId()), true )) .toList(); @@ -67,51 +80,40 @@ private List findTopicsInAtlas(Member member) { .toList(); } - private List findBookMarkedTopics(Member member) { - return member.getBookmarks() - .stream() - .map(Bookmark::getTopic) - .toList(); - } - - private boolean isInAtlas(List topicsInAtlas, Topic topic) { - return topicsInAtlas.contains(topic); + private boolean isInAtlas(Long memberId, Long topicId) { + return atlasRepository.existsByMemberIdAndTopicId(memberId, topicId); } public List findAllTopicsInAtlas(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - List bookMarkedTopics = findBookMarkedTopics(member); List topicsInAtlas = findTopicsInAtlas(member); return topicsInAtlas.stream() .map(topic -> TopicResponse.from( topic, true, - isBookMarked(bookMarkedTopics, topic) + isInBookmark(authMember.getMemberId(), topic.getId()) )) .toList(); } - private boolean isBookMarked(List bookMarkedTopics, Topic topic) { - return bookMarkedTopics.contains(topic); + private boolean isInBookmark(Long memberId, Long topicId) { + return bookmarkRepository.existsByMemberIdAndTopicId(memberId, topicId); } public List findMyAllTopics(AuthMember authMember) { - Member member = findMemberById(authMember.getMemberId()); - - List bookMarkedTopics = findBookMarkedTopics(member); - List topicsInAtlas = findTopicsInAtlas(member); + Long memberId = authMember.getMemberId(); + Member member = findMemberById(memberId); return member.getCreatedTopics() .stream() .map(topic -> TopicResponse.from( topic, - isInAtlas(topicsInAtlas, topic), - isBookMarked(bookMarkedTopics, topic) - )) - .toList(); + isInAtlas(memberId, topic.getId()), + isInBookmark(memberId, topic.getId()) + )).toList(); } public List findMyAllPins(AuthMember authMember) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java index 8081b9139..1496b0078 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java @@ -19,12 +19,13 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.PrePersist; import jakarta.persistence.PreUpdate; -import java.util.ArrayList; -import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; +import java.util.ArrayList; +import java.util.List; + @Entity @NoArgsConstructor(access = PROTECTED) @Getter diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java index 20ac5a4ac..6cfdcf0df 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java @@ -1,12 +1,14 @@ package com.mapbefine.mapbefine.pin.domain; -import java.util.List; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface PinRepository extends JpaRepository { @@ -22,8 +24,13 @@ public interface PinRepository extends JpaRepository { @Query("update Pin p set p.isDeleted = true where p.creator.id = :memberId") void deleteAllByMemberId(@Param("memberId") Long memberId); + List findAll(); + + @EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"}) List findAllByTopicId(Long topicId); + @EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"}) List findAllByCreatorId(Long creatorId); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index 5f217e71b..29b660060 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -3,9 +3,9 @@ import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.FORBIDDEN_TOPIC_READ; import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND; -import com.mapbefine.mapbefine.atlas.domain.Atlas; +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +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.topic.domain.Topic; @@ -14,12 +14,13 @@ import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicForbiddenException; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import java.util.Comparator; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Service @Transactional(readOnly = true) @@ -27,13 +28,19 @@ public class TopicQueryService { private final TopicRepository topicRepository; private final MemberRepository memberRepository; + private final AtlasRepository atlasRepository; + private final BookmarkRepository bookmarkRepository; public TopicQueryService( TopicRepository topicRepository, - MemberRepository memberRepository + MemberRepository memberRepository, + AtlasRepository atlasRepository, + BookmarkRepository bookmarkRepository ) { this.topicRepository = topicRepository; this.memberRepository = memberRepository; + this.atlasRepository = atlasRepository; + this.bookmarkRepository = bookmarkRepository; } public List findAllReadable(AuthMember authMember) { @@ -54,15 +61,12 @@ private List getGuestTopicResponses(AuthMember authMember) { private List getUserTopicResponses(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - List topicsInAtlas = findTopicsInAtlas(member); - List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findAll().stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( topic, - isInAtlas(topicsInAtlas, topic), - isBookMarked(topicsInBookMark, topic) + isInAtlas(member.getId(), topic.getId()), + isBookMarked(member.getId(), topic.getId()) )) .toList(); } @@ -72,26 +76,12 @@ private Member findMemberById(Long id) { .orElseThrow(() -> new NoSuchElementException("findCreatorByAuthMember; member not found; id=" + id)); } - private List findTopicsInAtlas(Member member) { - return member.getAtlantes() - .stream() - .map(Atlas::getTopic) - .toList(); - } - - private List findBookMarkedTopics(Member member) { - return member.getBookmarks() - .stream() - .map(Bookmark::getTopic) - .toList(); - } - - private boolean isInAtlas(List topicsInAtlas, Topic topic) { - return topicsInAtlas.contains(topic); + private boolean isInAtlas(Long memberId, Long topicId) { + return atlasRepository.existsByMemberIdAndTopicId(memberId, topicId); } - private boolean isBookMarked(List bookMarkedTopics, Topic topic) { - return bookMarkedTopics.contains(topic); + private boolean isBookMarked(Long memberId, Long topicId) { + return bookmarkRepository.existsByMemberIdAndTopicId(memberId, topicId); } public TopicDetailResponse findDetailById(AuthMember authMember, Long topicId) { @@ -104,13 +94,10 @@ public TopicDetailResponse findDetailById(AuthMember authMember, Long topicId) { Member member = findMemberById(authMember.getMemberId()); - List topicsInAtlas = findTopicsInAtlas(member); - List topicsInBookMark = findBookMarkedTopics(member); - return TopicDetailResponse.of( topic, - isInAtlas(topicsInAtlas, topic), - isBookMarked(topicsInBookMark, topic), + isInAtlas(member.getId(), topic.getId()), + isBookMarked(member.getId(), topic.getId()), authMember.canTopicUpdate(topic) ); } @@ -142,16 +129,11 @@ public List findDetailsByIds(AuthMember authMember, List getUserTopicDetailResponses(AuthMember authMember, List topics) { - Member member = findMemberById(authMember.getMemberId()); - - List topicsInAtlas = findTopicsInAtlas(member); - List topicsInBookMark = findBookMarkedTopics(member); - return topics.stream() .map(topic -> TopicDetailResponse.of( topic, - isInAtlas(topicsInAtlas, topic), - isBookMarked(topicsInBookMark, topic), + isInAtlas(authMember.getMemberId(), topic.getId()), + isBookMarked(authMember.getMemberId(), topic.getId()), authMember.canTopicUpdate(topic) )) .toList(); @@ -190,19 +172,15 @@ public List findAllTopicsByMemberId(AuthMember authMember, Long m Member member = findMemberById(authMember.getMemberId()); - List topicsInAtlas = findTopicsInAtlas(member); - List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( topic, - isInAtlas(topicsInAtlas, topic), - isBookMarked(topicsInBookMark, topic) + isInAtlas(member.getId(), topic.getId()), + isBookMarked(member.getId(), topic.getId()) )). toList(); - } public List findAllByOrderByUpdatedAtDesc(AuthMember authMember) { @@ -215,16 +193,13 @@ public List findAllByOrderByUpdatedAtDesc(AuthMember authMember) private List getUserNewestTopicResponse(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - List topicsInAtlas = findTopicsInAtlas(member); - List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findAllByOrderByLastPinUpdatedAtDesc() .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( topic, - isInAtlas(topicsInAtlas, topic), - isBookMarked(topicsInBookMark, topic) + isInAtlas(member.getId(), topic.getId()), + isBookMarked(member.getId(), topic.getId()) )). toList(); } @@ -256,18 +231,15 @@ private List getGuestBestTopicResponse(AuthMember authMember) { private List getUserBestTopicResponse(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - List topicsInAtlas = findTopicsInAtlas(member); - List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findAll() .stream() .filter(authMember::canRead) .sorted(Comparator.comparing(Topic::countBookmarks).reversed()) .map(topic -> TopicResponse.from( topic, - isInAtlas(topicsInAtlas, topic), - isBookMarked((topicsInBookMark), topic)) - ).toList(); + isInAtlas(member.getId(), topic.getId()), + isBookMarked(member.getId(), topic.getId()) + )).toList(); } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index 3483e21cf..6bd7ad3b0 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -11,6 +11,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -18,13 +19,14 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.PrePersist; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + @Entity @NoArgsConstructor(access = PROTECTED) @Getter @@ -40,18 +42,26 @@ public class Topic extends BaseTimeEntity { @Embedded private TopicStatus topicStatus; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member creator; @OneToMany(mappedBy = "topic") - private List permissions = new ArrayList<>(); + private Set permissions = new HashSet<>(); @OneToMany(mappedBy = "topic", cascade = CascadeType.PERSIST) - private List pins = new ArrayList<>(); + private Set pins = new HashSet<>(); - @OneToMany(mappedBy = "topic") - private List bookmarks = new ArrayList<>(); + @OneToMany(mappedBy = "topic", cascade = CascadeType.PERSIST, orphanRemoval = true) + private Set bookmarks = new HashSet<>(); + + @Column(nullable = false) + @ColumnDefault(value = "0") + private int pinCount = 0; + + @Column(nullable = false) + @ColumnDefault(value = "0") + private int bookmarkCount = 0; @Column(nullable = false) @ColumnDefault(value = "false") @@ -109,15 +119,17 @@ public void updateTopicStatus(Publicity publicity, PermissionType permissionType } public int countPins() { - return pins.size(); + return pinCount; } public void addPin(Pin pin) { pins.add(pin); + pinCount++; } public void addBookmark(Bookmark bookmark) { bookmarks.add(bookmark); + bookmarkCount++; } public void addMemberTopicPermission(Permission permission) { @@ -125,14 +137,19 @@ public void addMemberTopicPermission(Permission permission) { } public int countBookmarks() { - return bookmarks.size(); + return bookmarkCount; } public Publicity getPublicity() { return topicStatus.getPublicity(); } + public void removeImage() { this.topicInfo = topicInfo.removeImage(); } + public void removeBookmark(Bookmark bookmark) { + bookmarks.remove(bookmark); + bookmarkCount--; + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index 22fe220a5..28618cbba 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -1,21 +1,33 @@ package com.mapbefine.mapbefine.topic.domain; -import java.util.List; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface TopicRepository extends JpaRepository { + @EntityGraph(attributePaths = {"creator", "permissions", "bookmarks"}) + Optional findById(Long id); + + @EntityGraph(attributePaths = {"creator", "permissions"}) List findByIdIn(List ids); boolean existsById(Long id); + @EntityGraph(attributePaths = {"creator", "permissions", "bookmarks"}) + List findAll(); + + @EntityGraph(attributePaths = {"creator", "permissions", "bookmarks"}) List findAllByOrderByLastPinUpdatedAtDesc(); + @EntityGraph(attributePaths = {"creator", "permissions", "bookmarks"}) List findAllByCreatorId(Long creatorId); @Modifying(clearAutomatically = true) @@ -26,4 +38,5 @@ public interface TopicRepository extends JpaRepository { @Query("update Topic t set t.isDeleted = true where t.creator.id = :memberId") void deleteAllByMemberId(@Param("memberId") Long memberId); + List findTopicsByBookmarksMemberId(Long memberId); } diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 8069c700c..55bd81aff 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 8069c700c69d2b46b52c34817d8d4f66fe8d70b1 +Subproject commit 55bd81aff92bf46a1fdafd9573f3dea548f46f04 diff --git a/backend/src/main/resources/static/admin/bundle.js b/backend/src/main/resources/static/admin/bundle.js deleted file mode 100644 index 78540f406..000000000 --- a/backend/src/main/resources/static/admin/bundle.js +++ /dev/null @@ -1,270 +0,0 @@ -/*! For license information please see bundle.js.LICENSE.txt */ -(()=>{var e,t,n={448:(e,t,n)=>{"use strict";var r=n(294),a=n(840);function o(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n