diff --git a/backend/src/docs/asciidoc/pin.adoc b/backend/src/docs/asciidoc/pin.adoc index 050c490f..fab90bba 100644 --- a/backend/src/docs/asciidoc/pin.adoc +++ b/backend/src/docs/asciidoc/pin.adoc @@ -27,3 +27,23 @@ operation::pin-controller-test/add-image[snippets='http-request,http-response'] === 핀 이미지 삭제 operation::pin-controller-test/remove-image[snippets='http-request,http-response'] + +=== 핀 댓글 생성 + +operation::pin-controller-test/create-parent-pin-comment[snippets='http-request,http-response'] + +=== 핀 대댓글 생성 + +operation::pin-controller-test/create-child-pin-comment[snippets='http-request,http-response'] + +=== 핀 댓글 조회 + +operation::pin-controller-test/find-all-pin-comment-by-pin-id[snippets='http-request,http-response'] + +=== 핀 댓글 수정 + +operation::pin-controller-test/update-pin-comment[snippets='http-request,http-response'] + +=== 핀 댓글 삭제 + +operation::pin-controller-test/remove-pin-comment[snippets='http-request,http-response'] 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 358a481c..1cd455ed 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 @@ -3,6 +3,7 @@ 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; @Repository @@ -14,9 +15,9 @@ public interface AtlasRepository extends JpaRepository { @Modifying(clearAutomatically = true) @Query("delete from Atlas a where a.member.id = :memberId") - void deleteAllByMemberId(Long memberId); + void deleteAllByMemberId(@Param("memberId") Long memberId); @Modifying(clearAutomatically = true) @Query("delete from Atlas a where a.topic.id = :topicId") - void deleteAllByTopicId(Long topicId); + void deleteAllByTopicId(@Param("topicId") Long topicId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java index 3bfc0767..3c65bd27 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java @@ -27,6 +27,8 @@ protected AuthMember( public abstract boolean canTopicUpdate(Topic topic); public abstract boolean canPinCreateOrUpdate(Topic topic); + + public abstract boolean canPinCommentCreate(Topic topic); public Long getMemberId() { return memberId; @@ -36,4 +38,10 @@ public boolean isSameMember(Long memberId) { return Objects.equals(memberId, this.memberId); } + public abstract boolean isAdmin(); + + public abstract boolean isUser(); + + public abstract boolean isGuest(); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java index 3d97522c..73311c0a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java @@ -34,4 +34,24 @@ public boolean canPinCreateOrUpdate(Topic topic) { return true; } + @Override + public boolean canPinCommentCreate(Topic topic) { + return true; + } + + @Override + public boolean isAdmin() { + return true; + } + + @Override + public boolean isUser() { + return false; + } + + @Override + public boolean isGuest() { + return false; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java index 6c8c0edc..fb064a2f 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java @@ -36,4 +36,24 @@ public boolean canPinCreateOrUpdate(Topic topic) { return false; } + @Override + public boolean canPinCommentCreate(Topic topic) { + return false; + } + + @Override + public boolean isAdmin() { + return false; + } + + @Override + public boolean isUser() { + return false; + } + + @Override + public boolean isGuest() { + return true; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java index d2444d49..e7ebd671 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java @@ -22,12 +22,14 @@ public User( @Override public boolean canRead(Topic topic) { TopicStatus topicStatus = topic.getTopicStatus(); - return topicStatus.isPublic() || isGroup(topic.getId()); + + return topicStatus.isPublic() || hasPermission(topic.getId()); } @Override public boolean canDelete(Topic topic) { TopicStatus topicStatus = topic.getTopicStatus(); + return topicStatus.isPrivate() && isCreator(topic.getId()); } @@ -39,19 +41,36 @@ public boolean canTopicUpdate(Topic topic) { @Override public boolean canPinCreateOrUpdate(Topic topic) { TopicStatus topicStatus = topic.getTopicStatus(); + return topicStatus.isAllMembers() || hasPermission(topic.getId()); } - private boolean isCreator(Long topicId) { - return createdTopic.contains(topicId); + @Override + public boolean canPinCommentCreate(Topic topic) { + return canRead(topic); + } + + @Override + public boolean isAdmin() { + return false; } - private boolean isGroup(Long topicId) { - return isCreator(topicId) || hasPermission(topicId); + @Override + public boolean isUser() { + return true; + } + + @Override + public boolean isGuest() { + return false; + } + + private boolean isCreator(Long topicId) { + return createdTopic.contains(topicId); } private boolean hasPermission(Long topicId) { - return createdTopic.contains(topicId) || topicsWithPermission.contains(topicId); + return isCreator(topicId) || topicsWithPermission.contains(topicId); } } 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 602eb13b..f16d4079 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 @@ -4,6 +4,7 @@ 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; public interface BookmarkRepository extends JpaRepository { @@ -13,10 +14,10 @@ public interface BookmarkRepository extends JpaRepository { @Modifying(clearAutomatically = true) @Query("delete from Bookmark b where b.member.id = :memberId") - void deleteAllByMemberId(Long memberId); + void deleteAllByMemberId(@Param("memberId") Long memberId); @Modifying(clearAutomatically = true) @Query("delete from Bookmark b where b.topic.id = :topicId") - void deleteAllByTopicId(Long topicId); + void deleteAllByTopicId(@Param("topicId") Long topicId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index 6cbbae0b..3824d460 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java @@ -4,6 +4,7 @@ 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; public interface PermissionRepository extends JpaRepository { @@ -13,10 +14,10 @@ public interface PermissionRepository extends JpaRepository { @Modifying(clearAutomatically = true) @Query("delete from Permission p where p.member.id = :memberId") - void deleteAllByMemberId(Long memberId); + void deleteAllByMemberId(@Param("memberId") Long memberId); @Modifying(clearAutomatically = true) @Query("delete from Permission p where p.topic.id = :topicId") - void deleteAllByTopicId(Long topicId); + void deleteAllByTopicId(@Param("topicId") Long topicId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java index f9263c11..206fece1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java @@ -1,6 +1,10 @@ package com.mapbefine.mapbefine.pin.application; import static com.mapbefine.mapbefine.image.exception.ImageErrorCode.IMAGE_FILE_IS_NULL; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.FORBIDDEN_PIN_COMMENT_CREATE; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.FORBIDDEN_PIN_COMMENT_DELETE; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.FORBIDDEN_PIN_COMMENT_UPDATE; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.PIN_COMMENT_NOT_FOUND; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.FORBIDDEN_PIN_CREATE_OR_UPDATE; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_ID; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_IMAGE_ID; @@ -16,13 +20,19 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinImage; import com.mapbefine.mapbefine.pin.domain.PinImageRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentForbiddenException; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentNotFoundException; import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -45,29 +55,32 @@ public class PinCommandService { private static final double DUPLICATE_LOCATION_DISTANCE_METERS = 10.0; private final ApplicationEventPublisher eventPublisher; + private final ImageService imageService; private final PinRepository pinRepository; private final LocationRepository locationRepository; private final TopicRepository topicRepository; private final MemberRepository memberRepository; private final PinImageRepository pinImageRepository; - private final ImageService imageService; + private final PinCommentRepository pinCommentRepository; public PinCommandService( ApplicationEventPublisher eventPublisher, + ImageService imageService, PinRepository pinRepository, LocationRepository locationRepository, TopicRepository topicRepository, MemberRepository memberRepository, PinImageRepository pinImageRepository, - ImageService imageService + PinCommentRepository pinCommentRepository ) { + this.imageService = imageService; this.eventPublisher = eventPublisher; this.pinRepository = pinRepository; this.locationRepository = locationRepository; this.topicRepository = topicRepository; this.memberRepository = memberRepository; this.pinImageRepository = pinImageRepository; - this.imageService = imageService; + this.pinCommentRepository = pinCommentRepository; } public long save( @@ -202,4 +215,90 @@ private void validatePinCreateOrUpdate(AuthMember authMember, Topic topic) { throw new PinForbiddenException(FORBIDDEN_PIN_CREATE_OR_UPDATE); } + + public Long savePinComment(AuthMember authMember, PinCommentCreateRequest request) { + Pin pin = findPin(request.pinId()); + validatePinCommentCreate(authMember, pin); + Member member = findMember(authMember.getMemberId()); + PinComment pinComment = createPinComment( + pin, + member, + request.parentPinCommentId(), + request.content() + ); + pinCommentRepository.save(pinComment); + + return pinComment.getId(); + } + + private void validatePinCommentCreate(AuthMember authMember, Pin pin) { + if (authMember.canPinCommentCreate(pin.getTopic())) { + return; + } + + throw new PinCommentForbiddenException(FORBIDDEN_PIN_COMMENT_CREATE); + } + + private PinComment createPinComment( + Pin pin, + Member member, + Long parentPinCommentId, + String content + ) { + if (Objects.isNull(parentPinCommentId)) { + return PinComment.ofParentPinComment(pin, member, content); + } + + PinComment parentPinComment = findPinComment(parentPinCommentId); + + return PinComment.ofChildPinComment(pin, parentPinComment, member, content); + } + + public void updatePinComment( + AuthMember member, + Long pinCommentId, + PinCommentUpdateRequest request + ) { + PinComment pinComment = findPinComment(pinCommentId); + + validatePinCommentUpdate(member, pinComment); + + pinComment.updateContent(request.content()); + } + + private void validatePinCommentUpdate(AuthMember authMember, PinComment pinComment) { + if (isPinCommentCreatorOrAdmin(authMember, pinComment)) { + return; + } + + throw new PinCommentForbiddenException(FORBIDDEN_PIN_COMMENT_UPDATE); + } + + private boolean isPinCommentCreatorOrAdmin(AuthMember authMember, PinComment pinComment) { + Long creatorId = pinComment.getCreator().getId(); + + return authMember.isSameMember(creatorId) || authMember.isAdmin(); + } + + private PinComment findPinComment(Long pinCommentId) { + return pinCommentRepository.findById(pinCommentId) + .orElseThrow(() -> new PinCommentNotFoundException(PIN_COMMENT_NOT_FOUND, pinCommentId)); + } + + public void deletePinComment(AuthMember member, Long pinCommentId) { + PinComment pinComment = findPinComment(pinCommentId); + + validatePinCommentDelete(member, pinComment); + + pinCommentRepository.delete(pinComment); + } + + private void validatePinCommentDelete(AuthMember authMember, PinComment pinComment) { + if (isPinCommentCreatorOrAdmin(authMember, pinComment)) { + return; + } + + throw new PinCommentForbiddenException(FORBIDDEN_PIN_COMMENT_DELETE); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java index b2f4a1c0..746b8f06 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java @@ -5,13 +5,17 @@ import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.pin.exception.PinException.PinNotFoundException; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; +import java.util.Objects; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,8 +24,10 @@ public class PinQueryService { private final PinRepository pinRepository; + private final PinCommentRepository pinCommentRepository; - public PinQueryService(PinRepository pinRepository) { + public PinQueryService(PinRepository pinRepository, PinCommentRepository pinCommentRepository) { + this.pinCommentRepository = pinCommentRepository; this.pinRepository = pinRepository; } @@ -56,4 +62,29 @@ public List findAllPinsByMemberId(AuthMember authMember, Long membe .map(PinResponse::from) .toList(); } + + public List findAllPinCommentsByPinId(AuthMember member, Long pinId) { + Pin pin = findPin(pinId); + validateReadAuth(member, pin.getTopic()); + + List pinComments = pinCommentRepository.findAllByPinId(pinId); + + return pinComments.stream() + .map(pinComment -> PinCommentResponse.of(pinComment, isCanChangePinComment(member, pinComment))) + .toList(); + } + + private Pin findPin(Long pinId) { + return pinRepository.findById(pinId) + .orElseThrow(() -> new PinNotFoundException(PIN_NOT_FOUND, pinId)); + } + + private boolean isCanChangePinComment(AuthMember member, PinComment pinComment) { + Long creatorId = pinComment.getCreator().getId(); + + return (Objects.nonNull(member.getMemberId()) + && member.isSameMember(creatorId)) + || member.isAdmin(); + } + } 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 b205e3a5..1229f070 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 @@ -48,7 +48,7 @@ public class Pin extends BaseTimeEntity { private Topic topic; @ManyToOne - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member creator; @Column(nullable = false) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinComment.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinComment.java new file mode 100644 index 00000000..6981570d --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinComment.java @@ -0,0 +1,120 @@ +package com.mapbefine.mapbefine.pin.domain; + +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.ILLEGAL_CONTENT_LENGTH; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.ILLEGAL_CONTENT_NULL; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.ILLEGAL_PIN_COMMENT_DEPTH; +import static lombok.AccessLevel.PROTECTED; + +import com.mapbefine.mapbefine.common.entity.BaseTimeEntity; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentBadRequestException; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import java.util.Objects; +import java.util.Optional; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@NoArgsConstructor(access = PROTECTED) +@Getter +public class PinComment extends BaseTimeEntity { + + private static final int MAX_CONTENT_LENGTH = 1000; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "pin_id") + private Pin pin; + + @ManyToOne + @JoinColumn(name = "parent_pin_comment_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private PinComment parentPinComment; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member creator; + + @Lob + @Column(nullable = false, length = 1000) + private String content; + + private PinComment( + Pin pin, + PinComment parentPinComment, + Member creator, + String content + ) { + this.pin = pin; + this.parentPinComment = parentPinComment; + this.creator = creator; + this.content = content; + } + + public static PinComment ofParentPinComment( + Pin pin, + Member creator, + String content + ) { + validateContent(content); + + return new PinComment(pin, null, creator, content); + } + + public static PinComment ofChildPinComment( + Pin pin, + PinComment parentPinComment, + Member creator, + String content + ) { + validatePinCommentDepth(parentPinComment); + validateContent(content); + + + return new PinComment(pin, parentPinComment, creator, content); + } + + private static void validatePinCommentDepth(PinComment parentPinComment) { + if (parentPinComment.isParentComment()) { + return; + } + + throw new PinCommentBadRequestException(ILLEGAL_PIN_COMMENT_DEPTH); + } + + private static void validateContent(String content) { + if (Objects.isNull(content)) { + throw new PinCommentBadRequestException(ILLEGAL_CONTENT_NULL); + } + if (content.isBlank() || content.length() > MAX_CONTENT_LENGTH) { + throw new PinCommentBadRequestException(ILLEGAL_CONTENT_LENGTH); + } + } + + public void updateContent(String content) { + validateContent(content); + this.content = content; + } + + public boolean isParentComment() { + return Objects.isNull(parentPinComment); + } + + public Optional getParentPinCommentId() { + return Optional.ofNullable(parentPinComment) + .map(PinComment::getId); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinCommentRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinCommentRepository.java new file mode 100644 index 00000000..5f2256eb --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinCommentRepository.java @@ -0,0 +1,12 @@ +package com.mapbefine.mapbefine.pin.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PinCommentRepository extends JpaRepository { + + List findAllByPinId(Long pinId); + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentCreateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentCreateRequest.java new file mode 100644 index 00000000..ceb256b0 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentCreateRequest.java @@ -0,0 +1,8 @@ +package com.mapbefine.mapbefine.pin.dto.request; + +public record PinCommentCreateRequest( + Long pinId, + Long parentPinCommentId, + String content +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentUpdateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentUpdateRequest.java new file mode 100644 index 00000000..03e63609 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentUpdateRequest.java @@ -0,0 +1,6 @@ +package com.mapbefine.mapbefine.pin.dto.request; + +public record PinCommentUpdateRequest( + String content +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinCommentResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinCommentResponse.java new file mode 100644 index 00000000..8de49140 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinCommentResponse.java @@ -0,0 +1,33 @@ +package com.mapbefine.mapbefine.pin.dto.response; + +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import java.time.LocalDateTime; +import java.util.Optional; + +public record PinCommentResponse( + Long id, + String content, + String creator, + String creatorImageUrl, + Long parentPinCommentId, + boolean canChange, + LocalDateTime updatedAt +) { + + public static PinCommentResponse of(PinComment pinComment, boolean canChange) { + MemberInfo memberInfo = pinComment.getCreator().getMemberInfo(); + Optional parentPinCommentId = pinComment.getParentPinCommentId(); + + return new PinCommentResponse( + pinComment.getId(), + pinComment.getContent(), + memberInfo.getNickName(), + memberInfo.getImageUrl(), + parentPinCommentId.orElse(null), + canChange, + pinComment.getUpdatedAt() + ); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentErrorCode.java new file mode 100644 index 00000000..1cf0cb83 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentErrorCode.java @@ -0,0 +1,25 @@ +package com.mapbefine.mapbefine.pin.exception; + +import lombok.Getter; + +@Getter +public enum PinCommentErrorCode { + + ILLEGAL_CONTENT_NULL("11000", "핀 댓글의 내용은 필수로 입력해야합니다."), + ILLEGAL_CONTENT_LENGTH("11001", "핀 댓글의 내용이 최소 1 자에서 최대 1000 자여야 합니다."), + ILLEGAL_PIN_COMMENT_DEPTH("11002", "핀 대댓글에는 대댓글을 달 수 없습니다."), + FORBIDDEN_PIN_COMMENT_CREATE("11003", "핀 댓글을 추가할 권한이 없습니다."), + FORBIDDEN_PIN_COMMENT_UPDATE("11004", "핀 댓글을 수정할 권한이 없습니다."), + FORBIDDEN_PIN_COMMENT_DELETE("11005", "핀 댓글을 삭제할 권한이 없습니다."), + PIN_COMMENT_NOT_FOUND("11006", "존재하지 않는 핀 댓글입니다."), + ; + + private final String code; + private final String message; + + PinCommentErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentException.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentException.java new file mode 100644 index 00000000..c4e72923 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentException.java @@ -0,0 +1,27 @@ +package com.mapbefine.mapbefine.pin.exception; + +import com.mapbefine.mapbefine.common.exception.BadRequestException; +import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; + +public class PinCommentException { + + public static class PinCommentBadRequestException extends BadRequestException { + public PinCommentBadRequestException(PinCommentErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + + public static class PinCommentForbiddenException extends ForbiddenException { + public PinCommentForbiddenException(PinCommentErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + + public static class PinCommentNotFoundException extends ForbiddenException { + public PinCommentNotFoundException(PinCommentErrorCode errorCode, Long id) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java index fa98ffbc..a8afe8bc 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java @@ -18,10 +18,10 @@ public PinForbiddenException(PinErrorCode errorCode) { } } - public static class PinNotFoundException extends ForbiddenException { public PinNotFoundException(PinErrorCode errorCode, Long id) { super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); } } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java index bea54f7a..7effbe81 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java @@ -4,9 +4,12 @@ import com.mapbefine.mapbefine.common.interceptor.LoginRequired; import com.mapbefine.mapbefine.pin.application.PinCommandService; import com.mapbefine.mapbefine.pin.application.PinQueryService; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import java.net.URI; @@ -122,6 +125,44 @@ public ResponseEntity removeImage(AuthMember member, @PathVariable Long pi return ResponseEntity.noContent().build(); } + @LoginRequired + @PostMapping("/comments") + public ResponseEntity addPinComment(AuthMember member, @RequestBody PinCommentCreateRequest request) { + Long commentId = pinCommandService.savePinComment(member, request); + + return ResponseEntity.created(URI.create("/pins/comments/" + commentId)) + .build(); + } + + @GetMapping("/{pinId}/comments") + public ResponseEntity> findPinCommentByPinId(AuthMember member, @PathVariable Long pinId) { + List allResponse = pinQueryService.findAllPinCommentsByPinId(member, pinId); + + return ResponseEntity.ok(allResponse); + } + + @PutMapping("/comments/{pinCommentId}") + public ResponseEntity updatePinComment( + AuthMember member, + @PathVariable Long pinCommentId, + @RequestBody PinCommentUpdateRequest request + ) { + pinCommandService.updatePinComment(member, pinCommentId, request); + + return ResponseEntity.created(URI.create("/pins/comments/" + pinCommentId)) + .build(); + } + + @DeleteMapping("/comments/{pinCommentId}") + public ResponseEntity removePinComment( + AuthMember member, + @PathVariable Long pinCommentId + ) { + pinCommandService.deletePinComment(member, pinCommentId); + + return ResponseEntity.noContent().build(); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java index 739252a5..fb3fd938 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java @@ -79,8 +79,7 @@ void findAllMember() { .then().log().all() .extract(); - List memberResponses = response.as(new TypeRef<>() { - }); + List memberResponses = response.as(new TypeRef<>() {}); // then assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinCommentFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinCommentFixture.java new file mode 100644 index 00000000..a151203a --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinCommentFixture.java @@ -0,0 +1,33 @@ +package com.mapbefine.mapbefine.pin; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; + +public class PinCommentFixture { + + public static PinComment createParentComment( + Pin pin, + Member creator + ) { + return PinComment.ofParentPinComment( + pin, + creator, + "댓글" + ); + } + + public static PinComment createChildComment( + Pin pin, + Member creator, + PinComment savedParentPinComment + ) { + return PinComment.ofChildPinComment( + pin, + savedParentPinComment, + creator, + "댓글" + ); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java index 597772d1..8b6f13c6 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java @@ -8,4 +8,5 @@ public class PinImageFixture { public static PinImage create(Pin pin) { return PinImage.createPinImageAssociatedWithPin("https://example.com/image.jpg", pin); } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index 42fee1f0..b4833c74 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -14,18 +14,27 @@ 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.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.*; -import io.restassured.response.*; +import io.restassured.RestAssured; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.io.File; +import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -58,9 +67,13 @@ class PinIntegrationTest extends IntegrationTest { @Autowired private LocationRepository locationRepository; + @Autowired private PinRepository pinRepository; + @Autowired + private PinCommentRepository pinCommentRepository; + @BeforeEach void saveTopicAndLocation() { member = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.ADMIN)); @@ -313,6 +326,155 @@ void findAllPinsByMemberId_Success() { assertThat(pinResponses).hasSize(1); } + @Test + @DisplayName("핀 댓글을 조회하면 200을 반환한다.") + void findPinCommentPinId_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + Pin pin = pinRepository.findById(pinId).get(); + PinComment parentPinComment = pinCommentRepository.save( + PinCommentFixture.createParentComment(pin, member) + ); + PinComment childPinComment = pinCommentRepository.save( + PinCommentFixture.createChildComment(pin, member, parentPinComment) + ); + List expected = List.of( + PinCommentResponse.of(parentPinComment, true), + PinCommentResponse.of(childPinComment, true) + ); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/pins/ + "+ pinId + "/comments") + .then().log().all() + .extract(); + + // then + List pinCommentResponses = response.as(new TypeRef<>() {}); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(pinCommentResponses).hasSize(2); + assertThat(pinCommentResponses).usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .isEqualTo(expected); + } + + @Test + @DisplayName("핀 댓글을 생성하면 201 을 반환한다.") + void addParentPinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + PinCommentCreateRequest request = new PinCommentCreateRequest( + pinId, + null, + "댓글" + ); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().post("/pins/comments") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + @Test + @DisplayName("핀 대댓글을 생성하면 201 을 반환한다.") + void addChildPinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + Long parentPinCommentId = createParentPinComment(pinId); + PinCommentCreateRequest childPinCommentRequest = new PinCommentCreateRequest( + pinId, + parentPinCommentId, + "대댓글" + ); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(childPinCommentRequest) + .when().post("/pins/comments") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + private Long createParentPinComment(long pinId) { + PinCommentCreateRequest parentPinCommentRequest = new PinCommentCreateRequest( + pinId, + null, + "댓글" + ); + + ExtractableResponse createResponse = RestAssured.given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(parentPinCommentRequest) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().post("/pins/comments") + .then().log().all() + .extract(); + + String locationHeader = createResponse.header("Location"); + return Long.parseLong(locationHeader.split("/")[3]); + } + + @Test + @DisplayName("핀 댓글을 수정하면 201 을 반환한다.") + void updatePinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓그으으을" + ); + long pinCommentId = createParentPinComment(pinId); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().put("/pins/comments/" + pinCommentId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + @Test + @DisplayName("핀 댓글을 삭제하면 204 을 반환한다.") + void removePinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + Long parentPinCommentId = createParentPinComment(pinId); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .when().delete("/pins/comments/" + parentPinCommentId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + @Nested class EventListenerTest { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java index ce7b4227..4636eb69 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java @@ -1,5 +1,9 @@ package com.mapbefine.mapbefine.pin.application; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.ALL_MEMBERS; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.GROUP_ONLY; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PRIVATE; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PUBLIC; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; @@ -22,26 +26,41 @@ 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.pin.PinCommentFixture; +import com.mapbefine.mapbefine.pin.PinFixture; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinImage; import com.mapbefine.mapbefine.pin.domain.PinImageRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentBadRequestException; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentForbiddenException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.PermissionType; +import com.mapbefine.mapbefine.topic.domain.Publicity; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.time.LocalDateTime; import java.util.Collections; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.web.multipart.MultipartFile; @@ -69,15 +88,20 @@ class PinCommandServiceTest extends TestDatabaseContainer { private MemberRepository memberRepository; @Autowired private PinImageRepository pinImageRepository; + @Autowired + private PinCommentRepository pinCommentRepository; private Location location; private Topic topic; + private Member user; private AuthMember authMember; private PinCreateRequest createRequest; @BeforeEach void setUp() { Member member = memberRepository.save(MemberFixture.create("user1", "userfirst@naver.com", Role.ADMIN)); + user = memberRepository.save(MemberFixture.create("user2", "usersecond@naver.com", Role.USER)); + location = locationRepository.save(LocationFixture.create()); topic = topicRepository.save(TopicFixture.createByName("topic", member)); @@ -189,6 +213,7 @@ void save_FailByForbidden() { .isInstanceOf(PinForbiddenException.class); } + @Test @DisplayName("핀을 변경하면 토픽에 핀의 변경 일시를 새로 반영한다. (모든 일시는 영속화 시점 기준이다.)") void update_Success_UpdateLastPinsAddedAt() { @@ -350,4 +375,302 @@ void removeImageById_FailByForbidden() { .isInstanceOf(PinForbiddenException.class); } + @Test + @DisplayName("Guest 인 경우 핀 댓글을 생성하면 예외가 발생된다.") + void savePinComment_Fail_ByGuest() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest(savedPin.getId(), null, "댓글"); + + // when then + assertThatThrownBy(() -> pinCommandService.savePinComment(new Guest(), request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("일반 회원인 경우 공개 지도, 비공개 지도이지만 권한을 가진 지도에는 핀 댓글을 생성할 수 있다.") + void savePinComment_Success_ByCreator(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user); + topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), null, "댓글" + ); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when + Long pinCommentId = pinCommandService.savePinComment(creatorUser, request); + + // then + PinComment actual = pinCommentRepository.findById(pinCommentId).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글"); + + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("일반 회원인 경우 비공개 지도이면서 권한을 가지고 있지 않은 지도에 핀 댓글을 생성할 수 없다.") + void savePinComment_Fail_ByNonCreator() { + // given + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(user); + topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), null, "댓글" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.USER) + ); + AuthMember nonCreatorUser = MemberFixture.createUser(nonCreator); + + // when then + assertThatThrownBy(() -> pinCommandService.savePinComment(nonCreatorUser, request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("핀 대댓글에는 대댓글을 달 수 없다. (depth 2 이상)") + void savePinComment_Fail_ByIllegalDepth() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment parentPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinComment childPinComment = pinCommentRepository.save( + PinCommentFixture.createChildComment(savedPin, user, parentPinComment) + ); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), childPinComment.getId(), "대대댓글" + ); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when then + assertThatThrownBy(() -> pinCommandService.savePinComment(creatorUser, request)) + .isInstanceOf(PinCommentBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("Admin 인 경우 어떠한 유형의 지도라도 핀 댓글을 생성할 수 있다.") + void savePinComment_Success_ByAdmin(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user); + topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), null, "댓글" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("admin", "admin@naver.com", Role.ADMIN) + ); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + Long pinCommentId = pinCommandService.savePinComment(nonCreatorAdmin, request); + + // then + PinComment actual = pinCommentRepository.findById(pinCommentId).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, nonCreator, "댓글"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("Guest 인 경우 핀 댓글을 수정할 수 없다.") + void updatePinComment_Fail_ByGuest() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + + // when then + assertThatThrownBy(() -> pinCommandService.updatePinComment(new Guest(), pinComment.getId(), request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 단 핀 댓글을 수정할 수 있다.") + void updatePinComment_Success_ByCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when + pinCommandService.updatePinComment(creatorUser, pinComment.getId(), request); + + // then + PinComment actual = pinCommentRepository.findById(pinComment.getId()).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글 수정!"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 달지 않은 핀 댓글을 수정할 수 없다.") + void updatePinComment_Fail_ByNonCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.USER) + ); + AuthMember nonCreatorUser = MemberFixture.createUser(nonCreator); + + // when then + assertThatThrownBy(() -> pinCommandService.updatePinComment(nonCreatorUser, pinComment.getId(), request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("Admin 인 경우 본인이 단 핀 댓글을 수정할 수 있다.") + void updatePinComment_Success_ByAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + AuthMember creatorAdmin = new Admin(user.getId()); + + // when + pinCommandService.updatePinComment(creatorAdmin, pinComment.getId(), request); + + // then + PinComment actual = pinCommentRepository.findById(pinComment.getId()).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글 수정!"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("Admin 인 경우 본인이 달지 않은 핀 댓글을 수정할 수 있다.") + void updatePinComment_Success_ByNonCreatorAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.ADMIN) + ); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + pinCommandService.updatePinComment(nonCreatorAdmin, pinComment.getId(), request); + + // then + PinComment actual = pinCommentRepository.findById(pinComment.getId()).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글 수정!"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("Guest 인 경우 핀 댓글을 삭제할 수 없다.") + void deletePinComment_Fail_ByGuest() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + + // when then + assertThatThrownBy(() -> pinCommandService.deletePinComment(new Guest(), pinComment.getId())) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 단 핀 댓글을 삭제할 수 있다.") + void deletePinComment_Success_ByCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when + pinCommandService.deletePinComment(creatorUser, pinComment.getId()); + + // then + assertThat(pinCommentRepository.existsById(pinComment.getId())).isFalse(); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 달지 않은 핀 댓글을 삭제할 수 없다.") + void deletePinComment_Fail_ByNonCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.USER) + ); + AuthMember nonCreatorUser = MemberFixture.createUser(nonCreator); + + // when then + assertThatThrownBy(() -> pinCommandService.deletePinComment(nonCreatorUser, pinComment.getId())) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("Admin 인 경우 본인이 단 핀 댓글을 삭제할 수 있다.") + void deletePinComment_Success_ByAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + AuthMember creatorAdmin = new Admin(user.getId()); + + // when + pinCommandService.deletePinComment(creatorAdmin, pinComment.getId()); + + // then + assertThat(pinCommentRepository.existsById(pinComment.getId())).isFalse(); + } + + @Test + @DisplayName("Admin 인 경우 본인이 달지 않은 핀 댓글을 삭제할 수 있다.") + void deletePinComment_Success_ByNonCreatorAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + Member nonCreator = MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.ADMIN); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + pinCommandService.deletePinComment(nonCreatorAdmin, pinComment.getId()); + + // then + assertThat(pinCommentRepository.existsById(pinComment.getId())).isFalse(); + } + + static Stream publicAndPrivateTopicsStatus() { + return Stream.of( + Arguments.of(PUBLIC, ALL_MEMBERS), + Arguments.of(PUBLIC, GROUP_ONLY), + Arguments.of(PRIVATE, GROUP_ONLY) + ); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java index d5c1dc9f..4969730e 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java @@ -1,10 +1,15 @@ package com.mapbefine.mapbefine.pin.application; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.ALL_MEMBERS; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.GROUP_ONLY; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PRIVATE; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PUBLIC; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.auth.domain.member.Guest; import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.common.annotation.ServiceTest; import com.mapbefine.mapbefine.location.LocationFixture; @@ -14,25 +19,34 @@ 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.pin.PinCommentFixture; import com.mapbefine.mapbefine.pin.PinFixture; import com.mapbefine.mapbefine.pin.PinImageFixture; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.pin.exception.PinException.PinNotFoundException; import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.PermissionType; +import com.mapbefine.mapbefine.topic.domain.Publicity; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; -import java.util.ArrayList; -import java.util.List; - @ServiceTest class PinQueryServiceTest extends TestDatabaseContainer { @@ -46,6 +60,8 @@ class PinQueryServiceTest extends TestDatabaseContainer { private LocationRepository locationRepository; @Autowired private MemberRepository memberRepository; + @Autowired + private PinCommentRepository pinCommentRepository; private Location location; private Topic publicUser1Topic; @@ -190,4 +206,110 @@ void findAllPinsByMemberId_success() { assertThat(actual).extractingResultOf("id") .isEqualTo(pinIds); } + + @ParameterizedTest + @MethodSource("publicTopicsStatus") + @DisplayName("공개 지도인 경우, Guest 는 핀 댓글을 조회에 성공한다.") + void findAllPinCommentGuest_Success(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + PinComment savedPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + PinCommentResponse expected = PinCommentResponse.of(savedPinComment, false); + + // when + List actual = pinQueryService.findAllPinCommentsByPinId(new Guest(), savedPin.getId()); + + // then + assertThat(actual.get(0)).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + @DisplayName("비공개 지도인 경우, Guest 는 핀 댓글을 조회를 할 수 없다.") + void findAllPinCommentGuest_Fail() { + // given + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + + // when then + assertThatThrownBy(() -> pinQueryService.findAllPinCommentsByPinId(new Guest(), savedPin.getId())) + .isInstanceOf(PinForbiddenException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("일반 회원은 공개 지도인 경우와, 비공개 지도이면서 본인이 권한을 가진 지도의 핀 댓글을 조회할 수 있다.") + void findAllPinCommentUser_Success(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + PinComment savedPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + PinCommentResponse expected = PinCommentResponse.of(savedPinComment, true); + AuthMember creatorUser = MemberFixture.createUser(user1); + + // when + List actual = pinQueryService.findAllPinCommentsByPinId(creatorUser, savedPin.getId()); + + // then + assertThat(actual.get(0)).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + @DisplayName("일반 회원인 경우 비공개 지도이면서 권한을 가지지 않은 지도에 핀 댓글을 조회할 수 없다.") + void findAllPinCommentUser_Fail() { + // given + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + AuthMember nonCreatorUser = MemberFixture.createUser(user2); + + // when then + assertThatThrownBy(() -> pinQueryService.findAllPinCommentsByPinId(nonCreatorUser, savedPin.getId())) + .isInstanceOf(PinForbiddenException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("ADMIN 은 어떠한 유형의 지도의 핀 댓글을 조회할 수 있다.") + void findAllPinCommentAdmin_Success(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + PinComment savedPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + PinCommentResponse expected = PinCommentResponse.of(savedPinComment, true); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.ADMIN) + ); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + List actual = pinQueryService.findAllPinCommentsByPinId(nonCreatorAdmin, savedPin.getId()); + + // then + assertThat(actual.get(0)).usingRecursiveComparison() + .isEqualTo(expected); + } + + static Stream publicTopicsStatus() { + return Stream.of( + Arguments.of(PUBLIC, ALL_MEMBERS), + Arguments.of(PUBLIC, GROUP_ONLY) + ); + } + + static Stream publicAndPrivateTopicsStatus() { + return Stream.of( + Arguments.of(PUBLIC, ALL_MEMBERS), + Arguments.of(PUBLIC, GROUP_ONLY), + Arguments.of(PRIVATE, GROUP_ONLY) + ); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinCommentTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinCommentTest.java new file mode 100644 index 00000000..83c54e4a --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinCommentTest.java @@ -0,0 +1,96 @@ +package com.mapbefine.mapbefine.pin.domain; + +import static com.mapbefine.mapbefine.pin.domain.PinComment.ofParentPinComment; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentBadRequestException; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PinCommentTest { + + private Pin pin; + private Member creator; + + @BeforeEach + void beforeEach() { + Location location = LocationFixture.create(); + creator = MemberFixture.create("member", "member@naver.com", Role.USER); + Topic topic = TopicFixture.createPublicAndAllMembersTopic("https://imageUrl", creator); + pin = PinFixture.create(location, topic, creator); + } + + @ParameterizedTest + @MethodSource("validCommentContent") + @DisplayName("유효한 댓글 내용일 경우 핀 댓글 생성에 성공한다.") + void createPinComment_Success(String content) { + // when + PinComment pinComment = ofParentPinComment(pin, creator, content); + + // then + assertThat(pinComment.getContent()).isEqualTo(content); + } + + static Stream validCommentContent() { + return Stream.of( + Arguments.of("댓"), + Arguments.of("댓".repeat(1000)) + ); + } + + @ParameterizedTest + @MethodSource("invalidCommentContent") + @DisplayName("유효하지 않은 핀 댓글 생성에 실패한다.") + void createPinComment_Fail(String content) { + // when then + assertThatThrownBy(() -> ofParentPinComment(pin, creator, content)) + .isInstanceOf(PinCommentBadRequestException.class); + } + + static Stream invalidCommentContent() { + return Stream.of( + Arguments.of(""), + Arguments.of("댓".repeat(1001)) + ); + } + + @ParameterizedTest + @MethodSource("validCommentContent") + @DisplayName("유효한 댓글 내용일 경우 핀 댓글 내용 수정에 성공한다.") + void updatePinComment_Success(String content) { + // given + PinComment pinComment = ofParentPinComment(pin, creator, "댓글 수정 전"); + + // when + pinComment.updateContent(content); + + // then + assertThat(pinComment.getContent()).isEqualTo(content); + } + + + @ParameterizedTest + @MethodSource("invalidCommentContent") + @DisplayName("유효하지 않은 댓글 내용일 경우 핀 댓글 수정에 실패한다.") + void updatePinComment_Fail(String content) { + PinComment pinComment = ofParentPinComment(pin, creator, "댓글 수정 전"); + + // when then + assertThatThrownBy(() -> pinComment.updateContent(content)) + .isInstanceOf(PinCommentBadRequestException.class); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index 76949d4a..ff59c12c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.pin.presentation; import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.LOCATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.http.HttpMethod.POST; @@ -10,8 +11,11 @@ import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.pin.application.PinCommandService; import com.mapbefine.mapbefine.pin.application.PinQueryService; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; @@ -199,4 +203,99 @@ void findAllPinsByMemberId() throws Exception { .andDo(restDocs.document()); } + @Test + @DisplayName("핀 댓글 생성") + void createParentPinComment() throws Exception { + PinCommentCreateRequest pinCommentCreateRequest = new PinCommentCreateRequest( + 1L, + null, + "댓글" + ); + given(pinCommandService.savePinComment(any(), any())).willReturn(1L); + + mockMvc.perform(MockMvcRequestBuilders.post("/pins/comments") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .header(LOCATION, "/pins/comments/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pinCommentCreateRequest))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 생성") + void createChildPinComment() throws Exception { + PinCommentCreateRequest pinCommentCreateRequest = new PinCommentCreateRequest( + 1L, + 1L, + "댓글" + ); + given(pinCommandService.savePinComment(any(), any())).willReturn(1L); + + mockMvc.perform(MockMvcRequestBuilders.post("/pins/comments") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .header(LOCATION, "/pins/comments/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pinCommentCreateRequest))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 조회") + void findAllPinCommentByPinId() throws Exception { + List pinCommentResponses = List.of( + new PinCommentResponse( + 1L, + "댓글", + "creator", + "https://creatorImageUrl", + null, + true, + LocalDateTime.now() + ), + new PinCommentResponse( + 2L, + "대댓글", + "creator", + "https://creatorImageUrl", + 1L, + true, + LocalDateTime.now() + ) + ); + given(pinQueryService.findAllPinCommentsByPinId(any(), any())).willReturn(pinCommentResponses); + + mockMvc.perform(MockMvcRequestBuilders.get("/pins/1/comments") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .contentType(APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 수정") + void updatePinComment() throws Exception { + PinCommentUpdateRequest pinCommentUpdateRequest = new PinCommentUpdateRequest( + "댓글 수정" + ); + + mockMvc.perform(MockMvcRequestBuilders.put("/pins/comments/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .header(LOCATION, "/pins/comments/1") + .contentType(APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(pinCommentUpdateRequest))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 삭제") + void removePinComment() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.delete("/pins/comments/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java index 9bad3ece..d364100c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java @@ -9,11 +9,27 @@ import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import java.util.List; +import org.apache.http.conn.util.PublicSuffixList; public class TopicFixture { private static final String IMAGE_URL = "https://map-befine-official.github.io/favicon.png"; + public static Topic createByPublicityAndPermissionTypeAndCreator( + Publicity publicity, + PermissionType permissionType, + Member creator + ) { + return Topic.createTopicAssociatedWithCreator( + "토픽", + "토픽의 Publicity, PermissionType 이 동적으로 정해집니다.", + IMAGE_URL, + publicity, + permissionType, + creator + ); + } + public static Topic createPrivateAndGroupOnlyTopic(Member member) { return Topic.createTopicAssociatedWithCreator( "토픽 회원만 읽을 수 있는 토픽", @@ -36,6 +52,17 @@ public static Topic createPublicAndAllMembersTopic(Member member) { ); } + public static Topic createPublicAndGroupOnlyTopic(Member member) { + return Topic.createTopicAssociatedWithCreator( + "아무나 읽을 수 있는 토픽", + "아무나 읽지만 아무나 생성할 수는 없습니다.", + IMAGE_URL, + Publicity.PUBLIC, + PermissionType.GROUP_ONLY, + member + ); + } + public static Topic createPublicAndAllMembersTopic(String imageUrl, Member member) { return Topic.createTopicAssociatedWithCreator( "아무나 읽을 수 있는 토픽",