diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApi.java index 54933e138..5e7553d6d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApi.java @@ -10,7 +10,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.springframework.http.ResponseEntity; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; -import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.request.CapsuleCreateRequest; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleOpenedResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.ImagesPageResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.NearbyARCapsuleResponse; @@ -127,59 +126,5 @@ ResponseEntity> updateCapsuleOpened( @Parameter(in = ParameterIn.PATH, description = "캡슐 아이디", required = true) Long capsuleId ); - - @Operation( - summary = "비밀 캡슐 생성", - description = "사용자만 볼 수 있는 비밀 캡슐을 생성한다.", - security = {@SecurityRequirement(name = "user_token")}, - tags = {"secret capsule"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "202", - description = "처리 시작" - ), - @ApiResponse( - responseCode = "400", - description = "좌표변환을 할 수 없을 때 발생하는 예외, 입력좌표 확인 요망", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ), - @ApiResponse( - responseCode = "404", - description = "캡슐 스킨을 찾을 수 없을 경우 발생하는 예외", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ) - }) - ResponseEntity> createSecretCapsule( - Long memberId, - CapsuleCreateRequest request - ); - - @Operation( - summary = "공개 캡슐 생성", - description = "사용자의 친구들만 볼 수 있는 공개 캡슐을 생성한다.", - security = {@SecurityRequirement(name = "user_token")}, - tags = {"public capsule"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "202", - description = "처리 시작" - ), - @ApiResponse( - responseCode = "400", - description = "좌표변환을 할 수 없을 때 발생하는 예외, 입력좌표 확인 요망", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ), - @ApiResponse( - responseCode = "404", - description = "캡슐 스킨을 찾을 수 없을 경우 발생하는 예외", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ) - }) - ResponseEntity> createPublicCapsule( - Long memberId, - CapsuleCreateRequest request - ); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApiController.java index 3b73fb84e..c7ed509e4 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/api/CapsuleApiController.java @@ -1,6 +1,5 @@ package site.timecapsulearchive.core.domain.capsule.generic_capsule.api; -import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -8,8 +7,6 @@ import org.springframework.web.bind.annotation.GetMapping; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -17,7 +14,6 @@ import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CoordinateRangeDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.NearbyARCapsuleSummaryDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.NearbyCapsuleSummaryDto; -import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.request.CapsuleCreateRequest; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleOpenedResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.ImagesPageResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.NearbyARCapsuleResponse; @@ -109,32 +105,4 @@ public ResponseEntity> updateCapsuleOpened( ); } - @PostMapping(value = "/secret", consumes = {"application/json"}) - @Override - public ResponseEntity> createSecretCapsule( - @AuthenticationPrincipal final Long memberId, - @Valid @RequestBody final CapsuleCreateRequest request - ) { - capsuleFacade.saveCapsule(memberId, request.toDto(), CapsuleType.SECRET); - - return ResponseEntity.ok( - ApiSpec.empty( - SuccessCode.SUCCESS - ) - ); - } - - @PostMapping(value = "/public", consumes = {"application/json"}) - @Override - public ResponseEntity> createPublicCapsule( - @AuthenticationPrincipal final Long memberId, - @Valid @RequestBody final CapsuleCreateRequest request) { - capsuleFacade.saveCapsule(memberId, request.toDto(), CapsuleType.PUBLIC); - - return ResponseEntity.ok( - ApiSpec.empty( - SuccessCode.SUCCESS - ) - ); - } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/NearbyARCapsuleSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/NearbyARCapsuleSummaryDto.java index 7187bcdb7..9e8c378cc 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/NearbyARCapsuleSummaryDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/NearbyARCapsuleSummaryDto.java @@ -5,7 +5,6 @@ import org.locationtech.jts.geom.Point; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.NearbyARCapsuleSummaryResponse; -import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; public record NearbyARCapsuleSummaryDto( Long id, @@ -18,12 +17,6 @@ public record NearbyARCapsuleSummaryDto( CapsuleType capsuleType ) { - public NearbyARCapsuleSummaryDto { - if (dueDate != null) { - dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); - } - } - public NearbyARCapsuleSummaryResponse toResponse( final Function geoTransformFunction, final Function preSignUrlFunction 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 7f911bfc3..fe4777a1d 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 @@ -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; @Schema(description = "AR 캡슐 요약 정보") @Builder @@ -12,12 +13,12 @@ public record NearbyARCapsuleSummaryResponse( @Schema(description = "캡슐 아이디") Long id, - @Schema(description = "캡슐 경도 좌표") - Double longitude, - @Schema(description = "캡슐 위도 좌표") Double latitude, + @Schema(description = "캡슐 경도 좌표") + Double longitude, + @Schema(description = "생성자 닉네임") String nickname, @@ -34,4 +35,9 @@ public record NearbyARCapsuleSummaryResponse( CapsuleType capsuleType ) { + public NearbyARCapsuleSummaryResponse { + if (dueDate != null) { + dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyCapsuleSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyCapsuleSummaryResponse.java index fb5cd1e55..98e90d1f2 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyCapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyCapsuleSummaryResponse.java @@ -11,12 +11,12 @@ public record NearbyCapsuleSummaryResponse( @Schema(description = "캡슐 아이디") Long id, - @Schema(description = "캡슐 경도 좌표") - Double longitude, - @Schema(description = "캡슐 위도 좌표") Double latitude, + @Schema(description = "캡슐 경도 좌표") + Double longitude, + @Schema(description = "캡슐 타입") CapsuleType capsuleType ) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepository.java index b73ed8836..4eea36259 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepository.java @@ -1,9 +1,17 @@ package site.timecapsulearchive.core.domain.capsule.generic_capsule.repository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; +import static site.timecapsulearchive.core.domain.capsule.entity.QCapsule.capsule; +import static site.timecapsulearchive.core.domain.capsuleskin.entity.QCapsuleSkin.capsuleSkin; +import static site.timecapsulearchive.core.domain.member.entity.QMember.member; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.ComparablePath; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.springframework.stereotype.Repository; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; @@ -14,13 +22,14 @@ @RequiredArgsConstructor public class CapsuleQueryRepository { - private final EntityManager entityManager; + private final JPAQueryFactory jpaQueryFactory; /** - * 캡슐 타입에 따라 현재 위치에서 범위 내의 캡슐을 조회한다. + * AR에서 캡슐을 찾기 위해 캡슐 타입에 따라 현재 위치에서 범위 내의 사용자가 만든 캡슐을 조회한다. * * @param memberId 범위 내의 캡슙을 조회할 멤버 id - * @param mbr 캡슈을 조회할 범위 + * @param mbr 캡슐을 조회할 범위(최소사각형) + * @see site.timecapsulearchive.core.global.geography.GeoTransformManager * @param capsuleType 조회할 캡슐의 타입 * @return 범위 내에 조회된 캡슐들의 요약 정보들을 반환한다. */ @@ -29,100 +38,129 @@ public List findARCapsuleSummaryDtosByCurrentLocation final Polygon mbr, final CapsuleType capsuleType ) { - final TypedQuery query = generateSelectQueryOnARCapsuleSummaryDtoWith( - capsuleType); - - assignARCapsuleParameter(memberId, mbr, capsuleType, query); - - return query.getResultList(); - } - - private TypedQuery generateSelectQueryOnARCapsuleSummaryDtoWith( - final CapsuleType capsuleType - ) { - String queryString = """ - select new site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.NearbyARCapsuleSummaryDto( - c.id, - c.point, - m.nickname, - cs.imageUrl, - c.title, - c.dueDate, - c.isOpened, - c.type + return jpaQueryFactory + .select( + Projections.constructor( + NearbyARCapsuleSummaryDto.class, + capsule.id, + capsule.point, + member.nickname, + capsuleSkin.imageUrl, + capsule.title, + capsule.dueDate, + capsule.isOpened, + capsule.type + ) ) - from Capsule c - join c.member m - join c.capsuleSkin cs - where ST_Contains(:mbr, c.point) and m.id=:memberId - """; - - if (capsuleType != CapsuleType.ALL) { - queryString += " and c.type = :capsuleType"; - } - - return entityManager.createQuery(queryString, NearbyARCapsuleSummaryDto.class); + .from(capsule) + .join(capsule.capsuleSkin, capsuleSkin) + .join(capsule.member, member) + .where(ST_Contains(mbr, capsule.point).and(capsule.member.id.eq(memberId) + .and(eqCapsuleType(capsuleType))) + .and(capsule.type.eq(CapsuleType.PUBLIC))) + .fetch(); } - private void assignARCapsuleParameter( - final Long memberId, - final Polygon mbr, - final CapsuleType capsuleType, - final TypedQuery query - ) { - query.setParameter("mbr", mbr); - query.setParameter("memberId", memberId); - - if (capsuleType != CapsuleType.ALL) { - query.setParameter("capsuleType", capsuleType); + private BooleanExpression eqCapsuleType(CapsuleType capsuleType) { + if (capsuleType.equals(CapsuleType.ALL)) { + return null; } + + return capsule.type.eq(capsuleType); } + /** + * 지도에서 캡슐을 찾기 위해 캡슐 타입에 따라 현재 위치에서 범위 내의 사용자가 만든 캡슐을 조회한다. + * + * @param memberId 범위 내의 캡슙을 조회할 멤버 id + * @param mbr 캡슐을 조회할 범위(최소사각형), GeoTransformManager 참조 + * @param capsuleType 조회할 캡슐의 타입 + * @return 범위 내에 조회된 캡슐들의 요약 정보들을 반환한다. + */ public List findCapsuleSummaryDtosByCurrentLocationAndCapsuleType( final Long memberId, final Polygon mbr, final CapsuleType capsuleType ) { - final TypedQuery query = generateSelectQueryOnCapsuleSummaryDtoWith( - capsuleType); - - assignCapsuleParameter(memberId, mbr, capsuleType, query); - - return query.getResultList(); + return jpaQueryFactory + .select( + Projections.constructor( + NearbyCapsuleSummaryDto.class, + capsule.id, + capsule.point, + capsule.type + ) + ) + .from(capsule) + .join(capsule.capsuleSkin, capsuleSkin) + .join(capsule.member, member) + .where(ST_Contains(mbr, capsule.point).and(capsule.member.id.eq(memberId)) + .and(eqCapsuleType(capsuleType))) + .fetch(); } - private TypedQuery generateSelectQueryOnCapsuleSummaryDtoWith( - final CapsuleType capsuleType + /** + * 지도에서 사용자의 친구들의 캡슐을 찾기 위해 현재 위치에서 범위 내의 사용자의 친구가 만든 캡슐을 조회한다. + * + * @param friendIds 범위 내의 조회할 사용자 목록 + * @param mbr 캡슐을 조회할 범위(최소사각형), GeoTransformManager 참조 + * @return 범위 내에 조회된 캡슐들의 요약 정보들을 반환한다. + */ + public List findFriendsCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + List friendIds, + Polygon mbr ) { - String queryString = """ - select new site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.NearbyCapsuleSummaryDto( - c.id, - c.point, - c.type + return jpaQueryFactory + .select( + Projections.constructor( + NearbyCapsuleSummaryDto.class, + capsule.id, + capsule.point, + capsule.type + ) ) - from Capsule c - join c.member m - where ST_Contains(:mbr, c.point) and m.id=:memberId - """; - - if (capsuleType != CapsuleType.ALL) { - queryString += " and c.type = :capsuleType"; - } + .from(capsule) + .join(capsule.capsuleSkin, capsuleSkin) + .join(capsule.member, member) + .where(ST_Contains(mbr, capsule.point).and(capsule.member.id.in(friendIds)) + .and(capsule.type.eq(CapsuleType.PUBLIC))) + .fetch(); + } - return entityManager.createQuery(queryString, NearbyCapsuleSummaryDto.class); + private BooleanExpression ST_Contains(Polygon mbr, ComparablePath point) { + return Expressions.booleanTemplate("ST_Contains({0}, {1})", mbr, point); } - private void assignCapsuleParameter( - final Long memberId, - final Polygon mbr, - final CapsuleType capsuleType, - final TypedQuery query + /** + * AR에서 사용자의 친구들의 캡슐을 찾기 위해 현재 위치에서 범위 내의 사용자의 친구가 만든 캡슐을 조회한다. + * + * @param friendIds 범위 내의 조회할 사용자 목록 + * @param mbr 캡슐을 조회할 범위(최소사각형), GeoTransformManager 참조 + * @return 범위 내에 조회된 캡슐들의 요약 정보들을 반환한다. + */ + public List findFriendsARCapsulesByCurrentLocation( + List friendIds, + Polygon mbr ) { - query.setParameter("mbr", mbr); - query.setParameter("memberId", memberId); - - if (capsuleType != CapsuleType.ALL) { - query.setParameter("capsuleType", capsuleType); - } + return jpaQueryFactory + .select( + Projections.constructor( + NearbyARCapsuleSummaryDto.class, + capsule.id, + capsule.point, + member.nickname, + capsuleSkin.imageUrl, + capsule.title, + capsule.dueDate, + capsule.isOpened, + capsule.type + ) + ) + .from(capsule) + .join(capsule.capsuleSkin, capsuleSkin) + .join(capsule.member, member) + .where(ST_Contains(mbr, capsule.point).and(capsule.member.id.in(friendIds)) + .and(capsule.type.eq(CapsuleType.PUBLIC))) + .fetch(); } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java index b29542875..f48937506 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java @@ -16,6 +16,7 @@ import site.timecapsulearchive.core.domain.capsule.generic_capsule.repository.CapsuleQueryRepository; import site.timecapsulearchive.core.domain.capsule.generic_capsule.repository.CapsuleRepository; import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; +import site.timecapsulearchive.core.domain.friend.repository.MemberFriendQueryRepository; import site.timecapsulearchive.core.domain.member.entity.Member; import site.timecapsulearchive.core.global.geography.GeoTransformManager; @@ -26,6 +27,7 @@ public class CapsuleService { private final CapsuleQueryRepository capsuleQueryRepository; private final CapsuleRepository capsuleRepository; + private final MemberFriendQueryRepository memberFriendQueryRepository; private final GeoTransformManager geoTransformManager; /** @@ -90,4 +92,51 @@ public Capsule saveCapsule( return capsule; } + + /** + * 지도에서 캡슐을 찾기 위해 사용자의 현재 위치에서 특정 반경에서 친구들의 캡슐들을 요약 조회한다 + * @param memberId 사용자 아이디 + * @param dto 현재 위치와 반경 + * @return 지도용 캡슐 요약 조회들 + */ + @Transactional(readOnly = true) + public List findFriendsCapsulesByCurrentLocation( + Long memberId, + CoordinateRangeDto dto + ) { + final Point point = geoTransformManager.changePoint4326To3857(dto.latitude(), + dto.longitude()); + + final Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, dto.distance()); + + final List friendIds = memberFriendQueryRepository.findFriendIdsByOwnerId(memberId); + + return capsuleQueryRepository.findFriendsCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + friendIds, + mbr + ); + } + + /** + * AR로 캡슐을 찾기 위해 사용자의 현재 위치에서 특정 반경에서 친구들의 캡슐들을 요약 조회한다 + * @param memberId 사용자 아이디 + * @param dto 현재 위치와 반경 + * @return AR용 캡슐 요약 조회들 + */ + public List findFriendsARCapsulesByCurrentLocation( + Long memberId, + CoordinateRangeDto dto + ) { + final Point point = geoTransformManager.changePoint4326To3857(dto.latitude(), + dto.longitude()); + + final Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, dto.distance()); + + final List friendIds = memberFriendQueryRepository.findFriendIdsByOwnerId(memberId); + + return capsuleQueryRepository.findFriendsARCapsulesByCurrentLocation( + friendIds, + mbr + ); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApi.java index fbc5d9618..38a4fa382 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApi.java @@ -10,7 +10,10 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import java.time.ZonedDateTime; +import org.hibernate.validator.constraints.Range; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; @@ -21,10 +24,12 @@ import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsuleDetailResponse; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsulePageResponse; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsuleSummaryResponse; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.MyGroupCapsuleSliceResponse; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.error.ErrorResponse; +@Validated public interface GroupCapsuleApi { @Operation( @@ -57,7 +62,7 @@ ResponseEntity> createGroupCapsule( @Parameter(in = ParameterIn.PATH, description = "생성할 그룹 아이디", required = true) Long groupId, - GroupCapsuleCreateRequest request + @Valid GroupCapsuleCreateRequest request ); @Operation( @@ -125,6 +130,29 @@ ResponseEntity getGroupCapsules( @NotNull @Valid @RequestParam(value = "capsule_id") Long capsuleId ); + @Operation( + summary = "사용자가 만든 그룹 캡슐 목록 조회", + description = "사용자가 만든 그룹 캡슐 목록을 조회한다.", + security = {@SecurityRequirement(name = "user_token")}, + tags = {"group capsule"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "처리 완료" + ) + }) + ResponseEntity> getMyGroupCapsules( + Long memberId, + + @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true, schema = @Schema()) + @Range(min = 0, max = 50) + int size, + + @Parameter(in = ParameterIn.QUERY, description = "마지막 캡슐 생성 시간", required = true, schema = @Schema()) + ZonedDateTime createAt + ); + @Operation( summary = "그룹 캡슐 24시간 이내 수정", description = "사용자가 생성한 그룹 캡슐의 생성 시간이 24시간 이내라면 수정한다.", diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApiController.java index a5d6e7f9f..fa7270414 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/api/GroupCapsuleApiController.java @@ -1,7 +1,9 @@ package site.timecapsulearchive.core.domain.capsule.group_capsule.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; @@ -9,13 +11,16 @@ 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.capsule.group_capsule.data.dto.GroupCapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.MyGroupCapsuleDto; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.reqeust.GroupCapsuleCreateRequest; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.reqeust.GroupCapsuleUpdateRequest; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsuleDetailResponse; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsulePageResponse; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.MyGroupCapsuleSliceResponse; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsuleSummaryResponse; import site.timecapsulearchive.core.domain.capsule.group_capsule.facade.GroupCapsuleFacade; import site.timecapsulearchive.core.domain.capsule.group_capsule.service.GroupCapsuleService; @@ -25,7 +30,7 @@ import site.timecapsulearchive.core.infra.s3.manager.S3PreSignedUrlManager; @RestController -@RequestMapping("/group") +@RequestMapping("/groups-capsules") @RequiredArgsConstructor public class GroupCapsuleApiController implements GroupCapsuleApi { @@ -35,7 +40,7 @@ public class GroupCapsuleApiController implements GroupCapsuleApi { private final GeoTransformManager geoTransformManager; @PostMapping( - value = "/{group_id}/capsules", + value = "/{group_id}", consumes = {"application/json"} ) @Override @@ -53,7 +58,7 @@ public ResponseEntity> createGroupCapsule( } @GetMapping( - value = "/capsules/{capsule_id}/detail", + value = "/{capsule_id}/detail", produces = {"application/json"} ) @Override @@ -61,7 +66,7 @@ public ResponseEntity> getGroupCapsuleDetail @AuthenticationPrincipal Long memberId, @PathVariable("capsule_id") Long capsuleId ) { - final GroupCapsuleDetailDto detailDto = groupCapsuleService.findGroupCapsuleDetailByGroupIDAndCapsuleId( + final GroupCapsuleDetailDto detailDto = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( capsuleId); return ResponseEntity.ok( @@ -78,7 +83,7 @@ public ResponseEntity> getGroupCapsuleDetail } @GetMapping( - value = "/capsules/{capsule_id}/summary", + value = "/{capsule_id}/summary", produces = {"application/json"} ) @Override @@ -107,6 +112,31 @@ public ResponseEntity getGroupCapsules(Long groupId, L return null; } + @GetMapping(value = "/my", produces = {"application/json"}) + @Override + public ResponseEntity> getMyGroupCapsules( + @AuthenticationPrincipal final Long memberId, + @RequestParam(defaultValue = "20", value = "size") final int size, + @RequestParam(value = "created_at") final ZonedDateTime createdAt + ) { + final Slice groupCapsules = groupCapsuleService.findMyGroupCapsuleSlice( + memberId, + size, + createdAt + ); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + MyGroupCapsuleSliceResponse.createOf( + groupCapsules.getContent(), + groupCapsules.hasNext(), + s3PreSignedUrlManager::getS3PreSignedUrlForGet + ) + ) + ); + } + @Override public ResponseEntity updateGroupCapsuleByIdAndGroupId( Long groupId, Long capsuleId, GroupCapsuleUpdateRequest request) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/MyGroupCapsuleDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/MyGroupCapsuleDto.java new file mode 100644 index 000000000..e2adb4825 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/MyGroupCapsuleDto.java @@ -0,0 +1,30 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto; + +import java.time.ZonedDateTime; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.MyGroupCapsuleResponse; + +public record MyGroupCapsuleDto( + Long capsuleId, + String skinUrl, + ZonedDateTime dueDate, + ZonedDateTime createdAt, + String title, + Boolean isOpened, + CapsuleType capsuleType +) { + + public MyGroupCapsuleResponse toResponse( + final Function singlePreSignUrlFunction) { + return MyGroupCapsuleResponse.builder() + .capsuleId(capsuleId) + .skinUrl(singlePreSignUrlFunction.apply(skinUrl)) + .dueDate(dueDate) + .createdAt(createdAt) + .title(title) + .isOpened(isOpened) + .capsuleType(capsuleType) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/MyGroupCapsuleResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/MyGroupCapsuleResponse.java new file mode 100644 index 000000000..256d80354 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/MyGroupCapsuleResponse.java @@ -0,0 +1,44 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +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 = "사용자가 만든 그룹 캡슐") +public record MyGroupCapsuleResponse( + + @Schema(description = "캡슐 아이디") + Long capsuleId, + + @Schema(description = "스킨 url") + String skinUrl, + + @Schema(description = "개봉일") + ZonedDateTime dueDate, + + @Schema(description = "생성일") + ZonedDateTime createdAt, + + @Schema(description = "제목") + String title, + + @Schema(description = "캡슐 개봉 여부") + Boolean isOpened, + + @Schema(description = "캡슐 타입") + CapsuleType capsuleType +) { + + public MyGroupCapsuleResponse { + 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/capsule/group_capsule/data/response/MyGroupCapsuleSliceResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/MyGroupCapsuleSliceResponse.java new file mode 100644 index 000000000..dba16b276 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/MyGroupCapsuleSliceResponse.java @@ -0,0 +1,23 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.data.response; + +import java.util.List; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.MyGroupCapsuleDto; + +public record MyGroupCapsuleSliceResponse( + List groupCapsules, + Boolean hasNext +) { + + public static MyGroupCapsuleSliceResponse createOf( + final List groupCapsules, + final boolean hasNext, + final Function singlePreSignUrlFunction + ) { + List groupCapsuleResponses = groupCapsules.stream() + .map(capsule -> capsule.toResponse(singlePreSignUrlFunction)) + .toList(); + + return new MyGroupCapsuleSliceResponse(groupCapsuleResponses, hasNext); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java index c8525337f..a020ef026 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java @@ -11,15 +11,20 @@ import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.StringExpression; 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.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.MyGroupCapsuleDto; import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberSummaryDto; @Repository @@ -74,22 +79,6 @@ private StringExpression groupConcatDistinct(final StringExpression expression) return Expressions.stringTemplate("GROUP_CONCAT(DISTINCT {0})", expression); } - private List getGroupMemberSummaryDtos(Long capsuleId) { - return jpaQueryFactory - .select( - Projections.constructor( - GroupMemberSummaryDto.class, - member.nickname, - member.profileUrl, - groupCapsuleOpen.isOpened - ) - ) - .from(groupCapsuleOpen) - .join(member).on(member.id.eq(groupCapsuleOpen.member.id)) - .where(groupCapsuleOpen.capsule.id.eq(capsuleId)) - .fetch(); - } - public Optional findGroupCapsuleSummaryDtoByCapsuleId( final Long capsuleId) { final CapsuleSummaryDto capsuleSummaryDto = jpaQueryFactory @@ -124,4 +113,56 @@ public Optional findGroupCapsuleSummaryDtoByCapsuleId( return Optional.of(new GroupCapsuleSummaryDto(capsuleSummaryDto, memberSummaryDtos)); } + + private List getGroupMemberSummaryDtos(Long capsuleId) { + return jpaQueryFactory + .select( + Projections.constructor( + GroupMemberSummaryDto.class, + member.nickname, + member.profileUrl, + groupCapsuleOpen.isOpened + ) + ) + .from(groupCapsuleOpen) + .join(member).on(member.id.eq(groupCapsuleOpen.member.id)) + .where(groupCapsuleOpen.capsule.id.eq(capsuleId)) + .fetch(); + } + + public Slice findMyGroupCapsuleSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + final List groupCapsules = jpaQueryFactory + .select( + Projections.constructor( + MyGroupCapsuleDto.class, + capsule.id, + capsuleSkin.imageUrl, + capsule.dueDate, + capsule.createdAt, + capsule.title, + capsule.isOpened, + capsule.type + ) + ) + .from(capsule) + .join(member).on(capsule.member.id.eq(member.id)) + .join(capsuleSkin).on(capsule.capsuleSkin.id.eq(capsuleSkin.id)) + .where(capsule.member.id.eq(memberId) + .and(capsule.createdAt.lt(createdAt)) + .and(capsule.type.eq(CapsuleType.GROUP))) + .orderBy(capsule.createdAt.desc()) + .limit(size + 1) + .fetch(); + + final boolean hasNext = groupCapsules.size() > size; + if (hasNext) { + groupCapsules.remove(size); + } + + return new SliceImpl<>(groupCapsules, Pageable.ofSize(size), hasNext); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleService.java index a21db1a34..fa264165a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleService.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import site.timecapsulearchive.core.domain.capsule.entity.Capsule; @@ -13,6 +14,7 @@ import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleCreateRequestDto; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.MyGroupCapsuleDto; import site.timecapsulearchive.core.domain.capsule.group_capsule.repository.GroupCapsuleQueryRepository; import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; import site.timecapsulearchive.core.domain.group.entity.Group; @@ -42,7 +44,7 @@ public Capsule saveGroupCapsule( return capsule; } - public GroupCapsuleDetailDto findGroupCapsuleDetailByGroupIDAndCapsuleId( + public GroupCapsuleDetailDto findGroupCapsuleDetailByGroupIdAndCapsuleId( final Long capsuleId ) { final GroupCapsuleDetailDto detailDto = groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId( @@ -70,5 +72,21 @@ public GroupCapsuleSummaryDto findGroupCapsuleSummaryByGroupIDAndCapsuleId( return groupCapsuleQueryRepository.findGroupCapsuleSummaryDtoByCapsuleId(capsuleId) .orElseThrow(CapsuleNotFondException::new); } + + /** + * 사용자가 만든 모든 그룹 캡슐을 조회한다. + * + * @param memberId 조회할 사용자 아이디 + * @param size 조회할 캡슐 크기 + * @param createdAt 조회를 시작할 캡슐의 생성 시간, 첫 조회라면 현재 시간, 이후 조회라면 맨 마지막 데이터의 시간 + * @return 사용자가 생성한 그룹 캡슐 목록 + */ + public Slice findMyGroupCapsuleSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return groupCapsuleQueryRepository.findMyGroupCapsuleSlice(memberId, size, createdAt); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApi.java index 61c4a7921..8cc625267 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApi.java @@ -8,20 +8,53 @@ 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 java.time.ZonedDateTime; +import org.hibernate.validator.constraints.Range; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.request.CapsuleCreateRequest; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleDetailResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleSummaryResponse; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.MyPublicCapsuleSliceResponse; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.reqeust.PublicCapsuleUpdateRequest; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.response.PublicCapsuleSliceResponse; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.error.ErrorResponse; +@Validated public interface PublicCapsuleApi { + @Operation( + summary = "공개 캡슐 생성", + description = "사용자의 친구들만 볼 수 있는 공개 캡슐을 생성한다.", + security = {@SecurityRequirement(name = "user_token")}, + tags = {"public capsule"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "202", + description = "처리 시작" + ), + @ApiResponse( + responseCode = "400", + description = "좌표변환을 할 수 없을 때 발생하는 예외, 입력좌표 확인 요망", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "캡슐 스킨을 찾을 수 없을 경우 발생하는 예외", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + ResponseEntity> createPublicCapsule( + Long memberId, + @Valid CapsuleCreateRequest request + ); + @Operation( summary = "공개 캡슐 요약 조회", description = "사용자의 친구들만 볼 수 있는 공개 캡슐 내용을 요약 조회한다.", @@ -102,6 +135,29 @@ ResponseEntity> getPublicCapsules( ZonedDateTime createAt ); + @Operation( + summary = "사용자가 만든 공개 캡슐 목록 조회", + description = "사용자가 만든 공개 캡슐 목록을 조회한다.", + security = {@SecurityRequirement(name = "user_token")}, + tags = {"public capsule"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "ok" + ) + }) + ResponseEntity> getMyPublicCapsules( + Long memberId, + + @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true, schema = @Schema()) + @Range(min = 0, max = 50) + int size, + + @Parameter(in = ParameterIn.QUERY, description = "마지막 캡슐 생성 시간", required = true, schema = @Schema()) + ZonedDateTime createAt + ); + @Operation( summary = "공개 캡슐 24시간 이내 수정", description = "사용자가 생성한 공개 캡슐의 생성 시간이 24시간 이내라면 수정한다.", diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApiController.java index 33e0516f9..7b3e7665e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/api/PublicCapsuleApiController.java @@ -1,6 +1,7 @@ package site.timecapsulearchive.core.domain.capsule.public_capsule.api; +import jakarta.validation.Valid; import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Slice; @@ -8,13 +9,20 @@ 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.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.request.CapsuleCreateRequest; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleDetailResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleSummaryResponse; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.facade.CapsuleFacade; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.MyPublicCapsuleDto; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.MyPublicCapsuleSliceResponse; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.PublicCapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.reqeust.PublicCapsuleUpdateRequest; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.response.PublicCapsuleSliceResponse; @@ -25,16 +33,32 @@ import site.timecapsulearchive.core.infra.s3.manager.S3PreSignedUrlManager; @RestController -@RequestMapping("/public") +@RequestMapping("/public-capsules") @RequiredArgsConstructor public class PublicCapsuleApiController implements PublicCapsuleApi { private final PublicCapsuleService publicCapsuleService; + private final CapsuleFacade capsuleFacade; private final S3PreSignedUrlManager s3PreSignedUrlManager; private final GeoTransformManager geoTransformManager; + + @PostMapping(consumes = {"application/json"}) + @Override + public ResponseEntity> createPublicCapsule( + @AuthenticationPrincipal final Long memberId, + @Valid @RequestBody final CapsuleCreateRequest request) { + capsuleFacade.saveCapsule(memberId, request.toDto(), CapsuleType.PUBLIC); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); + } + @GetMapping( - value = "/capsules/{capsule_id}/summary", + value = "/{capsule_id}/summary", produces = {"application/json"} ) @Override @@ -58,7 +82,7 @@ public ResponseEntity> getPublicCapsuleSummaryBy } @GetMapping( - value = "/capsules/{capsule_id}/detail", + value = "/{capsule_id}/detail", produces = {"application/json"} ) @Override @@ -82,12 +106,12 @@ public ResponseEntity> getPublicCapsuleDetailById ); } - @GetMapping(value = "/capsules", produces = {"application/json"}) + @GetMapping(produces = {"application/json"}) @Override public ResponseEntity> getPublicCapsules( @AuthenticationPrincipal final Long memberId, @RequestParam(defaultValue = "20", value = "size") final int size, - @RequestParam(defaultValue = "0", value = "created_at") final ZonedDateTime createdAt + @RequestParam(value = "created_at") final ZonedDateTime createdAt ) { final Slice publicCapsuleSlice = publicCapsuleService.findPublicCapsulesMadeByFriend( memberId, size, createdAt); @@ -106,6 +130,28 @@ public ResponseEntity> getPublicCapsules( ); } + @GetMapping(value = "/my", produces = {"application/json"}) + @Override + public ResponseEntity> getMyPublicCapsules( + @AuthenticationPrincipal final Long memberId, + @RequestParam(defaultValue = "20", value = "size") final int size, + @RequestParam(value = "created_at") final ZonedDateTime createdAt + ) { + final Slice publicCapsules = publicCapsuleService.findMyPublicCapsuleSlice( + memberId, size, createdAt); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + MyPublicCapsuleSliceResponse.createOf( + publicCapsules.getContent(), + publicCapsules.hasNext(), + s3PreSignedUrlManager::getS3PreSignedUrlForGet + ) + ) + ); + } + @Override public ResponseEntity updatePublicCapsuleById(Long capsuleId, PublicCapsuleUpdateRequest request) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/MyPublicCapsuleDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/MyPublicCapsuleDto.java new file mode 100644 index 000000000..8fe22bbc9 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/MyPublicCapsuleDto.java @@ -0,0 +1,30 @@ +package site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto; + +import java.time.ZonedDateTime; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.response.MyPublicCapsuleResponse; + +public record MyPublicCapsuleDto( + Long capsuleId, + String skinUrl, + ZonedDateTime dueDate, + ZonedDateTime createdAt, + String title, + Boolean isOpened, + CapsuleType capsuleType +) { + + public MyPublicCapsuleResponse toResponse( + final Function singlePreSignUrlFunction) { + return MyPublicCapsuleResponse.builder() + .capsuleId(capsuleId) + .skinUrl(singlePreSignUrlFunction.apply(skinUrl)) + .dueDate(dueDate) + .createdAt(createdAt) + .title(title) + .isOpened(isOpened) + .capsuleType(capsuleType) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/MyPublicCapsuleSliceResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/MyPublicCapsuleSliceResponse.java new file mode 100644 index 000000000..a74bd4939 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/MyPublicCapsuleSliceResponse.java @@ -0,0 +1,29 @@ +package site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.response.MyPublicCapsuleResponse; + +@Schema(description = "사용자가 만든 공개 캡슐 슬라이싱") +public record MyPublicCapsuleSliceResponse( + + @Schema(description = "사용자가 만든 공개 캡슐 정보") + List publicCapsules, + + @Schema(description = "다음 페이지 유무") + Boolean hasNext +) { + + public static MyPublicCapsuleSliceResponse createOf( + final List publicCapsules, + final boolean hasNext, + final Function singlePreSignUrlFunction + ) { + List publicCapsuleResponses = publicCapsules.stream() + .map(capsule -> capsule.toResponse(singlePreSignUrlFunction)) + .toList(); + + return new MyPublicCapsuleSliceResponse(publicCapsuleResponses, hasNext); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/MyPublicCapsuleResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/MyPublicCapsuleResponse.java new file mode 100644 index 000000000..3dea37611 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/MyPublicCapsuleResponse.java @@ -0,0 +1,44 @@ +package site.timecapsulearchive.core.domain.capsule.public_capsule.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +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 = "사용자가 만든 공개 캡슐") +public record MyPublicCapsuleResponse( + + @Schema(description = "캡슐 아이디") + Long capsuleId, + + @Schema(description = "스킨 url") + String skinUrl, + + @Schema(description = "개봉일") + ZonedDateTime dueDate, + + @Schema(description = "생성일") + ZonedDateTime createdAt, + + @Schema(description = "제목") + String title, + + @Schema(description = "캡슐 개봉 여부") + Boolean isOpened, + + @Schema(description = "캡슐 타입") + CapsuleType capsuleType +) { + + public MyPublicCapsuleResponse { + 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/capsule/public_capsule/repository/PublicCapsuleQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/repository/PublicCapsuleQueryRepository.java index cb0153a7f..980a5caac 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/repository/PublicCapsuleQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/repository/PublicCapsuleQueryRepository.java @@ -22,6 +22,7 @@ import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.MyPublicCapsuleDto; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.PublicCapsuleDetailDto; @Repository @@ -169,4 +170,40 @@ public Slice findPublicCapsulesDtoMadeByFriend( private boolean canMoreRead(final int size, final int capsuleSize) { return capsuleSize > size; } + + public Slice findMyPublicCapsuleSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + final List publicCapsules = jpaQueryFactory + .select( + Projections.constructor( + MyPublicCapsuleDto.class, + capsule.id, + capsuleSkin.imageUrl, + capsule.dueDate, + capsule.createdAt, + capsule.title, + capsule.isOpened, + capsule.type + ) + ) + .from(capsule) + .join(member).on(capsule.member.id.eq(member.id)) + .join(capsuleSkin).on(capsule.capsuleSkin.id.eq(capsuleSkin.id)) + .where(capsule.member.id.eq(memberId) + .and(capsule.createdAt.lt(createdAt)) + .and(capsule.type.eq(CapsuleType.PUBLIC))) + .orderBy(capsule.createdAt.desc()) + .limit(size + 1) + .fetch(); + + final boolean hasNext = publicCapsules.size() > size; + if (hasNext) { + publicCapsules.remove(size); + } + + return new SliceImpl<>(publicCapsules, Pageable.ofSize(size), hasNext); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/service/PublicCapsuleService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/service/PublicCapsuleService.java index 45229096a..a127f9916 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/service/PublicCapsuleService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/service/PublicCapsuleService.java @@ -9,6 +9,7 @@ import site.timecapsulearchive.core.domain.capsule.exception.CapsuleNotFondException; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.MyPublicCapsuleDto; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.PublicCapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.public_capsule.repository.PublicCapsuleQueryRepository; @@ -60,4 +61,19 @@ public Slice findPublicCapsulesMadeByFriend( return publicCapsuleQueryRepository.findPublicCapsulesDtoMadeByFriend(memberId, size, createdAt); } + + /** + * 사용자가 생성한 공개 캡슐 목록을 반환한다 + * @param memberId 조회할 사용자 + * @param size 조회할 캡슐의 개수 + * @param createdAt 조회를 시작할 캡슐의 생성 시간, 첫 조회라면 현재 시간, 이후 조회라면 맨 마지막 데이터의 시간 + * @return 사용자가 생성한 공개 캡슐 목록 + */ + public Slice findMyPublicCapsuleSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return publicCapsuleQueryRepository.findMyPublicCapsuleSlice(memberId, size, createdAt); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApi.java index 6ec00bf3e..1fc3d6ecc 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApi.java @@ -8,8 +8,10 @@ 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 java.time.ZonedDateTime; import org.springframework.http.ResponseEntity; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.request.CapsuleCreateRequest; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleDetailResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleSummaryResponse; import site.timecapsulearchive.core.domain.capsule.secret_capsule.data.reqeust.SecretCapsuleUpdateRequest; @@ -19,6 +21,33 @@ public interface SecretCapsuleApi { + @Operation( + summary = "비밀 캡슐 생성", + description = "사용자만 볼 수 있는 비밀 캡슐을 생성한다.", + security = {@SecurityRequirement(name = "user_token")}, + tags = {"secret capsule"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "202", + description = "처리 시작" + ), + @ApiResponse( + responseCode = "400", + description = "좌표변환을 할 수 없을 때 발생하는 예외, 입력좌표 확인 요망", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "캡슐 스킨을 찾을 수 없을 경우 발생하는 예외", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + ResponseEntity> createSecretCapsule( + Long memberId, + @Valid CapsuleCreateRequest request + ); + @Operation( summary = "내 비밀 캡슐 목록 조회", description = "사용자가 생성한 비밀 캡슐 목록을 조회한다.", diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApiController.java index 6ad32f70d..15458cde6 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/api/SecretCapsuleApiController.java @@ -1,5 +1,6 @@ package site.timecapsulearchive.core.domain.capsule.secret_capsule.api; +import jakarta.validation.Valid; import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Slice; @@ -9,13 +10,18 @@ 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.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.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.request.CapsuleCreateRequest; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleDetailResponse; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleSummaryResponse; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.facade.CapsuleFacade; import site.timecapsulearchive.core.domain.capsule.secret_capsule.data.dto.MySecreteCapsuleDto; import site.timecapsulearchive.core.domain.capsule.secret_capsule.data.reqeust.SecretCapsuleUpdateRequest; import site.timecapsulearchive.core.domain.capsule.secret_capsule.data.response.MySecretCapsuleSliceResponse; @@ -26,15 +32,32 @@ import site.timecapsulearchive.core.infra.s3.manager.S3PreSignedUrlManager; @RestController -@RequestMapping("/secret") +@RequestMapping("/secret-capsules") @RequiredArgsConstructor public class SecretCapsuleApiController implements SecretCapsuleApi { private final SecretCapsuleService secretCapsuleService; private final S3PreSignedUrlManager s3PreSignedUrlManager; private final GeoTransformManager geoTransformManager; + private final CapsuleFacade capsuleFacade; - @GetMapping(value = "/capsules", produces = {"application/json"}) + + @PostMapping(consumes = {"application/json"}) + @Override + public ResponseEntity> createSecretCapsule( + @AuthenticationPrincipal final Long memberId, + @Valid @RequestBody final CapsuleCreateRequest request + ) { + capsuleFacade.saveCapsule(memberId, request.toDto(), CapsuleType.SECRET); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); + } + + @GetMapping(produces = {"application/json"}) @Override public ResponseEntity> getMySecretCapsules( @AuthenticationPrincipal final Long memberId, @@ -53,7 +76,7 @@ public ResponseEntity> getMySecretCapsules ); } - @GetMapping(value = "/capsules/{capsule_id}/summary", produces = {"application/json"}) + @GetMapping(value = "/{capsule_id}/summary", produces = {"application/json"}) @Override public ResponseEntity> getSecretCapsuleSummary( @AuthenticationPrincipal final Long memberId, @@ -75,7 +98,7 @@ public ResponseEntity> getSecretCapsuleSummary( ); } - @GetMapping(value = "/capsules/{capsule_id}/detail", produces = {"application/json"}) + @GetMapping(value = "/{capsule_id}/detail", produces = {"application/json"}) @Override public ResponseEntity> getSecretCapsuleDetail( @AuthenticationPrincipal final Long memberId, @@ -97,7 +120,7 @@ public ResponseEntity> getSecretCapsuleDetail( ); } - @PatchMapping(value = "/capsules/{capsule_id}", consumes = {"multipart/form-data"}) + @PatchMapping(value = "/{capsule_id}", consumes = {"multipart/form-data"}) @Override public ResponseEntity updateSecretCapsule( @AuthenticationPrincipal final Long memberId, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepository.java index c67e7063a..0f9dcba2c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepository.java @@ -151,4 +151,12 @@ public Optional findFriendsByTag( .fetchOne() ); } + + public List findFriendIdsByOwnerId(Long memberId) { + return jpaQueryFactory + .select(memberFriend.friend.id) + .from(memberFriend) + .where(memberFriend.owner.id.eq(memberId)) + .fetch(); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorResponse.java index 1356e94cf..03d0cb1ac 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorResponse.java @@ -1,8 +1,10 @@ package site.timecapsulearchive.core.global.error; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.ConstraintViolation; import java.util.Collections; import java.util.List; +import java.util.Set; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; @@ -58,18 +60,37 @@ public static ErrorResponse fromType( ); } + public static ErrorResponse ofConstraints( + ErrorCode errorCode, + Set> constraintViolations + ) { + List errors = constraintViolations.stream() + .map(e -> Error.of(e.getInvalidValue(), e.getMessage())) + .toList(); + + return new ErrorResponse( + errorCode.getCode(), + errorCode.getMessage(), + errors + ); + } + public record Error( String field, String value, String reason ) { + public static Error of(final Object value, final String reason) { + return new Error("", String.valueOf(value), reason); + } + public static Error fromType(final String parameterName, final String value) { return new Error(parameterName, value, "입력 파라미터의 타입이 올바르지 않습니다."); } public static Error fromParameter(final String parameterName) { - return new Error(parameterName, null, "필수 입력 파라미터를 포함하지 않았습니다."); + return new Error(parameterName, "", "필수 입력 파라미터를 포함하지 않았습니다."); } public static List fromBindingResult(final BindingResult bindingResult) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java index 6c5d54bde..e7ef5add0 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import static site.timecapsulearchive.core.global.error.ErrorCode.REQUEST_PARAMETER_TYPE_NOT_MATCH_ERROR; import jakarta.transaction.TransactionalException; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.ResponseEntity; @@ -144,4 +145,17 @@ protected ResponseEntity handleTransactionException(Transactional return ResponseEntity.status(errorCode.getStatus()) .body(errorResponse); } + + @ExceptionHandler(ConstraintViolationException.class) + protected ResponseEntity handleConstraintViolationException( + ConstraintViolationException e) { + log.warn(e.getMessage(), e); + + ErrorCode errorCode = INPUT_INVALID_VALUE_ERROR; + final ErrorResponse errorResponse = ErrorResponse.ofConstraints(errorCode, + e.getConstraintViolations()); + + return ResponseEntity.status(errorCode.getStatus()) + .body(errorResponse); + } } diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/GroupFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/GroupFixture.java new file mode 100644 index 000000000..200790db7 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/GroupFixture.java @@ -0,0 +1,19 @@ +package site.timecapsulearchive.core.common.fixture; + +import site.timecapsulearchive.core.domain.group.entity.Group; + +public class GroupFixture { + + /** + * 그룹 테스트 픽스처를 만든다 + * + * @return 그룹 테스트 픽스처 + */ + public static Group group() { + return Group.builder() + .groupName("test-group") + .groupDescription("test-group-description") + .groupProfileUrl("test-group-profile") + .build(); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberGroupFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberGroupFixture.java new file mode 100644 index 000000000..6c79050ae --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberGroupFixture.java @@ -0,0 +1,19 @@ +package site.timecapsulearchive.core.common.fixture; + +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.member.entity.Member; + +public class MemberGroupFixture { + + /** + * 테스트 픽스처로 그룹장으로 그룹 멤버를 생성한다 + * + * @param member 그룹장이 될 멤버 + * @param group 대상 그룹 + * @return 그룹 멤버 테스트 픽스처 + */ + public static MemberGroup memberGroup(Member member, Group group) { + return MemberGroup.createGroupOwner(member, group); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleFixture.java index 00c9846af..b691ddd19 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleFixture.java @@ -8,6 +8,7 @@ import site.timecapsulearchive.core.domain.capsule.entity.Capsule; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; +import site.timecapsulearchive.core.domain.group.entity.Group; import site.timecapsulearchive.core.domain.member.entity.Member; import site.timecapsulearchive.core.global.geography.GeoTransformManager; @@ -51,4 +52,42 @@ private static Address getTestAddress() { .zipCode("testZipCode") .build(); } + + /** + * 그룹 캡슐의 테스트 픽스처들을 생성한다 + * + * @param size 테스트 픽스처를 만들 캡슐의 개수 + * @param member 그룹 캡슐을 생성할 멤버 + * @param capsuleSkin 캡슐 스킨 + * @param group 그룹 + * @return 그룹 캡슐 테스트 픽스처 + */ + public static List groupCapsules(int size, Member member, CapsuleSkin capsuleSkin, Group group) { + return IntStream + .range(0, size) + .mapToObj(i -> groupCapsule(member, capsuleSkin, group)) + .toList(); + } + + /** + * 그룹 캡슐의 테스트 픽스처를 생성한다 + * + * @param member 그룹 캡슐을 생성할 멤버 + * @param capsuleSkin 캡슐 스킨 + * @param group 그룹 + * @return 그룹 캡슐 테스트 픽스처 + */ + public static Capsule groupCapsule(Member member, CapsuleSkin capsuleSkin, Group group) { + return Capsule.builder() + .dueDate(ZonedDateTime.now()) + .title("testTitle") + .content("testContent") + .type(CapsuleType.GROUP) + .address(getTestAddress()) + .point(geoTransformManager.changePoint4326To3857(TEST_LATITUDE, TEST_LONGITUDE)) + .member(member) + .capsuleSkin(capsuleSkin) + .group(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 74e90dc40..fff8d12ab 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 @@ -14,6 +14,14 @@ public class MemberFixture { private static final HashEncryptionManager hashEncryptionManager = UnitTestDependency.hashEncryptionManager(); + /** + * MemberdataPrefix 를 붙여서 만들어진 Member를 반환한다 + *

+ * 주의 - 하나의 테스트에서 여러 개의 멤버를 생성한다면 서로 다른 dataPrefix 필요 + * + * @param dataPrefix 생성될 Member 에 붙일 prefix + * @return dataPrefix가 붙어서 생성된 Member + */ public static Member member(int dataPrefix) { byte[] number = getPhoneBytes(dataPrefix); @@ -44,6 +52,16 @@ 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들 + */ public static List members(int start, int count) { List result = new ArrayList<>(); for (int index = start; index < start + count; index++) { diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepositoryTest.java new file mode 100644 index 000000000..d4ee5bd00 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepositoryTest.java @@ -0,0 +1,281 @@ +package site.timecapsulearchive.core.domain.capsule.generic_capsule.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.springframework.beans.factory.annotation.Autowired; +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.dependency.UnitTestDependency; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleFixture; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleSkinFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFriendFixture; +import site.timecapsulearchive.core.domain.capsule.entity.Capsule; +import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.NearbyARCapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.NearbyCapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; +import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; +import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.global.geography.GeoTransformManager; + +/** + * 캡슐 쿼리 테스트 + *

    + *
  1. 내가 만든 캡슐만 조회
  2. + *
      + *
    1. 내가 만든 캡슐들 타입별로 조회 되는지 테스트
    2. + *
    3. 내가 만든 캡슐만 조회되는지 테스트
    4. + *
    5. 조회된 캡슐이 최소 사각형 내부에 있는지 테스트
    6. + *
    + *
  3. 친구가 만든 캡슐만 조회
  4. + *
      + *
    1. 친구가 만든 캡슐만 조회되는지 테스트
    2. + *
    3. 조회된 캡슐이 최소 사각형 내부에 있는지 테스트
    4. + *
    + *
+ */ +@TestConstructor(autowireMode = AutowireMode.ALL) +class CapsuleQueryRepositoryTest extends RepositoryTest { + + private final CapsuleQueryRepository capsuleQueryRepository; + private final GeoTransformManager geoTransformManager = UnitTestDependency.geoTransformManager(); + + private Long memberId; + private List friendIds; + private List myCapsuleIds; + private List friendCapsuleIds; + private Point point; + + CapsuleQueryRepositoryTest(EntityManager entityManager) { + this.capsuleQueryRepository = new CapsuleQueryRepository( + new JPAQueryFactory(entityManager)); + } + + @BeforeEach + @Transactional + void setup(@Autowired EntityManager entityManager) { + //테스트할 사용자 + Member member = MemberFixture.member(0); + entityManager.persist(member); + memberId = member.getId(); + + //캡슐 스킨 + CapsuleSkin capsuleSkin = CapsuleSkinFixture.capsuleSkin(member); + entityManager.persist(capsuleSkin); + + //공개 캡슐 + List friendCapsules = CapsuleFixture.capsules(10, member, capsuleSkin, + CapsuleType.PUBLIC); + friendCapsules.forEach(entityManager::persist); + point = friendCapsules.get(0) + .getPoint(); + + //비밀 캡슐 + List secretCapsules = CapsuleFixture.capsules(10, member, capsuleSkin, + CapsuleType.SECRET); + secretCapsules.forEach(entityManager::persist); + + //사용자가 만든 캡슐 아이디들 + myCapsuleIds = Stream.concat(secretCapsules.stream(), friendCapsules.stream()) + .map(Capsule::getId) + .toList(); + + //친구 + List friends = MemberFixture.members(1, 20); + friends.forEach(entityManager::persist); + + //친구가 만든 캡슐 아이디들 + friendIds = friends.stream() + .map(Member::getId) + .toList(); + + //친구 관계 & 친구들의 캡슐 + friendCapsuleIds = new ArrayList<>(); + for (Member friend : friends) { + MemberFriend memberFriend = MemberFriendFixture.memberFriend(member, friend); + entityManager.persist(memberFriend); + + MemberFriend friendMember = MemberFriendFixture.memberFriend(friend, member); + entityManager.persist(friendMember); + + CapsuleSkin friendSkin = CapsuleSkinFixture.capsuleSkin(friend); + entityManager.persist(friendSkin); + + Capsule capsule = CapsuleFixture.capsule(friend, friendSkin, CapsuleType.PUBLIC); + entityManager.persist(capsule); + + friendCapsuleIds.add(capsule.getId()); + } + } + + @Test + void 지도용_비밀_타입으로_사용자가_만든_캡슐을_조회하면_해당_타입에_맞는_사용자가_만든_캡슐만_나온다() { + //given + CapsuleType capsuleType = CapsuleType.SECRET; + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + memberId, mbr, capsuleType + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch(c -> c.capsuleType().equals(capsuleType)); + assertThat(capsules).allMatch(c -> myCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } + + @Test + void AR용_비밀_타입으로_사용자가_만든_캡슐을_조회하면_해당_타입에_맞는_사용자가_만든_캡슐만_나온다() { + //given + CapsuleType capsuleType = CapsuleType.SECRET; + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findARCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + memberId, mbr, capsuleType + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch(c -> c.capsuleType().equals(capsuleType)); + assertThat(capsules).allMatch(c -> myCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } + + @Test + void 지도용_공개_타입으로_사용자가_만든_캡슐을_조회하면_해당_타입에_맞는_사용자가_만든_캡슐만_나온다() { + //given + CapsuleType capsuleType = CapsuleType.PUBLIC; + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + memberId, mbr, capsuleType + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch(c -> c.capsuleType().equals(capsuleType)); + assertThat(capsules).allMatch(c -> myCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } + + @Test + void AR용_공개_타입으로_사용자가_만든_캡슐을_조회하면_해당_타입에_맞는_사용자가_만든_캡슐만_나온다() { + //given + CapsuleType capsuleType = CapsuleType.PUBLIC; + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findARCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + memberId, mbr, capsuleType + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch(c -> c.capsuleType().equals(capsuleType)); + assertThat(capsules).allMatch(c -> myCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } + + @Test + void 지도용_전체_타입으로_사용자가_만든_캡슐을_조회하면_해당_타입에_맞는_사용자가_만든_캡슐만_나온다() { + //given + CapsuleType capsuleType = CapsuleType.ALL; + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + memberId, mbr, capsuleType + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch( + c -> c.capsuleType().equals(CapsuleType.PUBLIC) || + c.capsuleType().equals(CapsuleType.SECRET) || + c.capsuleType().equals(CapsuleType.GROUP) + ); + assertThat(capsules).allMatch(c -> myCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } + + @Test + void AR용_전체_타입으로_사용자가_만든_캡슐을_조회하면_해당_타입에_맞는_사용자가_만든_캡슐만_나온다() { + //given + CapsuleType capsuleType = CapsuleType.ALL; + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findARCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + memberId, mbr, capsuleType + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch( + c -> c.capsuleType().equals(CapsuleType.PUBLIC) || + c.capsuleType().equals(CapsuleType.SECRET) || + c.capsuleType().equals(CapsuleType.GROUP) + ); + assertThat(capsules).allMatch(c -> myCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } + + @Test + void 지도용_친구가_만든_캡슐을_조회하면_친구가_만든_캡슐만_나온다() { + //given + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findFriendsCapsuleSummaryDtosByCurrentLocationAndCapsuleType( + friendIds, mbr + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch(c -> friendCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> c.capsuleType().equals(CapsuleType.PUBLIC)); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } + + @Test + void AR용_친구가_만든_캡슐을_조회하면_친구가_만든_캡슐만_나온다() { + //given + Polygon mbr = geoTransformManager.getDistanceMBROf3857(point, 3); + + //when + List capsules = capsuleQueryRepository.findFriendsARCapsulesByCurrentLocation( + friendCapsuleIds, mbr + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).allMatch(c -> friendCapsuleIds.contains(c.id())); + assertThat(capsules).allMatch(c -> c.capsuleType().equals(CapsuleType.PUBLIC)); + assertThat(capsules).allMatch(c -> mbr.contains(c.point())); + }); + } +} \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepositoryTest.java new file mode 100644 index 000000000..de850c081 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepositoryTest.java @@ -0,0 +1,84 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.time.ZonedDateTime; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.GroupFixture; +import site.timecapsulearchive.core.common.fixture.MemberGroupFixture; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleFixture; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleSkinFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.MyGroupCapsuleDto; +import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; +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 GroupCapsuleQueryRepositoryTest extends RepositoryTest { + + private final GroupCapsuleQueryRepository groupCapsuleQueryRepository; + + private Long memberId; + + GroupCapsuleQueryRepositoryTest(EntityManager entityManager) { + this.groupCapsuleQueryRepository = new GroupCapsuleQueryRepository( + new JPAQueryFactory(entityManager)); + } + + @BeforeEach + @Transactional + void setup(@Autowired EntityManager entityManager) { + //사용자 + Member member = MemberFixture.member(0); + entityManager.persist(member); + memberId = member.getId(); + + //캡슐 스킨 + CapsuleSkin capsuleSkin = CapsuleSkinFixture.capsuleSkin(member); + entityManager.persist(capsuleSkin); + + //그룹 + Group group = GroupFixture.group(); + entityManager.persist(group); + + //그룹원 + MemberGroup memberGroup = MemberGroupFixture.memberGroup(member, group); + entityManager.persist(memberGroup); + + //그룹 캡슐 + CapsuleFixture.groupCapsules(20, member, capsuleSkin, group) + .forEach(entityManager::persist); + } + + @Test + void 사용자가_사용자가_만든_그룹_캡슐을_조회하면_사용자가_만든_그룹_캡슐만_나온다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().plusDays(1); + + //when + Slice groupCapsules = groupCapsuleQueryRepository.findMyGroupCapsuleSlice( + memberId, size, now); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(groupCapsules.hasContent()).isTrue(); + assertThat(groupCapsules).allMatch( + capsule -> capsule.capsuleType().equals(CapsuleType.GROUP)); + assertThat(groupCapsules).allMatch(capsule -> capsule.createdAt().isBefore(now)); + }); + } +} \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/PublicCapsuleQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/PublicCapsuleQueryRepositoryTest.java index 8612562d0..d88806caf 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/PublicCapsuleQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/PublicCapsuleQueryRepositoryTest.java @@ -6,6 +6,7 @@ import jakarta.persistence.EntityManager; import java.time.ZonedDateTime; import java.util.Optional; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +23,7 @@ import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.MyPublicCapsuleDto; import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.PublicCapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.public_capsule.repository.PublicCapsuleQueryRepository; import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; @@ -33,12 +35,11 @@ class PublicCapsuleQueryRepositoryTest extends RepositoryTest { private final PublicCapsuleQueryRepository publicCapsuleQueryRepository; - private Capsule friendCapsule; - private Capsule myCapsule; - private Member member; - private Member friend; - private Member notFriend; - private CapsuleSkin capsuleSkin; + private Long friendCapsuleId; + private Long myCapsuleId; + private Long memberId; + private Long friendId; + private Long notFriendId; PublicCapsuleQueryRepositoryTest(EntityManager entityManager) { this.publicCapsuleQueryRepository = new PublicCapsuleQueryRepository( @@ -48,46 +49,67 @@ class PublicCapsuleQueryRepositoryTest extends RepositoryTest { @BeforeEach @Transactional void setup(@Autowired EntityManager entityManager) { - member = MemberFixture.member(1); + //사용자 + Member member = MemberFixture.member(1); entityManager.persist(member); + memberId = member.getId(); - friend = MemberFixture.member(2); + //친구 + Member friend = MemberFixture.member(2); entityManager.persist(friend); + friendId = friend.getId(); - notFriend = MemberFixture.member(3); + //친구 아닌 사용자 + Member notFriend = MemberFixture.member(3); entityManager.persist(notFriend); + notFriendId = notFriend.getId(); + //사용자 <-> 친구 관계 MemberFriend ownerFriend = MemberFriendFixture.memberFriend(member, friend); MemberFriend friendOwner = MemberFriendFixture.memberFriend(friend, member); entityManager.persist(ownerFriend); entityManager.persist(friendOwner); - capsuleSkin = CapsuleSkinFixture.capsuleSkin(friend); - entityManager.persist(capsuleSkin); + //사용자 캡슐 스킨 & 캡슐 + CapsuleSkin myCapsuleSkin = CapsuleSkinFixture.capsuleSkin(member); + entityManager.persist(myCapsuleSkin); - myCapsule = CapsuleFixture.capsule(member, capsuleSkin, CapsuleType.PUBLIC); + Capsule myCapsule = CapsuleFixture.capsule(member, myCapsuleSkin, CapsuleType.PUBLIC); entityManager.persist(myCapsule); + myCapsuleId = myCapsule.getId(); - friendCapsule = CapsuleFixture.capsule(friend, capsuleSkin, CapsuleType.PUBLIC); - entityManager.persist(friendCapsule); + //사용자의 캡슐들 생성 + CapsuleFixture.capsules(10, member, myCapsuleSkin, CapsuleType.PUBLIC) + .forEach(entityManager::persist); + + //친구의 캡슐 스킨 & 캡슐 + CapsuleSkin friendCapsuleSkin = CapsuleSkinFixture.capsuleSkin(friend); + entityManager.persist(friendCapsuleSkin); - Capsule capsuleByNotFriend = CapsuleFixture.capsule(notFriend, capsuleSkin, + Capsule friendCapsule = CapsuleFixture.capsule(friend, friendCapsuleSkin, CapsuleType.PUBLIC); - entityManager.persist(capsuleByNotFriend); + entityManager.persist(friendCapsule); + friendCapsuleId = friendCapsule.getId(); - CapsuleFixture.capsules(10, friend, capsuleSkin, CapsuleType.PUBLIC) + //친구의 캡슐들 생성 + CapsuleFixture.capsules(10, friend, friendCapsuleSkin, CapsuleType.PUBLIC) .forEach(entityManager::persist); + + //사용자의 친구가 아닌 사용자의 캡슐 스킨 & 캡슐 + CapsuleSkin notFriendCapsuleSkin = CapsuleSkinFixture.capsuleSkin(notFriend); + entityManager.persist(notFriendCapsuleSkin); + + Capsule capsuleByNotFriend = CapsuleFixture.capsule(notFriend, notFriendCapsuleSkin, + CapsuleType.PUBLIC); + entityManager.persist(capsuleByNotFriend); } @Test void 친구가_공개_캡슐을_상세_조회하면_공개_캡슐_상세_내용을_볼_수_있다() { //given - Long friendId = friend.getId(); - Long capsuleId = friendCapsule.getId(); - //when Optional detailDto = publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( - friendId, capsuleId); + friendId, myCapsuleId); //then assertThat(detailDto.isPresent()).isTrue(); @@ -96,12 +118,9 @@ void setup(@Autowired EntityManager entityManager) { @Test void 친구가_아닌_사용자가_친구_캡슐을_상세_조회하면_친구_캡슐_상세_내용을_볼_수_없다() { //given - Long memberId = notFriend.getId(); - Long capsuleId = friendCapsule.getId(); - //when Optional detailDto = publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( - memberId, capsuleId); + notFriendId, friendCapsuleId); //then assertThat(detailDto.isEmpty()).isTrue(); @@ -110,12 +129,9 @@ void setup(@Autowired EntityManager entityManager) { @Test void 사용자가_만든_공개_캡슐을_상세_조회하면_공개_캡슐_상세_내용을_볼_수_있다() { //given - Long memberId = member.getId(); - Long capsuleId = myCapsule.getId(); - //when Optional detailDto = publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( - memberId, capsuleId); + memberId, myCapsuleId); //then assertThat(detailDto.isPresent()).isTrue(); @@ -124,12 +140,9 @@ void setup(@Autowired EntityManager entityManager) { @Test void 친구가_공개_캡슐을_요약_조회하면_공개_캡슐_요약_내용을_볼_수_있다() { //given - Long friendId = friend.getId(); - Long capsuleId = friendCapsule.getId(); - //when Optional detailDto = publicCapsuleQueryRepository.findPublicCapsuleSummaryDtosByMemberIdAndCapsuleId( - friendId, capsuleId); + friendId, friendCapsuleId); //then assertThat(detailDto.isPresent()).isTrue(); @@ -138,12 +151,9 @@ void setup(@Autowired EntityManager entityManager) { @Test void 친구가_아닌_사용자가_친구_캡슐을_요약_조회하면_친구_캡슐_요약_내용을_볼_수_없다() { //given - Long memberId = notFriend.getId(); - Long capsuleId = friendCapsule.getId(); - //when Optional detailDto = publicCapsuleQueryRepository.findPublicCapsuleSummaryDtosByMemberIdAndCapsuleId( - memberId, capsuleId); + notFriendId, friendCapsuleId); //then assertThat(detailDto.isEmpty()).isTrue(); @@ -152,22 +162,47 @@ void setup(@Autowired EntityManager entityManager) { @Test void 사용자는_나_혹은_친구가_생성한_공개_캡슐을_조회할_수_있다() { //given + int size = 3; + ZonedDateTime now = ZonedDateTime.now().plusDays(1); + //when Slice dto = publicCapsuleQueryRepository.findPublicCapsulesDtoMadeByFriend( - member.getId(), 3, ZonedDateTime.now()); + memberId, size, now); //then - assertThat(dto.getSize()).isEqualTo(3); + assertThat(dto.hasContent()).isTrue(); } @Test void 존재하지_않는_사용자는_공개_캡슐을_조회하면_빈_리스트를_반환한다() { //given + int size = 3; + ZonedDateTime now = ZonedDateTime.now().plusDays(1); + //when Slice dto = publicCapsuleQueryRepository.findPublicCapsulesDtoMadeByFriend( - 0L, 3, ZonedDateTime.now()); + 0L, size, now); //then assertThat(dto.isEmpty()).isTrue(); } + + @Test + void 사용자가_사용자가_만든_공개_캡슐을_조회하면_사용자가_만든_공개_캡슐만_나온다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().plusDays(1); + + //when + Slice publicCapsules = publicCapsuleQueryRepository.findMyPublicCapsuleSlice( + memberId, size, now); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(publicCapsules.hasContent()).isTrue(); + assertThat(publicCapsules).allMatch( + capsule -> capsule.capsuleType().equals(CapsuleType.PUBLIC)); + assertThat(publicCapsules).allMatch(capsule -> capsule.createdAt().isBefore(now)); + }); + } } \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/service/GroupCapsuleServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/service/GroupCapsuleServiceTest.java index cfa6edba2..97a0d0214 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/service/GroupCapsuleServiceTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/service/GroupCapsuleServiceTest.java @@ -39,7 +39,7 @@ public GroupCapsuleServiceTest() { ); //when - GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIDAndCapsuleId( + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( capsuleId); @@ -66,7 +66,7 @@ public GroupCapsuleServiceTest() { ); //when - GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIDAndCapsuleId( + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( capsuleId); @@ -93,7 +93,7 @@ public GroupCapsuleServiceTest() { ); //when - GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIDAndCapsuleId( + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( capsuleId); @@ -120,7 +120,7 @@ public GroupCapsuleServiceTest() { ); //when - GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIDAndCapsuleId( + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( capsuleId); @@ -145,7 +145,7 @@ public GroupCapsuleServiceTest() { ); //when - GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIDAndCapsuleId( + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( capsuleId); @@ -170,7 +170,7 @@ public GroupCapsuleServiceTest() { ); //when - GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIDAndCapsuleId( + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( capsuleId);