diff --git a/backend/src/main/java/com/happy/friendogly/common/ErrorCode.java b/backend/src/main/java/com/happy/friendogly/common/ErrorCode.java index 7705d6170..ee8d14529 100644 --- a/backend/src/main/java/com/happy/friendogly/common/ErrorCode.java +++ b/backend/src/main/java/com/happy/friendogly/common/ErrorCode.java @@ -7,6 +7,8 @@ public enum ErrorCode { INVALID_FIREBASE_CREDENTIALS, FILE_SIZE_EXCEED, NOT_ALLOW_OTHER_FOOTPRINT_CHANGE, + OVERLAP_PLAYGROUND_CREATION, + NO_PARTICIPATING_PLAYGROUND, + ALREADY_PARTICIPATE_PLAYGROUND, UNKNOWN_EXCEPTION, - ; } diff --git a/backend/src/main/java/com/happy/friendogly/playground/controller/PlaygroundController.java b/backend/src/main/java/com/happy/friendogly/playground/controller/PlaygroundController.java index c3ec3d292..80ad5cb4a 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/controller/PlaygroundController.java +++ b/backend/src/main/java/com/happy/friendogly/playground/controller/PlaygroundController.java @@ -4,17 +4,21 @@ import com.happy.friendogly.common.ApiResponse; import com.happy.friendogly.playground.dto.request.SavePlaygroundRequest; import com.happy.friendogly.playground.dto.request.UpdatePlaygroundArrivalRequest; +import com.happy.friendogly.playground.dto.request.UpdatePlaygroundMemberMessageRequest; import com.happy.friendogly.playground.dto.response.FindPlaygroundDetailResponse; import com.happy.friendogly.playground.dto.response.FindPlaygroundLocationResponse; import com.happy.friendogly.playground.dto.response.FindPlaygroundSummaryResponse; +import com.happy.friendogly.playground.dto.response.SaveJoinPlaygroundMemberResponse; import com.happy.friendogly.playground.dto.response.SavePlaygroundResponse; import com.happy.friendogly.playground.dto.response.UpdatePlaygroundArrivalResponse; +import com.happy.friendogly.playground.dto.response.UpdatePlaygroundMemberMessageResponse; import com.happy.friendogly.playground.service.PlaygroundCommandService; import com.happy.friendogly.playground.service.PlaygroundQueryService; import jakarta.validation.Valid; import java.net.URI; import java.util.List; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -54,9 +58,9 @@ public ApiResponse findById(@Auth Long memberId, @ return ApiResponse.ofSuccess(response); } - @GetMapping("/{id}/summary") - public ApiResponse findSummaryById(@PathVariable Long id) { - return ApiResponse.ofSuccess(new FindPlaygroundSummaryResponse(1L, 10, 4)); + @GetMapping("/{playgroundId}/summary") + public ApiResponse findSummaryById(@PathVariable Long playgroundId) { + return ApiResponse.ofSuccess(playgroundQueryService.findSummary(playgroundId)); } @GetMapping("/locations") @@ -69,6 +73,33 @@ public ApiResponse updateArrival( @Auth Long memberId, @Valid @RequestBody UpdatePlaygroundArrivalRequest request ) { - return ApiResponse.ofSuccess(new UpdatePlaygroundArrivalResponse(true)); + return ApiResponse.ofSuccess(playgroundCommandService.updateArrival(request, memberId)); + } + + @PostMapping("/{playgroundId}/join") + public ApiResponse saveJoinMember( + @Auth Long memberId, + @PathVariable Long playgroundId + ) { + SaveJoinPlaygroundMemberResponse response = playgroundCommandService + .joinPlayground(memberId, playgroundId); + return ApiResponse.ofSuccess(response); + } + + @DeleteMapping("/leave") + public ResponseEntity deleteJoinMember(@Auth Long memberId) { + playgroundCommandService.leavePlayground(memberId); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/message") + public ApiResponse updateMessage( + @Auth Long memberId, + @RequestBody UpdatePlaygroundMemberMessageRequest request + ) { + UpdatePlaygroundMemberMessageResponse response = playgroundCommandService.updateMemberMessage( + request, memberId + ); + return ApiResponse.ofSuccess(response); } } diff --git a/backend/src/main/java/com/happy/friendogly/playground/domain/Location.java b/backend/src/main/java/com/happy/friendogly/playground/domain/Location.java index d8a7a6b66..18a0c1779 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/domain/Location.java +++ b/backend/src/main/java/com/happy/friendogly/playground/domain/Location.java @@ -1,5 +1,7 @@ package com.happy.friendogly.playground.domain; +import static com.happy.friendogly.playground.domain.Playground.MAX_OVERLAP_DISTANCE; + import com.happy.friendogly.exception.FriendoglyException; import com.happy.friendogly.utils.GeoCalculator; import jakarta.persistence.Column; @@ -45,21 +47,23 @@ public boolean isWithin(Location other, int radius) { return distance <= radius; } - public Location plusLatitudeByMeters(int meters) { - double diffLatitude = GeoCalculator.calculateLatitudeOffset(this.latitude, meters); - return new Location(diffLatitude, this.longitude); + public Location plusLatitudeByOverlapDistance() { + double diffLatitude = GeoCalculator.calculateLatitudeOffset(latitude, MAX_OVERLAP_DISTANCE); + return new Location(diffLatitude, longitude); } - public Location minusLatitudeByMeters(int meters) { - return plusLatitudeByMeters(-meters); + public Location minusLatitudeByOverlapDistance() { + double diffLatitude = GeoCalculator.calculateLatitudeOffset(latitude, -MAX_OVERLAP_DISTANCE); + return new Location(diffLatitude, longitude); } - public Location plusLongitudeByMeters(int meters) { - double diffLongitude = GeoCalculator.calculateLongitudeOffset(this.latitude, this.longitude, meters); - return new Location(this.latitude, diffLongitude); + public Location plusLongitudeByOverlapDistance() { + double diffLongitude = GeoCalculator.calculateLongitudeOffset(latitude, longitude, MAX_OVERLAP_DISTANCE); + return new Location(latitude, diffLongitude); } - public Location minusLongitudeByMeters(int meters) { - return plusLongitudeByMeters(-meters); + public Location minusLongitudeByOverlapDistance() { + double diffLongitude = GeoCalculator.calculateLongitudeOffset(latitude, longitude, -MAX_OVERLAP_DISTANCE); + return new Location(latitude, diffLongitude); } } diff --git a/backend/src/main/java/com/happy/friendogly/playground/domain/Playground.java b/backend/src/main/java/com/happy/friendogly/playground/domain/Playground.java index d9eabd365..0471672e9 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/domain/Playground.java +++ b/backend/src/main/java/com/happy/friendogly/playground/domain/Playground.java @@ -14,6 +14,9 @@ @Getter public class Playground { + protected static final int PLAYGROUND_RADIUS = 150; + protected static final int MAX_OVERLAP_DISTANCE = PLAYGROUND_RADIUS * 2; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -24,4 +27,12 @@ public class Playground { public Playground(Location location) { this.location = location; } + + public boolean isInsideBoundary(Location location) { + return this.location.isWithin(location, PLAYGROUND_RADIUS); + } + + public boolean isOverlapLocation(Location location) { + return this.location.isWithin(location, MAX_OVERLAP_DISTANCE); + } } diff --git a/backend/src/main/java/com/happy/friendogly/playground/domain/PlaygroundMember.java b/backend/src/main/java/com/happy/friendogly/playground/domain/PlaygroundMember.java index 3692e4a6e..c325de64c 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/domain/PlaygroundMember.java +++ b/backend/src/main/java/com/happy/friendogly/playground/domain/PlaygroundMember.java @@ -32,7 +32,7 @@ public class PlaygroundMember { @JoinColumn(name = "member_id", nullable = false) private Member member; - @Column(name = "message") + @Column(name = "message", nullable = false) private String message; @Column(name = "is_inside", nullable = false) @@ -59,7 +59,7 @@ public PlaygroundMember( Playground playground, Member member ) { - this(playground, member, null, false, null); + this(playground, member, "", false, null); } public boolean equalsMemberId(Long memberId) { @@ -69,4 +69,16 @@ public boolean equalsMemberId(Long memberId) { public boolean isSamePlayground(Playground playground) { return this.playground.getId().equals(playground.getId()); } + + public void updateIsInside(boolean isInside) { + this.isInside = isInside; + } + + public void updateMessage(String message) { + this.message = message; + } + + public void updateExitTime(LocalDateTime exitTime) { + this.exitTime = exitTime; + } } diff --git a/backend/src/main/java/com/happy/friendogly/playground/dto/request/UpdatePlaygroundMemberMessageRequest.java b/backend/src/main/java/com/happy/friendogly/playground/dto/request/UpdatePlaygroundMemberMessageRequest.java new file mode 100644 index 000000000..4855368e8 --- /dev/null +++ b/backend/src/main/java/com/happy/friendogly/playground/dto/request/UpdatePlaygroundMemberMessageRequest.java @@ -0,0 +1,11 @@ +package com.happy.friendogly.playground.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record UpdatePlaygroundMemberMessageRequest( + + @NotBlank(message = "상태메세지는 빈 문자열이나 null을 입력할 수 없습니다.") + String message +) { + +} diff --git a/backend/src/main/java/com/happy/friendogly/playground/dto/response/FindPlaygroundSummaryResponse.java b/backend/src/main/java/com/happy/friendogly/playground/dto/response/FindPlaygroundSummaryResponse.java index 35d2e46ba..a33589d28 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/dto/response/FindPlaygroundSummaryResponse.java +++ b/backend/src/main/java/com/happy/friendogly/playground/dto/response/FindPlaygroundSummaryResponse.java @@ -1,10 +1,13 @@ package com.happy.friendogly.playground.dto.response; +import java.util.List; + public record FindPlaygroundSummaryResponse( - Long id, + Long playgroundId, int totalPetCount, - int arrivedPetCount + int arrivedPetCount, + List petImageUrls ) { } diff --git a/backend/src/main/java/com/happy/friendogly/playground/dto/response/SaveJoinPlaygroundMemberResponse.java b/backend/src/main/java/com/happy/friendogly/playground/dto/response/SaveJoinPlaygroundMemberResponse.java new file mode 100644 index 000000000..7f3a67c4f --- /dev/null +++ b/backend/src/main/java/com/happy/friendogly/playground/dto/response/SaveJoinPlaygroundMemberResponse.java @@ -0,0 +1,24 @@ +package com.happy.friendogly.playground.dto.response; + +import com.happy.friendogly.playground.domain.PlaygroundMember; +import java.time.LocalDateTime; + +public record SaveJoinPlaygroundMemberResponse( + Long playgroundId, + Long memberId, + String message, + boolean isArrived, + LocalDateTime exitTime +) { + public SaveJoinPlaygroundMemberResponse(PlaygroundMember playgroundMember) { + this( + playgroundMember.getPlayground().getId(), + playgroundMember.getMember().getId(), + playgroundMember.getMessage(), + playgroundMember.isInside(), + playgroundMember.getExitTime() + ); + } +} + + diff --git a/backend/src/main/java/com/happy/friendogly/playground/dto/response/UpdatePlaygroundMemberMessageResponse.java b/backend/src/main/java/com/happy/friendogly/playground/dto/response/UpdatePlaygroundMemberMessageResponse.java new file mode 100644 index 000000000..6077d9129 --- /dev/null +++ b/backend/src/main/java/com/happy/friendogly/playground/dto/response/UpdatePlaygroundMemberMessageResponse.java @@ -0,0 +1,7 @@ +package com.happy.friendogly.playground.dto.response; + +public record UpdatePlaygroundMemberMessageResponse( + String message +) { + +} diff --git a/backend/src/main/java/com/happy/friendogly/playground/repository/PlaygroundMemberRepository.java b/backend/src/main/java/com/happy/friendogly/playground/repository/PlaygroundMemberRepository.java index f23f77d6d..731fa99a6 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/repository/PlaygroundMemberRepository.java +++ b/backend/src/main/java/com/happy/friendogly/playground/repository/PlaygroundMemberRepository.java @@ -1,19 +1,39 @@ package com.happy.friendogly.playground.repository; +import com.happy.friendogly.common.ErrorCode; +import com.happy.friendogly.exception.FriendoglyException; import com.happy.friendogly.playground.domain.PlaygroundMember; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.http.HttpStatus; public interface PlaygroundMemberRepository extends JpaRepository { @EntityGraph(attributePaths = "member") List findAllByPlaygroundId(Long playgroundId); + @EntityGraph(attributePaths = "member") + List findAllByPlaygroundIdOrderByIsInsideDesc(Long playgroundId); + boolean existsByPlaygroundIdAndMemberId(Long playgroundId, Long memberId); boolean existsByMemberId(Long memberId); + @EntityGraph(attributePaths = {"playground", "member"}) Optional findByMemberId(Long memberId); + + default PlaygroundMember getByMemberId(Long memberId) { + return findByMemberId(memberId) + .orElseThrow( + () -> new FriendoglyException( + "멤버가 참여한 놀이터가 없습니다.", + ErrorCode.NO_PARTICIPATING_PLAYGROUND, + HttpStatus.BAD_REQUEST + ) + ); + } + + boolean existsByPlaygroundId(Long playgroundId); } diff --git a/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundCommandService.java b/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundCommandService.java index 1925b5856..15412a21b 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundCommandService.java +++ b/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundCommandService.java @@ -1,5 +1,9 @@ package com.happy.friendogly.playground.service; +import static com.happy.friendogly.common.ErrorCode.OVERLAP_PLAYGROUND_CREATION; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +import com.happy.friendogly.common.ErrorCode; import com.happy.friendogly.exception.FriendoglyException; import com.happy.friendogly.member.domain.Member; import com.happy.friendogly.member.repository.MemberRepository; @@ -7,9 +11,15 @@ import com.happy.friendogly.playground.domain.Playground; import com.happy.friendogly.playground.domain.PlaygroundMember; import com.happy.friendogly.playground.dto.request.SavePlaygroundRequest; +import com.happy.friendogly.playground.dto.request.UpdatePlaygroundArrivalRequest; +import com.happy.friendogly.playground.dto.request.UpdatePlaygroundMemberMessageRequest; +import com.happy.friendogly.playground.dto.response.SaveJoinPlaygroundMemberResponse; import com.happy.friendogly.playground.dto.response.SavePlaygroundResponse; +import com.happy.friendogly.playground.dto.response.UpdatePlaygroundArrivalResponse; +import com.happy.friendogly.playground.dto.response.UpdatePlaygroundMemberMessageResponse; import com.happy.friendogly.playground.repository.PlaygroundMemberRepository; import com.happy.friendogly.playground.repository.PlaygroundRepository; +import java.time.LocalDateTime; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,9 +28,6 @@ @Transactional public class PlaygroundCommandService { - private static final int PLAYGROUND_RADIUS = 150; - private static final int MAX_NON_OVERLAP_DISTANCE = PLAYGROUND_RADIUS * 2; - private final PlaygroundRepository playgroundRepository; private final PlaygroundMemberRepository playgroundMemberRepository; private final MemberRepository memberRepository; @@ -50,16 +57,16 @@ public SavePlaygroundResponse save(SavePlaygroundRequest request, Long memberId) private void validateExistParticipatingPlayground(Member member) { if (playgroundMemberRepository.existsByMemberId(member.getId())) { - throw new FriendoglyException("이미 참여한 놀이터가 존재합니다."); + throw new FriendoglyException("이미 참여한 놀이터가 존재합니다.", ErrorCode.ALREADY_PARTICIPATE_PLAYGROUND, BAD_REQUEST); } } private void validateOverlapPlayground(double latitude, double longitude) { Location location = new Location(latitude, longitude); - Location startLatitudeLocation = location.minusLatitudeByMeters(MAX_NON_OVERLAP_DISTANCE); - Location endLatitudeLocation = location.plusLatitudeByMeters(MAX_NON_OVERLAP_DISTANCE); - Location startLongitudeLocation = location.minusLongitudeByMeters(MAX_NON_OVERLAP_DISTANCE); - Location endLongitudeLocation = location.plusLongitudeByMeters(MAX_NON_OVERLAP_DISTANCE); + Location startLatitudeLocation = location.minusLatitudeByOverlapDistance(); + Location endLatitudeLocation = location.plusLatitudeByOverlapDistance(); + Location startLongitudeLocation = location.minusLongitudeByOverlapDistance(); + Location endLongitudeLocation = location.plusLongitudeByOverlapDistance(); List playgrounds = playgroundRepository.findAllByLatitudeBetweenAndLongitudeBetween( startLatitudeLocation.getLatitude(), @@ -69,11 +76,65 @@ private void validateOverlapPlayground(double latitude, double longitude) { ); boolean isExistWithinRadius = playgrounds.stream() - .anyMatch(playground -> location.isWithin(playground.getLocation(), - MAX_NON_OVERLAP_DISTANCE)); + .anyMatch(playground -> playground.isOverlapLocation(location)); if (isExistWithinRadius) { - throw new FriendoglyException("생성할 놀이터 범위내에 겹치는 다른 놀이터 범위가 있습니다."); + throw new FriendoglyException( + "생성할 놀이터 범위내에 겹치는 다른 놀이터 범위가 있습니다.", + OVERLAP_PLAYGROUND_CREATION, + BAD_REQUEST + ); } } + + public SaveJoinPlaygroundMemberResponse joinPlayground(Long memberId, Long playgroundId) { + Playground playground = playgroundRepository.getById(playgroundId); + Member member = memberRepository.getById(memberId); + + validateExistParticipatingPlayground(member); + + PlaygroundMember playgroundMember = playgroundMemberRepository.save( + new PlaygroundMember(playground, member) + ); + return new SaveJoinPlaygroundMemberResponse(playgroundMember); + } + + public void leavePlayground(Long memberId) { + playgroundMemberRepository.findByMemberId(memberId) + .ifPresent(playgroundMember -> { + playgroundMemberRepository.delete(playgroundMember); + deletePlaygroundConditional(playgroundMember.getPlayground()); + }); + } + + private void deletePlaygroundConditional(Playground playground) { + if (!playgroundMemberRepository.existsByPlaygroundId(playground.getId())) { + playgroundRepository.delete(playground); + } + } + + public UpdatePlaygroundArrivalResponse updateArrival(UpdatePlaygroundArrivalRequest request, Long memberId) { + PlaygroundMember playgroundMember = playgroundMemberRepository.getByMemberId(memberId); + Playground playground = playgroundMember.getPlayground(); + + Location location = new Location(request.latitude(), request.longitude()); + boolean isInsideBoundary = playground.isInsideBoundary(location); + + if (!isInsideBoundary) { + playgroundMember.updateExitTime(LocalDateTime.now()); + } + + playgroundMember.updateIsInside(isInsideBoundary); + + return new UpdatePlaygroundArrivalResponse(isInsideBoundary); + } + + public UpdatePlaygroundMemberMessageResponse updateMemberMessage( + UpdatePlaygroundMemberMessageRequest request, + Long memberId + ) { + PlaygroundMember playgroundMember = playgroundMemberRepository.getByMemberId(memberId); + playgroundMember.updateMessage(request.message()); + return new UpdatePlaygroundMemberMessageResponse(playgroundMember.getMessage()); + } } diff --git a/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundQueryService.java b/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundQueryService.java index c84674b90..0cc18267f 100644 --- a/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundQueryService.java +++ b/backend/src/main/java/com/happy/friendogly/playground/service/PlaygroundQueryService.java @@ -7,10 +7,12 @@ import com.happy.friendogly.playground.domain.PlaygroundMember; import com.happy.friendogly.playground.dto.response.FindPlaygroundDetailResponse; import com.happy.friendogly.playground.dto.response.FindPlaygroundLocationResponse; +import com.happy.friendogly.playground.dto.response.FindPlaygroundSummaryResponse; import com.happy.friendogly.playground.dto.response.detail.PlaygroundPetDetail; import com.happy.friendogly.playground.repository.PlaygroundMemberRepository; import com.happy.friendogly.playground.repository.PlaygroundRepository; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Service; @@ -20,6 +22,8 @@ @Transactional(readOnly = true) public class PlaygroundQueryService { + private static final int MAX_PET_PREVIEW_IMAGE_COUNT = 5; + private final PlaygroundRepository playgroundRepository; private final PlaygroundMemberRepository playgroundMemberRepository; private final PetRepository petRepository; @@ -56,6 +60,11 @@ public FindPlaygroundDetailResponse findDetail(Long callMemberId, Long playgroun ); } + playgroundPetDetails.sort(Comparator + .comparing(PlaygroundPetDetail::isMine).reversed() + .thenComparing(Comparator.comparing(PlaygroundPetDetail::isArrival).reversed()) + ); + boolean isParticipating = playgroundMembers.stream() .anyMatch(playgroundMember -> playgroundMember.equalsMemberId(callMemberId)); @@ -91,4 +100,41 @@ public List findLocations(Long memberId) { .map(playground -> new FindPlaygroundLocationResponse(playground, false)) .toList(); } + + public FindPlaygroundSummaryResponse findSummary(Long playgroundId) { + List playgroundMembers = playgroundMemberRepository + .findAllByPlaygroundIdOrderByIsInsideDesc(playgroundId); + + int totalPetCount = 0; + int arrivedPetCount = 0; + List petImages = new ArrayList<>(); + + for (PlaygroundMember playgroundMember : playgroundMembers) { + List pets = petRepository.findByMemberId(playgroundMember.getMember().getId()); + + totalPetCount += pets.size(); + arrivedPetCount += getArrivedPetCount(playgroundMember, pets); + petImages.addAll( + pets.stream() + .map(Pet::getImageUrl) + .toList() + ); + } + + petImages = cutPetImagesCount(petImages); + + return new FindPlaygroundSummaryResponse( + playgroundId, + totalPetCount, + arrivedPetCount, + petImages + ); + } + + private List cutPetImagesCount(List petImageUrls) { + if (petImageUrls.size() > MAX_PET_PREVIEW_IMAGE_COUNT) { + return petImageUrls.subList(0, MAX_PET_PREVIEW_IMAGE_COUNT); + } + return petImageUrls; + } } diff --git a/backend/src/test/java/com/happy/friendogly/docs/PlaygroundApiDocsTest.java b/backend/src/test/java/com/happy/friendogly/docs/PlaygroundApiDocsTest.java index 0323fb033..51014213e 100644 --- a/backend/src/test/java/com/happy/friendogly/docs/PlaygroundApiDocsTest.java +++ b/backend/src/test/java/com/happy/friendogly/docs/PlaygroundApiDocsTest.java @@ -1,12 +1,15 @@ package com.happy.friendogly.docs; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.headerWithName; import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -20,11 +23,19 @@ import com.happy.friendogly.pet.domain.Pet; import com.happy.friendogly.pet.domain.SizeType; import com.happy.friendogly.playground.controller.PlaygroundController; +import com.happy.friendogly.playground.domain.Location; +import com.happy.friendogly.playground.domain.Playground; +import com.happy.friendogly.playground.domain.PlaygroundMember; import com.happy.friendogly.playground.dto.request.SavePlaygroundRequest; import com.happy.friendogly.playground.dto.request.UpdatePlaygroundArrivalRequest; +import com.happy.friendogly.playground.dto.request.UpdatePlaygroundMemberMessageRequest; import com.happy.friendogly.playground.dto.response.FindPlaygroundDetailResponse; import com.happy.friendogly.playground.dto.response.FindPlaygroundLocationResponse; +import com.happy.friendogly.playground.dto.response.FindPlaygroundSummaryResponse; +import com.happy.friendogly.playground.dto.response.SaveJoinPlaygroundMemberResponse; import com.happy.friendogly.playground.dto.response.SavePlaygroundResponse; +import com.happy.friendogly.playground.dto.response.UpdatePlaygroundArrivalResponse; +import com.happy.friendogly.playground.dto.response.UpdatePlaygroundMemberMessageResponse; import com.happy.friendogly.playground.dto.response.detail.PlaygroundPetDetail; import com.happy.friendogly.playground.service.PlaygroundCommandService; import com.happy.friendogly.playground.service.PlaygroundQueryService; @@ -89,6 +100,10 @@ void save() throws Exception { @DisplayName("놀이터도착 유무 업데이트") @Test void updateArrival() throws Exception { + + when(playgroundCommandService.updateArrival(any(), anyLong())) + .thenReturn(new UpdatePlaygroundArrivalResponse(true)); + UpdatePlaygroundArrivalRequest request = new UpdatePlaygroundArrivalRequest(37.5173316, 127.1011661); mockMvc .perform(patch("/playgrounds/arrival") @@ -236,8 +251,19 @@ void findAllLocation() throws Exception { @DisplayName("놀이터의 참여 현황 요약 정보를 조회") @Test void findSummary() throws Exception { + + FindPlaygroundSummaryResponse response = new FindPlaygroundSummaryResponse( + 1L, + 3, + 1, + List.of("http://image1.jpg", "http://image2.jpg", "http://image3.jpg") + ); + + when(playgroundQueryService.findSummary(anyLong())) + .thenReturn(response); + mockMvc - .perform(get("/playgrounds/{id}/summary", 1L)) + .perform(get("/playgrounds/{playgroundId}/summary", 1L)) .andDo(document("playgrounds/summary", getDocumentRequest(), getDocumentResponse(), @@ -246,9 +272,10 @@ void findSummary() throws Exception { .summary("놀이터 참여 현황 요약 조회 API") .responseFields( fieldWithPath("isSuccess").description("응답 성공 여부"), - fieldWithPath("data.id").description("놀이터의 ID"), + fieldWithPath("data.playgroundId").description("놀이터의 ID"), fieldWithPath("data.totalPetCount").description("전체 참여 강아지 수"), - fieldWithPath("data.arrivedPetCount").description("현재 홯성화 강아지 수") + fieldWithPath("data.arrivedPetCount").description("현재 홯성화 강아지 수"), + fieldWithPath("data.petImageUrls").description("참여중인 강아지 이미지 url") ) .responseSchema(Schema.schema("PlaygroundSummaryResponse")) .build() @@ -257,6 +284,106 @@ void findSummary() throws Exception { .andExpect(status().isOk()); } + @DisplayName("놀이터 참여합니다.") + @Test + void joinPlayground() throws Exception { + + PlaygroundMember playgroundMember = new PlaygroundMember( + new Playground(new Location(37.5173316, 127.1011661)), + new Member("name", "tag", "imageUrl") + ); + SaveJoinPlaygroundMemberResponse response = new SaveJoinPlaygroundMemberResponse(playgroundMember); + + when(playgroundCommandService.joinPlayground(anyLong(), anyLong())) + .thenReturn(response); + + mockMvc + .perform(post("/playgrounds/{playgroundId}/join", 1) + .header(HttpHeaders.AUTHORIZATION, getMemberToken())) + .andExpect(status().isOk()) + .andDo(document("playgrounds/saveJoinPlaygroundMember", + getDocumentRequest(), + getDocumentResponse(), + resource(ResourceSnippetParameters.builder() + .tag("Playground API") + .summary("놀이터 참여 API") + .requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("로그인한 회원의 access token") + ) + .responseFields( + fieldWithPath("isSuccess").description("응답 성공 여부"), + fieldWithPath("data.playgroundId").description("놀이터의 ID"), + fieldWithPath("data.memberId").description("멤버의 ID"), + fieldWithPath("data.message").description("멤버의 상태 메세지"), + fieldWithPath("data.isArrived").description("멤버의 놀이터 도착 유무"), + fieldWithPath("data.exitTime").description("멤버의 놀이터 나간 시간") + ) + .responseSchema(Schema.schema("SavePlaygroundResponse")) + .build() + ) + )) + .andExpect(status().isOk()); + } + + @DisplayName("놀이터를 나간다") + @Test + void leavePlayground() throws Exception { + doNothing().when(playgroundCommandService).leavePlayground(anyLong()); + + mockMvc + .perform(delete("/playgrounds/leave") + .header(HttpHeaders.AUTHORIZATION, getMemberToken())) + .andExpect(status().isNoContent()) + .andDo(document("playgrounds/leavePlayground", + getDocumentRequest(), + getDocumentResponse(), + resource(ResourceSnippetParameters.builder() + .tag("Playground API") + .summary("놀이터 나가기 API") + .requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("로그인한 회원의 access token") + ) + .build() + ) + )) + .andExpect(status().isNoContent()); + } + + @DisplayName("놀이터에 참여한 멤버 메세지 수정") + @Test + void updateMessage() throws Exception { + + when(playgroundCommandService.updateMemberMessage(any(), anyLong())) + .thenReturn(new UpdatePlaygroundMemberMessageResponse("update")); + + UpdatePlaygroundMemberMessageRequest request = new UpdatePlaygroundMemberMessageRequest("update"); + + mockMvc + .perform(patch("/playgrounds/message") + .header(HttpHeaders.AUTHORIZATION, getMemberToken()) + .content(objectMapper.writeValueAsString(request)) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("playgrounds/message", + getDocumentRequest(), + getDocumentResponse(), + resource(ResourceSnippetParameters.builder() + .tag("Playground API") + .summary("멤버 메세지 수정 API") + .requestFields( + fieldWithPath("message").description("수정할 메세지") + ) + .responseFields( + fieldWithPath("isSuccess").description("응답 성공 여부"), + fieldWithPath("data.message").description("수정된 메세지") + ) + .responseSchema(Schema.schema("UpdatePlaygroundMemberMessageResponse")) + .build() + ) + )) + .andExpect(status().isOk()); + } + @Override protected Object controller() { return new PlaygroundController(playgroundCommandService, playgroundQueryService); diff --git a/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundCommandServiceTest.java b/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundCommandServiceTest.java index f5b4548ba..a8a461770 100644 --- a/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundCommandServiceTest.java +++ b/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundCommandServiceTest.java @@ -6,14 +6,20 @@ import com.happy.friendogly.exception.FriendoglyException; import com.happy.friendogly.member.domain.Member; +import com.happy.friendogly.playground.domain.Location; import com.happy.friendogly.playground.domain.Playground; import com.happy.friendogly.playground.domain.PlaygroundMember; import com.happy.friendogly.playground.dto.request.SavePlaygroundRequest; +import com.happy.friendogly.playground.dto.request.UpdatePlaygroundArrivalRequest; +import com.happy.friendogly.playground.dto.request.UpdatePlaygroundMemberMessageRequest; import com.happy.friendogly.playground.dto.response.SavePlaygroundResponse; +import com.happy.friendogly.playground.dto.response.UpdatePlaygroundArrivalResponse; +import com.happy.friendogly.playground.dto.response.UpdatePlaygroundMemberMessageResponse; import com.happy.friendogly.utils.GeoCalculator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; class PlaygroundCommandServiceTest extends PlaygroundServiceTest { @@ -103,4 +109,154 @@ void throwExceptionWhenOverlapPlaygroundScopeWithLatitudeDiff() { .isInstanceOf(FriendoglyException.class) .hasMessage("생성할 놀이터 범위내에 겹치는 다른 놀이터 범위가 있습니다."); } + + @DisplayName("놀이터 참여시, 이미 참여한 놀이터가 존재하면 예외가 발생한다.") + @Test + void throwExceptionWhenAlreadyJoinPlayground() { + // given + Member member = saveMember("김도선"); + double latitude = 37.516382; + double longitude = 127.120040; + Playground playground = savePlayground(latitude, longitude); + Playground secondPlayground = savePlayground(GeoCalculator.calculateLatitudeOffset(latitude, 500), longitude); + playgroundMemberRepository.save( + new PlaygroundMember( + playground, + member + ) + ); + + // when, then + assertThatThrownBy(() -> playgroundCommandService.joinPlayground(member.getId(), secondPlayground.getId())) + .isInstanceOf(FriendoglyException.class) + .hasMessage("이미 참여한 놀이터가 존재합니다."); + } + + @DisplayName("놀이터를 나갈 수 있다") + @Test + void leavePlayground() { + // given + Member member = saveMember("김도선"); + Playground playground = savePlayground(); + PlaygroundMember playgroundMember = playgroundMemberRepository.save( + new PlaygroundMember( + playground, + member + ) + ); + + // when + playgroundCommandService.leavePlayground(member.getId()); + + // then + assertThat(playgroundMemberRepository.findById(playgroundMember.getId())).isEmpty(); + } + + @DisplayName("놀이터를 나갔을 때, 놀이터에 참여중인 멤버가 하나도 없으면 놀이터는 삭제된다.") + @Transactional + @Test + void deletePlaygroundWhenLeavePlaygroundAndNoMemberInPlayground() { + // given + Member member = saveMember("김도선"); + Playground playground = savePlayground(); + PlaygroundMember playgroundMember = playgroundMemberRepository.save( + new PlaygroundMember( + playground, + member + ) + ); + + // when + playgroundCommandService.leavePlayground(member.getId()); + boolean isExistPlayground = playgroundRepository.existsById(playground.getId()); + + // then + assertThat(isExistPlayground).isFalse(); + } + + @DisplayName("놀이터 안에 들어오면 상태가 true가 된다.") + @Transactional + @Test + void updateIsInsideTrue() { + // given + Member member = saveMember("김도선"); + double latitude = 37.5173316; + double longitude = 127.1011661; + Location location = new Location(latitude, longitude); + Playground playground = savePlayground(location.getLatitude(), location.getLongitude()); + PlaygroundMember playgroundMember = savePlaygroundMember(playground, member); + + double insideLatitude = GeoCalculator.calculateLatitudeOffset(latitude, 149); + Location insideLocation = new Location(insideLatitude, longitude); + + UpdatePlaygroundArrivalRequest request = new UpdatePlaygroundArrivalRequest( + insideLocation.getLatitude(), insideLocation.getLongitude() + ); + + // when + UpdatePlaygroundArrivalResponse response = playgroundCommandService.updateArrival( + request, member.getId() + ); + + // then + assertAll( + () -> assertThat(playgroundMember.isInside()).isTrue(), + () -> assertThat(response.isArrived()).isTrue() + ); + } + + @DisplayName("놀이터 밖으로 가면 상태가 false가 된다.") + @Transactional + @Test + void updateIsInsideFalse() { + // given + Member member = saveMember("김도선"); + double latitude = 37.5173316; + double longitude = 127.1011661; + Location location = new Location(latitude, longitude); + Playground playground = savePlayground(location.getLatitude(), location.getLongitude()); + PlaygroundMember playgroundMember = saveArrivedPlaygroundMember(playground, member); + + double insideLatitude = GeoCalculator.calculateLatitudeOffset(latitude, 151); + Location insideLocation = new Location(insideLatitude, longitude); + + UpdatePlaygroundArrivalRequest request = new UpdatePlaygroundArrivalRequest( + insideLocation.getLatitude(), insideLocation.getLongitude() + ); + + // when + UpdatePlaygroundArrivalResponse response = playgroundCommandService.updateArrival( + request, member.getId() + ); + + // then + assertAll( + () -> assertThat(playgroundMember.isInside()).isFalse(), + () -> assertThat(response.isArrived()).isFalse() + ); + } + + @DisplayName("놀이터에 참여한 멤버의 상태메세지를 수정할 수 있다.") + @Transactional + @Test + void updateMemberMessage() { + // given + Member member = saveMember("김도선"); + Playground playground = savePlayground(); + PlaygroundMember playgroundMember = savePlaygroundMember(playground, member); + + String changeMessage = "수정된 메세지"; + UpdatePlaygroundMemberMessageRequest request = new UpdatePlaygroundMemberMessageRequest(changeMessage); + + // when + UpdatePlaygroundMemberMessageResponse response = playgroundCommandService.updateMemberMessage( + request, member.getId() + ); + + // then + assertAll( + () -> assertThat(response.message()).isEqualTo(changeMessage), + () -> assertThat(playgroundMember.getMessage()).isEqualTo(changeMessage) + ); + } } diff --git a/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundQueryServiceTest.java b/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundQueryServiceTest.java index f0c62cbfe..af7a43709 100644 --- a/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundQueryServiceTest.java +++ b/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundQueryServiceTest.java @@ -4,10 +4,16 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.happy.friendogly.member.domain.Member; +import com.happy.friendogly.pet.domain.Gender; +import com.happy.friendogly.pet.domain.Pet; +import com.happy.friendogly.pet.domain.SizeType; import com.happy.friendogly.playground.domain.Playground; import com.happy.friendogly.playground.domain.PlaygroundMember; import com.happy.friendogly.playground.dto.response.FindPlaygroundDetailResponse; import com.happy.friendogly.playground.dto.response.FindPlaygroundLocationResponse; +import com.happy.friendogly.playground.dto.response.FindPlaygroundSummaryResponse; +import com.happy.friendogly.playground.dto.response.detail.PlaygroundPetDetail; +import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -60,6 +66,39 @@ void findPlaygroundDetail() { ); } + @DisplayName("놀이터 상세정보 펫의 정렬조건은 내강아지우선->도착한강아지 순서이다.") + @Test + void findSortedDetailPlaygrounds() { + // given + Member me = saveMember("김도선"); + Member notArrivedMember = saveMember("이충렬"); + Member arrivedMember = saveMember("이충렬"); + savePet(me); + savePet(notArrivedMember); + savePet(arrivedMember); + Playground playground = savePlayground(); + playgroundMemberRepository.save(new PlaygroundMember( + playground, + arrivedMember, + "도착", + true, + null + )); + savePlaygroundMember(playground, me); + savePlaygroundMember(playground, notArrivedMember); + + // when + List detail = playgroundQueryService.findDetail(me.getId(), playground.getId()) + .playgroundPetDetails(); + + // when + assertAll( + () -> assertThat(detail.get(0).memberId()).isEqualTo(me.getId()), + () -> assertThat(detail.get(1).memberId()).isEqualTo(arrivedMember.getId()), + () -> assertThat(detail.get(2).memberId()).isEqualTo(notArrivedMember.getId()) + ); + } + @DisplayName("놀이터들의 위치를 조회한다.") @Test void findLocations() { @@ -118,4 +157,133 @@ void findLocationsWithIsParticipatingFalse() { // then assertThat(response.get(0).isParticipating()).isEqualTo(false); } + + @DisplayName("놀이터의 요약정보를 조회한다.") + @Test + void findSummary() { + // given + Member member = saveMember("김도선"); + Pet pet = savePet(member); + Playground playground = savePlayground(); + PlaygroundMember playgroundMember = playgroundMemberRepository.save( + new PlaygroundMember(playground, member, "message", true, null) + ); + + // when + FindPlaygroundSummaryResponse response = playgroundQueryService.findSummary(playground.getId()); + + // then + assertAll( + () -> assertThat(response.arrivedPetCount()).isEqualTo(1), + () -> assertThat(response.totalPetCount()).isEqualTo(1), + () -> assertThat(response.petImageUrls().get(0)).isEqualTo(pet.getImageUrl()) + ); + } + + @DisplayName("놀이터의 요약정보를 조회시 펫이미지는 5개까지만 조회된다.") + @Test + void limitPetImageIsFiveWhenFindSummary() { + // given + Member member1 = saveMember("김도선1"); + Member member2 = saveMember("김도선2"); + Pet pet = savePet(member1); + savePet(member1); + savePet(member1); + + savePet(member2); + savePet(member2); + Pet pet6 = savePet(member2); + + Playground playground = savePlayground(); + savePlaygroundMember(playground, member1); + savePlaygroundMember(playground, member2); + + // when + FindPlaygroundSummaryResponse response = playgroundQueryService.findSummary(playground.getId()); + + // then + assertThat(response.petImageUrls()).hasSize(5); + } + + @DisplayName("놀이터의 요약정보를 조회시 도착한 펫우선으로 이미지를 보여준다") + @Test + void showArrivedPetImageFirstWhenFindSummary() { + // given + Member member1 = saveMember("김도선1"); + Member member2 = saveMember("김도선2"); + Pet pet = savePet(member1); + savePet(member1); + savePet(member1); + + Pet arrivedPet = petRepository.save( + new Pet( + member2, + "name", + "description", + LocalDate.of(2023, 10, 10), + SizeType.LARGE, + Gender.FEMALE, + "arrivedPetImage" + ) + ); + + Playground playground = savePlayground(); + savePlaygroundMember(playground, member1); + saveArrivedPlaygroundMember(playground, member2); + + // when + FindPlaygroundSummaryResponse response = playgroundQueryService.findSummary(playground.getId()); + + // then + assertThat(response.petImageUrls().get(0)).isEqualTo("arrivedPetImage"); + } + + @DisplayName("놀이터의 요약정보를 조회시 도착한 펫우선으로 보여준후, 참여한 펫 우선으로 보여준다.") + @Test + void showParticipatingPetImageSecondWhenFindSummary() { + // given + Member member1 = saveMember("김도선"); + Member member2 = saveMember("이충렬"); + Member member3 = saveMember("박예찬"); + + savePet(member1); + + Pet arrivedPet = petRepository.save( + new Pet( + member2, + "name", + "description", + LocalDate.of(2023, 10, 10), + SizeType.LARGE, + Gender.FEMALE, + "arrivedPetImage" + ) + ); + + Pet participatingPet = petRepository.save( + new Pet( + member3, + "name", + "description", + LocalDate.of(2023, 10, 10), + SizeType.LARGE, + Gender.FEMALE, + "participatingPetImage" + ) + ); + + Playground playground = savePlayground(); + savePlaygroundMember(playground, member3); + saveArrivedPlaygroundMember(playground, member2); + savePlaygroundMember(playground, member1); + + // when + FindPlaygroundSummaryResponse response = playgroundQueryService.findSummary(playground.getId()); + + // then + assertAll( + () -> assertThat(response.petImageUrls().get(0)).isEqualTo("arrivedPetImage"), + () -> assertThat(response.petImageUrls().get(1)).isEqualTo("participatingPetImage") + ); + } } diff --git a/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundServiceTest.java b/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundServiceTest.java index fd9f0ecea..fd81d7a18 100644 --- a/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundServiceTest.java +++ b/backend/src/test/java/com/happy/friendogly/playground/service/PlaygroundServiceTest.java @@ -6,6 +6,7 @@ import com.happy.friendogly.pet.domain.SizeType; import com.happy.friendogly.playground.domain.Location; import com.happy.friendogly.playground.domain.Playground; +import com.happy.friendogly.playground.domain.PlaygroundMember; import com.happy.friendogly.support.ServiceTest; import java.time.LocalDate; @@ -17,10 +18,10 @@ protected Member saveMember(String name) { ); } - protected Pet savePet(Member member) { + protected Pet savePet(Member ownerMember) { return petRepository.save( new Pet( - member, + ownerMember, "petName", "description", LocalDate.of(2023, 10, 02), @@ -42,4 +43,25 @@ protected Playground savePlayground(double latitude, double longitude) { new Playground(new Location(latitude, longitude)) ); } + + protected PlaygroundMember savePlaygroundMember(Playground playground, Member member) { + return playgroundMemberRepository.save( + new PlaygroundMember( + playground, + member + ) + ); + } + + protected PlaygroundMember saveArrivedPlaygroundMember(Playground playground, Member member) { + return playgroundMemberRepository.save( + new PlaygroundMember( + playground, + member, + "message", + true, + null + ) + ); + } }