diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleDetailResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleDetailResponse.java index 25d4a84f4..f94246978 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleDetailResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleDetailResponse.java @@ -65,7 +65,9 @@ public record CapsuleDetailResponse( dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } - createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + if (createdDate != null) { + createdDate = createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } } public static CapsuleDetailResponse createOf( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java index ab905e71e..b96f6b99e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java @@ -51,7 +51,9 @@ public record CapsuleSummaryResponse( dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } - createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } } public static CapsuleSummaryResponse createOf( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java index fe4777a1d..66eaa0398 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java @@ -34,7 +34,6 @@ public record NearbyARCapsuleSummaryResponse( @Schema(description = "캡슐 타입") CapsuleType capsuleType ) { - public NearbyARCapsuleSummaryResponse { if (dueDate != null) { dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/PublicCapsuleDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/PublicCapsuleDetailDto.java index 6e724118f..84a3f0f40 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/PublicCapsuleDetailDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/PublicCapsuleDetailDto.java @@ -34,23 +34,23 @@ public PublicCapsuleDetailResponse toResponse( final List preSignedImageUrls = multiplePreSignUrlFunction.apply(images); final List preSignedVideoUrls = multiplePreSignUrlFunction.apply(videos); - return new PublicCapsuleDetailResponse( - capsuleId, - singlePreSignUrlFunction.apply(capsuleSkinUrl), - dueDate, - nickname, - profileUrl, - createdAt, - changePoint.getX(), - changePoint.getY(), - address, - roadName, - title, - content, - preSignedImageUrls, - preSignedVideoUrls, - isOpened, - capsuleType - ); + return PublicCapsuleDetailResponse.builder() + .capsuleId(capsuleId) + .capsuleSkinUrl(singlePreSignUrlFunction.apply(capsuleSkinUrl)) + .dueDate(dueDate) + .nickname(nickname) + .profileUrl(profileUrl) + .createdDate(createdAt) + .latitude(changePoint.getX()) + .longitude(changePoint.getY()) + .address(address) + .roadName(roadName) + .title(title) + .content(content) + .imageUrls(preSignedImageUrls) + .videoUrls(preSignedVideoUrls) + .isOpened(isOpened) + .capsuleType(capsuleType) + .build(); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/PublicCapsuleDetailResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/PublicCapsuleDetailResponse.java index 3a6c45ed9..606a03015 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/PublicCapsuleDetailResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/PublicCapsuleDetailResponse.java @@ -3,10 +3,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import java.util.List; -import java.util.function.Function; import lombok.Builder; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; -import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.PublicCapsuleDetailDto; import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @Schema(description = "공개 캡슐 상세 정보") @@ -67,36 +65,8 @@ public record PublicCapsuleDetailResponse( dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } - createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); - } - - public static PublicCapsuleDetailResponse createOf( - final PublicCapsuleDetailDto detailDto, - final Function singlePreSignUrlFunction, - final Function> multiplePreSignUrlFunction - ) { - final List preSignedImageUrls = multiplePreSignUrlFunction.apply( - detailDto.images()); - final List preSignedVideoUrls = multiplePreSignUrlFunction.apply( - detailDto.videos()); - - return new PublicCapsuleDetailResponse( - detailDto.capsuleId(), - singlePreSignUrlFunction.apply(detailDto.capsuleSkinUrl()), - detailDto.dueDate(), - detailDto.nickname(), - detailDto.profileUrl(), - detailDto.createdAt(), - detailDto.point().getX(), - detailDto.point().getY(), - detailDto.address(), - detailDto.roadName(), - detailDto.title(), - detailDto.content(), - preSignedImageUrls, - preSignedVideoUrls, - detailDto.isOpened(), - detailDto.capsuleType() - ); + if (createdDate != null) { + createdDate = createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java index 3f22e5503..9b3c37704 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java @@ -16,14 +16,6 @@ public record MySecreteCapsuleDto( CapsuleType type ) { - public MySecreteCapsuleDto { - if (dueDate != null) { - dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); - } - - createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); - } - public MySecreteCapsuleResponse toResponse( final Function preSignUrlFunction ) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java index 66a287a09..15314fed7 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import lombok.Builder; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @Builder @Schema(description = "내 비밀 캡슐 응답") @@ -30,4 +31,13 @@ public record MySecreteCapsuleResponse( CapsuleType type ) { + public MySecreteCapsuleResponse { + if (dueDate != null) { + dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java index 2241b2397..35bef1e4b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import lombok.Builder; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @Schema(description = "캡슐 스킨 요약 정보") @Builder @@ -20,4 +21,9 @@ public record CapsuleSkinSummaryResponse( ZonedDateTime createdAt ) { + public CapsuleSkinSummaryResponse { + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java index bd740b365..a70e24f4d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java @@ -24,7 +24,7 @@ public record FriendSummaryResponse( public FriendSummaryResponse { if (createdAt != null) { - createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java index 38e255fa7..d90e76ef5 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java @@ -7,20 +7,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; +import java.time.ZonedDateTime; 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.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupCreateRequest; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupUpdateRequest; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupsPageResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupsSliceResponse; import site.timecapsulearchive.core.global.common.response.ApiSpec; public interface GroupApi { @@ -133,15 +130,21 @@ ResponseEntity denyGroupInvitation( @ApiResponse( responseCode = "200", description = "ok" + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 파라미터를 받았을 때 발생하는 오류" + ), + @ApiResponse( + responseCode = "403", + description = "그룹에 포함된 사용자가 아닌 경우 발생하는 오류" ) }) - @GetMapping( - value = "/groups/{group_id}", - produces = {"application/json"} - ) - ResponseEntity findGroupById( + ResponseEntity> findGroupDetailById( + Long memberId, + @Parameter(in = ParameterIn.PATH, description = "조회할 그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId + Long groupId ); @Operation( @@ -156,16 +159,14 @@ ResponseEntity findGroupById( description = "ok" ) }) - @GetMapping( - value = "/groups", - produces = {"application/json"} - ) - ResponseEntity findGroups( - @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true, schema = @Schema()) - @NotNull @Valid @RequestParam(value = "size") Long size, + ResponseEntity> findGroups( + Long memberId, + + @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true) + int size, - @Parameter(in = ParameterIn.QUERY, description = "마지막 그룹 아이디", required = true, schema = @Schema()) - @NotNull @Valid @RequestParam(value = "group_id") Long groupId + @Parameter(in = ParameterIn.QUERY, description = "마지막 데이터의 시간", required = true) + ZonedDateTime createdAt ); @Operation( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java index 5dec2042c..e8a067e35 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java @@ -1,21 +1,29 @@ package site.timecapsulearchive.core.domain.group.api; import jakarta.validation.Valid; +import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupCreateRequest; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupUpdateRequest; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupsPageResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupsSliceResponse; import site.timecapsulearchive.core.domain.group.service.GroupService; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.common.response.SuccessCode; +import site.timecapsulearchive.core.infra.s3.manager.S3PreSignedUrlManager; import site.timecapsulearchive.core.infra.s3.manager.S3UrlGenerator; @@ -26,6 +34,7 @@ public class GroupApiController implements GroupApi { private final GroupService groupService; private final S3UrlGenerator s3UrlGenerator; + private final S3PreSignedUrlManager s3PreSignedUrlManager; @Override public ResponseEntity acceptGroupInvitation(Long groupId, Long memberId) { @@ -66,14 +75,47 @@ public ResponseEntity denyGroupInvitation(Long groupId, Long memberId) { return null; } + @GetMapping( + value = "/{group_id}", + produces = {"application/json"} + ) @Override - public ResponseEntity findGroupById(Long groupId) { - return null; + public ResponseEntity> findGroupDetailById( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId + ) { + final GroupDetailDto groupDetailDto = groupService.findGroupDetailByGroupId(memberId, groupId); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + groupDetailDto.toResponse(s3PreSignedUrlManager::getS3PreSignedUrlForGet) + ) + ); } + @GetMapping( + produces = {"application/json"} + ) @Override - public ResponseEntity findGroups(Long size, Long groupId) { - return null; + public ResponseEntity> findGroups( + @AuthenticationPrincipal final Long memberId, + @RequestParam(defaultValue = "20", value = "size") final int size, + @RequestParam(value = "created_at") final ZonedDateTime createdAt + ) { + final Slice groupsSlice = groupService.findGroupsSlice(memberId, size, + createdAt); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + GroupsSliceResponse.createOf( + groupsSlice.getContent(), + groupsSlice.hasNext(), + s3PreSignedUrlManager::getS3PreSignedUrlForGet + ) + ) + ); } @Override diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java new file mode 100644 index 000000000..c45a8a46e --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java @@ -0,0 +1,31 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; + +public record GroupDetailDto( + String groupName, + String groupDescription, + String groupProfileUrl, + ZonedDateTime createdAt, + List members +) { + + public GroupDetailResponse toResponse(Function singlePreSignUrlFunction) { + List members = this.members.stream() + .map(GroupMemberDto::toResponse) + .toList(); + + return GroupDetailResponse.builder() + .groupName(groupName) + .groupDescription(groupDescription) + .groupProfileUrl(singlePreSignUrlFunction.apply(groupProfileUrl)) + .createdAt(createdAt) + .members(members) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberDto.java new file mode 100644 index 000000000..09a03c5e1 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberDto.java @@ -0,0 +1,22 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberResponse; + +public record GroupMemberDto( + Long memberId, + String profileUrl, + String nickname, + String tag, + Boolean isOwner +) { + + public GroupMemberResponse toResponse() { + return GroupMemberResponse.builder() + .memberId(memberId) + .profileUrl(profileUrl) + .nickname(nickname) + .tag(tag) + .isOwner(isOwner) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupSummaryDto.java new file mode 100644 index 000000000..a900ff27d --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupSummaryDto.java @@ -0,0 +1,26 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +import java.time.ZonedDateTime; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.group.data.response.GroupSummaryResponse; + +public record GroupSummaryDto( + Long id, + String groupName, + String groupDescription, + String groupProfileUrl, + ZonedDateTime createdAt, + Boolean isOwner +) { + + public GroupSummaryResponse toResponse(Function preSignedUrlFunction) { + return GroupSummaryResponse.builder() + .id(id) + .name(groupName) + .description(groupDescription) + .profileUrl(preSignedUrlFunction.apply(groupProfileUrl)) + .createdAt(createdAt) + .isOwner(isOwner) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java index f1105e8dc..98b21892d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java @@ -3,24 +3,32 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import java.util.List; +import lombok.Builder; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; +@Builder @Schema(description = "그룹 생성 포맷") public record GroupDetailResponse( @Schema(description = "그룹 이름") - String name, - - @Schema(description = "그룹 생성일") - ZonedDateTime createdDate, + String groupName, @Schema(description = "그룹 프로필 url") - String profileUrl, + String groupProfileUrl, @Schema(description = "그룹 설명") - String description, + String groupDescription, + + @Schema(description = "그룹 생성일") + ZonedDateTime createdAt, @Schema(description = "그룹원 리스트") - List members + List members ) { + public GroupDetailResponse { + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupMemberResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupMemberResponse.java new file mode 100644 index 000000000..db025b957 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupMemberResponse.java @@ -0,0 +1,26 @@ +package site.timecapsulearchive.core.domain.group.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +@Schema(description = "그룹원 정보") +public record GroupMemberResponse( + + @Schema(description = "그룹원 아이디") + Long memberId, + + @Schema(description = "그룹원 프로필 url") + String profileUrl, + + @Schema(description = "그룹원 닉네임") + String nickname, + + @Schema(description = "그룹원 태그") + String tag, + + @Schema(description = "그룹장 여부") + Boolean isOwner +) { + +} \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java index 59e67168b..d43993dce 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java @@ -1,7 +1,11 @@ package site.timecapsulearchive.core.domain.group.data.response; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +import lombok.Builder; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; +@Builder @Schema(description = "그룹 요약 정보") public record GroupSummaryResponse( @@ -15,7 +19,18 @@ public record GroupSummaryResponse( String profileUrl, @Schema(description = "그룹 설명") - String description + String description, + + @Schema(description = "그룹 생성일") + ZonedDateTime createdAt, + + @Schema(description = "그룹장 여부") + Boolean isOwner ) { + public GroupSummaryResponse { + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsPageResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsPageResponse.java deleted file mode 100644 index c9f338b92..000000000 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsPageResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package site.timecapsulearchive.core.domain.group.data.response; - -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -@Schema(description = "그룹 리스트 페이징") -public record GroupsPageResponse( - - @Schema(description = "그룹 리스트") - List groups, - - @Schema(description = "다음 페이지 유무") - - Boolean hasNext, - - @Schema(description = "이전 페이지 유무") - - Boolean hasPrevious -) { - -} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsSliceResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsSliceResponse.java new file mode 100644 index 000000000..d0f4ad450 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsSliceResponse.java @@ -0,0 +1,29 @@ +package site.timecapsulearchive.core.domain.group.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; + +@Schema(description = "사용자의 그룹 목록 응답") +public record GroupsSliceResponse( + + @Schema(description = "그룹 목록") + List groups, + + @Schema(description = "다음 페이지 유무") + Boolean hasNext +) { + + public static GroupsSliceResponse createOf( + List groups, + Boolean hasNext, + Function preSignedUrlFunction + ) { + List responses = groups.stream() + .map(dto -> dto.toResponse(preSignedUrlFunction)) + .toList(); + + return new GroupsSliceResponse(responses, hasNext); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java new file mode 100644 index 000000000..488c3212f --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java @@ -0,0 +1,102 @@ +package site.timecapsulearchive.core.domain.group.repository; + +import static com.querydsl.core.group.GroupBy.groupBy; +import static com.querydsl.core.group.GroupBy.list; +import static site.timecapsulearchive.core.domain.group.entity.QGroup.group; +import static site.timecapsulearchive.core.domain.group.entity.QMemberGroup.memberGroup; +import static site.timecapsulearchive.core.domain.member.entity.QMember.member; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; + +@Repository +@RequiredArgsConstructor +public class GroupQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + final List groups = jpaQueryFactory + .select( + Projections.constructor( + GroupSummaryDto.class, + group.id, + group.groupName, + group.groupDescription, + group.groupProfileUrl, + group.createdAt, + memberGroup.isOwner + ) + ) + .from(memberGroup) + .join(memberGroup.group, group) + .where(memberGroup.member.id.eq(memberId).and(memberGroup.createdAt.lt(createdAt))) + .limit(size + 1) + .fetch(); + + final boolean hasNext = groups.size() > size; + if (hasNext) { + groups.remove(size); + } + + return new SliceImpl<>(groups, Pageable.ofSize(size), groups.size() > size); + } + + public Optional findGroupDetailByGroupId(final Long groupId) { + return Optional.ofNullable( + jpaQueryFactory + .select( + group.groupName, + group.groupDescription, + group.groupProfileUrl, + group.createdAt, + member.id, + member.profileUrl, + member.nickname, + member.tag, + memberGroup.isOwner + ) + .from(group) + .join(memberGroup).on(memberGroup.group.id.eq(group.id)) + .join(member).on(member.id.eq(memberGroup.member.id)) + .where(group.id.eq(groupId)) + .transform( + groupBy(group.id).as( + Projections.constructor( + GroupDetailDto.class, + group.groupName, + group.groupDescription, + group.groupProfileUrl, + group.createdAt, + list( + Projections.constructor( + GroupMemberDto.class, + member.id, + member.profileUrl, + member.nickname, + member.tag, + memberGroup.isOwner + ) + ) + ) + ) + ) + .get(groupId) + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java index 58ca64206..98b3b1603 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java @@ -1,15 +1,21 @@ package site.timecapsulearchive.core.domain.group.service; import lombok.RequiredArgsConstructor; +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; import site.timecapsulearchive.core.domain.group.entity.Group; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.repository.GroupQueryRepository; import site.timecapsulearchive.core.domain.group.repository.GroupRepository; import site.timecapsulearchive.core.domain.group.repository.MemberGroupRepository; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -26,6 +32,7 @@ public class GroupService { private final MemberGroupRepository memberGroupRepository; private final TransactionTemplate transactionTemplate; private final SocialNotificationManager socialNotificationManager; + private final GroupQueryRepository groupQueryRepository; public void createGroup(final Long memberId, final GroupCreateDto dto) { final Member member = memberRepository.findMemberById(memberId) @@ -52,4 +59,29 @@ public Group findGroupById(Long groupId) { return groupRepository.findGroupById(groupId) .orElseThrow(GroupNotFoundException::new); } + + @Transactional(readOnly = true) + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return groupQueryRepository.findGroupsSlice(memberId, size, createdAt); + } + + @Transactional(readOnly = true) + public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { + final GroupDetailDto groupDetailDto = groupQueryRepository.findGroupDetailByGroupId(groupId) + .orElseThrow(GroupNotFoundException::new); + + final boolean isGroupMember = groupDetailDto.members() + .stream() + .anyMatch(m -> m.memberId().equals(memberId)); + + if (!isGroupMember) { + throw new GroupNotFoundException(); + } + + return groupDetailDto; + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java index 93287c46e..2197b6b78 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java @@ -1,17 +1,31 @@ package site.timecapsulearchive.core.domain.member.data.response; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import lombok.Builder; import site.timecapsulearchive.core.domain.member.entity.CategoryName; import site.timecapsulearchive.core.domain.member.entity.NotificationStatus; +@Schema(description = "알림 정보") @Builder public record MemberNotificationResponse( + + @Schema(description = "알림 제목") String title, + + @Schema(description = "알림 내용") String text, + + @Schema(description = "알림 생성일") ZonedDateTime createdAt, + + @Schema(description = "알림 이미지 링크") String imageUrl, + + @Schema(description = "알림 카테고리 이름") CategoryName categoryName, + + @Schema(description = "알림 상태") NotificationStatus status ) { diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java index d9bfee830..c0430087e 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java @@ -8,8 +8,9 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import site.timecapsulearchive.core.global.config.JpaAuditingConfig; +import site.timecapsulearchive.core.global.config.QueryDSLConfig; -@Import(JpaAuditingConfig.class) +@Import(value = {JpaAuditingConfig.class, QueryDSLConfig.class}) @DataJpaTest @FlywayTestExtension @FlywayTest diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupFixture.java index 73d9fafb7..615613e58 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupFixture.java @@ -11,9 +11,9 @@ public class GroupFixture { */ public static Group group() { return Group.builder() - .groupName("test-group") - .groupDescription("test-group-description") - .groupProfileUrl("test-group-profile") + .groupName("test_group") + .groupDescription("test_group") + .groupProfileUrl("test_group") .build(); } } diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java index fff8d12ab..9126a5f9a 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java @@ -15,12 +15,10 @@ public class MemberFixture { private static final HashEncryptionManager hashEncryptionManager = UnitTestDependency.hashEncryptionManager(); /** - * MemberdataPrefix 를 붙여서 만들어진 Member를 반환한다 - *

- * 주의 - 하나의 테스트에서 여러 개의 멤버를 생성한다면 서로 다른 dataPrefix 필요 - * - * @param dataPrefix 생성될 Member 에 붙일 prefix - * @return dataPrefix가 붙어서 생성된 Member + * 테스트 픽스처 - 멤버 마다 상이한 값을 위한 dataPrefix를 주면 멤버를 생성한다. + *
주의 - 테스트에서 같은 prefix를 사용하면 오류가 발생하므로 서로 다른 prefix를 쓰도록 해야함. + * @param dataPrefix prefix + * @return {@code Member} 테스트 픽스처 */ public static Member member(int dataPrefix) { byte[] number = getPhoneBytes(dataPrefix); @@ -38,6 +36,13 @@ public static Member member(int dataPrefix) { return member; } + /** + * 테스트 픽스처 - 멤버마다 상이한 번호를 위해 dataPrefix를 주면 해당 dataPrefix에 대한 핸드폰 번호 바이트를 반환한다. + *
주의 - 테스트에서 같은 prefix를 사용하면 오류가 발생하므로 서로 다른 prefix를 쓰도록 해야함. + * + * @param dataPrefix prefix + * @return 핸드폰 번호 바이트 + */ public static byte[] getPhoneBytes(int dataPrefix) { return ("0" + (1000000000 + dataPrefix)).getBytes(StandardCharsets.UTF_8); } @@ -48,23 +53,16 @@ public static List getPhones(int count) { .toList(); } - private static byte[] getPhoneBytes(String phone) { - return hashEncryptionManager.encrypt(phone.getBytes(StandardCharsets.UTF_8)); - } - /** - * Memberstartcount까지 - * 하나씩 증가된 것을 붙여서 만들어진 Member를 반환한다 - *

- * 주의 - 하나의 테스트에서 여러 개의 멤버를 생성한다면 서로 다른 dataPrefix 필요 - * - * @param start 시작할 prefix - * @param count 반환받을 Member 개수 - * @return start가 하나씩 증가되어 붙여서 만들어진 Member들 + * 테스트 픽스처 - 크기와 멤버 마다 상이한 값을 위한 startDataPrefix를 주면 멤버들을 생성한다. + *
주의 - 테스트에서 같은 prefix를 사용하면 오류가 발생하므로 서로 다른 prefix를 쓰도록 해야함. + * @param startDataPrefix 시작 prefix + * @param count 크기 + * @return {@code List} 테스트 픽스처들 */ - public static List members(int start, int count) { + public static List members(int startDataPrefix, int count) { List result = new ArrayList<>(); - for (int index = start; index < start + count; index++) { + for (int index = startDataPrefix; index < startDataPrefix + count; index++) { result.add(member(index)); } diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java index a4fa04741..90f02b793 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java @@ -1,5 +1,8 @@ package site.timecapsulearchive.core.common.fixture.domain; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.List; import site.timecapsulearchive.core.domain.group.entity.Group; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -16,4 +19,46 @@ public class MemberGroupFixture { public static MemberGroup memberGroup(Member member, Group group) { return MemberGroup.createGroupOwner(member, group); } + + /** + * 테스트 픽스처 - 사용자, 그룹, 그룹장 여부를 받아 그룹원을 만들어준다. + * @param member 사용자 + * @param group 그룹 + * @param isOwner 그룹장 여부 + * @return MemberGroup 테스트 픽스처 + */ + public static MemberGroup memberGroup(Member member, Group group, Boolean isOwner) { + try { + Constructor declaredConstructor = MemberGroup.class.getDeclaredConstructor(); + declaredConstructor.setAccessible(true); + + MemberGroup memberGroup = declaredConstructor.newInstance(); + setFieldValue(memberGroup, "member", member); + setFieldValue(memberGroup, "group", group); + setFieldValue(memberGroup, "isOwner", isOwner); + + return memberGroup; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setFieldValue(Object instance, String fieldName, Object value) + throws NoSuchFieldException, IllegalAccessException { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, value); + } + + /** + * 테스트 픽스처 - 그룹원들과 그룹을 주면 그룹원들 목록을 만들어준다(그룹장 X) + * @param members 그룹원들 목록 + * @param group 그룹 + * @return {@code List} 테스트 픽스처들 + */ + public static List membersGroup(List members, Group group) { + return members.stream() + .map(m -> memberGroup(m, group, Boolean.FALSE)) + .toList(); + } } diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java new file mode 100644 index 000000000..66ff5a86f --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java @@ -0,0 +1,210 @@ +package site.timecapsulearchive.core.domain.group.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Slice; +import org.springframework.test.context.TestConstructor; +import org.springframework.test.context.TestConstructor.AutowireMode; +import org.springframework.transaction.annotation.Transactional; +import site.timecapsulearchive.core.common.RepositoryTest; +import site.timecapsulearchive.core.common.fixture.domain.GroupFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberGroupFixture; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.member.entity.Member; + +@TestConstructor(autowireMode = AutowireMode.ALL) +class MemberGroupQueryRepositoryTest extends RepositoryTest { + + private final static int GROUP_COUNT = 20; + + private final GroupQueryRepository groupQueryRepository; + + private Long memberId; + private Long memberIdWithNoGroup; + private Long ownerGroupId; + + MemberGroupQueryRepositoryTest(JPAQueryFactory jpaQueryFactory) { + this.groupQueryRepository = new GroupQueryRepository(jpaQueryFactory); + } + + @Transactional + @BeforeEach + void setup(@Autowired EntityManager entityManager) { + //사용자 + Member member = MemberFixture.member(0); + entityManager.persist(member); + memberId = member.getId(); + + //그룹이 없는 사용자 + Member memberNotInGroup = MemberFixture.member(1); + entityManager.persist(memberNotInGroup); + memberIdWithNoGroup = memberNotInGroup.getId(); + + //그룹 + List groups = new ArrayList<>(); + for (int count = 0; count < GROUP_COUNT; count++) { + Group group = GroupFixture.group(); + entityManager.persist(group); + groups.add(group); + } + //사용자가 그룹장인 그룹 + ownerGroupId = groups.get(0).getId(); + + //그룹원들 + List members = MemberFixture.members(4, 2); + members.forEach(entityManager::persist); + + //그룹에 사용자를 그룹장으로 설정 + for (int count = 0; count < GROUP_COUNT; count++) { + MemberGroup memberGroup = MemberGroupFixture.memberGroup(member, groups.get(count), + Boolean.TRUE); + entityManager.persist(memberGroup); + } + + //그룹원들 설정 + List memberGroups = MemberGroupFixture.membersGroup(members, groups.get(0)); + memberGroups.forEach(entityManager::persist); + } + + @ParameterizedTest + @ValueSource(ints = {1, 5, 15, 20}) + void 사용자와_개수_마지막_데이터_생성_시간으로_그룹_목록을_조회하면_개수만큼_그룹이_반환된다(int size) { + //given + ZonedDateTime now = ZonedDateTime.now().plusDays(3); + + //when + Slice groupsSlice = groupQueryRepository.findGroupsSlice(memberId, + size, now); + + //then + assertThat(groupsSlice.getNumberOfElements()).isEqualTo(size); + } + + @Test + void 사용자와_개수_마지막_데이터_생성_시간으로_그룹_목록을_조회하면_개수만큼_그룹의_내용들이_반환된다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().plusDays(3); + + //when + List groupsSlice = groupQueryRepository.findGroupsSlice(memberId, + size, now).getContent(); + + //then + assertSoftly(softly -> { + softly.assertThat(groupsSlice).allMatch(dto -> dto.id() != null); + softly.assertThat(groupsSlice).allMatch(dto -> !dto.groupName().isBlank()); + softly.assertThat(groupsSlice).allMatch(dto -> !dto.groupDescription().isBlank()); + softly.assertThat(groupsSlice).allMatch(dto -> !dto.groupProfileUrl().isBlank()); + softly.assertThat(groupsSlice).allMatch(dto -> dto.isOwner() != null); + }); + } + + @Test + void 그룹이_없는_사용자로_그룹_목록을_조회하면_빈_그룹_목록이_반환된다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().plusDays(3); + + //when + List groupsSlice = groupQueryRepository.findGroupsSlice( + memberIdWithNoGroup, + size, + now) + .getContent(); + + //then + assertThat(groupsSlice.isEmpty()).isTrue(); + } + + @Test + void 사용자와_범위에_없는_마지막_데이터_생성_시간으로_그룹_목록을_조회하면_빈_그룹_목록이_반환된다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().minusDays(5); + + //when + List groupsSlice = groupQueryRepository.findGroupsSlice(memberId, + size, now).getContent(); + + //then + assertThat(groupsSlice.isEmpty()).isTrue(); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_그룹_상세가_반환된다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + assertThat(groupDetail).isNotNull(); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_그룹_정보를_볼_수_있다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(groupDetail.groupName()).isNotBlank(); + assertThat(groupDetail.groupDescription()).isNotBlank(); + assertThat(groupDetail.groupProfileUrl()).isNotBlank(); + assertThat(groupDetail.createdAt()).isNotNull(); + }); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_그룹원들의_정보를_볼_수_있다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(groupDetail.members()).isNotEmpty(); + assertThat(groupDetail.members()).allSatisfy(m -> assertThat(m.memberId()).isNotNull()); + assertThat(groupDetail.members()).allSatisfy(m -> assertThat(m.tag()).isNotBlank()); + assertThat(groupDetail.members()).allSatisfy( + m -> assertThat(m.nickname()).isNotBlank()); + assertThat(groupDetail.members()).allSatisfy( + m -> assertThat(m.profileUrl()).isNotBlank()); + assertThat(groupDetail.members()).allSatisfy(m -> assertThat(m.isOwner()).isNotNull()); + }); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_한_명의_그룹장만_존재한다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(groupDetail.members()).satisfiesOnlyOnce( + m -> assertThat(m.isOwner()).isTrue()); + }); + } +} \ No newline at end of file